Skip to content

feat(rust): port stack push end-to-end + delete Python push.py#1551

Closed
jd wants to merge 10 commits into
devs/jd/feat/rust-stack-new/port-stack-hooks-stack-setup-native-rust--12f4b4affrom
devs/jd/feat/rust-stack-detect-change-type/port-stack-push-end-end-delete-python-push-py--1b2c3050
Closed

feat(rust): port stack push end-to-end + delete Python push.py#1551
jd wants to merge 10 commits into
devs/jd/feat/rust-stack-new/port-stack-hooks-stack-setup-native-rust--12f4b4affrom
devs/jd/feat/rust-stack-detect-change-type/port-stack-push-end-end-delete-python-push-py--1b2c3050

Conversation

@jd

@jd jd commented Jun 5, 2026

Copy link
Copy Markdown
Member

End-to-end native port of mergify stack push: wires every
leaf module, the orchestrator, and the binary dispatch in one
self-consistent change so mergify stack push works on the
Rust binary and the Python implementation gets deleted in the
same commit.

New mergify-stack modules (each is a faithful port from
mergify_cli/stack/):

  • change_type — patch-id-based rebase-vs-content
    classification + refs/pull/<n>/head fetch helper.
  • stack_comment — the "this PR is part of a stack" sticky
    comment renderer + header recogniser. JSON marker stays
    byte-for-byte compatible with historic comments.
  • replaygit merge-tree + git diff-tree + the HTTP
    upload (POST /git/trees + POST /git/commits) that
    materialise the rebase-aware revision-history compare URL.
  • revision_history — the "Revision history" sticky comment
    with parse + append + render round-tripping byte-for-byte
    against the Python implementation.
  • approvals — the rebase/no-rebase decision: skip the
    rebase when PRs are already approved (so reviews aren't
    dismissed) unless the bottom of the stack has a real merge
    conflict with trunk.
  • notes_pushgit fetch/git push plumbing for
    refs/notes/mergify/stack + the per-PR refspecs that
    stack push lands atomically with --force-with-lease.
  • rebase_log — pure formatters for the three rebase
    narration log lines.
  • push_helpersformat_pull_description (strip
    Change-Id + stale Depends-On, append fresh Depends-On) +
    build_change_tasks (the per-PR dependency graph).
  • pr_upsert — per-PR Create (POST /pulls) and Update
    (PATCH /pulls/) + orphan branch teardown.
  • comment_upsert — per-PR sticky-comment upserters: stack
    comment (skip-when-single-PR) + revision history (parse +
    append + recover-from-corrupt).
  • plan — the layer above classify that applies the
    --next-only / --only-update-existing-pulls overrides
    and resolves dest_branch / base_branch.
  • changes::Action extended with SkipCreate /
    SkipNextOnly so the planner can surface
    would-be-created PRs without opening any.
  • commands::push — the end-to-end orchestrator wiring
    every leaf above. Runs per-PR upserts sequentially
    (typical 2-5 PR stacks make the latency difference
    negligible vs. the GitHub round-trip cost, and the
    simpler code avoids a tokio::sync::Notify graph).

Binary wiring (crates/mergify-cli/src/main.rs):

  • NativeCommand::StackPush(StackPushOpts) variant +
    StackPushCli clap struct mirroring the Python click
    option surface 1:1 (--skip-rebase, --force-rebase,
    --next-only, --dry-run, --draft, --keep-pull- request-title-and-body, --author, --trunk,
    --branch-prefix, --only-update-existing-pulls,
    --no-revision-history, --no-verify).
  • dispatch_stack "push" arm routing through to
    commands::push::run.
  • ("stack", "push") added to NATIVE_COMMANDS.

Python deletion:

  • mergify_cli/stack/push.py (the orchestrator, 1394 LOC).
  • mergify_cli/stack/{sync,approvals,replay,changes}.py
    helpers only imported by push.py and its tests.
  • mergify_cli/tests/stack/test_{push,replay,approvals}.py.
  • mergify_cli/stack/cli.py — the push command + the
    stack_push_mod import. The click group itself stays
    so python -m mergify_cli stack keeps printing help
    until the package is removed wholesale in the follow-up.

