checkout -m: recreate merge when checking out of unmerged index
authorJunio C Hamano <gitster@pobox.com>
Sat, 30 Aug 2008 14:52:24 +0000 (07:52 -0700)
committerJunio C Hamano <gitster@pobox.com>
Sun, 31 Aug 2008 02:57:55 +0000 (19:57 -0700)
This teaches git-checkout to recreate a merge out of unmerged
index entries while resolving conflicts.

With this patch, checking out an unmerged path from the index
now have the following possibilities:

 * Without any option, an attempt to checkout an unmerged path
   will atomically fail (i.e. no other cleanly-merged paths are
   checked out either);

 * With "-f", other cleanly-merged paths are checked out, and
   unmerged paths are ignored;

 * With "--ours" or "--theirs, the contents from the specified
   stage is checked out;

 * With "-m" (we should add "--merge" as synonym), the 3-way merge
   is recreated from the staged object names and checked out.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/git-checkout.txt
builtin-checkout.c
t/t7201-co.sh

index a9ca2f552017cc592c8ff6b553bf0bbf875646a0..c884862e2fb91831c3962599507b4034d4bcb77e 100644 (file)
@@ -9,7 +9,7 @@ SYNOPSIS
 --------
 [verse]
 'git checkout' [-q] [-f] [[--track | --no-track] -b <new_branch> [-l]] [-m] [<branch>]
-'git checkout' [-f|--ours|--theirs] [<tree-ish>] [--] <paths>...
+'git checkout' [-f|--ours|--theirs|-m] [<tree-ish>] [--] <paths>...
 
 DESCRIPTION
 -----------
@@ -35,7 +35,8 @@ default, if you try to check out such an entry from the index, the
 checkout operation will fail and nothing will be checked out.
 Using -f will ignore these unmerged entries.  The contents from a
 specific side of the merge can be checked out of the index by
-using --ours or --theirs.
+using --ours or --theirs.  With -m, changes made to the working tree
+file can be discarded to recreate the original conflicted merge result.
 
 OPTIONS
 -------
@@ -83,7 +84,8 @@ entries; instead, unmerged entries are ignored.
        based sha1 expressions such as "<branchname>@\{yesterday}".
 
 -m::
-       If you have local modifications to one or more files that
+       When switching branches,
+       if you have local modifications to one or more files that
        are different between the current branch and the branch to
        which you are switching, the command refuses to switch
        branches in order to preserve your modifications in context.
@@ -95,6 +97,9 @@ When a merge conflict happens, the index entries for conflicting
 paths are left unmerged, and you need to resolve the conflicts
 and mark the resolved paths with `git add` (or `git rm` if the merge
 should result in deletion of the path).
++
+When checking out paths from the index, this option lets you recreate
+the conflicted merge in the specified paths.
 
 <new_branch>::
        Name for the new branch.
index 16bfbb66055951e44c19410b6c889b3aa63891ec..b957193155f799ebea0cabcc7496408d4d96d599 100644 (file)
@@ -13,6 +13,9 @@
 #include "diff.h"
 #include "revision.h"
 #include "remote.h"
+#include "blob.h"
+#include "xdiff-interface.h"
+#include "ll-merge.h"
 
 static const char * const checkout_usage[] = {
        "git checkout [options] <branch>",
@@ -109,6 +112,19 @@ static int check_stage(int stage, struct cache_entry *ce, int pos)
                     (stage == 2) ? "our" : "their");
 }
 
+static int check_all_stages(struct cache_entry *ce, int pos)
+{
+       if (ce_stage(ce) != 1 ||
+           active_nr <= pos + 2 ||
+           strcmp(active_cache[pos+1]->name, ce->name) ||
+           ce_stage(active_cache[pos+1]) != 2 ||
+           strcmp(active_cache[pos+2]->name, ce->name) ||
+           ce_stage(active_cache[pos+2]) != 3)
+               return error("path '%s' does not have all three versions",
+                            ce->name);
+       return 0;
+}
+
 static int checkout_stage(int stage, struct cache_entry *ce, int pos,
                          struct checkout *state)
 {
@@ -123,6 +139,77 @@ static int checkout_stage(int stage, struct cache_entry *ce, int pos,
                     (stage == 2) ? "our" : "their");
 }
 
