From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
Hi,
This is the 4th iteration of the 'git notes merge' patch series.
Changes between v3 and v4:
- Fix (void *) arithmetic in notes-merge.c:write_buf_to_worktree(), noticed
by Junio
- Change "CC:" into "Thanks-to:" in commit messages, to conform with current
conventions.
- Remove extranous show() function from notes-merge.c. Refactor OUTPUT()
macro accordingly
- notes_merge(): Rewrite logic around dereferencing o->local_ref into local
commit, and o->remote_ref into remote commit, to clarify how empty/missing
notes refs are handled. Also add more testcases verifying this part of the
code.
Some open questions:
- Sverre Rabbelier suggests renaming 'git notes merge --reset' to
'git notes merge --abort'. I sort of agree, but would want some
consistency with 'git merge', e.g. by providing 'git merge --abort'
as an alias to 'git reset --merge'.
- Should we refuse to finalize a notes merge when conflict markers
present in .git/NOTES_MERGE_WORKTREE? Since add/commit in regular
merges does NOT do this, I have not implemented it for notes merge.
- When resolving notes merge conflicts, you can add/remove files/notes
in .git/NOTES_MERGE_WORKTREE; 'git notes merge --commit' does not
check that the notes have any relationship to the notes originally
put there by 'git notes merge'. Should we warn about removed and
added notes in .git/NOTES_MERGE_WORKTREE? Currently we don't, and
I'm not sure it's worth it. Users can always review the merge commit
afterwards.
- Fetching and pushing note refs:
- Add refs/notes/* to default fetch refspec?
- A way to specify (at clone time) which refspec(s) to set up?
- A way for the remote repo to hint at which refspecs you might want
to set up (by default?)
Have fun! :)
...Johan
Johan Herland (21):
notes.c: Hexify SHA1 in die() message from init_notes()
(trivial) notes.h: Minor documentation fixes to copy_notes()
notes.h: Make default_notes_ref() available in notes API
notes.c: Reorder functions in preparation for next commit
notes.h/c: Clarify the handling of notes objects that are == null_sha1
notes.h/c: Propagate combine_notes_fn return value to add_note() and beyond
(trivial) t3303: Indent with tabs instead of spaces for consistency
notes.c: Use two newlines (instead of one) when concatenating notes
builtin/notes.c: Split notes ref DWIMmery into a separate function
git notes merge: Initial implementation handling trivial merges only
builtin/notes.c: Refactor creation of notes commits.
git notes merge: Handle real, non-conflicting notes merges
git notes merge: Add automatic conflict resolvers (ours, theirs, union)
Documentation: Preliminary docs on 'git notes merge'
git notes merge: Manual conflict resolution, part 1/2
git notes merge: Manual conflict resolution, part 2/2
git notes merge: List conflicting notes in notes merge commit message
git notes merge: --commit should fail if underlying notes ref has moved
git notes merge: Add another auto-resolving strategy: "cat_sort_uniq"
git notes merge: Add testcases for merging notes trees at different fanouts
Provide 'git notes get-ref' to easily retrieve current notes ref
Documentation/git-notes.txt | 85 ++++-
Makefile | 2 +
builtin.h | 2 +-
builtin/notes.c | 268 +++++++++++--
notes-cache.c | 3 +-
notes-merge.c | 735 +++++++++++++++++++++++++++++++++
notes-merge.h | 98 +++++
notes.c | 274 ++++++++----
notes.h | 47 ++-
t/t3301-notes.sh | 23 +
t/t3303-notes-subtrees.sh | 19 +-
t/t3308-notes-merge.sh | 368 +++++++++++++++++
t/t3309-notes-merge-auto-resolve.sh | 647 +++++++++++++++++++++++++++++
t/t3310-notes-merge-manual-resolve.sh | 556 +++++++++++++++++++++++++
t/t3311-notes-merge-fanout.sh | 436 +++++++++++++++++++
t/t3404-rebase-interactive.sh | 1 +
t/t9301-fast-import-notes.sh | 5 +
17 files changed, 3426 insertions(+), 143 deletions(-)
create mode 100644 notes-merge.c
create mode 100644 notes-merge.h
create mode 100755 t/t3308-notes-merge.sh
create mode 100755 t/t3309-notes-merge-auto-resolve.sh
create mode 100755 t/t3310-notes-merge-manual-resolve.sh
create mode 100755 t/t3311-notes-merge-fanout.sh
--
1.7.3.98.g5ad7d9
@@ -109,6 +110,10 @@ remove:: prune:: Remove all notes for non-existing/unreachable objects.+get-ref::+ Print the current notes ref. This provides an easy way to+ retrieve the current notes ref (e.g. from scripts).+ OPTIONS ------- -f::
@@ -1058,4 +1058,23 @@ test_expect_success 'git notes copy diagnoses too many or too few parameters' 'test_must_failgitnotescopyonetwothree'+test_expect_success'git notes get-ref (no overrides)''+gitconfig--unsetcore.notesRef&&+unsetGIT_NOTES_REF&&+test"$(gitnotesget-ref)"="refs/notes/commits"+'++test_expect_success'git notes get-ref (core.notesRef)''+gitconfigcore.notesRefrefs/notes/foo&&+test"$(gitnotesget-ref)"="refs/notes/foo"+'++test_expect_success'git notes get-ref (GIT_NOTES_REF)''+test"$(GIT_NOTES_REF=refs/notes/bargitnotesget-ref)"="refs/notes/bar"+'++test_expect_success'git notes get-ref (--ref)''+test"$(GIT_NOTES_REF=refs/notes/bargitnotes--ref=bazget-ref)"="refs/notes/baz"+'+ test_done
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
Notes trees may exist at different fanout levels internally. This
implementation detail should not be visible to the user, and it should
certainly not affect the merging of notes tree.
This patch adds testcases verifying the correctness of 'git notes merge'
when merging notes trees at different fanout levels.
Signed-off-by: Johan Herland <redacted>
---
t/t3311-notes-merge-fanout.sh | 436 +++++++++++++++++++++++++++++++++++++++++
1 files changed, 436 insertions(+), 0 deletions(-)
create mode 100755 t/t3311-notes-merge-fanout.sh
@@ -0,0 +1,436 @@+#!/bin/sh+#+# Copyright (c) 2010 Johan Herland+#++test_description='Test notes merging at various fanout levels'++../test-lib.sh++verify_notes(){+notes_ref="$1"+commit="$2"+iftest-f"expect_notes_$notes_ref"+then+git-ccore.notesRef="refs/notes/$notes_ref"notes|+sort>"output_notes_$notes_ref"&&+test_cmp"expect_notes_$notes_ref""output_notes_$notes_ref"||+return1+fi&&+git-ccore.notesRef="refs/notes/$notes_ref"log--format="%H %s%n%N"\+"$commit">"output_log_$notes_ref"&&+test_cmp"expect_log_$notes_ref""output_log_$notes_ref"+}++verify_fanout(){+notes_ref="$1"+# Expect entire notes tree to have a fanout == 1+gitrev-parse--quiet--verify"refs/notes/$notes_ref">/dev/null&&+gitls-tree-r--name-only"refs/notes/$notes_ref"|+whilereadpath+do+case"$path"in+??/??????????????????????????????????????)+:true+;;+*)+echo"Invalid path \"$path\""&&+return1+;;+esac+done+}++verify_no_fanout(){+notes_ref="$1"+# Expect entire notes tree to have a fanout == 0+gitrev-parse--quiet--verify"refs/notes/$notes_ref">/dev/null&&+gitls-tree-r--name-only"refs/notes/$notes_ref"|+whilereadpath+do+case"$path"in+????????????????????????????????????????)+:true+;;+*)+echo"Invalid path \"$path\""&&+return1+;;+esac+done+}++# Set up a notes merge scenario with different kinds of conflicts+test_expect_success'setup a few initial commits with notes (notes ref: x)''+gitconfigcore.notesRefrefs/notes/x&&+foriin12345+do+test_commit"commit$i">/dev/null&&+gitnotesadd-m"notes for commit$i"||return1+done+'++commit_sha1=$(gitrev-parsecommit1^{commit})+commit_sha2=$(gitrev-parsecommit2^{commit})+commit_sha3=$(gitrev-parsecommit3^{commit})+commit_sha4=$(gitrev-parsecommit4^{commit})+commit_sha5=$(gitrev-parsecommit5^{commit})++cat<<EOF|sort>expect_notes_x+aed91155c7a72c2188e781fdf40e0f3761b299db$commit_sha5+99fab268f9d7ee7b011e091a436c78def8eeee69$commit_sha4+953c20ae26c7aa0b428c20693fe38bc687f9d1a9$commit_sha3+6358796131b8916eaa2dde6902642942a1cb37e1$commit_sha2+b02d459c32f0e68f2fe0981033bb34f38776ba47$commit_sha1+EOF++cat>expect_log_x<<EOF+$commit_sha5commit5+notesforcommit5++$commit_sha4commit4+notesforcommit4++$commit_sha3commit3+notesforcommit3++$commit_sha2commit2+notesforcommit2++$commit_sha1commit1+notesforcommit1++EOF++test_expect_success'sanity check (x)''+verify_notesxcommit5&&+verify_no_fanoutx+'++num=300++cpexpect_log_xexpect_log_y++test_expect_success'Add a few hundred commits w/notes to trigger fanout (x -> y)''+gitupdate-refrefs/notes/yrefs/notes/x&&+gitconfigcore.notesRefrefs/notes/y&&+i=5&&+whiletest$i-lt$num+do+i=$(($i+1))&&+test_commit"commit$i">/dev/null&&+gitnotesadd-m"notes for commit$i"||return1+done&&+test"$(gitrev-parserefs/notes/y)"!="$(gitrev-parserefs/notes/x)"&&+# Expected number of commits and notes+test"$(gitrev-listHEAD|wc-l)"="$num"&&+test"$(gitnoteslist|wc-l)"="$num"&&+# 5 first notes unchanged+verify_notesycommit5+'++test_expect_success'notes tree has fanout (y)''verify_fanout y'++test_expect_success'No-op merge (already included) (x => y)''+gitupdate-refrefs/notes/mrefs/notes/y&&+gitconfigcore.notesRefrefs/notes/m&&+gitnotesmergex&&+test"$(gitrev-parserefs/notes/m)"="$(gitrev-parserefs/notes/y)"+'++test_expect_success'Fast-forward merge (y => x)''+gitupdate-refrefs/notes/mrefs/notes/x&&+gitnotesmergey&&+test"$(gitrev-parserefs/notes/m)"="$(gitrev-parserefs/notes/y)"+'++cat<<EOF|sort>expect_notes_z+9f506ee70e20379d7f78204c77b334f43d77410d$commit_sha3+23a47d6ea7d589895faf800752054818e1e7627b$commit_sha2+b02d459c32f0e68f2fe0981033bb34f38776ba47$commit_sha1+EOF++cat>expect_log_z<<EOF+$commit_sha5commit5++$commit_sha4commit4++$commit_sha3commit3+notesforcommit3++appendednotesforcommit3++$commit_sha2commit2+newnotesforcommit2++$commit_sha1commit1+notesforcommit1++EOF++test_expect_success'change some of the initial 5 notes (x -> z)''+gitupdate-refrefs/notes/zrefs/notes/x&&+gitconfigcore.notesRefrefs/notes/z&&+gitnotesadd-f-m"new notes for commit2"commit2&&+gitnotesappend-m"appended notes for commit3"commit3&&+gitnotesremovecommit4&&+gitnotesremovecommit5&&+verify_noteszcommit5+'++test_expect_success'notes tree has no fanout (z)''verify_no_fanout z'++cpexpect_log_zexpect_log_m++test_expect_success'successful merge without conflicts (y => z)''+gitupdate-refrefs/notes/mrefs/notes/z&&+gitconfigcore.notesRefrefs/notes/m&&+gitnotesmergey&&+verify_notesmcommit5&&+# x/y/z unchanged+verify_notesxcommit5&&+verify_notesycommit5&&+verify_noteszcommit5+'++test_expect_success'notes tree still has fanout after merge (m)''verify_fanout m'++cat>expect_log_w<<EOF+$commit_sha5commit5++$commit_sha4commit4+othernotesforcommit4++$commit_sha3commit3+othernotesforcommit3++$commit_sha2commit2+notesforcommit2++$commit_sha1commit1+othernotesforcommit1++EOF++test_expect_success'introduce conflicting changes (y -> w)''+gitupdate-refrefs/notes/wrefs/notes/y&&+gitconfigcore.notesRefrefs/notes/w&&+gitnotesadd-f-m"other notes for commit1"commit1&&+gitnotesadd-f-m"other notes for commit3"commit3&&+gitnotesadd-f-m"other notes for commit4"commit4&&+gitnotesremovecommit5&&+verify_noteswcommit5+'++cat>expect_log_m<<EOF+$commit_sha5commit5++$commit_sha4commit4+othernotesforcommit4++$commit_sha3commit3+othernotesforcommit3++$commit_sha2commit2+newnotesforcommit2++$commit_sha1commit1+othernotesforcommit1++EOF++test_expect_success'successful merge using "ours" strategy (z => w)''+gitupdate-refrefs/notes/mrefs/notes/w&&+gitconfigcore.notesRefrefs/notes/m&&+gitnotesmerge-soursz&&+verify_notesmcommit5&&+# w/x/y/z unchanged+verify_noteswcommit5&&+verify_notesxcommit5&&+verify_notesycommit5&&+verify_noteszcommit5+'++test_expect_success'notes tree still has fanout after merge (m)''verify_fanout m'++cat>expect_log_m<<EOF+$commit_sha5commit5++$commit_sha4commit4++$commit_sha3commit3+notesforcommit3++appendednotesforcommit3++$commit_sha2commit2+newnotesforcommit2++$commit_sha1commit1+othernotesforcommit1++EOF++test_expect_success'successful merge using "theirs" strategy (z => w)''+gitupdate-refrefs/notes/mrefs/notes/w&&+gitnotesmerge-stheirsz&&+verify_notesmcommit5&&+# w/x/y/z unchanged+verify_noteswcommit5&&+verify_notesxcommit5&&+verify_notesycommit5&&+verify_noteszcommit5+'++test_expect_success'notes tree still has fanout after merge (m)''verify_fanout m'++cat>expect_log_m<<EOF+$commit_sha5commit5++$commit_sha4commit4+othernotesforcommit4++$commit_sha3commit3+othernotesforcommit3++notesforcommit3++appendednotesforcommit3++$commit_sha2commit2+newnotesforcommit2++$commit_sha1commit1+othernotesforcommit1++EOF++test_expect_success'successful merge using "union" strategy (z => w)''+gitupdate-refrefs/notes/mrefs/notes/w&&+gitnotesmerge-sunionz&&+verify_notesmcommit5&&+# w/x/y/z unchanged+verify_noteswcommit5&&+verify_notesxcommit5&&+verify_notesycommit5&&+verify_noteszcommit5+'++test_expect_success'notes tree still has fanout after merge (m)''verify_fanout m'++cat>expect_log_m<<EOF+$commit_sha5commit5++$commit_sha4commit4+othernotesforcommit4++$commit_sha3commit3+appendednotesforcommit3+notesforcommit3+othernotesforcommit3++$commit_sha2commit2+newnotesforcommit2++$commit_sha1commit1+othernotesforcommit1++EOF++test_expect_success'successful merge using "cat_sort_uniq" strategy (z => w)''+gitupdate-refrefs/notes/mrefs/notes/w&&+gitnotesmerge-scat_sort_uniqz&&+verify_notesmcommit5&&+# w/x/y/z unchanged+verify_noteswcommit5&&+verify_notesxcommit5&&+verify_notesycommit5&&+verify_noteszcommit5+'++test_expect_success'notes tree still has fanout after merge (m)''verify_fanout m'++# We're merging z into w. Here are the conflicts we expect:+#+# commit | x -> w | x -> z | conflict?+# -------|-----------|-----------|----------+# 1 | changed | unchanged | no, use w+# 2 | unchanged | changed | no, use z+# 3 | changed | changed | yes (w, then z in conflict markers)+# 4 | changed | deleted | yes (w)+# 5 | deleted | deleted | no, deleted++test_expect_success'fails to merge using "manual" strategy (z => w)''+gitupdate-refrefs/notes/mrefs/notes/w&&+test_must_failgitnotesmergez+'++test_expect_success'notes tree still has fanout after merge (m)''verify_fanout m'++cat<<EOF|sort>expect_conflicts+$commit_sha3+$commit_sha4+EOF++cat>expect_conflict_$commit_sha3<<EOF+<<<<<<<refs/notes/m+othernotesforcommit3+=======+notesforcommit3++appendednotesforcommit3+>>>>>>>refs/notes/z+EOF++cat>expect_conflict_$commit_sha4<<EOF+othernotesforcommit4+EOF++test_expect_success'verify conflict entries (with no fanout)''+ls.git/NOTES_MERGE_WORKTREE>output_conflicts&&+test_cmpexpect_conflictsoutput_conflicts&&+(forfin$(catexpect_conflicts);do+test_cmp"expect_conflict_$f"".git/NOTES_MERGE_WORKTREE/$f"||+exit1+done)&&+# Verify that current notes tree (pre-merge) has not changed (m == w)+test"$(gitrev-parserefs/notes/m)"="$(gitrev-parserefs/notes/w)"+'++cat>expect_log_m<<EOF+$commit_sha5commit5++$commit_sha4commit4+othernotesforcommit4++$commit_sha3commit3+othernotesforcommit3++appendednotesforcommit3++$commit_sha2commit2+newnotesforcommit2++$commit_sha1commit1+othernotesforcommit1++EOF++test_expect_success'resolve and finalize merge (z => w)''+cat>.git/NOTES_MERGE_WORKTREE/$commit_sha3<<EOF&&+othernotesforcommit3++appendednotesforcommit3+EOF+gitnotesmerge--commit&&+verify_notesmcommit5&&+# w/x/y/z unchanged+verify_noteswcommit5&&+verify_notesxcommit5&&+verify_notesycommit5&&+verify_noteszcommit5+'++test_expect_success'notes tree still has fanout after merge (m)''verify_fanout m'++test_done
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
This patch has been improved by the following contributions:
- Stephen Boyd: Use "automatically resolves" instead of "auto-resolves"
- Stephen Boyd: Remove unbalanced '('
Thanks-to: Stephen Boyd [off-list ref]
Signed-off-by: Johan Herland <redacted>
---
Documentation/git-notes.txt | 44 ++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 43 insertions(+), 1 deletions(-)
@@ -83,6 +84,16 @@ edit:: show:: Show the notes for a given object (defaults to HEAD).+merge::+ Merge the given notes ref into the current notes ref.+ This will try to merge the changes made by the given+ notes ref (called "remote") since the merge-base (if+ any) into the current notes ref (called "local").+++If conflicts arise and a strategy for automatically resolving+conflicting notes (see the -s/--strategy option) is not given,+the merge fails (TODO).+ remove:: Remove the notes for a given object (defaults to HEAD). This is equivalent to specifying an empty note message to
@@ -133,9 +144,23 @@ OPTIONS Do not remove anything; just report the object names whose notes would be removed.+-s <strategy>::+--strategy=<strategy>::+ When merging notes, resolve notes conflicts using the given+ strategy. The following strategies are recognized: "manual"+ (default), "ours", "theirs" and "union".+ See the "NOTES MERGE STRATEGIES" section below for more+ information on each notes merge strategy.++-q::+--quiet::+ When merging notes, operate quietly.+ -v:: --verbose::- Report all object names whose notes are removed.+ When merging notes, be more verbose.+ When pruning notes, report all object names whose notes are+ removed. DISCUSSION
@@ -163,6 +188,23 @@ object, in which case the history of the notes can be read with `git log -p -g <refname>`.+NOTES MERGE STRATEGIES+----------------------++The default notes merge strategy is "manual", which is not yet+implemented (TODO).++"ours" automatically resolves conflicting notes in favor of the local+version (i.e. the current notes ref).++"theirs" automatically resolves notes conflicts in favor of the remote+version (i.e. the given notes ref being merged into the current notes+ref).++"union" automatically resolves notes conflicts by concatenating the+local and remote versions.++ EXAMPLES --------
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
This new strategy is similar to "concatenate", but in addition to
concatenating the two note candidates, this strategy sorts the resulting
lines, and removes duplicate lines from the result. This is equivalent to
applying the "cat | sort | uniq" shell pipeline to the two note candidates.
This strategy is useful if the notes follow a line-based format where one
wants to avoid duplicate lines in the merge result.
Note that if either of the note candidates contain duplicate lines _prior_
to the merge, these will also be removed by this merge strategy.
The patch also contains tests and documentation for the new strategy.
Signed-off-by: Johan Herland <redacted>
---
Documentation/git-notes.txt | 12 +++-
builtin/notes.c | 8 ++-
notes-merge.c | 6 ++
notes-merge.h | 3 +-
notes.c | 76 ++++++++++++++++++
notes.h | 1 +
t/t3309-notes-merge-auto-resolve.sh | 145 +++++++++++++++++++++++++++++++++++
7 files changed, 247 insertions(+), 4 deletions(-)
@@ -155,7 +155,7 @@ OPTIONS --strategy=<strategy>:: When merging notes, resolve notes conflicts using the given strategy. The following strategies are recognized: "manual"- (default), "ours", "theirs" and "union".+ (default), "ours", "theirs", "union" and "cat_sort_uniq". See the "NOTES MERGE STRATEGIES" section below for more information on each notes merge strategy.
@@ -230,6 +230,16 @@ ref). "union" automatically resolves notes conflicts by concatenating the local and remote versions.+"cat_sort_uniq" is similar to "union", but in addition to concatenating+the local and remote versions, this strategy also sorts the resulting+lines, and removes duplicate lines from the result. This is equivalent+to applying the "cat | sort | uniq" shell pipeline to the local and+remote versions. This strategy is useful if the notes follow a line-based+format where one wants to avoid duplicated lines in the merge result.+Note that if either the local or remote version contain duplicate lines+prior to the merge, these will also be removed by this notes merge+strategy.+ EXAMPLES --------
@@ -846,8 +848,8 @@ static int merge(int argc, const char **argv, const char *prefix)OPT__VERBOSITY(&verbosity),OPT_GROUP("Merge options"),OPT_STRING('s',"strategy",&strategy,"strategy",-"resolve notes conflicts using the given "-"strategy (manual/ours/theirs/union)"),+"resolve notes conflicts using the given strategy "+"(manual/ours/theirs/union/cat_sort_uniq)"),OPT_GROUP("Committing unmerged notes"),{OPTION_BOOLEAN,0,"commit",&do_commit,NULL,"finalize notes merge by committing unmerged notes",
@@ -452,6 +452,12 @@ static int merge_one_change(struct notes_merge_options *o,if(add_note(t,p->obj,p->remote,combine_notes_concatenate))die("confused: combine_notes_concatenate failed");return0;+caseNOTES_MERGE_RESOLVE_CAT_SORT_UNIQ:+OUTPUT(o,2,"Concatenating unique lines in local and remote "+"notes for %s",sha1_to_hex(p->obj));+if(add_note(t,p->obj,p->remote,combine_notes_cat_sort_uniq))+die("confused: combine_notes_cat_sort_uniq failed");+return0;}die("Unknown strategy (%i).",o->strategy);}
@@ -499,4 +499,149 @@ test_expect_success 'merge z into y with "union" strategy => Non-conflicting 3-wverify_notesyunion'+test_expect_success'reset to pre-merge state (y)''+gitupdate-refrefs/notes/yrefs/notes/y^1&&+# Verify pre-merge state+verify_notesyy+'++cat<<EOF|sort>expect_notes_union2+d682107b8bf7a7aea1e537a8d5cb6a12b60135f1$commit_sha15+5de7ea7ad4f47e7ff91989fb82234634730f75df$commit_sha14+3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc$commit_sha13+a66055fa82f7a03fe0c02a6aba3287a85abf7c62$commit_sha12+7e3c53503a3db8dd996cb62e37c66e070b44b54d$commit_sha11+b8d03e173f67f6505a76f6e00cf93440200dd9be$commit_sha10+851e1638784a884c7dd26c5d41f3340f6387413a$commit_sha8+357b6ca14c7afd59b7f8b8aaaa6b8b723771135b$commit_sha5+e2bfd06a37dd2031684a59a6e2b033e212239c78$commit_sha4+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1$commit_sha3+283b48219aee9a4105f6cab337e789065c82c2b9$commit_sha2+EOF++cat>expect_log_union2<<EOF+$commit_sha1515th+znoteson15thcommit++ynoteson15thcommit++$commit_sha1414th+ynoteson14thcommit++$commit_sha1313th+ynoteson13thcommit++$commit_sha1212th+ynoteson12thcommit++$commit_sha1111th+znoteson11thcommit++$commit_sha1010th+xnoteson10thcommit++$commit_sha99th++$commit_sha88th+znoteson8thcommit++$commit_sha77th++$commit_sha66th++$commit_sha55th+znoteson5thcommit++ynoteson5thcommit++$commit_sha44th+ynoteson4thcommit++$commit_sha33rd+ynoteson3rdcommit++$commit_sha22nd+znoteson2ndcommit++$commit_sha11st++EOF++test_expect_success'merge y into z with "union" strategy => Non-conflicting 3-way merge''+gitconfigcore.notesRefrefs/notes/z&&+gitnotesmerge--strategy=uniony&&+verify_noteszunion2+'++test_expect_success'reset to pre-merge state (z)''+gitupdate-refrefs/notes/zrefs/notes/z^1&&+# Verify pre-merge state+verify_noteszz+'++cat<<EOF|sort>expect_notes_cat_sort_uniq+6be90240b5f54594203e25d9f2f64b7567175aee$commit_sha15+5de7ea7ad4f47e7ff91989fb82234634730f75df$commit_sha14+3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc$commit_sha13+a66055fa82f7a03fe0c02a6aba3287a85abf7c62$commit_sha12+7e3c53503a3db8dd996cb62e37c66e070b44b54d$commit_sha11+b8d03e173f67f6505a76f6e00cf93440200dd9be$commit_sha10+851e1638784a884c7dd26c5d41f3340f6387413a$commit_sha8+660311d7f78dc53db12ac373a43fca7465381a7e$commit_sha5+e2bfd06a37dd2031684a59a6e2b033e212239c78$commit_sha4+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1$commit_sha3+283b48219aee9a4105f6cab337e789065c82c2b9$commit_sha2+EOF++cat>expect_log_cat_sort_uniq<<EOF+$commit_sha1515th+ynoteson15thcommit+znoteson15thcommit++$commit_sha1414th+ynoteson14thcommit++$commit_sha1313th+ynoteson13thcommit++$commit_sha1212th+ynoteson12thcommit++$commit_sha1111th+znoteson11thcommit++$commit_sha1010th+xnoteson10thcommit++$commit_sha99th++$commit_sha88th+znoteson8thcommit++$commit_sha77th++$commit_sha66th++$commit_sha55th+ynoteson5thcommit+znoteson5thcommit++$commit_sha44th+ynoteson4thcommit++$commit_sha33rd+ynoteson3rdcommit++$commit_sha22nd+znoteson2ndcommit++$commit_sha11st++EOF++test_expect_success'merge y into z with "cat_sort_uniq" strategy => Non-conflicting 3-way merge''+gitnotesmerge--strategy=cat_sort_uniqy&&+verify_noteszcat_sort_uniq+'+ test_done
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
This patch introduces no functional change. It consists solely of reordering
functions in notes.c to avoid use-before-declaration errors after applying
the next commit in this series.
Signed-off-by: Johan Herland <redacted>
---
notes.c | 146 +++++++++++++++++++++++++++++++-------------------------------
1 files changed, 73 insertions(+), 73 deletions(-)
@@ -150,6 +150,79 @@ static struct leaf_node *note_tree_find(struct notes_tree *t,}/*+*Howtoconsolidateanint_node:+*Ifthereare>1non-NULLentries,giveupandreturnnon-zero.+*Otherwisereplacetheint_nodeatthegivenindexinthegivenparentnode+*withtheonlyentry(oraNULLentryifnoentries)fromthegiventree,+*andreturn0.+*/+staticintnote_tree_consolidate(structint_node*tree,+structint_node*parent,unsignedcharindex)+{+unsignedinti;+void*p=NULL;++assert(tree&&parent);+assert(CLR_PTR_TYPE(parent->a[index])==tree);++for(i=0;i<16;i++){+if(GET_PTR_TYPE(tree->a[i])!=PTR_TYPE_NULL){+if(p)/* more than one entry */+return-2;+p=tree->a[i];+}+}++/* replace tree with p in parent[index] */+parent->a[index]=p;+free(tree);+return0;+}++/*+*Toremovealeaf_node:+*Searchtothetreelocationappropriateforthegivenleaf_node'skey:+*-Iflocationdoesnotholdamatchingentry,abortanddonothing.+*-Replacethematchingleaf_nodewithaNULLentry(andfreetheleaf_node).+*-Consolidateint_nodesrepeatedly,whilewalkingupthetreetowardsroot.+*/+staticvoidnote_tree_remove(structnotes_tree*t,structint_node*tree,+unsignedcharn,structleaf_node*entry)+{+structleaf_node*l;+structint_node*parent_stack[20];+unsignedchari,j;+void**p=note_tree_search(t,&tree,&n,entry->key_sha1);++assert(GET_PTR_TYPE(entry)==0);/* no type bits set */+if(GET_PTR_TYPE(*p)!=PTR_TYPE_NOTE)+return;/* type mismatch, nothing to remove */+l=(structleaf_node*)CLR_PTR_TYPE(*p);+if(hashcmp(l->key_sha1,entry->key_sha1))+return;/* key mismatch, nothing to remove */++/* we have found a matching entry */+free(l);+*p=SET_PTR_TYPE(NULL,PTR_TYPE_NULL);++/* consolidate this tree level, and parent levels, if possible */+if(!n)+return;/* cannot consolidate top level */+/* first, build stack of ancestors between root and current node */+parent_stack[0]=t->root;+for(i=0;i<n;i++){+j=GET_NIBBLE(i,entry->key_sha1);+parent_stack[i+1]=CLR_PTR_TYPE(parent_stack[i]->a[j]);+}+assert(i==n&&parent_stack[i]==tree);+/* next, unwind stack until note_tree_consolidate() is done */+while(i>0&&+!note_tree_consolidate(parent_stack[i],parent_stack[i-1],+GET_NIBBLE(i-1,entry->key_sha1)))+i--;+}++/**Toinsertaleaf_node:*Searchtothetreelocationappropriateforthegivenleaf_node'skey:*-Iflocationisunused(NULL),storethetweakedpointerdirectlythere
@@ -229,79 +302,6 @@ static void note_tree_insert(struct notes_tree *t, struct int_node *tree,note_tree_insert(t,new_node,n+1,entry,type,combine_notes);}-/*-*Howtoconsolidateanint_node:-*Ifthereare>1non-NULLentries,giveupandreturnnon-zero.-*Otherwisereplacetheint_nodeatthegivenindexinthegivenparentnode-*withtheonlyentry(oraNULLentryifnoentries)fromthegiventree,-*andreturn0.-*/-staticintnote_tree_consolidate(structint_node*tree,-structint_node*parent,unsignedcharindex)-{-unsignedinti;-void*p=NULL;--assert(tree&&parent);-assert(CLR_PTR_TYPE(parent->a[index])==tree);--for(i=0;i<16;i++){-if(GET_PTR_TYPE(tree->a[i])!=PTR_TYPE_NULL){-if(p)/* more than one entry */-return-2;-p=tree->a[i];-}-}--/* replace tree with p in parent[index] */-parent->a[index]=p;-free(tree);-return0;-}--/*-*Toremovealeaf_node:-*Searchtothetreelocationappropriateforthegivenleaf_node'skey:-*-Iflocationdoesnotholdamatchingentry,abortanddonothing.-*-Replacethematchingleaf_nodewithaNULLentry(andfreetheleaf_node).-*-Consolidateint_nodesrepeatedly,whilewalkingupthetreetowardsroot.-*/-staticvoidnote_tree_remove(structnotes_tree*t,structint_node*tree,-unsignedcharn,structleaf_node*entry)-{-structleaf_node*l;-structint_node*parent_stack[20];-unsignedchari,j;-void**p=note_tree_search(t,&tree,&n,entry->key_sha1);--assert(GET_PTR_TYPE(entry)==0);/* no type bits set */-if(GET_PTR_TYPE(*p)!=PTR_TYPE_NOTE)-return;/* type mismatch, nothing to remove */-l=(structleaf_node*)CLR_PTR_TYPE(*p);-if(hashcmp(l->key_sha1,entry->key_sha1))-return;/* key mismatch, nothing to remove */--/* we have found a matching entry */-free(l);-*p=SET_PTR_TYPE(NULL,PTR_TYPE_NULL);--/* consolidate this tree level, and parent levels, if possible */-if(!n)-return;/* cannot consolidate top level */-/* first, build stack of ancestors between root and current node */-parent_stack[0]=t->root;-for(i=0;i<n;i++){-j=GET_NIBBLE(i,entry->key_sha1);-parent_stack[i+1]=CLR_PTR_TYPE(parent_stack[i]->a[j]);-}-assert(i==n&&parent_stack[i]==tree);-/* next, unwind stack until note_tree_consolidate() is done */-while(i>0&&-!note_tree_consolidate(parent_stack[i],parent_stack[i-1],-GET_NIBBLE(i-1,entry->key_sha1)))-i--;-}-/* Free the entire notes data contained in the given tree */staticvoidnote_tree_free(structint_node*tree){
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
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()
Thanks-to: Stephen Boyd [off-list ref]
Thanks-to: Ævar Arnfjörð Bjarmason [off-list ref]
Thanks-to: Jonathan Nieder [off-list ref]
Signed-off-by: Johan Herland <redacted>
---
Makefile | 2 +
builtin/notes.c | 54 ++++++++++++++
notes-merge.c | 120 ++++++++++++++++++++++++++++++++
notes-merge.h | 36 ++++++++++
t/t3308-notes-merge.sh | 180 ++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 392 insertions(+), 0 deletions(-)
create mode 100644 notes-merge.c
create mode 100644 notes-merge.h
create mode 100755 t/t3308-notes-merge.sh
@@ -0,0 +1,180 @@+#!/bin/sh+#+# Copyright (c) 2010 Johan Herland+#++test_description='Test merging of notes trees'++../test-lib.sh++test_expect_successsetup'+test_commit1st&&+test_commit2nd&&+test_commit3rd&&+test_commit4th&&+test_commit5th&&+# Create notes on 4 first commits+gitconfigcore.notesRefrefs/notes/x&&+gitnotesadd-m"Notes on 1st commit"1st&&+gitnotesadd-m"Notes on 2nd commit"2nd&&+gitnotesadd-m"Notes on 3rd commit"3rd&&+gitnotesadd-m"Notes on 4th commit"4th+'++commit_sha1=$(gitrev-parse1st^{commit})+commit_sha2=$(gitrev-parse2nd^{commit})+commit_sha3=$(gitrev-parse3rd^{commit})+commit_sha4=$(gitrev-parse4th^{commit})+commit_sha5=$(gitrev-parse5th^{commit})++verify_notes(){+notes_ref="$1"+git-ccore.notesRef="refs/notes/$notes_ref"notes|+sort>"output_notes_$notes_ref"&&+test_cmp"expect_notes_$notes_ref""output_notes_$notes_ref"&&+git-ccore.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_sha55th++$commit_sha44th+Noteson4thcommit++$commit_sha33rd+Noteson3rdcommit++$commit_sha22nd+Noteson2ndcommit++$commit_sha11st+Noteson1stcommit++EOF++test_expect_success'verify initial notes (x)''+verify_notesx+'++cpexpect_notes_xexpect_notes_y+cpexpect_log_xexpect_log_y++test_expect_success'fail to merge empty notes ref into empty notes ref (z => y)''+test_must_failgit-c"core.notesRef=refs/notes/y"notesmergez+'++test_expect_success'fail to merge into various non-notes refs''+test_must_failgit-c"core.notesRef=refs/notes"notesmergex&&+test_must_failgit-c"core.notesRef=refs/notes/"notesmergex&&+mkdir-p.git/refs/notes/dir&&+test_must_failgit-c"core.notesRef=refs/notes/dir"notesmergex&&+test_must_failgit-c"core.notesRef=refs/notes/dir/"notesmergex&&+test_must_failgit-c"core.notesRef=refs/heads/master"notesmergex&&+test_must_failgit-c"core.notesRef=refs/notes/y:"notesmergex&&+test_must_failgit-c"core.notesRef=refs/notes/y:foo"notesmergex&&+test_must_failgit-c"core.notesRef=refs/notes/foo^{bar"notesmergex+'++test_expect_success'fail to merge various non-note-trees''+gitconfigcore.notesRefrefs/notes/y&&+test_must_failgitnotesmergerefs/notes&&+test_must_failgitnotesmergerefs/notes/&&+test_must_failgitnotesmergerefs/notes/dir&&+test_must_failgitnotesmergerefs/notes/dir/&&+test_must_failgitnotesmergerefs/heads/master&&+test_must_failgitnotesmergex:&&+test_must_failgitnotesmergex:foo&&+test_must_failgitnotesmergefoo^{bar+'++test_expect_success'merge notes into empty notes ref (x => y)''+gitconfigcore.notesRefrefs/notes/y&&+gitnotesmergex&&+verify_notesy&&+# x and y should point to the same notes commit+test"$(gitrev-parserefs/notes/x)"="$(gitrev-parserefs/notes/y)"+'++test_expect_success'merge empty notes ref (z => y)''+gitnotesmergez&&+# y should not change (still == x)+test"$(gitrev-parserefs/notes/x)"="$(gitrev-parserefs/notes/y)"+'++test_expect_success'change notes on other notes ref (y)''+# Not touching notes to 1st commit+gitnotesremove2nd&&+gitnotesappend-m"More notes on 3rd commit"3rd&&+gitnotesadd-f-m"New notes on 4th commit"4th&&+gitnotesadd-m"Notes on 5th commit"5th+'++test_expect_success'merge previous notes commit (y^ => y) => No-op''+pre_state="$(gitrev-parserefs/notes/y)"&&+gitnotesmergey^&&+# y should not move+test"$pre_state"="$(gitrev-parserefs/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_sha55th+Noteson5thcommit++$commit_sha44th+Newnoteson4thcommit++$commit_sha33rd+Noteson3rdcommit++Morenoteson3rdcommit++$commit_sha22nd++$commit_sha11st+Noteson1stcommit++EOF++test_expect_success'verify changed notes on other notes ref (y)''+verify_notesy+'++test_expect_success'verify unchanged notes on original notes ref (x)''+verify_notesx+'++test_expect_success'merge original notes (x) into changed notes (y) => No-op''+gitnotesmerge-vvvx&&+verify_notesy&&+verify_notesx+'++cpexpect_notes_yexpect_notes_x+cpexpect_log_yexpect_log_x++test_expect_success'merge changed (y) into original (x) => Fast-forward''+gitconfigcore.notesRefrefs/notes/x&&+gitnotesmergey&&+verify_notesx&&+verify_notesy&&+# x and y should point to same the notes commit+test"$(gitrev-parserefs/notes/x)"="$(gitrev-parserefs/notes/y)"+'++test_done
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
Clearly specify how combine_notes functions are expected to handle null_sha1
in input. Also specify (and implement) that returning null_sha1 from a
combine_notes function will cause the note in question to be removed.
Furthermore, document that passing note_sha1 == null_sha1 to add_note() is
usually a no-op, except in cases where combining it with an existing note
yields a new/changed result.
Signed-off-by: Johan Herland <redacted>
---
notes.c | 12 +++++++++++-
notes.h | 16 +++++++++++++++-
2 files changed, 26 insertions(+), 2 deletions(-)
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
When using combine_notes_concatenate() to concatenate notes, it currently
ensures exactly one newline character between the given notes. However,
when using builtin/notes.c:create_note() to concatenate notes (e.g. by
'git notes append'), it adds a newline character to the trailing newline
of the preceding notes object, thus resulting in _two_ newlines (aka. a
blank line) separating contents of the two notes.
This patch brings combine_notes_concatenate() into consistency with
builtin/notes.c:create_note(), by ensuring exactly _two_ newline characters
between concatenated notes.
The patch also changes a few notes-related selftests accordingly.
Signed-off-by: Johan Herland <redacted>
---
notes.c | 7 ++++---
t/t3301-notes.sh | 4 ++++
t/t3303-notes-subtrees.sh | 1 +
t/t3404-rebase-interactive.sh | 1 +
t/t9301-fast-import-notes.sh | 5 +++++
5 files changed, 15 insertions(+), 3 deletions(-)
@@ -814,16 +814,17 @@ int combine_notes_concatenate(unsigned char *cur_sha1,return0;}-/* we will separate the notes by a newline anyway */+/* we will separate the notes by two newlines anyway */if(cur_msg[cur_len-1]=='\n')cur_len--;/* concatenate cur_msg and new_msg into buf */-buf_len=cur_len+1+new_len;+buf_len=cur_len+2+new_len;buf=(char*)xmalloc(buf_len);memcpy(buf,cur_msg,cur_len);buf[cur_len]='\n';-memcpy(buf+cur_len+1,new_msg,new_len);+buf[cur_len+1]='\n';+memcpy(buf+cur_len+2,new_msg,new_len);free(cur_msg);free(new_msg);
@@ -173,6 +173,7 @@ verify_concatenated_notes () {while[$i-gt0];doecho" commit #$i"&&echo" first note for commit #$i"&&+echo" "&&echo" second note for commit #$i"&&i=$(($i-1));done>expect&&
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
Create new function create_notes_commit() which is slightly more general than
commit_notes() (accepts multiple commit parents and does not auto-update the
notes ref). This function will be used by the notes-merge functionality in
future patches.
Also rewrite builtin/notes.c:commit_notes() to reuse this new function.
Signed-off-by: Johan Herland <redacted>
---
builtin.h | 2 +-
builtin/notes.c | 28 +++++-----------------------
notes-merge.c | 27 +++++++++++++++++++++++++++
notes-merge.h | 14 ++++++++++++++
4 files changed, 47 insertions(+), 24 deletions(-)
@@ -288,18 +288,17 @@ static int parse_reedit_arg(const struct option *opt, const char *arg, int unsetreturnparse_reuse_arg(opt,arg,unset);}-intcommit_notes(structnotes_tree*t,constchar*msg)+voidcommit_notes(structnotes_tree*t,constchar*msg){-structcommit_list*parent;-unsignedchartree_sha1[20],prev_commit[20],new_commit[20];structstrbufbuf=STRBUF_INIT;+unsignedcharcommit_sha1[20];if(!t)t=&default_notes_tree;if(!t->initialized||!t->ref||!*t->ref)die("Cannot commit uninitialized/unreferenced notes tree");if(!t->dirty)-return0;/* don't have to commit an unchanged tree */+return;/* don't have to commit an unchanged tree *//* Prepare commit message and reflog message */strbuf_addstr(&buf,"notes: ");/* commit message starts at index 7 */
@@ -307,27 +306,10 @@ int commit_notes(struct notes_tree *t, const char *msg)if(buf.buf[buf.len-1]!='\n')strbuf_addch(&buf,'\n');/* Make sure msg ends with newline */-/* Convert notes tree to tree object */-if(write_notes_tree(t,tree_sha1))-die("Failed to write current notes tree to database");--/* Create new commit for the tree object */-if(!read_ref(t->ref,prev_commit)){/* retrieve parent commit */-parent=xmalloc(sizeof(*parent));-parent->item=lookup_commit(prev_commit);-parent->next=NULL;-}else{-hashclr(prev_commit);-parent=NULL;-}-if(commit_tree(buf.buf+7,tree_sha1,parent,new_commit,NULL))-die("Failed to commit notes tree to database");--/* Update notes ref with new commit */-update_ref(buf.buf,t->ref,new_commit,prev_commit,0,DIE_ON_ERR);+create_notes_commit(t,NULL,buf.buf+7,commit_sha1);+update_ref(buf.buf,t->ref,commit_sha1,NULL,0,DIE_ON_ERR);strbuf_release(&buf);-return0;}combine_notes_fnparse_combine_notes_fn(constchar*v)
@@ -17,6 +18,32 @@ void init_notes_merge_options(struct notes_merge_options *o)}\}while(0)+voidcreate_notes_commit(structnotes_tree*t,structcommit_list*parents,+constchar*msg,unsignedchar*result_sha1)+{+unsignedchartree_sha1[20];++assert(t->initialized);++if(write_notes_tree(t,tree_sha1))+die("Failed to write notes tree to database");++if(!parents){+/* Deduce parent commit from t->ref */+unsignedcharparent_sha1[20];+if(!read_ref(t->ref,parent_sha1)){+structcommit*parent=lookup_commit(parent_sha1);+if(!parent||parse_commit(parent))+die("Failed to find/parse commit %s",t->ref);+commit_list_insert(parent,&parents);+}+/* else: t->ref points to nothing, assume root/orphan commit */+}++if(commit_tree(msg,tree_sha1,parents,result_sha1,NULL))+die("Failed to commit notes tree to database");+}+intnotes_merge(structnotes_merge_options*o,unsignedchar*result_sha1){
@@ -384,6 +386,12 @@ static int merge_one_change_manual(struct notes_merge_options *o,sha1_to_hex(p->obj),sha1_to_hex(p->base),sha1_to_hex(p->local),sha1_to_hex(p->remote));+/* add "Conflicts:" section to commit message first time through */+if(!o->has_worktree)+strbuf_addstr(&(o->commit_msg),"\n\nConflicts:\n");++strbuf_addf(&(o->commit_msg),"\t%s\n",sha1_to_hex(p->obj));+OUTPUT(o,2,"Auto-merging notes for %s",sha1_to_hex(p->obj));check_notes_merge_worktree(o);if(is_null_sha1(p->local)){
@@ -639,12 +647,13 @@ int notes_merge(struct notes_merge_options *o,structcommit_list*parents=NULL;commit_list_insert(remote,&parents);/* LIFO order */commit_list_insert(local,&parents);-create_notes_commit(local_tree,parents,o->commit_msg,+create_notes_commit(local_tree,parents,o->commit_msg.buf,result_sha1);}found_result:free_commit_list(bases);+strbuf_release(&(o->commit_msg));trace_printf("notes_merge(): result = %i, result_sha1 = %.7s\n",result,sha1_to_hex(result_sha1));returnresult;
@@ -333,6 +333,12 @@ EOFgitlog-1--format=%Brefs/notes/m>merge_commit_msg&&grep-qrefs/notes/mmerge_commit_msg&&grep-qrefs/notes/zmerge_commit_msg&&+# Merge commit mentions conflicting notes+grep-q"Conflicts"merge_commit_msg&&+(forsha1in$(catexpect_conflicts);do+grep-q"$sha1"merge_commit_msg||+exit1+done)&&# Verify contents of merge resultverify_notesm&&# Verify that other notes refs has not changed (w, x, y and z)
@@ -456,6 +462,12 @@ EOFgitlog-1--format=%Brefs/notes/m>merge_commit_msg&&grep-qrefs/notes/mmerge_commit_msg&&grep-qrefs/notes/zmerge_commit_msg&&+# Merge commit mentions conflicting notes+grep-q"Conflicts"merge_commit_msg&&+(forsha1in$(catexpect_conflicts);do+grep-q"$sha1"merge_commit_msg||+exit1+done)&&# Verify contents of merge resultverify_notesm&&# Verify that other notes refs has not changed (w, x, y and z)
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
When manually resolving a notes merge, if the merging ref has moved since
the merge started, we should fail to complete the merge, and alert the user
to what's going on.
This situation may arise if you start a 'git notes merge' which results in
conflicts, and you then update the current notes ref (using for example
'git notes add/copy/amend/edit/remove/prune', 'git update-ref', etc.),
before you get around to resolving the notes conflicts and calling
'git notes merge --commit'.
We detect this situation by comparing the first parent of the partial merge
commit (which was created when the merge started) to the current value of the
merging notes ref (pointed to by the .git/NOTES_MERGE_REF symref).
If we don't fail in this situation, the notes merge commit would overwrite
the updated notes ref, thus losing the changes that happened in the meantime.
The patch includes a testcase verifying that we fail correctly in this
situation.
Signed-off-by: Johan Herland <redacted>
---
builtin/notes.c | 11 ++++-
t/t3310-notes-merge-manual-resolve.sh | 76 +++++++++++++++++++++++++++++++++
2 files changed, 85 insertions(+), 2 deletions(-)
@@ -786,7 +786,7 @@ static int merge_reset(struct notes_merge_options *o)staticintmerge_commit(structnotes_merge_options*o){structstrbufmsg=STRBUF_INIT;-unsignedcharsha1[20];+unsignedcharsha1[20],parent_sha1[20];structnotes_tree*t;structcommit*partial;structpretty_print_contextpretty_ctx;
@@ -803,6 +803,11 @@ static int merge_commit(struct notes_merge_options *o)elseif(parse_commit(partial))die("Could not parse commit from NOTES_MERGE_PARTIAL.");+if(partial->parents)+hashcpy(parent_sha1,partial->parents->item->object.sha1);+else+hashclr(parent_sha1);+t=xcalloc(1,sizeof(structnotes_tree));init_notes(t,"NOTES_MERGE_PARTIAL",combine_notes_overwrite,0);
@@ -818,7 +823,9 @@ static int merge_commit(struct notes_merge_options *o)format_commit_message(partial,"%s",&msg,&pretty_ctx);strbuf_trim(&msg);strbuf_insert(&msg,0,"notes: ",7);-update_ref(msg.buf,o->local_ref,sha1,NULL,0,DIE_ON_ERR);+update_ref(msg.buf,o->local_ref,sha1,+is_null_sha1(parent_sha1)?NULL:parent_sha1,+0,DIE_ON_ERR);free_notes(t);strbuf_release(&msg);
@@ -477,4 +477,80 @@ EOFverify_notesz'+cpexpect_notes_yexpect_notes_m+cpexpect_log_yexpect_log_m++test_expect_success'redo merge of z into m (== y) with default ("manual") resolver => Conflicting 3-way merge''+gitupdate-refrefs/notes/mrefs/notes/y&&+test_must_failgitnotesmergez>output&&+# Output should point to where to resolve conflicts+grep-q"\\.git/NOTES_MERGE_WORKTREE"output&&+# Inspect merge conflicts+ls.git/NOTES_MERGE_WORKTREE>output_conflicts&&+test_cmpexpect_conflictsoutput_conflicts&&+(forfin$(catexpect_conflicts);do+test_cmp"expect_conflict_$f"".git/NOTES_MERGE_WORKTREE/$f"||+exit1+done)&&+# Verify that current notes tree (pre-merge) has not changed (m == y)+verify_notesy&&+verify_notesm&&+test"$(gitrev-parserefs/notes/m)"="$(catpre_merge_y)"+'++cpexpect_notes_wexpect_notes_m+cpexpect_log_wexpect_log_m++test_expect_success'reset notes ref m to somewhere else (w)''+gitupdate-refrefs/notes/mrefs/notes/w&&+verify_notesm&&+test"$(gitrev-parserefs/notes/m)"="$(gitrev-parserefs/notes/w)"+'++test_expect_success'fail to finalize conflicting merge if underlying ref has moved in the meantime (m != NOTES_MERGE_PARTIAL^1)''+# Resolve conflicts+cat>.git/NOTES_MERGE_WORKTREE/$commit_sha1<<EOF&&+yandznoteson1stcommit+EOF+cat>.git/NOTES_MERGE_WORKTREE/$commit_sha4<<EOF&&+yandznoteson4thcommit+EOF+# Fail to finalize merge+test_must_failgitnotesmerge--commit>output2>&1&&+# .git/NOTES_MERGE_* must remain+test-f.git/NOTES_MERGE_PARTIAL&&+test-f.git/NOTES_MERGE_REF&&+test-f.git/NOTES_MERGE_WORKTREE/$commit_sha1&&+test-f.git/NOTES_MERGE_WORKTREE/$commit_sha2&&+test-f.git/NOTES_MERGE_WORKTREE/$commit_sha3&&+test-f.git/NOTES_MERGE_WORKTREE/$commit_sha4&&+# Refs are unchanged+test"$(gitrev-parserefs/notes/m)"="$(gitrev-parserefs/notes/w)"+test"$(gitrev-parserefs/notes/y)"="$(gitrev-parseNOTES_MERGE_PARTIAL^1)"+test"$(gitrev-parserefs/notes/m)"!="$(gitrev-parseNOTES_MERGE_PARTIAL^1)"+# Mention refs/notes/m, and its current and expected value in output+grep-q"refs/notes/m"output&&+grep-q"$(gitrev-parserefs/notes/m)"output&&+grep-q"$(gitrev-parseNOTES_MERGE_PARTIAL^1)"output&&+# Verify that other notes refs has not changed (w, x, y and z)+verify_notesw&&+verify_notesx&&+verify_notesy&&+verify_notesz+'++test_expect_success'resolve situation by aborting the notes merge''+gitnotesmerge--reset&&+# No .git/NOTES_MERGE_* files left+test_must_faills.git/NOTES_MERGE_*>output2>/dev/null&&+test_cmp/dev/nulloutput&&+# m has not moved (still == w)+test"$(gitrev-parserefs/notes/m)"="$(gitrev-parserefs/notes/w)"+# Verify that other notes refs has not changed (w, x, y and z)+verify_notesw&&+verify_notesx&&+verify_notesy&&+verify_notesz+'+ test_done
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
Conflicts (that are to be resolved manually) are written into a special-
purpose working tree, located at .git/NOTES_MERGE_WORKTREE. Within this
directory, conflicting notes entries are stored (with conflict markers
produced by ll_merge()) using the SHA1 of the annotated object. The
.git/NOTES_MERGE_WORKTREE directory will only contain the _conflicting_
note entries. The non-conflicting note entries (aka. the partial merge
result) are stored in 'local_tree', and the SHA1 of the resulting commit
is written to 'result_sha1'. The return value from notes_merge() is -1.
The user is told to edit the files within the .git/NOTES_MERGE_WORKTREE
directory in order to resolve the conflicts.
The patch also contains documentation and testcases for the correct setup
of .git/NOTES_MERGE_WORKTREE.
The next part will recombine the partial notes merge result with the
resolved conflicts in .git/NOTES_MERGE_WORKTREE to produce the complete
merge result.
This patch has been improved by the following contributions:
- Jonathan Nieder: Use trace_printf(...) instead of OUTPUT(o, 5, ...)
Thanks-to: Jonathan Nieder [off-list ref]
Signed-off-by: Johan Herland <redacted>
---
Documentation/git-notes.txt | 10 +-
builtin/notes.c | 8 +-
notes-merge.c | 166 ++++++++++++++++++-
notes-merge.h | 11 +-
t/t3310-notes-merge-manual-resolve.sh | 292 +++++++++++++++++++++++++++++++++
5 files changed, 474 insertions(+), 13 deletions(-)
create mode 100755 t/t3310-notes-merge-manual-resolve.sh
@@ -92,7 +92,9 @@ merge:: + If conflicts arise and a strategy for automatically resolving conflicting notes (see the -s/--strategy option) is not given,-the merge fails (TODO).+the "manual" resolver is used. This resolver checks out the+conflicting notes in a special worktree (`.git/NOTES_MERGE_WORKTREE`),+and instructs the user to manually resolve the conflicts there. remove:: Remove the notes for a given object (defaults to HEAD).
@@ -191,8 +193,10 @@ object, in which case the history of the notes can be read with NOTES MERGE STRATEGIES -----------------------The default notes merge strategy is "manual", which is not yet-implemented (TODO).+The default notes merge strategy is "manual", which checks out+conflicting notes in a special work tree for resolving notes conflicts+(`.git/NOTES_MERGE_WORKTREE`), and instructs the user to resolve the+conflicts in that work tree. "ours" automatically resolves conflicting notes in favor of the local version (i.e. the current notes ref).
@@ -263,16 +266,169 @@ static void diff_tree_local(struct notes_merge_options *o,diff_tree_release_paths(&opt);}+staticvoidcheck_notes_merge_worktree(structnotes_merge_options*o)+{+if(!o->has_worktree){+/*+*MustestablishNOTES_MERGE_WORKTREE.+*AbortifNOTES_MERGE_WORKTREEalreadyexists+*/+if(file_exists(git_path(NOTES_MERGE_WORKTREE))){+if(advice_resolve_conflict)+die("You have not concluded your previous "+"notes merge (%s exists).\nPlease, use "+"'git notes merge --commit' or 'git notes "+"merge --reset' to commit/abort the "+"previous merge before you start a new "+"notes merge.",git_path("NOTES_MERGE_*"));+else+die("You have not concluded your notes merge "+"(%s exists).",git_path("NOTES_MERGE_*"));+}++if(safe_create_leading_directories(git_path(+NOTES_MERGE_WORKTREE"/.test")))+die_errno("unable to create directory %s",+git_path(NOTES_MERGE_WORKTREE));+o->has_worktree=1;+}elseif(!file_exists(git_path(NOTES_MERGE_WORKTREE)))+/* NOTES_MERGE_WORKTREE should already be established */+die("missing '%s'. This should not happen",+git_path(NOTES_MERGE_WORKTREE));+}++staticvoidwrite_buf_to_worktree(constunsignedchar*obj,+constchar*buf,unsignedlongsize)+{+intfd;+char*path=git_path(NOTES_MERGE_WORKTREE"/%s",sha1_to_hex(obj));+if(safe_create_leading_directories(path))+die_errno("unable to create directory for '%s'",path);+if(file_exists(path))+die("found existing file at '%s'",path);++fd=open(path,O_WRONLY|O_TRUNC|O_CREAT,0666);+if(fd<0)+die_errno("failed to open '%s'",path);++while(size>0){+longret=write_in_full(fd,buf,size);+if(ret<0){+/* Ignore epipe */+if(errno==EPIPE)+break;+die_errno("notes-merge");+}elseif(!ret){+die("notes-merge: disk full?");+}+size-=ret;+buf+=ret;+}++close(fd);+}++staticvoidwrite_note_to_worktree(constunsignedchar*obj,+constunsignedchar*note)+{+enumobject_typetype;+unsignedlongsize;+void*buf=read_sha1_file(note,&type,&size);++if(!buf)+die("cannot read note %s for object %s",+sha1_to_hex(note),sha1_to_hex(obj));+if(type!=OBJ_BLOB)+die("blob expected in note %s for object %s",+sha1_to_hex(note),sha1_to_hex(obj));+write_buf_to_worktree(obj,buf,size);+free(buf);+}++staticintll_merge_in_worktree(structnotes_merge_options*o,+structnotes_merge_pair*p)+{+mmbuffer_tresult_buf;+mmfile_tbase,local,remote;+intstatus;++read_mmblob(&base,p->base);+read_mmblob(&local,p->local);+read_mmblob(&remote,p->remote);++status=ll_merge(&result_buf,sha1_to_hex(p->obj),&base,NULL,+&local,o->local_ref,&remote,o->remote_ref,0);++free(base.ptr);+free(local.ptr);+free(remote.ptr);++if((status<0)||!result_buf.ptr)+die("Failed to execute internal merge");++write_buf_to_worktree(p->obj,result_buf.ptr,result_buf.size);+free(result_buf.ptr);++returnstatus;+}++staticintmerge_one_change_manual(structnotes_merge_options*o,+structnotes_merge_pair*p,+structnotes_tree*t)+{+constchar*lref=o->local_ref?o->local_ref:"local version";+constchar*rref=o->remote_ref?o->remote_ref:"remote version";++trace_printf("\t\t\tmerge_one_change_manual(obj = %.7s, base = %.7s, "+"local = %.7s, remote = %.7s)\n",+sha1_to_hex(p->obj),sha1_to_hex(p->base),+sha1_to_hex(p->local),sha1_to_hex(p->remote));++OUTPUT(o,2,"Auto-merging notes for %s",sha1_to_hex(p->obj));+check_notes_merge_worktree(o);+if(is_null_sha1(p->local)){+/* D/F conflict, checkout p->remote */+assert(!is_null_sha1(p->remote));+OUTPUT(o,1,"CONFLICT (delete/modify): Notes for object %s "+"deleted in %s and modified in %s. Version from %s "+"left in tree.",sha1_to_hex(p->obj),lref,rref,rref);+write_note_to_worktree(p->obj,p->remote);+}elseif(is_null_sha1(p->remote)){+/* D/F conflict, checkout p->local */+assert(!is_null_sha1(p->local));+OUTPUT(o,1,"CONFLICT (delete/modify): Notes for object %s "+"deleted in %s and modified in %s. Version from %s "+"left in tree.",sha1_to_hex(p->obj),rref,lref,lref);+write_note_to_worktree(p->obj,p->local);+}else{+/* "regular" conflict, checkout result of ll_merge() */+constchar*reason="content";+if(is_null_sha1(p->base))+reason="add/add";+assert(!is_null_sha1(p->local));+assert(!is_null_sha1(p->remote));+OUTPUT(o,1,"CONFLICT (%s): Merge conflict in notes for "+"object %s",reason,sha1_to_hex(p->obj));+ll_merge_in_worktree(o,p);+}++trace_printf("\t\t\tremoving from partial merge result\n");+remove_note(t,p->obj);++return1;+}+staticintmerge_one_change(structnotes_merge_options*o,structnotes_merge_pair*p,structnotes_tree*t){/*-*Return0ifchangewasresolved(andaddedtonotes_tree),-*1ifconflict+*Return0ifchangeissuccessfullyresolved(storedinnotes_tree).+*Return1ischangeresultsinaconflict(NOTstoredinnotes_tree,+*butinsteadwrittentoNOTES_MERGE_WORKTREEwithconflictmarkers).*/switch(o->strategy){caseNOTES_MERGE_RESOLVE_MANUAL:-return1;+returnmerge_one_change_manual(o,p,t);caseNOTES_MERGE_RESOLVE_OURS:OUTPUT(o,2,"Using local notes for %s",sha1_to_hex(p->obj));/* nothing to do */
@@ -478,8 +634,8 @@ int notes_merge(struct notes_merge_options *o,result=merge_from_diffs(o,base_tree_sha1,local->tree->object.sha1,remote->tree->object.sha1,local_tree);-if(result>0){/* successful non-trivial merge */-/* Commit result */+if(result!=0){/* non-trivial merge (with or without conflicts) */+/* Commit (partial) result */structcommit_list*parents=NULL;commit_list_insert(remote,&parents);/* LIFO order */commit_list_insert(local,&parents);
@@ -0,0 +1,292 @@+#!/bin/sh+#+# Copyright (c) 2010 Johan Herland+#++test_description='Test notes merging with manual conflict resolution'++../test-lib.sh++# Set up a notes merge scenario with different kinds of conflicts+test_expect_success'setup commits''+test_commit1st&&+test_commit2nd&&+test_commit3rd&&+test_commit4th&&+test_commit5th+'++commit_sha1=$(gitrev-parse1st^{commit})+commit_sha2=$(gitrev-parse2nd^{commit})+commit_sha3=$(gitrev-parse3rd^{commit})+commit_sha4=$(gitrev-parse4th^{commit})+commit_sha5=$(gitrev-parse5th^{commit})++verify_notes(){+notes_ref="$1"+git-ccore.notesRef="refs/notes/$notes_ref"notes|+sort>"output_notes_$notes_ref"&&+test_cmp"expect_notes_$notes_ref""output_notes_$notes_ref"&&+git-ccore.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+6e8e3febca3c2bb896704335cc4d0c34cb2f8715$commit_sha4+e5388c10860456ee60673025345fe2e153eb8cf8$commit_sha3+ceefa674873670e7ecd131814d909723cce2b669$commit_sha2+EOF++cat>expect_log_x<<EOF+$commit_sha55th++$commit_sha44th+xnoteson4thcommit++$commit_sha33rd+xnoteson3rdcommit++$commit_sha22nd+xnoteson2ndcommit++$commit_sha11st++EOF++test_expect_success'setup merge base (x)''+gitconfigcore.notesRefrefs/notes/x&&+gitnotesadd-m"x notes on 2nd commit"2nd&&+gitnotesadd-m"x notes on 3rd commit"3rd&&+gitnotesadd-m"x notes on 4th commit"4th&&+verify_notesx+'++cat<<EOF|sort>expect_notes_y+e2bfd06a37dd2031684a59a6e2b033e212239c78$commit_sha4+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1$commit_sha3+b0a6021ec006d07e80e9b20ec9b444cbd9d560d3$commit_sha1+EOF++cat>expect_log_y<<EOF+$commit_sha55th++$commit_sha44th+ynoteson4thcommit++$commit_sha33rd+ynoteson3rdcommit++$commit_sha22nd++$commit_sha11st+ynoteson1stcommit++EOF++test_expect_success'setup local branch (y)''+gitupdate-refrefs/notes/yrefs/notes/x&&+gitconfigcore.notesRefrefs/notes/y&&+gitnotesadd-f-m"y notes on 1st commit"1st&&+gitnotesremove2nd&&+gitnotesadd-f-m"y notes on 3rd commit"3rd&&+gitnotesadd-f-m"y notes on 4th commit"4th&&+verify_notesy+'++cat<<EOF|sort>expect_notes_z+cff59c793c20bb49a4e01bc06fb06bad642e0d54$commit_sha4+283b48219aee9a4105f6cab337e789065c82c2b9$commit_sha2+0a81da8956346e19bcb27a906f04af327e03e31b$commit_sha1+EOF++cat>expect_log_z<<EOF+$commit_sha55th++$commit_sha44th+znoteson4thcommit++$commit_sha33rd++$commit_sha22nd+znoteson2ndcommit++$commit_sha11st+znoteson1stcommit++EOF++test_expect_success'setup remote branch (z)''+gitupdate-refrefs/notes/zrefs/notes/x&&+gitconfigcore.notesRefrefs/notes/z&&+gitnotesadd-f-m"z notes on 1st commit"1st&&+gitnotesadd-f-m"z notes on 2nd commit"2nd&&+gitnotesremove3rd&&+gitnotesadd-f-m"z notes on 4th commit"4th&&+verify_notesz+'++# At this point, before merging z into y, we have the following status:+#+# commit | base/x | local/y | remote/z | diff from x to y/z+# -------|---------|---------|----------|---------------------------+# 1st | [none] | b0a6021 | 0a81da8 | added / added (diff)+# 2nd | ceefa67 | [none] | 283b482 | removed / changed+# 3rd | e5388c1 | 5772f42 | [none] | changed / removed+# 4th | 6e8e3fe | e2bfd06 | cff59c7 | changed / changed (diff)+# 5th | [none] | [none] | [none] | [none]++cat<<EOF|sort>expect_conflicts+$commit_sha1+$commit_sha2+$commit_sha3+$commit_sha4+EOF++cat>expect_conflict_$commit_sha1<<EOF+<<<<<<<refs/notes/m+ynoteson1stcommit+=======+znoteson1stcommit+>>>>>>>refs/notes/z+EOF++cat>expect_conflict_$commit_sha2<<EOF+znoteson2ndcommit+EOF++cat>expect_conflict_$commit_sha3<<EOF+ynoteson3rdcommit+EOF++cat>expect_conflict_$commit_sha4<<EOF+<<<<<<<refs/notes/m+ynoteson4thcommit+=======+znoteson4thcommit+>>>>>>>refs/notes/z+EOF++cpexpect_notes_yexpect_notes_m+cpexpect_log_yexpect_log_m++gitrev-parserefs/notes/y>pre_merge_y++test_expect_success'merge z into m (== y) with default ("manual") resolver => Conflicting 3-way merge''+gitupdate-refrefs/notes/mrefs/notes/y&&+gitconfigcore.notesRefrefs/notes/m&&+test_must_failgitnotesmergez>output&&+# Output should point to where to resolve conflicts+grep-q"\\.git/NOTES_MERGE_WORKTREE"output&&+# Inspect merge conflicts+ls.git/NOTES_MERGE_WORKTREE>output_conflicts&&+test_cmpexpect_conflictsoutput_conflicts&&+(forfin$(catexpect_conflicts);do+test_cmp"expect_conflict_$f"".git/NOTES_MERGE_WORKTREE/$f"||+exit1+done)&&+# Verify that current notes tree (pre-merge) has not changed (m == y)+verify_notesy&&+verify_notesm&&+test"$(gitrev-parserefs/notes/m)"="$(catpre_merge_y)"+'++cat<<EOF|sort>expect_notes_z+00494adecf2d9635a02fa431308d67993f853968$commit_sha4+283b48219aee9a4105f6cab337e789065c82c2b9$commit_sha2+0a81da8956346e19bcb27a906f04af327e03e31b$commit_sha1+EOF++cat>expect_log_z<<EOF+$commit_sha55th++$commit_sha44th+znoteson4thcommit++Moreznoteson4thcommit++$commit_sha33rd++$commit_sha22nd+znoteson2ndcommit++$commit_sha11st+znoteson1stcommit++EOF++test_expect_success'change notes in z''+gitnotes--refzappend-m"More z notes on 4th commit"4th&&+verify_notesz+'++test_expect_success'cannot do merge w/conflicts when previous merge is unfinished''+test-d.git/NOTES_MERGE_WORKTREE&&+test_must_failgitnotesmergez>output2>&1&&+# Output should indicate what is wrong+grep-q"\\.git/NOTES_MERGE_\\* exists"output+'++# Setup non-conflicting merge between x and new notes ref w++cat<<EOF|sort>expect_notes_w+ceefa674873670e7ecd131814d909723cce2b669$commit_sha2+f75d1df88cbfe4258d49852f26cfc83f2ad4494b$commit_sha1+EOF++cat>expect_log_w<<EOF+$commit_sha55th++$commit_sha44th++$commit_sha33rd++$commit_sha22nd+xnoteson2ndcommit++$commit_sha11st+wnoteson1stcommit++EOF++test_expect_success'setup unrelated notes ref (w)''+gitconfigcore.notesRefrefs/notes/w&&+gitnotesadd-m"w notes on 1st commit"1st&&+gitnotesadd-m"x notes on 2nd commit"2nd&&+verify_notesw+'++cat<<EOF|sort>expect_notes_w+6e8e3febca3c2bb896704335cc4d0c34cb2f8715$commit_sha4+e5388c10860456ee60673025345fe2e153eb8cf8$commit_sha3+ceefa674873670e7ecd131814d909723cce2b669$commit_sha2+f75d1df88cbfe4258d49852f26cfc83f2ad4494b$commit_sha1+EOF++cat>expect_log_w<<EOF+$commit_sha55th++$commit_sha44th+xnoteson4thcommit++$commit_sha33rd+xnoteson3rdcommit++$commit_sha22nd+xnoteson2ndcommit++$commit_sha11st+wnoteson1stcommit++EOF++test_expect_success'can do merge without conflicts even if previous merge is unfinished (x => w)''+test-d.git/NOTES_MERGE_WORKTREE&&+gitnotesmergex&&+verify_notesw&&+# Verify that other notes refs has not changed (x and y)+verify_notesx&&+verify_notesy+'++test_done
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
When the notes merge conflicts in .git/NOTES_MERGE_WORKTREE have been
resolved, we need to record a new notes commit on the appropriate notes
ref with the resolved notes.
This patch implements 'git notes merge --commit' which the user should
run after resolving conflicts in the notes merge worktree. This command
finalizes the notes merge by recombining the partial notes tree from
part 1 with the now-resolved conflicts in the notes merge worktree in a
merge commit, and updating the appropriate ref to this merge commit.
In order to correctly finalize the merge, we need to keep track of three
things:
- The partial merge result from part 1, containing the auto-merged notes.
This is now stored into a ref called .git/NOTES_MERGE_PARTIAL.
- The unmerged notes. These are already stored in
.git/NOTES_MERGE_WORKTREE, thanks to part 1.
- The notes ref to be updated by the finalized merge result. This is now
stored in a symref called .git/NOTES_MERGE_REF.
In addition to "git notes merge --commit", which uses the above details
to create the finalized notes merge commit, this patch also implements
"git notes merge --reset", which aborts the ongoing notes merge by simply
removing the files/directory described above.
FTR, "git notes merge --commit" reuses "git notes merge --reset" to remove
the information described above (.git/NOTES_MERGE_*) after the notes merge
have been successfully finalized.
The patch also contains documentation and testcases for the two new options.
This patch has been improved by the following contributions:
- Ævar Arnfjörð Bjarmason: Fix nonsense sentence in --commit description
Thanks-to: Ævar Arnfjörð Bjarmason [off-list ref]
Signed-off-by: Johan Herland <redacted>
---
Documentation/git-notes.txt | 22 ++++
builtin/notes.c | 106 +++++++++++++++++++-
notes-merge.c | 69 +++++++++++++
notes-merge.h | 23 +++++
t/t3310-notes-merge-manual-resolve.sh | 176 +++++++++++++++++++++++++++++++++
5 files changed, 393 insertions(+), 3 deletions(-)
@@ -95,6 +97,9 @@ conflicting notes (see the -s/--strategy option) is not given, the "manual" resolver is used. This resolver checks out the conflicting notes in a special worktree (`.git/NOTES_MERGE_WORKTREE`), and instructs the user to manually resolve the conflicts there.+When done, the user can either finalize the merge with+'git notes merge --commit', or abort the merge with+'git notes merge --reset'. remove:: Remove the notes for a given object (defaults to HEAD).
@@ -154,6 +159,20 @@ OPTIONS See the "NOTES MERGE STRATEGIES" section below for more information on each notes merge strategy.+--commit::+ Finalize an in-progress 'git notes merge'. Use this option+ when you have resolved the conflicts that 'git notes merge'+ stored in .git/NOTES_MERGE_WORKTREE. This amends the partial+ merge commit created by 'git notes merge' (stored in+ .git/NOTES_MERGE_PARTIAL) by adding the notes in+ .git/NOTES_MERGE_WORKTREE. The notes ref stored in the+ .git/NOTES_MERGE_REF symref is updated to the resulting commit.++--reset::+ Reset/abort a in-progress 'git notes merge', i.e. a notes merge+ with conflicts. This simply removes all files related to the+ notes merge.+ -q:: --quiet:: When merging notes, operate quietly.
@@ -197,6 +216,9 @@ The default notes merge strategy is "manual", which checks out conflicting notes in a special work tree for resolving notes conflicts (`.git/NOTES_MERGE_WORKTREE`), and instructs the user to resolve the conflicts in that work tree.+When done, the user can either finalize the merge with+'git notes merge --commit', or abort the merge with+'git notes merge --reset'. "ours" automatically resolves conflicting notes in favor of the local version (i.e. the current notes ref).
@@ -761,33 +765,119 @@ static int show(int argc, const char **argv, const char *prefix)returnretval;}+staticintmerge_reset(structnotes_merge_options*o)+{+intret=0;++/*+*Remove.git/NOTES_MERGE_PARTIALand.git/NOTES_MERGE_REF,andcall+*notes_merge_reset()toremove.git/NOTES_MERGE_WORKTREE.+*/++if(delete_ref("NOTES_MERGE_PARTIAL",NULL,0))+ret+=error("Failed to delete ref NOTES_MERGE_PARTIAL");+if(delete_ref("NOTES_MERGE_REF",NULL,REF_NODEREF))+ret+=error("Failed to delete ref NOTES_MERGE_REF");+if(notes_merge_reset(o))+ret+=error("Failed to remove 'git notes merge' worktree");+returnret;+}++staticintmerge_commit(structnotes_merge_options*o)+{+structstrbufmsg=STRBUF_INIT;+unsignedcharsha1[20];+structnotes_tree*t;+structcommit*partial;+structpretty_print_contextpretty_ctx;++/*+*Readpartialmergeresultfrom.git/NOTES_MERGE_PARTIAL,+*andtargetnotesreffrom.git/NOTES_MERGE_REF.+*/++if(get_sha1("NOTES_MERGE_PARTIAL",sha1))+die("Failed to read ref NOTES_MERGE_PARTIAL");+elseif(!(partial=lookup_commit_reference(sha1)))+die("Could not find commit from NOTES_MERGE_PARTIAL.");+elseif(parse_commit(partial))+die("Could not parse commit from NOTES_MERGE_PARTIAL.");++t=xcalloc(1,sizeof(structnotes_tree));+init_notes(t,"NOTES_MERGE_PARTIAL",combine_notes_overwrite,0);++o->local_ref=resolve_ref("NOTES_MERGE_REF",sha1,0,0);+if(!o->local_ref)+die("Failed to resolve NOTES_MERGE_REF");++if(notes_merge_commit(o,t,partial,sha1))+die("Failed to finalize notes merge");++/* Reuse existing commit message in reflog message */+memset(&pretty_ctx,0,sizeof(pretty_ctx));+format_commit_message(partial,"%s",&msg,&pretty_ctx);+strbuf_trim(&msg);+strbuf_insert(&msg,0,"notes: ",7);+update_ref(msg.buf,o->local_ref,sha1,NULL,0,DIE_ON_ERR);++free_notes(t);+strbuf_release(&msg);+returnmerge_reset(o);+}+staticintmerge(intargc,constchar**argv,constchar*prefix){structstrbufremote_ref=STRBUF_INIT,msg=STRBUF_INIT;unsignedcharresult_sha1[20];structnotes_tree*t;structnotes_merge_optionso;+intdo_merge=0,do_commit=0,do_reset=0;intverbosity=0,result;constchar*strategy=NULL;structoptionoptions[]={+OPT_GROUP("General options"),OPT__VERBOSITY(&verbosity),+OPT_GROUP("Merge options"),OPT_STRING('s',"strategy",&strategy,"strategy","resolve notes conflicts using the given ""strategy (manual/ours/theirs/union)"),+OPT_GROUP("Committing unmerged notes"),+{OPTION_BOOLEAN,0,"commit",&do_commit,NULL,+"finalize notes merge by committing unmerged notes",+PARSE_OPT_NOARG|PARSE_OPT_NONEG},+OPT_GROUP("Aborting notes merge resolution"),+{OPTION_BOOLEAN,0,"reset",&do_reset,NULL,+"abort notes merge",+PARSE_OPT_NOARG|PARSE_OPT_NONEG},OPT_END()};argc=parse_options(argc,argv,prefix,options,git_notes_merge_usage,0);-if(argc!=1){+if(strategy||do_commit+do_reset==0)+do_merge=1;+if(do_commit+do_reset+do_merge!=1){+error("cannot mix --commit, --reset or -s/--strategy");+usage_with_options(git_notes_merge_usage,options);+}++if(do_merge&&argc!=1){error("Must specify a notes ref to merge");usage_with_options(git_notes_merge_usage,options);+}elseif(!do_merge&&argc){+error("too many parameters");+usage_with_options(git_notes_merge_usage,options);}init_notes_merge_options(&o);o.verbosity=verbosity+NOTES_MERGE_VERBOSITY_DEFAULT;+if(do_reset)+returnmerge_reset(&o);+if(do_commit)+returnmerge_commit(&o);+o.local_ref=default_notes_ref();strbuf_addstr(&remote_ref,argv[0]);expand_notes_ref(&remote_ref);
@@ -820,9 +910,19 @@ static int merge(int argc, const char **argv, const char *prefix)/* Update default notes ref with new commit */update_ref(msg.buf,default_notes_ref(),result_sha1,NULL,0,DIE_ON_ERR);-else/* Merge has unresolved conflicts */-printf("Automatic notes merge failed. Fix conflicts in %s.\n",+else{/* Merge has unresolved conflicts */+/* Update .git/NOTES_MERGE_PARTIAL with partial merge result */+update_ref(msg.buf,"NOTES_MERGE_PARTIAL",result_sha1,NULL,+0,DIE_ON_ERR);+/* Store ref-to-be-updated into .git/NOTES_MERGE_REF */+if(create_symref("NOTES_MERGE_REF",default_notes_ref(),NULL))+die("Failed to store link to current notes ref (%s)",+default_notes_ref());+printf("Automatic notes merge failed. Fix conflicts in %s and "+"commit the result with 'git notes merge --commit', or "+"abort the merge with 'git notes merge --reset'.\n",git_path(NOTES_MERGE_WORKTREE));+}free_notes(t);strbuf_release(&remote_ref);
@@ -649,3 +649,72 @@ found_result:result,sha1_to_hex(result_sha1));returnresult;}++intnotes_merge_commit(structnotes_merge_options*o,+structnotes_tree*partial_tree,+structcommit*partial_commit,+unsignedchar*result_sha1)+{+/*+*Iteratethroughfilesin.git/NOTES_MERGE_WORKTREEandaddall+*foundnotesto'partial_tree'.Writetheupdatesnotestreeto+*theDB,andcommittheresultingtreeobjectwhilereusingthe+*commitmessageandparentsfrom'partial_commit'.+*FinallystorethenewcommitobjectSHA1into'result_sha1'.+*/+structdir_structdir;+constchar*path=git_path(NOTES_MERGE_WORKTREE"/");+intpath_len=strlen(path),i;+constchar*msg=strstr(partial_commit->buffer,"\n\n");++OUTPUT(o,3,"Committing notes in notes merge worktree at %.*s",+path_len-1,path);++if(!msg||msg[2]=='\0')+die("partial notes commit has empty message");+msg+=2;++memset(&dir,0,sizeof(dir));+read_directory(&dir,path,path_len,NULL);+for(i=0;i<dir.nr;i++){+structdir_entry*ent=dir.entries[i];+structstatst;+constchar*relpath=ent->name+path_len;+unsignedcharobj_sha1[20],blob_sha1[20];++if(ent->len-path_len!=40||get_sha1_hex(relpath,obj_sha1)){+OUTPUT(o,3,"Skipping non-SHA1 entry '%s'",ent->name);+continue;+}++/* write file as blob, and add to partial_tree */+if(stat(ent->name,&st))+die_errno("Failed to stat '%s'",ent->name);+if(index_path(blob_sha1,ent->name,&st,1))+die("Failed to write blob object from '%s'",ent->name);+if(add_note(partial_tree,obj_sha1,blob_sha1,NULL))+die("Failed to add resolved note '%s' to notes tree",+ent->name);+OUTPUT(o,4,"Added resolved note for object %s: %s",+sha1_to_hex(obj_sha1),sha1_to_hex(blob_sha1));+}++create_notes_commit(partial_tree,partial_commit->parents,msg,+result_sha1);+OUTPUT(o,4,"Finalized notes merge commit: %s",+sha1_to_hex(result_sha1));+return0;+}++intnotes_merge_reset(structnotes_merge_options*o)+{+/* Remove .git/NOTES_MERGE_WORKTREE directory and all files within */+structstrbufbuf=STRBUF_INIT;+intret;++strbuf_addstr(&buf,git_path(NOTES_MERGE_WORKTREE));+OUTPUT(o,3,"Removing notes merge worktree at %s",buf.buf);+ret=remove_dir_recursively(&buf,0);+strbuf_release(&buf);+returnret;+}
@@ -171,6 +171,7 @@ cp expect_notes_y expect_notes_m cpexpect_log_yexpect_log_m gitrev-parserefs/notes/y>pre_merge_y+gitrev-parserefs/notes/z>pre_merge_z test_expect_success'merge z into m (== y) with default ("manual") resolver => Conflicting 3-way merge''gitupdate-refrefs/notes/mrefs/notes/y&&
@@ -289,4 +290,179 @@ test_expect_success 'can do merge without conflicts even if previous merge is unverify_notesy'+cat<<EOF|sort>expect_notes_m+021faa20e931fb48986ffc6282b4bb05553ac946$commit_sha4+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1$commit_sha3+283b48219aee9a4105f6cab337e789065c82c2b9$commit_sha2+0a59e787e6d688aa6309e56e8c1b89431a0fc1c1$commit_sha1+EOF++cat>expect_log_m<<EOF+$commit_sha55th++$commit_sha44th+yandznoteson4thcommit++$commit_sha33rd+ynoteson3rdcommit++$commit_sha22nd+znoteson2ndcommit++$commit_sha11st+yandznoteson1stcommit++EOF++test_expect_success'finalize conflicting merge (z => m)''+# Resolve conflicts and finalize merge+cat>.git/NOTES_MERGE_WORKTREE/$commit_sha1<<EOF&&+yandznoteson1stcommit+EOF+cat>.git/NOTES_MERGE_WORKTREE/$commit_sha4<<EOF&&+yandznoteson4thcommit+EOF+gitnotesmerge--commit&&+# No .git/NOTES_MERGE_* files left+test_must_faills.git/NOTES_MERGE_*>output2>/dev/null&&+test_cmp/dev/nulloutput&&+# Merge commit has pre-merge y and pre-merge z as parents+test"$(gitrev-parserefs/notes/m^1)"="$(catpre_merge_y)"&&+test"$(gitrev-parserefs/notes/m^2)"="$(catpre_merge_z)"&&+# Merge commit mentions the notes refs merged+gitlog-1--format=%Brefs/notes/m>merge_commit_msg&&+grep-qrefs/notes/mmerge_commit_msg&&+grep-qrefs/notes/zmerge_commit_msg&&+# Verify contents of merge result+verify_notesm&&+# Verify that other notes refs has not changed (w, x, y and z)+verify_notesw&&+verify_notesx&&+verify_notesy&&+verify_notesz+'++cat>expect_conflict_$commit_sha4<<EOF+<<<<<<<refs/notes/m+ynoteson4thcommit+=======+znoteson4thcommit++Moreznoteson4thcommit+>>>>>>>refs/notes/z+EOF++cpexpect_notes_yexpect_notes_m+cpexpect_log_yexpect_log_m++gitrev-parserefs/notes/y>pre_merge_y+gitrev-parserefs/notes/z>pre_merge_z++test_expect_success'redo merge of z into m (== y) with default ("manual") resolver => Conflicting 3-way merge''+gitupdate-refrefs/notes/mrefs/notes/y&&+gitconfigcore.notesRefrefs/notes/m&&+test_must_failgitnotesmergez>output&&+# Output should point to where to resolve conflicts+grep-q"\\.git/NOTES_MERGE_WORKTREE"output&&+# Inspect merge conflicts+ls.git/NOTES_MERGE_WORKTREE>output_conflicts&&+test_cmpexpect_conflictsoutput_conflicts&&+(forfin$(catexpect_conflicts);do+test_cmp"expect_conflict_$f"".git/NOTES_MERGE_WORKTREE/$f"||+exit1+done)&&+# Verify that current notes tree (pre-merge) has not changed (m == y)+verify_notesy&&+verify_notesm&&+test"$(gitrev-parserefs/notes/m)"="$(catpre_merge_y)"+'++test_expect_success'abort notes merge''+gitnotesmerge--reset&&+# No .git/NOTES_MERGE_* files left+test_must_faills.git/NOTES_MERGE_*>output2>/dev/null&&+test_cmp/dev/nulloutput&&+# m has not moved (still == y)+test"$(gitrev-parserefs/notes/m)"="$(catpre_merge_y)"+# Verify that other notes refs has not changed (w, x, y and z)+verify_notesw&&+verify_notesx&&+verify_notesy&&+verify_notesz+'++gitrev-parserefs/notes/y>pre_merge_y+gitrev-parserefs/notes/z>pre_merge_z++test_expect_success'redo merge of z into m (== y) with default ("manual") resolver => Conflicting 3-way merge''+test_must_failgitnotesmergez>output&&+# Output should point to where to resolve conflicts+grep-q"\\.git/NOTES_MERGE_WORKTREE"output&&+# Inspect merge conflicts+ls.git/NOTES_MERGE_WORKTREE>output_conflicts&&+test_cmpexpect_conflictsoutput_conflicts&&+(forfin$(catexpect_conflicts);do+test_cmp"expect_conflict_$f"".git/NOTES_MERGE_WORKTREE/$f"||+exit1+done)&&+# Verify that current notes tree (pre-merge) has not changed (m == y)+verify_notesy&&+verify_notesm&&+test"$(gitrev-parserefs/notes/m)"="$(catpre_merge_y)"+'++cat<<EOF|sort>expect_notes_m+304dfb4325cf243025b9957486eb605a9b51c199$commit_sha5+283b48219aee9a4105f6cab337e789065c82c2b9$commit_sha2+0a59e787e6d688aa6309e56e8c1b89431a0fc1c1$commit_sha1+EOF++cat>expect_log_m<<EOF+$commit_sha55th+newnoteon5thcommit++$commit_sha44th++$commit_sha33rd++$commit_sha22nd+znoteson2ndcommit++$commit_sha11st+yandznoteson1stcommit++EOF++test_expect_success'add + remove notes in finalized merge (z => m)''+# Resolve one conflict+cat>.git/NOTES_MERGE_WORKTREE/$commit_sha1<<EOF&&+yandznoteson1stcommit+EOF+# Remove another conflict+rm.git/NOTES_MERGE_WORKTREE/$commit_sha4&&+# Remove a D/F conflict+rm.git/NOTES_MERGE_WORKTREE/$commit_sha3&&+# Add a new note+echo"new note on 5th commit">.git/NOTES_MERGE_WORKTREE/$commit_sha5&&+# Finalize merge+gitnotesmerge--commit&&+# No .git/NOTES_MERGE_* files left+test_must_faills.git/NOTES_MERGE_*>output2>/dev/null&&+test_cmp/dev/nulloutput&&+# Merge commit has pre-merge y and pre-merge z as parents+test"$(gitrev-parserefs/notes/m^1)"="$(catpre_merge_y)"&&+test"$(gitrev-parserefs/notes/m^2)"="$(catpre_merge_z)"&&+# Merge commit mentions the notes refs merged+gitlog-1--format=%Brefs/notes/m>merge_commit_msg&&+grep-qrefs/notes/mmerge_commit_msg&&+grep-qrefs/notes/zmerge_commit_msg&&+# Verify contents of merge result+verify_notesm&&+# Verify that other notes refs has not changed (w, x, y and z)+verify_notesw&&+verify_notesx&&+verify_notesy&&+verify_notesz+'+ test_done
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
The rest of the file uses tabs for indenting. Fix the one function
that doesn't.
Signed-off-by: Johan Herland <redacted>
---
t/t3303-notes-subtrees.sh | 18 +++++++++---------
1 files changed, 9 insertions(+), 9 deletions(-)
@@ -168,15 +168,15 @@ INPUT_END} verify_concatenated_notes(){-gitlog|grep"^ ">output&&-i=$number_of_commits&&-while[$i-gt0];do-echo" commit #$i"&&-echo" first note for commit #$i"&&-echo" second note for commit #$i"&&-i=$(($i-1));-done>expect&&-test_cmpexpectoutput+gitlog|grep"^ ">output&&+i=$number_of_commits&&+while[$i-gt0];do+echo" commit #$i"&&+echo" first note for commit #$i"&&+echo" second note for commit #$i"&&+i=$(($i-1));+done>expect&&+test_cmpexpectoutput} test_expect_success'test notes in no fanout concatenated with 2/38-fanout''test_concatenated_notes "s|^..|&/|" ""'
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
expand_notes_ref() is a new function that performs the DWIM transformation
of "foo" -> "refs/notes/foo" where notes refs are expected.
This is done in preparation for future patches which will also need this
DWIM functionality.
Signed-off-by: Johan Herland <redacted>
---
builtin/notes.c | 17 +++++++++++------
1 files changed, 11 insertions(+), 6 deletions(-)
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
The combine_notes_fn functions uses a non-zero return value to indicate
failure. However, this return value was converted to a call to die()
in note_tree_insert().
Instead, propagate this return value out to add_note(), and return it
from there to enable the caller to handle errors appropriately.
Existing add_note() callers are updated to die() upon failure, thus
preserving the current behaviour. The only exceptions are copy_note()
and notes_cache_put() where we are able to propagate the add_note()
return value instead.
This patch has been improved by the following contributions:
- Jonathan Nieder: Future-proof by always checking add_note() return value
Thanks-to: Jonathan Nieder [off-list ref]
Signed-off-by: Johan Herland <redacted>
---
builtin/notes.c | 11 +++++----
notes-cache.c | 3 +-
notes.c | 59 ++++++++++++++++++++++++++++--------------------------
notes.h | 11 +++++++--
4 files changed, 46 insertions(+), 38 deletions(-)
@@ -303,13 +298,17 @@ static void note_tree_insert(struct notes_tree *t, struct int_node *tree,GET_PTR_TYPE(*p)==PTR_TYPE_SUBTREE);if(is_null_sha1(entry->val_sha1)){/* skip insertion of empty note */free(entry);-return;+return0;}new_node=(structint_node*)xcalloc(sizeof(structint_node),1);-note_tree_insert(t,new_node,n+1,l,GET_PTR_TYPE(*p),-combine_notes);-*p=SET_PTR_TYPE(new_node,PTR_TYPE_INTERNAL);-note_tree_insert(t,new_node,n+1,entry,type,combine_notes);+ret=note_tree_insert(t,new_node,n+1,l,GET_PTR_TYPE(*p),+combine_notes);+if(!ret){+*p=SET_PTR_TYPE(new_node,PTR_TYPE_INTERNAL);+ret=note_tree_insert(t,new_node,n+1,entry,type,+combine_notes);+}+returnret;}/* Free the entire notes data contained in the given tree */
@@ -452,8 +451,12 @@ static void load_subtree(struct notes_tree *t, struct leaf_node *subtree,l->key_sha1[19]=(unsignedchar)len;type=PTR_TYPE_SUBTREE;}-note_tree_insert(t,node,n,l,type,-combine_notes_concatenate);+if(note_tree_insert(t,node,n,l,type,+combine_notes_concatenate))+die("Failed to load %s %s into notes tree "+"from %s",+type==PTR_TYPE_NOTE?"note":"subtree",+sha1_to_hex(l->key_sha1),t->ref);}continue;
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
This continuation of the 'git notes merge' implementation teaches notes-merge
to properly do real merges between notes trees: Two diffs are performed, one
from $base to $remote, and another from $base to $local. The paths in each
diff are normalized to SHA1 object names. The two diffs are then consolidated
into a single list of change pairs to be evaluated. Each change pair consist
of:
- The annotated object's SHA1
- The $base SHA1 (i.e. the common ancestor notes for this object)
- The $local SHA1 (i.e. the current notes for this object)
- The $remote SHA1 (i.e. the to-be-merged notes for this object)
From the pair ($base -> $local, $base -> $remote), we can determine the merge
result using regular 3-way rules. If conflicts are encountered in this
process, we fail loudly and exit (conflict handling to be added in a future
patch), If we can complete the merge without conflicts, the resulting
notes tree is committed, and the current notes ref updated.
The patch includes added testcases verifying that we can successfully do real
conflict-less merges.
This patch has been improved by the following contributions:
- Jonathan Nieder: Future-proof by always checking add_note() return value
- Stephen Boyd: Use test_commit
- Jonathan Nieder: Use trace_printf(...) instead of OUTPUT(o, 5, ...)
Thanks-to: Jonathan Nieder [off-list ref]
Thanks-to: Stephen Boyd [off-list ref]
Signed-off-by: Johan Herland <redacted>
---
builtin/notes.c | 15 ++-
notes-merge.c | 325 +++++++++++++++++++++++++++++++++++++++++++++++-
notes-merge.h | 15 ++-
t/t3308-notes-merge.sh | 188 ++++++++++++++++++++++++++++
4 files changed, 532 insertions(+), 11 deletions(-)
@@ -18,6 +24,305 @@ void init_notes_merge_options(struct notes_merge_options *o)}\}while(0)+staticintpath_to_sha1(constchar*path,unsignedchar*sha1)+{+charhex_sha1[40];+inti=0;+while(*path&&i<40){+if(*path!='/')+hex_sha1[i++]=*path;+path++;+}+if(*path||i!=40)+return-1;+returnget_sha1_hex(hex_sha1,sha1);+}++staticintverify_notes_filepair(structdiff_filepair*p,unsignedchar*sha1)+{+switch(p->status){+caseDIFF_STATUS_MODIFIED:+assert(p->one->mode==p->two->mode);+assert(!is_null_sha1(p->one->sha1));+assert(!is_null_sha1(p->two->sha1));+break;+caseDIFF_STATUS_ADDED:+assert(is_null_sha1(p->one->sha1));+break;+caseDIFF_STATUS_DELETED:+assert(is_null_sha1(p->two->sha1));+break;+default:+return-1;+}+assert(!strcmp(p->one->path,p->two->path));+returnpath_to_sha1(p->one->path,sha1);+}++staticstructnotes_merge_pair*find_notes_merge_pair_pos(+structnotes_merge_pair*list,intlen,unsignedchar*obj,+intinsert_new,int*occupied)+{+/*+*Bothdiff_tree_remote()anddiff_tree_local()tendtoprocess+*merge_pairsinascendingorder.Therefore,cachelastreturned+*index,andsearchsequentiallyfromthereuntiltheappropriate+*positionisfound.+*+*Sinceinsertsonlyhappenfromdiff_tree_remote()(whichmainly+*_appends_),wedon'tcarethatinsertingintothemiddleofthe+*listisexpensive(usingmemmove()).+*/+staticintlast_index=0;+inti=last_index<len?last_index:len-1;+intprev_cmp=0,cmp=-1;+while(i>=0&&i<len){+cmp=hashcmp(obj,list[i].obj);+if(!cmp)/* obj belongs @ i */+break;+elseif(cmp<0&&prev_cmp<=0)/* obj belongs < i */+i--;+elseif(cmp<0)/* obj belongs between i-1 and i */+break;+elseif(cmp>0&&prev_cmp>=0)/* obj belongs > i */+i++;+else/* if (cmp > 0) */{/* obj belongs between i and i+1 */+i++;+break;+}+prev_cmp=cmp;+}+if(i<0)+i=0;+/* obj belongs at, or immediately preceding, index i (0 <= i <= len) */++if(!cmp)+*occupied=1;+else{+*occupied=0;+if(insert_new&&i<len){+memmove(list+i+1,list+i,+(len-i)*sizeof(structnotes_merge_pair));+memset(list+i,0,sizeof(structnotes_merge_pair));+}+}+last_index=i;+returnlist+i;+}++staticunsignedcharuninitialized[20]=+"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"\+"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff";++staticstructnotes_merge_pair*diff_tree_remote(structnotes_merge_options*o,+constunsignedchar*base,+constunsignedchar*remote,+int*num_changes)+{+structdiff_optionsopt;+structnotes_merge_pair*changes;+inti,len=0;++trace_printf("\tdiff_tree_remote(base = %.7s, remote = %.7s)\n",+sha1_to_hex(base),sha1_to_hex(remote));++diff_setup(&opt);+DIFF_OPT_SET(&opt,RECURSIVE);+opt.output_format=DIFF_FORMAT_NO_OUTPUT;+if(diff_setup_done(&opt)<0)+die("diff_setup_done failed");+diff_tree_sha1(base,remote,"",&opt);+diffcore_std(&opt);++changes=xcalloc(diff_queued_diff.nr,sizeof(structnotes_merge_pair));++for(i=0;i<diff_queued_diff.nr;i++){+structdiff_filepair*p=diff_queued_diff.queue[i];+structnotes_merge_pair*mp;+intoccupied;+unsignedcharobj[20];++if(verify_notes_filepair(p,obj)){+trace_printf("\t\tCannot merge entry '%s' (%c): "+"%.7s -> %.7s. Skipping!\n",p->one->path,+p->status,sha1_to_hex(p->one->sha1),+sha1_to_hex(p->two->sha1));+continue;+}+mp=find_notes_merge_pair_pos(changes,len,obj,1,&occupied);+if(occupied){+/* We've found an addition/deletion pair */+assert(!hashcmp(mp->obj,obj));+if(is_null_sha1(p->one->sha1)){/* addition */+assert(is_null_sha1(mp->remote));+hashcpy(mp->remote,p->two->sha1);+}elseif(is_null_sha1(p->two->sha1)){/* deletion */+assert(is_null_sha1(mp->base));+hashcpy(mp->base,p->one->sha1);+}else+assert(!"Invalid existing change recorded");+}else{+hashcpy(mp->obj,obj);+hashcpy(mp->base,p->one->sha1);+hashcpy(mp->local,uninitialized);+hashcpy(mp->remote,p->two->sha1);+len++;+}+trace_printf("\t\tStored remote change for %s: %.7s -> %.7s\n",+sha1_to_hex(mp->obj),sha1_to_hex(mp->base),+sha1_to_hex(mp->remote));+}+diff_flush(&opt);+diff_tree_release_paths(&opt);++*num_changes=len;+returnchanges;+}++staticvoiddiff_tree_local(structnotes_merge_options*o,+structnotes_merge_pair*changes,intlen,+constunsignedchar*base,+constunsignedchar*local)+{+structdiff_optionsopt;+inti;++trace_printf("\tdiff_tree_local(len = %i, base = %.7s, local = %.7s)\n",+len,sha1_to_hex(base),sha1_to_hex(local));++diff_setup(&opt);+DIFF_OPT_SET(&opt,RECURSIVE);+opt.output_format=DIFF_FORMAT_NO_OUTPUT;+if(diff_setup_done(&opt)<0)+die("diff_setup_done failed");+diff_tree_sha1(base,local,"",&opt);+diffcore_std(&opt);++for(i=0;i<diff_queued_diff.nr;i++){+structdiff_filepair*p=diff_queued_diff.queue[i];+structnotes_merge_pair*mp;+intmatch;+unsignedcharobj[20];++if(verify_notes_filepair(p,obj)){+trace_printf("\t\tCannot merge entry '%s' (%c): "+"%.7s -> %.7s. Skipping!\n",p->one->path,+p->status,sha1_to_hex(p->one->sha1),+sha1_to_hex(p->two->sha1));+continue;+}+mp=find_notes_merge_pair_pos(changes,len,obj,0,&match);+if(!match){+trace_printf("\t\tIgnoring local-only change for %s: "+"%.7s -> %.7s\n",sha1_to_hex(obj),+sha1_to_hex(p->one->sha1),+sha1_to_hex(p->two->sha1));+continue;+}++assert(!hashcmp(mp->obj,obj));+if(is_null_sha1(p->two->sha1)){/* deletion */+/*+*Eitherthisisatruedeletion(1),oritispart+*ofanA/Dpair(2),orD/Apair(3):+*+*(1)mp->localisuninitialized;setittonull_sha1+*(2)mp->localisnotuninitialized;don'ttouchit+*(3)mp->localisuninitialized;setittonull_sha1+*(willbeoverwrittenbyfollowingaddition)+*/+if(!hashcmp(mp->local,uninitialized))+hashclr(mp->local);+}elseif(is_null_sha1(p->one->sha1)){/* addition */+/*+*Eitherthisisatrueaddition(1),oritispart+*ofanA/Dpair(2),orD/Apair(3):+*+*(1)mp->localisuninitialized;settop->two->sha1+*(2)mp->localisuninitialized;settop->two->sha1+*(3)mp->localisnull_sha1;settop->two->sha1+*/+assert(is_null_sha1(mp->local)||+!hashcmp(mp->local,uninitialized));+hashcpy(mp->local,p->two->sha1);+}else{/* modification */+/*+*Thisisatruemodification.p->one->sha1shall+*matchmp->base,andmp->localshallbeuninitialized.+*Setmp->localtop->two->sha1.+*/+assert(!hashcmp(p->one->sha1,mp->base));+assert(!hashcmp(mp->local,uninitialized));+hashcpy(mp->local,p->two->sha1);+}+trace_printf("\t\tStored local change for %s: %.7s -> %.7s\n",+sha1_to_hex(mp->obj),sha1_to_hex(mp->base),+sha1_to_hex(mp->local));+}+diff_flush(&opt);+diff_tree_release_paths(&opt);+}++staticintmerge_changes(structnotes_merge_options*o,+structnotes_merge_pair*changes,int*num_changes,+structnotes_tree*t)+{+inti,conflicts=0;++trace_printf("\tmerge_changes(num_changes = %i)\n",*num_changes);+for(i=0;i<*num_changes;i++){+structnotes_merge_pair*p=changes+i;+trace_printf("\t\t%.7s: %.7s -> %.7s/%.7s\n",+sha1_to_hex(p->obj),sha1_to_hex(p->base),+sha1_to_hex(p->local),sha1_to_hex(p->remote));++if(!hashcmp(p->base,p->remote)){+/* no remote change; nothing to do */+trace_printf("\t\t\tskipping (no remote change)\n");+}elseif(!hashcmp(p->local,p->remote)){+/* same change in local and remote; nothing to do */+trace_printf("\t\t\tskipping (local == remote)\n");+}elseif(!hashcmp(p->local,uninitialized)||+!hashcmp(p->local,p->base)){+/* no local change; adopt remote change */+trace_printf("\t\t\tno local change, adopted remote\n");+if(add_note(t,p->obj,p->remote,+combine_notes_overwrite))+die("confused: combine_notes_overwrite failed");+}else{+/* need file-level merge between local and remote */+trace_printf("\t\t\tneed content-level merge\n");+conflicts+=1;/* TODO */+}+}++returnconflicts;+}++staticintmerge_from_diffs(structnotes_merge_options*o,+constunsignedchar*base,+constunsignedchar*local,+constunsignedchar*remote,structnotes_tree*t)+{+structnotes_merge_pair*changes;+intnum_changes,conflicts;++trace_printf("\tmerge_from_diffs(base = %.7s, local = %.7s, "+"remote = %.7s)\n",sha1_to_hex(base),sha1_to_hex(local),+sha1_to_hex(remote));++changes=diff_tree_remote(o,base,remote,&num_changes);+diff_tree_local(o,changes,num_changes,base,local);++conflicts=merge_changes(o,changes,&num_changes,t);+free(changes);++OUTPUT(o,4,"Merge result: %i unmerged notes and a %s notes tree",+conflicts,t->dirty?"dirty":"clean");++returnconflicts?-1:1;+}+voidcreate_notes_commit(structnotes_tree*t,structcommit_list*parents,constchar*msg,unsignedchar*result_sha1){
@@ -107,14 +414,17 @@ int notes_merge(struct notes_merge_options *o,bases=get_merge_bases(local,remote,1);if(!bases){base_sha1=null_sha1;+base_tree_sha1=(unsignedchar*)EMPTY_TREE_SHA1_BIN;OUTPUT(o,4,"No merge base found; doing history-less merge");}elseif(!bases->next){base_sha1=bases->item->object.sha1;+base_tree_sha1=bases->item->tree->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;+base_tree_sha1=bases->item->tree->object.sha1;OUTPUT(o,3,"Multiple merge bases found. Using the first ""(%.7s)",sha1_to_hex(base_sha1));}
@@ -136,8 +446,17 @@ int notes_merge(struct notes_merge_options *o,gotofound_result;}-/* TODO: */-result=error("notes_merge() cannot yet handle real merges.");+result=merge_from_diffs(o,base_tree_sha1,local->tree->object.sha1,+remote->tree->object.sha1,local_tree);++if(result>0){/* successful non-trivial merge */+/* Commit result */+structcommit_list*parents=NULL;+commit_list_insert(remote,&parents);/* LIFO order */+commit_list_insert(local,&parents);+create_notes_commit(local_tree,parents,o->commit_msg,+result_sha1);+}found_result:free_commit_list(bases);
@@ -177,4 +177,192 @@ test_expect_success 'merge changed (y) into original (x) => Fast-forward' 'test"$(gitrev-parserefs/notes/x)"="$(gitrev-parserefs/notes/y)"'+test_expect_success'merge empty notes ref (z => y)''+# Prepare empty (but valid) notes ref (z)+gitconfigcore.notesRefrefs/notes/z&&+gitnotesadd-m"foo"&&+gitnotesremove&&+gitnotes>output_notes_z&&+test_cmp/dev/nulloutput_notes_z&&+# Do the merge (z => y)+gitconfigcore.notesRefrefs/notes/y&&+gitnotesmergez&&+verify_notesy&&+# y should no longer point to the same notes commit as x+test"$(gitrev-parserefs/notes/x)"!="$(gitrev-parserefs/notes/y)"+'++cat<<EOF|sort>expect_notes_y+0f2efbd00262f2fd41dfae33df8765618eeacd99$commit_sha5+dec2502dac3ea161543f71930044deff93fa945c$commit_sha4+4069cdb399fd45463ec6eef8e051a16a03592d91$commit_sha3+d000d30e6ddcfce3a8122c403226a2ce2fd04d9d$commit_sha2+43add6bd0c8c0bc871ac7991e0f5573cfba27804$commit_sha1+EOF++cat>expect_log_y<<EOF+$commit_sha55th+Noteson5thcommit++$commit_sha44th+Newnoteson4thcommit++$commit_sha33rd+Noteson3rdcommit++Morenoteson3rdcommit++$commit_sha22nd+Newnoteson2ndcommit++$commit_sha11st+Noteson1stcommit++Morenoteson1stcommit++EOF++test_expect_success'change notes on other notes ref (y)''+# Append to 1st commit notes+gitnotesappend-m"More notes on 1st commit"1st&&+# Add new notes to 2nd commit+gitnotesadd-m"New notes on 2nd commit"2nd&&+verify_notesy+'++cat<<EOF|sort>expect_notes_x+0f2efbd00262f2fd41dfae33df8765618eeacd99$commit_sha5+1f257a3a90328557c452f0817d6cc50c89d315d4$commit_sha4+daa55ffad6cb99bf64226532147ffcaf5ce8bdd1$commit_sha1+EOF++cat>expect_log_x<<EOF+$commit_sha55th+Noteson5thcommit++$commit_sha44th+Newnoteson4thcommit++Morenoteson4thcommit++$commit_sha33rd++$commit_sha22nd++$commit_sha11st+Noteson1stcommit++EOF++test_expect_success'change notes on notes ref (x)''+gitconfigcore.notesRefrefs/notes/x&&+gitnotesremove3rd&&+gitnotesappend-m"More notes on 4th commit"4th&&+verify_notesx+'++cat<<EOF|sort>expect_notes_x+0f2efbd00262f2fd41dfae33df8765618eeacd99$commit_sha5+1f257a3a90328557c452f0817d6cc50c89d315d4$commit_sha4+d000d30e6ddcfce3a8122c403226a2ce2fd04d9d$commit_sha2+43add6bd0c8c0bc871ac7991e0f5573cfba27804$commit_sha1+EOF++cat>expect_log_x<<EOF+$commit_sha55th+Noteson5thcommit++$commit_sha44th+Newnoteson4thcommit++Morenoteson4thcommit++$commit_sha33rd++$commit_sha22nd+Newnoteson2ndcommit++$commit_sha11st+Noteson1stcommit++Morenoteson1stcommit++EOF++test_expect_success'merge y into x => Non-conflicting 3-way merge''+gitnotesmergey&&+verify_notesx&&+verify_notesy+'++cat<<EOF|sort>expect_notes_w+05a4927951bcef347f51486575b878b2b60137f2$commit_sha3+d000d30e6ddcfce3a8122c403226a2ce2fd04d9d$commit_sha2+EOF++cat>expect_log_w<<EOF+$commit_sha55th++$commit_sha44th++$commit_sha33rd+Newnoteson3rdcommit++$commit_sha22nd+Newnoteson2ndcommit++$commit_sha11st++EOF++test_expect_success'create notes on new, separate notes ref (w)''+gitconfigcore.notesRefrefs/notes/w&&+# Add same note as refs/notes/y on 2nd commit+gitnotesadd-m"New notes on 2nd commit"2nd&&+# Add new note on 3rd commit (non-conflicting)+gitnotesadd-m"New notes on 3rd commit"3rd&&+# Verify state of notes on new, separate notes ref (w)+verify_notesw+'++cat<<EOF|sort>expect_notes_x+0f2efbd00262f2fd41dfae33df8765618eeacd99$commit_sha5+1f257a3a90328557c452f0817d6cc50c89d315d4$commit_sha4+05a4927951bcef347f51486575b878b2b60137f2$commit_sha3+d000d30e6ddcfce3a8122c403226a2ce2fd04d9d$commit_sha2+43add6bd0c8c0bc871ac7991e0f5573cfba27804$commit_sha1+EOF++cat>expect_log_x<<EOF+$commit_sha55th+Noteson5thcommit++$commit_sha44th+Newnoteson4thcommit++Morenoteson4thcommit++$commit_sha33rd+Newnoteson3rdcommit++$commit_sha22nd+Newnoteson2ndcommit++$commit_sha11st+Noteson1stcommit++Morenoteson1stcommit++EOF++test_expect_success'merge w into x => Non-conflicting history-less merge''+gitconfigcore.notesRefrefs/notes/x&&+gitnotesmergew&&+# Verify new state of notes on other notes ref (x)+verify_notesx&&+# Also verify that nothing changed on other notes refs (y and w)+verify_notesy&&+verify_notesw+'+ test_done
From: Johan Herland <hidden> Date: 2016-06-15 22:49:49
The new -s/--strategy command-line option to 'git notes merge' allow the user
to choose how notes merge conflicts should be resolved. There are four valid
strategies to choose from:
1. "manual" (the default): This will let the user manually resolve conflicts.
This option currently fails with an error message. It will be implemented
properly in future patches.
2. "ours": This automatically chooses the local version of a conflict, and
discards the remote version.
3. "theirs": This automatically chooses the remote version of a conflict, and
discards the local version.
4. "union": This automatically resolves the conflict by appending the remote
version to the local version.
The strategies are implemented using the combine_notes_* functions from the
notes.h API.
The patch also includes testcases verifying the correct implementation of
these strategies.
This patch has been improved by the following contributions:
- Jonathan Nieder: Future-proof by always checking add_note() return value
- Stephen Boyd: Use test_commit
- Stephen Boyd: Use correct option name
Thanks-to: Jonathan Nieder [off-list ref]
Thanks-to: Stephen Boyd [off-list ref]
Signed-off-by: Johan Herland <redacted>
---
builtin/notes.c | 21 ++-
notes-merge.c | 31 ++-
notes-merge.h | 6 +
t/t3309-notes-merge-auto-resolve.sh | 502 +++++++++++++++++++++++++++++++++++
4 files changed, 558 insertions(+), 2 deletions(-)
create mode 100755 t/t3309-notes-merge-auto-resolve.sh
@@ -263,6 +263,35 @@ static void diff_tree_local(struct notes_merge_options *o,diff_tree_release_paths(&opt);}+staticintmerge_one_change(structnotes_merge_options*o,+structnotes_merge_pair*p,structnotes_tree*t)+{+/*+*Return0ifchangewasresolved(andaddedtonotes_tree),+*1ifconflict+*/+switch(o->strategy){+caseNOTES_MERGE_RESOLVE_MANUAL:+return1;+caseNOTES_MERGE_RESOLVE_OURS:+OUTPUT(o,2,"Using local notes for %s",sha1_to_hex(p->obj));+/* nothing to do */+return0;+caseNOTES_MERGE_RESOLVE_THEIRS:+OUTPUT(o,2,"Using remote notes for %s",sha1_to_hex(p->obj));+if(add_note(t,p->obj,p->remote,combine_notes_overwrite))+die("confused: combine_notes_overwrite failed");+return0;+caseNOTES_MERGE_RESOLVE_UNION:+OUTPUT(o,2,"Concatenating local and remote notes for %s",+sha1_to_hex(p->obj));+if(add_note(t,p->obj,p->remote,combine_notes_concatenate))+die("confused: combine_notes_concatenate failed");+return0;+}+die("Unknown strategy (%i).",o->strategy);+}+staticintmerge_changes(structnotes_merge_options*o,structnotes_merge_pair*changes,int*num_changes,structnotes_tree*t)
@@ -292,7 +321,7 @@ static int merge_changes(struct notes_merge_options *o,}else{/* need file-level merge between local and remote */trace_printf("\t\t\tneed content-level merge\n");-conflicts+=1;/* TODO */+conflicts+=merge_one_change(o,p,t);}}
From: Jonathan Nieder <hidden> Date: 2016-06-15 22:49:50
Johan Herland wrote:
Clearly specify how combine_notes functions are expected to handle null_sha1
in input. Also specify (and implement) that returning null_sha1 from a
combine_notes function will cause the note in question to be removed.
Ack again on patches 1-4. As for this one, I still think the log message
does not make the goal obvious.
1. Clearly specify how combine_notes functions are expected to
handle null_sha1 in input.
Wasn't it already clear? I guess you mean that the documentation was
updated. But surely that is less important than:
2. Also specify (and implement) that returning null_sha1 from a
combine_notes function will ...
A person reading this for the first time could be forgiven for thinking
this is like (1), i.e., documenting an edge case. But actually it's
the main point, and the part I omitted with "..." is the important
part!
Why not say something like:
Allow combine_notes functions to request that a note be
removed, by returning the object id of the empty blob.
For consistency, also teach note_tree_insert() to skip
insertion of an empty note when there is no note to
combine it with.
In general, an empty note is treated identically to no
note at all: for example, when merging two notes trees,
one of which does not have a certain note, combine_notes()
will be called as though that tree had an empty note
instead. Document this.
The above includes guesses, so please do not use it verbatim
unless it's true. :)
Of course these are minor nitpicks as compared to the content of
the patch itself. The patch still looks good.
Micronit: it would probably be clearer to write
if (ret)
return ret;
*p = SET_PTR_TYPE(...
return note_tree_insert(...
to avoid a little nesting.
The patch still looks good to me.
From: Johan Herland <hidden> Date: 2016-06-15 22:49:50
On Thursday 21 October 2010, Jonathan Nieder wrote:
Johan Herland wrote:
quoted
Clearly specify how combine_notes functions are expected to handle
null_sha1 in input. Also specify (and implement) that returning
null_sha1 from a combine_notes function will cause the note in
question to be removed.
Ack again on patches 1-4. As for this one, I still think the log
message does not make the goal obvious.
[...]
Ok, I liked your alternative message much better. The next iteration
will have this commit message:
notes.h/c: Allow combine_notes functions to remove notes
Allow combine_notes functions to request that a note be removed,
by setting the resulting note SHA1 to null_sha1 (0000000...).
For consistency, also teach note_tree_insert() to skip insertion
of an empty note when there is no note to combine it with.
In general, an empty note is treated identically to no note at
all, but when adding an empty note where there already exists a
non-empty note, we allow the combine_notes function to potentially
record a new/changed note. Document this behaviour, and clearly
specify how combine_notes functions are expected to handle null_sha1
in input.
Thanks again, for your superhuman patience in reviewing.
...Johan
--
Johan Herland, [off-list ref]
www.herland.net
From: Jonathan Nieder <hidden> Date: 2016-06-15 22:49:50
Johan Herland wrote:
Allow combine_notes functions to request that a note be removed,
by setting the resulting note SHA1 to null_sha1 (0000000...).
For consistency, also teach note_tree_insert() to skip insertion
of an empty note when there is no note to combine it with.
I guess it's probably also worth clarifying that the term "empty note"
means null_sha1 rather than empty_blob_sha1.
i.e. something like:
For consistency, also teach note_tree_insert() to skip insertion
of an empty note (a note with val_sha1 equal to null_sha1)
when there is no note to combine it with.
The previous behavior iiuc would have been for write_notes_tree() to
write invalid trees after such an operation.
Heya,
On Wed, Oct 20, 2010 at 21:08, Johan Herland [off-list ref] wrote:
- Sverre Rabbelier suggests renaming 'git notes merge --reset' to
'git notes merge --abort'. I sort of agree, but would want some
consistency with 'git merge', e.g. by providing 'git merge --abort'
as an alias to 'git reset --merge'.
Can someone comment on whether "git reset --merge" is a good
implementation of "git merge --abort"? If we can get an Ack from
someone I'd love to see this implemented.
- When resolving notes merge conflicts, you can add/remove files/notes
in .git/NOTES_MERGE_WORKTREE; 'git notes merge --commit' does not
check that the notes have any relationship to the notes originally
put there by 'git notes merge'. Should we warn about removed and
added notes in .git/NOTES_MERGE_WORKTREE? Currently we don't, and
I'm not sure it's worth it. Users can always review the merge commit
afterwards.
If it's easy to do, it would be useful.
- Fetching and pushing note refs:
- Add refs/notes/* to default fetch refspec?
- A way to specify (at clone time) which refspec(s) to set up?
- A way for the remote repo to hint at which refspecs you might want
to set up (by default?)
Didn't we already discuss this earlier? Can you summarize (or at least
link to) that discussion?
--
Cheers,
Sverre Rabbelier
From: Johan Herland <hidden> Date: 2016-06-15 22:49:51
On Thursday 21 October 2010, Jonathan Nieder wrote:
Johan Herland wrote:
quoted
Allow combine_notes functions to request that a note be removed,
by setting the resulting note SHA1 to null_sha1 (0000000...).
For consistency, also teach note_tree_insert() to skip insertion
of an empty note when there is no note to combine it with.
I guess it's probably also worth clarifying that the term "empty
note" means null_sha1 rather than empty_blob_sha1.
i.e. something like:
For consistency, also teach note_tree_insert() to skip insertion
of an empty note (a note with val_sha1 equal to null_sha1)
when there is no note to combine it with.
Agreed. Incorporated.
The previous behavior iiuc would have been for write_notes_tree() to
write invalid trees after such an operation.
Indeed, you are correct. I've added a note about this to the commit
message.
...Johan
--
Johan Herland, [off-list ref]
www.herland.net
From: Johan Herland <hidden> Date: 2016-06-15 22:49:51
On Thursday 21 October 2010, Sverre Rabbelier wrote:
On Wed, Oct 20, 2010 at 21:08, Johan Herland wrote:
quoted
- Fetching and pushing note refs:
- Add refs/notes/* to default fetch refspec?
- A way to specify (at clone time) which refspec(s) to set up?
- A way for the remote repo to hint at which refspecs you might
want to set up (by default?)
Didn't we already discuss this earlier? Can you summarize (or at
least link to) that discussion?
Yes, sorry for not answering you earlier. Here's what you wrote in the
previous thread:
On Saturday 09 October 2010, Sverre Rabbelier wrote:
Heya,
On Sat, Oct 9, 2010 at 03:08, Johan Herland [off-list ref] wrote:
quoted
- Fetching and pushing note refs:
- Add refs/notes/* to default fetch refspec?
Or at least add a '--notes[=<notes namespace>]' to fetch, pull, and
push.
Agreed, at least that.
In order to promote sharing of notes, though, I'd like for it to be
possible to configure the repo so that a vanilla 'git fetch' also
updates your notes. In fact, I wonder if this should even be made the
default.
quoted
- A way to specify (at clone time) which refspec(s) to set up?
How would that look like?
Maybe add an option to 'git clone' (and 'git remote add') that specifies
the refspec you want to use in your config for that remote. Something
like:
git clone --fetch="+refs/heads/*:refs/remotes/origin/*" \
--fetch="+refs/notes/*:refs/remotes/origin/notes/*" \
<source_url> ...
...will set up the following config:
[remote "origin"]
url = <source_url>
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = +refs/notes/*:refs/remotes/origin/notes/*
Obviously, we would probably want to provide shorthands for the most
common refspecs, like:
git clone --fetch=default,notes <source_url> ...
or
git clone --fetch-heads --fetch-notes <source_url> ...
quoted
- A way for the remote repo to hint at which refspecs you might
want to set up (by default?)
I assume this would be a generic mechanism of sorts? Are there any
other use cases for this other than notes?
Yes, I believe so (although I haven't thought much about this, yet).
There's been earlier discussions on hiding certain branches from view.
This could maybe be solved by the server suggesting a refspec that
excludes the stuff you don't want to share (by default). Similary, the
refspec could _include_ notes namespaces that you do want to share.
Of course (as today) the client should be free to demand a different
refspec, e.g. if it wants access to everything, or if it's only
interested in a subset of the "default" refs.
...Johan
--
Johan Herland, [off-list ref]
www.herland.net
Heya,
On Fri, Oct 22, 2010 at 10:41, Johan Herland [off-list ref] wrote:
Yes, sorry for not answering you earlier. Here's what you wrote in the
previous thread:
No problem :).
On Saturday 09 October 2010, Sverre Rabbelier wrote:
quoted
On Sat, Oct 9, 2010 at 03:08, Johan Herland [off-list ref] wrote:
quoted
- Fetching and pushing note refs:
- Add refs/notes/* to default fetch refspec?
Or at least add a '--notes[=<notes namespace>]' to fetch, pull, and
push.
Agreed, at least that.
In order to promote sharing of notes, though, I'd like for it to be
possible to configure the repo so that a vanilla 'git fetch' also
updates your notes. In fact, I wonder if this should even be made the
default.
I think notes directly under /refs/notes/ should be shared by default,
but those in sub-refs (such as the /refs/notes/am/ that's been
mentioned before) should not.
quoted
quoted
- A way to specify (at clone time) which refspec(s) to set up?
How would that look like?
Maybe add an option to 'git clone' (and 'git remote add') that specifies
the refspec you want to use in your config for that remote. Something
like:
git clone --fetch="+refs/heads/*:refs/remotes/origin/*" \
--fetch="+refs/notes/*:refs/remotes/origin/notes/*" \
<source_url> ...
Who's going to type that out though? The only use case I can think of
is if you want to be able to give someone a line they can paste and be
set right away, but I don't see why in that case pasting multiple
commands (i.e., calling 'git config' a few times) wouldn't suffice.
Obviously, we would probably want to provide shorthands for the most
common refspecs, like:
git clone --fetch=default,notes <source_url> ...
git clone --fetch-heads --fetch-notes <source_url> ...
Adding refspec shorthands _does_ make sense. However, it might make
more sense to put those under 'git remote' instead?
quoted
quoted
- A way for the remote repo to hint at which refspecs you might
want to set up (by default?)
I assume this would be a generic mechanism of sorts? Are there any
other use cases for this other than notes?
Yes, I believe so (although I haven't thought much about this, yet).
There's been earlier discussions on hiding certain branches from view.
This could maybe be solved by the server suggesting a refspec that
excludes the stuff you don't want to share (by default). Similary, the
refspec could _include_ notes namespaces that you do want to share.
That sounds like a good use case.
Of course (as today) the client should be free to demand a different
refspec, e.g. if it wants access to everything, or if it's only
interested in a subset of the "default" refs.
Of course, I reckon it would just set up their refspecs, and the user
would be free to change it. Especially if we inform the user that the
refspec was set to something other than the default refspec.
--
Cheers,
Sverre Rabbelier
From: Johan Herland <hidden> Date: 2016-06-15 22:49:51
(fragmenting this thread for the last time, I promise...)
On Thursday 21 October 2010, Sverre Rabbelier wrote:
On Wed, Oct 20, 2010 at 21:08, Johan Herland wrote:
quoted
- When resolving notes merge conflicts, you can add/remove files/notes
in .git/NOTES_MERGE_WORKTREE; 'git notes merge --commit' does not
check that the notes have any relationship to the notes originally
put there by 'git notes merge'. Should we warn about removed and
added notes in .git/NOTES_MERGE_WORKTREE? Currently we don't, and
I'm not sure it's worth it. Users can always review the merge commit
afterwards.
If it's easy to do, it would be useful.
Well, currently I don't store a separate list of conflicts anywhere, so I
would need to add that in order to have 'git notes merge --commit' warn
about deletions/additions in .git/NOTES_MERGE_WORKTREE.
Also, for conflicts where a note is modified in one branch and deleted in
the other, I don't think it makes sense to warn about their deletion (since
that in many cases is a perfectly valid resolution of those conflicts).
Which leaves warning about "other" additions/deletions. We could certainly
do this, but I'd frankly rather get the current patch series through without
broadening its scope. I've been sitting on this far too long as it is. I'll
keep it on my TODO list for a future series (if nobody else beats me to it).
...Johan
--
Johan Herland, [off-list ref]
www.herland.net
From: Johan Herland <hidden> Date: 2016-06-15 22:49:51
On Friday 22 October 2010, Sverre Rabbelier wrote:
On Fri, Oct 22, 2010 at 10:41, Johan Herland wrote:
quoted
On Saturday 09 October 2010, Sverre Rabbelier wrote:
quoted
On Sat, Oct 9, 2010 at 03:08, Johan Herland wrote:
quoted
- Fetching and pushing note refs:
- Add refs/notes/* to default fetch refspec?
Or at least add a '--notes[=<notes namespace>]' to fetch, pull, and
push.
Agreed, at least that.
In order to promote sharing of notes, though, I'd like for it to be
possible to configure the repo so that a vanilla 'git fetch' also
updates your notes. In fact, I wonder if this should even be made the
default.
I think notes directly under /refs/notes/ should be shared by default,
but those in sub-refs (such as the /refs/notes/am/ that's been
mentioned before) should not.
In theory that would make sense, but how you write that as a refspec? AFAIK,
refspecs don't (yet) support a syntax for (a) _excluding_ something from a
glob, and certainly not (b) excluding all "sub-refs" from a glob.
Alternatively, we could share everything under refs/notes/, except
/refs/notes/local/ (or refs/notes/private/ or whatever). That would just
require support for (a)...
quoted
quoted
quoted
- A way to specify (at clone time) which refspec(s) to set up?
How would that look like?
Maybe add an option to 'git clone' (and 'git remote add') that
specifies the refspec you want to use in your config for that remote.
Something like:
git clone --fetch="+refs/heads/*:refs/remotes/origin/*" \
--fetch="+refs/notes/*:refs/remotes/origin/notes/*" \
<source_url> ...
Who's going to type that out though? The only use case I can think of
is if you want to be able to give someone a line they can paste and be
set right away, but I don't see why in that case pasting multiple
commands (i.e., calling 'git config' a few times) wouldn't suffice.
You might be right. I was thinking that before we provide shorthands, we
should provide an option that let you specify _any_ refspec. Sort of as a
fallback if somebody's scenario doesn't fit into any of the shorthands.
But as you say, this can also be done with a few calls to git config.
quoted
Obviously, we would probably want to provide shorthands for the most
common refspecs, like:
git clone --fetch=default,notes <source_url> ...
git clone --fetch-heads --fetch-notes <source_url> ...
Adding refspec shorthands _does_ make sense. However, it might make
more sense to put those under 'git remote' instead?
I'd say _both_ 'git remote' and 'git clone'.
quoted
quoted
quoted
- A way for the remote repo to hint at which refspecs you might
want to set up (by default?)
I assume this would be a generic mechanism of sorts? Are there any
other use cases for this other than notes?
Yes, I believe so (although I haven't thought much about this, yet).
There's been earlier discussions on hiding certain branches from view.
This could maybe be solved by the server suggesting a refspec that
excludes the stuff you don't want to share (by default). Similary, the
refspec could _include_ notes namespaces that you do want to share.
That sounds like a good use case.
But I believe we need to extend the refspec syntax to support at least (a)
from above (excluding named entries from a refspec glob) in order to support
hiding of certain refs. Still, I think it would be a useful addition.
quoted
Of course (as today) the client should be free to demand a different
refspec, e.g. if it wants access to everything, or if it's only
interested in a subset of the "default" refs.
Of course, I reckon it would just set up their refspecs, and the user
would be free to change it. Especially if we inform the user that the
refspec was set to something other than the default refspec.
Agreed.
...Johan
--
Johan Herland, [off-list ref]
www.herland.net
Heya,
On Fri, Oct 22, 2010 at 15:28, Johan Herland [off-list ref] wrote:
Which leaves warning about "other" additions/deletions. We could certainly
do this, but I'd frankly rather get the current patch series through without
broadening its scope. I've been sitting on this far too long as it is. I'll
keep it on my TODO list for a future series (if nobody else beats me to it).
Fair enough, I don't think anyone will be seriously hurt by not having
this (especially since git notes is still more or less a 'beta
feature'), so I'm in favor of getting this merged now, and polishing
it more later if we decide it needs more polishing.
--
Cheers,
Sverre Rabbelier
Heya,
On Fri, Oct 22, 2010 at 17:47, Johan Herland [off-list ref] wrote:
On Friday 22 October 2010, Sverre Rabbelier wrote:
quoted
I think notes directly under /refs/notes/ should be shared by default,
but those in sub-refs (such as the /refs/notes/am/ that's been
mentioned before) should not.
In theory that would make sense, but how you write that as a refspec? AFAIK,
refspecs don't (yet) support a syntax for (a) _excluding_ something from a
glob, and certainly not (b) excluding all "sub-refs" from a glob.
Indeed, which I think is a shame. For that matter, I'd love to be able
to have negative globs for branches in general (e.g., 'git log
--branches :-heads/hidden/**).
Alternatively, we could share everything under refs/notes/, except
/refs/notes/local/ (or refs/notes/private/ or whatever). That would just
require support for (a)...
That goes against how notes are used though. The refs/notes/* space is
the one that is shown by default, not refs/notes/local/*.
quoted
Who's going to type that out though? The only use case I can think of
is if you want to be able to give someone a line they can paste and be
set right away, but I don't see why in that case pasting multiple
commands (i.e., calling 'git config' a few times) wouldn't suffice.
You might be right. I was thinking that before we provide shorthands, we
should provide an option that let you specify _any_ refspec. Sort of as a
fallback if somebody's scenario doesn't fit into any of the shorthands.
But as you say, this can also be done with a few calls to git config.
[...]
quoted
Adding refspec shorthands _does_ make sense. However, it might make
more sense to put those under 'git remote' instead?
I'd say _both_ 'git remote' and 'git clone'.
I think getting those shorthands in git config/git remote is step one,
if we do want that. Then, if desired, we can add that to git clone.
quoted
That sounds like a good use case.
But I believe we need to extend the refspec syntax to support at least (a)
from above (excluding named entries from a refspec glob) in order to support
hiding of certain refs. Still, I think it would be a useful addition.