Adds chrono (with clock feature) and tokio (with
time feature) + url to mergify-stack's prod deps to
support the revision-history timestamps, the
mergeable-retry sleep, and the wiremock test base-URL
parse.

End-to-end verified against the Python implementation on
real fixtures (stack-comment + revision-history bodies
match byte-for-byte; planner action overrides match the
Python get_changes semantics including the
--next-only × --only-update-existing-pulls interaction
at idx 0).

Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com

jd and others added 10 commits June 4, 2026 15:27
The Rust binary now serves ``mergify stack drop <COMMIT>...
[--dry-run]`` natively. The Python implementation
(``mergify_cli/stack/drop.py``, its click registration, and
``mergify_cli/tests/stack/test_drop.py``) is removed in the same
PR.

Fourth ``stack`` subcommand to land natively. Builds on the
rebase-todo machinery introduced with ``stack edit``: this slice
adds one ``Action::Drop { shas }`` variant to
``mergify_stack::rebase_todo`` (which deletes the targeted
``pick`` lines from the todo) and one new arm in the
``_internal rebase-todo-rewrite`` subcommand. No new bridge
surface — the existing
``_internal rebase-todo-rewrite --action drop --shas <SHA,…>``
fits the pre-port pattern.

``mergify stack drop <COMMIT>... [--dry-run]``:

1. Resolves the trunk and walks the stack (shared with
   ``stack edit``).
2. Matches each ``<COMMIT>`` argument against the walker by SHA
   prefix or Change-Id prefix. Duplicate prefixes (two args
   resolving to the same commit) are rejected with
   ``InvalidState``.
3. ``--dry-run`` short-circuits with a ``Drop plan:`` table and
   exits 0 without touching git.
4. Otherwise spawns ``git rebase -i <base>`` with
   ``GIT_SEQUENCE_EDITOR`` pointing at the binary's
   ``_internal rebase-todo-rewrite --action drop --shas
   <comma-separated>``; the todo rewriter removes the targeted
   ``pick`` lines and git replays the rebase.

The rewriter requires every targeted SHA to match exactly one
``pick`` line — a partial match aborts the rebase with
``InvalidState`` listing the missing SHAs, so the user never
ends up with a half-applied drop.

End-to-end coverage in
``crates/mergify-cli/tests/stack_drop.rs`` (6 cases mirroring
the deleted Python ``test_drop.py``): drop middle, drop
multiple, drop by Change-Id, ``--dry-run`` is a no-op, unknown
prefix exits non-zero, duplicate prefix rejected. The pure
transformer is covered by 5 new ``rebase_todo`` unit tests
(happy path, multi-target, no-match, partial match,
empty-target).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Change-Id: I22a924187bf504fd13515f0c8e5512e57b6dff40
The Rust binary now serves ``mergify stack fixup <COMMIT>...
[--dry-run]`` natively. The Python implementation
(``stack_fixup`` in ``mergify_cli/stack/squash.py``, its click
registration, and ``TestStackFixup`` in
``mergify_cli/tests/stack/test_squash.py``) is removed in the
same PR; ``stack_squash`` stays put.

Builds on the rebase-todo machinery: this slice adds
``Action::Fixup { shas }`` to ``mergify_stack::rebase_todo``
(rewrites the targeted ``pick`` lines to ``fixup``, preserving
the diff so it folds into the previous commit) and an ``Fixup``
variant on the ``_internal rebase-todo-rewrite --action`` enum.

Validation mirrors Python: each ``<COMMIT>`` argument resolves
against the stack walker by SHA-prefix or Change-Id prefix;
duplicates are rejected; the first commit of the stack is
rejected because it has no parent inside the stack to fold
into.

End-to-end coverage in
``crates/mergify-cli/tests/stack_fixup.rs`` (4 cases): folds B
into A leaving [A, C], dry-run is a no-op, first-commit fixup
is rejected, unknown prefix exits non-zero. The pure
transformer is covered by 3 new ``rebase_todo`` unit tests.

