Skip to main content

PRoctr - Doctor/Triage for your open Pull Requests

Test suites REUSE status The latest version can be found on PyPI. Information on what versions of Python are supported can be found on PyPI.

proctr is a terminal UI for triaging open pull/merge requests across many GitHub, GitLab, and Gitea repositories at once. Its prime use case is managing automated dependency-update PRs (Renovate, Dependabot, or similar bots), but it works for any PR/MR matched by a label and/or branch-name prefix — including hand-written ones, if you follow a consistent convention. It reads a simple YAML project registry, fetches matching PRs/MRs concurrently, and lets you review, merge, and jump into repos without leaving the terminal.

Screenshot of proctr showing a table of matching PRs across multiple repos, with color-coded mergeable state, pipeline status, and review decision.

Columns

The table shows a few independent signals per PR/MR, color-coded where the forge can tell reliably:

  • Mergeable — whether the forge sees a merge conflict or unmet approval rule (e.g. MERGEABLE/CONFLICTING). This does not reflect CI outcome.
  • Pipeline — the CI/pipeline outcome for the PR's head commit (e.g. success/failed/running, or N/A if the repo has no CI). For GitLab and Gitea this requires a follow-up API call per PR to fetch, since the list endpoints don't expose it directly. GitHub's BLOCKED state (e.g. unmet required reviews or status checks) is treated as a failure here too.
  • Review — the PR's review decision (e.g. APPROVED/REVIEW_REQUIRED/CHANGES_REQUESTED), shown as plain None when empty. GitHub exposes this directly (reviewDecision); Gitea derives it from the PR's reviews plus the base branch's required-approval count (one extra API call each); GitLab doesn't expose an equivalent today.

A PR is only color-coded ready when Mergeable and Pipeline are both favorable; see the Gitea note below for where this can't be determined with full confidence.

Features

  • Cross-repo overview — one table showing every open PR/MR matching your configured label(s) and/or branch prefix(es) (e.g. Renovate, Dependabot, or a custom convention) across all your repos, with age, mergeable state, pipeline status, and (on GitHub) review decision color-coded at a glance (where the forge can tell reliably — see the Gitea note below).
  • Multi-forge, multi-instance — mix GitHub, GitLab, and Gitea repos in one registry, including multiple self-hosted GitLab or Gitea instances at once, each with its own credentials.
  • Multi-select merge — tick PRs with space and merge them all with m; failures don't block the rest, and you get a summary afterward.
  • Multi-select approve — tick PRs with space and approve them all with a; failures don't block the rest, and you get a summary afterward.
  • Jump to context — open a PR in your browser (o) or drop into a shell at its local checkout (s), with your GitHub token already exported into the shell environment.
  • Configurable — merge method, detection labels, sort order, and the project registry path are all overridable via a TOML config file.
  • Concurrent fetching — all repos are queried in parallel via a thread pool, so refreshing dozens of repos takes seconds, not minutes.

Requirements

  • Python 3.11+
  • One CLI per forge you actually use:
    • GitHub CLI (gh), authenticated or provided a token via config/env (see below)
    • GitLab CLI (glab), authenticated or provided via token in config/env (see below)
    • Gitea CLI (tea), must be pre-authenticated yourself via tea login add (see the Gitea note below)

Installation

Requires at least Python 3.11.

With pipx:

pipx install proctr

With uv:

uv tool install proctr

With pip:

pip install proctr

From source

git clone https://github.com/mxmehl/proctr.git
cd proctr
uv sync --no-dev

