Skip to content

feat(rust): extract reusable terminal-UI primitives into mergify-tui#1379

Closed
jd wants to merge 5 commits intodevs/jd/worktree-rust-port/derive-native-queue-cmds-binary-hardcoded-list--74502fe8from
devs/jd/worktree-rust-port/extract-reusable-terminal-ui-primitives-mergify--bbdbce1e
Closed

feat(rust): extract reusable terminal-UI primitives into mergify-tui#1379
jd wants to merge 5 commits intodevs/jd/worktree-rust-port/derive-native-queue-cmds-binary-hardcoded-list--74502fe8from
devs/jd/worktree-rust-port/extract-reusable-terminal-ui-primitives-mergify--bbdbce1e

Conversation

@jd
Copy link
Copy Markdown
Member

@jd jd commented May 6, 2026

Reviewer feedback: the colored-tree rendering helpers in
crates/mergify-queue/src/status.rs (Theme + relative-time
formatter + box-drawing characters) are general-purpose and will
be needed by every command with structured human-readable output
(queue show, freeze list, future ports). Centralize them
in a new mergify-tui crate so each port consumes the same
primitives instead of forking its own copy.

The new crate has three modules:

  • theme: Theme struct with TTY/NO_COLOR-aware
    enable/disable. Pre-built named styles (bold, dim,
    cyan/green/red/yellow/magenta, warn)
    plus a fg(AnsiColor) helper for domain-specific palettes.
    Theme::detect is the production constructor;
    Theme::new(enabled) is for tests that need to exercise the
    styled or plain branch deterministically.

  • time: relative_time(iso, now, future) — coarse
    Ns/Nm/Nh/Nd formatter mirroring the Python
    CLI's _relative_time. Empty string on parse failure so a
    malformed timestamp doesn't abort the surrounding render.

  • tree: Unicode box-drawing constants (BRANCH,
    LAST_BRANCH, CONTINUATION, LAST_CONTINUATION) plus
    branch_chars(is_last) to pick both prefixes for a row in
    one call.

mergify-queue::status is refactored to consume from
mergify-tui: the local Theme struct and relative_time
function are deleted, the inline ├── /└── //
literals are replaced with tree::branch_chars calls,
and the queue-specific batch_status_style helper now uses
Theme::fg for its per-status color mapping.

Tests: 11 new in mergify-tui (theme on/off, relative-time
units + parse failure + future prefix, tree-character pairing).
The 6 relative_time_* tests in mergify-queue are removed
— their coverage moves with the function. Workspace count: queue
24 (was 30), tui 11.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

Depends-On: #1366

@jd
Copy link
Copy Markdown
Member Author

jd commented May 6, 2026

This pull request is part of a Mergify stack:

# Pull Request Link
1 fix(cli): surface clap's --help instead of erroring on native subcommands #1380
2 feat(rust): port queue pause and unpause to native Rust #1352
3 feat(rust): port ci git-refs and ci queue-info to native Rust #1353
4 feat(rust): port queue status to native Rust #1359
5 test: derive native queue commands from the binary, not a hardcoded list #1366
6 feat(rust): extract reusable terminal-UI primitives into mergify-tui #1379 👈

@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented May 6, 2026

Merge Protections

Your pull request matches the following merge protections and will not be merged until they are valid.

🔴 ⛓️ Depends-On Requirements

Waiting for

This rule is failing.

Requirement based on the presence of Depends-On in the body of the pull request

🔴 👀 Review Requirements

Waiting for

  • #approved-reviews-by>=2
This rule is failing.
  • any of:
    • #approved-reviews-by>=2
    • author = dependabot[bot]
    • author = mergify-ci-bot
    • author = renovate[bot]

🔴 🔎 Reviews

Waiting for

  • #review-requested = 0
This rule is failing.
  • #review-requested = 0
  • #changes-requested-reviews-by = 0
  • #review-threads-unresolved = 0

🟢 🤖 Continuous Integration

Wonderful, this rule succeeded.
  • all of:
    • check-success=ci-gate

🟢 Enforce conventional commit

Wonderful, this rule succeeded.

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert|ui)(?:\(.+\))?:

🟢 📕 PR description

Wonderful, this rule succeeded.
  • body ~= (?ms:.{48,})

@jd jd force-pushed the devs/jd/worktree-rust-port/extract-reusable-terminal-ui-primitives-mergify--bbdbce1e branch from bd4d24c to 3697e5a Compare May 7, 2026 13:40
@jd
Copy link
Copy Markdown
Member Author

jd commented May 7, 2026

Revision history

