Browse Source
* jc/cache-unmerge: rerere forget path: forget recorded resolution rerere: refactor rerere logic to make it independent from I/O rerere: remove silly 1024-byte line limit resolve-undo: teach "update-index --unresolve" to use resolve-undo info resolve-undo: "checkout -m path" uses resolve-undo information resolve-undo: allow plumbing to clear the information resolve-undo: basic tests resolve-undo: record resolved conflicts in a new index extension section builtin-merge.c: use standard active_cache macros Conflicts: builtin-ls-files.c builtin-merge.c builtin-rerere.cmaint

14 changed files with 652 additions and 47 deletions
@ -0,0 +1,176 @@ |
|||||||
|
#include "cache.h" |
||||||
|
#include "dir.h" |
||||||
|
#include "resolve-undo.h" |
||||||
|
#include "string-list.h" |
||||||
|
|
||||||
|
/* The only error case is to run out of memory in string-list */ |
||||||
|
void record_resolve_undo(struct index_state *istate, struct cache_entry *ce) |
||||||
|
{ |
||||||
|
struct string_list_item *lost; |
||||||
|
struct resolve_undo_info *ui; |
||||||
|
struct string_list *resolve_undo; |
||||||
|
int stage = ce_stage(ce); |
||||||
|
|
||||||
|
if (!stage) |
||||||
|
return; |
||||||
|
|
||||||
|
if (!istate->resolve_undo) { |
||||||
|
resolve_undo = xcalloc(1, sizeof(*resolve_undo)); |
||||||
|
resolve_undo->strdup_strings = 1; |
||||||
|
istate->resolve_undo = resolve_undo; |
||||||
|
} |
||||||
|
resolve_undo = istate->resolve_undo; |
||||||
|
lost = string_list_insert(ce->name, resolve_undo); |
||||||
|
if (!lost->util) |
||||||
|
lost->util = xcalloc(1, sizeof(*ui)); |
||||||
|
ui = lost->util; |
||||||
|
hashcpy(ui->sha1[stage - 1], ce->sha1); |
||||||
|
ui->mode[stage - 1] = ce->ce_mode; |
||||||
|
} |
||||||
|
|
||||||
|
static int write_one(struct string_list_item *item, void *cbdata) |
||||||
|
{ |
||||||
|
struct strbuf *sb = cbdata; |
||||||
|
struct resolve_undo_info *ui = item->util; |
||||||
|
int i; |
||||||
|
|
||||||
|
if (!ui) |
||||||
|
return 0; |
||||||
|
strbuf_addstr(sb, item->string); |
||||||
|
strbuf_addch(sb, 0); |
||||||
|
for (i = 0; i < 3; i++) |
||||||
|
strbuf_addf(sb, "%o%c", ui->mode[i], 0); |
||||||
|
for (i = 0; i < 3; i++) { |
||||||
|
if (!ui->mode[i]) |
||||||
|
continue; |
||||||
|
strbuf_add(sb, ui->sha1[i], 20); |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
void resolve_undo_write(struct strbuf *sb, struct string_list *resolve_undo) |
||||||
|
{ |
||||||
|
for_each_string_list(write_one, resolve_undo, sb); |
||||||
|
} |
||||||
|
|
||||||
|
struct string_list *resolve_undo_read(void *data, unsigned long size) |
||||||
|
{ |
||||||
|
struct string_list *resolve_undo; |
||||||
|
size_t len; |
||||||
|
char *endptr; |
||||||
|
int i; |
||||||
|
|
||||||
|
resolve_undo = xcalloc(1, sizeof(*resolve_undo)); |
||||||
|
resolve_undo->strdup_strings = 1; |
||||||
|
|
||||||
|
while (size) { |
||||||
|
struct string_list_item *lost; |
||||||
|
struct resolve_undo_info *ui; |
||||||
|
|
||||||
|
len = strlen(data) + 1; |
||||||
|
if (size <= len) |
||||||
|
goto error; |
||||||
|
lost = string_list_insert(data, resolve_undo); |
||||||
|
if (!lost->util) |
||||||
|
lost->util = xcalloc(1, sizeof(*ui)); |
||||||
|
ui = lost->util; |
||||||
|
size -= len; |
||||||
|
data += len; |
||||||
|
|
||||||
|
for (i = 0; i < 3; i++) { |
||||||
|
ui->mode[i] = strtoul(data, &endptr, 8); |
||||||
|
if (!endptr || endptr == data || *endptr) |
||||||
|
goto error; |
||||||
|
len = (endptr + 1) - (char*)data; |
||||||
|
if (size <= len) |
||||||
|
goto error; |
||||||
|
size -= len; |
||||||
|
data += len; |
||||||
|
} |
||||||
|
|
||||||
|
for (i = 0; i < 3; i++) { |
||||||
|
if (!ui->mode[i]) |
||||||
|
continue; |
||||||
|
if (size < 20) |
||||||
|
goto error; |
||||||
|
hashcpy(ui->sha1[i], data); |
||||||
|
size -= 20; |
||||||
|
data += 20; |
||||||
|
} |
||||||
|
} |
||||||
|
return resolve_undo; |
||||||
|
|
||||||
|
error: |
||||||
|
string_list_clear(resolve_undo, 1); |
||||||
|
error("Index records invalid resolve-undo information"); |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
void resolve_undo_clear_index(struct index_state *istate) |
||||||
|
{ |
||||||
|
struct string_list *resolve_undo = istate->resolve_undo; |
||||||
|
if (!resolve_undo) |
||||||
|
return; |
||||||
|
string_list_clear(resolve_undo, 1); |
||||||
|
free(resolve_undo); |
||||||
|
istate->resolve_undo = NULL; |
||||||
|
istate->cache_changed = 1; |
||||||
|
} |
||||||
|
|
||||||
|
int unmerge_index_entry_at(struct index_state *istate, int pos) |
||||||
|
{ |
||||||
|
struct cache_entry *ce; |
||||||
|
struct string_list_item *item; |
||||||
|
struct resolve_undo_info *ru; |
||||||
|
int i, err = 0; |
||||||
|
|
||||||
|
if (!istate->resolve_undo) |
||||||
|
return pos; |
||||||
|
|
||||||
|
ce = istate->cache[pos]; |
||||||
|
if (ce_stage(ce)) { |
||||||
|
/* already unmerged */ |
||||||
|
while ((pos < istate->cache_nr) && |
||||||
|
! strcmp(istate->cache[pos]->name, ce->name)) |
||||||
|
pos++; |
||||||
|
return pos - 1; /* return the last entry processed */ |
||||||
|
} |
||||||
|
item = string_list_lookup(ce->name, istate->resolve_undo); |
||||||
|
if (!item) |
||||||
|
return pos; |
||||||
|
ru = item->util; |
||||||
|
if (!ru) |
||||||
|
return pos; |
||||||
|
remove_index_entry_at(istate, pos); |
||||||
|
for (i = 0; i < 3; i++) { |
||||||
|
struct cache_entry *nce; |
||||||
|
if (!ru->mode[i]) |
||||||
|
continue; |
||||||
|
nce = make_cache_entry(ru->mode[i], ru->sha1[i], |
||||||
|
ce->name, i + 1, 0); |
||||||
|
if (add_index_entry(istate, nce, ADD_CACHE_OK_TO_ADD)) { |
||||||
|
err = 1; |
||||||
|
error("cannot unmerge '%s'", ce->name); |
||||||
|
} |
||||||
|
} |
||||||
|
if (err) |
||||||
|
return pos; |
||||||
|
free(ru); |
||||||
|
item->util = NULL; |
||||||
|
return unmerge_index_entry_at(istate, pos); |
||||||
|
} |
||||||
|
|
||||||
|
void unmerge_index(struct index_state *istate, const char **pathspec) |
||||||
|
{ |
||||||
|
int i; |
||||||
|
|
||||||
|
if (!istate->resolve_undo) |
||||||
|
return; |
||||||
|
|
||||||
|
for (i = 0; i < istate->cache_nr; i++) { |
||||||
|
struct cache_entry *ce = istate->cache[i]; |
||||||
|
if (!match_pathspec(pathspec, ce->name, ce_namelen(ce), 0, NULL)) |
||||||
|
continue; |
||||||
|
i = unmerge_index_entry_at(istate, i); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
#ifndef RESOLVE_UNDO_H |
||||||
|
#define RESOLVE_UNDO_H |
||||||
|
|
||||||
|
struct resolve_undo_info { |
||||||
|
unsigned int mode[3]; |
||||||
|
unsigned char sha1[3][20]; |
||||||
|
}; |
||||||
|
|
||||||
|
extern void record_resolve_undo(struct index_state *, struct cache_entry *); |
||||||
|
extern void resolve_undo_write(struct strbuf *, struct string_list *); |
||||||
|
extern struct string_list *resolve_undo_read(void *, unsigned long); |
||||||
|
extern void resolve_undo_clear_index(struct index_state *); |
||||||
|
extern int unmerge_index_entry_at(struct index_state *, int); |
||||||
|
extern void unmerge_index(struct index_state *, const char **); |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,143 @@ |
|||||||
|
#!/bin/sh |
||||||
|
|
||||||
|
test_description='undoing resolution' |
||||||
|
|
||||||
|
. ./test-lib.sh |
||||||
|
|
||||||
|
check_resolve_undo () { |
||||||
|
msg=$1 |
||||||
|
shift |
||||||
|
while case $# in |
||||||
|
0) break ;; |
||||||
|
1|2|3) die "Bug in check-resolve-undo test" ;; |
||||||
|
esac |
||||||
|
do |
||||||
|
path=$1 |
||||||
|
shift |
||||||
|
for stage in 1 2 3 |
||||||
|
do |
||||||
|
sha1=$1 |
||||||
|
shift |
||||||
|
case "$sha1" in |
||||||
|
'') continue ;; |
||||||
|
esac |
||||||
|
sha1=$(git rev-parse --verify "$sha1") |
||||||
|
printf "100644 %s %s\t%s\n" $sha1 $stage $path |
||||||
|
done |
||||||
|
done >"$msg.expect" && |
||||||
|
git ls-files --resolve-undo >"$msg.actual" && |
||||||
|
test_cmp "$msg.expect" "$msg.actual" |
||||||
|
} |
||||||
|
|
||||||
|
prime_resolve_undo () { |
||||||
|
git reset --hard && |
||||||
|
git checkout second^0 && |
||||||
|
test_tick && |
||||||
|
test_must_fail git merge third^0 && |
||||||
|
echo merge does not leave anything && |
||||||
|
check_resolve_undo empty && |
||||||
|
echo different >file && |
||||||
|
git add file && |
||||||
|
echo resolving records && |
||||||
|
check_resolve_undo recorded file initial:file second:file third:file |
||||||
|
} |
||||||
|
|
||||||
|
test_expect_success setup ' |
||||||
|
test_commit initial file first && |
||||||
|
git branch side && |
||||||
|
git branch another && |
||||||
|
test_commit second file second && |
||||||
|
git checkout side && |
||||||
|
test_commit third file third && |
||||||
|
git checkout another && |
||||||
|
test_commit fourth file fourth && |
||||||
|
git checkout master |
||||||
|
' |
||||||
|
|
||||||
|
test_expect_success 'add records switch clears' ' |
||||||
|
prime_resolve_undo && |
||||||
|
test_tick && |
||||||
|
git commit -m merged && |
||||||
|
echo committing keeps && |
||||||
|
check_resolve_undo kept file initial:file second:file third:file && |
||||||
|
git checkout second^0 && |
||||||
|
echo switching clears && |
||||||
|
check_resolve_undo cleared |
||||||
|
' |
||||||
|
|
||||||
|
test_expect_success 'rm records reset clears' ' |
||||||
|
prime_resolve_undo && |
||||||
|
test_tick && |
||||||
|
git commit -m merged && |
||||||
|
echo committing keeps && |
||||||
|
check_resolve_undo kept file initial:file second:file third:file && |
||||||
|
|
||||||
|
echo merge clears upfront && |
||||||
|
test_must_fail git merge fourth^0 && |
||||||
|
check_resolve_undo nuked && |
||||||
|
|
||||||
|
git rm -f file && |
||||||
|
echo resolving records && |
||||||
|
check_resolve_undo recorded file initial:file HEAD:file fourth:file && |
||||||
|
|
||||||
|
git reset --hard && |
||||||
|
echo resetting discards && |
||||||
|
check_resolve_undo discarded |
||||||
|
' |
||||||
|
|
||||||
|
test_expect_success 'plumbing clears' ' |
||||||
|
prime_resolve_undo && |
||||||
|
test_tick && |
||||||
|
git commit -m merged && |
||||||
|
echo committing keeps && |
||||||
|
check_resolve_undo kept file initial:file second:file third:file && |
||||||
|
|
||||||
|
echo plumbing clear && |
||||||
|
git update-index --clear-resolve-undo && |
||||||
|
check_resolve_undo cleared |
||||||
|
' |
||||||
|
|
||||||
|
test_expect_success 'add records checkout -m undoes' ' |
||||||
|
prime_resolve_undo && |
||||||
|
git diff HEAD && |
||||||
|
git checkout --conflict=merge file && |
||||||
|
echo checkout used the record and removed it && |
||||||
|
check_resolve_undo removed && |
||||||
|
echo the index and the work tree is unmerged again && |
||||||
|
git diff >actual && |
||||||
|
grep "^++<<<<<<<" actual |
||||||
|
' |
||||||
|
|
||||||
|
test_expect_success 'unmerge with plumbing' ' |
||||||
|
prime_resolve_undo && |
||||||
|
git update-index --unresolve file && |
||||||
|
git ls-files -u >actual && |
||||||
|
test $(wc -l <actual) = 3 |
||||||
|
' |
||||||
|
|
||||||
|
test_expect_success 'rerere and rerere --forget' ' |
||||||
|
mkdir .git/rr-cache && |
||||||
|
prime_resolve_undo && |
||||||
|
echo record the resolution && |
||||||
|
git rerere && |
||||||
|
rerere_id=$(cd .git/rr-cache && echo */postimage) && |
||||||
|
rerere_id=${rerere_id%/postimage} && |
||||||
|
test -f .git/rr-cache/$rerere_id/postimage && |
||||||
|
git checkout -m file && |
||||||
|
echo resurrect the conflict && |
||||||
|
grep "^=======" file && |
||||||
|
echo reresolve the conflict && |
||||||
|
git rerere && |
||||||
|
test "z$(cat file)" = zdifferent && |
||||||
|
echo register the resolution again && |
||||||
|
git add file && |
||||||
|
check_resolve_undo kept file initial:file second:file third:file && |
||||||
|
test -z "$(git ls-files -u)" && |
||||||
|
git rerere forget file && |
||||||
|
! test -f .git/rr-cache/$rerere_id/postimage && |
||||||
|
tr "\0" "\n" <.git/MERGE_RR >actual && |
||||||
|
echo "$rerere_id file" >expect && |
||||||
|
test_cmp expect actual |
||||||
|
' |
||||||
|
|
||||||
|
test_done |
Loading…
Reference in new issue