Browse Source
This initial implementation of 'git notes merge' only handles the trivial merge cases (i.e. where the merge is either a no-op, or a fast-forward). The patch includes testcases for these trivial merge cases. Future patches will extend the functionality of 'git notes merge'. This patch has been improved by the following contributions: - Stephen Boyd: Simplify argc logic - Stephen Boyd: Use test_commit - Ævar Arnfjörð Bjarmason: Don't use C99 comments. - Jonathan Nieder: Add constants for common verbosity values - Jonathan Nieder: Use trace_printf(...) instead of OUTPUT(o, 5, ...) - Jonathan Nieder: Remove extraneous show() function - Jonathan Nieder: Clarify handling of empty/missing notes ref in notes_merge() - Junio C Hamano: fixup minor style issues Thanks-to: Stephen Boyd <bebarino@gmail.com> Thanks-to: Ævar Arnfjörð Bjarmason <avarab@gmail.com> Thanks-to: Jonathan Nieder <jrnieder@gmail.com> Thanks-to: Junio C Hamano <gitster@pobox.com> Signed-off-by: Johan Herland <johan@herland.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>maint
Johan Herland
14 years ago
committed by
Junio C Hamano
5 changed files with 392 additions and 0 deletions
@ -0,0 +1,120 @@
@@ -0,0 +1,120 @@
|
||||
#include "cache.h" |
||||
#include "commit.h" |
||||
#include "refs.h" |
||||
#include "notes-merge.h" |
||||
|
||||
void init_notes_merge_options(struct notes_merge_options *o) |
||||
{ |
||||
memset(o, 0, sizeof(struct notes_merge_options)); |
||||
o->verbosity = NOTES_MERGE_VERBOSITY_DEFAULT; |
||||
} |
||||
|
||||
#define OUTPUT(o, v, ...) \ |
||||
do { \ |
||||
if ((o)->verbosity >= (v)) { \ |
||||
printf(__VA_ARGS__); \ |
||||
puts(""); \ |
||||
} \ |
||||
} while (0) |
||||
|
||||
int notes_merge(struct notes_merge_options *o, |
||||
unsigned char *result_sha1) |
||||
{ |
||||
unsigned char local_sha1[20], remote_sha1[20]; |
||||
struct commit *local, *remote; |
||||
struct commit_list *bases = NULL; |
||||
const unsigned char *base_sha1; |
||||
int result = 0; |
||||
|
||||
assert(o->local_ref && o->remote_ref); |
||||
hashclr(result_sha1); |
||||
|
||||
trace_printf("notes_merge(o->local_ref = %s, o->remote_ref = %s)\n", |
||||
o->local_ref, o->remote_ref); |
||||
|
||||
/* Dereference o->local_ref into local_sha1 */ |
||||
if (!resolve_ref(o->local_ref, local_sha1, 0, NULL)) |
||||
die("Failed to resolve local notes ref '%s'", o->local_ref); |
||||
else if (!check_ref_format(o->local_ref) && is_null_sha1(local_sha1)) |
||||
local = NULL; /* local_sha1 == null_sha1 indicates unborn ref */ |
||||
else if (!(local = lookup_commit_reference(local_sha1))) |
||||
die("Could not parse local commit %s (%s)", |
||||
sha1_to_hex(local_sha1), o->local_ref); |
||||
trace_printf("\tlocal commit: %.7s\n", sha1_to_hex(local_sha1)); |
||||
|
||||
/* Dereference o->remote_ref into remote_sha1 */ |
||||
if (get_sha1(o->remote_ref, remote_sha1)) { |
||||
/* |
||||
* Failed to get remote_sha1. If o->remote_ref looks like an |
||||
* unborn ref, perform the merge using an empty notes tree. |
||||
*/ |
||||
if (!check_ref_format(o->remote_ref)) { |
||||
hashclr(remote_sha1); |
||||
remote = NULL; |
||||
} else { |
||||
die("Failed to resolve remote notes ref '%s'", |
||||
o->remote_ref); |
||||
} |
||||
} else if (!(remote = lookup_commit_reference(remote_sha1))) { |
||||
die("Could not parse remote commit %s (%s)", |
||||
sha1_to_hex(remote_sha1), o->remote_ref); |
||||
} |
||||
trace_printf("\tremote commit: %.7s\n", sha1_to_hex(remote_sha1)); |
||||
|
||||
if (!local && !remote) |
||||
die("Cannot merge empty notes ref (%s) into empty notes ref " |
||||
"(%s)", o->remote_ref, o->local_ref); |
||||
if (!local) { |
||||
/* result == remote commit */ |
||||
hashcpy(result_sha1, remote_sha1); |
||||
goto found_result; |
||||
} |
||||
if (!remote) { |
||||
/* result == local commit */ |
||||
hashcpy(result_sha1, local_sha1); |
||||
goto found_result; |
||||
} |
||||
assert(local && remote); |
||||
|
||||
/* Find merge bases */ |
||||
bases = get_merge_bases(local, remote, 1); |
||||
if (!bases) { |
||||
base_sha1 = null_sha1; |
||||
OUTPUT(o, 4, "No merge base found; doing history-less merge"); |
||||
} else if (!bases->next) { |
||||
base_sha1 = bases->item->object.sha1; |
||||
OUTPUT(o, 4, "One merge base found (%.7s)", |
||||
sha1_to_hex(base_sha1)); |
||||
} else { |
||||
/* TODO: How to handle multiple merge-bases? */ |
||||
base_sha1 = bases->item->object.sha1; |
||||
OUTPUT(o, 3, "Multiple merge bases found. Using the first " |
||||
"(%.7s)", sha1_to_hex(base_sha1)); |
||||
} |
||||
|
||||
OUTPUT(o, 4, "Merging remote commit %.7s into local commit %.7s with " |
||||
"merge-base %.7s", sha1_to_hex(remote->object.sha1), |
||||
sha1_to_hex(local->object.sha1), sha1_to_hex(base_sha1)); |
||||
|
||||
if (!hashcmp(remote->object.sha1, base_sha1)) { |
||||
/* Already merged; result == local commit */ |
||||
OUTPUT(o, 2, "Already up-to-date!"); |
||||
hashcpy(result_sha1, local->object.sha1); |
||||
goto found_result; |
||||
} |
||||
if (!hashcmp(local->object.sha1, base_sha1)) { |
||||
/* Fast-forward; result == remote commit */ |
||||
OUTPUT(o, 2, "Fast-forward"); |
||||
hashcpy(result_sha1, remote->object.sha1); |
||||
goto found_result; |
||||
} |
||||
|
||||
/* TODO: */ |
||||
result = error("notes_merge() cannot yet handle real merges."); |
||||
|
||||
found_result: |
||||
free_commit_list(bases); |
||||
trace_printf("notes_merge(): result = %i, result_sha1 = %.7s\n", |
||||
result, sha1_to_hex(result_sha1)); |
||||
return result; |
||||
} |
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
#ifndef NOTES_MERGE_H |
||||
#define NOTES_MERGE_H |
||||
|
||||
enum notes_merge_verbosity { |
||||
NOTES_MERGE_VERBOSITY_DEFAULT = 2, |
||||
NOTES_MERGE_VERBOSITY_MAX = 5 |
||||
}; |
||||
|
||||
struct notes_merge_options { |
||||
const char *local_ref; |
||||
const char *remote_ref; |
||||
int verbosity; |
||||
}; |
||||
|
||||
void init_notes_merge_options(struct notes_merge_options *o); |
||||
|
||||
/* |
||||
* Merge notes from o->remote_ref into o->local_ref |
||||
* |
||||
* The commits given by the two refs are merged, producing one of the following |
||||
* outcomes: |
||||
* |
||||
* 1. The merge trivially results in an existing commit (e.g. fast-forward or |
||||
* already-up-to-date). The SHA1 of the result is written into 'result_sha1' |
||||
* and 0 is returned. |
||||
* 2. The merge fails. result_sha1 is set to null_sha1, and non-zero returned. |
||||
* |
||||
* Both o->local_ref and o->remote_ref must be given (non-NULL), but either ref |
||||
* (although not both) may refer to a non-existing notes ref, in which case |
||||
* that notes ref is interpreted as an empty notes tree, and the merge |
||||
* trivially results in what the other ref points to. |
||||
*/ |
||||
int notes_merge(struct notes_merge_options *o, |
||||
unsigned char *result_sha1); |
||||
|
||||
#endif |
@ -0,0 +1,180 @@
@@ -0,0 +1,180 @@
|
||||
#!/bin/sh |
||||
# |
||||
# Copyright (c) 2010 Johan Herland |
||||
# |
||||
|
||||
test_description='Test merging of notes trees' |
||||
|
||||
. ./test-lib.sh |
||||
|
||||
test_expect_success setup ' |
||||
test_commit 1st && |
||||
test_commit 2nd && |
||||
test_commit 3rd && |
||||
test_commit 4th && |
||||
test_commit 5th && |
||||
# Create notes on 4 first commits |
||||
git config core.notesRef refs/notes/x && |
||||
git notes add -m "Notes on 1st commit" 1st && |
||||
git notes add -m "Notes on 2nd commit" 2nd && |
||||
git notes add -m "Notes on 3rd commit" 3rd && |
||||
git notes add -m "Notes on 4th commit" 4th |
||||
' |
||||
|
||||
commit_sha1=$(git rev-parse 1st^{commit}) |
||||
commit_sha2=$(git rev-parse 2nd^{commit}) |
||||
commit_sha3=$(git rev-parse 3rd^{commit}) |
||||
commit_sha4=$(git rev-parse 4th^{commit}) |
||||
commit_sha5=$(git rev-parse 5th^{commit}) |
||||
|
||||
verify_notes () { |
||||
notes_ref="$1" |
||||
git -c core.notesRef="refs/notes/$notes_ref" notes | |
||||
sort >"output_notes_$notes_ref" && |
||||
test_cmp "expect_notes_$notes_ref" "output_notes_$notes_ref" && |
||||
git -c core.notesRef="refs/notes/$notes_ref" log --format="%H %s%n%N" \ |
||||
>"output_log_$notes_ref" && |
||||
test_cmp "expect_log_$notes_ref" "output_log_$notes_ref" |
||||
} |
||||
|
||||
cat <<EOF | sort >expect_notes_x |
||||
5e93d24084d32e1cb61f7070505b9d2530cca987 $commit_sha4 |
||||
8366731eeee53787d2bdf8fc1eff7d94757e8da0 $commit_sha3 |
||||
eede89064cd42441590d6afec6c37b321ada3389 $commit_sha2 |
||||
daa55ffad6cb99bf64226532147ffcaf5ce8bdd1 $commit_sha1 |
||||
EOF |
||||
|
||||
cat >expect_log_x <<EOF |
||||
$commit_sha5 5th |
||||
|
||||
$commit_sha4 4th |
||||
Notes on 4th commit |
||||
|
||||
$commit_sha3 3rd |
||||
Notes on 3rd commit |
||||
|
||||
$commit_sha2 2nd |
||||
Notes on 2nd commit |
||||
|
||||
$commit_sha1 1st |
||||
Notes on 1st commit |
||||
|
||||
EOF |
||||
|
||||
test_expect_success 'verify initial notes (x)' ' |
||||
verify_notes x |
||||
' |
||||
|
||||
cp expect_notes_x expect_notes_y |
||||
cp expect_log_x expect_log_y |
||||
|
||||
test_expect_success 'fail to merge empty notes ref into empty notes ref (z => y)' ' |
||||
test_must_fail git -c "core.notesRef=refs/notes/y" notes merge z |
||||
' |
||||
|
||||
test_expect_success 'fail to merge into various non-notes refs' ' |
||||
test_must_fail git -c "core.notesRef=refs/notes" notes merge x && |
||||
test_must_fail git -c "core.notesRef=refs/notes/" notes merge x && |
||||
mkdir -p .git/refs/notes/dir && |
||||
test_must_fail git -c "core.notesRef=refs/notes/dir" notes merge x && |
||||
test_must_fail git -c "core.notesRef=refs/notes/dir/" notes merge x && |
||||
test_must_fail git -c "core.notesRef=refs/heads/master" notes merge x && |
||||
test_must_fail git -c "core.notesRef=refs/notes/y:" notes merge x && |
||||
test_must_fail git -c "core.notesRef=refs/notes/y:foo" notes merge x && |
||||
test_must_fail git -c "core.notesRef=refs/notes/foo^{bar" notes merge x |
||||
' |
||||
|
||||
test_expect_success 'fail to merge various non-note-trees' ' |
||||
git config core.notesRef refs/notes/y && |
||||
test_must_fail git notes merge refs/notes && |
||||
test_must_fail git notes merge refs/notes/ && |
||||
test_must_fail git notes merge refs/notes/dir && |
||||
test_must_fail git notes merge refs/notes/dir/ && |
||||
test_must_fail git notes merge refs/heads/master && |
||||
test_must_fail git notes merge x: && |
||||
test_must_fail git notes merge x:foo && |
||||
test_must_fail git notes merge foo^{bar |
||||
' |
||||
|
||||
test_expect_success 'merge notes into empty notes ref (x => y)' ' |
||||
git config core.notesRef refs/notes/y && |
||||
git notes merge x && |
||||
verify_notes y && |
||||
# x and y should point to the same notes commit |
||||
test "$(git rev-parse refs/notes/x)" = "$(git rev-parse refs/notes/y)" |
||||
' |
||||
|
||||
test_expect_success 'merge empty notes ref (z => y)' ' |
||||
git notes merge z && |
||||
# y should not change (still == x) |
||||
test "$(git rev-parse refs/notes/x)" = "$(git rev-parse refs/notes/y)" |
||||
' |
||||
|
||||
test_expect_success 'change notes on other notes ref (y)' ' |
||||
# Not touching notes to 1st commit |
||||
git notes remove 2nd && |
||||
git notes append -m "More notes on 3rd commit" 3rd && |
||||
git notes add -f -m "New notes on 4th commit" 4th && |
||||
git notes add -m "Notes on 5th commit" 5th |
||||
' |
||||
|
||||
test_expect_success 'merge previous notes commit (y^ => y) => No-op' ' |
||||
pre_state="$(git rev-parse refs/notes/y)" && |
||||
git notes merge y^ && |
||||
# y should not move |
||||
test "$pre_state" = "$(git rev-parse refs/notes/y)" |
||||
' |
||||
|
||||
cat <<EOF | sort >expect_notes_y |
||||
0f2efbd00262f2fd41dfae33df8765618eeacd99 $commit_sha5 |
||||
dec2502dac3ea161543f71930044deff93fa945c $commit_sha4 |
||||
4069cdb399fd45463ec6eef8e051a16a03592d91 $commit_sha3 |
||||
daa55ffad6cb99bf64226532147ffcaf5ce8bdd1 $commit_sha1 |
||||
EOF |
||||
|
||||
cat >expect_log_y <<EOF |
||||
$commit_sha5 5th |
||||
Notes on 5th commit |
||||
|
||||
$commit_sha4 4th |
||||
New notes on 4th commit |
||||
|
||||
$commit_sha3 3rd |
||||
Notes on 3rd commit |
||||
|
||||
More notes on 3rd commit |
||||
|
||||
$commit_sha2 2nd |
||||
|
||||
$commit_sha1 1st |
||||
Notes on 1st commit |
||||
|
||||
EOF |
||||
|
||||
test_expect_success 'verify changed notes on other notes ref (y)' ' |
||||
verify_notes y |
||||
' |
||||
|
||||
test_expect_success 'verify unchanged notes on original notes ref (x)' ' |
||||
verify_notes x |
||||
' |
||||
|
||||
test_expect_success 'merge original notes (x) into changed notes (y) => No-op' ' |
||||
git notes merge -vvv x && |
||||
verify_notes y && |
||||
verify_notes x |
||||
' |
||||
|
||||
cp expect_notes_y expect_notes_x |
||||
cp expect_log_y expect_log_x |
||||
|
||||
test_expect_success 'merge changed (y) into original (x) => Fast-forward' ' |
||||
git config core.notesRef refs/notes/x && |
||||
git notes merge y && |
||||
verify_notes x && |
||||
verify_notes y && |
||||
# x and y should point to same the notes commit |
||||
test "$(git rev-parse refs/notes/x)" = "$(git rev-parse refs/notes/y)" |
||||
' |
||||
|
||||
test_done |
Loading…
Reference in new issue