Skip to content

fix(git/log): stop dropping merge commits when user pins a selection#2016

Open
YOMXXX wants to merge 1 commit into
rtk-ai:developfrom
YOMXXX:fix/git-log-merge-sha
Open

fix(git/log): stop dropping merge commits when user pins a selection#2016
YOMXXX wants to merge 1 commit into
rtk-ai:developfrom
YOMXXX:fix/git-log-merge-sha

Conversation

@YOMXXX
Copy link
Copy Markdown
Contributor

@YOMXXX YOMXXX commented May 21, 2026

Summary

`rtk git log -1 HEAD` on a repo whose HEAD is a `--no-ff` merge commit M (parents `[P1, P2]`) returns P2 instead of M. Same for explicit SHAs, annotated tags, `--graph`, and `--graph --oneline` — every shape that names a specific commit gets the second parent of the merge instead of the merge itself.

The caller can't tell the result is wrong, because P2 is a real recent commit. A release script that asks "what's the SHA of the merge I just landed?" gets a value that looks plausible and is wrong.

`rtk proxy git log -1 HEAD` returns M, confirming `git` itself is fine — the bug is in RTK's filter.

Root cause

`run_log` in `src/cmds/git/git.rs` unconditionally injects `--no-merges` as a token-saving default. When the user passes `-1`, git walks past the dropped merge to the next non-merge commit (the second parent, because `git log` defaults to a topology-aware traversal), so the precise pin silently shifts the selection.

`--no-merges` is the right default only when RTK is fully driving output — bare `git log` with no flags, no ref, no format. As soon as the user signals "I'm picking", the default has to stand down.

Fix

Extract a pure helper `should_add_no_merges_default(args, has_format, has_limit)`. It returns `true` only when none of these signals are present:

  • `--format` / `--pretty` / `--oneline`   (caller controls output shape)
  • `-N` / `-n N` / `--max-count=N`   (caller picked a precise window)
  • `--graph`   (caller wants topology — needs merges)
  • any positional argument   (ref / SHA / tag / pathspec)
  • explicit `--merges` / `--min-parents=2`   (caller already opted in)

`has_format` and `has_limit` are passed in from the caller's existing scan so we don't re-walk the args list.

Behaviour matrix

invocation `--no-merges` injected? Notes
`rtk git log` ✅ yes RTK token-saving default (unchanged)
`rtk git log -1 HEAD` ❌ no #2009 fix
`rtk git log -1` ❌ no precise pin
`rtk git log --format=%H HEAD` ❌ no format + ref
`rtk git log ` ❌ no positional = ref
`rtk git log --graph` ❌ no topology
`rtk git log --graph --oneline` ❌ no topology + compact
`rtk git log --merges` ❌ no explicit opt-in (pre-existing)
`rtk git log --min-parents=2` ❌ no explicit opt-in (pre-existing)
`rtk git log --author=foo` ✅ yes filter ≠ pin
`rtk git log --max-count=5` ❌ no explicit window

Test plan

11 new tests pin each shape in the table above:

  • `test_no_merges_added_for_bare_git_log` — token-saving default stays for bare invocation.
  • `test_no_merges_skipped_with_explicit_limit_one` + `_only` — `-1 HEAD` and bare `-1` (headline reproduction).
  • `test_no_merges_skipped_with_format_flag_and_ref` + `_explicit_sha` + `_graph_flag` + `_graph_and_oneline` — every other shape the issue body called out.
  • `test_no_merges_skipped_with_explicit_merges` + `_min_parents` — pre-existing opt-in stays correct.
  • `test_no_merges_still_added_with_author_filter` — documents the deliberate narrow scope: filter flags that don't pick a single commit still benefit from the token-saving default.
  • `test_no_merges_skipped_with_max_count_arg` — short-circuit via `has_limit`.

`cargo fmt --all` clean / `cargo clippy --all-targets` zero warnings / `cargo test --bin rtk -- --test-threads=8` → 1920 passed (was 1909).

Scope notes

  • The pure helper is unit-tested directly. The `run_log` integration path is unchanged otherwise, so existing `filter_log_output` / `parse_user_limit` tests stay green and document that the rest of the log filter is intact.
  • The `--author=foo` case is deliberately left in the default-merges path: an author filter doesn't pick a single commit, so `-N` summary behaviour with the token-saving default is still the right shape. If usage data later shows the opposite, that's a follow-up.

Fixes #2009

`rtk git log -1 HEAD` on a repo whose HEAD is a `--no-ff` merge commit
M (parents [P1, P2]) was returning P2 instead of M. Same for explicit
SHAs, annotated tags, `--graph`, and `--graph --oneline` — every shape
that names a specific commit got the second parent of the merge
instead of the merge itself. Caller can't tell the result is wrong
because P2 is a real recent commit.

Root cause: `run_log` unconditionally injects `--no-merges` as a
token-saving default. When the user passes `-1`, git walks past the
dropped merge to the next non-merge commit (the second parent because
git log defaults to a topology-aware traversal), so a precise pin
silently shifts the selection.

`--no-merges` is the right default only when RTK is fully driving the
output — bare `git log` with no flags, no ref, no format. As soon as
the user signals "I'm picking", the default has to stand down.

Extract `should_add_no_merges_default(args, has_format, has_limit)`
as a pure helper. Returns `true` only when none of these signals are
present:

- `--format` / `--pretty` / `--oneline`  (caller controls output shape)
- `-N` / `-n N` / `--max-count=N`        (caller picked a precise window)
- `--graph`                               (caller wants topology — needs merges)
- any positional argument                 (ref / SHA / tag / pathspec)
- explicit `--merges` / `--min-parents=2` (caller already opted in)

`has_format` and `has_limit` are passed in from the caller's existing
scan so we don't re-walk the args list.

Behaviour matrix (existing tests stayed green, the new ones pin each
shape):

| invocation                            | --no-merges? |
|---------------------------------------|--------------|
| `rtk git log`                         | yes (RTK default)
| `rtk git log -1 HEAD`                 | no  (rtk-ai#2009 fix)
| `rtk git log -1`                      | no
| `rtk git log --format=%H HEAD`        | no
| `rtk git log <sha>`                   | no
| `rtk git log --graph`                 | no
| `rtk git log --graph --oneline`       | no
| `rtk git log --merges`                | no  (explicit opt-in)
| `rtk git log --min-parents=2`         | no
| `rtk git log --author=foo`            | yes (filter, not a pin)
| `rtk git log --max-count=5`           | no

Tests:
- test_no_merges_added_for_bare_git_log — token-saving default stays.
- test_no_merges_skipped_with_explicit_limit_one + _only — covers
  `-1 HEAD` and bare `-1`, the headline reproduction from rtk-ai#2009.
- test_no_merges_skipped_with_format_flag_and_ref + _explicit_sha +
  _graph_flag + _graph_and_oneline — covers every other shape the
  issue body called out as broken.
- test_no_merges_skipped_with_explicit_merges + _min_parents — locks
  in the pre-existing opt-in semantics so the refactor doesn't
  regress it.
- test_no_merges_still_added_with_author_filter — documents the
  deliberate narrow scope: filter flags that don't pick a single
  commit still benefit from the token-saving default.
- test_no_merges_skipped_with_max_count_arg — short-circuit via
  has_limit.

cargo fmt / clippy --all-targets / test --bin rtk: 1920 passed,
0 warnings.

Fixes rtk-ai#2009
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

git log filter returns wrong SHA for merge commits (resolves to second parent)

1 participant