The pick→fixup transform shares its implementation with a new
generalised ``rewrite_replace_verb`` helper in ``rebase_todo``
— ``reword`` will slot into the same shape when that port
follows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Change-Id: I7a139112fd95ef2b1e5bd2763cb62b0b9b2f5879
The Rust binary now serves ``mergify stack reword <COMMIT>
[-m <msg>] [--dry-run]`` natively. The Python implementation
(``mergify_cli/stack/reword.py``, its click registration, and
``mergify_cli/tests/stack/test_reword.py``) is removed in the
same PR.

Two new ``rebase_todo`` actions back the two flavors:

- ``Action::Reword { sha }`` rewrites the target ``pick`` line as
  ``reword``. Git pauses on the commit and runs
  ``git commit --amend``, opening ``$GIT_EDITOR``. Used when no
  ``-m`` argument is given.

- ``Action::ExecAfter { sha, command }`` injects an ``exec
  <command>`` line directly after the target ``pick`` line. Used
  with ``-m``: the message is written to a tempfile and the
  exec runs ``git commit --amend -F <file>`` while HEAD still
  points at the target commit, so any ``prepare-commit-msg``
  hook re-attaches the Change-Id. Tempfile is intentionally
  leaked so ``git rebase --continue`` after a conflict can still
  find it. The ``ExecAfter`` shape lands here because it is also
  the squash-with-custom-message machinery, so the next slice
  reuses it for free.

``mergify stack reword <COMMIT> [-m <msg>] [--dry-run]``:

1. Resolves the trunk and walks the stack (shared with the
   other rebase-family commands).
2. Matches the ``<COMMIT>`` argument by SHA-prefix or Change-Id
   prefix.
3. ``--dry-run`` short-circuits with a ``Reword plan:`` line
   showing ``reword`` (no ``-m``) or ``amend`` (``-m`` given) so
   the dry-run text matches Python.
4. Otherwise spawns ``git rebase -i <base>`` with
   ``GIT_SEQUENCE_EDITOR`` pointing at the binary's
   ``_internal rebase-todo-rewrite`` subcommand.

End-to-end coverage in
``crates/mergify-cli/tests/stack_reword.rs`` (3 cases): ``-m``
replaces subject + body, ``--dry-run`` is a no-op, unknown
prefix exits non-zero. The pure transformer is covered by 4
new ``rebase_todo`` unit tests (reword happy path / no match;
exec-after happy path / no match).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Change-Id: I39f31d0f8b31b890a3a390e928f7342ca04cd7b7
The Rust binary now serves ``mergify stack reorder <COMMIT>...
[--dry-run]`` and ``mergify stack move <COMMIT> <POSITION>
[<TARGET>] [--dry-run]`` natively. The Python implementations
(``mergify_cli/stack/reorder.py::stack_reorder``,
``mergify_cli/stack/move.py``, their click registrations, and
``mergify_cli/tests/stack/test_{reorder,move}.py``) are removed.

Both commands reduce to the same rebase-todo transformation —
they only differ in how they compute the desired pick order —
so this slice ships both behind a single new
``Action::Reorder { ordered_shas }`` variant on
``mergify_stack::rebase_todo``. The transformer rebuilds the
todo with picks in the requested order and appends every
non-pick line (comments, blanks, ``exec`` annotations) at the
end, mirroring Python's ``run_action_rebase`` bucketing.

``mergify stack reorder``: the ``<COMMIT>...`` arguments list
every stack commit in the desired new order. Validation rejects
duplicate prefixes, prefixes that don't resolve, and length
mismatches. When the requested order already matches the
current order, we short-circuit with
``Stack is already in the requested order`` and don't spawn a
rebase.

``mergify stack move``: a thin wrapper that computes the new
order from ``<COMMIT> <POSITION> [<TARGET>]`` and then delegates
to the same ``Action::Reorder`` path. ``POSITION`` is one of
``first``, ``last``, ``before``, ``after``; ``before`` and
``after`` require a ``<TARGET>``, ``first`` and ``last`` reject
one.

The Python ``stack_reorder``/``stack_move`` functions are gone,
but ``mergify_cli/stack/reorder.py`` itself is kept as a
header file: it still hosts the ``get_stack_commits``,
``match_commit``, ``run_scripted_rebase``,
``run_action_rebase`` and ``display_*`` helpers that the
remaining Python ``stack_squash`` (the next slice) still depends
on. When ``stack squash`` follows, the whole file goes.

