Fixes to "git rerere" corner cases, especially when conflict
markers cannot be parsed in the file.

* tg/rerere:
  rerere: recalculate conflict ID when unresolved conflict is committed
  rerere: teach rerere to handle nested conflicts
  rerere: return strbuf from handle path
  rerere: factor out handle_conflict function
  rerere: only return whether a path has conflicts or not
  rerere: fix crash with files rerere can't handle
  rerere: add documentation for conflict normalization
  rerere: mark strings for translation
  rerere: wrap paths in output in sq
  rerere: lowercase error messages
  rerere: unify error messages when read_cache fails
This commit is contained in:
Junio C Hamano 2018-09-17 13:53:51 -07:00
Родитель 49f210fd52 bd7dfa543e
Коммит 39006893f9
4 изменённых файлов: 373 добавлений и 137 удалений

Просмотреть файл

@ -0,0 +1,182 @@
Rerere
======
This document describes the rerere logic.
Conflict normalization
----------------------
To ensure recorded conflict resolutions can be looked up in the rerere
database, even when branches are merged in a different order,
different branches are merged that result in the same conflict, or
when different conflict style settings are used, rerere normalizes the
conflicts before writing them to the rerere database.
Different conflict styles and branch names are normalized by stripping
the labels from the conflict markers, and removing the common ancestor
version from the `diff3` conflict style. Branches that are merged
in different order are normalized by sorting the conflict hunks. More
on each of those steps in the following sections.
Once these two normalization operations are applied, a conflict ID is
calculated based on the normalized conflict, which is later used by
rerere to look up the conflict in the rerere database.
Removing the common ancestor version
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Say we have three branches AB, AC and AC2. The common ancestor of
these branches has a file with a line containing the string "A" (for
brevity this is called "line A" in the rest of the document). In
branch AB this line is changed to "B", in AC, this line is changed to
"C", and branch AC2 is forked off of AC, after the line was changed to
"C".
Forking a branch ABAC off of branch AB and then merging AC into it, we
get a conflict like the following:
<<<<<<< HEAD
B
=======
C
>>>>>>> AC
Doing the analogous with AC2 (forking a branch ABAC2 off of branch AB
and then merging branch AC2 into it), using the diff3 conflict style,
we get a conflict like the following:
<<<<<<< HEAD
B
||||||| merged common ancestors
A
=======
C
>>>>>>> AC2
By resolving this conflict, to leave line D, the user declares:
After examining what branches AB and AC did, I believe that making
line A into line D is the best thing to do that is compatible with
what AB and AC wanted to do.
As branch AC2 refers to the same commit as AC, the above implies that
this is also compatible what AB and AC2 wanted to do.
By extension, this means that rerere should recognize that the above
conflicts are the same. To do this, the labels on the conflict
markers are stripped, and the common ancestor version is removed. The above
examples would both result in the following normalized conflict:
<<<<<<<
B
=======
C
>>>>>>>
Sorting hunks
~~~~~~~~~~~~~
As before, lets imagine that a common ancestor had a file with line A
its early part, and line X in its late part. And then four branches
are forked that do these things:
- AB: changes A to B
- AC: changes A to C
- XY: changes X to Y
- XZ: changes X to Z
Now, forking a branch ABAC off of branch AB and then merging AC into
it, and forking a branch ACAB off of branch AC and then merging AB
into it, would yield the conflict in a different order. The former
would say "A became B or C, what now?" while the latter would say "A
became C or B, what now?"
As a reminder, the act of merging AC into ABAC and resolving the
conflict to leave line D means that the user declares:
After examining what branches AB and AC did, I believe that
making line A into line D is the best thing to do that is
compatible with what AB and AC wanted to do.
So the conflict we would see when merging AB into ACAB should be
resolved the same way---it is the resolution that is in line with that
declaration.
Imagine that similarly previously a branch XYXZ was forked from XY,
and XZ was merged into it, and resolved "X became Y or Z" into "X
became W".
Now, if a branch ABXY was forked from AB and then merged XY, then ABXY
would have line B in its early part and line Y in its later part.
Such a merge would be quite clean. We can construct 4 combinations
using these four branches ((AB, AC) x (XY, XZ)).
Merging ABXY and ACXZ would make "an early A became B or C, a late X
became Y or Z" conflict, while merging ACXY and ABXZ would make "an
early A became C or B, a late X became Y or Z". We can see there are
4 combinations of ("B or C", "C or B") x ("X or Y", "Y or X").
By sorting, the conflict is given its canonical name, namely, "an
early part became B or C, a late part becames X or Y", and whenever
any of these four patterns appear, and we can get to the same conflict
and resolution that we saw earlier.
Without the sorting, we'd have to somehow find a previous resolution
from combinatorial explosion.
Conflict ID calculation
~~~~~~~~~~~~~~~~~~~~~~~
Once the conflict normalization is done, the conflict ID is calculated
as the sha1 hash of the conflict hunks appended to each other,
separated by <NUL> characters. The conflict markers are stripped out
before the sha1 is calculated. So in the example above, where we
merge branch AC which changes line A to line C, into branch AB, which
changes line A to line C, the conflict ID would be
SHA1('B<NUL>C<NUL>').
If there are multiple conflicts in one file, the sha1 is calculated
the same way with all hunks appended to each other, in the order in
which they appear in the file, separated by a <NUL> character.
Nested conflicts
~~~~~~~~~~~~~~~~
Nested conflicts are handled very similarly to "simple" conflicts.
Similar to simple conflicts, the conflict is first normalized by
stripping the labels from conflict markers, stripping the common ancestor
version, and the sorting the conflict hunks, both for the outer and the
inner conflict. This is done recursively, so any number of nested
conflicts can be handled.
The only difference is in how the conflict ID is calculated. For the
inner conflict, the conflict markers themselves are not stripped out
before calculating the sha1.
Say we have the following conflict for example:
<<<<<<< HEAD
1
=======
<<<<<<< HEAD
3
=======
2
>>>>>>> branch-2
>>>>>>> branch-3~
After stripping out the labels of the conflict markers, and sorting
the hunks, the conflict would look as follows:
<<<<<<<
1
=======
<<<<<<<
2
=======
3
>>>>>>>
>>>>>>>
and finally the conflict ID would be calculated as:
`sha1('1<NUL><<<<<<<\n3\n=======\n2\n>>>>>>><NUL>')`

