Skip to content

git fetch: add --rebase to rewrite upstream changes in place - #9506

Closed
LoganDark wants to merge 1 commit into
jj-vcs:mainfrom
LoganDark:git-fetch-rebase
Closed

git fetch: add --rebase to rewrite upstream changes in place#9506
LoganDark wants to merge 1 commit into
jj-vcs:mainfrom
LoganDark:git-fetch-rebase

Conversation

@LoganDark

Copy link
Copy Markdown

When upstream rewrites a change (e.g. amends or rebases) and you fetch the new revision, a normal jj git fetch keeps your old revision alongside the new one as a divergent change. With --rebase, the incoming revision rewrites your existing one in place: bookmarks, the working copy, and descendants are moved onto the new revision, and the old revision is hidden -- the same outcome jj produces when the rewrite happens locally.

If a change already has multiple visible revisions in the repo before the fetch, there is no unambiguous old-to-new mapping, so the import aborts with an error and a hint pointing at jj abandon / jj duplicate.

abandon_unreachable_commits now takes an exclusion set so that commits already recorded as rewritten don't have their Rewritten entry in parent_mapping overwritten with Abandoned, which would otherwise reparent descendants onto the old commit's parents instead of onto the rewrite target.

Implementation:

  • GitImportOptions gains replace_divergent_changes, applied in import_refs_inner after refs are updated but before abandon_unreachable_commits.
  • The pairing pass only considers commits that this fetch actually imported (tracked via newly_imported_ids), so an unchanged ref target that happens to be in the repo isn't treated as a rewrite of something.
  • abandon_unreachable_commits now takes an exclusion set so it doesn't overwrite the Rewritten entry in parent_mapping with Abandoned, which would otherwise reparent descendants onto the old commit's parents instead of onto the rewrite target.

Checklist