Also fixes a parallel-test flake in
``crates/mergify-cli/tests/stack_*.rs``: each integration test
file now spawns ``git`` through a local ``isolated_git()``
helper that sets ``GIT_CONFIG_GLOBAL=/dev/null`` and
``GIT_CONFIG_NOSYSTEM=1``, matching the lib-test fixture
behavior. Symptom was sporadic ``git`` failures when many test
binaries ran their per-test fixtures concurrently.

End-to-end coverage in
``crates/mergify-cli/tests/stack_reorder.rs`` (7 cases): reorder
to explicit sequence, already-in-order is a no-op, count
mismatch errors, move to ``first``, move ``before`` target, move
``before`` without target errors, move ``first`` with target
errors. The pure transformer adds 3 new ``rebase_todo`` unit
tests (happy path, count mismatch, unknown SHA).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Change-Id: I8b603412b742c1a863f5c807b05c954635dd0e94
The Rust binary now serves ``mergify stack squash <SRC>... into
<TARGET> [-m <msg>] [--dry-run]`` natively. With this slice, the
entire ``stack`` rebase family is native: ``edit``, ``drop``,
``fixup``, ``reword``, ``reorder``, ``move``, ``squash``.

Final cleanup: ``mergify_cli/stack/squash.py``,
``mergify_cli/stack/reorder.py`` (the helpers ``squash.py`` was
still using), and their tests (``test_squash.py``,
``test_squash_cli.py``) are removed. ``mergify_cli/stack/cli.py``
loses its ``squash`` click registration and its
``_parse_squash_tokens`` helper.

The rebase-todo machinery grows one new variant —
``Action::Squash`` — that combines what ``Reorder`` and
``Fixup`` did separately plus an optional ``ExecAfter``:

- ``ordered_shas`` — the full new pick order (target's neighbours
  rearranged so all sources sit directly after it).
- ``fixup_shas`` — the subset whose verb flips from ``pick`` to
  ``fixup``.
- ``exec_after_sha`` + ``exec_command`` — when ``-m`` is given,
  an ``exec git commit --amend -F <file>`` is injected right
  after the last fixed-up source. The amend runs while HEAD
  still points at the combined target commit so
  ``prepare-commit-msg`` re-attaches the Change-Id.

``mergify stack squash``:

1. Resolves the trunk and walks the stack.
2. Parses ``SRC... into TARGET`` — the ``into`` keyword splits
   the positional list. Mirrors the Python parser:
   exactly one ``into``, at least one source before, exactly
   one target after.
3. Validates: target is not among the sources; no source SHA
   appears twice.
4. Builds the new order: keep non-source commits in their
   original positions, insert all sources directly after
   target in the order they were listed.
5. ``--dry-run`` short-circuits with the plan; otherwise spawns
   ``git rebase -i <base>`` with the squash sequence editor.
6. With ``-m``: writes the message to a leaked tempfile and
   passes it through ``--sha``/``--command`` on the
   ``rebase-todo-rewrite`` self-invocation.

End-to-end coverage in
``crates/mergify-cli/tests/stack_squash.rs`` (6 cases): single
source into target keeps target's message, custom ``-m``
replaces subject + body, dry-run is a no-op, source-equals-
target errors, missing ``into`` errors, multi-source folds all
into target. The pure transformer adds 3 new ``rebase_todo``
unit tests (happy path, exec-after injection, count mismatch).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Change-Id: I380f216ca57ebf78c8fb6c62e3a5af48a9dfdd55
The Rust binary now serves ``mergify stack checkout <NAME>``
natively. ``mergify_cli/stack/checkout.py``,
``mergify_cli/tests/stack/test_checkout.py``, and the click
registration are removed in the same PR.

First GitHub-API-backed stack subcommand to land natively. The
slice introduces a shared ``mergify_stack::stack_context`` module
that resolves the per-command preamble — repository owner/repo,
GitHub API server URL, default branch prefix — that ``sync``,
``list``, ``open``, and ``push`` will all reuse when their ports
follow.

