Merge branch 'jh/notes-merge'
authorJunio C Hamano <gitster@pobox.com>
Wed, 8 Dec 2010 19:24:12 +0000 (11:24 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 8 Dec 2010 19:24:12 +0000 (11:24 -0800)
* jh/notes-merge: (23 commits)
Provide 'git merge --abort' as a synonym to 'git reset --merge'
cmd_merge(): Parse options before checking MERGE_HEAD
Provide 'git notes get-ref' to easily retrieve current notes ref
git notes merge: Add testcases for merging notes trees at different fanouts
git notes merge: Add another auto-resolving strategy: "cat_sort_uniq"
git notes merge: --commit should fail if underlying notes ref has moved
git notes merge: List conflicting notes in notes merge commit message
git notes merge: Manual conflict resolution, part 2/2
git notes merge: Manual conflict resolution, part 1/2
Documentation: Preliminary docs on 'git notes merge'
git notes merge: Add automatic conflict resolvers (ours, theirs, union)
git notes merge: Handle real, non-conflicting notes merges
builtin/notes.c: Refactor creation of notes commits.
git notes merge: Initial implementation handling trivial merges only
builtin/notes.c: Split notes ref DWIMmery into a separate function
notes.c: Use two newlines (instead of one) when concatenating notes
(trivial) t3303: Indent with tabs instead of spaces for consistency
notes.h/c: Propagate combine_notes_fn return value to add_note() and beyond
notes.h/c: Allow combine_notes functions to remove notes
notes.c: Reorder functions in preparation for next commit
...

Conflicts:
builtin.h

1  2 
Documentation/git-merge.txt
Makefile
builtin.h
builtin/merge.c
builtin/notes.c
notes.c
notes.h
t/t3301-notes.sh
t/t3404-rebase-interactive.sh
index d43416d299a7c028a88e8ddce89536c2c64e65c9,47b59a551d60a5445a65bdcc2450d1f3cde04134..c1efaaa5c52cd93e90a1a7427125e470f771146a
@@@ -13,6 -13,7 +13,7 @@@ SYNOPSI
        [-s <strategy>] [-X <strategy-option>]
        [--[no-]rerere-autoupdate] [-m <msg>] <commit>...
  'git merge' <msg> HEAD <commit>...
+ 'git merge' --abort
  
  DESCRIPTION
  -----------
@@@ -47,6 -48,14 +48,14 @@@ The second syntax (<msg> `HEAD` <commit
  historical reasons.  Do not use it from the command line or in
  new scripts.  It is the same as `git merge -m <msg> <commit>...`.
  
+ The third syntax ("`git merge --abort`") can only be run after the
+ merge has resulted in conflicts. 'git merge --abort' will abort the
+ merge process and try to reconstruct the pre-merge state. However,
+ if there were uncommitted changes when the merge started (and
+ especially if those changes were further modified after the merge
+ was started), 'git merge --abort' will in some cases be unable to
+ reconstruct the original (pre-merge) changes. Therefore:
  *Warning*: Running 'git merge' with uncommitted changes is
  discouraged: while possible, it leaves you in a state that is hard to
  back out of in the case of a conflict.
@@@ -59,19 -68,31 +68,31 @@@ include::merge-options.txt[
  -m <msg>::
        Set the commit message to be used for the merge commit (in
        case one is created).
 -
 -      If `--log` is specified, a shortlog of the commits being merged
 -      will be appended to the specified message.
 -
 -      The 'git fmt-merge-msg' command can be
 -      used to give a good default for automated 'git merge'
 -      invocations.
 ++
 +If `--log` is specified, a shortlog of the commits being merged
 +will be appended to the specified message.
 ++
 +The 'git fmt-merge-msg' command can be
 +used to give a good default for automated 'git merge'
 +invocations.
  
  --rerere-autoupdate::
  --no-rerere-autoupdate::
        Allow the rerere mechanism to update the index with the
        result of auto-conflict resolution if possible.
  
+ --abort::
+       Abort the current conflict resolution process, and
+       try to reconstruct the pre-merge state.
+ +
+ If there were uncommitted worktree changes present when the merge
+ started, 'git merge --abort' will in some cases be unable to
+ reconstruct these changes. It is therefore recommended to always
+ commit or stash your changes before running 'git merge'.
+ +
+ 'git merge --abort' is equivalent to 'git reset --merge' when
+ `MERGE_HEAD` is present.
  <commit>...::
        Commits, usually other branch heads, to merge into our branch.
        You need at least one <commit>.  Specifying more than one
@@@ -142,7 -163,7 +163,7 @@@ happens
     i.e. matching `HEAD`.
  
  If you tried a merge which resulted in complex conflicts and
- want to start over, you can recover with `git reset --merge`.
+ want to start over, you can recover with `git merge --abort`.
  
  HOW CONFLICTS ARE PRESENTED
  ---------------------------
@@@ -213,8 -234,8 +234,8 @@@ After seeing a conflict, you can do tw
  
   * Decide not to merge.  The only clean-ups you need are to reset
     the index file to the `HEAD` commit to reverse 2. and to clean
-    up working tree changes made by 2. and 3.; `git-reset --hard` can
-    be used for this.
+    up working tree changes made by 2. and 3.; `git merge --abort`
+    can be used for this.
  
   * Resolve the conflicts.  Git will mark the conflicts in
     the working tree.  Edit the files into shape and
diff --combined Makefile
index 7eb948d7c8a591139f8f3bb050fb051e53fefa5a,14c0ff166fe8c94bdc238b9b3c7b77c61a04923d..91567c651ef902b100976e0f1912e4d878cac7aa
+++ b/Makefile
@@@ -68,13 -68,6 +68,13 @@@ all:
  #
  # Define NO_MKSTEMPS if you don't have mkstemps in the C library.
  #
 +# Define NO_STRTOK_R if you don't have strtok_r in the C library.
 +#
 +# Define NO_FNMATCH if you don't have fnmatch in the C library.
 +#
 +# Define NO_FNMATCH_CASEFOLD if your fnmatch function doesn't have the
 +# FNM_CASEFOLD GNU extension.
 +#
  # Define NO_LIBGEN_H if you don't have libgen.h.
  #
  # Define NEEDS_LIBGEN if your libgen needs -lgen when linking
@@@ -275,7 -268,6 +275,7 @@@ STRIP ?= stri
  #   infodir
  #   htmldir
  #   ETC_GITCONFIG (but not sysconfdir)
 +#   ETC_GITATTRIBUTES
  # can be specified as a relative path some/where/else;
  # this is interpreted as relative to $(prefix) and "git" at
  # runtime figures out where they are based on the path to the executable.
@@@ -294,11 -286,9 +294,11 @@@ htmldir = share/doc/git-do
  ifeq ($(prefix),/usr)
  sysconfdir = /etc
  ETC_GITCONFIG = $(sysconfdir)/gitconfig
 +ETC_GITATTRIBUTES = $(sysconfdir)/gitattributes
  else
  sysconfdir = $(prefix)/etc
  ETC_GITCONFIG = etc/gitconfig
 +ETC_GITATTRIBUTES = etc/gitattributes
  endif
  lib = lib
  # DESTDIR=
@@@ -318,7 -308,6 +318,7 @@@ TCL_PATH = tcls
  TCLTK_PATH = wish
  PTHREAD_LIBS = -lpthread
  PTHREAD_CFLAGS =
 +GCOV = gcov
  
  export TCL_PATH TCLTK_PATH
  
@@@ -398,15 -387,12 +398,15 @@@ SCRIPTS = $(patsubst %.sh,%,$(SCRIPT_SH
          $(patsubst %.py,%,$(SCRIPT_PYTHON)) \
          git-instaweb
  
 +ETAGS_TARGET = TAGS
 +
  # Empty...
  EXTRA_PROGRAMS =
  
  # ... and all the rest that could be moved out of bindir to gitexecdir
  PROGRAMS += $(EXTRA_PROGRAMS)
  
 +PROGRAM_OBJS += daemon.o
  PROGRAM_OBJS += fast-import.o
  PROGRAM_OBJS += imap-send.o
  PROGRAM_OBJS += shell.o
@@@ -422,17 -408,12 +422,17 @@@ TEST_PROGRAMS_NEED_X += test-dat
  TEST_PROGRAMS_NEED_X += test-delta
  TEST_PROGRAMS_NEED_X += test-dump-cache-tree
  TEST_PROGRAMS_NEED_X += test-genrandom
 +TEST_PROGRAMS_NEED_X += test-line-buffer
  TEST_PROGRAMS_NEED_X += test-match-trees
 +TEST_PROGRAMS_NEED_X += test-obj-pool
  TEST_PROGRAMS_NEED_X += test-parse-options
  TEST_PROGRAMS_NEED_X += test-path-utils
  TEST_PROGRAMS_NEED_X += test-run-command
  TEST_PROGRAMS_NEED_X += test-sha1
  TEST_PROGRAMS_NEED_X += test-sigchain
 +TEST_PROGRAMS_NEED_X += test-string-pool
 +TEST_PROGRAMS_NEED_X += test-svn-fe
 +TEST_PROGRAMS_NEED_X += test-treap
  TEST_PROGRAMS_NEED_X += test-index-version
  
  TEST_PROGRAMS = $(patsubst %,%$X,$(TEST_PROGRAMS_NEED_X))
@@@ -487,7 -468,6 +487,7 @@@ export PYTHON_PAT
  
  LIB_FILE=libgit.a
  XDIFF_LIB=xdiff/lib.a
 +VCSSVN_LIB=vcs-svn/lib.a
  
  LIB_H += advice.h
  LIB_H += archive.h
@@@ -502,8 -482,6 +502,8 @@@ LIB_H += compat/bswap.
  LIB_H += compat/cygwin.h
  LIB_H += compat/mingw.h
  LIB_H += compat/win32/pthread.h
 +LIB_H += compat/win32/syslog.h
 +LIB_H += compat/win32/sys/poll.h
  LIB_H += csum-file.h
  LIB_H += decorate.h
  LIB_H += delta.h
@@@ -525,6 -503,7 +525,7 @@@ LIB_H += mailmap.
  LIB_H += merge-recursive.h
  LIB_H += notes.h
  LIB_H += notes-cache.h
+ LIB_H += notes-merge.h
  LIB_H += object.h
  LIB_H += pack.h
  LIB_H += pack-refs.h
@@@ -615,6 -594,7 +616,7 @@@ LIB_OBJS += merge-recursive.
  LIB_OBJS += name-hash.o
  LIB_OBJS += notes.o
  LIB_OBJS += notes-cache.o
+ LIB_OBJS += notes-merge.o
  LIB_OBJS += object.o
  LIB_OBJS += pack-check.o
  LIB_OBJS += pack-refs.o
@@@ -670,7 -650,6 +672,7 @@@ LIB_OBJS += write_or_die.
  LIB_OBJS += ws.o
  LIB_OBJS += wt-status.o
  LIB_OBJS += xdiff-interface.o
 +LIB_OBJS += zlib.o
  
  BUILTIN_OBJS += builtin/add.o
  BUILTIN_OBJS += builtin/annotate.o
@@@ -855,7 -834,6 +857,7 @@@ ifeq ($(uname_S),SunOS
        NO_MKDTEMP = YesPlease
        NO_MKSTEMPS = YesPlease
        NO_REGEX = YesPlease
 +      NO_FNMATCH_CASEFOLD = YesPlease
        ifeq ($(uname_R),5.6)
                SOCKLEN_T = int
                NO_HSTRERROR = YesPlease
@@@ -997,7 -975,6 +999,7 @@@ ifeq ($(uname_S),IRIX
        # NO_MMAP.  If you suspect that your compiler is not affected by this
        # issue, comment out the NO_MMAP statement.
        NO_MMAP = YesPlease
 +      NO_REGEX = YesPlease
        SNPRINTF_RETURNS_BOGUS = YesPlease
        SHELL_PATH = /usr/gnu/bin/bash
        NEEDS_LIBGEN = YesPlease
@@@ -1016,7 -993,6 +1018,7 @@@ ifeq ($(uname_S),IRIX64
        # NO_MMAP.  If you suspect that your compiler is not affected by this
        # issue, comment out the NO_MMAP statement.
        NO_MMAP = YesPlease
 +      NO_REGEX = YesPlease
        SNPRINTF_RETURNS_BOGUS = YesPlease
        SHELL_PATH=/usr/gnu/bin/bash
        NEEDS_LIBGEN = YesPlease
@@@ -1061,8 -1037,6 +1063,8 @@@ ifeq ($(uname_S),Windows
        NO_UNSETENV = YesPlease
        NO_STRCASESTR = YesPlease
        NO_STRLCPY = YesPlease
 +      NO_STRTOK_R = YesPlease
 +      NO_FNMATCH = YesPlease
        NO_MEMMEM = YesPlease
        # NEEDS_LIBICONV = YesPlease
        NO_ICONV = YesPlease
        NO_SVN_TESTS = YesPlease
        NO_PERL_MAKEMAKER = YesPlease
        RUNTIME_PREFIX = YesPlease
 -      NO_POSIX_ONLY_PROGRAMS = YesPlease
        NO_ST_BLOCKS_IN_STRUCT_STAT = YesPlease
        NO_NSEC = YesPlease
        USE_WIN32_MMAP = YesPlease
        NO_CURL = YesPlease
        NO_PYTHON = YesPlease
        BLK_SHA1 = YesPlease
 +      NO_POSIX_GOODIES = UnfortunatelyYes
        NATIVE_CRLF = YesPlease
  
        CC = compat/vcbuild/scripts/clink.pl
        AR = compat/vcbuild/scripts/lib.pl
        CFLAGS =
        BASIC_CFLAGS = -nologo -I. -I../zlib -Icompat/vcbuild -Icompat/vcbuild/include -DWIN32 -D_CONSOLE -DHAVE_STRING_H -D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE
 -      COMPAT_OBJS = compat/msvc.o compat/fnmatch/fnmatch.o compat/winansi.o compat/win32/pthread.o
 -      COMPAT_CFLAGS = -D__USE_MINGW_ACCESS -DNOGDI -DHAVE_STRING_H -DHAVE_ALLOCA_H -Icompat -Icompat/fnmatch -Icompat/regex -Icompat/fnmatch -Icompat/win32 -DSTRIP_EXTENSION=\".exe\"
 +      COMPAT_OBJS = compat/msvc.o compat/winansi.o compat/win32/pthread.o compat/win32/syslog.o compat/win32/sys/poll.o
 +      COMPAT_CFLAGS = -D__USE_MINGW_ACCESS -DNOGDI -DHAVE_STRING_H -DHAVE_ALLOCA_H -Icompat -Icompat/regex -Icompat/win32 -DSTRIP_EXTENSION=\".exe\"
        BASIC_LDFLAGS = -IGNORE:4217 -IGNORE:4049 -NOLOGO -SUBSYSTEM:CONSOLE -NODEFAULTLIB:MSVCRT.lib
        EXTLIBS = advapi32.lib shell32.lib wininet.lib ws2_32.lib
        PTHREAD_LIBS =
  endif
        X = .exe
  endif
 +ifeq ($(uname_S),Interix)
 +      NO_SYS_POLL_H = YesPlease
 +      NO_INTTYPES_H = YesPlease
 +      NO_INITGROUPS = YesPlease
 +      NO_IPV6 = YesPlease
 +      NO_MEMMEM = YesPlease
 +      NO_MKDTEMP = YesPlease
 +      NO_STRTOUMAX = YesPlease
 +      NO_NSEC = YesPlease
 +      NO_MKSTEMPS = YesPlease
 +      ifeq ($(uname_R),3.5)
 +              NO_INET_NTOP = YesPlease
 +              NO_INET_PTON = YesPlease
 +      endif
 +      ifeq ($(uname_R),5.2)
 +              NO_INET_NTOP = YesPlease
 +              NO_INET_PTON = YesPlease
 +      endif
 +endif
  ifneq (,$(findstring MINGW,$(uname_S)))
        pathsep = ;
        NO_PREAD = YesPlease
        NO_UNSETENV = YesPlease
        NO_STRCASESTR = YesPlease
        NO_STRLCPY = YesPlease
 +      NO_STRTOK_R = YesPlease
 +      NO_FNMATCH = YesPlease
        NO_MEMMEM = YesPlease
        NEEDS_LIBICONV = YesPlease
        OLD_ICONV = YesPlease
        NO_SVN_TESTS = YesPlease
        NO_PERL_MAKEMAKER = YesPlease
        RUNTIME_PREFIX = YesPlease
 -      NO_POSIX_ONLY_PROGRAMS = YesPlease
        NO_ST_BLOCKS_IN_STRUCT_STAT = YesPlease
        NO_NSEC = YesPlease
        USE_WIN32_MMAP = YesPlease
        NO_REGEX = YesPlease
        NO_PYTHON = YesPlease
        BLK_SHA1 = YesPlease
 -      COMPAT_CFLAGS += -D__USE_MINGW_ACCESS -DNOGDI -Icompat -Icompat/fnmatch -Icompat/win32
 +      ETAGS_TARGET = ETAGS
 +      NO_INET_PTON = YesPlease
 +      NO_INET_NTOP = YesPlease
 +      NO_POSIX_GOODIES = UnfortunatelyYes
 +      COMPAT_CFLAGS += -D__USE_MINGW_ACCESS -DNOGDI -Icompat -Icompat/win32
        COMPAT_CFLAGS += -DSTRIP_EXTENSION=\".exe\"
 -      COMPAT_OBJS += compat/mingw.o compat/fnmatch/fnmatch.o compat/winansi.o \
 -              compat/win32/pthread.o
 +      COMPAT_OBJS += compat/mingw.o compat/winansi.o \
 +              compat/win32/pthread.o compat/win32/syslog.o \
 +              compat/win32/sys/poll.o
        EXTLIBS += -lws2_32
        PTHREAD_LIBS =
        X = .exe
@@@ -1280,6 -1229,9 +1282,6 @@@ ifdef ZLIB_PAT
  endif
  EXTLIBS += -lz
  
 -ifndef NO_POSIX_ONLY_PROGRAMS
 -      PROGRAM_OBJS += daemon.o
 -endif
  ifndef NO_OPENSSL
        OPENSSL_LIBSSL = -lssl
        ifdef OPENSSLDIR
@@@ -1369,21 -1321,6 +1371,21 @@@ endi
  ifdef NO_STRTOULL
        COMPAT_CFLAGS += -DNO_STRTOULL
  endif
 +ifdef NO_STRTOK_R
 +      COMPAT_CFLAGS += -DNO_STRTOK_R
 +      COMPAT_OBJS += compat/strtok_r.o
 +endif
 +ifdef NO_FNMATCH
 +      COMPAT_CFLAGS += -Icompat/fnmatch
 +      COMPAT_CFLAGS += -DNO_FNMATCH
 +      COMPAT_OBJS += compat/fnmatch/fnmatch.o
 +else
 +ifdef NO_FNMATCH_CASEFOLD
 +      COMPAT_CFLAGS += -Icompat/fnmatch
 +      COMPAT_CFLAGS += -DNO_FNMATCH_CASEFOLD
 +      COMPAT_OBJS += compat/fnmatch/fnmatch.o
 +endif
 +endif
  ifdef NO_SETENV
        COMPAT_CFLAGS += -DNO_SETENV
        COMPAT_OBJS += compat/setenv.o
@@@ -1402,15 -1339,6 +1404,15 @@@ endi
  ifdef NO_SYS_SELECT_H
        BASIC_CFLAGS += -DNO_SYS_SELECT_H
  endif
 +ifdef NO_SYS_POLL_H
 +      BASIC_CFLAGS += -DNO_SYS_POLL_H
 +endif
 +ifdef NO_INTTYPES_H
 +      BASIC_CFLAGS += -DNO_INTTYPES_H
 +endif
 +ifdef NO_INITGROUPS
 +      BASIC_CFLAGS += -DNO_INITGROUPS
 +endif
  ifdef NO_MMAP
        COMPAT_CFLAGS += -DNO_MMAP
        COMPAT_OBJS += compat/mmap.o
@@@ -1448,11 -1376,9 +1450,11 @@@ endi
  endif
  ifdef NO_INET_NTOP
        LIB_OBJS += compat/inet_ntop.o
 +      BASIC_CFLAGS += -DNO_INET_NTOP
  endif
  ifdef NO_INET_PTON
        LIB_OBJS += compat/inet_pton.o
 +      BASIC_CFLAGS += -DNO_INET_PTON
  endif
  
  ifdef NO_ICONV
@@@ -1467,10 -1393,6 +1469,10 @@@ ifdef NO_DEFLATE_BOUN
        BASIC_CFLAGS += -DNO_DEFLATE_BOUND
  endif
  
 +ifdef NO_POSIX_GOODIES
 +      BASIC_CFLAGS += -DNO_POSIX_GOODIES
 +endif
 +
  ifdef BLK_SHA1
        SHA1_HEADER = "block-sha1/sha1.h"
        LIB_OBJS += block-sha1/sha1.o
@@@ -1528,7 -1450,7 +1530,7 @@@ ifdef NO_REGE
  endif
  
  ifdef USE_NED_ALLOCATOR
 -       COMPAT_CFLAGS += -DUSE_NED_ALLOCATOR -DOVERRIDE_STRDUP -DNDEBUG -DREPLACE_SYSTEM_ALLOCATOR -Icompat/nedmalloc
 +       COMPAT_CFLAGS += -Icompat/nedmalloc
         COMPAT_OBJS += compat/nedmalloc/nedmalloc.o
  endif
  
@@@ -1565,7 -1487,6 +1567,7 @@@ ifndef 
        QUIET_BUILT_IN = @echo '   ' BUILTIN $@;
        QUIET_GEN      = @echo '   ' GEN $@;
        QUIET_LNCP     = @echo '   ' LN/CP $@;
 +      QUIET_GCOV     = @echo '   ' GCOV $@;
        QUIET_SUBDIR0  = +@subdir=
        QUIET_SUBDIR1  = ;$(NO_SUBDIR) echo '   ' SUBDIR $$subdir; \
                         $(MAKE) $(PRINT_DIR) -C $$subdir
@@@ -1583,7 -1504,6 +1585,7 @@@ endi
  
  SHA1_HEADER_SQ = $(subst ','\'',$(SHA1_HEADER))
  ETC_GITCONFIG_SQ = $(subst ','\'',$(ETC_GITCONFIG))
 +ETC_GITATTRIBUTES_SQ = $(subst ','\'',$(ETC_GITATTRIBUTES))
  
  DESTDIR_SQ = $(subst ','\'',$(DESTDIR))
  bindir_SQ = $(subst ','\'',$(bindir))
@@@ -1821,11 -1741,7 +1823,11 @@@ ifndef NO_CUR
  endif
  XDIFF_OBJS = xdiff/xdiffi.o xdiff/xprepare.o xdiff/xutils.o xdiff/xemit.o \
        xdiff/xmerge.o xdiff/xpatience.o
 -OBJECTS := $(GIT_OBJS) $(XDIFF_OBJS)
 +VCSSVN_OBJS = vcs-svn/string_pool.o vcs-svn/line_buffer.o \
 +      vcs-svn/repo_tree.o vcs-svn/fast_export.o vcs-svn/svndump.o
 +VCSSVN_TEST_OBJS = test-obj-pool.o test-string-pool.o \
 +      test-line-buffer.o test-treap.o
 +OBJECTS := $(GIT_OBJS) $(XDIFF_OBJS) $(VCSSVN_OBJS)
  
  dep_files := $(foreach f,$(OBJECTS),$(dir $f).depend/$(notdir $f).d)
  dep_dirs := $(addsuffix .depend,$(sort $(dir $(OBJECTS))))
@@@ -1940,20 -1856,12 +1942,20 @@@ builtin/prune.o builtin/reflog.o reacha
  builtin/commit.o builtin/revert.o wt-status.o: wt-status.h
  builtin/tar-tree.o archive-tar.o: tar.h
  builtin/pack-objects.o: thread-utils.h
 +connect.o transport.o http-backend.o: url.h
  http-fetch.o http-walker.o remote-curl.o transport.o walker.o: walker.h
 -http.o http-walker.o http-push.o remote-curl.o: http.h
 +http.o http-walker.o http-push.o http-fetch.o remote-curl.o: http.h
  
  xdiff-interface.o $(XDIFF_OBJS): \
        xdiff/xinclude.h xdiff/xmacros.h xdiff/xdiff.h xdiff/xtypes.h \
        xdiff/xutils.h xdiff/xprepare.h xdiff/xdiffi.h xdiff/xemit.h
 +
 +$(VCSSVN_OBJS) $(VCSSVN_TEST_OBJS): $(LIB_H) \
 +      vcs-svn/obj_pool.h vcs-svn/trp.h vcs-svn/string_pool.h \
 +      vcs-svn/line_buffer.h vcs-svn/repo_tree.h vcs-svn/fast_export.h \
 +      vcs-svn/svndump.h
 +
 +test-svn-fe.o: vcs-svn/svndump.h
  endif
  
  exec_cmd.s exec_cmd.o: EXTRA_CPPFLAGS = \
@@@ -1966,29 -1874,18 +1968,29 @@@ builtin/init-db.s builtin/init-db.o: EX
  
  config.s config.o: EXTRA_CPPFLAGS = -DETC_GITCONFIG='"$(ETC_GITCONFIG_SQ)"'
  
 -http.s http.o: EXTRA_CPPFLAGS = -DGIT_USER_AGENT='"git/$(GIT_VERSION)"'
 +attr.s attr.o: EXTRA_CPPFLAGS = -DETC_GITATTRIBUTES='"$(ETC_GITATTRIBUTES_SQ)"'
 +
 +http.s http.o: EXTRA_CPPFLAGS = -DGIT_HTTP_USER_AGENT='"git/$(GIT_VERSION)"'
  
  ifdef NO_EXPAT
  http-walker.s http-walker.o: EXTRA_CPPFLAGS = -DNO_EXPAT
  endif
  
 +ifdef NO_REGEX
 +compat/regex/regex.o: EXTRA_CPPFLAGS = -DGAWK -DNO_MBSUPPORT
 +endif
 +
 +ifdef USE_NED_ALLOCATOR
 +compat/nedmalloc/nedmalloc.o: EXTRA_CPPFLAGS = \
 +      -DNDEBUG -DOVERRIDE_STRDUP -DREPLACE_SYSTEM_ALLOCATOR
 +endif
 +
  git-%$X: %.o $(GITLIBS)
        $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)
  
  git-imap-send$X: imap-send.o $(GITLIBS)
        $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
 -              $(LIBS) $(OPENSSL_LINK) $(OPENSSL_LIBSSL)
 +              $(LIBS) $(OPENSSL_LINK) $(OPENSSL_LIBSSL) $(LIB_4_CRYPTO)
  
  git-http-fetch$X: revision.o http.o http-walker.o http-fetch.o $(GITLIBS)
        $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
@@@ -2013,8 -1910,6 +2015,8 @@@ $(LIB_FILE): $(LIB_OBJS
  $(XDIFF_LIB): $(XDIFF_OBJS)
        $(QUIET_AR)$(RM) $@ && $(AR) rcs $@ $(XDIFF_OBJS)
  
 +$(VCSSVN_LIB): $(VCSSVN_OBJS)
 +      $(QUIET_AR)$(RM) $@ && $(AR) rcs $@ $(VCSSVN_OBJS)
  
  doc:
        $(MAKE) -C Documentation all
@@@ -2031,11 -1926,11 +2033,11 @@@ info
  pdf:
        $(MAKE) -C Documentation pdf
  
 -TAGS:
 -      $(RM) TAGS
 -      $(FIND) . -name '*.[hcS]' -print | xargs etags -a
 +$(ETAGS_TARGET): FORCE
 +      $(RM) $(ETAGS_TARGET)
 +      $(FIND) . -name '*.[hcS]' -print | xargs etags -a -o $(ETAGS_TARGET)
  
 -tags:
 +tags: FORCE
        $(RM) tags
        $(FIND) . -name '*.[hcS]' -print | xargs ctags -a
  
@@@ -2044,7 -1939,7 +2046,7 @@@ cscope
        $(FIND) . -name '*.[hcS]' -print | xargs cscope -b
  
  ### Detect prefix changes
 -TRACK_CFLAGS = $(subst ','\'',$(ALL_CFLAGS)):\
 +TRACK_CFLAGS = $(CC):$(subst ','\'',$(ALL_CFLAGS)):\
               $(bindir_SQ):$(gitexecdir_SQ):$(template_dir_SQ):$(prefix_SQ)
  
  GIT-CFLAGS: FORCE
@@@ -2113,18 -2008,12 +2115,18 @@@ test-date$X: date.o ctype.
  
  test-delta$X: diff-delta.o patch-delta.o
  
 +test-line-buffer$X: vcs-svn/lib.a
 +
  test-parse-options$X: parse-options.o
  
 +test-string-pool$X: vcs-svn/lib.a
 +
 +test-svn-fe$X: vcs-svn/lib.a
 +
  .PRECIOUS: $(TEST_OBJS)
  
  test-%$X: test-%.o $(GITLIBS)
 -      $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)
 +      $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(filter %.a,$^) $(LIBS)
  
  check-sha1:: test-sha1$X
        ./test-sha1.sh
@@@ -2188,19 -2077,10 +2190,19 @@@ endi
        bindir=$$(cd '$(DESTDIR_SQ)$(bindir_SQ)' && pwd) && \
        execdir=$$(cd '$(DESTDIR_SQ)$(gitexec_instdir_SQ)' && pwd) && \
        { test "$$bindir/" = "$$execdir/" || \
 -              { $(RM) "$$execdir/git$X" && \
 +        for p in git$X $(filter $(install_bindir_programs),$(ALL_PROGRAMS)); do \
 +              $(RM) "$$execdir/$$p" && \
                test -z "$(NO_CROSS_DIRECTORY_HARDLINKS)" && \
 -              ln "$$bindir/git$X" "$$execdir/git$X" 2>/dev/null || \
 -              cp "$$bindir/git$X" "$$execdir/git$X"; } ; } && \
 +              ln "$$bindir/$$p" "$$execdir/$$p" 2>/dev/null || \
 +              cp "$$bindir/$$p" "$$execdir/$$p" || exit; \
 +        done; \
 +      } && \
 +      for p in $(filter $(install_bindir_programs),$(BUILT_INS)); do \
 +              $(RM) "$$bindir/$$p" && \
 +              ln "$$bindir/git$X" "$$bindir/$$p" 2>/dev/null || \
 +              ln -s "git$X" "$$bindir/$$p" 2>/dev/null || \
 +              cp "$$bindir/git$X" "$$bindir/$$p" || exit; \
 +      done && \
        for p in $(BUILT_INS); do \
                $(RM) "$$execdir/$$p" && \
                ln "$$execdir/git$X" "$$execdir/$$p" 2>/dev/null || \
@@@ -2299,13 -2179,13 +2301,13 @@@ distclean: clea
        $(RM) configure
  
  clean:
 -      $(RM) *.o block-sha1/*.o ppc/*.o compat/*.o compat/*/*.o xdiff/*.o \
 -              builtin/*.o $(LIB_FILE) $(XDIFF_LIB)
 +      $(RM) *.o block-sha1/*.o ppc/*.o compat/*.o compat/*/*.o xdiff/*.o vcs-svn/*.o \
 +              builtin/*.o $(LIB_FILE) $(XDIFF_LIB) $(VCSSVN_LIB)
        $(RM) $(ALL_PROGRAMS) $(SCRIPT_LIB) $(BUILT_INS) git$X
        $(RM) $(TEST_PROGRAMS)
        $(RM) -r bin-wrappers
        $(RM) -r $(dep_dirs)
 -      $(RM) *.spec *.pyc *.pyo */*.pyc */*.pyo common-cmds.h TAGS tags cscope*
 +      $(RM) *.spec *.pyc *.pyo */*.pyc */*.pyo common-cmds.h $(ETAGS_TARGET) tags cscope*
        $(RM) -r autom4te.cache
        $(RM) config.log config.mak.autogen config.mak.append config.status config.cache
        $(RM) -r $(GIT_TARNAME) .doc-tmp-dir
@@@ -2329,7 -2209,7 +2331,7 @@@ endi
  
  .PHONY: all install clean strip
  .PHONY: shell_compatibility_test please_set_SHELL_PATH_to_a_more_modern_shell
 -.PHONY: FORCE TAGS tags cscope
 +.PHONY: FORCE cscope
  
  ### Check documentation
  #
@@@ -2403,18 -2283,11 +2405,18 @@@ coverage
        $(MAKE) coverage-build
        $(MAKE) coverage-report
  
 +object_dirs := $(sort $(dir $(OBJECTS)))
  coverage-clean:
 -      rm -f *.gcda *.gcno
 +      $(RM) $(addsuffix *.gcov,$(object_dirs))
 +      $(RM) $(addsuffix *.gcda,$(object_dirs))
 +      $(RM) $(addsuffix *.gcno,$(object_dirs))
 +      $(RM) coverage-untested-functions
 +      $(RM) -r cover_db/
 +      $(RM) -r cover_db_html/
  
  COVERAGE_CFLAGS = $(CFLAGS) -O0 -ftest-coverage -fprofile-arcs
  COVERAGE_LDFLAGS = $(CFLAGS)  -O0 -lgcov
 +GCOVFLAGS = --preserve-paths --branch-probabilities --all-blocks
  
  coverage-build: coverage-clean
        $(MAKE) CFLAGS="$(COVERAGE_CFLAGS)" LDFLAGS="$(COVERAGE_LDFLAGS)" all
                -j1 test
  
  coverage-report:
 -      gcov -b *.c
 +      $(QUIET_GCOV)for dir in $(object_dirs); do \
 +              $(GCOV) $(GCOVFLAGS) --object-directory=$$dir $$dir*.c || exit; \
 +      done
 +
 +coverage-untested-functions: coverage-report
        grep '^function.*called 0 ' *.c.gcov \
                | sed -e 's/\([^:]*\)\.gcov: *function \([^ ]*\) called.*/\1: \2/' \
 -              | tee coverage-untested-functions
 +              > coverage-untested-functions
 +
 +cover_db: coverage-report
 +      gcov2perl -db cover_db *.gcov
 +
 +cover_db_html: cover_db
 +      cover -report html -outputdir cover_db_html cover_db
diff --combined builtin.h
index 8dd4569b3c06654826c298b6b52dcf2ac17481bb,908d85064db32fa80e2b63e222ff0316088c3223..c3e5db268566d3f78cebb4021ed0a201e1dbfa87
+++ b/builtin.h
@@@ -7,16 -7,17 +7,16 @@@
  #include "commit.h"
  #include "notes.h"
  
 +#define DEFAULT_MERGE_LOG_LEN 20
 +
  extern const char git_version_string[];
  extern const char git_usage_string[];
  extern const char git_more_info_string[];
  
 -extern void list_common_cmds_help(void);
 -extern const char *help_unknown_cmd(const char *cmd);
  extern void prune_packed_objects(int);
 -extern int fmt_merge_msg(int merge_summary, struct strbuf *in,
 -      struct strbuf *out);
 -extern int fmt_merge_msg_shortlog(struct strbuf *in, struct strbuf *out);
 +extern int fmt_merge_msg(struct strbuf *in, struct strbuf *out,
 +                       int merge_title, int shortlog_len);
- extern int commit_notes(struct notes_tree *t, const char *msg);
+ extern void commit_notes(struct notes_tree *t, const char *msg);
  
  struct notes_rewrite_cfg {
        struct notes_tree **trees;
@@@ -36,7 -37,7 +36,7 @@@ void finish_copy_notes_for_rewrite(stru
  
  extern int check_pager_config(const char *cmd);
  
 -extern int textconv_object(const char *path, const unsigned char *sha1, char **buf, unsigned long *buf_size);
 +extern int textconv_object(const char *path, unsigned mode, const unsigned char *sha1, char **buf, unsigned long *buf_size);
  
  extern int cmd_add(int argc, const char **argv, const char *prefix);
  extern int cmd_annotate(int argc, const char **argv, const char *prefix);
diff --combined builtin/merge.c
index c24a7be020d10540581b41c21a2ecf83df5563d9,0539f7c91289a5707330391424a81d8ba3ba7df9..3921cd304086cbc2edff0e1481f7c7395df8b00a
@@@ -42,7 -42,7 +42,7 @@@ static const char * const builtin_merge
        NULL
  };
  
 -static int show_diffstat = 1, option_log, squash;
 +static int show_diffstat = 1, shortlog_len, squash;
  static int option_commit = 1, allow_fast_forward = 1;
  static int fast_forward_only;
  static int allow_trivial = 1, have_message;
@@@ -54,9 -54,9 +54,10 @@@ static size_t use_strategies_nr, use_st
  static const char **xopts;
  static size_t xopts_nr, xopts_alloc;
  static const char *branch;
 +static int option_renormalize;
  static int verbosity;
  static int allow_rerere_auto;
+ static int abort_current_merge;
  
  static struct strategy all_strategy[] = {
        { "recursive",  DEFAULT_TWOHEAD | NO_TRIVIAL },
@@@ -132,7 -132,6 +133,7 @@@ static struct strategy *get_strategy(co
  
        ret = xcalloc(1, sizeof(struct strategy));
        ret->name = xstrdup(name);
 +      ret->attr = NO_TRIVIAL;
        return ret;
  }
  
@@@ -177,9 -176,8 +178,9 @@@ static struct option builtin_merge_opti
        OPT_BOOLEAN(0, "stat", &show_diffstat,
                "show a diffstat at the end of the merge"),
        OPT_BOOLEAN(0, "summary", &show_diffstat, "(synonym to --stat)"),
 -      OPT_BOOLEAN(0, "log", &option_log,
 -              "add list of one-line log to merge commit message"),
 +      { OPTION_INTEGER, 0, "log", &shortlog_len, "n",
 +        "add (at most <n>) entries from shortlog to merge commit message",
 +        PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN },
        OPT_BOOLEAN(0, "squash", &squash,
                "create a single commit instead of doing a merge"),
        OPT_BOOLEAN(0, "commit", &option_commit,
                "message to be used for the merge commit (if any)",
                option_parse_message),
        OPT__VERBOSITY(&verbosity),
+       OPT_BOOLEAN(0, "abort", &abort_current_merge,
+               "abort the current in-progress merge"),
        OPT_END()
  };
  
@@@ -234,24 -234,6 +237,24 @@@ static void save_state(void
                die("not a valid object: %s", buffer.buf);
  }
  
 +static void read_empty(unsigned const char *sha1, int verbose)
 +{
 +      int i = 0;
 +      const char *args[7];
 +
 +      args[i++] = "read-tree";
 +      if (verbose)
 +              args[i++] = "-v";
 +      args[i++] = "-m";
 +      args[i++] = "-u";
 +      args[i++] = EMPTY_TREE_SHA1_HEX;
 +      args[i++] = sha1_to_hex(sha1);
 +      args[i] = NULL;
 +
 +      if (run_command_v_opt(args, RUN_GIT_CMD))
 +              die("read-tree failed");
 +}
 +
  static void reset_hard(unsigned const char *sha1, int verbose)
  {
        int i = 0;
@@@ -421,7 -403,7 +424,7 @@@ static void merge_name(const char *remo
                        goto cleanup;
                }
                if (!prefixcmp(found_ref, "refs/remotes/")) {
 -                      strbuf_addf(msg, "%s\t\tremote branch '%s' of .\n",
 +                      strbuf_addf(msg, "%s\t\tremote-tracking branch '%s' of .\n",
                                    sha1_to_hex(branch_head), remote);
                        goto cleanup;
                }
                strbuf_addstr(&truname, "refs/heads/");
                strbuf_addstr(&truname, remote);
                strbuf_setlen(&truname, truname.len - len);
 -              if (resolve_ref(truname.buf, buf_sha, 0, NULL)) {
 +              if (resolve_ref(truname.buf, buf_sha, 1, NULL)) {
                        strbuf_addf(msg,
                                    "%s\t\tbranch '%s'%s of .\n",
                                    sha1_to_hex(remote_head->sha1),
@@@ -507,8 -489,7 +510,8 @@@ static int git_merge_config(const char 
                buf = xstrdup(v);
                argc = split_cmdline(buf, &argv);
                if (argc < 0)
 -                      die("Bad branch.%s.mergeoptions string", branch);
 +                      die("Bad branch.%s.mergeoptions string: %s", branch,
 +                          split_cmdline_strerror(argc));
                argv = xrealloc(argv, sizeof(*argv) * (argc + 2));
                memmove(argv + 1, argv, sizeof(*argv) * (argc + 1));
                argc++;
                return git_config_string(&pull_twohead, k, v);
        else if (!strcmp(k, "pull.octopus"))
                return git_config_string(&pull_octopus, k, v);
 -      else if (!strcmp(k, "merge.log") || !strcmp(k, "merge.summary"))
 -              option_log = git_config_bool(k, v);
 +      else if (!strcmp(k, "merge.renormalize"))
 +              option_renormalize = git_config_bool(k, v);
 +      else if (!strcmp(k, "merge.log") || !strcmp(k, "merge.summary")) {
 +              int is_bool;
 +              shortlog_len = git_config_bool_or_int(k, v, &is_bool);
 +              if (!is_bool && shortlog_len < 0)
 +                      return error("%s: negative length %s", k, v);
 +              if (is_bool && shortlog_len)
 +                      shortlog_len = DEFAULT_MERGE_LOG_LEN;
 +              return 0;
 +      }
        return git_diff_ui_config(k, v, cb);
  }
  
@@@ -655,11 -627,18 +658,11 @@@ static int try_merge_strategy(const cha
                if (!strcmp(strategy, "subtree"))
                        o.subtree_shift = "";
  
 -              for (x = 0; x < xopts_nr; x++) {
 -                      if (!strcmp(xopts[x], "ours"))
 -                              o.recursive_variant = MERGE_RECURSIVE_OURS;
 -                      else if (!strcmp(xopts[x], "theirs"))
 -                              o.recursive_variant = MERGE_RECURSIVE_THEIRS;
 -                      else if (!strcmp(xopts[x], "subtree"))
 -                              o.subtree_shift = "";
 -                      else if (!prefixcmp(xopts[x], "subtree="))
 -                              o.subtree_shift = xopts[x]+8;
 -                      else
 +              o.renormalize = option_renormalize;
 +
 +              for (x = 0; x < xopts_nr; x++)
 +                      if (parse_merge_opt(&o, xopts[x]))
                                die("Unknown option for merge-recursive: -X%s", xopts[x]);
 -              }
  
                o.branch1 = head_arg;
                o.branch2 = remoteheads->item->util;
@@@ -728,7 -707,7 +731,7 @@@ int checkout_fast_forward(const unsigne
        opts.verbose_update = 1;
        opts.merge = 1;
        opts.fn = twoway_merge;
 -      opts.msgs = get_porcelain_error_msgs();
 +      setup_unpack_trees_porcelain(&opts, "merge");
  
        trees[nr_trees] = parse_tree_indirect(head);
        if (!trees[nr_trees++])
@@@ -840,7 -819,7 +843,7 @@@ static int finish_automerge(struct comm
        return 0;
  }
  
 -static int suggest_conflicts(void)
 +static int suggest_conflicts(int renormalizing)
  {
        FILE *fp;
        int pos;
@@@ -919,22 -898,6 +922,6 @@@ int cmd_merge(int argc, const char **ar
        const char *best_strategy = NULL, *wt_strategy = NULL;
        struct commit_list **remotes = &remoteheads;
  
-       if (read_cache_unmerged()) {
-               die_resolve_conflict("merge");
-       }
-       if (file_exists(git_path("MERGE_HEAD"))) {
-               /*
-                * There is no unmerged entry, don't advise 'git
-                * add/rm <file>', just 'git commit'.
-                */
-               if (advice_resolve_conflict)
-                       die("You have not concluded your merge (MERGE_HEAD exists).\n"
-                           "Please, commit your changes before you can merge.");
-               else
-                       die("You have not concluded your merge (MERGE_HEAD exists).");
-       }
-       resolve_undo_clear();
        /*
         * Check if we are _not_ on a detached HEAD, i.e. if there is a
         * current branch.
  
        argc = parse_options(argc, argv, prefix, builtin_merge_options,
                        builtin_merge_usage, 0);
+       if (abort_current_merge) {
+               int nargc = 2;
+               const char *nargv[] = {"reset", "--merge", NULL};
+               if (!file_exists(git_path("MERGE_HEAD")))
+                       die("There is no merge to abort (MERGE_HEAD missing).");
+               /* Invoke 'git reset --merge' */
+               return cmd_reset(nargc, nargv, prefix);
+       }
+       if (read_cache_unmerged())
+               die_resolve_conflict("merge");
+       if (file_exists(git_path("MERGE_HEAD"))) {
+               /*
+                * There is no unmerged entry, don't advise 'git
+                * add/rm <file>', just 'git commit'.
+                */
+               if (advice_resolve_conflict)
+                       die("You have not concluded your merge (MERGE_HEAD exists).\n"
+                           "Please, commit your changes before you can merge.");
+               else
+                       die("You have not concluded your merge (MERGE_HEAD exists).");
+       }
+       resolve_undo_clear();
        if (verbosity < 0)
                show_diffstat = 0;
  
                        die("%s - not something we can merge", argv[0]);
                update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0,
                                DIE_ON_ERR);
 -              reset_hard(remote_head->sha1, 0);
 +              read_empty(remote_head->sha1, 0);
                return 0;
        } else {
                struct strbuf merge_names = STRBUF_INIT;
                for (i = 0; i < argc; i++)
                        merge_name(argv[i], &merge_names);
  
 -              if (have_message && option_log)
 -                      fmt_merge_msg_shortlog(&merge_names, &merge_msg);
 -              else if (!have_message)
 -                      fmt_merge_msg(option_log, &merge_names, &merge_msg);
 -
 -
 -              if (!(have_message && !option_log) && merge_msg.len)
 -                      strbuf_setlen(&merge_msg, merge_msg.len-1);
 +              if (!have_message || shortlog_len) {
 +                      fmt_merge_msg(&merge_names, &merge_msg, !have_message,
 +                                    shortlog_len);
 +                      if (merge_msg.len)
 +                              strbuf_setlen(&merge_msg, merge_msg.len - 1);
 +              }
        }
  
        if (head_invalid || !argc)
                        "stopped before committing as requested\n");
                return 0;
        } else
 -              return suggest_conflicts();
 +              return suggest_conflicts(option_renormalize);
  }
diff --combined builtin/notes.c
index c85cbf5a4758c8c08ef43dd65d9f85669c0063f2,f5abf7aa6e70d42b8f1e8ca498e5ea084f1c73ba..4d5556e2cb5bccaf0100d9f5019e1a1c493e35e7
@@@ -17,6 -17,7 +17,7 @@@
  #include "run-command.h"
  #include "parse-options.h"
  #include "string-list.h"
+ #include "notes-merge.h"
  
  static const char * const git_notes_usage[] = {
        "git notes [--ref <notes_ref>] [list [<object>]]",
        "git notes [--ref <notes_ref>] append [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]",
        "git notes [--ref <notes_ref>] edit [<object>]",
        "git notes [--ref <notes_ref>] show [<object>]",
+       "git notes [--ref <notes_ref>] merge [-v | -q] [-s <strategy> ] <notes_ref>",
+       "git notes merge --commit [-v | -q]",
+       "git notes merge --abort [-v | -q]",
        "git notes [--ref <notes_ref>] remove [<object>]",
        "git notes [--ref <notes_ref>] prune [-n | -v]",
+       "git notes [--ref <notes_ref>] get-ref",
        NULL
  };
  
@@@ -61,6 -66,13 +66,13 @@@ static const char * const git_notes_sho
        NULL
  };
  
+ static const char * const git_notes_merge_usage[] = {
+       "git notes merge [<options>] <notes_ref>",
+       "git notes merge --commit [<options>]",
+       "git notes merge --abort [<options>]",
+       NULL
+ };
  static const char * const git_notes_remove_usage[] = {
        "git notes remove [<object>]",
        NULL
@@@ -71,6 -83,11 +83,11 @@@ static const char * const git_notes_pru
        NULL
  };
  
+ static const char * const git_notes_get_ref_usage[] = {
+       "git notes get-ref",
+       NULL
+ };
  static const char note_template[] =
        "\n"
        "#\n"
@@@ -83,6 -100,16 +100,16 @@@ struct msg_arg 
        struct strbuf buf;
  };
  
+ static void expand_notes_ref(struct strbuf *sb)
+ {
+       if (!prefixcmp(sb->buf, "refs/notes/"))
+               return; /* we're happy */
+       else if (!prefixcmp(sb->buf, "notes/"))
+               strbuf_insert(sb, 0, "refs/", 5);
+       else
+               strbuf_insert(sb, 0, "refs/notes/", 11);
+ }
  static int list_each_note(const unsigned char *object_sha1,
                const unsigned char *note_sha1, char *note_path,
                void *cb_data)
@@@ -271,18 -298,17 +298,17 @@@ static int parse_reedit_arg(const struc
        return parse_reuse_arg(opt, arg, unset);
  }
  
int commit_notes(struct notes_tree *t, const char *msg)
void commit_notes(struct notes_tree *t, const char *msg)
  {
-       struct commit_list *parent;
-       unsigned char tree_sha1[20], prev_commit[20], new_commit[20];
        struct strbuf buf = STRBUF_INIT;
+       unsigned char commit_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)
-               return 0; /* 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 */
        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);
-       return 0;
  }
  
  combine_notes_fn parse_combine_notes_fn(const char *v)
                return combine_notes_ignore;
        else if (!strcasecmp(v, "concatenate"))
                return combine_notes_concatenate;
+       else if (!strcasecmp(v, "cat_sort_uniq"))
+               return combine_notes_cat_sort_uniq;
        else
                return NULL;
  }
@@@ -538,7 -549,7 +549,7 @@@ static int add(int argc, const char **a
                { OPTION_CALLBACK, 'C', "reuse-message", &msg, "OBJECT",
                        "reuse specified note object", PARSE_OPT_NONEG,
                        parse_reuse_arg},
 -              OPT_BOOLEAN('f', "force", &force, "replace existing notes"),
 +              OPT__FORCE(&force, "replace existing notes"),
                OPT_END()
        };
  
  
        if (is_null_sha1(new_note))
                remove_note(t, object);
-       else
-               add_note(t, object, new_note, combine_notes_overwrite);
+       else if (add_note(t, object, new_note, combine_notes_overwrite))
+               die("BUG: combine_notes_overwrite failed");
  
        snprintf(logmsg, sizeof(logmsg), "Notes %s by 'git notes %s'",
                 is_null_sha1(new_note) ? "removed" : "added", "add");
@@@ -594,7 -605,7 +605,7 @@@ static int copy(int argc, const char **
        struct notes_tree *t;
        const char *rewrite_cmd = NULL;
        struct option options[] = {
 -              OPT_BOOLEAN('f', "force", &force, "replace existing notes"),
 +              OPT__FORCE(&force, "replace existing notes"),
                OPT_BOOLEAN(0, "stdin", &from_stdin, "read objects from stdin"),
                OPT_STRING(0, "for-rewrite", &rewrite_cmd, "command",
                           "load rewriting config for <command> (implies "
                goto out;
        }
  
-       add_note(t, object, from_note, combine_notes_overwrite);
+       if (add_note(t, object, from_note, combine_notes_overwrite))
+               die("BUG: combine_notes_overwrite failed");
        commit_notes(t, "Notes added by 'git notes copy'");
  out:
        free_notes(t);
@@@ -712,8 -724,8 +724,8 @@@ static int append_edit(int argc, const 
  
        if (is_null_sha1(new_note))
                remove_note(t, object);
-       else
-               add_note(t, object, new_note, combine_notes_overwrite);
+       else if (add_note(t, object, new_note, combine_notes_overwrite))
+               die("BUG: combine_notes_overwrite failed");
  
        snprintf(logmsg, sizeof(logmsg), "Notes %s by 'git notes %s'",
                 is_null_sha1(new_note) ? "removed" : "added", argv[0]);
@@@ -761,6 -773,180 +773,180 @@@ static int show(int argc, const char **
        return retval;
  }
  
+ static int merge_abort(struct notes_merge_options *o)
+ {
+       int ret = 0;
+       /*
+        * Remove .git/NOTES_MERGE_PARTIAL and .git/NOTES_MERGE_REF, and call
+        * notes_merge_abort() to remove .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_abort(o))
+               ret += error("Failed to remove 'git notes merge' worktree");
+       return ret;
+ }
+ static int merge_commit(struct notes_merge_options *o)
+ {
+       struct strbuf msg = STRBUF_INIT;
+       unsigned char sha1[20], parent_sha1[20];
+       struct notes_tree *t;
+       struct commit *partial;
+       struct pretty_print_context pretty_ctx;
+       /*
+        * Read partial merge result from .git/NOTES_MERGE_PARTIAL,
+        * and target notes ref from .git/NOTES_MERGE_REF.
+        */
+       if (get_sha1("NOTES_MERGE_PARTIAL", sha1))
+               die("Failed to read ref NOTES_MERGE_PARTIAL");
+       else if (!(partial = lookup_commit_reference(sha1)))
+               die("Could not find commit from NOTES_MERGE_PARTIAL.");
+       else if (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(struct notes_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,
+                  is_null_sha1(parent_sha1) ? NULL : parent_sha1,
+                  0, DIE_ON_ERR);
+       free_notes(t);
+       strbuf_release(&msg);
+       return merge_abort(o);
+ }
+ static int merge(int argc, const char **argv, const char *prefix)
+ {
+       struct strbuf remote_ref = STRBUF_INIT, msg = STRBUF_INIT;
+       unsigned char result_sha1[20];
+       struct notes_tree *t;
+       struct notes_merge_options o;
+       int do_merge = 0, do_commit = 0, do_abort = 0;
+       int verbosity = 0, result;
+       const char *strategy = NULL;
+       struct option options[] = {
+               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/cat_sort_uniq)"),
+               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, "abort", &do_abort, 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 (strategy || do_commit + do_abort == 0)
+               do_merge = 1;
+       if (do_merge + do_commit + do_abort != 1) {
+               error("cannot mix --commit, --abort 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);
+       } else if (!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_abort)
+               return merge_abort(&o);
+       if (do_commit)
+               return merge_commit(&o);
+       o.local_ref = default_notes_ref();
+       strbuf_addstr(&remote_ref, argv[0]);
+       expand_notes_ref(&remote_ref);
+       o.remote_ref = remote_ref.buf;
+       if (strategy) {
+               if (!strcmp(strategy, "manual"))
+                       o.strategy = NOTES_MERGE_RESOLVE_MANUAL;
+               else if (!strcmp(strategy, "ours"))
+                       o.strategy = NOTES_MERGE_RESOLVE_OURS;
+               else if (!strcmp(strategy, "theirs"))
+                       o.strategy = NOTES_MERGE_RESOLVE_THEIRS;
+               else if (!strcmp(strategy, "union"))
+                       o.strategy = NOTES_MERGE_RESOLVE_UNION;
+               else if (!strcmp(strategy, "cat_sort_uniq"))
+                       o.strategy = NOTES_MERGE_RESOLVE_CAT_SORT_UNIQ;
+               else {
+                       error("Unknown -s/--strategy: %s", strategy);
+                       usage_with_options(git_notes_merge_usage, options);
+               }
+       }
+       t = init_notes_check("merge");
+       strbuf_addf(&msg, "notes: Merged notes from %s into %s",
+                   remote_ref.buf, default_notes_ref());
+       strbuf_add(&(o.commit_msg), msg.buf + 7, msg.len - 7); /* skip "notes: " */
+       result = notes_merge(&o, t, result_sha1);
+       if (result >= 0) /* Merge resulted (trivially) in result_sha1 */
+               /* 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 */
+               /* 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 --abort'.\n",
+                      git_path(NOTES_MERGE_WORKTREE));
+       }
+       free_notes(t);
+       strbuf_release(&remote_ref);
+       strbuf_release(&msg);
+       return result < 0; /* return non-zero on conflicts */
+ }
  static int remove_cmd(int argc, const char **argv, const char *prefix)
  {
        struct option options[] = {
        const char *object_ref;
        struct notes_tree *t;
        unsigned char object[20];
 +      int retval;
  
        argc = parse_options(argc, argv, prefix, options,
                             git_notes_remove_usage, 0);
  
        t = init_notes_check("remove");
  
 -      fprintf(stderr, "Removing note for object %s\n", sha1_to_hex(object));
 -      remove_note(t, object);
 +      retval = remove_note(t, object);
 +      if (retval)
 +              fprintf(stderr, "Object %s has no note\n", sha1_to_hex(object));
 +      else {
 +              fprintf(stderr, "Removing note for object %s\n",
 +                      sha1_to_hex(object));
  
 -      commit_notes(t, "Notes removed by 'git notes remove'");
 +              commit_notes(t, "Notes removed by 'git notes remove'");
 +      }
        free_notes(t);
 -      return 0;
 +      return retval;
  }
  
  static int prune(int argc, const char **argv, const char *prefix)
        struct notes_tree *t;
        int show_only = 0, verbose = 0;
        struct option options[] = {
 -              OPT_BOOLEAN('n', "dry-run", &show_only,
 -                          "do not remove, show only"),
 -              OPT_BOOLEAN('v', "verbose", &verbose, "report pruned notes"),
 +              OPT__DRY_RUN(&show_only, "do not remove, show only"),
 +              OPT__VERBOSE(&verbose, "report pruned notes"),
                OPT_END()
        };
  
        return 0;
  }
  
+ static int get_ref(int argc, const char **argv, const char *prefix)
+ {
+       struct option options[] = { OPT_END() };
+       argc = parse_options(argc, argv, prefix, options,
+                            git_notes_get_ref_usage, 0);
+       if (argc) {
+               error("too many parameters");
+               usage_with_options(git_notes_get_ref_usage, options);
+       }
+       puts(default_notes_ref());
+       return 0;
+ }
  int cmd_notes(int argc, const char **argv, const char *prefix)
  {
        int result;
  
        if (override_notes_ref) {
                struct strbuf sb = STRBUF_INIT;
-               if (!prefixcmp(override_notes_ref, "refs/notes/"))
-                       /* we're happy */;
-               else if (!prefixcmp(override_notes_ref, "notes/"))
-                       strbuf_addstr(&sb, "refs/");
-               else
-                       strbuf_addstr(&sb, "refs/notes/");
                strbuf_addstr(&sb, override_notes_ref);
+               expand_notes_ref(&sb);
                setenv("GIT_NOTES_REF", sb.buf, 1);
                strbuf_release(&sb);
        }
                result = append_edit(argc, argv, prefix);
        else if (!strcmp(argv[0], "show"))
                result = show(argc, argv, prefix);
+       else if (!strcmp(argv[0], "merge"))
+               result = merge(argc, argv, prefix);
        else if (!strcmp(argv[0], "remove"))
                result = remove_cmd(argc, argv, prefix);
        else if (!strcmp(argv[0], "prune"))
                result = prune(argc, argv, prefix);
+       else if (!strcmp(argv[0], "get-ref"))
+               result = get_ref(argc, argv, prefix);
        else {
                result = error("Unknown subcommand: %s", argv[0]);
                usage_with_options(git_notes_usage, options);
diff --combined notes.c
index 70d00135eb5b67cd6f21b416cde2ae4a1967fd0a,96cde4213403c6b77ca5490b459db5fad17776be..a013c1bc638dbf5a8111183a3f9d154721ec5e04
+++ b/notes.c
@@@ -149,86 -149,6 +149,6 @@@ static struct leaf_node *note_tree_find
        return NULL;
  }
  
- /*
-  * To insert a leaf_node:
-  * Search to the tree location appropriate for the given leaf_node's key:
-  * - If location is unused (NULL), store the tweaked pointer directly there
-  * - If location holds a note entry that matches the note-to-be-inserted, then
-  *   combine the two notes (by calling the given combine_notes function).
-  * - If location holds a note entry that matches the subtree-to-be-inserted,
-  *   then unpack the subtree-to-be-inserted into the location.
-  * - If location holds a matching subtree entry, unpack the subtree at that
-  *   location, and restart the insert operation from that level.
-  * - Else, create a new int_node, holding both the node-at-location and the
-  *   node-to-be-inserted, and store the new int_node into the location.
-  */
- static void note_tree_insert(struct notes_tree *t, struct int_node *tree,
-               unsigned char n, struct leaf_node *entry, unsigned char type,
-               combine_notes_fn combine_notes)
- {
-       struct int_node *new_node;
-       struct leaf_node *l;
-       void **p = note_tree_search(t, &tree, &n, entry->key_sha1);
-       assert(GET_PTR_TYPE(entry) == 0); /* no type bits set */
-       l = (struct leaf_node *) CLR_PTR_TYPE(*p);
-       switch (GET_PTR_TYPE(*p)) {
-       case PTR_TYPE_NULL:
-               assert(!*p);
-               *p = SET_PTR_TYPE(entry, type);
-               return;
-       case PTR_TYPE_NOTE:
-               switch (type) {
-               case PTR_TYPE_NOTE:
-                       if (!hashcmp(l->key_sha1, entry->key_sha1)) {
-                               /* skip concatenation if l == entry */
-                               if (!hashcmp(l->val_sha1, entry->val_sha1))
-                                       return;
-                               if (combine_notes(l->val_sha1, entry->val_sha1))
-                                       die("failed to combine notes %s and %s"
-                                           " for object %s",
-                                           sha1_to_hex(l->val_sha1),
-                                           sha1_to_hex(entry->val_sha1),
-                                           sha1_to_hex(l->key_sha1));
-                               free(entry);
-                               return;
-                       }
-                       break;
-               case PTR_TYPE_SUBTREE:
-                       if (!SUBTREE_SHA1_PREFIXCMP(l->key_sha1,
-                                                   entry->key_sha1)) {
-                               /* unpack 'entry' */
-                               load_subtree(t, entry, tree, n);
-                               free(entry);
-                               return;
-                       }
-                       break;
-               }
-               break;
-       case PTR_TYPE_SUBTREE:
-               if (!SUBTREE_SHA1_PREFIXCMP(entry->key_sha1, l->key_sha1)) {
-                       /* unpack 'l' and restart insert */
-                       *p = NULL;
-                       load_subtree(t, l, tree, n);
-                       free(l);
-                       note_tree_insert(t, tree, n, entry, type,
-                                        combine_notes);
-                       return;
-               }
-               break;
-       }
-       /* non-matching leaf_node */
-       assert(GET_PTR_TYPE(*p) == PTR_TYPE_NOTE ||
-              GET_PTR_TYPE(*p) == PTR_TYPE_SUBTREE);
-       new_node = (struct int_node *) xcalloc(sizeof(struct int_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);
- }
  /*
   * How to consolidate an int_node:
   * If there are > 1 non-NULL entries, give up and return non-zero.
@@@ -263,13 -183,11 +183,13 @@@ static int note_tree_consolidate(struc
   * To remove a leaf_node:
   * Search to the tree location appropriate for the given leaf_node's key:
   * - If location does not hold a matching entry, abort and do nothing.
 + * - Copy the matching entry's value into the given entry.
   * - Replace the matching leaf_node with a NULL entry (and free the leaf_node).
   * - Consolidate int_nodes repeatedly, while walking up the tree towards root.
   */
 -static void note_tree_remove(struct notes_tree *t, struct int_node *tree,
 -              unsigned char n, struct leaf_node *entry)
 +static void note_tree_remove(struct notes_tree *t,
 +              struct int_node *tree, unsigned char n,
 +              struct leaf_node *entry)
  {
        struct leaf_node *l;
        struct int_node *parent_stack[20];
                return; /* key mismatch, nothing to remove */
  
        /* we have found a matching entry */
 +      hashcpy(entry->val_sha1, l->val_sha1);
        free(l);
        *p = SET_PTR_TYPE(NULL, PTR_TYPE_NULL);
  
                i--;
  }
  
+ /*
+  * To insert a leaf_node:
+  * Search to the tree location appropriate for the given leaf_node's key:
+  * - If location is unused (NULL), store the tweaked pointer directly there
+  * - If location holds a note entry that matches the note-to-be-inserted, then
+  *   combine the two notes (by calling the given combine_notes function).
+  * - If location holds a note entry that matches the subtree-to-be-inserted,
+  *   then unpack the subtree-to-be-inserted into the location.
+  * - If location holds a matching subtree entry, unpack the subtree at that
+  *   location, and restart the insert operation from that level.
+  * - Else, create a new int_node, holding both the node-at-location and the
+  *   node-to-be-inserted, and store the new int_node into the location.
+  */
+ static int note_tree_insert(struct notes_tree *t, struct int_node *tree,
+               unsigned char n, struct leaf_node *entry, unsigned char type,
+               combine_notes_fn combine_notes)
+ {
+       struct int_node *new_node;
+       struct leaf_node *l;
+       void **p = note_tree_search(t, &tree, &n, entry->key_sha1);
+       int ret = 0;
+       assert(GET_PTR_TYPE(entry) == 0); /* no type bits set */
+       l = (struct leaf_node *) CLR_PTR_TYPE(*p);
+       switch (GET_PTR_TYPE(*p)) {
+       case PTR_TYPE_NULL:
+               assert(!*p);
+               if (is_null_sha1(entry->val_sha1))
+                       free(entry);
+               else
+                       *p = SET_PTR_TYPE(entry, type);
+               return 0;
+       case PTR_TYPE_NOTE:
+               switch (type) {
+               case PTR_TYPE_NOTE:
+                       if (!hashcmp(l->key_sha1, entry->key_sha1)) {
+                               /* skip concatenation if l == entry */
+                               if (!hashcmp(l->val_sha1, entry->val_sha1))
+                                       return 0;
+                               ret = combine_notes(l->val_sha1,
+                                                   entry->val_sha1);
+                               if (!ret && is_null_sha1(l->val_sha1))
+                                       note_tree_remove(t, tree, n, entry);
+                               free(entry);
+                               return ret;
+                       }
+                       break;
+               case PTR_TYPE_SUBTREE:
+                       if (!SUBTREE_SHA1_PREFIXCMP(l->key_sha1,
+                                                   entry->key_sha1)) {
+                               /* unpack 'entry' */
+                               load_subtree(t, entry, tree, n);
+                               free(entry);
+                               return 0;
+                       }
+                       break;
+               }
+               break;
+       case PTR_TYPE_SUBTREE:
+               if (!SUBTREE_SHA1_PREFIXCMP(entry->key_sha1, l->key_sha1)) {
+                       /* unpack 'l' and restart insert */
+                       *p = NULL;
+                       load_subtree(t, l, tree, n);
+                       free(l);
+                       return note_tree_insert(t, tree, n, entry, type,
+                                               combine_notes);
+               }
+               break;
+       }
+       /* non-matching leaf_node */
+       assert(GET_PTR_TYPE(*p) == PTR_TYPE_NOTE ||
+              GET_PTR_TYPE(*p) == PTR_TYPE_SUBTREE);
+       if (is_null_sha1(entry->val_sha1)) { /* skip insertion of empty note */
+               free(entry);
+               return 0;
+       }
+       new_node = (struct int_node *) xcalloc(sizeof(struct int_node), 1);
+       ret = note_tree_insert(t, new_node, n + 1, l, GET_PTR_TYPE(*p),
+                              combine_notes);
+       if (ret)
+               return ret;
+       *p = SET_PTR_TYPE(new_node, PTR_TYPE_INTERNAL);
+       return note_tree_insert(t, new_node, n + 1, entry, type, combine_notes);
+ }
  /* Free the entire notes data contained in the given tree */
  static void note_tree_free(struct int_node *tree)
  {
@@@ -445,8 -449,12 +452,12 @@@ static void load_subtree(struct notes_t
                                l->key_sha1[19] = (unsigned char) 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;
  
@@@ -804,16 -812,17 +815,17 @@@ int combine_notes_concatenate(unsigned 
                return 0;
        }
  
-       /* 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);
  
@@@ -836,6 -845,82 +848,82 @@@ int combine_notes_ignore(unsigned char 
        return 0;
  }
  
+ static int string_list_add_note_lines(struct string_list *sort_uniq_list,
+                                     const unsigned char *sha1)
+ {
+       char *data;
+       unsigned long len;
+       enum object_type t;
+       struct strbuf buf = STRBUF_INIT;
+       struct strbuf **lines = NULL;
+       int i, list_index;
+       if (is_null_sha1(sha1))
+               return 0;
+       /* read_sha1_file NUL-terminates */
+       data = read_sha1_file(sha1, &t, &len);
+       if (t != OBJ_BLOB || !data || !len) {
+               free(data);
+               return t != OBJ_BLOB || !data;
+       }
+       strbuf_attach(&buf, data, len, len + 1);
+       lines = strbuf_split(&buf, '\n');
+       for (i = 0; lines[i]; i++) {
+               if (lines[i]->buf[lines[i]->len - 1] == '\n')
+                       strbuf_setlen(lines[i], lines[i]->len - 1);
+               if (!lines[i]->len)
+                       continue; /* skip empty lines */
+               list_index = string_list_find_insert_index(sort_uniq_list,
+                                                          lines[i]->buf, 0);
+               if (list_index < 0)
+                       continue; /* skip duplicate lines */
+               string_list_insert_at_index(sort_uniq_list, list_index,
+                                           lines[i]->buf);
+       }
+       strbuf_list_free(lines);
+       strbuf_release(&buf);
+       return 0;
+ }
+ static int string_list_join_lines_helper(struct string_list_item *item,
+                                        void *cb_data)
+ {
+       struct strbuf *buf = cb_data;
+       strbuf_addstr(buf, item->string);
+       strbuf_addch(buf, '\n');
+       return 0;
+ }
+ int combine_notes_cat_sort_uniq(unsigned char *cur_sha1,
+               const unsigned char *new_sha1)
+ {
+       struct string_list sort_uniq_list = { NULL, 0, 0, 1 };
+       struct strbuf buf = STRBUF_INIT;
+       int ret = 1;
+       /* read both note blob objects into unique_lines */
+       if (string_list_add_note_lines(&sort_uniq_list, cur_sha1))
+               goto out;
+       if (string_list_add_note_lines(&sort_uniq_list, new_sha1))
+               goto out;
+       /* create a new blob object from sort_uniq_list */
+       if (for_each_string_list(&sort_uniq_list,
+                                string_list_join_lines_helper, &buf))
+               goto out;
+       ret = write_sha1_file(buf.buf, buf.len, blob_type, cur_sha1);
+ out:
+       strbuf_release(&buf);
+       string_list_clear(&sort_uniq_list, 0);
+       return ret;
+ }
  static int string_list_add_one_ref(const char *path, const unsigned char *sha1,
                                   int flag, void *cb)
  {
@@@ -880,6 -965,14 +968,6 @@@ void string_list_add_refs_from_colon_se
        strbuf_release(&globbuf);
  }
  
 -static int string_list_add_refs_from_list(struct string_list_item *item,
 -                                        void *cb)
 -{
 -      struct string_list *list = cb;
 -      string_list_add_refs_by_glob(list, item->string);
 -      return 0;
 -}
 -
  static int notes_display_config(const char *k, const char *v, void *cb)
  {
        int *load_refs = cb;
        return 0;
  }
  
static const char *default_notes_ref(void)
+ const char *default_notes_ref(void)
  {
        const char *notes_ref = NULL;
        if (!notes_ref)
@@@ -935,25 -1028,37 +1023,25 @@@ void init_notes(struct notes_tree *t, c
                return;
        if (get_tree_entry(object_sha1, "", sha1, &mode))
                die("Failed to read notes tree referenced by %s (%s)",
-                   notes_ref, object_sha1);
+                   notes_ref, sha1_to_hex(object_sha1));
  
        hashclr(root_tree.key_sha1);
        hashcpy(root_tree.val_sha1, sha1);
        load_subtree(t, &root_tree, t->root, 0);
  }
  
 -struct load_notes_cb_data {
 -      int counter;
 -      struct notes_tree **trees;
 -};
 -
 -static int load_one_display_note_ref(struct string_list_item *item,
 -                                   void *cb_data)
 -{
 -      struct load_notes_cb_data *c = cb_data;
 -      struct notes_tree *t = xcalloc(1, sizeof(struct notes_tree));
 -      init_notes(t, item->string, combine_notes_ignore, 0);
 -      c->trees[c->counter++] = t;
 -      return 0;
 -}
 -
  struct notes_tree **load_notes_trees(struct string_list *refs)
  {
 +      struct string_list_item *item;
 +      int counter = 0;
        struct notes_tree **trees;
 -      struct load_notes_cb_data cb_data;
        trees = xmalloc((refs->nr+1) * sizeof(struct notes_tree *));
 -      cb_data.counter = 0;
 -      cb_data.trees = trees;
 -      for_each_string_list(refs, load_one_display_note_ref, &cb_data);
 -      trees[cb_data.counter] = NULL;
 +      for_each_string_list_item(item, refs) {
 +              struct notes_tree *t = xcalloc(1, sizeof(struct notes_tree));
 +              init_notes(t, item->string, combine_notes_ignore, 0);
 +              trees[counter++] = t;
 +      }
 +      trees[counter] = NULL;
        return trees;
  }
  
@@@ -978,18 -1083,16 +1066,18 @@@ void init_display_notes(struct display_
  
        git_config(notes_display_config, &load_config_refs);
  
 -      if (opt && opt->extra_notes_refs)
 -              for_each_string_list(opt->extra_notes_refs,
 -                                   string_list_add_refs_from_list,
 -                                   &display_notes_refs);
 +      if (opt && opt->extra_notes_refs) {
 +              struct string_list_item *item;
 +              for_each_string_list_item(item, opt->extra_notes_refs)
 +                      string_list_add_refs_by_glob(&display_notes_refs,
 +                                                   item->string);
 +      }
  
        display_notes_trees = load_notes_trees(&display_notes_refs);
        string_list_clear(&display_notes_refs, 0);
  }
  
void add_note(struct notes_tree *t, const unsigned char *object_sha1,
int add_note(struct notes_tree *t, const unsigned char *object_sha1,
                const unsigned char *note_sha1, combine_notes_fn combine_notes)
  {
        struct leaf_node *l;
        l = (struct leaf_node *) xmalloc(sizeof(struct leaf_node));
        hashcpy(l->key_sha1, object_sha1);
        hashcpy(l->val_sha1, note_sha1);
-       note_tree_insert(t, t->root, 0, l, PTR_TYPE_NOTE, combine_notes);
+       return note_tree_insert(t, t->root, 0, l, PTR_TYPE_NOTE, combine_notes);
  }
  
 -void remove_note(struct notes_tree *t, const unsigned char *object_sha1)
 +int remove_note(struct notes_tree *t, const unsigned char *object_sha1)
  {
        struct leaf_node l;
  
        if (!t)
                t = &default_notes_tree;
        assert(t->initialized);
 -      t->dirty = 1;
        hashcpy(l.key_sha1, object_sha1);
        hashclr(l.val_sha1);
        note_tree_remove(t, t->root, 0, &l);
 +      if (is_null_sha1(l.val_sha1)) // no note was removed
 +              return 1;
 +      t->dirty = 1;
 +      return 0;
  }
  
  const unsigned char *get_note(struct notes_tree *t,
@@@ -1182,7 -1282,7 +1270,7 @@@ void format_display_notes(const unsigne
  
  int copy_note(struct notes_tree *t,
              const unsigned char *from_obj, const unsigned char *to_obj,
-             int force, combine_notes_fn combine_fn)
+             int force, combine_notes_fn combine_notes)
  {
        const unsigned char *note = get_note(t, from_obj);
        const unsigned char *existing_note = get_note(t, to_obj);
                return 1;
  
        if (note)
-               add_note(t, to_obj, note, combine_fn);
+               return add_note(t, to_obj, note, combine_notes);
        else if (existing_note)
-               add_note(t, to_obj, null_sha1, combine_fn);
+               return add_note(t, to_obj, null_sha1, combine_notes);
  
        return 0;
  }
diff --combined notes.h
index 5106761534cfb6c5b97baede700e99b0aa5dfc3b,93a73657e249734dc7cd0fea779c12fc102f0557..83bd6e0ec02a1a8650004f14cd7476eedbdff518
+++ b/notes.h
   * resulting SHA1 into the first SHA1 argument (cur_sha1). A non-zero return
   * value indicates failure.
   *
-  * The two given SHA1s must both be non-NULL and different from each other.
+  * The two given SHA1s shall both be non-NULL and different from each other.
+  * Either of them (but not both) may be == null_sha1, which indicates an
+  * empty/non-existent note. If the resulting SHA1 (cur_sha1) is == null_sha1,
+  * the note will be removed from the notes tree.
   *
   * The default combine_notes function (you get this when passing NULL) is
   * combine_notes_concatenate(), which appends the contents of the new note to
@@@ -24,6 -27,7 +27,7 @@@ typedef int (*combine_notes_fn)(unsigne
  int combine_notes_concatenate(unsigned char *cur_sha1, const unsigned char *new_sha1);
  int combine_notes_overwrite(unsigned char *cur_sha1, const unsigned char *new_sha1);
  int combine_notes_ignore(unsigned char *cur_sha1, const unsigned char *new_sha1);
+ int combine_notes_cat_sort_uniq(unsigned char *cur_sha1, const unsigned char *new_sha1);
  
  /*
   * Notes tree object
@@@ -43,6 -47,20 +47,20 @@@ extern struct notes_tree 
        int dirty;
  } default_notes_tree;
  
+ /*
+  * Return the default notes ref.
+  *
+  * The default notes ref is the notes ref that is used when notes_ref == NULL
+  * is passed to init_notes().
+  *
+  * This the first of the following to be defined:
+  * 1. The '--ref' option to 'git notes', if given
+  * 2. The $GIT_NOTES_REF environment variable, if set
+  * 3. The value of the core.notesRef config variable, if set
+  * 4. GIT_NOTES_DEFAULT_REF (i.e. "refs/notes/commits")
+  */
+ const char *default_notes_ref(void);
  /*
   * Flags controlling behaviour of notes tree initialization
   *
@@@ -76,11 -94,24 +94,24 @@@ void init_notes(struct notes_tree *t, c
  /*
   * Add the given note object to the given notes_tree structure
   *
+  * If there already exists a note for the given object_sha1, the given
+  * combine_notes function is invoked to break the tie. If not given (i.e.
+  * combine_notes == NULL), the default combine_notes function for the given
+  * notes_tree is used.
+  *
+  * Passing note_sha1 == null_sha1 indicates the addition of an
+  * empty/non-existent note. This is a (potentially expensive) no-op unless
+  * there already exists a note for the given object_sha1, AND combining that
+  * note with the empty note (using the given combine_notes function) results
+  * in a new/changed note.
+  *
+  * Returns zero on success; non-zero means combine_notes failed.
+  *
   * IMPORTANT: The changes made by add_note() to the given notes_tree structure
   * are not persistent until a subsequent call to write_notes_tree() returns
   * zero.
   */
void add_note(struct notes_tree *t, const unsigned char *object_sha1,
int add_note(struct notes_tree *t, const unsigned char *object_sha1,
                const unsigned char *note_sha1, combine_notes_fn combine_notes);
  
  /*
   * IMPORTANT: The changes made by remove_note() to the given notes_tree
   * structure are not persistent until a subsequent call to write_notes_tree()
   * returns zero.
 + *
 + * Return 0 if a note was removed; 1 if there was no note to remove.
   */
 -void remove_note(struct notes_tree *t, const unsigned char *object_sha1);
 +int remove_note(struct notes_tree *t, const unsigned char *object_sha1);
  
  /*
   * Get the note object SHA1 containing the note data for the given object
@@@ -105,11 -134,18 +136,18 @@@ const unsigned char *get_note(struct no
  /*
   * Copy a note from one object to another in the given notes_tree.
   *
-  * Fails if the to_obj already has a note unless 'force' is true.
+  * Returns 1 if the to_obj already has a note and 'force' is false. Otherwise,
+  * returns non-zero if 'force' is true, but the given combine_notes function
+  * failed to combine from_obj's note with to_obj's existing note.
+  * Returns zero on success.
+  *
+  * IMPORTANT: The changes made by copy_note() to the given notes_tree structure
+  * are not persistent until a subsequent call to write_notes_tree() returns
+  * zero.
   */
  int copy_note(struct notes_tree *t,
              const unsigned char *from_obj, const unsigned char *to_obj,
-             int force, combine_notes_fn combine_fn);
+             int force, combine_notes_fn combine_notes);
  
  /*
   * Flags controlling behaviour of for_each_note()
   * notes tree) from within the callback:
   * - add_note()
   * - remove_note()
+  * - copy_note()
   * - free_notes()
   */
  typedef int each_note_fn(const unsigned char *object_sha1,
diff --combined t/t3301-notes.sh
index 7e84ab97903a890a7d9fabc304cad7531e9b915b,f5b72c7b002de66579ab7c8fc9bac38dd22b10bf..dc2e04a0161c8d6b3f6eefc1873852a377a26393
@@@ -52,7 -52,7 +52,7 @@@ test_expect_success 'refusing to edit n
  
  # 1 indicates caught gracefully by die, 128 means git-show barked
  test_expect_success 'handle empty notes gracefully' '
 -      git notes show ; test 1 = $?
 +      test_expect_code 1 git notes show
  '
  
  test_expect_success 'show non-existent notes entry with %N' '
@@@ -299,7 -299,7 +299,7 @@@ cat expect-F >> expect-rm-
  test_expect_success 'verify note removal with -F /dev/null' '
        git log -4 > output &&
        test_cmp expect-rm-F output &&
 -      ! git notes show
 +      test_must_fail git notes show
  '
  
  test_expect_success 'do not create empty note with -m "" (setup)' '
  test_expect_success 'verify non-creation of note with -m ""' '
        git log -4 > output &&
        test_cmp expect-rm-F output &&
 -      ! git notes show
 +      test_must_fail git notes show
  '
  
  cat > expect-combine_m_and_F << EOF
@@@ -357,7 -357,7 +357,7 @@@ cat expect-multiline >> expect-rm-remov
  test_expect_success 'verify note removal with "git notes remove"' '
        git log -4 > output &&
        test_cmp expect-rm-remove output &&
 -      ! git notes show HEAD^
 +      test_must_fail git notes show HEAD^
  '
  
  cat > expect << EOF
@@@ -365,13 -365,6 +365,13 @@@ c18dc024e14f08d18d14eea0d747ff692d66d6a
  c9c6af7f78bc47490dbf3e822cf2f3c24d4b9061 268048bfb8a1fb38e703baceb8ab235421bf80c5
  EOF
  
 +test_expect_success 'removing non-existing note should not create new commit' '
 +      git rev-parse --verify refs/notes/commits > before_commit &&
 +      test_must_fail git notes remove HEAD^ &&
 +      git rev-parse --verify refs/notes/commits > after_commit &&
 +      test_cmp before_commit after_commit
 +'
 +
  test_expect_success 'list notes with "git notes list"' '
        git notes list > output &&
        test_cmp expect output
@@@ -627,16 -620,16 +627,16 @@@ test_expect_success '--show-notes=ref a
  
  test_expect_success 'Allow notes on non-commits (trees, blobs, tags)' '
        git config core.notesRef refs/notes/other &&
 -      echo "Note on a tree" > expect
 +      echo "Note on a tree" > expect &&
        git notes add -m "Note on a tree" HEAD: &&
        git notes show HEAD: > actual &&
        test_cmp expect actual &&
 -      echo "Note on a blob" > expect
 +      echo "Note on a blob" > expect &&
        filename=$(git ls-tree --name-only HEAD | head -n1) &&
        git notes add -m "Note on a blob" HEAD:$filename &&
        git notes show HEAD:$filename > actual &&
        test_cmp expect actual &&
 -      echo "Note on a tag" > expect
 +      echo "Note on a tag" > expect &&
        git tag -a -m "This is an annotated tag" foobar HEAD^ &&
        git notes add -m "Note on a tag" foobar &&
        git notes show foobar > actual &&
@@@ -962,6 -955,7 +962,7 @@@ Date:   Thu Apr 7 15:27:13 2005 -070
  
  Notes (other):
      a fresh note
+ $whitespace
      another fresh note
  EOF
  
@@@ -983,8 -977,11 +984,11 @@@ Date:   Thu Apr 7 15:27:13 2005 -070
  
  Notes (other):
      a fresh note
+ $whitespace
      another fresh note
+ $whitespace
      append 1
+ $whitespace
      append 2
  EOF
  
@@@ -1061,4 -1058,23 +1065,23 @@@ test_expect_success 'git notes copy dia
        test_must_fail git notes copy one two three
  '
  
+ test_expect_success 'git notes get-ref (no overrides)' '
+       git config --unset core.notesRef &&
+       unset GIT_NOTES_REF &&
+       test "$(git notes get-ref)" = "refs/notes/commits"
+ '
+ test_expect_success 'git notes get-ref (core.notesRef)' '
+       git config core.notesRef refs/notes/foo &&
+       test "$(git notes get-ref)" = "refs/notes/foo"
+ '
+ test_expect_success 'git notes get-ref (GIT_NOTES_REF)' '
+       test "$(GIT_NOTES_REF=refs/notes/bar git notes get-ref)" = "refs/notes/bar"
+ '
+ test_expect_success 'git notes get-ref (--ref)' '
+       test "$(GIT_NOTES_REF=refs/notes/bar git notes --ref=baz get-ref)" = "refs/notes/baz"
+ '
  test_done
index 5cb7e70d54267b53ffb4b0f4c46875308d9de5a6,9ed70dc7eb23c66493b4e6b6cf11869c8182b3a7..d3a3bd2679e061ce391930d2b59831aa7610926f
@@@ -7,39 -7,34 +7,39 @@@ test_description='git rebase interactiv
  
  This test runs git rebase "interactively", by faking an edit, and verifies
  that the result still makes sense.
 +
 +Initial setup:
 +
 +     one - two - three - four (conflict-branch)
 +   /
 + A - B - C - D - E            (master)
 + | \
 + |   F - G - H                (branch1)
 + |     \
 + |\      I                    (branch2)
 + | \
 + |   J - K - L - M            (no-conflict-branch)
 +  \
 +    N - O - P                 (no-ff-branch)
 +
 + where A, B, D and G all touch file1, and one, two, three, four all
 + touch file "conflict".
  '
  . ./test-lib.sh
  
  . "$TEST_DIRECTORY"/lib-rebase.sh
  
 +test_cmp_rev () {
 +      git rev-parse --verify "$1" >expect.rev &&
 +      git rev-parse --verify "$2" >actual.rev &&
 +      test_cmp expect.rev actual.rev
 +}
 +
  set_fake_editor
  
 -# Set up the repository like this:
 -#
 -#     one - two - three - four (conflict-branch)
 -#   /
 -# A - B - C - D - E            (master)
 -# | \
 -# |   F - G - H                (branch1)
 -# |     \
 -# |\      I                    (branch2)
 -# | \
 -# |   J - K - L - M            (no-conflict-branch)
 -#  \
 -#    N - O - P                 (no-ff-branch)
 -#
 -# where A, B, D and G all touch file1, and one, two, three, four all
 -# touch file "conflict".
 -#
  # WARNING: Modifications to the initial repository can change the SHA ID used
  # in the expect2 file for the 'stop on conflicting pick' test.
  
 -
  test_expect_success 'setup' '
        test_commit A file1 &&
        test_commit B file1 &&
        test_commit G file1 &&
        test_commit H file5 &&
        git checkout -b branch2 F &&
 -      test_commit I file6
 +      test_commit I file6 &&
        git checkout -b conflict-branch A &&
 -      for n in one two three four
 -      do
 -              test_commit $n conflict
 -      done &&
 +      test_commit one conflict &&
 +      test_commit two conflict &&
 +      test_commit three conflict &&
 +      test_commit four conflict &&
        git checkout -b no-conflict-branch A &&
 -      for n in J K L M
 -      do
 -              test_commit $n file$n
 -      done &&
 +      test_commit J fileJ &&
 +      test_commit K fileK &&
 +      test_commit L fileL &&
 +      test_commit M fileM &&
        git checkout -b no-ff-branch A &&
 -      for n in N O P
 -      do
 -              test_commit $n file$n
 -      done
 +      test_commit N fileN &&
 +      test_commit O fileO &&
 +      test_commit P fileP
 +'
 +
 +# "exec" commands are ran with the user shell by default, but this may
 +# be non-POSIX. For example, if SHELL=zsh then ">file" doesn't work
 +# to create a file. Unseting SHELL avoids such non-portable behavior
 +# in tests.
 +SHELL=
 +
 +test_expect_success 'rebase -i with the exec command' '
 +      git checkout master &&
 +      (
 +      FAKE_LINES="1 exec_>touch-one
 +              2 exec_>touch-two exec_false exec_>touch-three
 +              3 4 exec_>\"touch-file__name_with_spaces\";_>touch-after-semicolon 5" &&
 +      export FAKE_LINES &&
 +      test_must_fail git rebase -i A
 +      ) &&
 +      test_path_is_file touch-one &&
 +      test_path_is_file touch-two &&
 +      test_path_is_missing touch-three " (should have stopped before)" &&
 +      test_cmp_rev C HEAD &&
 +      git rebase --continue &&
 +      test_path_is_file touch-three &&
 +      test_path_is_file "touch-file  name with spaces" &&
 +      test_path_is_file touch-after-semicolon &&
 +      test_cmp_rev master HEAD &&
 +      rm -f touch-*
 +'
 +
 +test_expect_success 'rebase -i with the exec command runs from tree root' '
 +      git checkout master &&
 +      mkdir subdir && (cd subdir &&
 +      FAKE_LINES="1 exec_>touch-subdir" \
 +              git rebase -i HEAD^
 +      ) &&
 +      test_path_is_file touch-subdir &&
 +      rm -fr subdir
 +'
 +
 +test_expect_success 'rebase -i with the exec command checks tree cleanness' '
 +      git checkout master &&
 +      (
 +      FAKE_LINES="exec_echo_foo_>file1 1" &&
 +      export FAKE_LINES &&
 +      test_must_fail git rebase -i HEAD^
 +      ) &&
 +      test_cmp_rev master^ HEAD &&
 +      git reset --hard &&
 +      git rebase --continue
  '
  
  test_expect_success 'no changes are a nop' '
@@@ -196,17 -143,16 +196,17 @@@ test_expect_success 'abort' 
        git rebase --abort &&
        test $(git rev-parse new-branch1) = $(git rev-parse HEAD) &&
        test "$(git symbolic-ref -q HEAD)" = "refs/heads/branch1" &&
 -      ! test -d .git/rebase-merge
 +      test_path_is_missing .git/rebase-merge
  '
  
  test_expect_success 'abort with error when new base cannot be checked out' '
        git rm --cached file1 &&
        git commit -m "remove file in base" &&
        test_must_fail git rebase -i master > output 2>&1 &&
 -      grep "Untracked working tree file .file1. would be overwritten" \
 +      grep "The following untracked working tree files would be overwritten by checkout:" \
                output &&
 -      ! test -d .git/rebase-merge &&
 +      grep "file1" output &&
 +      test_path_is_missing .git/rebase-merge &&
        git reset --hard HEAD^
  '
  
@@@ -576,7 -522,7 +576,7 @@@ test_expect_success 'do "noop" when the
  
        git checkout -b branch4 HEAD &&
        GIT_EDITOR=: git commit --amend \
 -              --author="Somebody else <somebody@else.com>" 
 +              --author="Somebody else <somebody@else.com>" &&
        test $(git rev-parse branch3) != $(git rev-parse branch4) &&
        git rebase -i branch3 &&
        test $(git rev-parse branch3) = $(git rev-parse branch4)
@@@ -591,7 -537,7 +591,7 @@@ test_expect_success 'submodule rebase s
                git add elif && git commit -m "submodule initial"
        ) &&
        echo 1 >file1 &&
 -      git add file1 sub
 +      git add file1 sub &&
        test_tick &&
        git commit -m "One" &&
        echo 2 >file1 &&
@@@ -647,6 -593,7 +647,7 @@@ test_expect_success 'rebase -i can cop
  
  cat >expect <<EOF
  an earlier note
  a note
  EOF