"reset --merge": fix unmerged case
authorJunio C Hamano <gitster@pobox.com>
Fri, 1 Jan 2010 07:04:04 +0000 (23:04 -0800)
committerJunio C Hamano <gitster@pobox.com>
Mon, 4 Jan 2010 00:01:05 +0000 (16:01 -0800)
Commit 9e8ecea (Add 'merge' mode to 'git reset', 2008-12-01) disallowed
"git reset --merge" when there was unmerged entries. But it wished if
unmerged entries were reset as if --hard (instead of --merge) has been
used. This makes sense because all "mergy" operations makes sure that
any path involved in the merge does not have local modifications before
starting, so resetting such a path away won't lose any information.

The previous commit changed the behavior of --merge to accept resetting
unmerged entries if they are reset to a different state than HEAD, but it
did not reset the changes in the work tree, leaving the conflict markers
in the resulting file in the work tree.

Fix it by doing three things:

- Update the documentation to match the wish of original "reset --merge"
better, namely, "An unmerged entry is a sign that the path didn't have
any local modification and can be safely resetted to whatever the new
HEAD records";

- Update read_index_unmerged(), which reads the index file into the cache
while dropping any higher-stage entries down to stage #0, not to copy
the object name from the higher stage entry. The code used to take the
object name from the a stage entry ("base" if you happened to have
stage #1, or "ours" if both sides added, etc.), which essentially meant
that you are getting random results depending on what the merge did.

The _only_ reason we want to keep a previously unmerged entry in the
index at stage #0 is so that we don't forget the fact that we have
corresponding file in the work tree in order to be able to remove it
when the tree we are resetting to does not have the path. In order to
differentiate such an entry from ordinary cache entry, the cache entry
added by read_index_unmerged() is marked as CE_CONFLICTED.

- Update merged_entry() and deleted_entry() so that they pay attention to
cache entries marked as CE_CONFLICTED. They are previously unmerged
entries, and the files in the work tree that correspond to them are
resetted away by oneway_merge() to the version from the tree we are
resetting to.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/git-reset.txt
cache.h
read-cache.c
t/t7110-reset-merge.sh
unpack-trees.c
index cf2433d52c831569aae73abefd60dfbf7241f5df..dc73dca7361b6f7c608b2d80646bafa7bde3d4e1 100644 (file)
@@ -122,14 +122,14 @@ entries:
        X       U     A    B     --soft  (disallowed)
                                --mixed  X       B     B
                                --hard   B       B     B
-                               --merge  X       B     B
+                               --merge  B       B     B
 
       working index HEAD target         working index HEAD
       ----------------------------------------------------
        X       U     A    A     --soft  (disallowed)
                                --mixed  X       A     A
                                --hard   A       A     A
-                               --merge (disallowed)
+                               --merge  A       A     A
 
 X means any state and U means an unmerged index.
 
diff --git a/cache.h b/cache.h
index bf468e52352c193b355222b718d9f5125c26052c..da8ec92415eed9cb0de9cae18eef8f2720a672c8 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -177,6 +177,7 @@ struct cache_entry {
 
 #define CE_HASHED    (0x100000)
 #define CE_UNHASHED  (0x200000)
+#define CE_CONFLICTED (0x400000)
 
 /*
  * Extended on-disk flags
index 1bbaf1cffba3058667787fc4a267b87e21a6cad3..16c4548a74561fdb8b744167b24af580234317e8 100644 (file)
@@ -1606,9 +1606,8 @@ int read_index_unmerged(struct index_state *istate)
                len = strlen(ce->name);
                size = cache_entry_size(len);
                new_ce = xcalloc(1, size);
-               hashcpy(new_ce->sha1, ce->sha1);
                memcpy(new_ce->name, ce->name, len);
-               new_ce->ce_flags = create_ce_flags(len, 0);
+               new_ce->ce_flags = create_ce_flags(len, 0) | CE_CONFLICTED;
                new_ce->ce_mode = ce->ce_mode;
                if (add_index_entry(istate, new_ce, 0))
                        return error("%s: cannot drop to stage #0",
index ff2875c00b856fa6b4badbcbb87e45d7fd0b48b7..8704d0019655d591785c0cf0eadb7d846c6b4469 100755 (executable)
@@ -116,10 +116,11 @@ test_expect_success 'reset --merge fails with changes in file it touches' '
     grep file1 err.log | grep "not uptodate"
 '
 
-test_expect_success 'setup 2 different branches' '
+test_expect_success 'setup 3 different branches' '
     git reset --hard second &&
     git branch branch1 &&
     git branch branch2 &&
+    git branch branch3 &&
     git checkout branch1 &&
     echo "line 5 in branch1" >> file1 &&
     test_tick &&
@@ -128,34 +129,55 @@ test_expect_success 'setup 2 different branches' '
     echo "line 5 in branch2" >> file1 &&
     test_tick &&
     git commit -a -m "change in branch2" &&
-    git tag third
+    git tag third &&
+    git checkout branch3 &&
+    echo a new file >file3 &&
+    rm -f file1 &&
+    git add file3 &&
+    test_tick &&
+    git commit -a -m "change in branch3"
 '
 
 # The next test will test the following:
 #
 #           working index HEAD target         working index HEAD
 #           ----------------------------------------------------
-# file1:     X       U     B    C     --merge  X       C     C
+# file1:     X       U     B    C     --merge  C       C     C
 test_expect_success '"reset --merge HEAD^" is ok with pending merge' '
+    git checkout third &&
     test_must_fail git merge branch1 &&
-    cat file1 >orig_file1 &&
     git reset --merge HEAD^ &&
     test "$(git rev-parse HEAD)" = "$(git rev-parse second)" &&
     test -z "$(git diff --cached)" &&
-    test_cmp file1 orig_file1
+    test -z "$(git diff)"
 '
 
 # The next test will test the following:
 #
 #           working index HEAD target         working index HEAD
 #           ----------------------------------------------------
-# file1:     X       U     B    B     --merge  (disallowed)
-test_expect_success '"reset --merge HEAD" fails with pending merge' '
+# file1:     X       U     B    B     --merge  B       B     B
+test_expect_success '"reset --merge HEAD" is ok with pending merge' '
     git reset --hard third &&
     test_must_fail git merge branch1 &&
-    test_must_fail git reset --merge HEAD &&
+    git reset --merge HEAD &&
     test "$(git rev-parse HEAD)" = "$(git rev-parse third)" &&
-    test -n "$(git diff --cached)"
+    test -z "$(git diff --cached)" &&
+    test -z "$(git diff)"
+'
+
+test_expect_success '--merge with added/deleted' '
+    git reset --hard third &&
+    rm -f file2 &&
+    test_must_fail git merge branch3 &&
+    ! test -f file2 &&
+    test -f file3 &&
+    git diff --exit-code file3 &&
+    git diff --exit-code branch3 file3 &&
+    git reset --merge HEAD &&
+    ! test -f file3 &&
+    ! test -f file2 &&
+    git diff --exit-code --cached
 '
 
 test_done
index dd5999c3562219b7993420b22257f4088ab82b8d..3df0de60054a2cd82b1284f5c223ff8c44a0768b 100644 (file)
@@ -436,6 +436,8 @@ static int same(struct cache_entry *a, struct cache_entry *b)
                return 0;
        if (!a && !b)
                return 1;
+       if ((a->ce_flags | b->ce_flags) & CE_CONFLICTED)
+               return 0;
        return a->ce_mode == b->ce_mode &&
               !hashcmp(a->sha1, b->sha1);
 }
@@ -666,7 +668,11 @@ static int merged_entry(struct cache_entry *merge, struct cache_entry *old,
 {
        int update = CE_UPDATE;
 
-       if (old) {
+       if (!old) {
+               if (verify_absent(merge, "overwritten", o))
+                       return -1;
+               invalidate_ce_path(merge, o);
+       } else if (!(old->ce_flags & CE_CONFLICTED)) {
                /*
                 * See if we can re-use the old CE directly?
                 * That way we get the uptodate stat info.
@@ -682,11 +688,12 @@ static int merged_entry(struct cache_entry *merge, struct cache_entry *old,
                                return -1;
                        invalidate_ce_path(old, o);
                }
-       }
-       else {
-               if (verify_absent(merge, "overwritten", o))
-                       return -1;
-               invalidate_ce_path(merge, o);
+       } else {
+               /*
+                * Previously unmerged entry left as an existence
+                * marker by read_index_unmerged();
+                */
+               invalidate_ce_path(old, o);
        }
 
        add_entry(o, merge, update, CE_STAGEMASK);
@@ -702,7 +709,7 @@ static int deleted_entry(struct cache_entry *ce, struct cache_entry *old,
                        return -1;
                return 0;
        }
-       if (verify_uptodate(old, o))
+       if (!(old->ce_flags & CE_CONFLICTED) && verify_uptodate(old, o))
                return -1;
        add_entry(o, ce, CE_REMOVE, 0);
        invalidate_ce_path(ce, o);