Currently when a user does a conflict resolution and ends it (in any
way that calls 'git rerere' again) with a file 'rerere' can't handle,
subsequent rerere operations that are interested in that path, such as
'rerere clear' or 'rerere forget <path>' will fail, or even worse in
the case of 'rerere clear' segfault.

Such states include nested conflicts, or a conflict marker that
doesn't have any match.

This is because 'git rerere' calculates a conflict file and writes it
to the MERGE_RR file.  When the user then changes the file in any way
rerere can't handle, and then calls 'git rerere' on it again to record
the conflict resolution, the handle_file function fails, and removes
the 'preimage' file in the rr-cache in the process, while leaving the
ID in the MERGE_RR file.

Now when 'rerere clear' is run, it reads the ID from the MERGE_RR
file, however the 'fit_variant' function for the ID is never called as
the 'preimage' file does not exist anymore.  This means
'collection->status' in 'has_rerere_resolution' is NULL, and the
command will crash.

To fix this, remove the rerere ID from the MERGE_RR file in the case
when we can't handle it, just after the 'preimage' file was removed
and remove the corresponding variant from .git/rr-cache/.  Removing it
unconditionally is fine here, because if the user would have resolved
the conflict and ran rerere, the entry would no longer be in the
MERGE_RR file, so we wouldn't have this problem in the first place,
while if the conflict was not resolved.

Currently there is nothing left in this folder, as the 'preimage'
was already deleted by the 'handle_file' function, so 'remove_variant'
is a no-op.  Still call the function, to make sure we clean everything
up, in case we add some other files corresponding to a variant in the
future.

Note that other variants that have the same conflict ID will not be
touched.

Signed-off-by: Thomas Gummerer <t.gumme...@gmail.com>
---
 rerere.c          | 12 +++++++-----
 t/t4200-rerere.sh | 21 +++++++++++++++++++++
 2 files changed, 28 insertions(+), 5 deletions(-)

diff --git a/rerere.c b/rerere.c
index da1ab54027..895ad80c0c 100644
--- a/rerere.c
+++ b/rerere.c
@@ -823,10 +823,7 @@ static int do_plain_rerere(struct string_list *rr, int fd)
                struct rerere_id *id;
                unsigned char sha1[20];
                const char *path = conflict.items[i].string;
-               int ret;
-
-               if (string_list_has_string(rr, path))
-                       continue;
+               int ret, has_string;
 
                /*
                 * Ask handle_file() to scan and assign a
@@ -834,7 +831,12 @@ static int do_plain_rerere(struct string_list *rr, int fd)
                 * yet.
                 */
                ret = handle_file(path, sha1, NULL);
-               if (ret < 1)
+               has_string = string_list_has_string(rr, path);
+               if (ret < 0 && has_string) {
+                       remove_variant(string_list_lookup(rr, path)->util);
+                       string_list_remove(rr, path, 1);
+               }
+               if (ret < 1 || has_string)
                        continue;
 
                id = new_rerere_id(sha1);
diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh
index 8417e5a4b1..23f9c0ca45 100755
--- a/t/t4200-rerere.sh
+++ b/t/t4200-rerere.sh
@@ -580,4 +580,25 @@ test_expect_success 'multiple identical conflicts' '
        count_pre_post 0 0
 '
 
+test_expect_success 'rerere with unexpected conflict markers does not crash' '
+       git reset --hard &&
+
+       git checkout -b branch-1 master &&
+       echo "bar" >test &&
+       git add test &&
+       git commit -q -m two &&
+
+       git reset --hard &&
+       git checkout -b branch-2 master &&
+       echo "foo" >test &&
+       git add test &&
+       git commit -q -a -m one &&
+
+       test_must_fail git merge branch-1 &&
+       echo "<<<<<<< a" >test &&
+       git rerere &&
+
+       git rerere clear
+'
+
 test_done
-- 
2.18.0.720.gf7a957e2e7

Reply via email to