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>
Junio C Hamano committed
Aug 30, 2008 at 07:52 UTC
0cf8581e330e7140c9f5c94a53d441187c0f8ff9
3 files changed
+173
-5
Documentation/git-checkout.txt
+8
-3
@@ -9,7 +9,7 @@ SYNOPSIS
9
--------
10
[verse]
11
'git checkout' [-q] [-f] [[--track | --no-track] -b <new_branch> [-l]] [-m] [<branch>]
12
-'git checkout' [-f|--ours|--theirs] [<tree-ish>] [--] <paths>...
12
+'git checkout' [-f|--ours|--theirs|-m] [<tree-ish>] [--] <paths>...
13
14
DESCRIPTION
15
-----------
@@ -35,7 +35,8 @@ default, if you try to check out such an entry from the index, the
35
checkout operation will fail and nothing will be checked out.
36
Using -f will ignore these unmerged entries. The contents from a
37
specific side of the merge can be checked out of the index by
38
-using --ours or --theirs.
38
+using --ours or --theirs. With -m, changes made to the working tree
39
+file can be discarded to recreate the original conflicted merge result.
40
41
OPTIONS
42
-------
@@ -83,7 +84,8 @@ entries; instead, unmerged entries are ignored.
84
based sha1 expressions such as "<branchname>@\{yesterday}".
85
86
-m::
86
- If you have local modifications to one or more files that
87
+ When switching branches,
88
+ if you have local modifications to one or more files that
89
are different between the current branch and the branch to
90
which you are switching, the command refuses to switch
91
branches in order to preserve your modifications in context.
@@ -95,6 +97,9 @@ When a merge conflict happens, the index entries for conflicting
97
paths are left unmerged, and you need to resolve the conflicts
98
and mark the resolved paths with `git add` (or `git rm` if the merge
99
should result in deletion of the path).
100
++
101
+When checking out paths from the index, this option lets you recreate
102
+the conflicted merge in the specified paths.
103
104
<new_branch>::
105
Name for the new branch.
builtin-checkout.c
+102
-2
@@ -13,6 +13,9 @@
13
#include "diff.h"
14
#include "revision.h"
15
#include "remote.h"
16
+#include "blob.h"
17
+#include "xdiff-interface.h"
18
+#include "ll-merge.h"
19
20
static const char * const checkout_usage[] = {
21
"git checkout [options] <branch>",
@@ -109,6 +112,19 @@ static int check_stage(int stage, struct cache_entry *ce, int pos)
112
(stage == 2) ? "our" : "their");
113
}
114
115
+static int check_all_stages(struct cache_entry *ce, int pos)
116
+{
117
+ if (ce_stage(ce) != 1 ||
118
+ active_nr <= pos + 2 ||
119
+ strcmp(active_cache[pos+1]->name, ce->name) ||
120
+ ce_stage(active_cache[pos+1]) != 2 ||
121
+ strcmp(active_cache[pos+2]->name, ce->name) ||
122
+ ce_stage(active_cache[pos+2]) != 3)
123
+ return error("path '%s' does not have all three versions",
124
+ ce->name);
125
+ return 0;
126
+}
127
+
128
static int checkout_stage(int stage, struct cache_entry *ce, int pos,
129
struct checkout *state)
130
{
@@ -123,6 +139,77 @@ static int checkout_stage(int stage, struct cache_entry *ce, int pos,
139
(stage == 2) ? "our" : "their");
140
}
141
142
+/* NEEDSWORK: share with merge-recursive */
143
+static void fill_mm(const unsigned char *sha1, mmfile_t *mm)
144
+{
145
+ unsigned long size;
146
+ enum object_type type;
147
+
148
+ if (!hashcmp(sha1, null_sha1)) {
149
+ mm->ptr = xstrdup("");
150
+ mm->size = 0;
151
+ return;
152
+ }
153
+
154
+ mm->ptr = read_sha1_file(sha1, &type, &size);
155
+ if (!mm->ptr || type != OBJ_BLOB)
156
+ die("unable to read blob object %s", sha1_to_hex(sha1));
157
+ mm->size = size;
158
+}
159
+
160
+static int checkout_merged(int pos, struct checkout *state)
161
+{
162
+ struct cache_entry *ce = active_cache[pos];
163
+ const char *path = ce->name;
164
+ mmfile_t ancestor, ours, theirs;
165
+ int status;
166
+ unsigned char sha1[20];
167
+ mmbuffer_t result_buf;
168
+
169
+ if (ce_stage(ce) != 1 ||
170
+ active_nr <= pos + 2 ||
171
+ strcmp(active_cache[pos+1]->name, path) ||
172
+ ce_stage(active_cache[pos+1]) != 2 ||
173
+ strcmp(active_cache[pos+2]->name, path) ||
174
+ ce_stage(active_cache[pos+2]) != 3)
175
+ return error("path '%s' does not have all 3 versions", path);
176
+
177
+ fill_mm(active_cache[pos]->sha1, &ancestor);
178
+ fill_mm(active_cache[pos+1]->sha1, &ours);
179
+ fill_mm(active_cache[pos+2]->sha1, &theirs);
180
+
181
+ status = ll_merge(&result_buf, path, &ancestor,
182
+ &ours, "ours", &theirs, "theirs", 1);
183
+ free(ancestor.ptr);
184
+ free(ours.ptr);
185
+ free(theirs.ptr);
186
+ if (status < 0 || !result_buf.ptr) {
187
+ free(result_buf.ptr);
188
+ return error("path '%s': cannot merge", path);
189
+ }
190
+
191
+ /*
192
+ * NEEDSWORK:
193
+ * There is absolutely no reason to write this as a blob object
194
+ * and create a phoney cache entry just to leak. This hack is
195
+ * primarily to get to the write_entry() machinery that massages
196
+ * the contents to work-tree format and writes out which only
197
+ * allows it for a cache entry. The code in write_entry() needs
198
+ * to be refactored to allow us to feed a <buffer, size, mode>
199
+ * instead of a cache entry. Such a refactoring would help
200
+ * merge_recursive as well (it also writes the merge result to the
201
+ * object database even when it may contain conflicts).
202
+ */
203
+ if (write_sha1_file(result_buf.ptr, result_buf.size,
204
+ blob_type, sha1))
205
+ die("Unable to add merge result for '%s'", path);
206
+ ce = make_cache_entry(create_ce_mode(active_cache[pos+1]->ce_mode),
207
+ sha1,
208
+ path, 2, 0);
209
+ status = checkout_entry(ce, state, NULL);
210
+ return status;
211
+}
212
+
213
static int checkout_paths(struct tree *source_tree, const char **pathspec,
214
struct checkout_opts *opts)
215
{
@@ -134,6 +221,7 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec,
221
struct commit *head;
222
int errs = 0;
223
int stage = opts->writeout_stage;
224
+ int merge = opts->merge;
225
int newfd;
226
struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file));
227
@@ -165,6 +253,8 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec,
253
warning("path '%s' is unmerged", ce->name);
254
} else if (stage) {
255
errs |= check_stage(stage, ce, pos);
256
+ } else if (opts->merge) {
257
+ errs |= check_all_stages(ce, pos);
258
} else {
259
errs = 1;
260
error("path '%s' is unmerged", ce->name);
@@ -188,6 +278,8 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec,
278
}
279
if (stage)
280
errs |= checkout_stage(stage, ce, pos, &state);
281
+ else if (merge)
282
+ errs |= checkout_merged(pos, &state);
283
pos = skip_same_name(ce, pos) - 1;
284
}
285
}
@@ -476,6 +568,11 @@ static int switch_branches(struct checkout_opts *opts, struct branch_info *new)
568
return ret || opts->writeout_error;
569
}
570
571
+static int git_checkout_config(const char *var, const char *value, void *cb)
572
+{
573
+ return git_xmerge_config(var, value, cb);
574
+}
575
+
576
int cmd_checkout(int argc, const char **argv, const char *prefix)
577
{
578
struct checkout_opts opts;
@@ -502,7 +599,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
599
memset(&opts, 0, sizeof(opts));
600
memset(&new, 0, sizeof(new));
601
505
- git_config(git_default_config, NULL);
602
+ git_config(git_checkout_config, NULL);
603
604
opts.track = git_branch_track;
605
@@ -594,7 +691,7 @@ no_reference:
691
die("invalid path specification");
692
693
/* Checkout paths */
597
- if (opts.new_branch || opts.merge) {
694
+ if (opts.new_branch) {
695
if (argc == 1) {
696
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]);
697
} else {
@@ -602,6 +699,9 @@ no_reference:
699
}
700
}
701
702
+ if (1 < !!opts.writeout_stage + !!opts.force + !!opts.merge)
703
+ die("git checkout: --ours/--theirs, --force and --merge are incompatible when\nchecking out of the index.");
704
+
705
return checkout_paths(source_tree, pathspec, &opts);
706
}
707
t/t7201-co.sh
+63
@@ -407,4 +407,67 @@ test_expect_success 'checkout unmerged stage' '
407
test ztheirside = "z$(cat file)"
408
'
409
410
+test_expect_success 'checkout with --merge' '
411
+ rm -f .git/index &&
412
+ O=$(echo original | git hash-object -w --stdin) &&
413
+ A=$(echo ourside | git hash-object -w --stdin) &&
414
+ B=$(echo theirside | git hash-object -w --stdin) &&
415
+ (
416
+ echo "100644 $A 0 fild" &&
417
+ echo "100644 $O 1 file" &&
418
+ echo "100644 $A 2 file" &&
419
+ echo "100644 $B 3 file" &&
420
+ echo "100644 $A 0 filf"
421
+ ) | git update-index --index-info &&
422
+ echo "none of the above" >sample &&
423
+ echo ourside >expect &&
424
+ cat sample >fild &&
425
+ cat sample >file &&
426
+ cat sample >filf &&
427
+ git checkout -m -- fild file filf &&
428
+ (
429
+ echo "<<<<<<< ours"
430
+ echo ourside
431
+ echo "======="
432
+ echo theirside
433
+ echo ">>>>>>> theirs"
434
+ ) >merged &&
435
+ test_cmp expect fild &&
436
+ test_cmp expect filf &&
437
+ test_cmp merged file
438
+'
439
+
440
+test_expect_success 'checkout with --merge, in diff3 -m style' '
441
+ git config merge.conflictstyle diff3 &&
442
+ rm -f .git/index &&
443
+ O=$(echo original | git hash-object -w --stdin) &&
444
+ A=$(echo ourside | git hash-object -w --stdin) &&
445
+ B=$(echo theirside | git hash-object -w --stdin) &&
446
+ (
447
+ echo "100644 $A 0 fild" &&
448
+ echo "100644 $O 1 file" &&
449
+ echo "100644 $A 2 file" &&
450
+ echo "100644 $B 3 file" &&
451
+ echo "100644 $A 0 filf"
452
+ ) | git update-index --index-info &&
453
+ echo "none of the above" >sample &&
454
+ echo ourside >expect &&
455
+ cat sample >fild &&
456
+ cat sample >file &&
457
+ cat sample >filf &&
458
+ git checkout -m -- fild file filf &&
459
+ (
460
+ echo "<<<<<<< ours"
461
+ echo ourside
462
+ echo "|||||||"
463
+ echo original
464
+ echo "======="
465
+ echo theirside
466
+ echo ">>>>>>> theirs"
467
+ ) >merged &&
468
+ test_cmp expect fild &&
469
+ test_cmp expect filf &&
470
+ test_cmp merged file
471
+'
472
+
473
test_done