Quick start

  1. Create a project registry — a YAML file listing the repos to monitor:

    root_path: ~/Git  # optional; overrides the default local clone root. Default: ~/Git
    myprojects:
      github:
        my-tool: # Expected local path: ~/Git/github/my-tool
          forge: github
          url: https://github.com/myuser/my-tool
      personal:
        proctr:
          forge: github
          url: https://github.com/mxmehl/proctr
          path: ~/Git/others/proctr  # optional; override the default local path
        my-blog: # Expected local path: ~/Git/personal/my-blog
          forge: gitea
          url: https://myowngit.example.com/myuser/my-blog
      work:
        internal-service:
          forge: gitlab
          url: https://company.example.com/team/internal-service
    

    Repos are grouped by an arbitrary top-level key (e.g. github, work); this key also determines the local checkout path convention <root_path>/<group>/<project>, used by the "open shell" action. root_path defaults to ~/Git if omitted. forge must be github, gitlab, or gitea. If your local clones don't follow that convention, set an optional path on the project to use an explicit local path instead, e.g. path: ~/code/my-tool.

    Save this file somewhere (e.g. ~/myprojects.yaml) and point to it in your config file (see below).

    Or generate it automatically by scanning a directory for git repos instead of writing it by hand:

    proctr config myprojects --root-dir ~/Git
    

    This walks ~/Git for git repos (stopping at the first one found in each branch, so nested/vendored repos are skipped), reads each one's origin remote URL, and writes a myprojects.yaml to myprojects_path from config.toml, or next to config.toml if that's unset (override either with -o/--output; refuses to overwrite an existing file unless --force is passed). github.com and gitlab.com are recognized automatically; any other host is resolved interactively (you're asked whether it's a GitLab or Gitea instance) and remembered in config.toml, so it's never asked again. For GitLab, you're also asked to confirm the instance's HTTPS/API host (defaulting to the discovered one) — press Enter if the discovered host is already correct, or type the real one if the origin remote uses a separate SSH-only hostname (see ssh_host below); that's persisted alongside the new [gitlab."<host>"] table too. If the same remote repo is found cloned into more than one local path, only the first one encountered is kept (the rest are listed in the summary) — otherwise proctr would fetch and list that repo's PRs twice.

  2. Provide token for your forges (e.g. GitHub), in order of precedence:

    • GITHUB_TOKEN / GITLAB_TOKEN environment variable, or
    • [github].token_command ([gitea.*/gitlab.*].token_command respectively) in the config file — a command that prints the token to stdout, e.g. a password manager CLI, or
    • [github].token ([gitea.*/gitlab.*].token respectively) in the config file (plaintext), or
    • fall back to gh/glab/tea's own stored authentication.

    You can also set config.toml values from the command line instead of hand-editing the file:

    proctr config set merge_method rebase
    proctr config set gitlab."gitlab.example.com".token glpat-xxxxx
    
  3. Run it:

    proctr
    

    Or preview the UI with canned sample data (no config/credentials/repos needed), e.g. for screenshots:

    proctr --demo
    

Configuration

proctr reads an optional TOML config file at your platform's user config directory (e.g. ~/Library/Application Support/proctr/config.toml on macOS, ~/.config/proctr/config.toml on Linux):

merge_method = "squash"                # "squash" (default), "merge", or "rebase"
labels = []                            # default PR label(s) to filter on; all must match
branch_prefixes = ["renovate/", "dependabot/"]  # default branch-name prefix(es) to filter on; any one matches
match_mode = "and"                     # "and" (default) or "or" - how labels + branch_prefixes combine
sort_by = "repo"                       # "repo" (default), "age", or "title"
myprojects_path = "~/path/to/myprojects.yaml"  # defaults to a file next to this config

[github]
token = "ghp_..."                      # optional; env var GITHUB_TOKEN takes precedence
# token_command = ["pass", "github"]   # optional; takes precedence over token
# labels = ["Renovate"]                # optional; overrides the global `labels` for GitHub only
# branch_prefixes = ["renovate/"]      # optional; overrides the global `branch_prefixes` for GitHub only
# match_mode = "or"                    # optional; overrides the global `match_mode` for GitHub only

