From 8640d5b86526c03e029c7d591084d96ec5d0fb10 Mon Sep 17 00:00:00 2001 From: Amey Pawar Date: Thu, 6 Aug 2026 22:10:09 +0530 Subject: [PATCH 1/3] change!: accept `:3:` for the 'theirs' index stage, like Git does. The parser had arms for stages 0, 1 and 2 and let everything else fall through to the catch-all, so `:3:file` was looked up as a path literally named `3:file` at stage 0. `gitrevisions(7)` documents a stage number of 0 to 3, and Git resolves `:3:file` to the blob from the branch being merged. The trait's own docs said stages range from 0 to 2 and labelled them base, ours and theirs. That is off by one: 0 is unconflicted, and 1, 2 and 3 are the common ancestor, the target branch and the branch being merged. The docs came first, in cee04e126, and the arms written twenty minutes later in ea22d3e7c matched them. This is marked breaking because a `Navigate` implementation written against the old documented range can now be handed a stage it does not expect. The one in `gix` already maps 3 to `Stage::Theirs`, but the one behind `gix revision explain` did not, which the next commit addresses. `:4:` and other numbers stay part of the path, which Git agrees with. --- gix-revision/src/spec/parse/delegate.rs | 3 ++- gix-revision/src/spec/parse/function.rs | 5 +++++ .../tests/revision/spec/parse/anchor/colon_symbol.rs | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/gix-revision/src/spec/parse/delegate.rs b/gix-revision/src/spec/parse/delegate.rs index f768fbcd196..24971bfc59f 100644 --- a/gix-revision/src/spec/parse/delegate.rs +++ b/gix-revision/src/spec/parse/delegate.rs @@ -72,7 +72,8 @@ pub trait Navigate { /// or return `None` if it doesn't exist at this `stage`. /// Note that this implies no revision is needed and no anchor is set yet. /// - /// * `stage` ranges from 0 to 2, with 0 being the base, 1 being ours, 2 being theirs. + /// * `stage` ranges from 0 to 3, with 0 being unconflicted, 1 being the base, 2 being ours + /// and 3 being theirs, just like the stages Git records in the index. /// * `path` without prefix is relative to the root of the repository, while prefixes like `./` and `../` make it /// relative to the current working directory. fn index_lookup(&mut self, path: &BStr, stage: u8) -> Result<(), Exn>; diff --git a/gix-revision/src/spec/parse/function.rs b/gix-revision/src/spec/parse/function.rs index 8eed1a8453d..54ea931264f 100644 --- a/gix-revision/src/spec/parse/function.rs +++ b/gix-revision/src/spec/parse/function.rs @@ -403,6 +403,11 @@ where format!("Couldn't find index '{path}' stage 2", path = path.as_bstr()) }); } + [b':', b'3', b':', path @ ..] => { + return consume_all(delegate.index_lookup(path.as_bstr(), 3), || { + format!("Couldn't find index '{path}' stage 3", path = path.as_bstr()) + }); + } [b':', path @ ..] => { return consume_all(delegate.index_lookup(path.as_bstr(), 0), || { format!("Couldn't find index '{path}' stage 0 (implicit)", path = path.as_bstr()) diff --git a/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs b/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs index e04a0eefefe..aed7bc92d98 100644 --- a/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs +++ b/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs @@ -87,6 +87,8 @@ fn various_valid_index_lookups_by_path_and_stage() { (":0:path", "path", 0), (":1:dir/path", "dir/path", 1), (":2:dir/path@{part-of-path}", "dir/path@{part-of-path}", 2), + // Git records conflicts as stages 1, 2 and 3, so `:3:` is the 'theirs' side. + (":3:dir/path", "dir/path", 3), ] { let rec = parse(spec); From 21c5614e1eb3cfa21d208083544913305b3986f1 Mon Sep 17 00:00:00 2001 From: Amey Pawar Date: Thu, 6 Aug 2026 22:10:09 +0530 Subject: [PATCH 2/3] adapt to changes in `gix-revision`. `gix revision explain` panicked on `:3:` through its `unreachable!()` arm now that the parser can emit stage 3. Its stage labels carried the same off-by-one as the trait docs, printing "stage 1 (ours)" where `gitrevisions(7)` has stage 1 as the common ancestor. --- gitoxide-core/src/repository/revision/explain.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gitoxide-core/src/repository/revision/explain.rs b/gitoxide-core/src/repository/revision/explain.rs index 75ac2c62823..f2c042e6c86 100644 --- a/gitoxide-core/src/repository/revision/explain.rs +++ b/gitoxide-core/src/repository/revision/explain.rs @@ -197,9 +197,10 @@ impl delegate::Navigate for Explain<'_> { path, stage, match stage { - 0 => "base", - 1 => "ours", - 2 => "theirs", + 0 => "unconflicted", + 1 => "base", + 2 => "ours", + 3 => "theirs", _ => unreachable!("BUG: parser assures of that"), } ) From 5a4e97bf27752091ec48cf1128645d20ea135b9d Mon Sep 17 00:00:00 2001 From: Byron Date: Thu, 6 Aug 2026 19:51:15 +0200 Subject: [PATCH 3/3] review --- gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs b/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs index aed7bc92d98..6ed3ff26d6c 100644 --- a/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs +++ b/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs @@ -87,7 +87,6 @@ fn various_valid_index_lookups_by_path_and_stage() { (":0:path", "path", 0), (":1:dir/path", "dir/path", 1), (":2:dir/path@{part-of-path}", "dir/path@{part-of-path}", 2), - // Git records conflicts as stages 1, 2 and 3, so `:3:` is the 'theirs' side. (":3:dir/path", "dir/path", 3), ] { let rec = parse(spec);