# Type Changes Reason Date
1 initial bd4d24c 2026-05-07 13:40 UTC
2 content bd4d24c → 3697e5a 2026-05-07 13:40 UTC
3 rebase 3697e5a → 9839923 2026-05-07 15:17 UTC

@jd jd temporarily deployed to func-tests-live May 7, 2026 13:40 — with GitHub Actions Inactive
@mergify mergify Bot had a problem deploying to Mergify Merge Protections May 7, 2026 13:40 Failure
@mergify mergify Bot requested a review from a team May 7, 2026 13:56
jd and others added 5 commits May 7, 2026 17:15
Two queue commands in one PR — both are idempotent one-shot API
calls that share the same auth + repository resolution. Pause
exercises the new PUT method; unpause exercises the new
DELETE-with-status-check method.

PUTs ``{"reason": "..."}`` to
``/v1/repos/<repo>/merge-queue/pause``, prints a confirmation line
with the reason and timestamp.

Safety rails match Python:

- ``--yes-i-am-sure`` skips confirmation outright.
- Interactive (TTY): prompts "Proceed? [y/N]". Anything other than
  ``y``/``yes`` aborts as a generic error.
- Non-interactive without the flag: refuses with INVALID_STATE
  (exit 7), matching Python's
  ``raise SystemExit(ExitCode.INVALID_STATE)``.

``--reason`` has a 255-char cap enforced by clap's ``value_parser``
— bad input exits 2.

DELETEs the same path. On 404 the API is telling us the queue
wasn't paused, so the command prints "Queue is not currently
paused" and exits MERGIFY_API_ERROR (matches Python). On 2xx it
prints "Queue resumed." and exits 0.

Two new methods on ``mergify_core::HttpClient``:

- ``put<B, T>(path, body) -> Result<T, CliError>`` — mirror of
  ``post``, different verb.
- ``delete_if_exists(path) -> Result<DeleteOutcome, CliError>`` —
  returns ``Deleted`` on 2xx, ``NotFound`` on 404, errors on any
  other non-success status. Lets commands like ``unpause`` give
  a friendlier 404 message without parsing error strings.

Auth resolution (token / api-url / repository) goes through the
shared ``mergify_core::auth`` module added earlier in the stack —
no per-crate copy of the resolvers in ``mergify-queue``.

5 new unit tests in the queue crate:

- ``parse_reason`` accepts short strings and rejects > 255 chars
- ``run`` pauses and prints the API-returned reason + timestamp
- ``run`` prints "Queue resumed" on 2xx
- ``run`` errors with MERGIFY_API_ERROR on 404 carrying the
  "not currently paused" message

End-to-end smoke tested three paths:
``queue pause --reason X -r owner/repo`` → exit 8 (missing token),
``queue unpause -r owner/repo`` → exit 8 (missing token),
``echo n | queue pause --reason X`` → exit 7 (non-TTY, no --sure).

The Python ``queue pause`` / ``queue unpause`` implementations and
their tests are removed in the same PR — the Rust binary now owns
both commands end-to-end. Binary: 8.4 MB → 8.5 MB. 56 Rust tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Change-Id: Idba6fa38caf403fd5f4184cda462b5f7c1eb3ebf
Adds `github_event` (GitHub Actions event payload deserialization) and
`queue_metadata` (MQ YAML fenced-block extraction) shared modules to the
mergify-ci crate, and ports two commands on top of them:

- `ci queue-info` — prints MQ batch metadata as pretty JSON; errors
  INVALID_STATE (exit 7) outside an MQ context. Appends to
  `$GITHUB_OUTPUT` with the same `ghadelimiter_<uuid>` heredoc the
  Python version uses.

- `ci git-refs` — detects base/head refs from Buildkite env, GitHub
  event payload, `refs/notes/mergify/<branch>` git notes, MQ PR body,
  or falls back to `HEAD^..HEAD`. Supports `text`/`shell`/`json`
  output formats, writes `base`/`head` to `$GITHUB_OUTPUT`, and calls
  `buildkite-agent meta-data set` when `BUILDKITE=true`.

The notes reader is injected as a trait-object callback so unit tests
can exercise the note-driven detection path without touching a real
git repository; the production path shells out via
`real_notes_reader`.

The Python implementations of `ci git-refs` and `ci queue-info` are
removed in the same PR — the Rust binary now owns both commands
end-to-end. The looks-native heuristic in `main.rs` learns the two
new subcommand names so argv errors surface as clap exit 2 instead
of silently falling through to the Python shim.

Adds `serde_yaml_ng` (YAML parser) and `uuid` (ghadelimiter) deps to
the mergify-ci crate. Binary grows ~100KB to 8.6MB.

