Skip to content

Commit

Permalink
checkout -m: recreate merge when checking out of unmerged index
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
gitster committed Aug 31, 2008
1 parent 29a1f99 commit 0cf8581
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 5 deletions.
11 changes: 8 additions & 3 deletions Documentation/git-checkout.txt
Expand Up @@ -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
-----------
Expand All @@ -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
-------
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
104 changes: 102 additions & 2 deletions builtin-checkout.c
Expand Up @@ -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>",
Expand Down Expand Up @@ -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)
{
Expand All @@ -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)
{
Expand All @@ -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));

Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -594,14 +691,17 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
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 {
die("git checkout: updating paths is incompatible with switching branches.");
}
}

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);
}

Expand Down
63 changes: 63 additions & 0 deletions t/t7201-co.sh
Expand Up @@ -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

0 comments on commit 0cf8581

Please sign in to comment.