+/* NEEDSWORK: share with merge-recursive */
+static void fill_mm(const unsigned char *sha1, mmfile_t *mm)
+{
+       unsigned long size;
+       enum object_type type;
+
+       if (!hashcmp(sha1, null_sha1)) {
+               mm->ptr = xstrdup("");
+               mm->size = 0;
+               return;
+       }
+
+       mm->ptr = read_sha1_file(sha1, &type, &size);
+       if (!mm->ptr || type != OBJ_BLOB)
+               die("unable to read blob object %s", sha1_to_hex(sha1));
+       mm->size = size;
+}
+
+static int checkout_merged(int pos, struct checkout *state)
+{
+       struct cache_entry *ce = active_cache[pos];
+       const char *path = ce->name;
+       mmfile_t ancestor, ours, theirs;
+       int status;
+       unsigned char sha1[20];
+       mmbuffer_t result_buf;
+
+       if (ce_stage(ce) != 1 ||
+           active_nr <= pos + 2 ||
+           strcmp(active_cache[pos+1]->name, path) ||
+           ce_stage(active_cache[pos+1]) != 2 ||
+           strcmp(active_cache[pos+2]->name, path) ||
+           ce_stage(active_cache[pos+2]) != 3)
+               return error("path '%s' does not have all 3 versions", path);
+
+       fill_mm(active_cache[pos]->sha1, &ancestor);
+       fill_mm(active_cache[pos+1]->sha1, &ours);
+       fill_mm(active_cache[pos+2]->sha1, &theirs);
+
+       status = ll_merge(&result_buf, path, &ancestor,
+                         &ours, "ours", &theirs, "theirs", 1);
+       free(ancestor.ptr);
+       free(ours.ptr);
+       free(theirs.ptr);
+       if (status < 0 || !result_buf.ptr) {
+               free(result_buf.ptr);
+               return error("path '%s': cannot merge", path);
+       }
+
+       /*
+        * NEEDSWORK:
+        * There is absolutely no reason to write this as a blob object
+        * and create a phoney cache entry just to leak.  This hack is
+        * primarily to get to the write_entry() machinery that massages
+        * the contents to work-tree format and writes out which only
+        * allows it for a cache entry.  The code in write_entry() needs
+        * to be refactored to allow us to feed a <buffer, size, mode>
+        * instead of a cache entry.  Such a refactoring would help
+        * merge_recursive as well (it also writes the merge result to the
+        * object database even when it may contain conflicts).
+        */
+       if (write_sha1_file(result_buf.ptr, result_buf.size,
+                           blob_type, sha1))
+               die("Unable to add merge result for '%s'", path);
+       ce = make_cache_entry(create_ce_mode(active_cache[pos+1]->ce_mode),
+                             sha1,
+                             path, 2, 0);
+       status = checkout_entry(ce, state, NULL);
+       return status;
+}
+
 static int checkout_paths(struct tree *source_tree, const char **pathspec,
                          struct checkout_opts *opts)
 {
@@ -134,6 +221,7 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec,
        struct commit *head;
        int errs = 0;
        int stage = opts->writeout_stage;
+       int merge = opts->merge;
        int newfd;
        struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file));
 
@@ -165,6 +253,8 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec,
                                warning("path '%s' is unmerged", ce->name);
                        } else if (stage) {
                                errs |= check_stage(stage, ce, pos);
+                       } else if (opts->merge) {
+                               errs |= check_all_stages(ce, pos);
                        } else {
                                errs = 1;
                                error("path '%s' is unmerged", ce->name);
@@ -188,6 +278,8 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec,
                        }
                        if (stage)
                                errs |= checkout_stage(stage, ce, pos, &state);
+                       else if (merge)
+                               errs |= checkout_merged(pos, &state);
                        pos = skip_same_name(ce, pos) - 1;
                }
        }