``mergify_stack::stack_context``:
- ``parse_slug`` — owner/repo extraction handling both
  ``https://github.com/o/r.git`` and ``git@github.com:o/r.git``
  shapes, port of ``utils.get_slug``.
- ``resolve_repo`` — explicit ``--repository OWNER/REPO``
  takes precedence; otherwise we parse
  ``git config remote.<remote>.url``.
- ``resolve_github_server`` — reads
  ``mergify-cli.github-server`` git config, falls back to
  ``https://api.github.com/``, mirrors Python's https-coercion +
  ``/api/v3`` suffix for GitHub Enterprise.
- ``resolve_default_branch_prefix`` — reads
  ``mergify-cli.stack-branch-prefix``, falls back to
  ``stack/<author>``.

``mergify_stack::commands::checkout``:

1. Normalises the ``NAME`` argument — strip any trailing
   ``/Ixxxx…`` Change-Id suffix (via the new
   ``change_id::strip_branch_suffix`` helper) and any leading
   ``<branch_prefix>/`` so users can paste a leaf branch ref
   verbatim.
2. Searches GitHub for the stack's PRs via the existing
   ``mergify_stack::remote_changes::get_remote_changes``.
3. Builds the open-PR chain from root to leaf — root is the PR
   whose ``base.ref`` doesn't start with the stack branch
   prefix; we walk up via ``head.ref`` → ``base.ref`` linking.
   Two roots surface as ``InvalidState``, matching Python.
4. ``--dry-run`` short-circuits with the printed chain.
   Otherwise ``git fetch <remote> <leaf-head>``,
   ``git checkout -b <local> <remote>/<leaf-head>``,
   ``git branch --set-upstream-to=<remote>/<root-base>``.

Binary handler resolves the trunk via ``--trunk REMOTE/BRANCH``
or the existing ``mergify_stack::trunk::get_trunk`` fallback,
the token via ``mergify_core::auth::resolve_token``, and the
author via either ``--author`` or ``GET /user`` against the
resolved client.

Coverage in ``crates/mergify-stack/src/commands/checkout.rs``
(7 tests): the pure chain builder (root-to-leaf, closed-PR
filter, two-root detection, empty case) plus three end-to-end
``run`` tests against wiremock — no-stacked-prs, dry-run returns
chain without touching git, name normalisation strips both the
Change-Id suffix and the branch prefix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Change-Id: I71e74cae43846d9708bef9c57d32ba81b8499edc
The Rust binary now serves ``mergify stack sync [--dry-run]``
natively. The Python ``stack_sync`` entry point and
``test_sync.py`` are gone; the click registration is gone too.

The rest of ``mergify_cli/stack/sync.py`` (``SyncStatus``,
``get_sync_status``, ``smart_rebase``, ``_write_drop_script``)
is kept for now because the still-Python ``stack push`` calls
``smart_rebase`` directly. The whole file is queued for deletion
with the ``stack push`` port.

New infrastructure that the remaining slices (``list``,
``push``, ``open``) will reuse:

- ``mergify_stack::sync_status`` — sync-specific classifier that
  walks the local stack commits, matches each to a remote PR by
  ``Change-Id`` (with the cross-prefix tolerance Python's
  ``pop_remote_change`` had), and buckets them into
  ``MergedCommit`` (closed-and-merged AND head SHA matches the
  local commit) and ``RemainingCommit`` (everything else). The
  full create/update/skip-up-to-date classifier needed by
  ``stack push``/``stack list`` lands with those slices.
- ``mergify_stack::stack_context::check_local_branch`` — rejects
  local branches whose shape matches the auto-generated
  ``<prefix>/.../<slug>--<hex8>`` or legacy ``/I<40hex>`` forms.
- ``MERGIFY_GITHUB_SERVER`` env var on
  ``stack_context::resolve_github_server`` — bypasses git config
  and the unconditional ``https://`` coercion, so integration
  tests can point at a wiremock server speaking plain ``http``.

``mergify stack sync``:

1. Validates the local branch isn't an auto-generated leaf branch
   and that it isn't the trunk itself.
