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>
This commit is contained in:
parent
29a1f99b4b
commit
0cf8581e33
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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…
Reference in New Issue
Block a user