Browse Source

checkout -m: recreate merge when checking out of unmerged index

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>
maint
Junio C Hamano 17 years ago
parent
commit
0cf8581e33
  1. 11
      Documentation/git-checkout.txt
  2. 104
      builtin-checkout.c
  3. 63
      t/t7201-co.sh

11
Documentation/git-checkout.txt

@ -9,7 +9,7 @@ SYNOPSIS @@ -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 @@ -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. @@ -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 @@ -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.

104
builtin-checkout.c

@ -13,6 +13,9 @@ @@ -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) @@ -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, @@ -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, @@ -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, @@ -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, @@ -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) @@ -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) @@ -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: @@ -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: @@ -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);
}


63
t/t7201-co.sh

@ -407,4 +407,67 @@ test_expect_success 'checkout unmerged stage' ' @@ -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

Loading…
Cancel
Save