2. Resolves ``user/repo``, the trunk, and the
   ``git merge-base --fork-point`` (falling back to plain
   ``merge-base`` for sandboxes with no reflog).
3. Searches GitHub for the stack PRs and classifies each local
   commit via ``sync_status::classify``.
4. ``--dry-run`` prints a plan and exits 0. Otherwise:
   - When every commit is merged or none are, ``git pull --rebase``.
   - Mixed case: ``git rebase -i <trunk>`` with
     ``GIT_SEQUENCE_EDITOR`` pointing at this binary's
     ``_internal rebase-todo-rewrite --action drop --shas <merged>``
     — single rebase pass, no separate drop step.

End-to-end coverage in
``crates/mergify-cli/tests/stack_sync.rs`` (3 cases against a
real binary + wiremock + real git): up-to-date when no PRs are
merged, dry-run lists merged commits, validation rejects
auto-generated branch names. The classifier adds 7 unit tests
(merged / amended-after-merge / open / no-pr / cross-prefix
match / ``all_merged`` / ``up_to_date`` predicates).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Change-Id: I7f75be875289707f492ecbf59f6bcb6530554f24
The Rust binary now serves ``mergify stack list [--json]
[--verbose]`` and ``mergify stack open [<commit>]`` natively.
The Python implementations
(``mergify_cli/stack/list.py``,
``mergify_cli/stack/list_schema.py``,
``mergify_cli/stack/open.py``, the click registrations, and
``test_{list,open}.py``) are removed.

New shared infrastructure that the remaining slices
(``stack push``) will reuse:

- ``mergify_stack::changes::classify`` — full per-commit
  classifier that buckets each local commit as
  ``Create`` / ``Update`` / ``SkipMerged`` / ``SkipUpToDate``,
  the same shape Python's ``get_changes`` returns. Also
  surfaces orphan PRs (open remote changes that don't match
  any local commit). The merged-vs-remaining ``sync_status``
  module stays for ``stack sync`` since sync only needs the
  bucket; the full classifier here is what ``list``, ``open``,
  and ``push`` need.

- ``StackContext`` + ``resolve_stack_context`` in the binary
  — bundles token / GitHub server / repo slug / author /
  branch prefix / trunk resolution that every API-backed stack
  subcommand runs. ``checkout`` and ``sync`` will pick this up
  in a follow-up cleanup; for now they keep their inline
  copies.

``mergify stack list``:

1. Runs the shared stack-context preamble.
2. Walks the local stack via ``local_commits::read``, fetches
   the matching remote PRs via ``remote_changes::get_remote_changes``,
   classifies via ``changes::classify``.
3. For each entry, computes the display status
   (``open`` / ``draft`` / ``merged`` / ``no_pr``) from the
   classifier's action plus the PR's ``merged_at`` / ``draft``
   fields.
4. Optionally fans out per-PR ``/check-runs`` and ``/reviews``
   fetches and folds them into the entry's ``ci_*`` / ``review_*``
   fields. Sequential (not concurrent) — GitHub's secondary
   rate limit on the same endpoint pool starts firing around
   ~80 concurrent calls, and a typical stack is well under
   that. ``stack open`` passes ``include_status=false`` to
   skip this.
5. Renders either JSON (``serde_json::to_string_pretty`` of
   ``StackListOutput``, byte-compatible with the pre-port
   ``--json`` output) or a plain-text table.

``mergify stack open``:

1. Runs ``stack::list`` with ``include_status=false``.
2. ``None`` commit → leaf entry (the Python interactive picker
   defaulted there too; we leave the picker out of the port for
   now).
3. Explicit commit → ``git rev-parse --verify`` then look up
   the matching ``StackListEntry``.
4. Hand the ``html_url`` to the OS opener: ``open`` (macOS),
   ``xdg-open`` (other Unix), ``cmd /C start ""`` (Windows).
5. PR-less commits surface ``StackNotFound`` with a hint to
   run ``stack push`` first.