@@ -476,6 +568,11 @@ static int switch_branches(struct checkout_opts *opts, struct branch_info *new)
        return ret || opts->writeout_error;
 }
 
+static int git_checkout_config(const char *var, const char *value, void *cb)
+{
+       return git_xmerge_config(var, value, cb);
+}
+
 int cmd_checkout(int argc, const char **argv, const char *prefix)
 {
        struct checkout_opts opts;
@@ -502,7 +599,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
        memset(&opts, 0, sizeof(opts));
        memset(&new, 0, sizeof(new));
 
-       git_config(git_default_config, NULL);
+       git_config(git_checkout_config, NULL);
 
        opts.track = git_branch_track;
 
@@ -594,7 +691,7 @@ no_reference:
                        die("invalid path specification");
 
                /* Checkout paths */
-               if (opts.new_branch || opts.merge) {
+               if (opts.new_branch) {
                        if (argc == 1) {
                                die("git checkout: updating paths is incompatible with switching branches.\nDid you intend to checkout '%s' which can not be resolved as commit?", argv[0]);
                        } else {
@@ -602,6 +699,9 @@ no_reference:
                        }
                }
 
+               if (1 < !!opts.writeout_stage + !!opts.force + !!opts.merge)
+                       die("git checkout: --ours/--theirs, --force and --merge are incompatible when\nchecking out of the index.");
+
                return checkout_paths(source_tree, pathspec, &opts);
        }
 
index c7ae14118a538414c71170516d683a888878a656..1d4ff6e8d30ce49a2792107be1c23a66b1df3a8b 100755 (executable)
@@ -407,4 +407,67 @@ test_expect_success 'checkout unmerged stage' '
        test ztheirside = "z$(cat file)"
 '
 
+test_expect_success 'checkout with --merge' '
+       rm -f .git/index &&
+       O=$(echo original | git hash-object -w --stdin) &&
+       A=$(echo ourside | git hash-object -w --stdin) &&
+       B=$(echo theirside | git hash-object -w --stdin) &&
+       (
+               echo "100644 $A 0       fild" &&
+               echo "100644 $O 1       file" &&
+               echo "100644 $A 2       file" &&
+               echo "100644 $B 3       file" &&
+               echo "100644 $A 0       filf"
+       ) | git update-index --index-info &&
+       echo "none of the above" >sample &&
+       echo ourside >expect &&
+       cat sample >fild &&
+       cat sample >file &&
+       cat sample >filf &&
+       git checkout -m -- fild file filf &&
+       (
+               echo "<<<<<<< ours"
+               echo ourside
+               echo "======="
+               echo theirside
+               echo ">>>>>>> theirs"
+       ) >merged &&
+       test_cmp expect fild &&
+       test_cmp expect filf &&
+       test_cmp merged file
+'
+
+test_expect_success 'checkout with --merge, in diff3 -m style' '
+       git config merge.conflictstyle diff3 &&
+       rm -f .git/index &&
+       O=$(echo original | git hash-object -w --stdin) &&
+       A=$(echo ourside | git hash-object -w --stdin) &&
+       B=$(echo theirside | git hash-object -w --stdin) &&
+       (
+               echo "100644 $A 0       fild" &&
+               echo "100644 $O 1       file" &&
+               echo "100644 $A 2       file" &&
+               echo "100644 $B 3       file" &&
+               echo "100644 $A 0       filf"
+       ) | git update-index --index-info &&
+       echo "none of the above" >sample &&
+       echo ourside >expect &&
+       cat sample >fild &&
+       cat sample >file &&
+       cat sample >filf &&
+       git checkout -m -- fild file filf &&
+       (
+               echo "<<<<<<< ours"
+               echo ourside
+               echo "|||||||"
+               echo original
+               echo "======="
+               echo theirside
+               echo ">>>>>>> theirs"
+       ) >merged &&
+       test_cmp expect fild &&
+       test_cmp expect filf &&
+       test_cmp merged file
+'
+
 test_done