If applicable:

  • I have updated CHANGELOG.md
  • I have updated the documentation (README.md, docs/, demos/)
  • I have updated the config schema (cli/src/config-schema.json)
  • I have added/updated tests to cover my changes
  • I fully understand the code that I am submitting (what it does,
    how it works, how it's organized), including any code drafted by an LLM.
  • For any prose generated by an LLM, I have proof-read and copy-edited with
    an eye towards deleting anything that is irrelevant, clarifying anything
    that is confusing, and adding details that are relevant. This includes,
    for example, commit descriptions, PR descriptions, and code comments.

Note: the changes and much of the PR description have definitely been generated by LLM. However, I've tried my best to make sure everything user-facing fits with how the rest of Jujutsu's help is written. As far as I can tell, this implementation is correct (I've looked it over myself to the best of my ability), and I've been using this workflow myself since just a couple hours after this comment.

@LoganDark
LoganDark requested a review from a team as a code owner May 15, 2026 12:11
Comment thread lib/src/git.rs Outdated
let iter_changed_refs = || itertools::chain(&changed_remote_bookmarks, &changed_remote_tags);
let index = mut_repo.index();
let missing_head_ids: Vec<&CommitId> = iter_changed_refs()
let newly_imported_ids: HashSet<CommitId> = iter_changed_refs()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means jj git fetch behaves differently from jj git fetch; jj undo; jj git fetch. We'll have to generate rewrite mapping for newly-visible changes, not newly-imported.

This comment was marked as resolved.

Comment thread lib/src/git.rs Outdated
// We collect first so an error on a later commit doesn't leave the repo
// with a half-applied rewrite mapping.
let mut rewrites: Vec<(CommitId, CommitId)> = Vec::with_capacity(new_commits.len());
for new_commit in new_commits {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm testing a similar change right now, and I found that we need to build {change_id: [commit_id]} mappings for both old and new candidates to handle newly-divergent and previously-divergent cases.

We'll also need to generate rewrite mappings for ancestors of bookmarked revisions.

Comment thread cli/tests/test_git_fetch.rs Outdated
");
}

/// Set up a colocated source repo with a single tracked commit on a bookmark

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic tests should be added to lib/tests/test_git.rs. CLI tests are super slow.

@PhilipMetzger

Copy link
Copy Markdown
Contributor

I don't think we should support this, #1039 is the appropriate place for such things.

@LoganDark
LoganDark marked this pull request as draft May 15, 2026 13:54
@LoganDark

LoganDark commented May 15, 2026

Copy link
Copy Markdown
Author

I don't think we should support this, #1039 is the appropriate place for such things.

This fixes a real pain in my workflow, so I would really like to see it somewhere in jj. To me this feels like just an extension of jj git fetch. Is there a good reason to defer it entirely until jj git sync?

I'm still in the process of investigating based on review comments, so I've marked as draft.

@yuja

yuja commented May 15, 2026

Copy link
Copy Markdown
Contributor

We've discussed this in #7632, but jj git fetch will do by default rebase descendants based on change IDs (or refs) instead of just abandoning old revisions. jj git sync will do more than that, such as rebasing other WIP changes onto the main branch.

I'm working on patches to fix #6326 and #7632.

@LoganDark

LoganDark commented May 15, 2026

Copy link
Copy Markdown
Author

We've discussed this in #7632, but jj git fetch will do by default rebase descendants based on change IDs (or refs) instead of just abandoning old revisions. jj git sync will do more than that, such as rebasing other WIP changes onto the main branch.

I'm working on patches to fix #6326 and #7632.

What will this mean for workflows that are helped / made feasible by this feature? Do you think jj will have something like it soon?

I was nearly considering moving all of my projects into a monorepo just to get proper rebases for the ones that depend on shared structure. This is almost the only reason I did not have to.

Another huge help to my workflow was #9487, which created huge speedups for my scripts.

@LoganDark
LoganDark force-pushed the git-fetch-rebase branch 2 times, most recently from 716f5ae to 1d33da0 Compare May 16, 2026 05:54
When upstream rewrites a change (e.g. amends or rebases) and you fetch the
new revision, a normal `jj git fetch` keeps your old revision alongside the
new one as a divergent change. With `--rebase`, the incoming revision
rewrites your existing one in place: bookmarks, the working copy, and
descendants are moved onto the new revision, and the old revision is hidden
-- the same outcome jj produces when the rewrite happens locally.

If a change already has multiple visible revisions in the repo before the
fetch, there is no unambiguous old-to-new mapping, so the import aborts with
an error and a hint pointing at `jj abandon` / `jj duplicate`.

`abandon_unreachable_commits` now takes an exclusion set so that commits
already recorded as rewritten don't have their `Rewritten` entry in
`parent_mapping` overwritten with `Abandoned`, which would otherwise
reparent descendants onto the old commit's parents instead of onto the
rewrite target.

git import: rewrite stacks and reject newly-introduced divergence

Address two related gaps in --rebase from the previous commit:

* Stack rebases: `replace_divergent_changes` only iterated head commits, so
  when upstream rebased a stack of N changes only the head was paired with
  its local counterpart. Now we walk every commit that became newly visible
  in jj (head plus newly-imported ancestors, stopping at commits already
  reachable from a pre-transaction head) and pair each one's change id.

* Fetch-introduced divergence: when one fetch brought in multiple new
  commits sharing a change id with a single existing local commit, the
  per-commit loop observed the first new commit as already-visible while
  processing the second and aborted with `PreExistingDivergentChange`,
  which is misleading. The rewrite logic now groups by change id and
  matches old/new candidates by cardinality, and the new
  `NewlyDivergentChange` error signals the fetch itself was the source.

git fetch: shift --rebase coverage into lib tests

Move the bulk of --rebase verification out of `cli/tests/test_git_fetch.rs`
(where each case needs a colocated source repo and a full CLI roundtrip) into
`lib/tests/test_git.rs`, which exercises `git::import_refs` directly against a
TestRepo. The CLI-only assertions that lib can't reach — the `Display` for
`GitImportError::DivergentChanges` and the exact contents of each problem's
commit lists — are pinned at the lib level via `err.to_string()` and sorted
commit-id checks. The remote-tracking ref is also asserted in the happy-path
lib test so the rewrite covers both `feat` and `feat@origin`.

A single CLI snapshot remains for the divergent-changes error renderer
(`cli/src/git_util.rs::divergent_changes_error`): it pins the formatted hint
header, the indented commit list rendered through `commit_summary_template`,
and the trailing hint. The pre-existing case is enough — the newly-introduced
path uses the same code with different strings, and those strings are pinned
by the lib `err.to_string()` assertions.
@yuja

yuja commented May 16, 2026

Copy link
Copy Markdown
Contributor

What will this mean for workflows that are helped / made feasible by this feature? Do you think jj will have something like it soon?

#9510

@LoganDark

LoganDark commented May 16, 2026

Copy link
Copy Markdown
Author

What will this mean for workflows that are helped / made feasible by this feature? Do you think jj will have something like it soon?

#9510

That looks like a much better solution!

@LoganDark LoganDark closed this May 16, 2026
@LoganDark
LoganDark deleted the git-fetch-rebase branch May 16, 2026 14:55
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.

3 participants