Coverage: 7 ``changes::tests`` for the classifier (create /
skip-merged / amended-after-merge / skip-up-to-date / update /
orphan / closed-unmatched), 6 ``commands::list::tests`` for
the CI / review status reducers (passing / failing-over-pending
/ pending / approved / changes-requested / pending-from-comments).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Change-Id: I0a12ccb23061dceba00b94cbddf93df388452657
The Rust binary now serves both ``mergify stack hooks`` (status
+ ``--setup``) and ``mergify stack setup`` (alias for
``hooks --setup``, plus ``--check``). The Python implementation
(``mergify_cli/stack/setup.py``, the click registrations, the
``test_setup.py`` suite, and the hook resource tree under
``mergify_cli/stack/hooks/``) is removed. The hook scripts and
wrappers are renamed to ``crates/mergify-stack/hooks/{scripts,
wrappers}/`` and embedded into the binary via ``include_str!``.

``mergify_stack::commands::setup``:

- Mirrors Python's on-disk layout exactly so a checkout
  upgraded from a Python install is recognised without
  re-installation: ``.git/hooks/<hook>`` is the thin wrapper
  (user-editable below the marker comment),
  ``.git/hooks/mergify-hooks/<hook>.sh`` is the managed script
  (always upgradable).
- ``WrapperStatus`` detection mirrors ``_get_wrapper_status``:
  ``mergify-hooks`` + ``<hook>.sh`` substring pair = ours;
  legacy heuristics (``Change-Id: I${random}`` on
  ``commit-msg``, ``is_amend_with_m_flag`` on
  ``prepare-commit-msg``) match the pre-sourcing-architecture
  installs so ``--force`` knows to migrate them.