19 new Rust tests (8 event/metadata, 3 queue-info, 12 git-refs + 2
format round-trips merged in). Full workspace: 79 tests green,
compat harness 4/4.

Change-Id: I8d3f96e6cb4eb51e6cd195951b3e622cee7efdd4
The Rust binary now serves ``mergify queue status`` natively. The
Python implementation (``mergify_cli/queue/cli.py:status`` plus the
batch/scope/topology helpers it depended on) is removed in the
same PR — the port-and-delete rule we adopted in #1322 keeps a
single live copy of every command.

``mergify queue status [-r REPO] [-t TOKEN] [-u URL] [-b BRANCH]
[--json]``:

1. Resolves repository / token / API URL via the shared
   ``mergify_queue::auth`` resolver introduced in #1352.
2. Fetches ``GET /v1/repos/<repo>/merge-queue/status``, optionally
   with ``?branch=<branch>`` (URL-encoded via
   ``url::form_urlencoded::byte_serialize``).
3. With ``--json``: pretty-prints the raw response. The schema is
   Mergify's API contract, not this CLI's, so we deserialize into
   ``serde_json::Value`` and emit verbatim — unknown fields and
   future schema additions survive the round trip.
4. Without ``--json``: deserializes into a typed ``StatusView``
   that uses ``#[serde(default)] Option<…>`` for every field the
   Mergify API has historically treated as optional/nullable
   (matches the port checklist from #1357), then renders a
   header, an optional pause indicator, the batch tree (grouped
   by scope when there is more than one), and the waiting-PR
   list. Status icons (``● ◑ ◌ ✓ ✗ ◎ ⏳ ↻ ⏰ ❄``) and relative
   times (``5m ago`` / ``~1h``) match the Python implementation.

Python used Rich's ``Tree`` for batches. The Rust port emits
flat indented text instead — same data, simpler rendering. Both
are line-oriented and round-trip cleanly through pipes; the
fancy box-drawing was visual sugar that didn't survive piping
anyway.

- ``build_path`` covers no-branch, branch, and URL-encoding of a
  branch name with slashes + spaces (e.g. ``feature/foo bar``
  becomes ``feature%2Ffoo+bar`` in the query).
- ``relative_time`` covers seconds / minutes / hours / days,
  future prefix, and graceful empty-string return on a malformed
  timestamp (matches Python's "degrade rather than fail").
- ``topological_sort`` covers parents-before-children ordering
  and tolerance of ``parent_ids`` that reference missing batches.
- ``group_by_scope`` covers the ``[]`` → ``"default"`` fallback
  and multi-scope batches appearing under each scope they claim.
- ``status_icon`` covers known + unknown codes.
- End-to-end wiremock tests: empty queue, paused queue, batches +
  waiting PRs, multi-scope grouping, ``?branch=…`` query
  threading, JSON-passthrough preserving an ``extra_field``,
  and tolerance of a response that omits all optional fields.

- ``crates/mergify-queue/Cargo.toml``: adds ``chrono`` (relative
  time math), ``indexmap`` (scope groups in insertion order),
  promotes ``serde_json`` from dev to runtime (used for the
  ``serde_json::Value`` passthrough).
- ``crates/mergify-cli/src/main.rs``: registers the ``status``
  subcommand under ``QueueSubcommand``, threads the
  ``--branch``/``--json`` flags, dispatches to
  ``mergify_queue::status::run``. Adds ``status`` to
  ``looks_native``.
- ``mergify_cli/queue/api.py``: removes ``QueueStatusResponse``,
  ``QueueBatch``, ``QueuePause``, ``QueueChecksSummary``,
  ``QueueBatchStatus``, ``QueuePullRequest``,
  ``QueuePullRequestAuthor``, and ``get_queue_status`` — all
  now Rust-native. ``QueuePullResponse`` and friends stay for
  the still-shimmed ``queue show`` (next phase).
- ``mergify_cli/queue/cli.py``: removes the ``@queue.command
  status`` block and the helpers it owned (``STATUS_STYLES``,
  ``_status_text``, ``_batch_label``, ``_pr_label``,
  ``_topological_sort``, ``_group_batches_by_scope``,
  ``_print_batches``, ``_print_waiting_prs``).
  ``_relative_time`` stays — ``show`` still uses it.
- ``mergify_cli/tests/queue/test_cli.py``: deletes
  ``TestStatusCommand``, ``TestTopologicalSort``, and the
  ``_invoke_status`` helper. ``TestRelativeTime`` stays.

Workspace: 138 Rust tests green, 590 Python tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Change-Id: I8cebcd325f05173dfa41083da2ec6516a6ec3a3f
Reviewer feedback (#1352): hardcoding the set of Rust-native queue
subcommands in ``mergify_cli/tests/queue/test_skill.py`` made the
skill-reference validation drift-prone — a port PR that forgot to
update ``NATIVE_QUEUE_COMMANDS`` would silently pass even though
the skill was referencing a command the binary couldn't handle.

Two changes make the binary the single source of truth:

1. ``crates/mergify-cli/src/main.rs`` factors the
   ``(group, subcommand)`` pairs into a top-level
   ``NATIVE_COMMANDS`` const. ``looks_native`` iterates that
   const instead of a `match` arm. A new hidden flag
   ``--list-native-commands`` (intercepted before clap or the
   shim) prints one ``<group> <subcommand>`` pair per line and
   exits ``0``.

2. ``test_skill.py`` queries the installed ``mergify`` binary via
   ``subprocess.run([…, "--list-native-commands"])`` to discover
   the native set, replacing the ``NATIVE_QUEUE_COMMANDS``
   frozenset. The test skips cleanly when the binary isn't on
   ``PATH`` (rare; ``uv run pytest`` installs it first).

The result: the next port PR adds an entry to ``NATIVE_COMMANDS``
in main.rs as part of its normal wiring, and ``test_skill.py``
picks it up automatically. No parallel list to maintain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Change-Id: I74502fe8affcc58f26eaaa9d058668eb36fec83b
Reviewer feedback: the colored-tree rendering helpers in
``crates/mergify-queue/src/status.rs`` (Theme + relative-time
formatter + box-drawing characters) are general-purpose and will
be needed by every command with structured human-readable output
(``queue show``, ``freeze list``, future ports). Centralize them
in a new ``mergify-tui`` crate so each port consumes the same
primitives instead of forking its own copy.

The new crate has three modules:

- ``theme``: ``Theme`` struct with TTY/``NO_COLOR``-aware
  enable/disable. Pre-built named styles (``bold``, ``dim``,
  ``cyan``/``green``/``red``/``yellow``/``magenta``, ``warn``)
  plus a ``fg(AnsiColor)`` helper for domain-specific palettes.
  ``Theme::detect`` is the production constructor;
  ``Theme::new(enabled)`` is for tests that need to exercise the
  styled or plain branch deterministically.

- ``time``: ``relative_time(iso, now, future)`` — coarse
  ``Ns``/``Nm``/``Nh``/``Nd`` formatter mirroring the Python
  CLI's ``_relative_time``. Empty string on parse failure so a
  malformed timestamp doesn't abort the surrounding render.

- ``tree``: Unicode box-drawing constants (``BRANCH``,
  ``LAST_BRANCH``, ``CONTINUATION``, ``LAST_CONTINUATION``) plus
  ``branch_chars(is_last)`` to pick both prefixes for a row in
  one call.

``mergify-queue::status`` is refactored to consume from
``mergify-tui``: the local ``Theme`` struct and ``relative_time``
function are deleted, the inline ``├── ``/``└── ``/``│   ``/
``    `` literals are replaced with ``tree::branch_chars`` calls,
and the queue-specific ``batch_status_style`` helper now uses
``Theme::fg`` for its per-status color mapping.

Tests: 11 new in ``mergify-tui`` (theme on/off, relative-time
units + parse failure + future prefix, tree-character pairing).
The 6 ``relative_time_*`` tests in ``mergify-queue`` are removed
— their coverage moves with the function. Workspace count: queue
24 (was 30), tui 11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Change-Id: Ibbdbce1e8272ab6d81b2dc8a194d527dd41d8744
@jd jd force-pushed the devs/jd/worktree-rust-port/derive-native-queue-cmds-binary-hardcoded-list--74502fe8 branch from 41db42a to ae15b73 Compare May 7, 2026 15:17
@jd jd force-pushed the devs/jd/worktree-rust-port/extract-reusable-terminal-ui-primitives-mergify--bbdbce1e branch from 3697e5a to 9839923 Compare May 7, 2026 15:17
@jd jd temporarily deployed to func-tests-live May 7, 2026 15:17 — with GitHub Actions Inactive
@mergify mergify Bot had a problem deploying to Mergify Merge Protections May 7, 2026 15:17 Failure
@jd jd force-pushed the devs/jd/worktree-rust-port/derive-native-queue-cmds-binary-hardcoded-list--74502fe8 branch from ae15b73 to 4a7622b Compare May 7, 2026 18:21
@jd jd closed this May 7, 2026
@jd jd deleted the devs/jd/worktree-rust-port/extract-reusable-terminal-ui-primitives-mergify--bbdbce1e branch May 7, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant