Conversation
jochenhz
force-pushed
the
fix/push-reparent-only-on-rewrite
branch
from
September 15, 2026 20:47
259b7a1 to
300cce1
Compare
jochenhz
force-pushed
the
fix/push-reparent-only-on-rewrite
branch
from
September 16, 2026 06:43
300cce1 to
a6eec66
Compare
jochenhz
force-pushed
the
fix/push-reparent-only-on-rewrite
branch
from
September 16, 2026 18:29
a6eec66 to
abe7c41
Compare
A push carrying more than one local revision into a branch head that has moved drops the other side's work. The push succeeds, reports no conflict, and leaves the overwritten commit in the history, so nothing looks wrong afterwards. collect_fragments_and_push moved a revision's parent pointer onto the value the previous iteration left in current_latest, keeping its tree. That is correct for the case the TODO describes, where the server only rewrote the revision number. After a server-side fast-forward merge current_latest is a revision the server built from the other side's work, and the reparented revision then looks like a direct descendant: the server takes the ordinary fast-forward path with no three-way diff, and the branch becomes a tree that never held the other side's changes. Track the rename instead. Remember the pushed revision as (signature in local history, signature on the server) and follow the parent only to that revision under a new signature. Everything else is pushed as it stands, so the server merges it against a base it already holds. The guard !current_latest.is_zero() is why a single-revision push never hit this: the block cannot fire on the first revision. Regression test in scripts/test/test_push.py. Against 0.9.0 the second revision's tree replaces the branch, and the file the other client pushed is missing from a fresh clone. Signed-off-by: Jochen Hunz <j.hunz@anchorpoint.app>
jochenhz
force-pushed
the
fix/push-reparent-only-on-rewrite
branch
from
September 16, 2026 19:12
abe7c41 to
212b90e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A push that carries more than one local revision into a branch head that has moved silently drops the other side's work. The push succeeds, reports a fast-forward merge, raises no conflict — and the overwritten commit is still in the history, so nothing looks wrong to anyone reading it afterwards.
Two ordinary local commits are enough, no
--forceanywhere. Reproducible with the released 0.9.0 CLI.Cause
collect_fragments_and_pushbent a revision's parent pointer onto whatever the previous iteration left incurrent_latest, keeping its tree:That is correct for the case the TODO names: the server accepts the revision as it stands and only rewrites its revision number, so the same content is stored under a new signature and the next revision has to follow it.
It is wrong after a server-side fast-forward merge.
current_latestis then a revision the server built, carrying the other side's work. Re-pointing at it makes this revision's stale tree look like a direct descendant of it, sobranch_pushtakes the ordinary fast-forward path —current_head == state.parent_self()holds legitimately — with no merge, no three-way diff and no conflict check. The branch simply becomes that tree, and everything the other side added is absent from it.The guard
!current_latest.is_zero()is why this has not shown up with a single-revision push: the block cannot fire for the first revision of a push, only from the second on.Fix
Track the rename instead of the head. Remember the pushed revision as
(signature in local history, signature on the server)and follow a parent only when it is exactly that revision under a new signature. After a server-side merge the recorded signature is the one the server stored, which is what the merge holds as its other parent — not necessarily the one the revision has in local history, since this loop may have re-serialized it.Any other mismatch is now left alone, which sends the revision as it stands and lets the server three-way merge it against a base it already holds — the revision's own parent, which reached the server as the previous merge's second parent. So a chain of N local revisions pushes correctly, one server merge each, rather than the last one winning outright.
Verification
scripts/test/test_push.py::test_push_fast_forward_merge_two_local_revisions, next to the existing fast-forward-merge tests. Againstmainit fails withMissing: {'from_other.txt'}; with this change it passes.test_push_fast_forward_mergestill passes either way.cargo test -p lore-revisionclean,cargo clippy -p lore-revision --all-targets -- -D warnings --no-depsclean,cargo +nightly fmt --checkclean.Not covered
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.