Просмотреть файл

@ -75,7 +75,7 @@ int cmd_rerere(int argc, const char **argv, const char *prefix)
if (!strcmp(argv[0], "forget")) {
struct pathspec pathspec;
if (argc < 2)
warning("'git rerere forget' without paths is deprecated");
warning(_("'git rerere forget' without paths is deprecated"));
parse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD,
prefix, argv + 1);
return rerere_forget(&pathspec);
@ -107,7 +107,7 @@ int cmd_rerere(int argc, const char **argv, const char *prefix)
const char *path = merge_rr.items[i].string;
const struct rerere_id *id = merge_rr.items[i].util;
if (diff_two(rerere_path(id, "preimage"), path, path, path))
die("unable to generate diff for %s", rerere_path(id, NULL));
die(_("unable to generate diff for '%s'"), rerere_path(id, NULL));
}
} else
usage_with_options(rerere_usage, options);

259
rerere.c
Просмотреть файл

@ -213,7 +213,7 @@ static void read_rr(struct string_list *rr)
/* There has to be the hash, tab, path and then NUL */
if (buf.len < 42 || get_sha1_hex(buf.buf, sha1))
die("corrupt MERGE_RR");
die(_("corrupt MERGE_RR"));
if (buf.buf[40] != '.') {
variant = 0;
@ -222,10 +222,10 @@ static void read_rr(struct string_list *rr)
errno = 0;
variant = strtol(buf.buf + 41, &path, 10);
if (errno)
die("corrupt MERGE_RR");
die(_("corrupt MERGE_RR"));
}
if (*(path++) != '\t')
die("corrupt MERGE_RR");
die(_("corrupt MERGE_RR"));
buf.buf[40] = '\0';
id = new_rerere_id_hex(buf.buf);
id->variant = variant;
@ -260,12 +260,12 @@ static int write_rr(struct string_list *rr, int out_fd)
rr->items[i].string, 0);
if (write_in_full(out_fd, buf.buf, buf.len) < 0)
die("unable to write rerere record");
die(_("unable to write rerere record"));
strbuf_release(&buf);
}
if (commit_lock_file(&write_lock) != 0)
die("unable to write rerere record");
die(_("unable to write rerere record"));
return 0;
}
@ -303,38 +303,6 @@ static void rerere_io_putstr(const char *str, struct rerere_io *io)
ferr_puts(str, io->output, &io->wrerror);
}
/*
* Write a conflict marker to io->output (if defined).
*/
static void rerere_io_putconflict(int ch, int size, struct rerere_io *io)
{
char buf[64];
while (size) {
if (size <= sizeof(buf) - 2) {
memset(buf, ch, size);
buf[size] = '\n';
buf[size + 1] = '\0';
size = 0;
} else {
int sz = sizeof(buf) - 1;
/*
* Make sure we will not write everything out
* in this round by leaving at least 1 byte
* for the next round, giving the next round
* a chance to add the terminating LF. Yuck.
*/
if (size <= sz)
sz -= (sz - size) + 1;
memset(buf, ch, sz);
buf[sz] = '\0';
size -= sz;
}
rerere_io_putstr(buf, io);
}
}
static void rerere_io_putmem(const char *mem, size_t sz, struct rerere_io *io)
{
if (io->output)
@ -385,6 +353,71 @@ static int is_cmarker(char *buf, int marker_char, int marker_size)
return isspace(*buf);
}
static void rerere_strbuf_putconflict(struct strbuf *buf, int ch, size_t size)
{
strbuf_addchars(buf, ch, size);
strbuf_addch(buf, '\n');
}
static int handle_conflict(struct strbuf *out, struct rerere_io *io,
int marker_size, git_SHA_CTX *ctx)
{
enum {
RR_SIDE_1 = 0, RR_SIDE_2, RR_ORIGINAL
} hunk = RR_SIDE_1;
struct strbuf one = STRBUF_INIT, two = STRBUF_INIT;
struct strbuf buf = STRBUF_INIT, conflict = STRBUF_INIT;
int has_conflicts = -1;
while (!io->getline(&buf, io)) {
if (is_cmarker(buf.buf, '<', marker_size)) {
if (handle_conflict(&conflict, io, marker_size, NULL) < 0)
break;
if (hunk == RR_SIDE_1)
strbuf_addbuf(&one, &conflict);
else
strbuf_addbuf(&two, &conflict);
strbuf_release(&conflict);
} else if (is_cmarker(buf.buf, '|', marker_size)) {
if (hunk != RR_SIDE_1)
break;
hunk = RR_ORIGINAL;
} else if (is_cmarker(buf.buf, '=', marker_size)) {
if (hunk != RR_SIDE_1 && hunk != RR_ORIGINAL)
break;
hunk = RR_SIDE_2;
} else if (is_cmarker(buf.buf, '>', marker_size)) {
if (hunk != RR_SIDE_2)
break;
if (strbuf_cmp(&one, &two) > 0)
strbuf_swap(&one, &two);
has_conflicts = 1;
rerere_strbuf_putconflict(out, '<', marker_size);
strbuf_addbuf(out, &one);
rerere_strbuf_putconflict(out, '=', marker_size);
strbuf_addbuf(out, &two);
rerere_strbuf_putconflict(out, '>', marker_size);
if (ctx) {
git_SHA1_Update(ctx, one.buf ? one.buf : "",
one.len + 1);
git_SHA1_Update(ctx, two.buf ? two.buf : "",
two.len + 1);
}
break;
} else if (hunk == RR_SIDE_1)
strbuf_addbuf(&one, &buf);
else if (hunk == RR_ORIGINAL)
; /* discard */
else if (hunk == RR_SIDE_2)
strbuf_addbuf(&two, &buf);
}
strbuf_release(&one);
strbuf_release(&two);
strbuf_release(&buf);
return has_conflicts;
}
/*
* Read contents a file with conflicts, normalize the conflicts
* by (1) discarding the common ancestor version in diff3-style,
@ -394,80 +427,35 @@ static int is_cmarker(char *buf, int marker_char, int marker_size)
* one side of the conflict, NUL, the other side of the conflict,
* and NUL concatenated together.
*
* Return the number of conflict hunks found.
*
* NEEDSWORK: the logic and theory of operation behind this conflict
* normalization may deserve to be documented somewhere, perhaps in
* Documentation/technical/rerere.txt.
* Return 1 if conflict hunks are found, 0 if there are no conflict
* hunks and -1 if an error occured.
*/
static int handle_path(unsigned char *sha1, struct rerere_io *io, int marker_size)
{
git_SHA_CTX ctx;
int hunk_no = 0;
enum {
RR_CONTEXT = 0, RR_SIDE_1, RR_SIDE_2, RR_ORIGINAL
} hunk = RR_CONTEXT;
struct strbuf one = STRBUF_INIT, two = STRBUF_INIT;
struct strbuf buf = STRBUF_INIT;
struct strbuf buf = STRBUF_INIT, out = STRBUF_INIT;
int has_conflicts = 0;
if (sha1)
git_SHA1_Init(&ctx);
while (!io->getline(&buf, io)) {
if (is_cmarker(buf.buf, '<', marker_size)) {
if (hunk != RR_CONTEXT)
goto bad;
hunk = RR_SIDE_1;
} else if (is_cmarker(buf.buf, '|', marker_size)) {
if (hunk != RR_SIDE_1)
goto bad;
hunk = RR_ORIGINAL;
} else if (is_cmarker(buf.buf, '=', marker_size)) {
if (hunk != RR_SIDE_1 && hunk != RR_ORIGINAL)
goto bad;
hunk = RR_SIDE_2;
} else if (is_cmarker(buf.buf, '>', marker_size)) {
if (hunk != RR_SIDE_2)
goto bad;
if (strbuf_cmp(&one, &two) > 0)
strbuf_swap(&one, &two);
hunk_no++;
hunk = RR_CONTEXT;
rerere_io_putconflict('<', marker_size, io);
rerere_io_putmem(one.buf, one.len, io);
rerere_io_putconflict('=', marker_size, io);
rerere_io_putmem(two.buf, two.len, io);
rerere_io_putconflict('>', marker_size, io);
if (sha1) {
git_SHA1_Update(&ctx, one.buf ? one.buf : "",
one.len + 1);
git_SHA1_Update(&ctx, two.buf ? two.buf : "",
two.len + 1);
}
strbuf_reset(&one);
strbuf_reset(&two);
} else if (hunk == RR_SIDE_1)
strbuf_addbuf(&one, &buf);
else if (hunk == RR_ORIGINAL)
; /* discard */
else if (hunk == RR_SIDE_2)
strbuf_addbuf(&two, &buf);
else
has_conflicts = handle_conflict(&out, io, marker_size,
sha1 ? &ctx : NULL);
if (has_conflicts < 0)
break;
rerere_io_putmem(out.buf, out.len, io);
strbuf_reset(&out);
} else
rerere_io_putstr(buf.buf, io);
continue;
bad:
hunk = 99; /* force error exit */
break;
}
strbuf_release(&one);
strbuf_release(&two);
strbuf_release(&buf);
strbuf_release(&out);
if (sha1)
git_SHA1_Final(sha1, &ctx);
if (hunk != RR_CONTEXT)
return -1;
return hunk_no;
return has_conflicts;
}
/*
@ -476,7 +464,7 @@ static int handle_path(unsigned char *sha1, struct rerere_io *io, int marker_siz
*/
static int handle_file(const char *path, unsigned char *sha1, const char *output)
{
int hunk_no = 0;
int has_conflicts = 0;
struct rerere_io_file io;
int marker_size = ll_merge_marker_size(path);
@ -485,34 +473,34 @@ static int handle_file(const char *path, unsigned char *sha1, const char *output
io.input = fopen(path, "r");
io.io.wrerror = 0;
if (!io.input)
return error_errno("Could not open %s", path);
return error_errno(_("could not open '%s'"), path);
if (output) {
io.io.output = fopen(output, "w");
if (!io.io.output) {
error_errno("Could not write %s", output);
error_errno(_("could not write '%s'"), output);
fclose(io.input);
return -1;
}
}
hunk_no = handle_path(sha1, (struct rerere_io *)&io, marker_size);
has_conflicts = handle_path(sha1, (struct rerere_io *)&io, marker_size);
fclose(io.input);
if (io.io.wrerror)
error("There were errors while writing %s (%s)",
error(_("there were errors while writing '%s' (%s)"),
path, strerror(io.io.wrerror));
if (io.io.output && fclose(io.io.output))
io.io.wrerror = error_errno("Failed to flush %s", path);
io.io.wrerror = error_errno(_("failed to flush '%s'"), path);
if (hunk_no < 0) {
if (has_conflicts < 0) {
if (output)
unlink_or_warn(output);
return error("Could not parse conflict hunks in %s", path);
return error(_("could not parse conflict hunks in '%s'"), path);
}
if (io.io.wrerror)
return -1;
return hunk_no;
return has_conflicts;
}
/*
@ -569,7 +557,7 @@ static int find_conflict(struct string_list *conflict)
{
int i;
if (read_cache() < 0)
return error("Could not read index");
return error(_("index file corrupt"));
for (i = 0; i < active_nr;) {
int conflict_type;
@ -602,7 +590,7 @@ int rerere_remaining(struct string_list *merge_rr)
if (setup_rerere(merge_rr, RERERE_READONLY))
return 0;
if (read_cache() < 0)
return error("Could not read index");
return error(_("index file corrupt"));
for (i = 0; i < active_nr;) {
int conflict_type;
@ -685,17 +673,17 @@ static int merge(const struct rerere_id *id, const char *path)
* Mark that "postimage" was used to help gc.
*/
if (utime(rerere_path(id, "postimage"), NULL) < 0)
warning_errno("failed utime() on %s",
warning_errno(_("failed utime() on '%s'"),
rerere_path(id, "postimage"));
/* Update "path" with the resolution */
f = fopen(path, "w");
if (!f)
return error_errno("Could not open %s", path);
return error_errno(_("could not open '%s'"), path);
if (fwrite(result.ptr, result.size, 1, f) != 1)
error_errno("Could not write %s", path);
error_errno(_("could not write '%s'"), path);
if (fclose(f))
return error_errno("Writing %s failed", path);
return error_errno(_("writing '%s' failed"), path);
out:
free(cur.ptr);
@ -715,13 +703,13 @@ static void update_paths(struct string_list *update)
struct string_list_item *item = &update->items[i];
if (add_file_to_cache(item->string, 0))
exit(128);
fprintf(stderr, "Staged '%s' using previous resolution.\n",
fprintf_ln(stderr, _("Staged '%s' using previous resolution."),
item->string);
}
if (write_locked_index(&the_index, &index_lock,
COMMIT_LOCK | SKIP_IF_UNCHANGED))
die("Unable to write new index file");
die(_("unable to write new index file"));
}
static void remove_variant(struct rerere_id *id)
@ -753,7 +741,7 @@ static void do_rerere_one_path(struct string_list_item *rr_item,
if (!handle_file(path, NULL, NULL)) {
copy_file(rerere_path(id, "postimage"), path, 0666);
id->collection->status[variant] |= RR_HAS_POSTIMAGE;
fprintf(stderr, "Recorded resolution for '%s'.\n", path);
fprintf_ln(stderr, _("Recorded resolution for '%s'."), path);
free_rerere_id(rr_item);
rr_item->util = NULL;
return;
@ -787,9 +775,9 @@ static void do_rerere_one_path(struct string_list_item *rr_item,
if (rerere_autoupdate)
string_list_insert(update, path);
else
fprintf(stderr,
"Resolved '%s' using previous resolution.\n",
path);
fprintf_ln(stderr,
_("Resolved '%s' using previous resolution."),
path);
free_rerere_id(rr_item);
rr_item->util = NULL;
return;
@ -803,11 +791,11 @@ static void do_rerere_one_path(struct string_list_item *rr_item,
if (id->collection->status[variant] & RR_HAS_POSTIMAGE) {
const char *path = rerere_path(id, "postimage");
if (unlink(path))
die_errno("cannot unlink stray '%s'", path);
die_errno(_("cannot unlink stray '%s'"), path);
id->collection->status[variant] &= ~RR_HAS_POSTIMAGE;
}
id->collection->status[variant] |= RR_HAS_PREIMAGE;
fprintf(stderr, "Recorded preimage for '%s'\n", path);
fprintf_ln(stderr, _("Recorded preimage for '%s'"), path);
}
static int do_plain_rerere(struct string_list *rr, int fd)
@ -830,15 +818,16 @@ static int do_plain_rerere(struct string_list *rr, int fd)
const char *path = conflict.items[i].string;
int ret;
if (string_list_has_string(rr, path))
continue;
/*
* Ask handle_file() to scan and assign a
* conflict ID. No need to write anything out
* yet.
*/
ret = handle_file(path, sha1, NULL);
if (ret != 0 && string_list_has_string(rr, path)) {
remove_variant(string_list_lookup(rr, path)->util);
string_list_remove(rr, path, 1);
}
if (ret < 1)
continue;
@ -879,7 +868,7 @@ static int is_rerere_enabled(void)
return rr_cache_exists;
if (!rr_cache_exists && mkdir_in_gitdir(git_path_rr_cache()))
die("Could not create directory %s", git_path_rr_cache());
die(_("could not create directory '%s'"), git_path_rr_cache());
return 1;
}
@ -958,7 +947,7 @@ static int handle_cache(const char *path, unsigned char *sha1, const char *outpu
mmfile_t mmfile[3] = {{NULL}};
mmbuffer_t result = {NULL, 0};
const struct cache_entry *ce;
int pos, len, i, hunk_no;
int pos, len, i, has_conflicts;
struct rerere_io_mem io;
int marker_size = ll_merge_marker_size(path);
@ -1012,11 +1001,11 @@ static int handle_cache(const char *path, unsigned char *sha1, const char *outpu
* Grab the conflict ID and optionally write the original
* contents with conflict markers out.
*/
hunk_no = handle_path(sha1, (struct rerere_io *)&io, marker_size);
has_conflicts = handle_path(sha1, (struct rerere_io *)&io, marker_size);
strbuf_release(&io.input);
if (io.io.output)
fclose(io.io.output);
return hunk_no;
return has_conflicts;
}
static int rerere_forget_one_path(const char *path, struct string_list *rr)
@ -1033,7 +1022,7 @@ static int rerere_forget_one_path(const char *path, struct string_list *rr)
*/
ret = handle_cache(path, sha1, NULL);
if (ret < 1)
return error("Could not parse conflict hunks in '%s'", path);
return error(_("could not parse conflict hunks in '%s'"), path);
/* Nuke the recorded resolution for the conflict */
id = new_rerere_id(sha1);
@ -1051,7 +1040,7 @@ static int rerere_forget_one_path(const char *path, struct string_list *rr)
handle_cache(path, sha1, rerere_path(id, "thisimage"));
if (read_mmfile(&cur, rerere_path(id, "thisimage"))) {
free(cur.ptr);
error("Failed to update conflicted state in '%s'", path);
error(_("failed to update conflicted state in '%s'"), path);
goto fail_exit;
}
cleanly_resolved = !try_merge(id, path, &cur, &result);
@ -1062,16 +1051,16 @@ static int rerere_forget_one_path(const char *path, struct string_list *rr)
}
if (id->collection->status_nr <= id->variant) {
error("no remembered resolution for '%s'", path);
error(_("no remembered resolution for '%s'"), path);
goto fail_exit;
}
filename = rerere_path(id, "postimage");
if (unlink(filename)) {
if (errno == ENOENT)
error("no remembered resolution for %s", path);
error(_("no remembered resolution for '%s'"), path);
else
error_errno("cannot unlink %s", filename);
error_errno(_("cannot unlink '%s'"), filename);
goto fail_exit;
}
@ -1081,7 +1070,7 @@ static int rerere_forget_one_path(const char *path, struct string_list *rr)
* the postimage.
*/
handle_cache(path, sha1, rerere_path(id, "preimage"));
fprintf(stderr, "Updated preimage for '%s'\n", path);
fprintf_ln(stderr, _("Updated preimage for '%s'"), path);
/*
* And remember that we can record resolution for this
@ -1090,7 +1079,7 @@ static int rerere_forget_one_path(const char *path, struct string_list *rr)
item = string_list_insert(rr, path);
free_rerere_id(item);
item->util = id;
fprintf(stderr, "Forgot resolution for %s\n", path);
fprintf(stderr, _("Forgot resolution for '%s'\n"), path);
return 0;
fail_exit:
@ -1105,7 +1094,7 @@ int rerere_forget(struct pathspec *pathspec)
struct string_list merge_rr = STRING_LIST_INIT_DUP;
if (read_cache() < 0)
return error("Could not read index");
return error(_("index file corrupt"));
fd = setup_rerere(&merge_rr, RERERE_NOAUTOUPDATE);
if (fd < 0)
@ -1193,7 +1182,7 @@ void rerere_gc(struct string_list *rr)
git_config(git_default_config, NULL);
dir = opendir(git_path("rr-cache"));
if (!dir)
die_errno("unable to open rr-cache directory");
die_errno(_("unable to open rr-cache directory"));
/* Collect stale conflict IDs ... */
while ((e = readdir(dir))) {
struct rerere_dir *rr_dir;

Просмотреть файл

@ -577,4 +577,69 @@ test_expect_success 'multiple identical conflicts' '
count_pre_post 0 0
'
test_expect_success 'rerere with unexpected conflict markers does not crash' '
git reset --hard &&
git checkout -b branch-1 master &&
echo "bar" >test &&
git add test &&
git commit -q -m two &&
git reset --hard &&
git checkout -b branch-2 master &&
echo "foo" >test &&
git add test &&
git commit -q -a -m one &&
test_must_fail git merge branch-1 &&
echo "<<<<<<< a" >test &&
git rerere &&
git rerere clear
'
test_expect_success 'rerere with inner conflict markers' '
git reset --hard &&
git checkout -b A master &&
echo "bar" >test &&
git add test &&
git commit -q -m two &&
echo "baz" >test &&
git add test &&
git commit -q -m three &&
git reset --hard &&
git checkout -b B master &&
echo "foo" >test &&
git add test &&
git commit -q -a -m one &&
test_must_fail git merge A~ &&
git add test &&
git commit -q -m "will solve conflicts later" &&
test_must_fail git merge A &&
echo "resolved" >test &&
git add test &&
git commit -q -m "solved conflict" &&
echo "resolved" >expect &&
git reset --hard HEAD~~ &&
test_must_fail git merge A~ &&
git add test &&
git commit -q -m "will solve conflicts later" &&
test_must_fail git merge A &&
cat test >actual &&
test_cmp expect actual &&
git add test &&
git commit -m "rerere solved conflict" &&
git reset --hard HEAD~ &&
test_must_fail git merge A &&
cat test >actual &&
test_cmp expect actual
'
test_done