- ``ensure_notes_display_ref`` adds the
  ``notes.displayRef = refs/notes/mergify/*`` git config line
  so ``git log`` surfaces the amend notes attached by
  ``mergify stack note``.
- ``HookAction`` log values let the binary handler render the
  same per-hook lines Python printed
  (``Installing hook wrapper: …`` /
  ``Updating managed hook script: mergify-hooks/<hook>.sh`` /
  ``Migrating legacy hook to new format: …`` /
  ``Found legacy hook: …``).

The (still-Python) ``stack push`` pre-flight used to call
``stack_setup_mod.stack_setup()`` and
``stack_setup_mod.ensure_hooks_updated()`` directly. With
``setup.py`` gone, those two calls in ``mergify_cli/stack/cli.py``
shell out to ``mergify stack setup`` instead — same behaviour,
goes through the native installer. The whole pre-flight block
goes away when ``stack push`` itself lands native.

Coverage: 6 lib tests over a tempdir-backed git repo —
``status`` reports all-missing on a fresh repo; ``install``
writes wrappers + scripts and is idempotent on a second run;
legacy wrappers report ``WrapperLegacyNeedsForce`` without
``--force`` and ``WrapperMigrated`` with it; user-written
wrappers (no ``mergify-hooks`` sentinel) are left untouched;
the ``notes.displayRef`` config line is added on first install.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Change-Id: I12f4b4af3bf3dcae8002687f03eb55876c61170b
End-to-end native port of `mergify stack push`: wires every
leaf module, the orchestrator, and the binary dispatch in one
self-consistent change so `mergify stack push` works on the
Rust binary and the Python implementation gets deleted in the
same commit.

New `mergify-stack` modules (each is a faithful port from
`mergify_cli/stack/`):

- `change_type` — patch-id-based rebase-vs-content
  classification + `refs/pull/<n>/head` fetch helper.
- `stack_comment` — the "this PR is part of a stack" sticky
  comment renderer + header recogniser. JSON marker stays
  byte-for-byte compatible with historic comments.
- `replay` — `git merge-tree` + `git diff-tree` + the HTTP
  upload (`POST /git/trees` + `POST /git/commits`) that
  materialise the rebase-aware revision-history compare URL.
- `revision_history` — the "Revision history" sticky comment
  with parse + append + render round-tripping byte-for-byte
  against the Python implementation.
- `approvals` — the rebase/no-rebase decision: skip the
  rebase when PRs are already approved (so reviews aren't
  dismissed) unless the bottom of the stack has a real merge
  conflict with trunk.
- `notes_push` — `git fetch`/`git push` plumbing for
  `refs/notes/mergify/stack` + the per-PR refspecs that
  `stack push` lands atomically with `--force-with-lease`.
- `rebase_log` — pure formatters for the three rebase
  narration log lines.
- `push_helpers` — `format_pull_description` (strip
  Change-Id + stale Depends-On, append fresh Depends-On) +
  `build_change_tasks` (the per-PR dependency graph).
- `pr_upsert` — per-PR Create (POST /pulls) and Update
  (PATCH /pulls/<n>) + orphan branch teardown.
- `comment_upsert` — per-PR sticky-comment upserters: stack
  comment (skip-when-single-PR) + revision history (parse +
  append + recover-from-corrupt).
- `plan` — the layer above `classify` that applies the
  `--next-only` / `--only-update-existing-pulls` overrides
  and resolves `dest_branch` / `base_branch`.
- `changes::Action` extended with `SkipCreate` /
  `SkipNextOnly` so the planner can surface
  would-be-created PRs without opening any.
- `commands::push` — the end-to-end orchestrator wiring
  every leaf above. Runs per-PR upserts sequentially
  (typical 2-5 PR stacks make the latency difference
  negligible vs. the GitHub round-trip cost, and the
  simpler code avoids a `tokio::sync::Notify` graph).

Binary wiring (`crates/mergify-cli/src/main.rs`):

- `NativeCommand::StackPush(StackPushOpts)` variant +
  `StackPushCli` clap struct mirroring the Python click
  option surface 1:1 (`--skip-rebase`, `--force-rebase`,
  `--next-only`, `--dry-run`, `--draft`, `--keep-pull-
  request-title-and-body`, `--author`, `--trunk`,
  `--branch-prefix`, `--only-update-existing-pulls`,
  `--no-revision-history`, `--no-verify`).
- `dispatch_stack` "push" arm routing through to
  `commands::push::run`.
- `("stack", "push")` added to `NATIVE_COMMANDS`.

Python deletion:

- `mergify_cli/stack/push.py` (the orchestrator, 1394 LOC).
- `mergify_cli/stack/{sync,approvals,replay,changes}.py` —
  helpers only imported by push.py and its tests.
- `mergify_cli/tests/stack/test_{push,replay,approvals}.py`.
- `mergify_cli/stack/cli.py` — the `push` command + the
  `stack_push_mod` import. The click group itself stays
  so `python -m mergify_cli stack` keeps printing help
  until the package is removed wholesale in the follow-up.

Adds `chrono` (with `clock` feature) and `tokio` (with
`time` feature) + `url` to `mergify-stack`'s prod deps to
support the revision-history timestamps, the
mergeable-retry sleep, and the wiremock test base-URL
parse.

End-to-end verified against the Python implementation on
real fixtures (stack-comment + revision-history bodies
match byte-for-byte; planner action overrides match the
Python `get_changes` semantics including the
`--next-only` × `--only-update-existing-pulls` interaction
at idx 0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Change-Id: I1b2c30509153a19071cf495155a03e9a2e616e87
@jd

jd commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

This pull request is part of a Mergify stack:

# Pull Request Link
1 feat(rust): port stack push end-to-end + delete Python push.py #1551 👈
2 chore: delete Python tree + simplify wheel/CI for pure-Rust binary #1547

@mergify mergify Bot had a problem deploying to Mergify Merge Protections June 5, 2026 06:33 Failure
@jd jd temporarily deployed to func-tests-live June 5, 2026 06:33 — with GitHub Actions Inactive
@mergify

mergify Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

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

🔴 👀 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,})

@mergify mergify Bot requested a review from a team June 5, 2026 06:43
@jd jd force-pushed the devs/jd/feat/rust-stack-new/port-stack-hooks-stack-setup-native-rust--12f4b4af branch from 56cb890 to e5c33cc Compare June 5, 2026 07:00
@mergify

mergify Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

@jd this pull request is now in conflict 😩

@mergify mergify Bot added the conflict label Jun 5, 2026
@jd

jd commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

Superseded by #1553 — restructured into a single-branch stack via mergify stack.

@jd jd closed this Jun 5, 2026
@mergify mergify Bot removed the conflict label Jun 5, 2026
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