# One [gitlab."<host>"] table per self-hosted GitLab instance you use.
# <host> is the HTTPS/API host and must match the hostname in the repo's url in myprojects.yaml.
[gitlab."gitlab.example.com"]
token = "glpat-..."                    # optional; or use token_command like above
# token_command = ["pass", "gitlab"]   # optional; takes precedence over token
# ssh_host = "ssh.gitlab.example.com"  # optional; set this to whatever hostname `glab auth status`
                                       # shows your credentials stored under, if that differs
                                       # from the HTTPS/API host above (also used by
                                       # `proctr config myprojects` to recognize scanned SSH remotes)
# labels = ["dependencies"]            # optional; overrides the global `labels` for this instance only
# branch_prefixes = ["renovate/"]      # optional; overrides the global `branch_prefixes` for this instance only

# One [gitea."<host>"] table per Gitea instance you use. proctr never
# handles Gitea tokens itself — see the note below.
[gitea."gitea.example.com"]
login = "gitea.example.com"            # optional; defaults to the host itself
# labels = ["dependencies"]            # optional; overrides the global `labels` for this instance only
# branch_prefixes = ["renovate/"]      # optional; overrides the global `branch_prefixes` for this instance only

token_command (under [github] or any [gitlab."<host>"] table) is run directly via subprocess (no shell involved, so it works the same regardless of your login shell), and its stdout (trimmed) is used as the token. This avoids storing a plaintext token in the config file. If it fails, proctr falls back to the plaintext token (or, for GitHub, gh's own auth) and shows a warning on startup.

Filtering PRs: labels and branch prefixes

proctr doesn't care which bot (or human) opened a PR — it matches purely on labels and/or branch_prefixes, so it works equally well for Renovate, Dependabot, another bot, or a hand-maintained convention, as long as it's consistent.

By default, proctr matches PRs by branch prefix renovate/ or dependabot/ (Renovate's and Dependabot's own default branch-naming conventions), not by label — this works out of the box without any config for the most common case, and covers both bots for free since branch-prefix matching is always done client-side (checking one extra prefix costs nothing extra). Add further prefixes (e.g. for another bot or your own convention) the same way.

Bots don't always use the same label across every forge — for example, GitHub/GitLab repos might use Renovate while Gitea repos use dependencies. Set labels at the top level for the default used everywhere, and override it per forge ([github]) or per instance ([gitlab."<host>"], [gitea."<host>"]) wherever it differs. Multiple labels are always matched with AND semantics — a PR must carry every configured label, not just one.

You can additionally (or instead) filter by branch name prefix via branch_prefixes, e.g. ["renovate/"] or ["dependabot/"]. Multiple prefixes are matched with OR semantics — a PR matches if its branch starts with any one of them. branch_prefixes follows the same global/per-forge/per-instance override hierarchy as labels.

labels may be set to [] to disable label filtering entirely at a given level (e.g. labels = [] with branch_prefixes = ["renovate/"] set at the same level matches by branch prefix alone). As a convenience, setting branch_prefixes at a level without also setting labels there implies labels = [] at that level — you don't need to explicitly disable labels when you only care about the branch prefix. At least one of labels or branch_prefixes must be non-empty (after overrides are resolved) for each forge/instance actually in use, or proctr raises a configuration error at startup.

When both labels and branch_prefixes are configured (and non-empty) for the same forge/instance, match_mode decides how they combine:

  • "and" (default): a PR must satisfy both filters.
  • "or": a PR is included if it satisfies either filter.

match_mode = "or" has an extra cost on GitHub and GitLab: gh/glab can only pre-filter PRs by label server-side (not by branch prefix), so satisfying OR semantics correctly requires one additional, unfiltered list call per repo (to also catch PRs that match the branch prefix but not the label), whose results are merged with the label-filtered query. This only happens when match_mode = "or" and both filters are non-empty; the common cases (AND mode, or only one filter configured) stay a single call. Gitea has no server-side label filter at all, so it always does one call and matches everything client-side regardless of match_mode.

Large repos: prefer filtering by label

gh pr list, glab mr list, and tea pulls list each default to fetching only the first 30 open PRs/MRs per repo, and proctr doesn't override this. Branch-prefix matching is always applied client-side to whatever page that call returns — it can't ask the forge to filter by branch prefix server-side. So if labels is empty for a forge/instance (e.g. you rely on the default branch_prefixes = ["renovate/", "dependabot/"] alone) and a repo has more than 30 open PRs of any kind, PRs beyond the first page are silently not considered, even if their branch matches.

Configuring labels lets gh/glab pre-filter server-side instead of paging through everything, so this only matters for repos that both have >30 open PRs and rely on branch-prefix-only matching. If that applies to you, set a labels filter (in addition to, or instead of, branch_prefixes) to get complete, accurate results. Raising the page size isn't a good workaround: it just fetches more unfiltered data per call and increases the risk of API timeouts on large repos.

Gitea limitation: no token management

The tea CLI has no way to pass a token or host per invocation — it only works against named logins that are registered ahead of time. proctr does not manage Gitea credentials at all: before using a Gitea instance, register it yourself with:

tea login add --name gitea.example.com --url https://gitea.example.com --token <your-token>

The login field in [gitea."<host>"] just tells proctr which registered login name to pass to tea --login; it defaults to the host itself, which matches the convention of naming logins after their host.

Gitea's own mergeable field is documented as sometimes wrong upstream — it can report false for a PR that's actually mergeable. proctr still color-codes it, though: attempting to merge a PR that turns out to have a real conflict fails cleanly with an error rather than silently doing anything harmful, so a wrong "mergeable" signal only costs you one failed merge attempt, not a bad merge. Pipeline/CI status is fetched separately via the Gitea API's combined commit status endpoint (one follow-up call per PR) and factors into the same color, alongside mergeable.

Logs

The TUI runs on the terminal's alternate screen, so warnings and errors (e.g. a repo fetch failure, a failed merge/approve, a token_command that failed) only ever show as a toast notification that disappears after a few seconds. Every one of those is also written to a log file at your platform's user log directory (e.g. ~/Library/Logs/proctr/proctr.log on macOS, ~/.local/state/proctr/log/proctr.log on Linux), so you can review or tail -f it after the fact. Informational notifications (e.g. "Sorted by age") are toast-only and not logged.

Keybindings

Key Action
space Toggle selection of the row under the cursor
m Merge selected PRs (or the focused one if none selected)
a Approve selected PRs (or the focused one if none selected)
o Open selected PRs (or the focused one) in the browser
s Check out the focused PR's branch (force-resetting any stale branch) and open a shell there
t Cycle sort order (repo → age → title)
r Refresh the PR list
q Quit

This project is licensed under the Apache License 2.0, copyrighted by Max Mehl. As the project follows the REUSE best practices, you can find licensing information for each individual file in the LICENSES directory or corresponding file headers.

Note: In its earliest days, this project went under the name lsrenovate. It became proctr when it was generalized to work with any PR/MR, not just Renovate ones. The old name still appears in older changelog entries.

Metadata

Release files for proctr 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for proctr 2.0.0
File Size Uploaded
proctr-2.0.0.tar.gz 44.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for proctr 2.0.0
File Interpreter ABI Platform
proctr-2.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 91.0 kB

Release files / proctr-2.0.0.tar.gz

Download URL proctr-2.0.0.tar.gz
Size 44.0 kB
Tags Source
SHA-256 checksum
How to use checksums
114fca0967e648aa83d065871a29ac855f74c48e93e96293ea18728e3ca1a5bc
BLAKE2b-256 checksum
How to use checksums
40413608bf3647a4a049eb96175e91a05db7606ad079f09f246313731572f05d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release files / proctr-2.0.0-py3-none-any.whl

Download URL proctr-2.0.0-py3-none-any.whl
Size 47.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
21f0202e47567c64a3a2fac496d2f3265d4c91da1040dadc20a4c60cc70b35e3
BLAKE2b-256 checksum
How to use checksums
823d226936b32f687a5d314709b142b99f551bfe56e925de588a079cedaa1623
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 release files

1.1.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page