]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Merge branch 'ld/p4-changes-block-size'
authorJunio C Hamano <gitster@pobox.com>
Wed, 24 Jun 2015 19:21:56 +0000 (12:21 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 24 Jun 2015 19:21:57 +0000 (12:21 -0700)
More Perforce row number limit workaround for "git p4".

* ld/p4-changes-block-size:
  git-p4: fixing --changes-block-size handling
  git-p4: add tests for non-numeric revision range
  git-p4: test with limited p4 server results
  git-p4: additional testing of --changes-block-size

83 files changed:
Documentation/RelNotes/2.4.3.txt [new file with mode: 0644]
Documentation/RelNotes/2.4.4.txt [new file with mode: 0644]
Documentation/RelNotes/2.5.0.txt
Documentation/config.txt
Documentation/diff-options.txt
Documentation/fmt-merge-msg-config.txt [new file with mode: 0644]
Documentation/git-am.txt
Documentation/git-blame.txt
Documentation/git-fmt-merge-msg.txt
Documentation/git-format-patch.txt
Documentation/git-pull.txt
Documentation/git-send-email.txt
Documentation/git-submodule.txt
Documentation/git.txt
Documentation/gitattributes.txt
Documentation/glossary-content.txt
Documentation/merge-config.txt
Documentation/technical/pack-protocol.txt
Makefile
builtin/apply.c
builtin/blame.c
builtin/clone.c
builtin/for-each-ref.c
builtin/fsck.c
builtin/index-pack.c
builtin/log.c
cache.h
combine-diff.c
commit.c
commit.h
config.c
config.mak.uname
configure.ac
contrib/completion/git-completion.bash
contrib/completion/git-completion.tcsh
contrib/hooks/multimail/CHANGES
contrib/hooks/multimail/README
contrib/hooks/multimail/README.Git
contrib/hooks/multimail/git_multimail.py
contrib/hooks/multimail/migrate-mailhook-config
contrib/hooks/multimail/post-receive.example [moved from contrib/hooks/multimail/post-receive with 74% similarity]
diff.c
diff.h
ewah/bitmap.c
ewah/ewah_bitmap.c
ewah/ewok.h
git-am.sh
git-compat-util.h
git-p4.py
git-pull.sh
git-send-email.perl
help.c
line-log.c
list-objects.c
ll-merge.c
lockfile.c
pack-bitmap.c
po/de.po
read-cache.c
refs.c
revision.c
setup.c
sha1_file.c
t/t1400-update-ref.sh
t/t1510-repo-setup.sh
t/t4014-format-patch.sh
t/t4015-diff-whitespace.sh
t/t4136-apply-check.sh
t/t4150-am.sh
t/t4151-am-abort.sh
t/t5520-pull.sh
t/t5521-pull-options.sh
t/t6026-merge-attr.sh
t/t6301-for-each-ref-errors.sh [new file with mode: 0755]
t/t6501-freshen-objects.sh
t/t7502-commit.sh
t/t8002-blame.sh
t/t9001-send-email.sh
tree.c
tree.h
utf8.h
wrapper.c
wt-status.c

diff --git a/Documentation/RelNotes/2.4.3.txt b/Documentation/RelNotes/2.4.3.txt
new file mode 100644 (file)
index 0000000..914d2c1
--- /dev/null
@@ -0,0 +1,76 @@
+Git v2.4.3 Release Notes
+========================
+
+Fixes since v2.4.3
+------------------
+
+ * Error messages from "git branch" called remote-tracking branches as
+   "remote branches".
+
+ * "git rerere forget" in a repository without rerere enabled gave a
+   cryptic error message; it should be a silent no-op instead.
+
+ * "git pull --log" and "git pull --no-log" worked as expected, but
+   "git pull --log=20" did not.
+
+ * The pull.ff configuration was supposed to override the merge.ff
+   configuration, but it didn't.
+
+ * The code to read pack-bitmap wanted to allocate a few hundred
+   pointers to a structure, but by mistake allocated and leaked memory
+   enough to hold that many actual structures.  Correct the allocation
+   size and also have it on stack, as it is small enough.
+
+ * Various documentation mark-up fixes to make the output more
+   consistent in general and also make AsciiDoctor (an alternative
+   formatter) happier.
+
+ * "git bundle verify" did not diagnose extra parameters on the
+   command line.
+
+ * Multi-ref transaction support we merged a few releases ago
+   unnecessarily kept many file descriptors open, risking to fail with
+   resource exhaustion.
+
+ * The ref API did not handle cases where 'refs/heads/xyzzy/frotz' is
+   removed at the same time as 'refs/heads/xyzzy' is added (or vice
+   versa) very well.
+
+ * The "log --decorate" enhancement in Git 2.4 that shows the commit
+   at the tip of the current branch e.g. "HEAD -> master", did not
+   work with --decorate=full.
+
+ * There was a commented-out (instead of being marked to expect
+   failure) test that documented a breakage that was fixed since the
+   test was written; turn it into a proper test.
+
+ * core.excludesfile (defaulting to $XDG_HOME/git/ignore) is supposed
+   to be overridden by repository-specific .git/info/exclude file, but
+   the order was swapped from the beginning. This belatedly fixes it.
+
+ * The connection initiation code for "ssh" transport tried to absorb
+   differences between the stock "ssh" and Putty-supplied "plink" and
+   its derivatives, but the logic to tell that we are using "plink"
+   variants were too loose and falsely triggered when "plink" appeared
+   anywhere in the path (e.g. "/home/me/bin/uplink/ssh").
+
+ * "git rebase -i" moved the "current" command from "todo" to "done" a
+   bit too prematurely, losing a step when a "pick" did not even start.
+
+ * "git add -e" did not allow the user to abort the operation by
+   killing the editor.
+
+ * Git 2.4 broke setting verbosity and progress levels on "git clone"
+   with native transports.
+
+ * Some time ago, "git blame" (incorrectly) lost the convert_to_git()
+   call when synthesizing a fake "tip" commit that represents the
+   state in the working tree, which broke folks who record the history
+   with LF line ending to make their project portabile across
+   platforms while terminating lines in their working tree files with
+   CRLF for their platform.
+
+ * Code clean-up for xdg configuration path support.
+
+Also contains typofixes, documentation updates and trivial code
+clean-ups.
diff --git a/Documentation/RelNotes/2.4.4.txt b/Documentation/RelNotes/2.4.4.txt
new file mode 100644 (file)
index 0000000..f1ccd00
--- /dev/null
@@ -0,0 +1,35 @@
+Git v2.4.4 Release Notes
+========================
+
+Fixes since v2.4.3
+------------------
+
+ * l10n updates for German.
+
+ * An earlier leakfix to bitmap testing code was incomplete.
+
+ * "git clean pathspec..." tried to lstat(2) and complain even for
+   paths outside the given pathspec.
+
+ * Communication between the HTTP server and http_backend process can
+   lead to a dead-lock when relaying a large ref negotiation request.
+   Diagnose the situation better, and mitigate it by reading such a
+   request first into core (to a reasonable limit).
+
+ * The clean/smudge interface did not work well when filtering an
+   empty contents (failed and then passed the empty input through).
+   It can be argued that a filter that produces anything but empty for
+   an empty input is nonsense, but if the user wants to do strange
+   things, then why not?
+
+ * Make "git stash something --help" error out, so that users can
+   safely say "git stash drop --help".
+
+ * Clarify that "log --raw" and "log --format=raw" are unrelated
+   concepts.
+
+ * Catch a programmer mistake to feed a pointer not an array to
+   ARRAY_SIZE() macro, by using a couple of GCC extensions.
+
+Also contains typofixes, documentation updates and trivial code
+clean-ups.
index 0b7fe2af33df36c1d5341912ee5f6ba480b8bcd0..e39f327341cd7e28f609735c53dbdcf457f73ceb 100644 (file)
@@ -9,6 +9,13 @@ Ports
 
 UI, Workflows & Features
 
+ * The bash completion script (in contrib/) learned a few options that
+   "git revert" takes.
+
+ * Whitespace breakages in deleted and context lines can also be
+   painted in the output of "git diff" and friends with the new
+   --ws-error-highlight option.
+
  * List of commands shown by "git help" are grouped along the workflow
    elements to help early learners.
 
@@ -21,6 +28,17 @@ UI, Workflows & Features
    chunks from Perforce, instead of making one call to "p4 changes"
    that may trigger "too many rows scanned" error from Perforce.
 
+ * Unlike "$EDITOR" and "$GIT_EDITOR" that can hold the path to the
+   command and initial options (e.g. "/path/to/emacs -nw"), 'git p4'
+   did not let the shell interpolate the contents of the environment
+   variable that name the editor "$P4EDITOR" (and "$EDITOR", too).
+   This release makes it in line with the rest of Git, as well as with
+   Perforce.
+
+ * A new short-hand <branch>@{push} denotes the remote-tracking branch
+   that tracks the branch at the remote the <branch> would be pushed
+   to.
+
  * "git show-branch --topics HEAD" (with no other arguments) did not
    do anything interesting.  Instead, contrast the given revision
    against all the local branches by default.
@@ -102,6 +120,9 @@ UI, Workflows & Features
    behaves as if HEAD:Documentation/RelNotes/2.5.0.txt was given as
    input instead.
 
+ * "git send-email" learned the alias file format used by the sendmail
+   program (in an abbreviated form).
+
 
 Performance, Internal Implementation, Development Support etc.
 
@@ -150,6 +171,14 @@ Performance, Internal Implementation, Development Support etc.
    the semantics of the option changed back in Git 1.9 days.
    (merge 19d122b pt/pull-tags-error-diag later to maint).
 
+ * for_each_ref() callback functions were taught to name the objects
+   not with "unsigned char sha1[20]" but with "struct object_id".
+
+ * Error reporting mechanism used in "refs" API has been made more
+   consistent.
+
+ * "git pull" has more test coverage now.
+
 
 Also contains various documentation updates and code clean-ups.
 
@@ -171,7 +200,7 @@ notes for details).
 
  * Memory usage of "git index-pack" has been trimmed by tens of
    per-cent.
-   (merge c6458e6 nd/slim-index-pack-memory-usage later to maint).
+   (merge a78c5b3 nd/slim-index-pack-memory-usage later to maint).
 
  * "git rev-list --objects $old --not --all" to see if everything that
    is reachable from $old is already connected to the existing refs
@@ -376,6 +405,23 @@ notes for details).
    paths outside the given pathspec.
    (merge 838d6a9 dt/clean-pathspec-filter-then-lstat later to maint).
 
+ * Recent "git prune" traverses young unreachable objects to safekeep
+   old objects in the reachability chain from them, which sometimes
+   caused error messages that are unnecessarily alarming.
+   (merge ce4e7b2 jk/squelch-missing-link-warning-for-unreachable later to maint).
+
+ * The configuration reader/writer uses mmap(2) interface to access
+   the files; when we find a directory, it barfed with "Out of memory?".
+   (merge 9ca0aaf jk/diagnose-config-mmap-failure later to maint).
+
+ * "color.diff.plain" was a misnomer; give it 'color.diff.context' as
+   a more logical synonym.
+   (merge 8dbf3eb jk/color-diff-plain-is-context later to maint).
+
+ * The setup code used to die when core.bare and core.worktree are set
+   inconsistently, even for commands that do not need working tree.
+   (merge fada767 jk/die-on-bogus-worktree-late later to maint).
+
  * Code cleanups and documentation updates.
    (merge 0269f96 mm/usage-log-l-can-take-regex later to maint).
    (merge 64f2589 nd/t1509-chroot-test later to maint).
@@ -391,3 +437,9 @@ notes for details).
    (merge 22570b6 rs/janitorial later to maint).
    (merge 5c2a581 mc/commit-doc-grammofix later to maint).
    (merge ce41720 ah/usage-strings later to maint).
+   (merge e6a268c sb/glossary-submodule later to maint).
+   (merge ec48a76 sb/submodule-doc-intro later to maint).
+   (merge 14f8b9b jk/clone-dissociate later to maint).
+   (merge 055c7e9 sb/pack-protocol-mention-smart-http later to maint).
+   (merge 7c37a5d jk/make-fix-dependencies later to maint).
+   (merge fc0aa39 sg/merge-summary-config later to maint).
index 4d21ce1647f6125584872c8c67823ae8368b1132..3e37b93ed2ac3edfe478952ef445678ae51a5d47 100644 (file)
@@ -769,6 +769,14 @@ am.keepcr::
        by giving '--no-keep-cr' from the command line.
        See linkgit:git-am[1], linkgit:git-mailsplit[1].
 
+am.threeWay::
+       By default, `git am` will fail if the patch does not apply cleanly. When
+       set to true, this setting tells `git am` to fall back on 3-way merge if
+       the patch records the identity of blobs it is supposed to apply to and
+       we have those blobs available locally (equivalent to giving the `--3way`
+       option from the command line). Defaults to `false`.
+       See linkgit:git-am[1].
+
 apply.ignoreWhitespace::
        When set to 'change', tells 'git apply' to ignore changes in
        whitespace, in the same way as the '--ignore-space-change'
@@ -914,7 +922,8 @@ command line with the `--color[=<when>]` option.
 color.diff.<slot>::
        Use customized color for diff colorization.  `<slot>` specifies
        which part of the patch to use the specified color, and is one
-       of `plain` (context text), `meta` (metainformation), `frag`
+       of `context` (context text - `plain` is a historical synonym),
+       `meta` (metainformation), `frag`
        (hunk header), 'func' (function in hunk header), `old` (removed lines),
        `new` (added lines), `commit` (commit headers), or `whitespace`
        (highlighting whitespace errors).
index 3ad6404dbcf2915721ac963085bbc0269c1d7312..d56ca90998a740dbe4cc5c242326023b4cc769b3 100644 (file)
@@ -291,6 +291,16 @@ ifndef::git-format-patch[]
        initial indent of the line are considered whitespace errors.
        Exits with non-zero status if problems are found. Not compatible
        with --exit-code.
+
+--ws-error-highlight=<kind>::
+       Highlight whitespace errors on lines specified by <kind>
+       in the color specified by `color.diff.whitespace`.  <kind>
+       is a comma separated list of `old`, `new`, `context`.  When
+       this option is not given, only whitespace errors in `new`
+       lines are highlighted.  E.g. `--ws-error-highlight=new,old`
+       highlights whitespace errors on both deleted and added lines.
+       `all` can be used as a short-hand for `old,new,context`.
+
 endif::git-format-patch[]
 
 --full-index::
diff --git a/Documentation/fmt-merge-msg-config.txt b/Documentation/fmt-merge-msg-config.txt
new file mode 100644 (file)
index 0000000..c73cfa9
--- /dev/null
@@ -0,0 +1,10 @@
+merge.branchdesc::
+       In addition to branch names, populate the log message with
+       the branch description text associated with them.  Defaults
+       to false.
+
+merge.log::
+       In addition to branch names, populate the log message with at
+       most the specified number of one-line descriptions from the
+       actual commits that are being merged.  Defaults to false, and
+       true is a synonym for 20.
index 0d8ba48f792ae82237a56aa0dd2aca03f4e16020..dbea6e7ae9131a1e5c51dec460d58cc6deda701b 100644 (file)
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [verse]
 'git am' [--signoff] [--keep] [--[no-]keep-cr] [--[no-]utf8]
-        [--3way] [--interactive] [--committer-date-is-author-date]
+        [--[no-]3way] [--interactive] [--committer-date-is-author-date]
         [--ignore-date] [--ignore-space-change | --ignore-whitespace]
         [--whitespace=<option>] [-C<n>] [-p<n>] [--directory=<dir>]
         [--exclude=<path>] [--include=<path>] [--reject] [-q | --quiet]
@@ -90,10 +90,13 @@ default.   You can use `--no-utf8` to override this.
 
 -3::
 --3way::
+--no-3way::
        When the patch does not apply cleanly, fall back on
        3-way merge if the patch records the identity of blobs
        it is supposed to apply to and we have those blobs
-       available locally.
+       available locally. `--no-3way` can be used to override
+       am.threeWay configuration variable. For more information,
+       see am.threeWay in linkgit:git-config[1].
 
 --ignore-space-change::
 --ignore-whitespace::
index 9f23a861ce3a8dfd009532b469290cb6470b500d..e6e947c808d5e419a9d974d63ab7b95195b667c8 100644 (file)
@@ -76,6 +76,8 @@ include::blame-options.txt[]
 -e::
 --show-email::
        Show the author email instead of author name (Default: off).
+       This can also be controlled via the `blame.showEmail` config
+       option.
 
 -w::
        Ignore whitespace when comparing the parent's version and
index bb1232a52c4f0e4995f1fd38737461165772c239..55a9a4b93a2cba2d1d760b3529f1604b61359004 100644 (file)
@@ -51,17 +51,7 @@ OPTIONS
 
 CONFIGURATION
 -------------
-
-merge.branchdesc::
-       In addition to branch names, populate the log message with
-       the branch description text associated with them.  Defaults
-       to false.
-
-merge.log::
-       In addition to branch names, populate the log message with at
-       most the specified number of one-line descriptions from the
-       actual commits that are being merged.  Defaults to false, and
-       true is a synonym for 20.
+include::fmt-merge-msg-config.txt[]
 
 merge.summary::
        Synonym to `merge.log`; this is deprecated and will be removed in
index bb3ea9372f873ee50c3c6fb4092cd95ebbba3312..0dac4e9b86150ef2546b9fca02e211db6e667dcc 100644 (file)
@@ -170,7 +170,7 @@ will want to ensure that threading is disabled for `git send-email`.
 -v <n>::
 --reroll-count=<n>::
        Mark the series as the <n>-th iteration of the topic. The
-       output filenames have `v<n>` pretended to them, and the
+       output filenames have `v<n>` prepended to them, and the
        subject prefix ("PATCH" by default, but configurable via the
        `--subject-prefix` option) has ` v<n>` appended to it.  E.g.
        `--reroll-count=4` may produce `v4-0001-add-makefile.patch`
index 712ab4baed2c6802ddd96681f11c7f05a436f48b..93c72a29cecb7ce005dc579e79c5e5bacaaac420 100644 (file)
@@ -74,9 +74,6 @@ pulling or stash them away with linkgit:git-stash[1].
 OPTIONS
 -------
 
-Options meant for 'git pull' itself and the underlying 'git merge'
-must be given before the options meant for 'git fetch'.
-
 -q::
 --quiet::
        This is passed to both underlying git-fetch to squelch reporting of
index 804554609def705bee51e9be087623233c857935..7ae467ba415e5cb4413d0246883b8a620b8960e3 100644 (file)
@@ -383,7 +383,24 @@ sendemail.aliasesFile::
 
 sendemail.aliasFileType::
        Format of the file(s) specified in sendemail.aliasesFile. Must be
-       one of 'mutt', 'mailrc', 'pine', 'elm', or 'gnus'.
+       one of 'mutt', 'mailrc', 'pine', 'elm', or 'gnus', or 'sendmail'.
++
+What an alias file in each format looks like can be found in
+the documentation of the email program of the same name. The
+differences and limitations from the standard formats are
+described below:
++
+--
+sendmail;;
+*      Quoted aliases and quoted addresses are not supported: lines that
+       contain a `"` symbol are ignored.
+*      Redirection to a file (`/path/name`) or pipe (`|command`) is not
+       supported.
+*      File inclusion (`:include: /path/name`) is not supported.
+*      Warnings are printed on the standard error output for any
+       explicitly unsupported constructs, and any other lines that are not
+       recognized by the parser.
+--
 
 sendemail.multiEdit::
        If true (default), a single editor instance will be spawned to edit
index 2c25916f8f7cd0a7ca69c3b980d85251a29b681b..f17687e09d322f909ae4236c297e769d2e949b90 100644 (file)
@@ -25,22 +25,17 @@ SYNOPSIS
 
 DESCRIPTION
 -----------
-Submodules allow foreign repositories to be embedded within
-a dedicated subdirectory of the source tree, always pointed
-at a particular commit.
+Inspects, updates and manages submodules.
 
-They are not to be confused with remotes, which are meant mainly
-for branches of the same project; submodules are meant for
-different projects you would like to make part of your source tree,
-while the history of the two projects still stays completely
-independent and you cannot modify the contents of the submodule
-from within the main project.
-If you want to merge the project histories and want to treat the
-aggregated whole as a single project from then on, you may want to
-add a remote for the other project and use the 'subtree' merge strategy,
-instead of treating the other project as a submodule. Directories
-that come from both projects can be cloned and checked out as a whole
-if you choose to go that route.
+A submodule allows you to keep another Git repository in a subdirectory
+of your repository. The other repository has its own history, which does not
+interfere with the history of the current repository. This can be used to
+have external dependencies such as third party libraries for example.
+
+When cloning or pulling a repository containing submodules however,
+these will not be checked out by default; the 'init' and 'update'
+subcommands will maintain submodules checked out and at
+appropriate revision in your working tree.
 
 Submodules are composed from a so-called `gitlink` tree entry
 in the main repository that refers to a particular commit object
@@ -51,19 +46,18 @@ describes the default URL the submodule shall be cloned from.
 The logical name can be used for overriding this URL within your
 local repository configuration (see 'submodule init').
 
-This command will manage the tree entries and contents of the
-gitmodules file for you, as well as inspect the status of your
-submodules and update them.
-When adding a new submodule to the tree, the 'add' subcommand
-is to be used.  However, when pulling a tree containing submodules,
-these will not be checked out by default;
-the 'init' and 'update' subcommands will maintain submodules
-checked out and at appropriate revision in your working tree.
-You can briefly inspect the up-to-date status of your submodules
-using the 'status' subcommand and get a detailed overview of the
-difference between the index and checkouts using the 'summary'
-subcommand.
-
+Submodules are not to be confused with remotes, which are other
+repositories of the same project; submodules are meant for
+different projects you would like to make part of your source tree,
+while the history of the two projects still stays completely
+independent and you cannot modify the contents of the submodule
+from within the main project.
+If you want to merge the project histories and want to treat the
+aggregated whole as a single project from then on, you may want to
+add a remote for the other project and use the 'subtree' merge strategy,
+instead of treating the other project as a submodule. Directories
+that come from both projects can be cloned and checked out as a whole
+if you choose to go that route.
 
 COMMANDS
 --------
index ccc12b280652609bfd5eb089e194a9219b7fc9f2..dfbc2290c01dbeac8f6c290c66236baf9e3b3c44 100644 (file)
@@ -43,9 +43,11 @@ unreleased) version of Git, that is available from the 'master'
 branch of the `git.git` repository.
 Documentation for older releases are available here:
 
-* link:v2.4.2/git.html[documentation for release 2.4.2]
+* link:v2.4.4/git.html[documentation for release 2.4.4]
 
 * release notes for
+  link:RelNotes/2.4.4.txt[2.4.4],
+  link:RelNotes/2.4.3.txt[2.4.3],
   link:RelNotes/2.4.2.txt[2.4.2],
   link:RelNotes/2.4.1.txt[2.4.1],
   link:RelNotes/2.4.0.txt[2.4].
index 70899b302365f1f441422778d7683d277924279f..81fe586948582fae4945029a5cd49e61b09fac5b 100644 (file)
@@ -774,7 +774,7 @@ To define a custom merge driver `filfre`, add a section to your
 ----------------------------------------------------------------
 [merge "filfre"]
        name = feel-free merge driver
-       driver = filfre %O %A %B
+       driver = filfre %O %A %B %L %P
        recursive = binary
 ----------------------------------------------------------------
 
@@ -800,6 +800,9 @@ merge between common ancestors, when there are more than one.
 When left unspecified, the driver itself is used for both
 internal merge and the final merge.
 
+The merge driver can learn the pathname in which the merged result
+will be stored via placeholder `%P`.
+
 
 `conflict-marker-size`
 ^^^^^^^^^^^^^^^^^^^^^^
index bf383c2e8ce47da8f4da21e9852011520df9fce6..ab18f4baca4b49d362e9905990e034c4cf0a4e9e 100644 (file)
@@ -469,6 +469,11 @@ The most notable example is `HEAD`.
        <<def_push,push>> to describe the mapping between remote
        <<def_ref,ref>> and local ref.
 
+[[def_remote]]remote repository::
+       A <<def_repository,repository>> which is used to track the same
+       project but resides somewhere else. To communicate with remotes,
+       see <<def_fetch,fetch>> or <<def_push,push>>.
+
 [[def_remote_tracking_branch]]remote-tracking branch::
        A <<def_ref,ref>> that is used to follow changes from another
        <<def_repository,repository>>. It typically looks like
@@ -515,6 +520,17 @@ The most notable example is `HEAD`.
        is created by giving the `--depth` option to linkgit:git-clone[1], and
        its history can be later deepened with linkgit:git-fetch[1].
 
+[[def_submodule]]submodule::
+       A <<def_repository,repository>> that holds the history of a
+       separate project inside another repository (the latter of
+       which is called <<def_superproject, superproject>>).
+
+[[def_superproject]]superproject::
+       A <<def_repository,repository>> that references repositories
+       of other projects in its working tree as <<def_submodule,submodules>>.
+       The superproject knows about the names of (but does not hold
+       copies of) commit objects of the contained submodules.
+
 [[def_symref]]symref::
        Symbolic reference: instead of containing the <<def_SHA1,SHA-1>>
        id itself, it is of the format 'ref: refs/some/thing' and when
index 8a0e52f8ee64564ba1dcac63fc91cdef075996a7..002ca58c21e2f653ffd5651a8aedfe865400e166 100644 (file)
@@ -26,11 +26,7 @@ merge.ff::
        allowed (equivalent to giving the `--ff-only` option from the
        command line).
 
-merge.log::
-       In addition to branch names, populate the log message with at
-       most the specified number of one-line descriptions from the
-       actual commits that are being merged.  Defaults to false, and
-       true is a synonym for 20.
+include::fmt-merge-msg-config.txt[]
 
 merge.renameLimit::
        The number of files to consider when performing rename detection
index fc09c63b32e1776347f6b6c562739f9245593639..4064fc796fe36e58eb580de2c66a00196051ba5f 100644 (file)
@@ -1,11 +1,11 @@
 Packfile transfer protocols
 ===========================
 
-Git supports transferring data in packfiles over the ssh://, git:// and
+Git supports transferring data in packfiles over the ssh://, git://, http:// and
 file:// transports.  There exist two sets of protocols, one for pushing
 data from a client to a server and another for fetching data from a
-server to a client.  All three transports (ssh, git, file) use the same
-protocol to transfer data.
+server to a client.  The three transports (ssh, git, file) use the same
+protocol to transfer data. http is documented in http-protocol.txt.
 
 The processes invoked in the canonical Git implementation are 'upload-pack'
 on the server side and 'fetch-pack' on the client side for fetching data;
index 54ec511dffb0a7b6775f360fc10deaa2008fdee9..149f1c7c3740e9513cc2e35f590b987bbee6f0a4 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1747,7 +1747,7 @@ $(SCRIPT_PERL_GEN): perl/perl.mak
 perl/perl.mak: perl/PM.stamp
 
 perl/PM.stamp: FORCE
-       $(QUIET_GEN)$(FIND) perl -type f -name '*.pm' | sort >$@+ && \
+       @$(FIND) perl -type f -name '*.pm' | sort >$@+ && \
        { cmp $@+ $@ >/dev/null 2>/dev/null || mv $@+ $@; } && \
        $(RM) $@+
 
@@ -1784,7 +1784,7 @@ GIT-PERL-DEFINES: FORCE
 gitweb:
        $(QUIET_SUBDIR0)gitweb $(QUIET_SUBDIR1) all
 
-git-instaweb: git-instaweb.sh gitweb GIT-SCRIPT-DEFINES
+git-instaweb: git-instaweb.sh GIT-SCRIPT-DEFINES
        $(QUIET_GEN)$(cmd_munge_script) && \
        chmod +x $@+ && \
        mv $@+ $@
@@ -2103,46 +2103,47 @@ GIT-LDFLAGS: FORCE
 # that runs GIT-BUILD-OPTIONS, and then again to protect it
 # and the first level quoting from the shell that runs "echo".
 GIT-BUILD-OPTIONS: FORCE
-       @echo SHELL_PATH=\''$(subst ','\'',$(SHELL_PATH_SQ))'\' >$@
-       @echo PERL_PATH=\''$(subst ','\'',$(PERL_PATH_SQ))'\' >>$@
-       @echo DIFF=\''$(subst ','\'',$(subst ','\'',$(DIFF)))'\' >>$@
-       @echo PYTHON_PATH=\''$(subst ','\'',$(PYTHON_PATH_SQ))'\' >>$@
-       @echo TAR=\''$(subst ','\'',$(subst ','\'',$(TAR)))'\' >>$@
-       @echo NO_CURL=\''$(subst ','\'',$(subst ','\'',$(NO_CURL)))'\' >>$@
-       @echo NO_EXPAT=\''$(subst ','\'',$(subst ','\'',$(NO_EXPAT)))'\' >>$@
-       @echo USE_LIBPCRE=\''$(subst ','\'',$(subst ','\'',$(USE_LIBPCRE)))'\' >>$@
-       @echo NO_PERL=\''$(subst ','\'',$(subst ','\'',$(NO_PERL)))'\' >>$@
-       @echo NO_PYTHON=\''$(subst ','\'',$(subst ','\'',$(NO_PYTHON)))'\' >>$@
-       @echo NO_UNIX_SOCKETS=\''$(subst ','\'',$(subst ','\'',$(NO_UNIX_SOCKETS)))'\' >>$@
+       @echo SHELL_PATH=\''$(subst ','\'',$(SHELL_PATH_SQ))'\' >$@+
+       @echo PERL_PATH=\''$(subst ','\'',$(PERL_PATH_SQ))'\' >>$@+
+       @echo DIFF=\''$(subst ','\'',$(subst ','\'',$(DIFF)))'\' >>$@+
+       @echo PYTHON_PATH=\''$(subst ','\'',$(PYTHON_PATH_SQ))'\' >>$@+
+       @echo TAR=\''$(subst ','\'',$(subst ','\'',$(TAR)))'\' >>$@+
+       @echo NO_CURL=\''$(subst ','\'',$(subst ','\'',$(NO_CURL)))'\' >>$@+
+       @echo NO_EXPAT=\''$(subst ','\'',$(subst ','\'',$(NO_EXPAT)))'\' >>$@+
+       @echo USE_LIBPCRE=\''$(subst ','\'',$(subst ','\'',$(USE_LIBPCRE)))'\' >>$@+
+       @echo NO_PERL=\''$(subst ','\'',$(subst ','\'',$(NO_PERL)))'\' >>$@+
+       @echo NO_PYTHON=\''$(subst ','\'',$(subst ','\'',$(NO_PYTHON)))'\' >>$@+
+       @echo NO_UNIX_SOCKETS=\''$(subst ','\'',$(subst ','\'',$(NO_UNIX_SOCKETS)))'\' >>$@+
 ifdef TEST_OUTPUT_DIRECTORY
-       @echo TEST_OUTPUT_DIRECTORY=\''$(subst ','\'',$(subst ','\'',$(TEST_OUTPUT_DIRECTORY)))'\' >>$@
+       @echo TEST_OUTPUT_DIRECTORY=\''$(subst ','\'',$(subst ','\'',$(TEST_OUTPUT_DIRECTORY)))'\' >>$@+
 endif
 ifdef GIT_TEST_OPTS
-       @echo GIT_TEST_OPTS=\''$(subst ','\'',$(subst ','\'',$(GIT_TEST_OPTS)))'\' >>$@
+       @echo GIT_TEST_OPTS=\''$(subst ','\'',$(subst ','\'',$(GIT_TEST_OPTS)))'\' >>$@+
 endif
 ifdef GIT_TEST_CMP
-       @echo GIT_TEST_CMP=\''$(subst ','\'',$(subst ','\'',$(GIT_TEST_CMP)))'\' >>$@
+       @echo GIT_TEST_CMP=\''$(subst ','\'',$(subst ','\'',$(GIT_TEST_CMP)))'\' >>$@+
 endif
 ifdef GIT_TEST_CMP_USE_COPIED_CONTEXT
-       @echo GIT_TEST_CMP_USE_COPIED_CONTEXT=YesPlease >>$@
+       @echo GIT_TEST_CMP_USE_COPIED_CONTEXT=YesPlease >>$@+
 endif
-       @echo NO_GETTEXT=\''$(subst ','\'',$(subst ','\'',$(NO_GETTEXT)))'\' >>$@
-       @echo GETTEXT_POISON=\''$(subst ','\'',$(subst ','\'',$(GETTEXT_POISON)))'\' >>$@
+       @echo NO_GETTEXT=\''$(subst ','\'',$(subst ','\'',$(NO_GETTEXT)))'\' >>$@+
+       @echo GETTEXT_POISON=\''$(subst ','\'',$(subst ','\'',$(GETTEXT_POISON)))'\' >>$@+
 ifdef GIT_PERF_REPEAT_COUNT
-       @echo GIT_PERF_REPEAT_COUNT=\''$(subst ','\'',$(subst ','\'',$(GIT_PERF_REPEAT_COUNT)))'\' >>$@
+       @echo GIT_PERF_REPEAT_COUNT=\''$(subst ','\'',$(subst ','\'',$(GIT_PERF_REPEAT_COUNT)))'\' >>$@+
 endif
 ifdef GIT_PERF_REPO
-       @echo GIT_PERF_REPO=\''$(subst ','\'',$(subst ','\'',$(GIT_PERF_REPO)))'\' >>$@
+       @echo GIT_PERF_REPO=\''$(subst ','\'',$(subst ','\'',$(GIT_PERF_REPO)))'\' >>$@+
 endif
 ifdef GIT_PERF_LARGE_REPO
-       @echo GIT_PERF_LARGE_REPO=\''$(subst ','\'',$(subst ','\'',$(GIT_PERF_LARGE_REPO)))'\' >>$@
+       @echo GIT_PERF_LARGE_REPO=\''$(subst ','\'',$(subst ','\'',$(GIT_PERF_LARGE_REPO)))'\' >>$@+
 endif
 ifdef GIT_PERF_MAKE_OPTS
-       @echo GIT_PERF_MAKE_OPTS=\''$(subst ','\'',$(subst ','\'',$(GIT_PERF_MAKE_OPTS)))'\' >>$@
+       @echo GIT_PERF_MAKE_OPTS=\''$(subst ','\'',$(subst ','\'',$(GIT_PERF_MAKE_OPTS)))'\' >>$@+
 endif
 ifdef TEST_GIT_INDEX_VERSION
-       @echo TEST_GIT_INDEX_VERSION=\''$(subst ','\'',$(subst ','\'',$(TEST_GIT_INDEX_VERSION)))'\' >>$@
+       @echo TEST_GIT_INDEX_VERSION=\''$(subst ','\'',$(subst ','\'',$(TEST_GIT_INDEX_VERSION)))'\' >>$@+
 endif
+       @if cmp $@+ $@ >/dev/null 2>&1; then $(RM) $@+; else mv $@+ $@; fi
 
 ### Detect Python interpreter path changes
 ifndef NO_PYTHON
index 146be97a1a879242aa12eb066c5c69cd6af92409..54aba4e351257f8bc4b46d3f507c1ed9525ec7fc 100644 (file)
@@ -1638,6 +1638,9 @@ static int parse_fragment(const char *line, unsigned long size,
        }
        if (oldlines || newlines)
                return -1;
+       if (!deleted && !added)
+               return -1;
+
        fragment->leading = leading;
        fragment->trailing = trailing;
 
index b3e948e757e97947211f598488339cd4f03c43eb..a22ac174078742cc8991c0ee563980d239731e9b 100644 (file)
@@ -2176,6 +2176,14 @@ static int git_blame_config(const char *var, const char *value, void *cb)
                blank_boundary = git_config_bool(var, value);
                return 0;
        }
+       if (!strcmp(var, "blame.showemail")) {
+               int *output_option = cb;
+               if (git_config_bool(var, value))
+                       *output_option |= OUTPUT_SHOW_EMAIL;
+               else
+                       *output_option &= ~OUTPUT_SHOW_EMAIL;
+               return 0;
+       }
        if (!strcmp(var, "blame.date")) {
                if (!value)
                        return config_error_nonbool(var);
@@ -2520,7 +2528,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
        unsigned int range_i;
        long anchor;
 
-       git_config(git_blame_config, NULL);
+       git_config(git_blame_config, &output_option);
        init_revisions(&revs, NULL);
        revs.date_mode = blame_date_mode;
        DIFF_OPT_SET(&revs.diffopt, ALLOW_TEXTCONV);
index b878252bc2e64bbe95adbae5554618f017c9ac85..00535d0178f1a0ee562bf008e8b072f68410dc3d 100644 (file)
@@ -51,15 +51,6 @@ static struct string_list option_config;
 static struct string_list option_reference;
 static int option_dissociate;
 
-static int opt_parse_reference(const struct option *opt, const char *arg, int unset)
-{
-       struct string_list *option_reference = opt->value;
-       if (!arg)
-               return -1;
-       string_list_append(option_reference, arg);
-       return 0;
-}
-
 static struct option builtin_clone_options[] = {
        OPT__VERBOSITY(&option_verbosity),
        OPT_BOOL(0, "progress", &option_progress,
@@ -83,8 +74,10 @@ static struct option builtin_clone_options[] = {
                    N_("initialize submodules in the clone")),
        OPT_STRING(0, "template", &option_template, N_("template-directory"),
                   N_("directory from which templates will be used")),
-       OPT_CALLBACK(0 , "reference", &option_reference, N_("repo"),
-                    N_("reference repository"), &opt_parse_reference),
+       OPT_STRING_LIST(0, "reference", &option_reference, N_("repo"),
+                       N_("reference repository")),
+       OPT_BOOL(0, "dissociate", &option_dissociate,
+                N_("use --reference only while cloning")),
        OPT_STRING('o', "origin", &option_origin, N_("name"),
                   N_("use <name> instead of 'origin' to track upstream")),
        OPT_STRING('b', "branch", &option_branch, N_("branch"),
@@ -95,8 +88,6 @@ static struct option builtin_clone_options[] = {
                    N_("create a shallow clone of that depth")),
        OPT_BOOL(0, "single-branch", &option_single_branch,
                    N_("clone only one branch, HEAD or --branch")),
-       OPT_BOOL(0, "dissociate", &option_dissociate,
-                N_("use --reference only while cloning")),
        OPT_STRING(0, "separate-git-dir", &real_git_dir, N_("gitdir"),
                   N_("separate git dir from working tree")),
        OPT_STRING_LIST('c', "config", &option_config, N_("key=value"),
index f7e51a7fadc40b1e4484a657fc55af8a3043967c..cb7db230d3ef76ee6a8b73971cbdabcc6d6590a9 100644 (file)
@@ -866,6 +866,11 @@ static int grab_single_ref(const char *refname, const struct object_id *oid,
                  return 0;
        }
 
+       if (flag & REF_ISBROKEN) {
+                 warning("ignoring broken ref %s", refname);
+                 return 0;
+       }
+
        if (*cb->grab_pattern) {
                const char **pattern;
                int namelen = strlen(refname);
index 4e8e2ee5b739379e387c3875a1d71de8a22a6db6..267979304994158c491b2306388b932c50d7438b 100644 (file)
@@ -451,35 +451,41 @@ static void fsck_dir(int i, char *path)
 
 static int default_refs;
 
+static void fsck_handle_reflog_sha1(const char *refname, unsigned char *sha1)
+{
+       struct object *obj;
+
+       if (!is_null_sha1(sha1)) {
+               obj = lookup_object(sha1);
+               if (obj) {
+                       obj->used = 1;
+                       mark_object_reachable(obj);
+               } else {
+                       error("%s: invalid reflog entry %s", refname, sha1_to_hex(sha1));
+                       errors_found |= ERROR_REACHABLE;
+               }
+       }
+}
+
 static int fsck_handle_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
                const char *email, unsigned long timestamp, int tz,
                const char *message, void *cb_data)
 {
-       struct object *obj;
+       const char *refname = cb_data;
 
        if (verbose)
                fprintf(stderr, "Checking reflog %s->%s\n",
                        sha1_to_hex(osha1), sha1_to_hex(nsha1));
 
-       if (!is_null_sha1(osha1)) {
-               obj = lookup_object(osha1);
-               if (obj) {
-                       obj->used = 1;
-                       mark_object_reachable(obj);
-               }
-       }
-       obj = lookup_object(nsha1);
-       if (obj) {
-               obj->used = 1;
-               mark_object_reachable(obj);
-       }
+       fsck_handle_reflog_sha1(refname, osha1);
+       fsck_handle_reflog_sha1(refname, nsha1);
        return 0;
 }
 
 static int fsck_handle_reflog(const char *logname, const struct object_id *oid,
                              int flag, void *cb_data)
 {
-       for_each_reflog_ent(logname, fsck_handle_reflog_ent, NULL);
+       for_each_reflog_ent(logname, fsck_handle_reflog_ent, (void *)logname);
        return 0;
 }
 
index 7ea2020d821004569a6913d9cf550149032a1b96..48fa4724aa01b67fd53682e8db51c8013c5bc5fe 100644 (file)
@@ -616,7 +616,9 @@ static int compare_ofs_delta_bases(off_t offset1, off_t offset2,
        int cmp = type1 - type2;
        if (cmp)
                return cmp;
-       return offset1 - offset2;
+       return offset1 < offset2 ? -1 :
+              offset1 > offset2 ?  1 :
+              0;
 }
 
 static int find_ofs_delta(const off_t offset, enum object_type type)
@@ -784,7 +786,7 @@ static void sha1_object(const void *data, struct object_entry *obj_entry,
        assert(data || obj_entry);
 
        read_lock();
-       collision_test_needed = has_sha1_file(sha1);
+       collision_test_needed = has_sha1_file_with_flags(sha1, HAS_SHA1_QUICK);
        read_unlock();
 
        if (collision_test_needed && !data) {
@@ -1051,7 +1053,9 @@ static int compare_ofs_delta_entry(const void *a, const void *b)
        const struct ofs_delta_entry *delta_a = a;
        const struct ofs_delta_entry *delta_b = b;
 
-       return delta_a->offset - delta_b->offset;
+       return delta_a->offset < delta_b->offset ? -1 :
+              delta_a->offset > delta_b->offset ?  1 :
+              0;
 }
 
 static int compare_ref_delta_entry(const void *a, const void *b)
index e67671e37ab51d49424bb9a8945901dc9f9776ed..878104943f04b6302dfdfd279a2afabadf89c191 100644 (file)
@@ -795,7 +795,7 @@ static int reopen_stdout(struct commit *commit, const char *subject,
 static void get_patch_ids(struct rev_info *rev, struct patch_ids *ids)
 {
        struct rev_info check_rev;
-       struct commit *commit;
+       struct commit *commit, *c1, *c2;
        struct object *o1, *o2;
        unsigned flags1, flags2;
 
@@ -803,9 +803,11 @@ static void get_patch_ids(struct rev_info *rev, struct patch_ids *ids)
                die(_("Need exactly one range."));
 
        o1 = rev->pending.objects[0].item;
-       flags1 = o1->flags;
        o2 = rev->pending.objects[1].item;
+       flags1 = o1->flags;
        flags2 = o2->flags;
+       c1 = lookup_commit_reference(o1->sha1);
+       c2 = lookup_commit_reference(o2->sha1);
 
        if ((flags1 & UNINTERESTING) == (flags2 & UNINTERESTING))
                die(_("Not a range."));
@@ -827,10 +829,8 @@ static void get_patch_ids(struct rev_info *rev, struct patch_ids *ids)
        }
 
        /* reset for next revision walk */
-       clear_commit_marks((struct commit *)o1,
-                       SEEN | UNINTERESTING | SHOWN | ADDED);
-       clear_commit_marks((struct commit *)o2,
-                       SEEN | UNINTERESTING | SHOWN | ADDED);
+       clear_commit_marks(c1, SEEN | UNINTERESTING | SHOWN | ADDED);
+       clear_commit_marks(c2, SEEN | UNINTERESTING | SHOWN | ADDED);
        o1->flags = flags1;
        o2->flags = flags2;
 }
diff --git a/cache.h b/cache.h
index 571c98f5e2406afad1352fcaddd1efff1af65cc1..4f554664c5bd064405082797ee1e8786ebdcea75 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -943,8 +943,17 @@ extern int has_sha1_pack(const unsigned char *sha1);
  * Return true iff we have an object named sha1, whether local or in
  * an alternate object database, and whether packed or loose.  This
  * function does not respect replace references.
+ *
+ * If the QUICK flag is set, do not re-check the pack directory
+ * when we cannot find the object (this means we may give a false
+ * negative answer if another process is simultaneously repacking).
  */
-extern int has_sha1_file(const unsigned char *sha1);
+#define HAS_SHA1_QUICK 0x1
+extern int has_sha1_file_with_flags(const unsigned char *sha1, int flags);
+static inline int has_sha1_file(const unsigned char *sha1)
+{
+       return has_sha1_file_with_flags(sha1, 0);
+}
 
 /*
  * Return true iff an alternate object database has a loose object
@@ -1698,5 +1707,6 @@ int stat_validity_check(struct stat_validity *sv, const char *path);
 void stat_validity_update(struct stat_validity *sv, int fd);
 
 int versioncmp(const char *s1, const char *s2);
+void sleep_millisec(int millisec);
 
 #endif /* CACHE_H */
index 8eb7278978ddaadc605787eb4139d688a01fb052..30c7eb6d3cddbdf17d6ca8ff3bc85267041d1669 100644 (file)
@@ -730,7 +730,7 @@ static void dump_sline(struct sline *sline, const char *line_prefix,
        const char *c_func = diff_get_color(use_color, DIFF_FUNCINFO);
        const char *c_new = diff_get_color(use_color, DIFF_FILE_NEW);
        const char *c_old = diff_get_color(use_color, DIFF_FILE_OLD);
-       const char *c_plain = diff_get_color(use_color, DIFF_PLAIN);
+       const char *c_context = diff_get_color(use_color, DIFF_CONTEXT);
        const char *c_reset = diff_get_color(use_color, DIFF_RESET);
 
        if (result_deleted)
@@ -793,7 +793,7 @@ static void dump_sline(struct sline *sline, const char *line_prefix,
                        }
                        if (comment_end)
                                printf("%s%s %s%s", c_reset,
-                                                   c_plain, c_reset,
+                                                   c_context, c_reset,
                                                    c_func);
                        for (i = 0; i < comment_end; i++)
                                putchar(hunk_comment[i]);
@@ -828,7 +828,7 @@ static void dump_sline(struct sline *sline, const char *line_prefix,
                                 */
                                if (!context)
                                        continue;
-                               fputs(c_plain, stdout);
+                               fputs(c_context, stdout);
                        }
                        else
                                fputs(c_new, stdout);
index 2d9de807aeb230c97e74928c1b446544186bc4e1..6e2103cef60e8a0c2df18685280dbb5fda5c133f 100644 (file)
--- a/commit.c
+++ b/commit.c
@@ -357,7 +357,7 @@ int parse_commit_buffer(struct commit *item, const void *buffer, unsigned long s
        return 0;
 }
 
-int parse_commit(struct commit *item)
+int parse_commit_gently(struct commit *item, int quiet_on_missing)
 {
        enum object_type type;
        void *buffer;
@@ -370,7 +370,8 @@ int parse_commit(struct commit *item)
                return 0;
        buffer = read_sha1_file(item->object.sha1, &type, &size);
        if (!buffer)
-               return error("Could not read %s",
+               return quiet_on_missing ? -1 :
+                       error("Could not read %s",
                             sha1_to_hex(item->object.sha1));
        if (type != OBJ_COMMIT) {
                free(buffer);
index ed3a1d59a553b498b20d28f4999effde78e51c3c..9a1fa961d2ba0e3ec3eae9096ef49bc7155cdd92 100644 (file)
--- a/commit.h
+++ b/commit.h
@@ -59,7 +59,11 @@ struct commit *lookup_commit_reference_by_name(const char *name);
 struct commit *lookup_commit_or_die(const unsigned char *sha1, const char *ref_name);
 
 int parse_commit_buffer(struct commit *item, const void *buffer, unsigned long size);
-int parse_commit(struct commit *item);
+int parse_commit_gently(struct commit *item, int quiet_on_missing);
+static inline int parse_commit(struct commit *item)
+{
+       return parse_commit_gently(item, 0);
+}
 void parse_commit_or_die(struct commit *item);
 
 /*
index ab46462e151dd5cd9d1e5a1fecd23fe5c6607c5d..29fa0121d583d3a77fcc2e8319cc484c317dcca3 100644 (file)
--- a/config.c
+++ b/config.c
@@ -1939,6 +1939,8 @@ int git_config_set_multivar_in_file(const char *config_filename,
        int ret;
        struct lock_file *lock = NULL;
        char *filename_buf = NULL;
+       char *contents = NULL;
+       size_t contents_sz;
 
        /* parse-key returns negative; flip the sign to feed exit(3) */
        ret = 0 - git_config_parse_key(key, &store.key, &store.baselen);
@@ -1988,8 +1990,7 @@ int git_config_set_multivar_in_file(const char *config_filename,
                        goto write_err_out;
        } else {
                struct stat st;
-               char *contents;
-               size_t contents_sz, copy_begin, copy_end;
+               size_t copy_begin, copy_end;
                int i, new_line = 0;
 
                if (value_regex == NULL)
@@ -2052,8 +2053,17 @@ int git_config_set_multivar_in_file(const char *config_filename,
 
                fstat(in_fd, &st);
                contents_sz = xsize_t(st.st_size);
-               contents = xmmap(NULL, contents_sz, PROT_READ,
-                       MAP_PRIVATE, in_fd, 0);
+               contents = xmmap_gently(NULL, contents_sz, PROT_READ,
+                                       MAP_PRIVATE, in_fd, 0);
+               if (contents == MAP_FAILED) {
+                       if (errno == ENODEV && S_ISDIR(st.st_mode))
+                               errno = EISDIR;
+                       error("unable to mmap '%s': %s",
+                             config_filename, strerror(errno));
+                       ret = CONFIG_INVALID_FILE;
+                       contents = NULL;
+                       goto out_free;
+               }
                close(in_fd);
 
                if (chmod(lock->filename.buf, st.st_mode & 07777) < 0) {
@@ -2108,8 +2118,6 @@ int git_config_set_multivar_in_file(const char *config_filename,
                                          contents_sz - copy_begin) <
                            contents_sz - copy_begin)
                                goto write_err_out;
-
-               munmap(contents, contents_sz);
        }
 
        if (commit_lock_file(lock) < 0) {
@@ -2135,6 +2143,8 @@ out_free:
        if (lock)
                rollback_lock_file(lock);
        free(filename_buf);
+       if (contents)
+               munmap(contents, contents_sz);
        return ret;
 
 write_err_out:
index d26665fa54c90a45724c0eb6bdbcbb81c94574a1..943c43965e68fe39fa6e0ea266c3ac376f6e0125 100644 (file)
@@ -102,6 +102,9 @@ ifeq ($(uname_S),Darwin)
        ifeq ($(shell expr "$(uname_R)" : '[15]\.'),2)
                NO_STRLCPY = YesPlease
        endif
+       ifeq ($(shell test "`expr "$(uname_R)" : '\([0-9][0-9]*\)\.'`" -ge 11 && echo 1),1)
+               HAVE_GETDELIM = YesPlease
+       endif
        NO_MEMMEM = YesPlease
        USE_ST_TIMESPEC = YesPlease
        HAVE_DEV_TTY = YesPlease
index bbdde85c3dcca460dd0aebee6415ccce184885dc..14012fad720c1fdade1f305b1a7092abf2146ac1 100644 (file)
@@ -1041,6 +1041,12 @@ GIT_CHECK_FUNC(initgroups,
 [NO_INITGROUPS=YesPlease])
 GIT_CONF_SUBST([NO_INITGROUPS])
 #
+# Define HAVE_GETDELIM if you have getdelim in the C library.
+GIT_CHECK_FUNC(getdelim,
+[HAVE_GETDELIM=YesPlease],
+[HAVE_GETDELIM=])
+GIT_CONF_SUBST([HAVE_GETDELIM])
+#
 #
 # Define NO_MMAP if you want to avoid mmap.
 #
index bfc74e9d57a5293fce39362c68e7490888ec08e6..c97c648d7e07fe9eeea1b1afff1ce55ac730cf4f 100644 (file)
@@ -1108,7 +1108,7 @@ _git_commit ()
 
        case "$cur" in
        --cleanup=*)
-               __gitcomp "default strip verbatim whitespace
+               __gitcomp "default scissors strip verbatim whitespace
                        " "" "${cur##--cleanup=}"
                return
                ;;
@@ -2282,6 +2282,11 @@ _git_reset ()
 
 _git_revert ()
 {
+       local dir="$(__gitdir)"
+       if [ -f "$dir"/REVERT_HEAD ]; then
+               __gitcomp "--continue --quit --abort"
+               return
+       fi
        case "$cur" in
        --*)
                __gitcomp "--edit --mainline --no-edit --no-commit --signoff"
index 6104a42a23c4f38848a34c5c212bd7b74de81ab4..4a790d8f4ec18e0bfedbc7f8ce9f2d4291c284e4 100644 (file)
@@ -41,7 +41,7 @@ if ( ! -e ${__git_tcsh_completion_original_script} ) then
        exit
 endif
 
-cat << EOF > ${__git_tcsh_completion_script}
+cat << EOF >! ${__git_tcsh_completion_script}
 #!bash
 #
 # This script is GENERATED and will be overwritten automatically.
index 3603d56c26e21f44ca4983cf85aca95a6aa6d69d..0b823d8f5f6eaa13f900bfbb0152383b32be20b8 100644 (file)
@@ -1,3 +1,51 @@
+Release 1.1.0
+=============
+
+* When a single commit is pushed, omit the reference changed email.
+  Set multimailhook.combineWhenSingleCommit to false to disable this
+  new feature.
+
+* In gitolite environments, the pusher's email address can be used as
+  the From address by creating a specially formatted comment block in
+  gitolite.conf (see multimailhook.from in README).
+
+* Support for SMTP authentication and SSL/TLS encryption was added,
+  see smtpUser, smtpPass, smtpEncryption in README.
+
+* A new option scanCommitForCc was added to allow git-multimail to
+  search the commit message for 'Cc: ...' lines, and add the
+  corresponding emails in Cc.
+
+* If $USER is not set, use the variable $USERNAME. This is needed on
+  Windows platform to recognize the pusher.
+
+* The emailPrefix variable can now be set to an empty string to remove
+  the prefix.
+
+* A short tutorial was added in doc/gitolite.rst to set up
+  git-multimail with gitolite.
+
+* The post-receive file was renamed to post-receive.example. It has
+  always been an example (the standard way to call git-multimail is to
+  call git_multimail.py), but it was unclear to many users.
+
+* A new refchangeShowGraph option was added to make it possible to
+  include both a graph and a log in the summary emails.  The options
+  to control the graph formatting can be set via the new graphOpts
+  option.
+
+* New option --force-send was added to disable new commit detection
+  for update hook. One use-case is to run git_multimail.py after
+  running "git fetch" to send emails about commits that have just been
+  fetched (the detection of new commits was unreliable in this mode).
+
+* The testing infrastructure was considerably improved (continuous
+  integration with travis-ci, automatic check of PEP8 and RST syntax,
+  many improvements to the test scripts).
+
+This version has been tested with Python 2.4 to 2.7, and Git 1.7.1 to
+2.4.
+
 Release 1.0.0
 =============
 
index 6efa4ffe173b3185cecb6192e74f754bc3d5a003..3a33cb734ab1d9297d036167cc3f7ef13241f2bf 100644 (file)
@@ -1,5 +1,8 @@
-                          git-multimail
-                          =============
+git-multimail Version 1.1.0
+===========================
+
+.. image:: https://travis-ci.org/git-multimail/git-multimail.svg?branch=master
+    :target: https://travis-ci.org/git-multimail/git-multimail
 
 git-multimail is a tool for sending notification emails on pushes to a
 Git repository.  It includes a Python module called git_multimail.py,
@@ -38,17 +41,17 @@ By default, for each push received by the repository, git-multimail:
    list) makes it easy to scan through the emails, jump to patches
    that need further attention, and write comments about specific
    commits.  Commits are handled in reverse topological order (i.e.,
-   parents shown before children).  For example,
-
-   [git] branch master updated
-   + [git] 01/08: doc: fix xref link from api docs to manual pages
-   + [git] 02/08: api-credentials.txt: show the big picture first
-   + [git] 03/08: api-credentials.txt: mention credential.helper explicitly
-   + [git] 04/08: api-credentials.txt: add "see also" section
-   + [git] 05/08: t3510 (cherry-pick-sequence): add missing '&&'
-   + [git] 06/08: Merge branch 'rr/maint-t3510-cascade-fix'
-   + [git] 07/08: Merge branch 'mm/api-credentials-doc'
-   + [git] 08/08: Git 1.7.11-rc2
+   parents shown before children).  For example::
+
+     [git] branch master updated
+     + [git] 01/08: doc: fix xref link from api docs to manual pages
+     + [git] 02/08: api-credentials.txt: show the big picture first
+     + [git] 03/08: api-credentials.txt: mention credential.helper explicitly
+     + [git] 04/08: api-credentials.txt: add "see also" section
+     + [git] 05/08: t3510 (cherry-pick-sequence): add missing '&&'
+     + [git] 06/08: Merge branch 'rr/maint-t3510-cascade-fix'
+     + [git] 07/08: Merge branch 'mm/api-credentials-doc'
+     + [git] 08/08: Git 1.7.11-rc2
 
    Each commit appears in exactly one commit email, the first time
    that it is pushed to the repository.  If a commit is later merged
@@ -74,19 +77,19 @@ Requirements
   3.x.
 
   The example scripts invoke Python using the following shebang line
-  (following PEP 394 [1]):
+  (following PEP 394 [1]_)::
 
       #! /usr/bin/env python2
 
   If your system's Python2 interpreter is not in your PATH or is not
-  called "python2", you can change the lines accordingly.  Or you can
+  called ``python2``, you can change the lines accordingly.  Or you can
   invoke the Python interpreter explicitly, for example via a tiny
-  shell script like
+  shell script like::
 
       #! /bin/sh
       /usr/local/bin/python /path/to/git_multimail.py "$@"
 
-* The "git" command must be in your PATH.  git-multimail is known to
+* The ``git`` command must be in your PATH.  git-multimail is known to
   work with Git versions back to 1.7.1.  (Earlier versions have not
   been tested; if you do so, please report your results.)
 
@@ -101,7 +104,7 @@ Requirements
 Invocation
 ----------
 
-git_multimail.py is designed to be used as a "post-receive" hook in a
+git_multimail.py is designed to be used as a ``post-receive`` hook in a
 Git repository (see githooks(5)).  Link or copy it to
 $GIT_DIR/hooks/post-receive within the repository for which email
 notifications are desired.  Usually it should be installed on the
@@ -109,10 +112,10 @@ central repository for a project, to which all commits are eventually
 pushed.
 
 For use on pre-v1.5.1 Git servers, git_multimail.py can also work as
-an "update" hook, taking its arguments on the command line.  To use
+an ``update`` hook, taking its arguments on the command line.  To use
 this script in this manner, link or copy it to $GIT_DIR/hooks/update.
 Please note that the script is not completely reliable in this mode
-[2].
+[2]_.
 
 Alternatively, git_multimail.py can be imported as a Python module
 into your own Python post-receive script.  This method is a bit more
@@ -129,7 +132,7 @@ arbitrary Python code.  For example, you can use a custom environment
   only about changes affecting particular files or subdirectories)
 
 Or you can change how emails are sent by writing your own Mailer
-class.  The "post-receive" script in this directory demonstrates how
+class.  The ``post-receive`` script in this directory demonstrates how
 to use git_multimail.py as a Python module.  (If you make interesting
 changes of this type, please consider sharing them with the
 community.)
@@ -139,18 +142,26 @@ Configuration
 -------------
 
 By default, git-multimail mostly takes its configuration from the
-following "git config" settings:
+following ``git config`` settings:
 
 multimailhook.environment
 
     This describes the general environment of the repository.
     Currently supported values:
 
-    "generic" -- the username of the pusher is read from $USER and the
-        repository name is derived from the repository's path.
+    * generic
+
+      the username of the pusher is read from $USER or $USERNAME and
+      the repository name is derived from the repository's path.
+
+    * gitolite
 
-    "gitolite" -- the username of the pusher is read from $GL_USER and
-        the repository name from $GL_REPO.
+      the username of the pusher is read from $GL_USER, the repository
+      name is read from $GL_REPO, and the From: header value is
+      optionally read from gitolite.conf (see multimailhook.from).
+
+      For more information about gitolite and git-multimail, read
+      doc/gitolite.rst
 
     If neither of these environments is suitable for your setup, then
     you can implement a Python class that inherits from Environment
@@ -160,8 +171,8 @@ multimailhook.environment
     The environment value can be specified on the command line using
     the --environment option.  If it is not specified on the command
     line or by multimailhook.environment, then it defaults to
-    "gitolite" if the environment contains variables $GL_USER and
-    $GL_REPO; otherwise "generic".
+    ``gitolite`` if the environment contains variables $GL_USER and
+    $GL_REPO; otherwise ``generic``.
 
 multimailhook.repoName
 
@@ -219,61 +230,109 @@ multimailhook.announceShortlog
     not so straightforward, then the shortlog might be confusing
     rather than useful.  Default is false.
 
+multimailhook.refchangeShowGraph
+
+    If this option is set to true, then summary emails about reference
+    changes will additionally include:
+
+    * a graph of the added commits (if any)
+
+    * a graph of the discarded commits (if any)
+
+    The log is generated by running ``git log --graph`` with the options
+    specified in graphOpts.  The default is false.
+
 multimailhook.refchangeShowLog
 
     If this option is set to true, then summary emails about reference
     changes will include a detailed log of the added commits in
     addition to the one line summary.  The log is generated by running
-    "git log" with the options specified in multimailhook.logOpts.
+    ``git log`` with the options specified in multimailhook.logOpts.
     Default is false.
 
 multimailhook.mailer
 
     This option changes the way emails are sent.  Accepted values are:
 
-    - sendmail (the default): use the command /usr/sbin/sendmail or
-      /usr/lib/sendmail (or sendmailCommand, if configured).  This
+    - sendmail (the default): use the command ``/usr/sbin/sendmail`` or
+      ``/usr/lib/sendmail`` (or sendmailCommand, if configured).  This
       mode can be further customized via the following options:
 
-       multimailhook.sendmailCommand
+      * multimailhook.sendmailCommand
 
-           The command used by mailer "sendmail" to send emails.  Shell
-           quoting is allowed in the value of this setting, but remember that
-           Git requires double-quotes to be escaped; e.g.,
+        The command used by mailer ``sendmail`` to send emails.  Shell
+        quoting is allowed in the value of this setting, but remember that
+        Git requires double-quotes to be escaped; e.g.::
 
              git config multimailhook.sendmailcommand '/usr/sbin/sendmail -oi -t -F \"Git Repo\"'
 
-           Default is '/usr/sbin/sendmail -oi -t' or
-           '/usr/lib/sendmail -oi -t' (depending on which file is
-           present and executable).
+        Default is '/usr/sbin/sendmail -oi -t' or
+        '/usr/lib/sendmail -oi -t' (depending on which file is
+        present and executable).
 
-       multimailhook.envelopeSender
+      * multimailhook.envelopeSender
 
-           If set then pass this value to sendmail via the -f option to set
-           the envelope sender address.
+        If set then pass this value to sendmail via the -f option to set
+        the envelope sender address.
 
     - smtp: use Python's smtplib.  This is useful when the sendmail
       command is not available on the system.  This mode can be
       further customized via the following options:
 
-       multimailhook.smtpServer
+      * multimailhook.smtpServer
+
+        The name of the SMTP server to connect to.  The value can
+        also include a colon and a port number; e.g.,
+        ``mail.example.com:25``.  Default is 'localhost' using port 25.
+
+      * multimailhook.smtpUser
+      * multimailhook.smtpPass
+
+        Server username and password. Required if smtpEncryption is 'ssl'.
+        Note that the username and password currently need to be
+        set cleartext in the configuration file, which is not
+        recommended. If you need to use this option, be sure your
+        configuration file is read-only.
+
+      * multimailhook.envelopeSender
+
+        The sender address to be passed to the SMTP server.  If
+        unset, then the value of multimailhook.from is used.
+
+      * multimailhook.smtpServerTimeout
+
+        Timeout in seconds.
 
-           The name of the SMTP server to connect to.  The value can
-           also include a colon and a port number; e.g.,
-           "mail.example.com:25".  Default is 'localhost' using port
-           25.
+      * multimailhook.smtpEncryption
 
-       multimailhook.envelopeSender
+        Set the security type. Allowed values: none, ssl.
+        Default=none.
 
-           The sender address to be passed to the SMTP server.  If
-           unset, then the value of multimailhook.from is used.
+      * multimailhook.smtpServerDebugLevel
+
+        Integer number. Set to greater than 0 to activate debugging.
 
 multimailhook.from
 
-    If set then use this value in the From: field of generated emails.
-    If unset, then use the repository's user configuration (user.name
-    and user.email).  If user.email is also unset, then use
-    multimailhook.envelopeSender.
+    If set, use this value in the From: field of generated emails.  If
+    unset, the value of the From: header is determined as follows:
+
+    1. (gitolite environment only) Parse gitolite.conf, looking for a
+       block of comments that looks like this::
+
+           # BEGIN USER EMAILS
+           # username Firstname Lastname <email@example.com>
+           # END USER EMAILS
+
+       If that block exists, and there is a line between the BEGIN
+       USER EMAILS and END USER EMAILS lines where the first field
+       matches the gitolite username ($GL_USER), use the rest of the
+       line for the From: header.
+
+    2. If the user.email configuration setting is set, use its value
+       (and the value of user.name, if set).
+
+    3. Use the value of multimailhook.envelopeSender.
 
 multimailhook.administrator
 
@@ -287,7 +346,8 @@ multimailhook.emailPrefix
     All emails have this string prepended to their subjects, to aid
     email filtering (though filtering based on the X-Git-* email
     headers is probably more robust).  Default is the short name of
-    the repository in square brackets; e.g., "[myrepo]".
+    the repository in square brackets; e.g., ``[myrepo]``.  Set this
+    value to the empty string to suppress the email prefix.
 
 multimailhook.emailMaxLines
 
@@ -299,7 +359,7 @@ multimailhook.emailMaxLines
 multimailhook.emailMaxLineLength
 
     The maximum length of a line in the email body.  Lines longer than
-    this limit are truncated to this length with a trailing " [...]"
+    this limit are truncated to this length with a trailing `` [...]``
     added to indicate the missing text.  The default is 500, because
     (a) diffs with longer lines are probably from binary files, for
     which a diff is useless, and (b) even if a text file has such long
@@ -316,54 +376,62 @@ multimailhook.maxCommitEmails
 
 multimailhook.emailStrictUTF8
 
-    If this boolean option is set to "true", then the main part of the
+    If this boolean option is set to `true`, then the main part of the
     email body is forced to be valid UTF-8.  Any characters that are
     not valid UTF-8 are converted to the Unicode replacement
-    character, U+FFFD.  The default is "true".
+    character, U+FFFD.  The default is `true`.
 
 multimailhook.diffOpts
 
-    Options passed to "git diff-tree" when generating the summary
-    information for ReferenceChange emails.  Default is "--stat
-    --summary --find-copies-harder".  Add -p to those options to
+    Options passed to ``git diff-tree`` when generating the summary
+    information for ReferenceChange emails.  Default is ``--stat
+    --summary --find-copies-harder``.  Add -p to those options to
     include a unified diff of changes in addition to the usual summary
     output.  Shell quoting is allowed; see multimailhook.logOpts for
     details.
 
+multimailhook.graphOpts
+
+    Options passed to ``git log --graph`` when generating graphs for the
+    reference change summary emails (used only if refchangeShowGraph
+    is true).  The default is '--oneline --decorate'.
+
+    Shell quoting is allowed; see logOpts for details.
+
 multimailhook.logOpts
 
-    Options passed to "git log" to generate additional info for
+    Options passed to ``git log`` to generate additional info for
     reference change emails (used only if refchangeShowLog is set).
-    For example, adding --graph will show the graph of revisions, -p
-    will show the complete diff, etc.  The default is empty.
+    For example, adding -p will show each commit's complete diff.  The
+    default is empty.
 
     Shell quoting is allowed; for example, a log format that contains
-    spaces can be specified using something like:
+    spaces can be specified using something like::
 
       git config multimailhook.logopts '--pretty=format:"%h %aN <%aE>%n%s%n%n%b%n"'
 
     If you want to set this by editing your configuration file
     directly, remember that Git requires double-quotes to be escaped
-    (see git-config(1) for more information):
+    (see git-config(1) for more information)::
 
       [multimailhook]
               logopts = --pretty=format:\"%h %aN <%aE>%n%s%n%n%b%n\"
 
 multimailhook.commitLogOpts
 
-    Options passed to "git log" to generate additional info for
+    Options passed to ``git log`` to generate additional info for
     revision change emails.  For example, adding --ignore-all-spaces
-    will suppress whitespace changes.  The default options are "-C
-    --stat -p --cc".  Shell quoting is allowed; see
+    will suppress whitespace changes.  The default options are ``-C
+    --stat -p --cc``.  Shell quoting is allowed; see
     multimailhook.logOpts for details.
 
 multimailhook.emailDomain
 
     Domain name appended to the username of the person doing the push
-    to convert it into an email address (via "%s@%s" % (username,
-    emaildomain)).  More complicated schemes can be implemented by
-    overriding Environment and overriding its get_pusher_email()
-    method.
+    to convert it into an email address
+    (via ``"%s@%s" % (username, emaildomain)``). More complicated
+    schemes can be implemented by overriding Environment and
+    overriding its get_pusher_email() method.
 
 multimailhook.replyTo
 multimailhook.replyToCommit
@@ -377,26 +445,48 @@ multimailhook.replyToRefchange
 
     - An email address, which will be used directly.
 
-    - The value "pusher", in which case the pusher's address (if
+    - The value `pusher`, in which case the pusher's address (if
       available) will be used.  This is the default for refchange
       emails.
 
-    - The value "author" (meaningful only for replyToCommit), in which
+    - The value `author` (meaningful only for replyToCommit), in which
       case the commit author's address will be used.  This is the
       default for commit emails.
 
-    - The value "none", in which case the Reply-To: field will be
+    - The value `none`, in which case the Reply-To: field will be
       omitted.
 
+multimailhook.quiet
+
+    Do not output the list of email recipients from the hook
+
+multimailhook.stdout
+
+    For debugging, send emails to stdout rather than to the
+    mailer.  Equivalent to the --stdout command line option
+
+multimailhook.scanCommitForCc
+
+    If this option is set to true, than recipients from lines in commit body
+    that starts with ``CC:`` will be added to CC list.
+    Default: false
+
+multimailhook.combineWhenSingleCommit
+
+    If this option is set to true and a single new commit is pushed to
+    a branch, combine the summary and commit email messages into a
+    single email.
+    Default: true
+
 
 Email filtering aids
 --------------------
 
 All emails include extra headers to enable fine tuned filtering and
 give information for debugging.  All emails include the headers
-"X-Git-Host", "X-Git-Repo", "X-Git-Refname", and "X-Git-Reftype".
-ReferenceChange emails also include headers "X-Git-Oldrev" and "X-Git-Newrev";
-Revision emails also include header "X-Git-Rev".
+``X-Git-Host``, ``X-Git-Repo``, ``X-Git-Refname``, and ``X-Git-Reftype``.
+ReferenceChange emails also include headers ``X-Git-Oldrev`` and ``X-Git-Newrev``;
+Revision emails also include header ``X-Git-Rev``.
 
 
 Customizing email contents
@@ -420,16 +510,17 @@ environment are built in:
 * GenericEnvironment: a stand-alone Git repository.
 
 * GitoliteEnvironment: a Git repository that is managed by gitolite
-  [3].  For such repositories, the identity of the pusher is read from
-  environment variable $GL_USER, and the name of the repository is
-  read from $GL_REPO (if it is not overridden by
-  multimailhook.reponame).
+  [3]_.  For such repositories, the identity of the pusher is read from
+  environment variable $GL_USER, the name of the repository is read
+  from $GL_REPO (if it is not overridden by multimailhook.reponame),
+  and the From: header value is optionally read from gitolite.conf
+  (see multimailhook.from).
 
 By default, git-multimail assumes GitoliteEnvironment if $GL_USER and
 $GL_REPO are set, and otherwise assumes GenericEnvironment.
 Alternatively, you can choose one of these two environments explicitly
-by setting a "multimailhook.environment" config setting (which can
-have the value "generic" or "gitolite") or by passing an --environment
+by setting a ``multimailhook.environment`` config setting (which can
+have the value `generic` or `gitolite`) or by passing an --environment
 option to the script.
 
 If you need to customize the script in ways that are not supported by
@@ -439,8 +530,8 @@ git_multimail.py as a Python module, as demonstrated by the example
 post-receive script.  Then implement your environment class; it should
 usually inherit from one of the existing Environment classes and
 possibly one or more of the EnvironmentMixin classes.  Then set the
-"environment" variable to an instance of your own environment class
-and pass it to run_as_post_receive_hook().
+``environment`` variable to an instance of your own environment class
+and pass it to ``run_as_post_receive_hook()``.
 
 The standard environment classes, GenericEnvironment and
 GitoliteEnvironment, are in fact themselves put together out of a
@@ -490,12 +581,14 @@ don't overlook them.
 Footnotes
 ---------
 
-[1] http://www.python.org/dev/peps/pep-0394/
+.. [1] http://www.python.org/dev/peps/pep-0394/
 
-[2] Because of the way information is passed to update hooks, the
-    script's method of determining whether a commit has already been
-    seen does not work when it is used as an "update" script.  In
-    particular, no notification email will be generated for a new
-    commit that is added to multiple references in the same push.
+.. [2] Because of the way information is passed to update hooks, the
+       script's method of determining whether a commit has already
+       been seen does not work when it is used as an ``update`` script.
+       In particular, no notification email will be generated for a
+       new commit that is added to multiple references in the same
+       push. A workaround is to use --force-send to force sending the
+       emails.
 
-[3] https://github.com/sitaramc/gitolite
+.. [3] https://github.com/sitaramc/gitolite
index ab3ece5221b7ecbc37994139f587bb2a90ab14dd..449d36f1564b16daceae7efe79755da9d5920e1c 100644 (file)
@@ -6,10 +6,10 @@ website:
     https://github.com/git-multimail/git-multimail
 
 The version in this directory was obtained from the upstream project
-on 2015-04-27 and consists of the "git-multimail" subdirectory from
+on Jun 18 2015 and consists of the "git-multimail" subdirectory from
 revision
 
-    8c3aaafa873bf10de8dddf1d202c449b3eff3b42 refs/tags/1.0.2
+    1f0dbb3b60035767889b913df16d9231ecdb8709 refs/tags/1.1.0
 
 Please see the README file in this directory for information about how
 to report bugs or contribute to git-multimail.
index 8b58ed644423932309c3193ac46a5213ea29ca73..7cb2b36cb43da930fbe5baa2cb120676fe7663ae 100755 (executable)
@@ -1,5 +1,6 @@
 #! /usr/bin/env python2
 
+# Copyright (c) 2015 Matthieu Moy and others
 # Copyright (c) 2012-2014 Michael Haggerty and others
 # Derived from contrib/hooks/post-receive-email, which is
 # Copyright (c) 2007 Andy Parkins
@@ -99,6 +100,10 @@ REF_DELETED_SUBJECT_TEMPLATE = (
     ' (was %(oldrev_short)s)'
     )
 
+COMBINED_REFCHANGE_REVISION_SUBJECT_TEMPLATE = (
+    '%(emailprefix)s%(refname_type)s %(short_refname)s updated: %(oneline)s'
+    )
+
 REFCHANGE_HEADER_TEMPLATE = """\
 Date: %(send_date)s
 To: %(recipients)s
@@ -230,6 +235,7 @@ how to provide full information about this reference change.
 REVISION_HEADER_TEMPLATE = """\
 Date: %(send_date)s
 To: %(recipients)s
+Cc: %(cc_recipients)s
 Subject: %(emailprefix)s%(num)02d/%(tot)02d: %(oneline)s
 MIME-Version: 1.0
 Content-Type: text/plain; charset=%(charset)s
@@ -258,6 +264,38 @@ in repository %(repo_shortname)s.
 REVISION_FOOTER_TEMPLATE = FOOTER_TEMPLATE
 
 
+# Combined, meaning refchange+revision email (for single-commit additions)
+COMBINED_HEADER_TEMPLATE = """\
+Date: %(send_date)s
+To: %(recipients)s
+Subject: %(subject)s
+MIME-Version: 1.0
+Content-Type: text/plain; charset=%(charset)s
+Content-Transfer-Encoding: 8bit
+Message-ID: %(msgid)s
+From: %(fromaddr)s
+Reply-To: %(reply_to)s
+X-Git-Host: %(fqdn)s
+X-Git-Repo: %(repo_shortname)s
+X-Git-Refname: %(refname)s
+X-Git-Reftype: %(refname_type)s
+X-Git-Oldrev: %(oldrev)s
+X-Git-Newrev: %(newrev)s
+X-Git-Rev: %(rev)s
+Auto-Submitted: auto-generated
+"""
+
+COMBINED_INTRO_TEMPLATE = """\
+This is an automated email from the git hooks/post-receive script.
+
+%(pusher)s pushed a commit to %(refname_type)s %(short_refname)s
+in repository %(repo_shortname)s.
+
+"""
+
+COMBINED_FOOTER_TEMPLATE = FOOTER_TEMPLATE
+
+
 class CommandError(Exception):
     def __init__(self, cmd, retcode):
         self.cmd = cmd
@@ -336,6 +374,47 @@ def read_git_lines(args, keepends=False, **kw):
     return read_git_output(args, keepends=True, **kw).splitlines(keepends)
 
 
+def git_rev_list_ish(cmd, spec, args=None, **kw):
+    """Common functionality for invoking a 'git rev-list'-like command.
+
+    Parameters:
+      * cmd is the Git command to run, e.g., 'rev-list' or 'log'.
+      * spec is a list of revision arguments to pass to the named
+        command.  If None, this function returns an empty list.
+      * args is a list of extra arguments passed to the named command.
+      * All other keyword arguments (if any) are passed to the
+        underlying read_git_lines() function.
+
+    Return the output of the Git command in the form of a list, one
+    entry per output line.
+    """
+    if spec is None:
+        return []
+    if args is None:
+        args = []
+    args = [cmd, '--stdin'] + args
+    spec_stdin = ''.join(s + '\n' for s in spec)
+    return read_git_lines(args, input=spec_stdin, **kw)
+
+
+def git_rev_list(spec, **kw):
+    """Run 'git rev-list' with the given list of revision arguments.
+
+    See git_rev_list_ish() for parameter and return value
+    documentation.
+    """
+    return git_rev_list_ish('rev-list', spec, **kw)
+
+
+def git_log(spec, **kw):
+    """Run 'git log' with the given list of revision arguments.
+
+    See git_rev_list_ish() for parameter and return value
+    documentation.
+    """
+    return git_rev_list_ish('log', spec, **kw)
+
+
 def header_encode(text, header_name=None):
     """Encode and line-wrap the value of an email header field."""
 
@@ -388,9 +467,9 @@ class Config(object):
     def get(self, name, default=None):
         try:
             values = self._split(read_git_output(
-                    ['config', '--get', '--null', '%s.%s' % (self.section, name)],
-                    env=self.env, keepends=True,
-                    ))
+                ['config', '--get', '--null', '%s.%s' % (self.section, name)],
+                env=self.env, keepends=True,
+                ))
             assert len(values) == 1
             return values[0]
         except CommandError:
@@ -449,9 +528,14 @@ class Config(object):
             env=self.env,
             )
 
-    def has_key(self, name):
+    def __contains__(self, name):
         return self.get_all(name, default=None) is not None
 
+    # We don't use this method anymore internally, but keep it here in
+    # case somebody is calling it from their own code:
+    def has_key(self, name):
+        return name in self
+
     def unset_all(self, name):
         try:
             read_git_output(
@@ -579,7 +663,7 @@ class Change(object):
         self._values = None
 
     def _compute_values(self):
-        """Return a dictionary {keyword : expansion} for this Change.
+        """Return a dictionary {keyword: expansion} for this Change.
 
         Derived classes overload this method to add more entries to
         the return value.  This method is used internally by
@@ -589,7 +673,7 @@ class Change(object):
         return self.environment.get_values()
 
     def get_values(self, **extra_values):
-        """Return a dictionary {keyword : expansion} for this Change.
+        """Return a dictionary {keyword: expansion} for this Change.
 
         Return a dictionary mapping keywords to the values that they
         should be expanded to for this Change (used when interpolating
@@ -636,7 +720,7 @@ class Change(object):
                 value = value % values
             except KeyError, e:
                 if DEBUG:
-                    sys.stderr.write(
+                    self.environment.log_warning(
                         'Warning: unknown variable %r in the following line; line skipped:\n'
                         '    %s\n'
                         % (e.args[0], line,)
@@ -711,6 +795,8 @@ class Change(object):
 class Revision(Change):
     """A Change consisting of a single git commit."""
 
+    CC_RE = re.compile(r'^\s*C[Cc]:\s*(?P<to>[^#]+@[^\s#]*)\s*(#.*)?$')
+
     def __init__(self, reference_change, rev, num, tot):
         Change.__init__(self, reference_change.environment)
         self.reference_change = reference_change
@@ -722,6 +808,24 @@ class Revision(Change):
         self.author = read_git_output(['log', '--no-walk', '--format=%aN <%aE>', self.rev.sha1])
         self.recipients = self.environment.get_revision_recipients(self)
 
+        self.cc_recipients = ''
+        if self.environment.get_scancommitforcc():
+            self.cc_recipients = ', '.join(to.strip() for to in self._cc_recipients())
+            if self.cc_recipients:
+                self.environment.log_msg(
+                    'Add %s to CC for %s\n' % (self.cc_recipients, self.rev.sha1))
+
+    def _cc_recipients(self):
+        cc_recipients = []
+        message = read_git_output(['log', '--no-walk', '--format=%b', self.rev.sha1])
+        lines = message.strip().split('\n')
+        for line in lines:
+            m = re.match(self.CC_RE, line)
+            if m:
+                cc_recipients.append(m.group('to'))
+
+        return cc_recipients
+
     def _compute_values(self):
         values = Change._compute_values(self)
 
@@ -739,6 +843,8 @@ class Revision(Change):
         values['num'] = self.num
         values['tot'] = self.tot
         values['recipients'] = self.recipients
+        if self.cc_recipients:
+            values['cc_recipients'] = self.cc_recipients
         values['oneline'] = oneline
         values['author'] = self.author
 
@@ -750,8 +856,8 @@ class Revision(Change):
 
     def generate_email_header(self, **extra_values):
         for line in self.expand_header_lines(
-            REVISION_HEADER_TEMPLATE, **extra_values
-            ):
+                REVISION_HEADER_TEMPLATE, **extra_values
+                ):
             yield line
 
     def generate_email_intro(self):
@@ -822,26 +928,26 @@ class ReferenceChange(Change):
                 klass = BranchChange
             elif area == 'remotes':
                 # Tracking branch:
-                sys.stderr.write(
+                environment.log_warning(
                     '*** Push-update of tracking branch %r\n'
                     '***  - incomplete email generated.\n'
-                     % (refname,)
+                    % (refname,)
                     )
                 klass = OtherReferenceChange
             else:
                 # Some other reference namespace:
-                sys.stderr.write(
+                environment.log_warning(
                     '*** Push-update of strange reference %r\n'
                     '***  - incomplete email generated.\n'
-                     % (refname,)
+                    % (refname,)
                     )
                 klass = OtherReferenceChange
         else:
             # Anything else (is there anything else?)
-            sys.stderr.write(
+            environment.log_warning(
                 '*** Unknown type of update to %r (%s)\n'
                 '***  - incomplete email generated.\n'
-                 % (refname, rev.type,)
+                % (refname, rev.type,)
                 )
             klass = OtherReferenceChange
 
@@ -854,9 +960,9 @@ class ReferenceChange(Change):
     def __init__(self, environment, refname, short_refname, old, new, rev):
         Change.__init__(self, environment)
         self.change_type = {
-            (False, True) : 'create',
-            (True, True) : 'update',
-            (True, False) : 'delete',
+            (False, True): 'create',
+            (True, True): 'update',
+            (True, False): 'delete',
             }[bool(old), bool(new)]
         self.refname = refname
         self.short_refname = short_refname
@@ -865,10 +971,16 @@ class ReferenceChange(Change):
         self.rev = rev
         self.msgid = make_msgid()
         self.diffopts = environment.diffopts
+        self.graphopts = environment.graphopts
         self.logopts = environment.logopts
         self.commitlogopts = environment.commitlogopts
+        self.showgraph = environment.refchange_showgraph
         self.showlog = environment.refchange_showlog
 
+        self.header_template = REFCHANGE_HEADER_TEMPLATE
+        self.intro_template = REFCHANGE_INTRO_TEMPLATE
+        self.footer_template = FOOTER_TEMPLATE
+
     def _compute_values(self):
         values = Change._compute_values(self)
 
@@ -894,11 +1006,39 @@ class ReferenceChange(Change):
 
         return values
 
+    def send_single_combined_email(self, known_added_sha1s):
+        """Determine if a combined refchange/revision email should be sent
+
+        If there is only a single new (non-merge) commit added by a
+        change, it is useful to combine the ReferenceChange and
+        Revision emails into one.  In such a case, return the single
+        revision; otherwise, return None.
+
+        This method is overridden in BranchChange."""
+
+        return None
+
+    def generate_combined_email(self, push, revision, body_filter=None, extra_header_values={}):
+        """Generate an email describing this change AND specified revision.
+
+        Iterate over the lines (including the header lines) of an
+        email describing this change.  If body_filter is not None,
+        then use it to filter the lines that are intended for the
+        email body.
+
+        The extra_header_values field is received as a dict and not as
+        **kwargs, to allow passing other keyword arguments in the
+        future (e.g. passing extra values to generate_email_intro()
+
+        This method is overridden in BranchChange."""
+
+        raise NotImplementedError
+
     def get_subject(self):
         template = {
-            'create' : REF_CREATED_SUBJECT_TEMPLATE,
-            'update' : REF_UPDATED_SUBJECT_TEMPLATE,
-            'delete' : REF_DELETED_SUBJECT_TEMPLATE,
+            'create': REF_CREATED_SUBJECT_TEMPLATE,
+            'update': REF_UPDATED_SUBJECT_TEMPLATE,
+            'delete': REF_DELETED_SUBJECT_TEMPLATE,
             }[self.change_type]
         return self.expand(template)
 
@@ -907,12 +1047,12 @@ class ReferenceChange(Change):
             extra_values['subject'] = self.get_subject()
 
         for line in self.expand_header_lines(
-            REFCHANGE_HEADER_TEMPLATE, **extra_values
-            ):
+                self.header_template, **extra_values
+                ):
             yield line
 
     def generate_email_intro(self):
-        for line in self.expand_lines(REFCHANGE_INTRO_TEMPLATE):
+        for line in self.expand_lines(self.intro_template):
             yield line
 
     def generate_email_body(self, push):
@@ -922,9 +1062,9 @@ class ReferenceChange(Change):
         generate_update_summary() / generate_delete_summary()."""
 
         change_summary = {
-            'create' : self.generate_create_summary,
-            'delete' : self.generate_delete_summary,
-            'update' : self.generate_update_summary,
+            'create': self.generate_create_summary,
+            'delete': self.generate_delete_summary,
+            'update': self.generate_update_summary,
             }[self.change_type](push)
         for line in change_summary:
             yield line
@@ -933,7 +1073,23 @@ class ReferenceChange(Change):
             yield line
 
     def generate_email_footer(self):
-        return self.expand_lines(FOOTER_TEMPLATE)
+        return self.expand_lines(self.footer_template)
+
+    def generate_revision_change_graph(self, push):
+        if self.showgraph:
+            args = ['--graph'] + self.graphopts
+            for newold in ('new', 'old'):
+                has_newold = False
+                spec = push.get_commits_spec(newold, self)
+                for line in git_log(spec, args=args, keepends=True):
+                    if not has_newold:
+                        has_newold = True
+                        yield '\n'
+                        yield 'Graph of %s commits:\n\n' % (
+                            {'new': 'new', 'old': 'discarded'}[newold],)
+                    yield '  ' + line
+                if has_newold:
+                    yield '\n'
 
     def generate_revision_change_log(self, new_commits_list):
         if self.showlog:
@@ -945,9 +1101,17 @@ class ReferenceChange(Change):
                     + new_commits_list
                     + ['--'],
                     keepends=True,
-                ):
+                    ):
                 yield line
 
+    def generate_new_revision_summary(self, tot, new_commits_list, push):
+        for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=tot):
+            yield line
+        for line in self.generate_revision_change_graph(push):
+            yield line
+        for line in self.generate_revision_change_log(new_commits_list):
+            yield line
+
     def generate_revision_change_summary(self, push):
         """Generate a summary of the revisions added/removed by this change."""
 
@@ -960,7 +1124,7 @@ class ReferenceChange(Change):
             sha1s.reverse()
             tot = len(sha1s)
             new_revisions = [
-                Revision(self, GitObject(sha1), num=i+1, tot=tot)
+                Revision(self, GitObject(sha1), num=i + 1, tot=tot)
                 for (i, sha1) in enumerate(sha1s)
                 ]
 
@@ -973,9 +1137,8 @@ class ReferenceChange(Change):
                         BRIEF_SUMMARY_TEMPLATE, action='new', text=subject,
                         )
                 yield '\n'
-                for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=tot):
-                    yield line
-                for line in self.generate_revision_change_log([r.rev.sha1 for r in new_revisions]):
+                for line in self.generate_new_revision_summary(
+                        tot, [r.rev.sha1 for r in new_revisions], push):
                     yield line
             else:
                 for line in self.expand_lines(NO_NEW_REVISIONS_TEMPLATE):
@@ -993,16 +1156,16 @@ class ReferenceChange(Change):
             # revisions in the summary even though we will not send
             # new notification emails for them.
             adds = list(generate_summaries(
-                    '--topo-order', '--reverse', '%s..%s'
-                    % (self.old.commit_sha1, self.new.commit_sha1,)
-                    ))
+                '--topo-order', '--reverse', '%s..%s'
+                % (self.old.commit_sha1, self.new.commit_sha1,)
+                ))
 
             # List of the revisions that were removed from the branch
             # by this update.  This will be empty except for
             # non-fast-forward updates.
             discards = list(generate_summaries(
-                    '%s..%s' % (self.new.commit_sha1, self.old.commit_sha1,)
-                    ))
+                '%s..%s' % (self.new.commit_sha1, self.old.commit_sha1,)
+                ))
 
             if adds:
                 new_commits_list = push.get_new_commits(self)
@@ -1071,13 +1234,14 @@ class ReferenceChange(Change):
             yield '\n'
 
             if new_commits:
-                for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=len(new_commits)):
-                    yield line
-                for line in self.generate_revision_change_log(new_commits_list):
+                for line in self.generate_new_revision_summary(
+                        len(new_commits), new_commits_list, push):
                     yield line
             else:
                 for line in self.expand_lines(NO_NEW_REVISIONS_TEMPLATE):
                     yield line
+                for line in self.generate_revision_change_graph(push):
+                    yield line
 
             # The diffstat is shown from the old revision to the new
             # revision.  This is to show the truth of what happened in
@@ -1089,11 +1253,11 @@ class ReferenceChange(Change):
             yield '\n'
             yield 'Summary of changes:\n'
             for line in read_git_lines(
-                ['diff-tree']
-                + self.diffopts
-                + ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)],
-                keepends=True,
-                ):
+                    ['diff-tree']
+                    + self.diffopts
+                    + ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)],
+                    keepends=True,
+                    ):
                 yield line
 
         elif self.old.commit_sha1 and not self.new.commit_sha1:
@@ -1103,7 +1267,7 @@ class ReferenceChange(Change):
             sha1s = list(push.get_discarded_commits(self))
             tot = len(sha1s)
             discarded_revisions = [
-                Revision(self, GitObject(sha1), num=i+1, tot=tot)
+                Revision(self, GitObject(sha1), num=i + 1, tot=tot)
                 for (i, sha1) in enumerate(sha1s)
                 ]
 
@@ -1116,6 +1280,8 @@ class ReferenceChange(Change):
                     yield r.expand(
                         BRIEF_SUMMARY_TEMPLATE, action='discards', text=subject,
                         )
+                for line in self.generate_revision_change_graph(push):
+                    yield line
             else:
                 for line in self.expand_lines(NO_DISCARDED_REVISIONS_TEMPLATE):
                     yield line
@@ -1161,6 +1327,150 @@ class BranchChange(ReferenceChange):
             old=old, new=new, rev=rev,
             )
         self.recipients = environment.get_refchange_recipients(self)
+        self._single_revision = None
+
+    def send_single_combined_email(self, known_added_sha1s):
+        if not self.environment.combine_when_single_commit:
+            return None
+
+        # In the sadly-all-too-frequent usecase of people pushing only
+        # one of their commits at a time to a repository, users feel
+        # the reference change summary emails are noise rather than
+        # important signal.  This is because, in this particular
+        # usecase, there is a reference change summary email for each
+        # new commit, and all these summaries do is point out that
+        # there is one new commit (which can readily be inferred by
+        # the existence of the individual revision email that is also
+        # sent).  In such cases, our users prefer there to be a combined
+        # reference change summary/new revision email.
+        #
+        # So, if the change is an update and it doesn't discard any
+        # commits, and it adds exactly one non-merge commit (gerrit
+        # forces a workflow where every commit is individually merged
+        # and the git-multimail hook fired off for just this one
+        # change), then we send a combined refchange/revision email.
+        try:
+            # If this change is a reference update that doesn't discard
+            # any commits...
+            if self.change_type != 'update':
+                return None
+
+            if read_git_lines(
+                    ['merge-base', self.old.sha1, self.new.sha1]
+                    ) != [self.old.sha1]:
+                return None
+
+            # Check if this update introduced exactly one non-merge
+            # commit:
+
+            def split_line(line):
+                """Split line into (sha1, [parent,...])."""
+
+                words = line.split()
+                return (words[0], words[1:])
+
+            # Get the new commits introduced by the push as a list of
+            # (sha1, [parent,...])
+            new_commits = [
+                split_line(line)
+                for line in read_git_lines(
+                    [
+                        'log', '-3', '--format=%H %P',
+                        '%s..%s' % (self.old.sha1, self.new.sha1),
+                        ]
+                    )
+                ]
+
+            if not new_commits:
+                return None
+
+            # If the newest commit is a merge, save it for a later check
+            # but otherwise ignore it
+            merge = None
+            tot = len(new_commits)
+            if len(new_commits[0][1]) > 1:
+                merge = new_commits[0][0]
+                del new_commits[0]
+
+            # Our primary check: we can't combine if more than one commit
+            # is introduced.  We also currently only combine if the new
+            # commit is a non-merge commit, though it may make sense to
+            # combine if it is a merge as well.
+            if not (
+                    len(new_commits) == 1
+                    and len(new_commits[0][1]) == 1
+                    and new_commits[0][0] in known_added_sha1s
+                    ):
+                return None
+
+            # We do not want to combine revision and refchange emails if
+            # those go to separate locations.
+            rev = Revision(self, GitObject(new_commits[0][0]), 1, tot)
+            if rev.recipients != self.recipients:
+                return None
+
+            # We ignored the newest commit if it was just a merge of the one
+            # commit being introduced.  But we don't want to ignore that
+            # merge commit it it involved conflict resolutions.  Check that.
+            if merge and merge != read_git_output(['diff-tree', '--cc', merge]):
+                return None
+
+            # We can combine the refchange and one new revision emails
+            # into one.  Return the Revision that a combined email should
+            # be sent about.
+            return rev
+        except CommandError:
+            # Cannot determine number of commits in old..new or new..old;
+            # don't combine reference/revision emails:
+            return None
+
+    def generate_combined_email(self, push, revision, body_filter=None, extra_header_values={}):
+        values = revision.get_values()
+        if extra_header_values:
+            values.update(extra_header_values)
+        if 'subject' not in extra_header_values:
+            values['subject'] = self.expand(COMBINED_REFCHANGE_REVISION_SUBJECT_TEMPLATE, **values)
+
+        self._single_revision = revision
+        self.header_template = COMBINED_HEADER_TEMPLATE
+        self.intro_template = COMBINED_INTRO_TEMPLATE
+        self.footer_template = COMBINED_FOOTER_TEMPLATE
+        for line in self.generate_email(push, body_filter, values):
+            yield line
+
+    def generate_email_body(self, push):
+        '''Call the appropriate body generation routine.
+
+        If this is a combined refchange/revision email, the special logic
+        for handling this combined email comes from this function.  For
+        other cases, we just use the normal handling.'''
+
+        # If self._single_revision isn't set; don't override
+        if not self._single_revision:
+            for line in super(BranchChange, self).generate_email_body(push):
+                yield line
+            return
+
+        # This is a combined refchange/revision email; we first provide
+        # some info from the refchange portion, and then call the revision
+        # generate_email_body function to handle the revision portion.
+        adds = list(generate_summaries(
+            '--topo-order', '--reverse', '%s..%s'
+            % (self.old.commit_sha1, self.new.commit_sha1,)
+            ))
+
+        yield self.expand("The following commit(s) were added to %(refname)s by this push:\n")
+        for (sha1, subject) in adds:
+            yield self.expand(
+                BRIEF_SUMMARY_TEMPLATE, action='new',
+                rev_short=sha1, text=subject,
+                )
+
+        yield self._single_revision.rev.short + " is described below\n"
+        yield '\n'
+
+        for line in self._single_revision.generate_email_body(push):
+            yield line
 
 
 class AnnotatedTagChange(ReferenceChange):
@@ -1390,13 +1700,17 @@ class SendMailer(Mailer):
             sys.exit(1)
         try:
             p.stdin.writelines(lines)
-        except:
+        except Exception, e:
             sys.stderr.write(
                 '*** Error while generating commit email\n'
                 '***  - mail sending aborted.\n'
                 )
-            p.terminate()
-            raise
+            try:
+                # subprocess.terminate() is not available in Python 2.4
+                p.terminate()
+            except AttributeError:
+                pass
+            raise e
         else:
             p.stdin.close()
             retcode = p.wait()
@@ -1407,34 +1721,72 @@ class SendMailer(Mailer):
 class SMTPMailer(Mailer):
     """Send emails using Python's smtplib."""
 
-    def __init__(self, envelopesender, smtpserver):
+    def __init__(self, envelopesender, smtpserver,
+                 smtpservertimeout=10.0, smtpserverdebuglevel=0,
+                 smtpencryption='none',
+                 smtpuser='', smtppass='',
+                 ):
         if not envelopesender:
             sys.stderr.write(
                 'fatal: git_multimail: cannot use SMTPMailer without a sender address.\n'
                 'please set either multimailhook.envelopeSender or user.email\n'
                 )
             sys.exit(1)
+        if smtpencryption == 'ssl' and not (smtpuser and smtppass):
+            raise ConfigurationException(
+                'Cannot use SMTPMailer with security option ssl '
+                'without options username and password.'
+                )
         self.envelopesender = envelopesender
         self.smtpserver = smtpserver
+        self.smtpservertimeout = smtpservertimeout
+        self.smtpserverdebuglevel = smtpserverdebuglevel
+        self.security = smtpencryption
+        self.username = smtpuser
+        self.password = smtppass
         try:
-            self.smtp = smtplib.SMTP(self.smtpserver)
+            if self.security == 'none':
+                self.smtp = smtplib.SMTP(self.smtpserver, timeout=self.smtpservertimeout)
+            elif self.security == 'ssl':
+                self.smtp = smtplib.SMTP_SSL(self.smtpserver, timeout=self.smtpservertimeout)
+            elif self.security == 'tls':
+                if ':' not in self.smtpserver:
+                    self.smtpserver += ':587'  # default port for TLS
+                self.smtp = smtplib.SMTP(self.smtpserver, timeout=self.smtpservertimeout)
+                self.smtp.ehlo()
+                self.smtp.starttls()
+                self.smtp.ehlo()
+            else:
+                sys.stdout.write('*** Error: Control reached an invalid option. ***')
+                sys.exit(1)
+            if self.smtpserverdebuglevel > 0:
+                sys.stdout.write(
+                    "*** Setting debug on for SMTP server connection (%s) ***\n"
+                    % self.smtpserverdebuglevel)
+                self.smtp.set_debuglevel(self.smtpserverdebuglevel)
         except Exception, e:
-            sys.stderr.write('*** Error establishing SMTP connection to %s***\n' % self.smtpserver)
+            sys.stderr.write(
+                '*** Error establishing SMTP connection to %s ***\n'
+                % self.smtpserver)
             sys.stderr.write('*** %s\n' % str(e))
             sys.exit(1)
 
     def __del__(self):
-        self.smtp.quit()
+        if hasattr(self, 'smtp'):
+            self.smtp.quit()
 
     def send(self, lines, to_addrs):
         try:
+            if self.username or self.password:
+                sys.stderr.write("*** Authenticating as %s ***\n" % self.username)
+                self.smtp.login(self.username, self.password)
             msg = ''.join(lines)
             # turn comma-separated list into Python list if needed.
             if isinstance(to_addrs, basestring):
                 to_addrs = [email for (name, email) in getaddresses([to_addrs])]
             self.smtp.sendmail(self.envelopesender, to_addrs, msg)
         except Exception, e:
-            sys.stderr.write('*** Error sending email***\n')
+            sys.stderr.write('*** Error sending email ***\n')
             sys.stderr.write('*** %s\n' % str(e))
             self.smtp.quit()
             sys.exit(1)
@@ -1549,6 +1901,10 @@ class Environment(object):
 
             True iff announce emails should include a shortlog.
 
+        refchange_showgraph (bool)
+
+            True iff refchanges emails should include a detailed graph.
+
         refchange_showlog (bool)
 
             True iff refchanges emails should include a detailed log.
@@ -1559,6 +1915,12 @@ class Environment(object):
             summary email.  The value should be a list of strings
             representing words to be passed to the command.
 
+        graphopts (list of strings)
+
+            Analogous to diffopts, but contains options passed to
+            'git log --graph' when generating the detailed graph for
+            a set of commits (see refchange_showgraph)
+
         logopts (list of strings)
 
             Analogous to diffopts, but contains options passed to
@@ -1571,6 +1933,17 @@ class Environment(object):
             commit mail.  The value should be a list of strings
             representing words to be passed to the command.
 
+        quiet (bool)
+            On success do not write to stderr
+
+        stdout (bool)
+            Write email to stdout rather than emailing. Useful for debugging
+
+        combine_when_single_commit (bool)
+
+            True if a combined email should be produced when a single
+            new commit is pushed to a branch, False otherwise.
+
     """
 
     REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\.git)$')
@@ -1580,9 +1953,14 @@ class Environment(object):
         self.announce_show_shortlog = False
         self.maxcommitemails = 500
         self.diffopts = ['--stat', '--summary', '--find-copies-harder']
+        self.graphopts = ['--oneline', '--decorate']
         self.logopts = []
+        self.refchange_showgraph = False
         self.refchange_showlog = False
         self.commitlogopts = ['-C', '--stat', '-p', '--cc']
+        self.quiet = False
+        self.stdout = False
+        self.combine_when_single_commit = True
 
         self.COMPUTED_KEYS = [
             'administrator',
@@ -1614,6 +1992,14 @@ class Environment(object):
     def get_pusher_email(self):
         return None
 
+    def get_fromaddr(self):
+        config = Config('user')
+        fromname = config.get('name', default='')
+        fromemail = config.get('email', default='')
+        if fromemail:
+            return formataddr([fromname, fromemail])
+        return self.get_sender()
+
     def get_administrator(self):
         return 'the administrator of this repository'
 
@@ -1631,7 +2017,7 @@ class Environment(object):
         return CHARSET
 
     def get_values(self):
-        """Return a dictionary {keyword : expansion} for this Environment.
+        """Return a dictionary {keyword: expansion} for this Environment.
 
         This method is called by Change._compute_values().  The keys
         in the returned dictionary are available to be used in any of
@@ -1699,6 +2085,24 @@ class Environment(object):
 
         return lines
 
+    def log_msg(self, msg):
+        """Write the string msg on a log file or on stderr.
+
+        Sends the text to stderr by default, override to change the behavior."""
+        sys.stderr.write(msg)
+
+    def log_warning(self, msg):
+        """Write the string msg on a log file or on stderr.
+
+        Sends the text to stderr by default, override to change the behavior."""
+        sys.stderr.write(msg)
+
+    def log_error(self, msg):
+        """Write the string msg on a log file or on stderr.
+
+        Sends the text to stderr by default, override to change the behavior."""
+        sys.stderr.write(msg)
+
 
 class ConfigEnvironmentMixin(Environment):
     """A mixin that sets self.config to its constructor's config argument.
@@ -1723,20 +2127,23 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
             config=config, **kw
             )
 
-        self.announce_show_shortlog = config.get_bool(
-            'announceshortlog', default=self.announce_show_shortlog
-            )
-
-        self.refchange_showlog = config.get_bool(
-            'refchangeshowlog', default=self.refchange_showlog
-            )
+        for var, cfg in (
+                ('announce_show_shortlog', 'announceshortlog'),
+                ('refchange_showgraph', 'refchangeShowGraph'),
+                ('refchange_showlog', 'refchangeshowlog'),
+                ('quiet', 'quiet'),
+                ('stdout', 'stdout'),
+                ):
+            val = config.get_bool(cfg)
+            if val is not None:
+                setattr(self, var, val)
 
         maxcommitemails = config.get('maxcommitemails')
         if maxcommitemails is not None:
             try:
                 self.maxcommitemails = int(maxcommitemails)
             except ValueError:
-                sys.stderr.write(
+                self.log_warning(
                     '*** Malformed value for multimailhook.maxCommitEmails: %s\n' % maxcommitemails
                     + '*** Expected a number.  Ignoring.\n'
                     )
@@ -1745,6 +2152,10 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
         if diffopts is not None:
             self.diffopts = shlex.split(diffopts)
 
+        graphopts = config.get('graphOpts')
+        if graphopts is not None:
+            self.graphopts = shlex.split(graphopts)
+
         logopts = config.get('logopts')
         if logopts is not None:
             self.logopts = shlex.split(logopts)
@@ -1756,14 +2167,18 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
         reply_to = config.get('replyTo')
         self.__reply_to_refchange = config.get('replyToRefchange', default=reply_to)
         if (
-            self.__reply_to_refchange is not None
-            and self.__reply_to_refchange.lower() == 'author'
-            ):
+                self.__reply_to_refchange is not None
+                and self.__reply_to_refchange.lower() == 'author'
+                ):
             raise ConfigurationException(
                 '"author" is not an allowed setting for replyToRefchange'
                 )
         self.__reply_to_commit = config.get('replyToCommit', default=reply_to)
 
+        combine = config.get_bool('combineWhenSingleCommit')
+        if combine is not None:
+            self.combine_when_single_commit = combine
+
     def get_administrator(self):
         return (
             self.config.get('administrator')
@@ -1779,8 +2194,12 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
 
     def get_emailprefix(self):
         emailprefix = self.config.get('emailprefix')
-        if emailprefix and emailprefix.strip():
-            return emailprefix.strip() + ' '
+        if emailprefix is not None:
+            emailprefix = emailprefix.strip()
+            if emailprefix:
+                return emailprefix + ' '
+            else:
+                return ''
         else:
             return '[%s] ' % (self.get_repo_shortname(),)
 
@@ -1791,14 +2210,7 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
         fromaddr = self.config.get('from')
         if fromaddr:
             return fromaddr
-        else:
-            config = Config('user')
-            fromname = config.get('name', default='')
-            fromemail = config.get('email', default='')
-            if fromemail:
-                return formataddr([fromname, fromemail])
-            else:
-                return self.get_sender()
+        return super(ConfigOptionsEnvironmentMixin, self).get_fromaddr()
 
     def get_reply_to_refchange(self, refchange):
         if self.__reply_to_refchange is None:
@@ -1814,7 +2226,7 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
         if self.__reply_to_commit is None:
             return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_commit(revision)
         elif self.__reply_to_commit.lower() == 'author':
-            return revision.get_author()
+            return revision.author
         elif self.__reply_to_commit.lower() == 'pusher':
             return self.get_pusher_email()
         elif self.__reply_to_commit.lower() == 'none':
@@ -1822,6 +2234,9 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
         else:
             return self.__reply_to_commit
 
+    def get_scancommitforcc(self):
+        return self.config.get('scancommitforcc')
+
 
 class FilterLinesEnvironmentMixin(Environment):
     """Handle encoding and maximum line length of body lines.
@@ -1862,9 +2277,9 @@ class FilterLinesEnvironmentMixin(Environment):
 
 
 class ConfigFilterLinesEnvironmentMixin(
-    ConfigEnvironmentMixin,
-    FilterLinesEnvironmentMixin,
-    ):
+        ConfigEnvironmentMixin,
+        FilterLinesEnvironmentMixin,
+        ):
     """Handle encoding and maximum line length based on config."""
 
     def __init__(self, config, **kw):
@@ -1896,9 +2311,9 @@ class MaxlinesEnvironmentMixin(Environment):
 
 
 class ConfigMaxlinesEnvironmentMixin(
-    ConfigEnvironmentMixin,
-    MaxlinesEnvironmentMixin,
-    ):
+        ConfigEnvironmentMixin,
+        MaxlinesEnvironmentMixin,
+        ):
     """Limit the email body to the number of lines specified in config."""
 
     def __init__(self, config, **kw):
@@ -1927,9 +2342,9 @@ class FQDNEnvironmentMixin(Environment):
 
 
 class ConfigFQDNEnvironmentMixin(
-    ConfigEnvironmentMixin,
-    FQDNEnvironmentMixin,
-    ):
+        ConfigEnvironmentMixin,
+        FQDNEnvironmentMixin,
+        ):
     """Read the FQDN from the config."""
 
     def __init__(self, config, **kw):
@@ -1970,10 +2385,10 @@ class StaticRecipientsEnvironmentMixin(Environment):
     """Set recipients statically based on constructor parameters."""
 
     def __init__(
-        self,
-        refchange_recipients, announce_recipients, revision_recipients,
-        **kw
-        ):
+            self,
+            refchange_recipients, announce_recipients, revision_recipients, scancommitforcc,
+            **kw
+            ):
         super(StaticRecipientsEnvironmentMixin, self).__init__(**kw)
 
         # The recipients for various types of notification emails, as
@@ -1985,7 +2400,8 @@ class StaticRecipientsEnvironmentMixin(Environment):
         # compute them once and for all:
         if not (refchange_recipients
                 or announce_recipients
-                or revision_recipients):
+                or revision_recipients
+                or scancommitforcc):
             raise ConfigurationException('No email recipients configured!')
         self.__refchange_recipients = refchange_recipients
         self.__announce_recipients = announce_recipients
@@ -2002,9 +2418,9 @@ class StaticRecipientsEnvironmentMixin(Environment):
 
 
 class ConfigRecipientsEnvironmentMixin(
-    ConfigEnvironmentMixin,
-    StaticRecipientsEnvironmentMixin
-    ):
+        ConfigEnvironmentMixin,
+        StaticRecipientsEnvironmentMixin
+        ):
     """Determine recipients statically based on config."""
 
     def __init__(self, config, **kw):
@@ -2019,6 +2435,7 @@ class ConfigRecipientsEnvironmentMixin(
             revision_recipients=self._get_recipients(
                 config, 'commitlist', 'mailinglist',
                 ),
+            scancommitforcc=config.get('scancommitforcc'),
             **kw
             )
 
@@ -2067,20 +2484,20 @@ class ProjectdescEnvironmentMixin(Environment):
 
 class GenericEnvironmentMixin(Environment):
     def get_pusher(self):
-        return self.osenv.get('USER', 'unknown user')
+        return self.osenv.get('USER', self.osenv.get('USERNAME', 'unknown user'))
 
 
 class GenericEnvironment(
-    ProjectdescEnvironmentMixin,
-    ConfigMaxlinesEnvironmentMixin,
-    ComputeFQDNEnvironmentMixin,
-    ConfigFilterLinesEnvironmentMixin,
-    ConfigRecipientsEnvironmentMixin,
-    PusherDomainEnvironmentMixin,
-    ConfigOptionsEnvironmentMixin,
-    GenericEnvironmentMixin,
-    Environment,
-    ):
+        ProjectdescEnvironmentMixin,
+        ConfigMaxlinesEnvironmentMixin,
+        ComputeFQDNEnvironmentMixin,
+        ConfigFilterLinesEnvironmentMixin,
+        ConfigRecipientsEnvironmentMixin,
+        PusherDomainEnvironmentMixin,
+        ConfigOptionsEnvironmentMixin,
+        GenericEnvironmentMixin,
+        Environment,
+        ):
     pass
 
 
@@ -2097,6 +2514,45 @@ class GitoliteEnvironmentMixin(Environment):
     def get_pusher(self):
         return self.osenv.get('GL_USER', 'unknown user')
 
+    def get_fromaddr(self):
+        GL_USER = self.osenv.get('GL_USER')
+        if GL_USER is not None:
+            # Find the path to gitolite.conf.  Note that gitolite v3
+            # did away with the GL_ADMINDIR and GL_CONF environment
+            # variables (they are now hard-coded).
+            GL_ADMINDIR = self.osenv.get(
+                'GL_ADMINDIR',
+                os.path.expanduser(os.path.join('~', '.gitolite')))
+            GL_CONF = self.osenv.get(
+                'GL_CONF',
+                os.path.join(GL_ADMINDIR, 'conf', 'gitolite.conf'))
+            if os.path.isfile(GL_CONF):
+                f = open(GL_CONF, 'rU')
+                try:
+                    in_user_emails_section = False
+                    re_template = r'^\s*#\s*{}\s*$'
+                    re_begin, re_user, re_end = (
+                        re.compile(re_template.format(x))
+                        for x in (
+                            r'BEGIN\s+USER\s+EMAILS',
+                            re.escape(GL_USER) + r'\s+(.*)',
+                            r'END\s+USER\s+EMAILS',
+                            ))
+                    for l in f:
+                        l = l.rstrip('\n')
+                        if not in_user_emails_section:
+                            if re_begin.match(l):
+                                in_user_emails_section = True
+                            continue
+                        if re_end.match(l):
+                            break
+                        m = re_user.match(l)
+                        if m:
+                            return m.group(1)
+                finally:
+                    f.close()
+        return super(GitoliteEnvironmentMixin, self).get_fromaddr()
+
 
 class IncrementalDateTime(object):
     """Simple wrapper to give incremental date/times.
@@ -2116,16 +2572,16 @@ class IncrementalDateTime(object):
 
 
 class GitoliteEnvironment(
-    ProjectdescEnvironmentMixin,
-    ConfigMaxlinesEnvironmentMixin,
-    ComputeFQDNEnvironmentMixin,
-    ConfigFilterLinesEnvironmentMixin,
-    ConfigRecipientsEnvironmentMixin,
-    PusherDomainEnvironmentMixin,
-    ConfigOptionsEnvironmentMixin,
-    GitoliteEnvironmentMixin,
-    Environment,
-    ):
+        ProjectdescEnvironmentMixin,
+        ConfigMaxlinesEnvironmentMixin,
+        ComputeFQDNEnvironmentMixin,
+        ConfigFilterLinesEnvironmentMixin,
+        ConfigRecipientsEnvironmentMixin,
+        PusherDomainEnvironmentMixin,
+        ConfigOptionsEnvironmentMixin,
+        GitoliteEnvironmentMixin,
+        Environment,
+        ):
     pass
 
 
@@ -2149,9 +2605,9 @@ class Push(object):
     references.
 
     The first step is to determine the "other" references--those
-    unaffected by the current push.  They are computed by
-    Push._compute_other_ref_sha1s() by listing all references then
-    removing any affected by this push.
+    unaffected by the current push.  They are computed by listing all
+    references then removing any affected by this push.  The results
+    are stored in Push._other_ref_sha1s.
 
     The commits contained in the repository before this push were
 
@@ -2187,7 +2643,7 @@ class Push(object):
     possible and working with SHA1s thereafter (because SHA1s are
     immutable)."""
 
-    # A map {(changeclass, changetype) : integer} specifying the order
+    # A map {(changeclass, changetype): integer} specifying the order
     # that reference changes will be processed if multiple reference
     # changes are included in a single push.  The order is significant
     # mostly because new commit notifications are threaded together
@@ -2211,66 +2667,134 @@ class Push(object):
             ])
         )
 
-    def __init__(self, changes):
+    def __init__(self, changes, ignore_other_refs=False):
         self.changes = sorted(changes, key=self._sort_key)
+        self.__other_ref_sha1s = None
+        self.__cached_commits_spec = {}
 
-        # The SHA-1s of commits referred to by references unaffected
-        # by this push:
-        other_ref_sha1s = self._compute_other_ref_sha1s()
+        if ignore_other_refs:
+            self.__other_ref_sha1s = set()
 
-        self._old_rev_exclusion_spec = self._compute_rev_exclusion_spec(
-            other_ref_sha1s.union(
-                change.old.sha1
+    @classmethod
+    def _sort_key(klass, change):
+        return (klass.SORT_ORDER[change.__class__, change.change_type], change.refname,)
+
+    @property
+    def _other_ref_sha1s(self):
+        """The GitObjects referred to by references unaffected by this push.
+        """
+        if self.__other_ref_sha1s is None:
+            # The refnames being changed by this push:
+            updated_refs = set(
+                change.refname
                 for change in self.changes
-                if change.old.type in ['commit', 'tag']
                 )
-            )
-        self._new_rev_exclusion_spec = self._compute_rev_exclusion_spec(
-            other_ref_sha1s.union(
-                change.new.sha1
-                for change in self.changes
-                if change.new.type in ['commit', 'tag']
+
+            # The SHA-1s of commits referred to by all references in this
+            # repository *except* updated_refs:
+            sha1s = set()
+            fmt = (
+                '%(objectname) %(objecttype) %(refname)\n'
+                '%(*objectname) %(*objecttype) %(refname)'
                 )
-            )
+            for line in read_git_lines(
+                    ['for-each-ref', '--format=%s' % (fmt,)]):
+                (sha1, type, name) = line.split(' ', 2)
+                if sha1 and type == 'commit' and name not in updated_refs:
+                    sha1s.add(sha1)
 
-    @classmethod
-    def _sort_key(klass, change):
-        return (klass.SORT_ORDER[change.__class__, change.change_type], change.refname,)
+            self.__other_ref_sha1s = sha1s
+
+        return self.__other_ref_sha1s
+
+    def _get_commits_spec_incl(self, new_or_old, reference_change=None):
+        """Get new or old SHA-1 from one or each of the changed refs.
 
-    def _compute_other_ref_sha1s(self):
-        """Return the GitObjects referred to by references unaffected by this push."""
+        Return a list of SHA-1 commit identifier strings suitable as
+        arguments to 'git rev-list' (or 'git log' or ...).  The
+        returned identifiers are either the old or new values from one
+        or all of the changed references, depending on the values of
+        new_or_old and reference_change.
 
-        # The refnames being changed by this push:
-        updated_refs = set(
-            change.refname
+        new_or_old is either the string 'new' or the string 'old'.  If
+        'new', the returned SHA-1 identifiers are the new values from
+        each changed reference.  If 'old', the SHA-1 identifiers are
+        the old values from each changed reference.
+
+        If reference_change is specified and not None, only the new or
+        old reference from the specified reference is included in the
+        return value.
+
+        This function returns None if there are no matching revisions
+        (e.g., because a branch was deleted and new_or_old is 'new').
+        """
+
+        if not reference_change:
+            incl_spec = sorted(
+                getattr(change, new_or_old).sha1
+                for change in self.changes
+                if getattr(change, new_or_old)
+                )
+            if not incl_spec:
+                incl_spec = None
+        elif not getattr(reference_change, new_or_old).commit_sha1:
+            incl_spec = None
+        else:
+            incl_spec = [getattr(reference_change, new_or_old).commit_sha1]
+        return incl_spec
+
+    def _get_commits_spec_excl(self, new_or_old):
+        """Get exclusion revisions for determining new or discarded commits.
+
+        Return a list of strings suitable as arguments to 'git
+        rev-list' (or 'git log' or ...) that will exclude all
+        commits that, depending on the value of new_or_old, were
+        either previously in the repository (useful for determining
+        which commits are new to the repository) or currently in the
+        repository (useful for determining which commits were
+        discarded from the repository).
+
+        new_or_old is either the string 'new' or the string 'old'.  If
+        'new', the commits to be excluded are those that were in the
+        repository before the push.  If 'old', the commits to be
+        excluded are those that are currently in the repository.  """
+
+        old_or_new = {'old': 'new', 'new': 'old'}[new_or_old]
+        excl_revs = self._other_ref_sha1s.union(
+            getattr(change, old_or_new).sha1
             for change in self.changes
+            if getattr(change, old_or_new).type in ['commit', 'tag']
             )
+        return ['^' + sha1 for sha1 in sorted(excl_revs)]
 
-        # The SHA-1s of commits referred to by all references in this
-        # repository *except* updated_refs:
-        sha1s = set()
-        fmt = (
-            '%(objectname) %(objecttype) %(refname)\n'
-            '%(*objectname) %(*objecttype) %(refname)'
-            )
-        for line in read_git_lines(['for-each-ref', '--format=%s' % (fmt,)]):
-            (sha1, type, name) = line.split(' ', 2)
-            if sha1 and type == 'commit' and name not in updated_refs:
-                sha1s.add(sha1)
+    def get_commits_spec(self, new_or_old, reference_change=None):
+        """Get rev-list arguments for added or discarded commits.
 
-        return sha1s
+        Return a list of strings suitable as arguments to 'git
+        rev-list' (or 'git log' or ...) that select those commits
+        that, depending on the value of new_or_old, are either new to
+        the repository or were discarded from the repository.
 
-    def _compute_rev_exclusion_spec(self, sha1s):
-        """Return an exclusion specification for 'git rev-list'.
+        new_or_old is either the string 'new' or the string 'old'.  If
+        'new', the returned list is used to select commits that are
+        new to the repository.  If 'old', the returned value is used
+        to select the commits that have been discarded from the
+        repository.
 
-        git_objects is an iterable over GitObject instances.  Return a
-        string that can be passed to the standard input of 'git
-        rev-list --stdin' to exclude all of the commits referred to by
-        git_objects."""
+        If reference_change is specified and not None, the new or
+        discarded commits are limited to those that are reachable from
+        the new or old value of the specified reference.
 
-        return ''.join(
-            ['^%s\n' % (sha1,) for sha1 in sorted(sha1s)]
-            )
+        This function returns None if there are no added (or discarded)
+        revisions.
+        """
+        key = (new_or_old, reference_change)
+        if key not in self.__cached_commits_spec:
+            ret = self._get_commits_spec_incl(new_or_old, reference_change)
+            if ret is not None:
+                ret.extend(self._get_commits_spec_excl(new_or_old))
+            self.__cached_commits_spec[key] = ret
+        return self.__cached_commits_spec[key]
 
     def get_new_commits(self, reference_change=None):
         """Return a list of commits added by this push.
@@ -2280,19 +2804,8 @@ class Push(object):
         reference_change is None, then return a list of *all* commits
         added by this push."""
 
-        if not reference_change:
-            new_revs = sorted(
-                change.new.sha1
-                for change in self.changes
-                if change.new
-                )
-        elif not reference_change.new.commit_sha1:
-            return []
-        else:
-            new_revs = [reference_change.new.commit_sha1]
-
-        cmd = ['rev-list', '--stdin'] + new_revs
-        return read_git_lines(cmd, input=self._old_rev_exclusion_spec)
+        spec = self.get_commits_spec('new', reference_change)
+        return git_rev_list(spec)
 
     def get_discarded_commits(self, reference_change):
         """Return a list of commits discarded by this push.
@@ -2301,13 +2814,8 @@ class Push(object):
         entirely discarded from the repository by the part of this
         push represented by reference_change."""
 
-        if not reference_change.old.commit_sha1:
-            return []
-        else:
-            old_revs = [reference_change.old.commit_sha1]
-
-        cmd = ['rev-list', '--stdin'] + old_revs
-        return read_git_lines(cmd, input=self._new_rev_exclusion_spec)
+        spec = self.get_commits_spec('old', reference_change)
+        return git_rev_list(spec)
 
     def send_emails(self, mailer, body_filter=None):
         """Use send all of the notification emails needed for this push.
@@ -2325,30 +2833,43 @@ class Push(object):
         unhandled_sha1s = set(self.get_new_commits())
         send_date = IncrementalDateTime()
         for change in self.changes:
+            sha1s = []
+            for sha1 in reversed(list(self.get_new_commits(change))):
+                if sha1 in unhandled_sha1s:
+                    sha1s.append(sha1)
+                    unhandled_sha1s.remove(sha1)
+
             # Check if we've got anyone to send to
             if not change.recipients:
-                sys.stderr.write(
+                change.environment.log_warning(
                     '*** no recipients configured so no email will be sent\n'
                     '*** for %r update %s->%s\n'
                     % (change.refname, change.old.sha1, change.new.sha1,)
                     )
             else:
-                sys.stderr.write('Sending notification emails to: %s\n' % (change.recipients,))
-                extra_values = {'send_date' : send_date.next()}
-                mailer.send(
-                    change.generate_email(self, body_filter, extra_values),
-                    change.recipients,
-                    )
+                if not change.environment.quiet:
+                    change.environment.log_msg(
+                        'Sending notification emails to: %s\n' % (change.recipients,))
+                extra_values = {'send_date': send_date.next()}
 
-            sha1s = []
-            for sha1 in reversed(list(self.get_new_commits(change))):
-                if sha1 in unhandled_sha1s:
-                    sha1s.append(sha1)
-                    unhandled_sha1s.remove(sha1)
+                rev = change.send_single_combined_email(sha1s)
+                if rev:
+                    mailer.send(
+                        change.generate_combined_email(self, rev, body_filter, extra_values),
+                        rev.recipients,
+                        )
+                    # This change is now fully handled; no need to handle
+                    # individual revisions any further.
+                    continue
+                else:
+                    mailer.send(
+                        change.generate_email(self, body_filter, extra_values),
+                        change.recipients,
+                        )
 
             max_emails = change.environment.maxcommitemails
             if max_emails and len(sha1s) > max_emails:
-                sys.stderr.write(
+                change.environment.log_warning(
                     '*** Too many new commits (%d), not sending commit emails.\n' % len(sha1s)
                     + '*** Try setting multimailhook.maxCommitEmails to a greater value\n'
                     + '*** Currently, multimailhook.maxCommitEmails=%d\n' % max_emails
@@ -2356,9 +2877,13 @@ class Push(object):
                 return
 
             for (num, sha1) in enumerate(sha1s):
-                rev = Revision(change, GitObject(sha1), num=num+1, tot=len(sha1s))
+                rev = Revision(change, GitObject(sha1), num=num + 1, tot=len(sha1s))
+                if not rev.recipients and rev.cc_recipients:
+                    change.environment.log_msg('*** Replacing Cc: with To:\n')
+                    rev.recipients = rev.cc_recipients
+                    rev.cc_recipients = None
                 if rev.recipients:
-                    extra_values = {'send_date' : send_date.next()}
+                    extra_values = {'send_date': send_date.next()}
                     mailer.send(
                         rev.generate_email(self, body_filter, extra_values),
                         rev.recipients,
@@ -2366,7 +2891,7 @@ class Push(object):
 
         # Consistency check:
         if unhandled_sha1s:
-            sys.stderr.write(
+            change.environment.log_error(
                 'ERROR: No emails were sent for the following new commits:\n'
                 '    %s\n'
                 % ('\n    '.join(sorted(unhandled_sha1s)),)
@@ -2384,7 +2909,7 @@ def run_as_post_receive_hook(environment, mailer):
     push.send_emails(mailer, body_filter=environment.filter_body)
 
 
-def run_as_update_hook(environment, mailer, refname, oldrev, newrev):
+def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False):
     changes = [
         ReferenceChange.create(
             environment,
@@ -2393,7 +2918,7 @@ def run_as_update_hook(environment, mailer, refname, oldrev, newrev):
             refname,
             ),
         ]
-    push = Push(changes)
+    push = Push(changes, force_send)
     push.send_emails(mailer, body_filter=environment.filter_body)
 
 
@@ -2402,9 +2927,18 @@ def choose_mailer(config, environment):
 
     if mailer == 'smtp':
         smtpserver = config.get('smtpserver', default='localhost')
+        smtpservertimeout = float(config.get('smtpservertimeout', default=10.0))
+        smtpserverdebuglevel = int(config.get('smtpserverdebuglevel', default=0))
+        smtpencryption = config.get('smtpencryption', default='none')
+        smtpuser = config.get('smtpuser', default='')
+        smtppass = config.get('smtppass', default='')
         mailer = SMTPMailer(
             envelopesender=(environment.get_sender() or environment.get_fromaddr()),
-            smtpserver=smtpserver,
+            smtpserver=smtpserver, smtpservertimeout=smtpservertimeout,
+            smtpserverdebuglevel=smtpserverdebuglevel,
+            smtpencryption=smtpencryption,
+            smtpuser=smtpuser,
+            smtppass=smtppass,
             )
     elif mailer == 'sendmail':
         command = config.get('sendmailcommand')
@@ -2412,7 +2946,7 @@ def choose_mailer(config, environment):
             command = shlex.split(command)
         mailer = SendMailer(command=command, envelopesender=environment.get_sender())
     else:
-        sys.stderr.write(
+        environment.log_error(
             'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer
             + 'please use one of "smtp" or "sendmail".\n'
             )
@@ -2421,8 +2955,8 @@ def choose_mailer(config, environment):
 
 
 KNOWN_ENVIRONMENTS = {
-    'generic' : GenericEnvironmentMixin,
-    'gitolite' : GitoliteEnvironmentMixin,
+    'generic': GenericEnvironmentMixin,
+    'gitolite': GitoliteEnvironmentMixin,
     }
 
 
@@ -2439,8 +2973,8 @@ def choose_environment(config, osenv=None, env=None, recipients=None):
         ConfigOptionsEnvironmentMixin,
         ]
     environment_kw = {
-        'osenv' : osenv,
-        'config' : config,
+        'osenv': osenv,
+        'config': config,
         }
 
     if not env:
@@ -2459,6 +2993,7 @@ def choose_environment(config, osenv=None, env=None, recipients=None):
         environment_kw['refchange_recipients'] = recipients
         environment_kw['announce_recipients'] = recipients
         environment_kw['revision_recipients'] = recipients
+        environment_kw['scancommitforcc'] = config.get('scancommitforcc')
     else:
         environment_mixins.insert(0, ConfigRecipientsEnvironmentMixin)
 
@@ -2499,6 +3034,14 @@ def main(args):
             '(intended for debugging purposes).'
             ),
         )
+    parser.add_option(
+        '--force-send', action='store_true', default=False,
+        help=(
+            'Force sending refchange email when using as an update hook. '
+            'This is useful to work around the unreliable new commits '
+            'detection in this mode.'
+            ),
+        )
 
     (options, args) = parser.parse_args(args)
 
@@ -2513,11 +3056,11 @@ def main(args):
 
         if options.show_env:
             sys.stderr.write('Environment values:\n')
-            for (k,v) in sorted(environment.get_values().items()):
-                sys.stderr.write('    %s : %r\n' % (k,v))
+            for (k, v) in sorted(environment.get_values().items()):
+                sys.stderr.write('    %s : %r\n' % (k, v))
             sys.stderr.write('\n')
 
-        if options.stdout:
+        if options.stdout or environment.stdout:
             mailer = OutputMailer(sys.stdout)
         else:
             mailer = choose_mailer(config, environment)
@@ -2528,7 +3071,7 @@ def main(args):
             if len(args) != 3:
                 parser.error('Need zero or three non-option arguments')
             (refname, oldrev, newrev) = args
-            run_as_update_hook(environment, mailer, refname, oldrev, newrev)
+            run_as_update_hook(environment, mailer, refname, oldrev, newrev, options.force_send)
         else:
             run_as_post_receive_hook(environment, mailer)
     except ConfigurationException, e:
index 04eeaac413fc57bc17572de630e8bacb03372f99..d0e9b392013b1dc0f8beeaead3a08d91ce5c3f03 100755 (executable)
@@ -22,6 +22,7 @@ OLD_NAMES = [
     'showrev',
     'emailmaxlines',
     'diffopts',
+    'scancommitforcc',
     ]
 
 NEW_NAMES = [
@@ -38,6 +39,7 @@ NEW_NAMES = [
     'emailmaxlines',
     'diffopts',
     'emaildomain',
+    'scancommitforcc',
     ]
 
 
@@ -61,7 +63,7 @@ def _check_old_config_exists(old):
     """Check that at least one old configuration value is set."""
 
     for name in OLD_NAMES:
-        if old.has_key(name):
+        if name in old:
             return True
 
     return False
@@ -72,7 +74,7 @@ def _check_new_config_clear(new):
 
     retval = True
     for name in NEW_NAMES:
-        if new.has_key(name):
+        if name in new:
             if retval:
                 sys.stderr.write('INFO: The following configuration values already exist:\n\n')
             sys.stderr.write('    "%s.%s"\n' % (new.section, name))
@@ -83,7 +85,7 @@ def _check_new_config_clear(new):
 
 def erase_values(config, names):
     for name in names:
-        if config.has_key(name):
+        if name in config:
             try:
                 sys.stderr.write('...unsetting "%s.%s"\n' % (config.section, name))
                 config.unset_all(name)
@@ -170,7 +172,7 @@ def migrate_config(strict=False, retain=False, overwrite=False):
                 )
 
     name = 'showrev'
-    if old.has_key(name):
+    if name in old:
         msg = 'git-multimail does not support "%s.%s"' % (old.section, name,)
         if strict:
             sys.exit(
@@ -182,7 +184,7 @@ def migrate_config(strict=False, retain=False, overwrite=False):
             sys.stderr.write('\nWARNING: %s (ignoring).\n\n' % (msg,))
 
     for name in ['mailinglist', 'announcelist']:
-        if old.has_key(name):
+        if name in old:
             sys.stderr.write(
                 '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
                 )
@@ -198,15 +200,15 @@ def migrate_config(strict=False, retain=False, overwrite=False):
             )
         new.set('announceshortlog', 'true')
 
-    for name in ['envelopesender', 'emailmaxlines', 'diffopts']:
-        if old.has_key(name):
+    for name in ['envelopesender', 'emailmaxlines', 'diffopts', 'scancommitforcc']:
+        if name in old:
             sys.stderr.write(
                 '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
                 )
             new.set(name, old.get(name))
 
     name = 'emailprefix'
-    if old.has_key(name):
+    if name in old:
         sys.stderr.write(
             '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
             )
similarity index 74%
rename from contrib/hooks/multimail/post-receive
rename to contrib/hooks/multimail/post-receive.example
index 4d46828ba5574aba988e8a89d99386b5558b299a..43f7b6b635b15b1dd3648d2f91b78d4e31acf504 100755 (executable)
@@ -2,16 +2,18 @@
 
 """Example post-receive hook based on git-multimail.
 
-This script is a simple example of a post-receive hook implemented
-using git_multimail.py as a Python module.  It is intended to be
-customized before use; see the comments in the script to help you get
-started.
+The simplest way to use git-multimail is to use the script
+git_multimail.py directly as a post-receive hook, and to configure it
+using Git's configuration files and command-line parameters.  You can
+also write your own Python wrapper for more advanced configurability,
+using git_multimail.py as a Python module.
 
-It is possible to use git_multimail.py itself as a post-receive or
-update hook, configured via git config settings and/or command-line
-parameters.  But for more flexibility, it can also be imported as a
-Python module by a custom post-receive script as done here.  The
-latter has the following advantages:
+This script is a simple example of such a post-receive hook.  It is
+intended to be customized before use; see the comments in the script
+to help you get started.
+
+Using git-multimail as a Python module as done here provides more
+flexibility.  It has the following advantages:
 
 * The tool's behavior can be customized using arbitrary Python code,
   without having to edit git_multimail.py.
@@ -56,8 +58,11 @@ config = git_multimail.Config('multimailhook')
 
 
 # Select the type of environment:
-environment = git_multimail.GenericEnvironment(config=config)
-#environment = git_multimail.GitoliteEnvironment(config=config)
+try:
+    environment = git_multimail.GenericEnvironment(config=config)
+    #environment = git_multimail.GitoliteEnvironment(config=config)
+except git_multimail.ConfigurationException, e:
+    sys.exit(str(e))
 
 
 # Choose the method of sending emails based on the git config:
diff --git a/diff.c b/diff.c
index 7500c5509550ccd9a86d620e06c51a71d61f8b6c..87b16d5613adb9046501a1b0b285f9f954abbaa5 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -42,7 +42,7 @@ static long diff_algorithm;
 
 static char diff_colors[][COLOR_MAXLEN] = {
        GIT_COLOR_RESET,
-       GIT_COLOR_NORMAL,       /* PLAIN */
+       GIT_COLOR_NORMAL,       /* CONTEXT */
        GIT_COLOR_BOLD,         /* METAINFO */
        GIT_COLOR_CYAN,         /* FRAGINFO */
        GIT_COLOR_RED,          /* OLD */
@@ -54,8 +54,8 @@ static char diff_colors[][COLOR_MAXLEN] = {
 
 static int parse_diff_color_slot(const char *var)
 {
-       if (!strcasecmp(var, "plain"))
-               return DIFF_PLAIN;
+       if (!strcasecmp(var, "context") || !strcasecmp(var, "plain"))
+               return DIFF_CONTEXT;
        if (!strcasecmp(var, "meta"))
                return DIFF_METAINFO;
        if (!strcasecmp(var, "frag"))
@@ -478,30 +478,63 @@ static int new_blank_line_at_eof(struct emit_callback *ecbdata, const char *line
        return ws_blank_line(line, len, ecbdata->ws_rule);
 }
 
-static void emit_add_line(const char *reset,
-                         struct emit_callback *ecbdata,
-                         const char *line, int len)
+static void emit_line_checked(const char *reset,
+                             struct emit_callback *ecbdata,
+                             const char *line, int len,
+                             enum color_diff color,
+                             unsigned ws_error_highlight,
+                             char sign)
 {
-       const char *ws = diff_get_color(ecbdata->color_diff, DIFF_WHITESPACE);
-       const char *set = diff_get_color(ecbdata->color_diff, DIFF_FILE_NEW);
+       const char *set = diff_get_color(ecbdata->color_diff, color);
+       const char *ws = NULL;
 
-       if (!*ws)
-               emit_line_0(ecbdata->opt, set, reset, '+', line, len);
-       else if (new_blank_line_at_eof(ecbdata, line, len))
+       if (ecbdata->opt->ws_error_highlight & ws_error_highlight) {
+               ws = diff_get_color(ecbdata->color_diff, DIFF_WHITESPACE);
+               if (!*ws)
+                       ws = NULL;
+       }
+
+       if (!ws)
+               emit_line_0(ecbdata->opt, set, reset, sign, line, len);
+       else if (sign == '+' && new_blank_line_at_eof(ecbdata, line, len))
                /* Blank line at EOF - paint '+' as well */
-               emit_line_0(ecbdata->opt, ws, reset, '+', line, len);
+               emit_line_0(ecbdata->opt, ws, reset, sign, line, len);
        else {
                /* Emit just the prefix, then the rest. */
-               emit_line_0(ecbdata->opt, set, reset, '+', "", 0);
+               emit_line_0(ecbdata->opt, set, reset, sign, "", 0);
                ws_check_emit(line, len, ecbdata->ws_rule,
                              ecbdata->opt->file, set, reset, ws);
        }
 }
 
+static void emit_add_line(const char *reset,
+                         struct emit_callback *ecbdata,
+                         const char *line, int len)
+{
+       emit_line_checked(reset, ecbdata, line, len,
+                         DIFF_FILE_NEW, WSEH_NEW, '+');
+}
+
+static void emit_del_line(const char *reset,
+                         struct emit_callback *ecbdata,
+                         const char *line, int len)
+{
+       emit_line_checked(reset, ecbdata, line, len,
+                         DIFF_FILE_OLD, WSEH_OLD, '-');
+}
+
+static void emit_context_line(const char *reset,
+                             struct emit_callback *ecbdata,
+                             const char *line, int len)
+{
+       emit_line_checked(reset, ecbdata, line, len,
+                         DIFF_CONTEXT, WSEH_CONTEXT, ' ');
+}
+
 static void emit_hunk_header(struct emit_callback *ecbdata,
                             const char *line, int len)
 {
-       const char *plain = diff_get_color(ecbdata->color_diff, DIFF_PLAIN);
+       const char *context = diff_get_color(ecbdata->color_diff, DIFF_CONTEXT);
        const char *frag = diff_get_color(ecbdata->color_diff, DIFF_FRAGINFO);
        const char *func = diff_get_color(ecbdata->color_diff, DIFF_FUNCINFO);
        const char *reset = diff_get_color(ecbdata->color_diff, DIFF_RESET);
@@ -518,7 +551,7 @@ static void emit_hunk_header(struct emit_callback *ecbdata,
        if (len < 10 ||
            memcmp(line, atat, 2) ||
            !(ep = memmem(line + 2, len - 2, atat, 2))) {
-               emit_line(ecbdata->opt, plain, reset, line, len);
+               emit_line(ecbdata->opt, context, reset, line, len);
                return;
        }
        ep += 2; /* skip over @@ */
@@ -540,7 +573,7 @@ static void emit_hunk_header(struct emit_callback *ecbdata,
                if (*ep != ' ' && *ep != '\t')
                        break;
        if (ep != cp) {
-               strbuf_addstr(&msgbuf, plain);
+               strbuf_addstr(&msgbuf, context);
                strbuf_add(&msgbuf, cp, ep - cp);
                strbuf_addstr(&msgbuf, reset);
        }
@@ -603,7 +636,6 @@ static void emit_rewrite_lines(struct emit_callback *ecb,
 {
        const char *endp = NULL;
        static const char *nneof = " No newline at end of file\n";
-       const char *old = diff_get_color(ecb->color_diff, DIFF_FILE_OLD);
        const char *reset = diff_get_color(ecb->color_diff, DIFF_RESET);
 
        while (0 < size) {
@@ -613,8 +645,7 @@ static void emit_rewrite_lines(struct emit_callback *ecb,
                len = endp ? (endp - data + 1) : size;
                if (prefix != '+') {
                        ecb->lno_in_preimage++;
-                       emit_line_0(ecb->opt, old, reset, '-',
-                                   data, len);
+                       emit_del_line(reset, ecb, data, len);
                } else {
                        ecb->lno_in_postimage++;
                        emit_add_line(reset, ecb, data, len);
@@ -623,10 +654,10 @@ static void emit_rewrite_lines(struct emit_callback *ecb,
                data += len;
        }
        if (!endp) {
-               const char *plain = diff_get_color(ecb->color_diff,
-                                                  DIFF_PLAIN);
+               const char *context = diff_get_color(ecb->color_diff,
+                                                    DIFF_CONTEXT);
                putc('\n', ecb->opt->file);
-               emit_line_0(ecb->opt, plain, reset, '\\',
+               emit_line_0(ecb->opt, context, reset, '\\',
                            nneof, strlen(nneof));
        }
 }
@@ -1086,7 +1117,7 @@ static void init_diff_words_data(struct emit_callback *ecbdata,
                struct diff_words_style *st = ecbdata->diff_words->style;
                st->old.color = diff_get_color_opt(o, DIFF_FILE_OLD);
                st->new.color = diff_get_color_opt(o, DIFF_FILE_NEW);
-               st->ctx.color = diff_get_color_opt(o, DIFF_PLAIN);
+               st->ctx.color = diff_get_color_opt(o, DIFF_CONTEXT);
        }
 }
 
@@ -1162,7 +1193,7 @@ static void fn_out_consume(void *priv, char *line, unsigned long len)
 {
        struct emit_callback *ecbdata = priv;
        const char *meta = diff_get_color(ecbdata->color_diff, DIFF_METAINFO);
-       const char *plain = diff_get_color(ecbdata->color_diff, DIFF_PLAIN);
+       const char *context = diff_get_color(ecbdata->color_diff, DIFF_CONTEXT);
        const char *reset = diff_get_color(ecbdata->color_diff, DIFF_RESET);
        struct diff_options *o = ecbdata->opt;
        const char *line_prefix = diff_line_prefix(o);
@@ -1233,7 +1264,7 @@ static void fn_out_consume(void *priv, char *line, unsigned long len)
                }
                diff_words_flush(ecbdata);
                if (ecbdata->diff_words->type == DIFF_WORDS_PORCELAIN) {
-                       emit_line(ecbdata->opt, plain, reset, line, len);
+                       emit_line(ecbdata->opt, context, reset, line, len);
                        fputs("~\n", ecbdata->opt->file);
                } else {
                        /*
@@ -1245,22 +1276,32 @@ static void fn_out_consume(void *priv, char *line, unsigned long len)
                              line++;
                              len--;
                        }
-                       emit_line(ecbdata->opt, plain, reset, line, len);
+                       emit_line(ecbdata->opt, context, reset, line, len);
                }
                return;
        }
 
-       if (line[0] != '+') {
-               const char *color =
-                       diff_get_color(ecbdata->color_diff,
-                                      line[0] == '-' ? DIFF_FILE_OLD : DIFF_PLAIN);
-               ecbdata->lno_in_preimage++;
-               if (line[0] == ' ')
-                       ecbdata->lno_in_postimage++;
-               emit_line(ecbdata->opt, color, reset, line, len);
-       } else {
+       switch (line[0]) {
+       case '+':
                ecbdata->lno_in_postimage++;
                emit_add_line(reset, ecbdata, line + 1, len - 1);
+               break;
+       case '-':
+               ecbdata->lno_in_preimage++;
+               emit_del_line(reset, ecbdata, line + 1, len - 1);
+               break;
+       case ' ':
+               ecbdata->lno_in_postimage++;
+               ecbdata->lno_in_preimage++;
+               emit_context_line(reset, ecbdata, line + 1, len - 1);
+               break;
+       default:
+               /* incomplete line at the end */
+               ecbdata->lno_in_preimage++;
+               emit_line(ecbdata->opt,
+                         diff_get_color(ecbdata->color_diff, DIFF_CONTEXT),
+                         reset, line, len);
+               break;
        }
 }
 
@@ -3223,6 +3264,7 @@ void diff_setup(struct diff_options *options)
        options->rename_limit = -1;
        options->dirstat_permille = diff_dirstat_permille_default;
        options->context = diff_context_default;
+       options->ws_error_highlight = WSEH_NEW;
        DIFF_OPT_SET(options, RENAME_EMPTY);
 
        /* pathchange left =NULL by default */
@@ -3609,6 +3651,40 @@ static void enable_patch_output(int *fmt) {
        *fmt |= DIFF_FORMAT_PATCH;
 }
 
+static int parse_one_token(const char **arg, const char *token)
+{
+       return skip_prefix(*arg, token, arg) && (!**arg || **arg == ',');
+}
+
+static int parse_ws_error_highlight(struct diff_options *opt, const char *arg)
+{
+       const char *orig_arg = arg;
+       unsigned val = 0;
+       while (*arg) {
+               if (parse_one_token(&arg, "none"))
+                       val = 0;
+               else if (parse_one_token(&arg, "default"))
+                       val = WSEH_NEW;
+               else if (parse_one_token(&arg, "all"))
+                       val = WSEH_NEW | WSEH_OLD | WSEH_CONTEXT;
+               else if (parse_one_token(&arg, "new"))
+                       val |= WSEH_NEW;
+               else if (parse_one_token(&arg, "old"))
+                       val |= WSEH_OLD;
+               else if (parse_one_token(&arg, "context"))
+                       val |= WSEH_CONTEXT;
+               else {
+                       error("unknown value after ws-error-highlight=%.*s",
+                             (int)(arg - orig_arg), orig_arg);
+                       return 0;
+               }
+               if (*arg)
+                       arg++;
+       }
+       opt->ws_error_highlight = val;
+       return 1;
+}
+
 int diff_opt_parse(struct diff_options *options, const char **av, int ac)
 {
        const char *arg = av[0];
@@ -3806,6 +3882,8 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac)
                DIFF_OPT_SET(options, SUBMODULE_LOG);
        else if (skip_prefix(arg, "--submodule=", &arg))
                return parse_submodule_opt(options, arg);
+       else if (skip_prefix(arg, "--ws-error-highlight=", &arg))
+               return parse_ws_error_highlight(options, arg);
 
        /* misc options */
        else if (!strcmp(arg, "-z"))
diff --git a/diff.h b/diff.h
index f6fdf49e14b7c0e2997e7516275013555555e908..c7ad42addf8e4cd91b423d96526a5557473e5688 100644 (file)
--- a/diff.h
+++ b/diff.h
@@ -138,6 +138,11 @@ struct diff_options {
        int dirstat_permille;
        int setup;
        int abbrev;
+/* white-space error highlighting */
+#define WSEH_NEW 1
+#define WSEH_CONTEXT 2
+#define WSEH_OLD 4
+       unsigned ws_error_highlight;
        const char *prefix;
        int prefix_length;
        const char *stat_sep;
@@ -176,7 +181,7 @@ struct diff_options {
 
 enum color_diff {
        DIFF_RESET = 0,
-       DIFF_PLAIN = 1,
+       DIFF_CONTEXT = 1,
        DIFF_METAINFO = 2,
        DIFF_FRAGINFO = 3,
        DIFF_FILE_OLD = 4,
index 710e58c1bfc652d5f01b47cba5016d2e2f91f590..47ad6747c415372da89fa7c88dd8ce03b2765676 100644 (file)
@@ -20,8 +20,8 @@
 #include "git-compat-util.h"
 #include "ewok.h"
 
-#define MASK(x) ((eword_t)1 << (x % BITS_IN_WORD))
-#define BLOCK(x) (x / BITS_IN_WORD)
+#define EWAH_MASK(x) ((eword_t)1 << (x % BITS_IN_EWORD))
+#define EWAH_BLOCK(x) (x / BITS_IN_EWORD)
 
 struct bitmap *bitmap_new(void)
 {
@@ -33,7 +33,7 @@ struct bitmap *bitmap_new(void)
 
 void bitmap_set(struct bitmap *self, size_t pos)
 {
-       size_t block = BLOCK(pos);
+       size_t block = EWAH_BLOCK(pos);
 
        if (block >= self->word_alloc) {
                size_t old_size = self->word_alloc;
@@ -45,22 +45,22 @@ void bitmap_set(struct bitmap *self, size_t pos)
                        (self->word_alloc - old_size) * sizeof(eword_t));
        }
 
-       self->words[block] |= MASK(pos);
+       self->words[block] |= EWAH_MASK(pos);
 }
 
 void bitmap_clear(struct bitmap *self, size_t pos)
 {
-       size_t block = BLOCK(pos);
+       size_t block = EWAH_BLOCK(pos);
 
        if (block < self->word_alloc)
-               self->words[block] &= ~MASK(pos);
+               self->words[block] &= ~EWAH_MASK(pos);
 }
 
 int bitmap_get(struct bitmap *self, size_t pos)
 {
-       size_t block = BLOCK(pos);
+       size_t block = EWAH_BLOCK(pos);
        return block < self->word_alloc &&
-               (self->words[block] & MASK(pos)) != 0;
+               (self->words[block] & EWAH_MASK(pos)) != 0;
 }
 
 struct ewah_bitmap *bitmap_to_ewah(struct bitmap *bitmap)
@@ -127,7 +127,7 @@ void bitmap_and_not(struct bitmap *self, struct bitmap *other)
 void bitmap_or_ewah(struct bitmap *self, struct ewah_bitmap *other)
 {
        size_t original_size = self->word_alloc;
-       size_t other_final = (other->bit_size / BITS_IN_WORD) + 1;
+       size_t other_final = (other->bit_size / BITS_IN_EWORD) + 1;
        size_t i = 0;
        struct ewah_iterator it;
        eword_t word;
@@ -155,17 +155,17 @@ void bitmap_each_bit(struct bitmap *self, ewah_callback callback, void *data)
                uint32_t offset;
 
                if (word == (eword_t)~0) {
-                       for (offset = 0; offset < BITS_IN_WORD; ++offset)
+                       for (offset = 0; offset < BITS_IN_EWORD; ++offset)
                                callback(pos++, data);
                } else {
-                       for (offset = 0; offset < BITS_IN_WORD; ++offset) {
+                       for (offset = 0; offset < BITS_IN_EWORD; ++offset) {
                                if ((word >> offset) == 0)
                                        break;
 
                                offset += ewah_bit_ctz64(word >> offset);
                                callback(pos + offset, data);
                        }
-                       pos += BITS_IN_WORD;
+                       pos += BITS_IN_EWORD;
                }
        }
 }
index fccb42b52cbcbbfd2b5ec9f7f535f2e82a327090..b522437c0a0b1ba4906feadeb734542ec941b49a 100644 (file)
@@ -102,7 +102,7 @@ size_t ewah_add_empty_words(struct ewah_bitmap *self, int v, size_t number)
        if (number == 0)
                return 0;
 
-       self->bit_size += number * BITS_IN_WORD;
+       self->bit_size += number * BITS_IN_EWORD;
        return add_empty_words(self, v, number);
 }
 
@@ -152,7 +152,7 @@ void ewah_add_dirty_words(
                        self->buffer_size += can_add;
                }
 
-               self->bit_size += can_add * BITS_IN_WORD;
+               self->bit_size += can_add * BITS_IN_EWORD;
 
                if (number - can_add == 0)
                        break;
@@ -197,7 +197,7 @@ static size_t add_empty_word(struct ewah_bitmap *self, int v)
 
 size_t ewah_add(struct ewah_bitmap *self, eword_t word)
 {
-       self->bit_size += BITS_IN_WORD;
+       self->bit_size += BITS_IN_EWORD;
 
        if (word == 0)
                return add_empty_word(self, 0);
@@ -211,8 +211,8 @@ size_t ewah_add(struct ewah_bitmap *self, eword_t word)
 void ewah_set(struct ewah_bitmap *self, size_t i)
 {
        const size_t dist =
-               (i + BITS_IN_WORD) / BITS_IN_WORD -
-               (self->bit_size + BITS_IN_WORD - 1) / BITS_IN_WORD;
+               (i + BITS_IN_EWORD) / BITS_IN_EWORD -
+               (self->bit_size + BITS_IN_EWORD - 1) / BITS_IN_EWORD;
 
        assert(i >= self->bit_size);
 
@@ -222,19 +222,19 @@ void ewah_set(struct ewah_bitmap *self, size_t i)
                if (dist > 1)
                        add_empty_words(self, 0, dist - 1);
 
-               add_literal(self, (eword_t)1 << (i % BITS_IN_WORD));
+               add_literal(self, (eword_t)1 << (i % BITS_IN_EWORD));
                return;
        }
 
        if (rlw_get_literal_words(self->rlw) == 0) {
                rlw_set_running_len(self->rlw,
                        rlw_get_running_len(self->rlw) - 1);
-               add_literal(self, (eword_t)1 << (i % BITS_IN_WORD));
+               add_literal(self, (eword_t)1 << (i % BITS_IN_EWORD));
                return;
        }
 
        self->buffer[self->buffer_size - 1] |=
-               ((eword_t)1 << (i % BITS_IN_WORD));
+               ((eword_t)1 << (i % BITS_IN_EWORD));
 
        /* check if we just completed a stream of 1s */
        if (self->buffer[self->buffer_size - 1] == (eword_t)(~0)) {
@@ -255,11 +255,11 @@ void ewah_each_bit(struct ewah_bitmap *self, void (*callback)(size_t, void*), vo
                eword_t *word = &self->buffer[pointer];
 
                if (rlw_get_run_bit(word)) {
-                       size_t len = rlw_get_running_len(word) * BITS_IN_WORD;
+                       size_t len = rlw_get_running_len(word) * BITS_IN_EWORD;
                        for (k = 0; k < len; ++k, ++pos)
                                callback(pos, payload);
                } else {
-                       pos += rlw_get_running_len(word) * BITS_IN_WORD;
+                       pos += rlw_get_running_len(word) * BITS_IN_EWORD;
                }
 
                ++pointer;
@@ -268,7 +268,7 @@ void ewah_each_bit(struct ewah_bitmap *self, void (*callback)(size_t, void*), vo
                        int c;
 
                        /* todo: zero count optimization */
-                       for (c = 0; c < BITS_IN_WORD; ++c, ++pos) {
+                       for (c = 0; c < BITS_IN_EWORD; ++c, ++pos) {
                                if ((self->buffer[pointer] & ((eword_t)1 << c)) != 0)
                                        callback(pos, payload);
                        }
index e73252536702aaf9fed17757937fbaf4b4593f91..6e2c5e1e3d4475b31ba3c556b31b9d5d72e26d59 100644 (file)
@@ -32,7 +32,7 @@
 
 struct strbuf;
 typedef uint64_t eword_t;
-#define BITS_IN_WORD (sizeof(eword_t) * 8)
+#define BITS_IN_EWORD (sizeof(eword_t) * 8)
 
 /**
  * Do not use __builtin_popcountll. The GCC implementation
index 761befbd37088c176a65e4d3a12e8a8281e47553..8733071624df86f21373db8c3e3cc5a1ece37830 100755 (executable)
--- a/git-am.sh
+++ b/git-am.sh
@@ -69,6 +69,8 @@ then
        cmdline="$cmdline -3"
 fi
 
+empty_tree=4b825dc642cb6eb9a060e54bf8d69288fbee4904
+
 sq () {
        git rev-parse --sq-quote "$@"
 }
@@ -85,7 +87,7 @@ safe_to_abort () {
                return 1
        fi
 
-       if ! test -s "$dotest/abort-safety"
+       if ! test -f "$dotest/abort-safety"
        then
                return 0
        fi
@@ -177,7 +179,8 @@ It does not apply to blobs recorded in its index.")"
     then
            GIT_MERGE_VERBOSITY=0 && export GIT_MERGE_VERBOSITY
     fi
-    git-merge-recursive $orig_tree -- HEAD $his_tree || {
+    our_tree=$(git rev-parse --verify -q HEAD || echo $empty_tree)
+    git-merge-recursive $orig_tree -- $our_tree $his_tree || {
            git rerere $allow_rerere_autoupdate
            die "$(gettext "Failed to merge in the changes.")"
     }
@@ -378,6 +381,7 @@ committer_date_is_author_date=
 ignore_date=
 allow_rerere_autoupdate=
 gpg_sign_opt=
+threeway=
 
 if test "$(git config --bool --get am.messageid)" = true
 then
@@ -389,6 +393,11 @@ then
     keepcr=t
 fi
 
+if test "$(git config --bool --get am.threeWay)" = true
+then
+    threeway=t
+fi
+
 while test $# != 0
 do
        case "$1" in
@@ -400,6 +409,8 @@ it will be removed. Please do not use it anymore."
                ;;
        -3|--3way)
                threeway=t ;;
+       --no-3way)
+               threeway=f ;;
        -s|--signoff)
                sign=t ;;
        -u|--utf8)
@@ -502,10 +513,11 @@ then
                ;;
        t,)
                git rerere clear
-               git read-tree --reset -u HEAD HEAD
-               orig_head=$(cat "$GIT_DIR/ORIG_HEAD")
-               git reset HEAD
-               git update-ref ORIG_HEAD $orig_head
+               head_tree=$(git rev-parse --verify -q HEAD || echo $empty_tree) &&
+               git read-tree --reset -u $head_tree $head_tree &&
+               index_tree=$(git write-tree) &&
+               git read-tree -m -u $index_tree $head_tree
+               git read-tree $head_tree
                ;;
        ,t)
                if test -f "$dotest/rebasing"
@@ -515,8 +527,19 @@ then
                git rerere clear
                if safe_to_abort
                then
-                       git read-tree --reset -u HEAD ORIG_HEAD
-                       git reset ORIG_HEAD
+                       head_tree=$(git rev-parse --verify -q HEAD || echo $empty_tree) &&
+                       git read-tree --reset -u $head_tree $head_tree &&
+                       index_tree=$(git write-tree) &&
+                       orig_head=$(git rev-parse --verify -q ORIG_HEAD || echo $empty_tree) &&
+                       git read-tree -m -u $index_tree $orig_head
+                       if git rev-parse --verify -q ORIG_HEAD >/dev/null 2>&1
+                       then
+                               git reset ORIG_HEAD
+                       else
+                               git read-tree $empty_tree
+                               curr_branch=$(git symbolic-ref HEAD 2>/dev/null) &&
+                               git update-ref -d $curr_branch
+                       fi
                fi
                rm -fr "$dotest"
                exit ;;
@@ -657,6 +680,8 @@ fi
 if test "$(cat "$dotest/threeway")" = t
 then
        threeway=t
+else
+       threeway=f
 fi
 git_apply_opt=$(cat "$dotest/apply-opt")
 if test "$(cat "$dotest/sign")" = t
index 17584adbd093a0848debcc23b2a73dcf520a8fb4..0cc7ae84ba1fc0cb41c4bb32f445307a72d09c44 100644 (file)
@@ -718,6 +718,7 @@ extern char *xstrndup(const char *str, size_t len);
 extern void *xrealloc(void *ptr, size_t size);
 extern void *xcalloc(size_t nmemb, size_t size);
 extern void *xmmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
+extern void *xmmap_gently(void *start, size_t length, int prot, int flags, int fd, off_t offset);
 extern ssize_t xread(int fd, void *buf, size_t len);
 extern ssize_t xwrite(int fd, const void *buf, size_t len);
 extern ssize_t xpread(int fd, void *buf, size_t len, off_t offset);
index 73f05f0a88c2c32002994e2236481d7cb8800691..073f87bbfdc1dc7ebc3fe610e78709667f597d82 100755 (executable)
--- a/git-p4.py
+++ b/git-p4.py
@@ -2186,7 +2186,7 @@ class P4Sync(Command, P4UserMap):
             # them back too.  This is not needed to the cygwin windows version,
             # just the native "NT" type.
             #
-            text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
+            text = p4_read_pipe(['print', '-q', '-o', '-', "%s@%s" % (file['depotFile'], file['change']) ])
             if p4_version_string().find("/NT") >= 0:
                 text = text.replace("\r\n", "\n")
             contents = [ text ]
index 0917d0d056573912df60afd7b556efe04eeebae8..a814bf61aa9a4a0fa92acc90b937dc83ff1995b3 100755 (executable)
@@ -4,13 +4,53 @@
 #
 # Fetch one or more remote refs and merge it/them into the current HEAD.
 
-USAGE='[-n | --no-stat] [--[no-]commit] [--[no-]squash] [--[no-]ff|--ff-only] [--[no-]rebase|--rebase=preserve] [-s strategy]... [<fetch-options>] <repo> <head>...'
-LONG_USAGE='Fetch one or more remote refs and integrate it/them with the current HEAD.'
 SUBDIRECTORY_OK=Yes
-OPTIONS_SPEC=
+OPTIONS_KEEPDASHDASH=
+OPTIONS_STUCKLONG=Yes
+OPTIONS_SPEC="\
+git pull [options] [<repository> [<refspec>...]]
+
+Fetch one or more remote refs and integrate it/them with the current HEAD.
+--
+v,verbose                  be more verbose
+q,quiet                    be more quiet
+progress                   force progress reporting
+
+  Options related to merging
+r,rebase?false|true|preserve incorporate changes by rebasing rather than merging
+n!                         do not show a diffstat at the end of the merge
+stat                       show a diffstat at the end of the merge
+summary                    (synonym to --stat)
+log?n                      add (at most <n>) entries from shortlog to merge commit message
+squash                     create a single commit instead of doing a merge
+commit                     perform a commit if the merge succeeds (default)
+e,edit                       edit message before committing
+ff                         allow fast-forward
+ff-only!                   abort if fast-forward is not possible
+verify-signatures          verify that the named commit has a valid GPG signature
+s,strategy=strategy        merge strategy to use
+X,strategy-option=option   option for selected merge strategy
+S,gpg-sign?key-id          GPG sign commit
+
+  Options related to fetching
+all                        fetch from all remotes
+a,append                   append to .git/FETCH_HEAD instead of overwriting
+upload-pack=path           path to upload pack on remote end
+f,force                    force overwrite of local branch
+t,tags                     fetch all tags and associated objects
+p,prune                    prune remote-tracking branches no longer on remote
+recurse-submodules?on-demand control recursive fetching of submodules
+dry-run                    dry run
+k,keep                     keep downloaded pack
+depth=depth                deepen history of shallow clone
+unshallow                  convert to a complete repository
+update-shallow             accept refs that update .git/shallow
+refmap=refmap              specify fetch refmap
+"
+test $# -gt 0 && args="$*"
 . git-sh-setup
 . git-sh-i18n
-set_reflog_action "pull${1+ $*}"
+set_reflog_action "pull${args+ $args}"
 require_work_tree_exists
 cd_to_toplevel
 
@@ -44,7 +84,8 @@ bool_or_string_config () {
 
 strategy_args= diffstat= no_commit= squash= no_ff= ff_only=
 log_arg= verbosity= progress= recurse_submodules= verify_signatures=
-merge_args= edit= rebase_args=
+merge_args= edit= rebase_args= all= append= upload_pack= force= tags= prune=
+keep= depth= unshallow= update_shallow= refmap=
 curr_branch=$(git symbolic-ref -q HEAD)
 curr_branch_short="${curr_branch#refs/heads/}"
 rebase=$(bool_or_string_config branch.$curr_branch_short.rebase)
@@ -86,17 +127,17 @@ do
                diffstat=--stat ;;
        --log|--log=*|--no-log)
                log_arg="$1" ;;
-       --no-c|--no-co|--no-com|--no-comm|--no-commi|--no-commit)
+       --no-commit)
                no_commit=--no-commit ;;
-       --c|--co|--com|--comm|--commi|--commit)
+       --commit)
                no_commit=--commit ;;
        -e|--edit)
                edit=--edit ;;
        --no-edit)
                edit=--no-edit ;;
-       --sq|--squ|--squa|--squas|--squash)
+       --squash)
                squash=--squash ;;
-       --no-sq|--no-squ|--no-squa|--no-squas|--no-squash)
+       --no-squash)
                squash=--no-squash ;;
        --ff)
                no_ff=--ff ;;
@@ -104,39 +145,19 @@ do
                no_ff=--no-ff ;;
        --ff-only)
                ff_only=--ff-only ;;
-       -s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\
-               --strateg=*|--strategy=*|\
-       -s|--s|--st|--str|--stra|--strat|--strate|--strateg|--strategy)
-               case "$#,$1" in
-               *,*=*)
-                       strategy=$(expr "z$1" : 'z-[^=]*=\(.*\)') ;;
-               1,*)
-                       usage ;;
-               *)
-                       strategy="$2"
-                       shift ;;
-               esac
-               strategy_args="${strategy_args}-s $strategy "
+       -s*|--strategy=*)
+               strategy_args="$strategy_args $1"
                ;;
-       -X*)
-               case "$#,$1" in
-               1,-X)
-                       usage ;;
-               *,-X)
-                       xx="-X $(git rev-parse --sq-quote "$2")"
-                       shift ;;
-               *,*)
-                       xx=$(git rev-parse --sq-quote "$1") ;;
-               esac
-               merge_args="$merge_args$xx "
+       -X*|--strategy-option=*)
+               merge_args="$merge_args $(git rev-parse --sq-quote "$1")"
                ;;
-       -r=*|--r=*|--re=*|--reb=*|--reba=*|--rebas=*|--rebase=*)
+       -r*|--rebase=*)
                rebase="${1#*=}"
                ;;
-       -r|--r|--re|--reb|--reba|--rebas|--rebase)
+       --rebase)
                rebase=true
                ;;
-       --no-r|--no-re|--no-reb|--no-reba|--no-rebas|--no-rebase)
+       --no-rebase)
                rebase=false
                ;;
        --recurse-submodules)
@@ -163,16 +184,41 @@ do
        -S*)
                gpg_sign_args=$(git rev-parse --sq-quote "$1")
                ;;
-       --d|--dr|--dry|--dry-|--dry-r|--dry-ru|--dry-run)
+       --dry-run)
                dry_run=--dry-run
                ;;
+       --all|--no-all)
+               all=$1 ;;
+       -a|--append|--no-append)
+               append=$1 ;;
+       --upload-pack=*|--no-upload-pack)
+               upload_pack=$1 ;;
+       -f|--force|--no-force)
+               force="$force $1" ;;
+       -t|--tags|--no-tags)
+               tags=$1 ;;
+       -p|--prune|--no-prune)
+               prune=$1 ;;
+       -k|--keep|--no-keep)
+               keep=$1 ;;
+       --depth=*|--no-depth)
+               depth=$1 ;;
+       --unshallow|--no-unshallow)
+               unshallow=$1 ;;
+       --update-shallow|--no-update-shallow)
+               update_shallow=$1 ;;
+       --refmap=*|--no-refmap)
+               refmap=$1 ;;
        -h|--help-all)
                usage
                ;;
-       *)
-               # Pass thru anything that may be meant for fetch.
+       --)
+               shift
                break
                ;;
+       *)
+               usage
+               ;;
        esac
        shift
 done
@@ -248,7 +294,9 @@ test true = "$rebase" && {
        oldremoteref=$(git merge-base --fork-point "$remoteref" $curr_branch 2>/dev/null)
 }
 orig_head=$(git rev-parse -q --verify HEAD)
-git fetch $verbosity $progress $dry_run $recurse_submodules --update-head-ok "$@" || exit 1
+git fetch $verbosity $progress $dry_run $recurse_submodules $all $append \
+$upload_pack $force $tags $prune $keep $depth $unshallow $update_shallow \
+$refmap --update-head-ok "$@" || exit 1
 test -z "$dry_run" || exit 0
 
 curr_head=$(git rev-parse -q --verify HEAD)
index e1e9b1460ced5f660b32796890df7336bc3d01af..ae9f8698c5a4842c2c0d63db51dde067ddeeb1c9 100755 (executable)
@@ -487,6 +487,37 @@ sub split_addrs {
 }
 
 my %aliases;
+
+sub parse_sendmail_alias {
+       local $_ = shift;
+       if (/"/) {
+               print STDERR "warning: sendmail alias with quotes is not supported: $_\n";
+       } elsif (/:include:/) {
+               print STDERR "warning: `:include:` not supported: $_\n";
+       } elsif (/[\/|]/) {
+               print STDERR "warning: `/file` or `|pipe` redirection not supported: $_\n";
+       } elsif (/^(\S+?)\s*:\s*(.+)$/) {
+               my ($alias, $addr) = ($1, $2);
+               $aliases{$alias} = [ split_addrs($addr) ];
+       } else {
+               print STDERR "warning: sendmail line is not recognized: $_\n";
+       }
+}
+
+sub parse_sendmail_aliases {
+       my $fh = shift;
+       my $s = '';
+       while (<$fh>) {
+               chomp;
+               next if /^\s*$/ || /^\s*#/;
+               $s .= $_, next if $s =~ s/\\$// || s/^\s+//;
+               parse_sendmail_alias($s) if $s;
+               $s = $_;
+       }
+       $s =~ s/\\$//; # silently tolerate stray '\' on last line
+       parse_sendmail_alias($s) if $s;
+}
+
 my %parse_alias = (
        # multiline formats can be supported in the future
        mutt => sub { my $fh = shift; while (<$fh>) {
@@ -515,7 +546,7 @@ my %parse_alias = (
                               $aliases{$alias} = [ split_addrs($addr) ];
                          }
                      } },
-
+       sendmail => \&parse_sendmail_aliases,
        gnus => sub { my $fh = shift; while (<$fh>) {
                if (/\(define-mail-alias\s+"(\S+?)"\s+"(\S+?)"\)/) {
                        $aliases{$1} = [ $2 ];
diff --git a/help.c b/help.c
index 80ca8ee68d5af25b43314d351bc7ad09d9d9d03b..d996b340669a66d0b1619472fedf5c11b940d7c5 100644 (file)
--- a/help.c
+++ b/help.c
@@ -394,7 +394,7 @@ const char *help_unknown_cmd(const char *cmd)
                if (autocorrect > 0) {
                        fprintf_ln(stderr, _("in %0.1f seconds automatically..."),
                                (float)autocorrect/10.0);
-                       poll(NULL, 0, autocorrect * 100);
+                       sleep_millisec(autocorrect * 100);
                }
                return assumed;
        }
index a5ed9e3642271afd69a0434876515b97aaceb657..c12c69f05ab478cd35208eed4fdc6697977b86de 100644 (file)
@@ -893,7 +893,7 @@ static void dump_diff_hacky_one(struct rev_info *rev, struct line_log_data *rang
        const char *c_meta = diff_get_color(opt->use_color, DIFF_METAINFO);
        const char *c_old = diff_get_color(opt->use_color, DIFF_FILE_OLD);
        const char *c_new = diff_get_color(opt->use_color, DIFF_FILE_NEW);
-       const char *c_plain = diff_get_color(opt->use_color, DIFF_PLAIN);
+       const char *c_context = diff_get_color(opt->use_color, DIFF_CONTEXT);
 
        if (!pair || !diff)
                return;
@@ -957,7 +957,7 @@ static void dump_diff_hacky_one(struct rev_info *rev, struct line_log_data *rang
                        int k;
                        for (; t_cur < diff->target.ranges[j].start; t_cur++)
                                print_line(prefix, ' ', t_cur, t_ends, pair->two->data,
-                                          c_plain, c_reset);
+                                          c_context, c_reset);
                        for (k = diff->parent.ranges[j].start; k < diff->parent.ranges[j].end; k++)
                                print_line(prefix, '-', k, p_ends, pair->one->data,
                                           c_old, c_reset);
@@ -968,7 +968,7 @@ static void dump_diff_hacky_one(struct rev_info *rev, struct line_log_data *rang
                }
                for (; t_cur < t_end; t_cur++)
                        print_line(prefix, ' ', t_cur, t_ends, pair->two->data,
-                                  c_plain, c_reset);
+                                  c_context, c_reset);
        }
 
        free(p_ends);
index 2a139b6ced68809f3a324f493325ec62c3a44044..41736d23727e2ff246d7000ee74a47ce80ea189d 100644 (file)
@@ -81,7 +81,7 @@ static void process_tree(struct rev_info *revs,
                die("bad tree object");
        if (obj->flags & (UNINTERESTING | SEEN))
                return;
-       if (parse_tree(tree) < 0) {
+       if (parse_tree_gently(tree, revs->ignore_missing_links) < 0) {
                if (revs->ignore_missing_links)
                        return;
                die("bad tree object %s", sha1_to_hex(obj->sha1));
index 8ea03e536a56655ff48f4fa8a3050c0225d52f38..fc3c0495942e2a8d7c31a087429f894633c603d8 100644 (file)
@@ -9,6 +9,7 @@
 #include "xdiff-interface.h"
 #include "run-command.h"
 #include "ll-merge.h"
+#include "quote.h"
 
 struct ll_merge_driver;
 
@@ -166,17 +167,20 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,
 {
        char temp[4][50];
        struct strbuf cmd = STRBUF_INIT;
-       struct strbuf_expand_dict_entry dict[5];
+       struct strbuf_expand_dict_entry dict[6];
+       struct strbuf path_sq = STRBUF_INIT;
        const char *args[] = { NULL, NULL };
        int status, fd, i;
        struct stat st;
        assert(opts);
 
+       sq_quote_buf(&path_sq, path);
        dict[0].placeholder = "O"; dict[0].value = temp[0];
        dict[1].placeholder = "A"; dict[1].value = temp[1];
        dict[2].placeholder = "B"; dict[2].value = temp[2];
        dict[3].placeholder = "L"; dict[3].value = temp[3];
-       dict[4].placeholder = NULL; dict[4].value = NULL;
+       dict[4].placeholder = "P"; dict[4].value = path_sq.buf;
+       dict[5].placeholder = NULL; dict[5].value = NULL;
 
        if (fn->cmdline == NULL)
                die("custom merge driver %s lacks command line.", fn->name);
@@ -210,6 +214,7 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,
        for (i = 0; i < 3; i++)
                unlink_or_warn(temp[i]);
        strbuf_release(&cmd);
+       strbuf_release(&path_sq);
        return status;
 }
 
@@ -269,6 +274,7 @@ static int read_merge_config(const char *var, const char *value, void *cb)
                 *    %A - temporary file name for our version.
                 *    %B - temporary file name for the other branches' version.
                 *    %L - conflict marker length
+                *    %P - the original path (safely quoted for the shell)
                 *
                 * The external merge driver should write the results in the
                 * file named by %A, and signal that it has done with zero exit
index 5a93bc7bc2425bf01e88f55024854225d6a78aca..993bb8274833651159cec6f0571b5b555ea073ca 100644 (file)
@@ -157,14 +157,6 @@ static int lock_file(struct lock_file *lk, const char *path, int flags)
        return lk->fd;
 }
 
-static int sleep_microseconds(long us)
-{
-       struct timeval tv;
-       tv.tv_sec = 0;
-       tv.tv_usec = us;
-       return select(0, NULL, NULL, NULL, &tv);
-}
-
 /*
  * Constants defining the gaps between attempts to lock a file. The
  * first backoff period is approximately INITIAL_BACKOFF_MS
@@ -184,27 +176,22 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,
 {
        int n = 1;
        int multiplier = 1;
-       long remaining_us = 0;
+       long remaining_ms = 0;
        static int random_initialized = 0;
 
        if (timeout_ms == 0)
                return lock_file(lk, path, flags);
 
        if (!random_initialized) {
-               srandom((unsigned int)getpid());
+               srand((unsigned int)getpid());
                random_initialized = 1;
        }
 
-       if (timeout_ms > 0) {
-               /* avoid overflow */
-               if (timeout_ms <= LONG_MAX / 1000)
-                       remaining_us = timeout_ms * 1000;
-               else
-                       remaining_us = LONG_MAX;
-       }
+       if (timeout_ms > 0)
+               remaining_ms = timeout_ms;
 
        while (1) {
-               long backoff_ms, wait_us;
+               long backoff_ms, wait_ms;
                int fd;
 
                fd = lock_file(lk, path, flags);
@@ -213,14 +200,14 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,
                        return fd; /* success */
                else if (errno != EEXIST)
                        return -1; /* failure other than lock held */
-               else if (timeout_ms > 0 && remaining_us <= 0)
+               else if (timeout_ms > 0 && remaining_ms <= 0)
                        return -1; /* failure due to timeout */
 
                backoff_ms = multiplier * INITIAL_BACKOFF_MS;
                /* back off for between 0.75*backoff_ms and 1.25*backoff_ms */
-               wait_us = (750 + random() % 500) * backoff_ms;
-               sleep_microseconds(wait_us);
-               remaining_us -= wait_us;
+               wait_ms = (750 + rand() % 500) * backoff_ms / 1000;
+               sleep_millisec(wait_ms);
+               remaining_ms -= wait_ms;
 
                /* Recursion: (n+1)^2 = n^2 + 2n + 1 */
                multiplier += 2*n + 1;
index 2b3ff2379747f7931167d3099c624113bf032536..637770af813eee643c8375051e419715f56a2db5 100644 (file)
@@ -622,7 +622,7 @@ static void show_objects_for_type(
        while (i < objects->word_alloc && ewah_iterator_next(&filter, &it)) {
                eword_t word = objects->words[i] & filter;
 
-               for (offset = 0; offset < BITS_IN_WORD; ++offset) {
+               for (offset = 0; offset < BITS_IN_EWORD; ++offset) {
                        const unsigned char *sha1;
                        struct revindex_entry *entry;
                        uint32_t hash = 0;
@@ -644,7 +644,7 @@ static void show_objects_for_type(
                        show_reach(sha1, object_type, 0, hash, bitmap_git.pack, entry->offset);
                }
 
-               pos += BITS_IN_WORD;
+               pos += BITS_IN_EWORD;
                i++;
        }
 }
@@ -776,7 +776,7 @@ int reuse_partial_packfile_from_bitmap(struct packed_git **packfile,
                        break;
                }
 
-               reuse_objects += BITS_IN_WORD;
+               reuse_objects += BITS_IN_EWORD;
        }
 
 #ifdef GIT_BITMAP_DEBUG
@@ -1001,7 +1001,7 @@ static int rebuild_bitmap(uint32_t *reposition,
        while (ewah_iterator_next(&word, &it)) {
                uint32_t offset, bit_pos;
 
-               for (offset = 0; offset < BITS_IN_WORD; ++offset) {
+               for (offset = 0; offset < BITS_IN_EWORD; ++offset) {
                        if ((word >> offset) == 0)
                                break;
 
@@ -1014,7 +1014,7 @@ static int rebuild_bitmap(uint32_t *reposition,
                                return -1;
                }
 
-               pos += BITS_IN_WORD;
+               pos += BITS_IN_EWORD;
        }
        return 0;
 }
index 2feaec1888faeae1690a4a6a00d923f181262805..7d603c26026bf9c2104efb2626e39d8e09bc1cba 100644 (file)
--- a/po/de.po
+++ b/po/de.po
@@ -2183,7 +2183,7 @@ msgstr "Nichts spezifiziert, nichts hinzugefügt.\n"
 #: builtin/add.c:358
 #, c-format
 msgid "Maybe you wanted to say 'git add .'?\n"
-msgstr "Wollten Sie vielleicht 'git add .' sagen?\n"
+msgstr "Meinten Sie vielleicht 'git add .'?\n"
 
 #: builtin/add.c:363 builtin/check-ignore.c:172 builtin/clean.c:920
 #: builtin/commit.c:335 builtin/mv.c:130 builtin/reset.c:235 builtin/rm.c:299
@@ -10478,8 +10478,8 @@ msgstr ""
 #: git-am.sh:142
 msgid "Using index info to reconstruct a base tree..."
 msgstr ""
-"Verwende Informationen aus der Staging-Area, um einen Basisverzeichnis "
-"nachzustellen"
+"Verwende Informationen aus der Staging-Area, um ein Basisverzeichnis "
+"nachzustellen ..."
 
 #: git-am.sh:157
 msgid ""
@@ -10491,11 +10491,11 @@ msgstr ""
 
 #: git-am.sh:166
 msgid "Falling back to patching base and 3-way merge..."
-msgstr "Falle zurück zum Patchen der Basis und des 3-Wege-Merges ..."
+msgstr "Falle zurück zum Patchen der Basis und zum 3-Wege-Merge ..."
 
 #: git-am.sh:182
 msgid "Failed to merge in the changes."
-msgstr "Merge der Ã„nderungen fehlgeschlagen"
+msgstr "Merge der Ã„nderungen fehlgeschlagen."
 
 #: git-am.sh:277
 msgid "Only one StGIT patch series can be applied at once"
index 723d48dddfe58f50b30105689dfeb9bcb388c8f2..89dbc0837a4155f9e70b56c7d61c8742482f3659 100644 (file)
@@ -999,7 +999,8 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_e
        }
        pos = -pos-1;
 
-       untracked_cache_add_to_index(istate, ce->name);
+       if (!(option & ADD_CACHE_KEEP_CACHE_TREE))
+               untracked_cache_add_to_index(istate, ce->name);
 
        /*
         * Inserting a merged entry ("stage 0") into the index
@@ -1562,7 +1563,7 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist)
        if (mmap_size < sizeof(struct cache_header) + 20)
                die("index file smaller than expected");
 
-       mmap = xmmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
+       mmap = xmmap(NULL, mmap_size, PROT_READ, MAP_PRIVATE, fd, 0);
        if (mmap == MAP_FAILED)
                die_errno("unable to map index file");
        close(fd);
diff --git a/refs.c b/refs.c
index a742d7925dfbd08ff7623427b74183406fdb85ec..7ac05cf21a25802f8e16d45ef75e37b7de2cda8a 100644 (file)
--- a/refs.c
+++ b/refs.c
@@ -1373,19 +1373,34 @@ static void read_loose_refs(const char *dirname, struct ref_dir *dir)
                                         create_dir_entry(refs, refname.buf,
                                                          refname.len, 1));
                } else {
+                       int read_ok;
+
                        if (*refs->name) {
                                hashclr(sha1);
                                flag = 0;
-                               if (resolve_gitlink_ref(refs->name, refname.buf, sha1) < 0) {
-                                       hashclr(sha1);
-                                       flag |= REF_ISBROKEN;
-                               }
-                       } else if (read_ref_full(refname.buf,
-                                                RESOLVE_REF_READING,
-                                                sha1, &flag)) {
+                               read_ok = !resolve_gitlink_ref(refs->name,
+                                                              refname.buf, sha1);
+                       } else {
+                               read_ok = !read_ref_full(refname.buf,
+                                                        RESOLVE_REF_READING,
+                                                        sha1, &flag);
+                       }
+
+                       if (!read_ok) {
                                hashclr(sha1);
                                flag |= REF_ISBROKEN;
+                       } else if (is_null_sha1(sha1)) {
+                               /*
+                                * It is so astronomically unlikely
+                                * that NULL_SHA1 is the SHA-1 of an
+                                * actual object that we consider its
+                                * appearance in a loose reference
+                                * file to be repo corruption
+                                * (probably due to a software bug).
+                                */
+                               flag |= REF_ISBROKEN;
                        }
+
                        if (check_refname_format(refname.buf,
                                                 REFNAME_ALLOW_ONELEVEL)) {
                                if (!refname_is_safe(refname.buf))
@@ -2219,27 +2234,35 @@ static void unlock_ref(struct ref_lock *lock)
        free(lock);
 }
 
-/* This function should make sure errno is meaningful on error */
-static struct ref_lock *verify_lock(struct ref_lock *lock,
-       const unsigned char *old_sha1, int mustexist)
+/*
+ * Verify that the reference locked by lock has the value old_sha1.
+ * Fail if the reference doesn't exist and mustexist is set. Return 0
+ * on success. On error, write an error message to err, set errno, and
+ * return a negative value.
+ */
+static int verify_lock(struct ref_lock *lock,
+                      const unsigned char *old_sha1, int mustexist,
+                      struct strbuf *err)
 {
+       assert(err);
+
        if (read_ref_full(lock->ref_name,
                          mustexist ? RESOLVE_REF_READING : 0,
                          lock->old_oid.hash, NULL)) {
                int save_errno = errno;
-               error("Can't verify ref %s", lock->ref_name);
-               unlock_ref(lock);
+               strbuf_addf(err, "can't verify ref %s", lock->ref_name);
                errno = save_errno;
-               return NULL;
+               return -1;
        }
        if (hashcmp(lock->old_oid.hash, old_sha1)) {
-               error("Ref %s is at %s but expected %s", lock->ref_name,
-                       oid_to_hex(&lock->old_oid), sha1_to_hex(old_sha1));
-               unlock_ref(lock);
+               strbuf_addf(err, "ref %s is at %s but expected %s",
+                           lock->ref_name,
+                           sha1_to_hex(lock->old_oid.hash),
+                           sha1_to_hex(old_sha1));
                errno = EBUSY;
-               return NULL;
+               return -1;
        }
-       return lock;
+       return 0;
 }
 
 static int remove_empty_directories(const char *file)
@@ -2467,7 +2490,11 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
                        goto error_return;
                }
        }
-       return old_sha1 ? verify_lock(lock, old_sha1, mustexist) : lock;
+       if (old_sha1 && verify_lock(lock, old_sha1, mustexist, err)) {
+               last_errno = errno;
+               goto error_return;
+       }
+       return lock;
 
  error_return:
        unlock_ref(lock);
@@ -3912,7 +3939,7 @@ int ref_transaction_commit(struct ref_transaction *transaction,
                                ? TRANSACTION_NAME_CONFLICT
                                : TRANSACTION_GENERIC_ERROR;
                        reason = strbuf_detach(err, NULL);
-                       strbuf_addf(err, "Cannot lock ref '%s': %s",
+                       strbuf_addf(err, "cannot lock ref '%s': %s",
                                    update->refname, reason);
                        free(reason);
                        goto cleanup;
@@ -3935,7 +3962,7 @@ int ref_transaction_commit(struct ref_transaction *transaction,
                                 * write_ref_to_lockfile():
                                 */
                                update->lock = NULL;
-                               strbuf_addf(err, "Cannot update the ref '%s'.",
+                               strbuf_addf(err, "cannot update the ref '%s'.",
                                            update->refname);
                                ret = TRANSACTION_GENERIC_ERROR;
                                goto cleanup;
index 1d903cf3115b46ab204a877924dded50e7122111..3ff8723da463b87268146ce3c7680d691f5bba4f 100644 (file)
@@ -817,7 +817,7 @@ static int add_parents_to_list(struct rev_info *revs, struct commit *commit,
                        parent = parent->next;
                        if (p)
                                p->object.flags |= UNINTERESTING;
-                       if (parse_commit(p) < 0)
+                       if (parse_commit_gently(p, 1) < 0)
                                continue;
                        if (p->parents)
                                mark_parents_uninteresting(p);
@@ -844,7 +844,7 @@ static int add_parents_to_list(struct rev_info *revs, struct commit *commit,
        for (parent = commit->parents; parent; parent = parent->next) {
                struct commit *p = parent->item;
 
-               if (parse_commit(p) < 0)
+               if (parse_commit_gently(p, revs->ignore_missing_links) < 0)
                        return -1;
                if (revs->show_source && !p->util)
                        p->util = commit->util;
diff --git a/setup.c b/setup.c
index 863ddfd938d29e58341bd2be5dd3b7ecd4db7df2..82c0cc2a13bfeae3ec364dce2438fb4c67e46a35 100644 (file)
--- a/setup.c
+++ b/setup.c
@@ -4,6 +4,7 @@
 
 static int inside_git_dir = -1;
 static int inside_work_tree = -1;
+static int work_tree_config_is_bogus;
 
 /*
  * The input parameter must contain an absolute path, and it must already be
@@ -327,6 +328,10 @@ void setup_work_tree(void)
 
        if (initialized)
                return;
+
+       if (work_tree_config_is_bogus)
+               die("unable to set up work tree using invalid config");
+
        work_tree = get_git_work_tree();
        git_dir = get_git_dir();
        if (!is_absolute_path(git_dir))
@@ -495,8 +500,11 @@ static const char *setup_explicit_git_dir(const char *gitdirenv,
        if (work_tree_env)
                set_git_work_tree(work_tree_env);
        else if (is_bare_repository_cfg > 0) {
-               if (git_work_tree_cfg) /* #22.2, #30 */
-                       die("core.bare and core.worktree do not make sense");
+               if (git_work_tree_cfg) {
+                       /* #22.2, #30 */
+                       warning("core.bare and core.worktree do not make sense");
+                       work_tree_config_is_bogus = 1;
+               }
 
                /* #18, #26 */
                set_git_dir(gitdirenv);
index 7e38148fe52959e1cb8435132c65491dba6850b0..77cd81db319c4863e38be3b03260ea8d16f3e222 100644 (file)
@@ -707,8 +707,8 @@ static void mmap_limit_check(size_t length)
                    (uintmax_t)length, (uintmax_t)limit);
 }
 
-void *xmmap(void *start, size_t length,
-       int prot, int flags, int fd, off_t offset)
+void *xmmap_gently(void *start, size_t length,
+                 int prot, int flags, int fd, off_t offset)
 {
        void *ret;
 
@@ -719,12 +719,19 @@ void *xmmap(void *start, size_t length,
                        return NULL;
                release_pack_memory(length);
                ret = mmap(start, length, prot, flags, fd, offset);
-               if (ret == MAP_FAILED)
-                       die_errno("Out of memory? mmap failed");
        }
        return ret;
 }
 
+void *xmmap(void *start, size_t length,
+       int prot, int flags, int fd, off_t offset)
+{
+       void *ret = xmmap_gently(start, length, prot, flags, fd, offset);
+       if (ret == MAP_FAILED)
+               die_errno("mmap failed");
+       return ret;
+}
+
 void close_pack_windows(struct packed_git *p)
 {
        while (p->windows) {
@@ -3161,7 +3168,7 @@ int has_sha1_pack(const unsigned char *sha1)
        return find_pack_entry(sha1, &e);
 }
 
-int has_sha1_file(const unsigned char *sha1)
+int has_sha1_file_with_flags(const unsigned char *sha1, int flags)
 {
        struct pack_entry e;
 
@@ -3169,6 +3176,8 @@ int has_sha1_file(const unsigned char *sha1)
                return 1;
        if (has_loose_object(sha1))
                return 1;
+       if (flags & HAS_SHA1_QUICK)
+               return 0;
        reprepare_packed_git();
        return find_pack_entry(sha1, &e);
 }
index ba89f4c00959112b2cfc0efaff94f3447df32357..d787bf50f801442b52aa914b5318359f2ecd6d2f 100755 (executable)
@@ -519,7 +519,7 @@ test_expect_success 'stdin create ref works with path with space to blob' '
 test_expect_success 'stdin update ref fails with wrong old value' '
        echo "update $c $m $m~1" >stdin &&
        test_must_fail git update-ref --stdin <stdin 2>err &&
-       grep "fatal: Cannot lock ref '"'"'$c'"'"'" err &&
+       grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
        test_must_fail git rev-parse --verify -q $c
 '
 
@@ -555,7 +555,7 @@ test_expect_success 'stdin update ref works with right old value' '
 test_expect_success 'stdin delete ref fails with wrong old value' '
        echo "delete $a $m~1" >stdin &&
        test_must_fail git update-ref --stdin <stdin 2>err &&
-       grep "fatal: Cannot lock ref '"'"'$a'"'"'" err &&
+       grep "fatal: cannot lock ref '"'"'$a'"'"'" err &&
        git rev-parse $m >expect &&
        git rev-parse $a >actual &&
        test_cmp expect actual
@@ -688,7 +688,7 @@ test_expect_success 'stdin update refs fails with wrong old value' '
        update $c  ''
        EOF
        test_must_fail git update-ref --stdin <stdin 2>err &&
-       grep "fatal: Cannot lock ref '"'"'$c'"'"'" err &&
+       grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
        git rev-parse $m >expect &&
        git rev-parse $a >actual &&
        test_cmp expect actual &&
@@ -883,7 +883,7 @@ test_expect_success 'stdin -z create ref works with path with space to blob' '
 test_expect_success 'stdin -z update ref fails with wrong old value' '
        printf $F "update $c" "$m" "$m~1" >stdin &&
        test_must_fail git update-ref -z --stdin <stdin 2>err &&
-       grep "fatal: Cannot lock ref '"'"'$c'"'"'" err &&
+       grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
        test_must_fail git rev-parse --verify -q $c
 '
 
@@ -899,7 +899,7 @@ test_expect_success 'stdin -z create ref fails when ref exists' '
        git rev-parse "$c" >expect &&
        printf $F "create $c" "$m~1" >stdin &&
        test_must_fail git update-ref -z --stdin <stdin 2>err &&
-       grep "fatal: Cannot lock ref '"'"'$c'"'"'" err &&
+       grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
        git rev-parse "$c" >actual &&
        test_cmp expect actual
 '
@@ -930,7 +930,7 @@ test_expect_success 'stdin -z update ref works with right old value' '
 test_expect_success 'stdin -z delete ref fails with wrong old value' '
        printf $F "delete $a" "$m~1" >stdin &&
        test_must_fail git update-ref -z --stdin <stdin 2>err &&
-       grep "fatal: Cannot lock ref '"'"'$a'"'"'" err &&
+       grep "fatal: cannot lock ref '"'"'$a'"'"'" err &&
        git rev-parse $m >expect &&
        git rev-parse $a >actual &&
        test_cmp expect actual
@@ -1045,7 +1045,7 @@ test_expect_success 'stdin -z update refs fails with wrong old value' '
        git update-ref $c $m &&
        printf $F "update $a" "$m" "$m" "update $b" "$m" "$m" "update $c" "$m" "$Z" >stdin &&
        test_must_fail git update-ref -z --stdin <stdin 2>err &&
-       grep "fatal: Cannot lock ref '"'"'$c'"'"'" err &&
+       grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
        git rev-parse $m >expect &&
        git rev-parse $a >actual &&
        test_cmp expect actual &&
index 33c1a587b3b9f28ecc824a17650467bdc6011bc6..13ae12dfa7d494f50fc2e728d3e7ee36ff5534bc 100755 (executable)
@@ -599,11 +599,20 @@ test_expect_success '#20b/c: core.worktree and core.bare conflict' '
        mkdir -p 20b/.git/wt/sub &&
        (
                cd 20b/.git &&
-               test_must_fail git symbolic-ref HEAD >/dev/null
+               test_must_fail git status >/dev/null
        ) 2>message &&
        grep "core.bare and core.worktree" message
 '
 
+test_expect_success '#20d: core.worktree and core.bare OK when working tree not needed' '
+       setup_repo 20d non-existent "" true &&
+       mkdir -p 20d/.git/wt/sub &&
+       (
+               cd 20d/.git &&
+               git config foo.bar value
+       )
+'
+
 # Case #21: core.worktree/GIT_WORK_TREE overrides core.bare' '
 test_expect_success '#21: setup, core.worktree warns before overriding core.bare' '
        setup_repo 21 non-existent "" unset &&
@@ -612,7 +621,7 @@ test_expect_success '#21: setup, core.worktree warns before overriding core.bare
                cd 21/.git &&
                GIT_WORK_TREE="$here/21" &&
                export GIT_WORK_TREE &&
-               git symbolic-ref HEAD >/dev/null
+               git status >/dev/null
        ) 2>message &&
        ! test -s message
 
@@ -701,13 +710,13 @@ test_expect_success '#22.2: core.worktree and core.bare conflict' '
                cd 22/.git &&
                GIT_DIR=. &&
                export GIT_DIR &&
-               test_must_fail git symbolic-ref HEAD 2>result
+               test_must_fail git status 2>result
        ) &&
        (
                cd 22 &&
                GIT_DIR=.git &&
                export GIT_DIR &&
-               test_must_fail git symbolic-ref HEAD 2>result
+               test_must_fail git status 2>result
        ) &&
        grep "core.bare and core.worktree" 22/.git/result &&
        grep "core.bare and core.worktree" 22/result
@@ -753,9 +762,8 @@ test_expect_success '#28: core.worktree and core.bare conflict (gitfile case)' '
        setup_repo 28 "$here/28" gitfile true &&
        (
                cd 28 &&
-               test_must_fail git symbolic-ref HEAD
+               test_must_fail git status
        ) 2>message &&
-       ! grep "^warning:" message &&
        grep "core.bare and core.worktree" message
 '
 
@@ -767,7 +775,7 @@ test_expect_success '#29: setup' '
                cd 29 &&
                GIT_WORK_TREE="$here/29" &&
                export GIT_WORK_TREE &&
-               git symbolic-ref HEAD >/dev/null
+               git status
        ) 2>message &&
        ! test -s message
 '
@@ -778,7 +786,7 @@ test_expect_success '#30: core.worktree and core.bare conflict (gitfile version)
        setup_repo 30 "$here/30" gitfile true &&
        (
                cd 30 &&
-               test_must_fail env GIT_DIR=.git git symbolic-ref HEAD 2>result
+               test_must_fail env GIT_DIR=.git git status 2>result
        ) &&
        grep "core.bare and core.worktree" 30/result
 '
index c39e50028f256b6850327b90aa1137bd8e35ef1a..890db1174f75172ee6b342e26cbbc58e3e4cd03f 100755 (executable)
@@ -57,6 +57,14 @@ test_expect_success "format-patch --ignore-if-in-upstream" '
 
 '
 
+test_expect_success "format-patch --ignore-if-in-upstream handles tags" '
+       git tag -a v1 -m tag side &&
+       git tag -a v2 -m tag master &&
+       git format-patch --stdout --ignore-if-in-upstream v2..v1 >patch1 &&
+       cnt=$(grep "^From " patch1 | wc -l) &&
+       test $cnt = 2
+'
+
 test_expect_success "format-patch doesn't consider merge commits" '
 
        git checkout -b slave master &&
index 604a838c1ab4d173f9910158935d74398fabf788..2434157aa7632551e8f7b9db59b34405f8fbefa3 100755 (executable)
@@ -9,138 +9,144 @@ test_description='Test special whitespace in diff engine.
 . ./test-lib.sh
 . "$TEST_DIRECTORY"/diff-lib.sh
 
-# Ray Lehtiniemi's example
+test_expect_success "Ray Lehtiniemi's example" '
+       cat <<-\EOF >x &&
+       do {
+          nothing;
+       } while (0);
+       EOF
+       git update-index --add x &&
 
-cat << EOF > x
-do {
-   nothing;
-} while (0);
-EOF
+       cat <<-\EOF >x &&
+       do
+       {
+          nothing;
+       }
+       while (0);
+       EOF
+
+       cat <<-\EOF >expect &&
+       diff --git a/x b/x
+       index adf3937..6edc172 100644
+       --- a/x
+       +++ b/x
+       @@ -1,3 +1,5 @@
+       -do {
+       +do
+       +{
+           nothing;
+       -} while (0);
+       +}
+       +while (0);
+       EOF
 
-git update-index --add x
+       git diff >out &&
+       test_cmp expect out &&
 
-cat << EOF > x
-do
-{
-   nothing;
-}
-while (0);
-EOF
+       git diff -w >out &&
+       test_cmp expect out &&
 
-cat << EOF > expect
-diff --git a/x b/x
-index adf3937..6edc172 100644
---- a/x
-+++ b/x
-@@ -1,3 +1,5 @@
--do {
-+do
-+{
-    nothing;
--} while (0);
-+}
-+while (0);
-EOF
+       git diff -b >out &&
+       test_cmp expect out
+'
 
-git diff > out
-test_expect_success "Ray's example without options" 'test_cmp expect out'
+test_expect_success 'another test, without options' '
+       tr Q "\015" <<-\EOF >x &&
+       whitespace at beginning
+       whitespace change
+       whitespace in the middle
+       whitespace at end
+       unchanged line
+       CR at endQ
+       EOF
 
-git diff -w > out
-test_expect_success "Ray's example with -w" 'test_cmp expect out'
+       git update-index x &&
 
-git diff -b > out
-test_expect_success "Ray's example with -b" 'test_cmp expect out'
+       tr "_" " " <<-\EOF >x &&
+       _       whitespace at beginning
+       whitespace       change
+       white space in the middle
+       whitespace at end__
+       unchanged line
+       CR at end
+       EOF
 
-tr 'Q' '\015' << EOF > x
-whitespace at beginning
-whitespace change
-whitespace in the middle
-whitespace at end
-unchanged line
-CR at endQ
-EOF
+       tr "Q_" "\015 " <<-\EOF >expect &&
+       diff --git a/x b/x
+       index d99af23..22d9f73 100644
+       --- a/x
+       +++ b/x
+       @@ -1,6 +1,6 @@
+       -whitespace at beginning
+       -whitespace change
+       -whitespace in the middle
+       -whitespace at end
+       +       whitespace at beginning
+       +whitespace      change
+       +white space in the middle
+       +whitespace at end__
+        unchanged line
+       -CR at endQ
+       +CR at end
+       EOF
 
-git update-index x
+       git diff >out &&
+       test_cmp expect out &&
 
-tr '_' ' ' << EOF > x
-       whitespace at beginning
-whitespace      change
-white space in the middle
-whitespace at end__
-unchanged line
-CR at end
-EOF
+       >expect &&
+       git diff -w >out &&
+       test_cmp expect out &&
 
-tr 'Q_' '\015 ' << EOF > expect
-diff --git a/x b/x
-index d99af23..8b32fb5 100644
---- a/x
-+++ b/x
-@@ -1,6 +1,6 @@
--whitespace at beginning
--whitespace change
--whitespace in the middle
--whitespace at end
-+      whitespace at beginning
-+whitespace     change
-+white space in the middle
-+whitespace at end__
- unchanged line
--CR at endQ
-+CR at end
-EOF
-git diff > out
-test_expect_success 'another test, without options' 'test_cmp expect out'
+       git diff -w -b >out &&
+       test_cmp expect out &&
 
-cat << EOF > expect
-EOF
-git diff -w > out
-test_expect_success 'another test, with -w' 'test_cmp expect out'
-git diff -w -b > out
-test_expect_success 'another test, with -w -b' 'test_cmp expect out'
-git diff -w --ignore-space-at-eol > out
-test_expect_success 'another test, with -w --ignore-space-at-eol' 'test_cmp expect out'
-git diff -w -b --ignore-space-at-eol > out
-test_expect_success 'another test, with -w -b --ignore-space-at-eol' 'test_cmp expect out'
-
-tr 'Q_' '\015 ' << EOF > expect
-diff --git a/x b/x
-index d99af23..8b32fb5 100644
---- a/x
-+++ b/x
-@@ -1,6 +1,6 @@
--whitespace at beginning
-+      whitespace at beginning
- whitespace     change
--whitespace in the middle
-+white space in the middle
- whitespace at end__
- unchanged line
- CR at end
-EOF
-git diff -b > out
-test_expect_success 'another test, with -b' 'test_cmp expect out'
-git diff -b --ignore-space-at-eol > out
-test_expect_success 'another test, with -b --ignore-space-at-eol' 'test_cmp expect out'
-
-tr 'Q_' '\015 ' << EOF > expect
-diff --git a/x b/x
-index d99af23..8b32fb5 100644
---- a/x
-+++ b/x
-@@ -1,6 +1,6 @@
--whitespace at beginning
--whitespace change
--whitespace in the middle
-+      whitespace at beginning
-+whitespace     change
-+white space in the middle
- whitespace at end__
- unchanged line
- CR at end
-EOF
-git diff --ignore-space-at-eol > out
-test_expect_success 'another test, with --ignore-space-at-eol' 'test_cmp expect out'
+       git diff -w --ignore-space-at-eol >out &&
+       test_cmp expect out &&
+
+       git diff -w -b --ignore-space-at-eol >out &&
+       test_cmp expect out &&
+
+
+       tr "Q_" "\015 " <<-\EOF >expect &&
+       diff --git a/x b/x
+       index d99af23..22d9f73 100644
+       --- a/x
+       +++ b/x
+       @@ -1,6 +1,6 @@
+       -whitespace at beginning
+       +_      whitespace at beginning
+        whitespace      change
+       -whitespace in the middle
+       +white space in the middle
+        whitespace at end__
+        unchanged line
+        CR at end
+       EOF
+       git diff -b >out &&
+       test_cmp expect out &&
+
+       git diff -b --ignore-space-at-eol >out &&
+       test_cmp expect out &&
+
+       tr "Q_" "\015 " <<-\EOF >expect &&
+       diff --git a/x b/x
+       index d99af23..22d9f73 100644
+       --- a/x
+       +++ b/x
+       @@ -1,6 +1,6 @@
+       -whitespace at beginning
+       -whitespace change
+       -whitespace in the middle
+       +_      whitespace at beginning
+       +whitespace      change
+       +white space in the middle
+        whitespace at end__
+        unchanged line
+        CR at end
+       EOF
+       git diff --ignore-space-at-eol >out &&
+       test_cmp expect out
+'
 
 test_expect_success 'ignore-blank-lines: only new lines' '
        test_seq 5 >x &&
@@ -489,291 +495,219 @@ test_expect_success 'ignore-blank-lines: mix changes and blank lines' '
 '
 
 test_expect_success 'check mixed spaces and tabs in indent' '
-
        # This is indented with SP HT SP.
-       echo "   foo();" > x &&
+       echo "   foo();" >x &&
        git diff --check | grep "space before tab in indent"
-
 '
 
 test_expect_success 'check mixed tabs and spaces in indent' '
-
        # This is indented with HT SP HT.
-       echo "          foo();" > x &&
+       echo "          foo();" >x &&
        git diff --check | grep "space before tab in indent"
-
 '
 
 test_expect_success 'check with no whitespace errors' '
-
        git commit -m "snapshot" &&
-       echo "foo();" > x &&
+       echo "foo();" >x &&
        git diff --check
-
 '
 
 test_expect_success 'check with trailing whitespace' '
-
-       echo "foo(); " > x &&
+       echo "foo(); " >x &&
        test_must_fail git diff --check
-
 '
 
 test_expect_success 'check with space before tab in indent' '
-
        # indent has space followed by hard tab
-       echo "  foo();" > x &&
+       echo "  foo();" >x &&
        test_must_fail git diff --check
-
 '
 
 test_expect_success '--check and --exit-code are not exclusive' '
-
        git checkout x &&
        git diff --check --exit-code
-
 '
 
 test_expect_success '--check and --quiet are not exclusive' '
-
        git diff --check --quiet
-
 '
 
 test_expect_success 'check staged with no whitespace errors' '
-
-       echo "foo();" > x &&
+       echo "foo();" >x &&
        git add x &&
        git diff --cached --check
-
 '
 
 test_expect_success 'check staged with trailing whitespace' '
-
-       echo "foo(); " > x &&
+       echo "foo(); " >x &&
        git add x &&
        test_must_fail git diff --cached --check
-
 '
 
 test_expect_success 'check staged with space before tab in indent' '
-
        # indent has space followed by hard tab
-       echo "  foo();" > x &&
+       echo "  foo();" >x &&
        git add x &&
        test_must_fail git diff --cached --check
-
 '
 
 test_expect_success 'check with no whitespace errors (diff-index)' '
-
-       echo "foo();" > x &&
+       echo "foo();" >x &&
        git add x &&
        git diff-index --check HEAD
-
 '
 
 test_expect_success 'check with trailing whitespace (diff-index)' '
-
-       echo "foo(); " > x &&
+       echo "foo(); " >x &&
        git add x &&
        test_must_fail git diff-index --check HEAD
-
 '
 
 test_expect_success 'check with space before tab in indent (diff-index)' '
-
        # indent has space followed by hard tab
-       echo "  foo();" > x &&
+       echo "  foo();" >x &&
        git add x &&
        test_must_fail git diff-index --check HEAD
-
 '
 
 test_expect_success 'check staged with no whitespace errors (diff-index)' '
-
-       echo "foo();" > x &&
+       echo "foo();" >x &&
        git add x &&
        git diff-index --cached --check HEAD
-
 '
 
 test_expect_success 'check staged with trailing whitespace (diff-index)' '
-
-       echo "foo(); " > x &&
+       echo "foo(); " >x &&
        git add x &&
        test_must_fail git diff-index --cached --check HEAD
-
 '
 
 test_expect_success 'check staged with space before tab in indent (diff-index)' '
-
        # indent has space followed by hard tab
-       echo "  foo();" > x &&
+       echo "  foo();" >x &&
        git add x &&
        test_must_fail git diff-index --cached --check HEAD
-
 '
 
 test_expect_success 'check with no whitespace errors (diff-tree)' '
-
-       echo "foo();" > x &&
+       echo "foo();" >x &&
        git commit -m "new commit" x &&
        git diff-tree --check HEAD^ HEAD
-
 '
 
 test_expect_success 'check with trailing whitespace (diff-tree)' '
-
-       echo "foo(); " > x &&
+       echo "foo(); " >x &&
        git commit -m "another commit" x &&
        test_must_fail git diff-tree --check HEAD^ HEAD
-
 '
 
 test_expect_success 'check with space before tab in indent (diff-tree)' '
-
        # indent has space followed by hard tab
-       echo "  foo();" > x &&
+       echo "  foo();" >x &&
        git commit -m "yet another" x &&
        test_must_fail git diff-tree --check HEAD^ HEAD
-
 '
 
 test_expect_success 'check trailing whitespace (trailing-space: off)' '
-
        git config core.whitespace "-trailing-space" &&
-       echo "foo ();   " > x &&
+       echo "foo ();   " >x &&
        git diff --check
-
 '
 
 test_expect_success 'check trailing whitespace (trailing-space: on)' '
-
        git config core.whitespace "trailing-space" &&
-       echo "foo ();   " > x &&
+       echo "foo ();   " >x &&
        test_must_fail git diff --check
-
 '
 
 test_expect_success 'check space before tab in indent (space-before-tab: off)' '
-
        # indent contains space followed by HT
        git config core.whitespace "-space-before-tab" &&
-       echo "  foo ();" > x &&
+       echo "  foo ();" >x &&
        git diff --check
-
 '
 
 test_expect_success 'check space before tab in indent (space-before-tab: on)' '
-
        # indent contains space followed by HT
        git config core.whitespace "space-before-tab" &&
-       echo "  foo ();   " > x &&
+       echo "  foo ();   " >x &&
        test_must_fail git diff --check
-
 '
 
 test_expect_success 'check spaces as indentation (indent-with-non-tab: off)' '
-
        git config core.whitespace "-indent-with-non-tab" &&
-       echo "        foo ();" > x &&
+       echo "        foo ();" >x &&
        git diff --check
-
 '
 
 test_expect_success 'check spaces as indentation (indent-with-non-tab: on)' '
-
        git config core.whitespace "indent-with-non-tab" &&
-       echo "        foo ();" > x &&
+       echo "        foo ();" >x &&
        test_must_fail git diff --check
-
 '
 
 test_expect_success 'ditto, but tabwidth=9' '
-
        git config core.whitespace "indent-with-non-tab,tabwidth=9" &&
        git diff --check
-
 '
 
 test_expect_success 'check tabs and spaces as indentation (indent-with-non-tab: on)' '
-
        git config core.whitespace "indent-with-non-tab" &&
-       echo "                  foo ();" > x &&
+       echo "                  foo ();" >x &&
        test_must_fail git diff --check
-
 '
 
 test_expect_success 'ditto, but tabwidth=10' '
-
        git config core.whitespace "indent-with-non-tab,tabwidth=10" &&
        test_must_fail git diff --check
-
 '
 
 test_expect_success 'ditto, but tabwidth=20' '
-
        git config core.whitespace "indent-with-non-tab,tabwidth=20" &&
        git diff --check
-
 '
 
 test_expect_success 'check tabs as indentation (tab-in-indent: off)' '
-
        git config core.whitespace "-tab-in-indent" &&
-       echo "  foo ();" > x &&
+       echo "  foo ();" >x &&
        git diff --check
-
 '
 
 test_expect_success 'check tabs as indentation (tab-in-indent: on)' '
-
        git config core.whitespace "tab-in-indent" &&
-       echo "  foo ();" > x &&
+       echo "  foo ();" >x &&
        test_must_fail git diff --check
-
 '
 
 test_expect_success 'check tabs and spaces as indentation (tab-in-indent: on)' '
-
        git config core.whitespace "tab-in-indent" &&
-       echo "                  foo ();" > x &&
+       echo "                  foo ();" >x &&
        test_must_fail git diff --check
-
 '
 
 test_expect_success 'ditto, but tabwidth=1 (must be irrelevant)' '
-
        git config core.whitespace "tab-in-indent,tabwidth=1" &&
        test_must_fail git diff --check
-
 '
 
 test_expect_success 'check tab-in-indent and indent-with-non-tab conflict' '
-
        git config core.whitespace "tab-in-indent,indent-with-non-tab" &&
-       echo "foo ();" > x &&
+       echo "foo ();" >x &&
        test_must_fail git diff --check
-
 '
 
 test_expect_success 'check tab-in-indent excluded from wildcard whitespace attribute' '
-
        git config --unset core.whitespace &&
-       echo "x whitespace" > .gitattributes &&
-       echo "    foo ();" > x &&
+       echo "x whitespace" >.gitattributes &&
+       echo "    foo ();" >x &&
        git diff --check &&
        rm -f .gitattributes
-
 '
 
 test_expect_success 'line numbers in --check output are correct' '
-
-       echo "" > x &&
-       echo "foo(); " >> x &&
+       echo "" >x &&
+       echo "foo(); " >>x &&
        git diff --check | grep "x:2:"
-
 '
 
 test_expect_success 'checkdiff detects new trailing blank lines (1)' '
@@ -876,29 +810,127 @@ test_expect_success 'setup diff colors' '
        git config color.diff.old red &&
        git config color.diff.new green &&
        git config color.diff.commit yellow &&
-       git config color.diff.whitespace "normal red" &&
+       git config color.diff.whitespace blue &&
 
        git config core.autocrlf false
 '
-cat >expected <<\EOF
-<BOLD>diff --git a/x b/x<RESET>
-<BOLD>index 9daeafb..2874b91 100644<RESET>
-<BOLD>--- a/x<RESET>
-<BOLD>+++ b/x<RESET>
-<CYAN>@@ -1 +1,4 @@<RESET>
- test<RESET>
-<GREEN>+<RESET><GREEN>{<RESET>
-<GREEN>+<RESET><BRED>  <RESET>
-<GREEN>+<RESET><GREEN>}<RESET>
-EOF
 
 test_expect_success 'diff that introduces a line with only tabs' '
        git config core.whitespace blank-at-eol &&
        git reset --hard &&
-       echo "test" > x &&
+       echo "test" >x &&
        git commit -m "initial" x &&
-       echo "{NTN}" | tr "NT" "\n\t" >> x &&
+       echo "{NTN}" | tr "NT" "\n\t" >>x &&
        git -c color.diff=always diff | test_decode_color >current &&
+
+       cat >expected <<-\EOF &&
+       <BOLD>diff --git a/x b/x<RESET>
+       <BOLD>index 9daeafb..2874b91 100644<RESET>
+       <BOLD>--- a/x<RESET>
+       <BOLD>+++ b/x<RESET>
+       <CYAN>@@ -1 +1,4 @@<RESET>
+        test<RESET>
+       <GREEN>+<RESET><GREEN>{<RESET>
+       <GREEN>+<RESET><BLUE>   <RESET>
+       <GREEN>+<RESET><GREEN>}<RESET>
+       EOF
+
+       test_cmp expected current
+'
+
+test_expect_success 'diff that introduces and removes ws breakages' '
+       git reset --hard &&
+       {
+               echo "0. blank-at-eol " &&
+               echo "1. blank-at-eol "
+       } >x &&
+       git commit -a --allow-empty -m preimage &&
+       {
+               echo "0. blank-at-eol " &&
+               echo "1. still-blank-at-eol " &&
+               echo "2. and a new line "
+       } >x &&
+
+       git -c color.diff=always diff |
+       test_decode_color >current &&
+
+       cat >expected <<-\EOF &&
+       <BOLD>diff --git a/x b/x<RESET>
+       <BOLD>index d0233a2..700886e 100644<RESET>
+       <BOLD>--- a/x<RESET>
+       <BOLD>+++ b/x<RESET>
+       <CYAN>@@ -1,2 +1,3 @@<RESET>
+        0. blank-at-eol <RESET>
+       <RED>-1. blank-at-eol <RESET>
+       <GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
+       <GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
+       EOF
+
+       test_cmp expected current
+'
+
+test_expect_success 'the same with --ws-error-highlight' '
+       git reset --hard &&
+       {
+               echo "0. blank-at-eol " &&
+               echo "1. blank-at-eol "
+       } >x &&
+       git commit -a --allow-empty -m preimage &&
+       {
+               echo "0. blank-at-eol " &&
+               echo "1. still-blank-at-eol " &&
+               echo "2. and a new line "
+       } >x &&
+
+       git -c color.diff=always diff --ws-error-highlight=default,old |
+       test_decode_color >current &&
+
+       cat >expected <<-\EOF &&
+       <BOLD>diff --git a/x b/x<RESET>
+       <BOLD>index d0233a2..700886e 100644<RESET>
+       <BOLD>--- a/x<RESET>
+       <BOLD>+++ b/x<RESET>
+       <CYAN>@@ -1,2 +1,3 @@<RESET>
+        0. blank-at-eol <RESET>
+       <RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
+       <GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
+       <GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
+       EOF
+
+       test_cmp expected current &&
+
+       git -c color.diff=always diff --ws-error-highlight=all |
+       test_decode_color >current &&
+
+       cat >expected <<-\EOF &&
+       <BOLD>diff --git a/x b/x<RESET>
+       <BOLD>index d0233a2..700886e 100644<RESET>
+       <BOLD>--- a/x<RESET>
+       <BOLD>+++ b/x<RESET>
+       <CYAN>@@ -1,2 +1,3 @@<RESET>
+        <RESET>0. blank-at-eol<RESET><BLUE> <RESET>
+       <RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
+       <GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
+       <GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
+       EOF
+
+       test_cmp expected current &&
+
+       git -c color.diff=always diff --ws-error-highlight=none |
+       test_decode_color >current &&
+
+       cat >expected <<-\EOF &&
+       <BOLD>diff --git a/x b/x<RESET>
+       <BOLD>index d0233a2..700886e 100644<RESET>
+       <BOLD>--- a/x<RESET>
+       <BOLD>+++ b/x<RESET>
+       <CYAN>@@ -1,2 +1,3 @@<RESET>
+        0. blank-at-eol <RESET>
+       <RED>-1. blank-at-eol <RESET>
+       <GREEN>+1. still-blank-at-eol <RESET>
+       <GREEN>+2. and a new line <RESET>
+       EOF
+
        test_cmp expected current
 '
 
index a321f7c245da531cb8fea495a5e7ba02b3a6463b..4b0a374b63f4128405c88fdb2410fb679bdd06a1 100755 (executable)
@@ -16,4 +16,17 @@ test_expect_success 'apply --check exits non-zero with unrecognized input' '
        EOF
 '
 
+test_expect_success 'apply exits non-zero with no-op patch' '
+       cat >input <<-\EOF &&
+       diff --get a/1 b/1
+       index 6696ea4..606eddd 100644
+       --- a/1
+       +++ b/1
+       @@ -1,1 +1,1 @@
+        1
+       EOF
+       test_must_fail git apply --stat input &&
+       test_must_fail git apply --check input
+'
+
 test_done
index 306e6f39ac88e7ee4ee0fb16dee12cf999fbba1b..b822a3918d32ca57afb7d6585ac927b55b22bde9 100755 (executable)
@@ -274,15 +274,21 @@ test_expect_success 'am --keep-non-patch really keeps the non-patch part' '
        grep "^\[foo\] third" actual
 '
 
-test_expect_success 'am -3 falls back to 3-way merge' '
+test_expect_success 'setup am -3' '
        rm -fr .git/rebase-apply &&
        git reset --hard &&
-       git checkout -b lorem2 master2 &&
+       git checkout -b base3way master2 &&
        sed -n -e "3,\$p" msg >file &&
        head -n 9 msg >>file &&
        git add file &&
        test_tick &&
-       git commit -m "copied stuff" &&
+       git commit -m "copied stuff"
+'
+
+test_expect_success 'am -3 falls back to 3-way merge' '
+       rm -fr .git/rebase-apply &&
+       git reset --hard &&
+       git checkout -b lorem2 base3way &&
        git am -3 lorem-move.patch &&
        test_path_is_missing .git/rebase-apply &&
        git diff --exit-code lorem
@@ -291,17 +297,31 @@ test_expect_success 'am -3 falls back to 3-way merge' '
 test_expect_success 'am -3 -p0 can read --no-prefix patch' '
        rm -fr .git/rebase-apply &&
        git reset --hard &&
-       git checkout -b lorem3 master2 &&
-       sed -n -e "3,\$p" msg >file &&
-       head -n 9 msg >>file &&
-       git add file &&
-       test_tick &&
-       git commit -m "copied stuff" &&
+       git checkout -b lorem3 base3way &&
        git am -3 -p0 lorem-zero.patch &&
        test_path_is_missing .git/rebase-apply &&
        git diff --exit-code lorem
 '
 
+test_expect_success 'am with config am.threeWay falls back to 3-way merge' '
+       rm -fr .git/rebase-apply &&
+       git reset --hard &&
+       git checkout -b lorem4 base3way &&
+       test_config am.threeWay 1 &&
+       git am lorem-move.patch &&
+       test_path_is_missing .git/rebase-apply &&
+       git diff --exit-code lorem
+'
+
+test_expect_success 'am with config am.threeWay overridden by --no-3way' '
+       rm -fr .git/rebase-apply &&
+       git reset --hard &&
+       git checkout -b lorem5 base3way &&
+       test_config am.threeWay 1 &&
+       test_must_fail git am --no-3way lorem-move.patch &&
+       test_path_is_dir .git/rebase-apply
+'
+
 test_expect_success 'am can rename a file' '
        grep "^rename from" rename.patch &&
        rm -fr .git/rebase-apply &&
@@ -338,12 +358,7 @@ test_expect_success 'am -3 can rename a file after falling back to 3-way merge'
 test_expect_success 'am -3 -q is quiet' '
        rm -fr .git/rebase-apply &&
        git checkout -f lorem2 &&
-       git reset master2 --hard &&
-       sed -n -e "3,\$p" msg >file &&
-       head -n 9 msg >>file &&
-       git add file &&
-       test_tick &&
-       git commit -m "copied stuff" &&
+       git reset base3way --hard &&
        git am -3 -q lorem-move.patch >output.out 2>&1 &&
        ! test -s output.out
 '
index 8d90634ab81b3d24326c519a8bae52c2e2cb3ac0..833e7b2ceae99cbf53bac63e239b362e368ddf8f 100755 (executable)
@@ -14,6 +14,7 @@ test_expect_success setup '
        git add file-1 file-2 &&
        git commit -m initial &&
        git tag initial &&
+       git format-patch --stdout --root initial >initial.patch &&
        for i in 2 3 4 5 6
        do
                echo $i >>file-1 &&
@@ -63,6 +64,28 @@ do
 
 done
 
+test_expect_success 'am -3 --skip removes otherfile-4' '
+       git reset --hard initial &&
+       test_must_fail git am -3 0003-*.patch &&
+       test 3 -eq $(git ls-files -u | wc -l) &&
+       test 4 = "$(cat otherfile-4)" &&
+       git am --skip &&
+       test_cmp_rev initial HEAD &&
+       test -z "$(git ls-files -u)" &&
+       test_path_is_missing otherfile-4
+'
+
+test_expect_success 'am -3 --abort removes otherfile-4' '
+       git reset --hard initial &&
+       test_must_fail git am -3 0003-*.patch &&
+       test 3 -eq $(git ls-files -u | wc -l) &&
+       test 4 = "$(cat otherfile-4)" &&
+       git am --abort &&
+       test_cmp_rev initial HEAD &&
+       test -z $(git ls-files -u) &&
+       test_path_is_missing otherfile-4
+'
+
 test_expect_success 'am --abort will keep the local commits intact' '
        test_must_fail git am 0004-*.patch &&
        test_commit unrelated &&
@@ -72,4 +95,62 @@ test_expect_success 'am --abort will keep the local commits intact' '
        test_cmp expect actual
 '
 
+test_expect_success 'am -3 stops on conflict on unborn branch' '
+       git checkout -f --orphan orphan &&
+       git reset &&
+       rm -f otherfile-4 &&
+       test_must_fail git am -3 0003-*.patch &&
+       test 2 -eq $(git ls-files -u | wc -l) &&
+       test 4 = "$(cat otherfile-4)"
+'
+
+test_expect_success 'am -3 --skip clears index on unborn branch' '
+       test_path_is_dir .git/rebase-apply &&
+       echo tmpfile >tmpfile &&
+       git add tmpfile &&
+       git am --skip &&
+       test -z "$(git ls-files)" &&
+       test_path_is_missing otherfile-4 &&
+       test_path_is_missing tmpfile
+'
+
+test_expect_success 'am -3 --abort removes otherfile-4 on unborn branch' '
+       git checkout -f --orphan orphan &&
+       git reset &&
+       rm -f otherfile-4 file-1 &&
+       test_must_fail git am -3 0003-*.patch &&
+       test 2 -eq $(git ls-files -u | wc -l) &&
+       test 4 = "$(cat otherfile-4)" &&
+       git am --abort &&
+       test -z "$(git ls-files -u)" &&
+       test_path_is_missing otherfile-4
+'
+
+test_expect_success 'am -3 --abort on unborn branch removes applied commits' '
+       git checkout -f --orphan orphan &&
+       git reset &&
+       rm -f otherfile-4 otherfile-2 file-1 file-2 &&
+       test_must_fail git am -3 initial.patch 0003-*.patch &&
+       test 3 -eq $(git ls-files -u | wc -l) &&
+       test 4 = "$(cat otherfile-4)" &&
+       git am --abort &&
+       test -z "$(git ls-files -u)" &&
+       test_path_is_missing otherfile-4 &&
+       test_path_is_missing file-1 &&
+       test_path_is_missing file-2 &&
+       test 0 -eq $(git log --oneline 2>/dev/null | wc -l) &&
+       test refs/heads/orphan = "$(git symbolic-ref HEAD)"
+'
+
+test_expect_success 'am --abort on unborn branch will keep local commits intact' '
+       git checkout -f --orphan orphan &&
+       git reset &&
+       test_must_fail git am 0004-*.patch &&
+       test_commit unrelated2 &&
+       git rev-parse HEAD >expect &&
+       git am --abort &&
+       git rev-parse HEAD >actual &&
+       test_cmp expect actual
+'
+
 test_done
index 7efd45bc27687a619d149bb581cb6cb2582241e9..f4a7193677a4861440d79e761f02315c790e275f 100755 (executable)
@@ -86,16 +86,19 @@ test_expect_success 'pulling into void must not create an octopus' '
 '
 
 test_expect_success 'test . as a remote' '
-
        git branch copy master &&
        git config branch.copy.remote . &&
        git config branch.copy.merge refs/heads/master &&
        echo updated >file &&
        git commit -a -m updated &&
        git checkout copy &&
-       test `cat file` = file &&
+       test "$(cat file)" = file &&
        git pull &&
-       test `cat file` = updated
+       test "$(cat file)" = updated &&
+       git reflog -1 >reflog.actual &&
+       sed "s/^[0-9a-f][0-9a-f]*/OBJID/" reflog.actual >reflog.fuzzy &&
+       echo "OBJID HEAD@{0}: pull: Fast-forward" >reflog.expected &&
+       test_cmp reflog.expected reflog.fuzzy
 '
 
 test_expect_success 'the default remote . should not break explicit pull' '
@@ -104,9 +107,120 @@ test_expect_success 'the default remote . should not break explicit pull' '
        git commit -a -m modified &&
        git checkout copy &&
        git reset --hard HEAD^ &&
-       test `cat file` = file &&
+       test "$(cat file)" = file &&
        git pull . second &&
-       test `cat file` = modified
+       test "$(cat file)" = modified &&
+       git reflog -1 >reflog.actual &&
+       sed "s/^[0-9a-f][0-9a-f]*/OBJID/" reflog.actual >reflog.fuzzy &&
+       echo "OBJID HEAD@{0}: pull . second: Fast-forward" >reflog.expected &&
+       test_cmp reflog.expected reflog.fuzzy
+'
+
+test_expect_success 'fail if wildcard spec does not match any refs' '
+       git checkout -b test copy^ &&
+       test_when_finished "git checkout -f copy && git branch -D test" &&
+       test "$(cat file)" = file &&
+       test_must_fail git pull . "refs/nonexisting1/*:refs/nonexisting2/*" 2>err &&
+       test_i18ngrep "no candidates for merging" err &&
+       test "$(cat file)" = file
+'
+
+test_expect_success 'fail if no branches specified with non-default remote' '
+       git remote add test_remote . &&
+       test_when_finished "git remote remove test_remote" &&
+       git checkout -b test copy^ &&
+       test_when_finished "git checkout -f copy && git branch -D test" &&
+       test "$(cat file)" = file &&
+       test_config branch.test.remote origin &&
+       test_must_fail git pull test_remote 2>err &&
+       test_i18ngrep "specify a branch on the command line" err &&
+       test "$(cat file)" = file
+'
+
+test_expect_success 'fail if not on a branch' '
+       git remote add origin . &&
+       test_when_finished "git remote remove origin" &&
+       git checkout HEAD^ &&
+       test_when_finished "git checkout -f copy" &&
+       test "$(cat file)" = file &&
+       test_must_fail git pull 2>err &&
+       test_i18ngrep "not currently on a branch" err &&
+       test "$(cat file)" = file
+'
+
+test_expect_success 'fail if no configuration for current branch' '
+       git remote add test_remote . &&
+       test_when_finished "git remote remove test_remote" &&
+       git checkout -b test copy^ &&
+       test_when_finished "git checkout -f copy && git branch -D test" &&
+       test_config branch.test.remote test_remote &&
+       test "$(cat file)" = file &&
+       test_must_fail git pull 2>err &&
+       test_i18ngrep "no tracking information" err &&
+       test "$(cat file)" = file
+'
+
+test_expect_success 'pull --all: fail if no configuration for current branch' '
+       git remote add test_remote . &&
+       test_when_finished "git remote remove test_remote" &&
+       git checkout -b test copy^ &&
+       test_when_finished "git checkout -f copy && git branch -D test" &&
+       test_config branch.test.remote test_remote &&
+       test "$(cat file)" = file &&
+       test_must_fail git pull --all 2>err &&
+       test_i18ngrep "There is no tracking information" err &&
+       test "$(cat file)" = file
+'
+
+test_expect_success 'fail if upstream branch does not exist' '
+       git checkout -b test copy^ &&
+       test_when_finished "git checkout -f copy && git branch -D test" &&
+       test_config branch.test.remote . &&
+       test_config branch.test.merge refs/heads/nonexisting &&
+       test "$(cat file)" = file &&
+       test_must_fail git pull 2>err &&
+       test_i18ngrep "no such ref was fetched" err &&
+       test "$(cat file)" = file
+'
+
+test_expect_success 'fail if the index has unresolved entries' '
+       git checkout -b third second^ &&
+       test_when_finished "git checkout -f copy && git branch -D third" &&
+       test "$(cat file)" = file &&
+       test_commit modified2 file &&
+       test -z "$(git ls-files -u)" &&
+       test_must_fail git pull . second &&
+       test -n "$(git ls-files -u)" &&
+       cp file expected &&
+       test_must_fail git pull . second 2>err &&
+       test_i18ngrep "Pull is not possible because you have unmerged files" err &&
+       test_cmp expected file &&
+       git add file &&
+       test -z "$(git ls-files -u)" &&
+       test_must_fail git pull . second 2>err &&
+       test_i18ngrep "You have not concluded your merge" err &&
+       test_cmp expected file
+'
+
+test_expect_success 'fast-forwards working tree if branch head is updated' '
+       git checkout -b third second^ &&
+       test_when_finished "git checkout -f copy && git branch -D third" &&
+       test "$(cat file)" = file &&
+       git pull . second:third 2>err &&
+       test_i18ngrep "fetch updated the current branch head" err &&
+       test "$(cat file)" = modified &&
+       test "$(git rev-parse third)" = "$(git rev-parse second)"
+'
+
+test_expect_success 'fast-forward fails with conflicting work tree' '
+       git checkout -b third second^ &&
+       test_when_finished "git checkout -f copy && git branch -D third" &&
+       test "$(cat file)" = file &&
+       echo conflict >file &&
+       test_must_fail git pull . second:third 2>err &&
+       test_i18ngrep "Cannot fast-forward your working tree" err &&
+       test "$(cat file)" = conflict &&
+       test "$(git rev-parse third)" = "$(git rev-parse second)"
 '
 
 test_expect_success '--rebase' '
@@ -119,23 +233,32 @@ test_expect_success '--rebase' '
        git commit -m "new file" &&
        git tag before-rebase &&
        git pull --rebase . copy &&
-       test $(git rev-parse HEAD^) = $(git rev-parse copy) &&
-       test new = $(git show HEAD:file2)
+       test "$(git rev-parse HEAD^)" = "$(git rev-parse copy)" &&
+       test new = "$(git show HEAD:file2)"
 '
+
+test_expect_success '--rebase fails with multiple branches' '
+       git reset --hard before-rebase &&
+       test_must_fail git pull --rebase . copy master 2>err &&
+       test "$(git rev-parse HEAD)" = "$(git rev-parse before-rebase)" &&
+       test_i18ngrep "Cannot rebase onto multiple branches" err &&
+       test modified = "$(git show HEAD:file)"
+'
+
 test_expect_success 'pull.rebase' '
        git reset --hard before-rebase &&
        test_config pull.rebase true &&
        git pull . copy &&
-       test $(git rev-parse HEAD^) = $(git rev-parse copy) &&
-       test new = $(git show HEAD:file2)
+       test "$(git rev-parse HEAD^)" = "$(git rev-parse copy)" &&
+       test new = "$(git show HEAD:file2)"
 '
 
 test_expect_success 'branch.to-rebase.rebase' '
        git reset --hard before-rebase &&
        test_config branch.to-rebase.rebase true &&
        git pull . copy &&
-       test $(git rev-parse HEAD^) = $(git rev-parse copy) &&
-       test new = $(git show HEAD:file2)
+       test "$(git rev-parse HEAD^)" = "$(git rev-parse copy)" &&
+       test new = "$(git show HEAD:file2)"
 '
 
 test_expect_success 'branch.to-rebase.rebase should override pull.rebase' '
@@ -143,8 +266,8 @@ test_expect_success 'branch.to-rebase.rebase should override pull.rebase' '
        test_config pull.rebase true &&
        test_config branch.to-rebase.rebase false &&
        git pull . copy &&
-       test $(git rev-parse HEAD^) != $(git rev-parse copy) &&
-       test new = $(git show HEAD:file2)
+       test "$(git rev-parse HEAD^)" != "$(git rev-parse copy)" &&
+       test new = "$(git show HEAD:file2)"
 '
 
 # add a feature branch, keep-merge, that is merged into master, so the
@@ -163,33 +286,33 @@ test_expect_success 'pull.rebase=false create a new merge commit' '
        git reset --hard before-preserve-rebase &&
        test_config pull.rebase false &&
        git pull . copy &&
-       test $(git rev-parse HEAD^1) = $(git rev-parse before-preserve-rebase) &&
-       test $(git rev-parse HEAD^2) = $(git rev-parse copy) &&
-       test file3 = $(git show HEAD:file3.t)
+       test "$(git rev-parse HEAD^1)" = "$(git rev-parse before-preserve-rebase)" &&
+       test "$(git rev-parse HEAD^2)" = "$(git rev-parse copy)" &&
+       test file3 = "$(git show HEAD:file3.t)"
 '
 
 test_expect_success 'pull.rebase=true flattens keep-merge' '
        git reset --hard before-preserve-rebase &&
        test_config pull.rebase true &&
        git pull . copy &&
-       test $(git rev-parse HEAD^^) = $(git rev-parse copy) &&
-       test file3 = $(git show HEAD:file3.t)
+       test "$(git rev-parse HEAD^^)" = "$(git rev-parse copy)" &&
+       test file3 = "$(git show HEAD:file3.t)"
 '
 
 test_expect_success 'pull.rebase=1 is treated as true and flattens keep-merge' '
        git reset --hard before-preserve-rebase &&
        test_config pull.rebase 1 &&
        git pull . copy &&
-       test $(git rev-parse HEAD^^) = $(git rev-parse copy) &&
-       test file3 = $(git show HEAD:file3.t)
+       test "$(git rev-parse HEAD^^)" = "$(git rev-parse copy)" &&
+       test file3 = "$(git show HEAD:file3.t)"
 '
 
 test_expect_success 'pull.rebase=preserve rebases and merges keep-merge' '
        git reset --hard before-preserve-rebase &&
        test_config pull.rebase preserve &&
        git pull . copy &&
-       test $(git rev-parse HEAD^^) = $(git rev-parse copy) &&
-       test $(git rev-parse HEAD^2) = $(git rev-parse keep-merge)
+       test "$(git rev-parse HEAD^^)" = "$(git rev-parse copy)" &&
+       test "$(git rev-parse HEAD^2)" = "$(git rev-parse keep-merge)"
 '
 
 test_expect_success 'pull.rebase=invalid fails' '
@@ -202,25 +325,25 @@ test_expect_success '--rebase=false create a new merge commit' '
        git reset --hard before-preserve-rebase &&
        test_config pull.rebase true &&
        git pull --rebase=false . copy &&
-       test $(git rev-parse HEAD^1) = $(git rev-parse before-preserve-rebase) &&
-       test $(git rev-parse HEAD^2) = $(git rev-parse copy) &&
-       test file3 = $(git show HEAD:file3.t)
+       test "$(git rev-parse HEAD^1)" = "$(git rev-parse before-preserve-rebase)" &&
+       test "$(git rev-parse HEAD^2)" = "$(git rev-parse copy)" &&
+       test file3 = "$(git show HEAD:file3.t)"
 '
 
 test_expect_success '--rebase=true rebases and flattens keep-merge' '
        git reset --hard before-preserve-rebase &&
        test_config pull.rebase preserve &&
        git pull --rebase=true . copy &&
-       test $(git rev-parse HEAD^^) = $(git rev-parse copy) &&
-       test file3 = $(git show HEAD:file3.t)
+       test "$(git rev-parse HEAD^^)" = "$(git rev-parse copy)" &&
+       test file3 = "$(git show HEAD:file3.t)"
 '
 
 test_expect_success '--rebase=preserve rebases and merges keep-merge' '
        git reset --hard before-preserve-rebase &&
        test_config pull.rebase true &&
        git pull --rebase=preserve . copy &&
-       test $(git rev-parse HEAD^^) = $(git rev-parse copy) &&
-       test $(git rev-parse HEAD^2) = $(git rev-parse keep-merge)
+       test "$(git rev-parse HEAD^^)" = "$(git rev-parse copy)" &&
+       test "$(git rev-parse HEAD^2)" = "$(git rev-parse keep-merge)"
 '
 
 test_expect_success '--rebase=invalid fails' '
@@ -232,8 +355,8 @@ test_expect_success '--rebase overrides pull.rebase=preserve and flattens keep-m
        git reset --hard before-preserve-rebase &&
        test_config pull.rebase preserve &&
        git pull --rebase . copy &&
-       test $(git rev-parse HEAD^^) = $(git rev-parse copy) &&
-       test file3 = $(git show HEAD:file3.t)
+       test "$(git rev-parse HEAD^^)" = "$(git rev-parse copy)" &&
+       test file3 = "$(git show HEAD:file3.t)"
 '
 
 test_expect_success '--rebase with rebased upstream' '
@@ -250,10 +373,18 @@ test_expect_success '--rebase with rebased upstream' '
        git tag to-rebase-orig &&
        git pull --rebase me copy &&
        test "conflicting modification" = "$(cat file)" &&
-       test file = $(cat file2)
+       test file = "$(cat file2)"
 
 '
 
+test_expect_success '--rebase -f with rebased upstream' '
+       test_when_finished "test_might_fail git rebase --abort" &&
+       git reset --hard to-rebase-orig &&
+       git pull --rebase -f me copy &&
+       test "conflicting modification" = "$(cat file)" &&
+       test file = "$(cat file2)"
+'
+
 test_expect_success '--rebase with rebased default upstream' '
 
        git update-ref refs/remotes/me/copy copy-orig &&
@@ -261,7 +392,7 @@ test_expect_success '--rebase with rebased default upstream' '
        git reset --hard to-rebase-orig &&
        git pull --rebase &&
        test "conflicting modification" = "$(cat file)" &&
-       test file = $(cat file2)
+       test file = "$(cat file2)"
 
 '
 
@@ -282,7 +413,7 @@ test_expect_success 'pull --rebase dies early with dirty working directory' '
 
        git checkout to-rebase &&
        git update-ref refs/remotes/me/copy copy^ &&
-       COPY=$(git rev-parse --verify me/copy) &&
+       COPY="$(git rev-parse --verify me/copy)" &&
        git rebase --onto $COPY copy &&
        test_config branch.to-rebase.remote me &&
        test_config branch.to-rebase.merge refs/heads/copy &&
@@ -290,10 +421,10 @@ test_expect_success 'pull --rebase dies early with dirty working directory' '
        echo dirty >> file &&
        git add file &&
        test_must_fail git pull &&
-       test $COPY = $(git rev-parse --verify me/copy) &&
+       test "$COPY" = "$(git rev-parse --verify me/copy)" &&
        git checkout HEAD -- file &&
        git pull &&
-       test $COPY != $(git rev-parse --verify me/copy)
+       test "$COPY" != "$(git rev-parse --verify me/copy)"
 
 '
 
@@ -308,6 +439,21 @@ test_expect_success 'pull --rebase works on branch yet to be born' '
        test_cmp expect actual
 '
 
+test_expect_success 'pull --rebase fails on unborn branch with staged changes' '
+       test_when_finished "rm -rf empty_repo2" &&
+       git init empty_repo2 &&
+       (
+               cd empty_repo2 &&
+               echo staged-file >staged-file &&
+               git add staged-file &&
+               test "$(git ls-files)" = staged-file &&
+               test_must_fail git pull --rebase .. master 2>err &&
+               test "$(git ls-files)" = staged-file &&
+               test "$(git show :staged-file)" = staged-file &&
+               test_i18ngrep "unborn branch with changes added to the index" err
+       )
+'
+
 test_expect_success 'setup for detecting upstreamed changes' '
        mkdir src &&
        (cd src &&
index 453aba53f45e34f6b17bcad7628f4224d6c2de4c..18372caa151a30ac0ec1f247e793164fe667358a 100755 (executable)
@@ -117,4 +117,31 @@ test_expect_success 'git pull --all' '
        )
 '
 
+test_expect_success 'git pull --dry-run' '
+       test_when_finished "rm -rf clonedry" &&
+       git init clonedry &&
+       (
+               cd clonedry &&
+               git pull --dry-run ../parent &&
+               test_path_is_missing .git/FETCH_HEAD &&
+               test_path_is_missing .git/refs/heads/master &&
+               test_path_is_missing .git/index &&
+               test_path_is_missing file
+       )
+'
+
+test_expect_success 'git pull --all --dry-run' '
+       test_when_finished "rm -rf cloneddry" &&
+       git init clonedry &&
+       (
+               cd clonedry &&
+               git remote add origin ../parent &&
+               git pull --all --dry-run &&
+               test_path_is_missing .git/FETCH_HEAD &&
+               test_path_is_missing .git/refs/remotes/origin/master &&
+               test_path_is_missing .git/index &&
+               test_path_is_missing file
+       )
+'
+
 test_done
index 3c21938a6891feb61f821bc46ff8f09f1ee009d1..04c0509c476de87bfb679f1c50556ca4c3a3d615 100755 (executable)
@@ -85,11 +85,12 @@ test_expect_success 'retry the merge with longer context' '
 cat >./custom-merge <<\EOF
 #!/bin/sh
 
-orig="$1" ours="$2" theirs="$3" exit="$4"
+orig="$1" ours="$2" theirs="$3" exit="$4" path=$5
 (
        echo "orig is $orig"
        echo "ours is $ours"
        echo "theirs is $theirs"
+       echo "path is $path"
        echo "=== orig ==="
        cat "$orig"
        echo "=== ours ==="
@@ -110,7 +111,7 @@ test_expect_success 'custom merge backend' '
 
        git reset --hard anchor &&
        git config --replace-all \
-       merge.custom.driver "./custom-merge %O %A %B 0" &&
+       merge.custom.driver "./custom-merge %O %A %B 0 %P" &&
        git config --replace-all \
        merge.custom.name "custom merge driver for testing" &&
 
@@ -121,7 +122,7 @@ test_expect_success 'custom merge backend' '
        o=$(git unpack-file master^:text) &&
        a=$(git unpack-file side^:text) &&
        b=$(git unpack-file master:text) &&
-       sh -c "./custom-merge $o $a $b 0" &&
+       sh -c "./custom-merge $o $a $b 0 'text'" &&
        sed -e 1,3d $a >check-2 &&
        cmp check-1 check-2 &&
        rm -f $o $a $b
@@ -131,7 +132,7 @@ test_expect_success 'custom merge backend' '
 
        git reset --hard anchor &&
        git config --replace-all \
-       merge.custom.driver "./custom-merge %O %A %B 1" &&
+       merge.custom.driver "./custom-merge %O %A %B 1 %P" &&
        git config --replace-all \
        merge.custom.name "custom merge driver for testing" &&
 
@@ -148,9 +149,12 @@ test_expect_success 'custom merge backend' '
        o=$(git unpack-file master^:text) &&
        a=$(git unpack-file anchor:text) &&
        b=$(git unpack-file master:text) &&
-       sh -c "./custom-merge $o $a $b 0" &&
+       sh -c "./custom-merge $o $a $b 0 'text'" &&
        sed -e 1,3d $a >check-2 &&
        cmp check-1 check-2 &&
+       sed -e 1,3d -e 4q $a >check-3 &&
+       echo "path is text" >expect &&
+       cmp expect check-3 &&
        rm -f $o $a $b
 '
 
diff --git a/t/t6301-for-each-ref-errors.sh b/t/t6301-for-each-ref-errors.sh
new file mode 100755 (executable)
index 0000000..cdb67a0
--- /dev/null
@@ -0,0 +1,56 @@
+#!/bin/sh
+
+test_description='for-each-ref errors for broken refs'
+
+. ./test-lib.sh
+
+ZEROS=$_z40
+MISSING=abababababababababababababababababababab
+
+test_expect_success setup '
+       git commit --allow-empty -m "Initial" &&
+       git tag testtag &&
+       git for-each-ref >full-list &&
+       git for-each-ref --format="%(objectname) %(refname)" >brief-list
+'
+
+test_expect_success 'Broken refs are reported correctly' '
+       r=refs/heads/bogus &&
+       : >.git/$r &&
+       test_when_finished "rm -f .git/$r" &&
+       echo "warning: ignoring broken ref $r" >broken-err &&
+       git for-each-ref >out 2>err &&
+       test_cmp full-list out &&
+       test_cmp broken-err err
+'
+
+test_expect_success 'NULL_SHA1 refs are reported correctly' '
+       r=refs/heads/zeros &&
+       echo $ZEROS >.git/$r &&
+       test_when_finished "rm -f .git/$r" &&
+       echo "warning: ignoring broken ref $r" >zeros-err &&
+       git for-each-ref >out 2>err &&
+       test_cmp full-list out &&
+       test_cmp zeros-err err &&
+       git for-each-ref --format="%(objectname) %(refname)" >brief-out 2>brief-err &&
+       test_cmp brief-list brief-out &&
+       test_cmp zeros-err brief-err
+'
+
+test_expect_success 'Missing objects are reported correctly' '
+       r=refs/heads/missing &&
+       echo $MISSING >.git/$r &&
+       test_when_finished "rm -f .git/$r" &&
+       echo "fatal: missing object $MISSING for $r" >missing-err &&
+       test_must_fail git for-each-ref 2>err &&
+       test_cmp missing-err err &&
+       (
+               cat brief-list &&
+               echo "$MISSING $r"
+       ) | sort -k 2 >missing-brief-expected &&
+       git for-each-ref --format="%(objectname) %(refname)" >brief-out 2>brief-err &&
+       test_cmp missing-brief-expected brief-out &&
+       test_must_be_empty brief-err
+'
+
+test_done
index 157f3f91db1e54f3069e45a5087cdb6a8838c7db..2adf825076b9c8f774973dcc83686980dca7725b 100755 (executable)
@@ -129,4 +129,19 @@ for repack in '' true; do
        '
 done
 
+test_expect_success 'do not complain about existing broken links' '
+       cat >broken-commit <<-\EOF &&
+       tree 0000000000000000000000000000000000000001
+       parent 0000000000000000000000000000000000000002
+       author whatever <whatever@example.com> 1234 -0000
+       committer whatever <whatever@example.com> 1234 -0000
+
+       some message
+       EOF
+       commit=$(git hash-object -t commit -w broken-commit) &&
+       git gc 2>stderr &&
+       verbose git cat-file -e $commit &&
+       test_must_be_empty stderr
+'
+
 test_done
index 2e0d557243d6dadc856e5ebe7446aacd905047f1..b39e313ac2a7393899e2fbaa5bc860c4d95c68ee 100755 (executable)
@@ -229,14 +229,36 @@ test_expect_success 'cleanup commit messages (scissors option,-F,-e)' '
        cat >text <<EOF &&
 
 # to be kept
+
+  # ------------------------ >8 ------------------------
+# to be kept, too
 # ------------------------ >8 ------------------------
 to be removed
+# ------------------------ >8 ------------------------
+to be removed, too
+EOF
+
+       cat >expect <<EOF &&
+# to be kept
+
+  # ------------------------ >8 ------------------------
+# to be kept, too
 EOF
-       echo "# to be kept" >expect &&
        git commit --cleanup=scissors -e -F text -a &&
        git cat-file -p HEAD |sed -e "1,/^\$/d">actual &&
        test_cmp expect actual
+'
 
+test_expect_success 'cleanup commit messages (scissors option,-F,-e, scissors on first line)' '
+
+       echo >>negative &&
+       cat >text <<EOF &&
+# ------------------------ >8 ------------------------
+to be removed
+EOF
+       git commit --cleanup=scissors -e -F text -a --allow-empty-message &&
+       git cat-file -p HEAD |sed -e "1,/^\$/d">actual &&
+       test_must_be_empty actual
 '
 
 test_expect_success 'cleanup commit messages (strip option,-F)' '
index 5cdf3f178e8b94481370bcc8385248ec88e46f2c..ff09aced6855dbc9eaa963571b698919d7289871 100755 (executable)
@@ -19,4 +19,66 @@ test_expect_success 'blame --show-email' '
                "<E at test dot git>" 1
 '
 
+test_expect_success 'setup showEmail tests' '
+       echo "bin: test number 1" >one &&
+       git add one &&
+       GIT_AUTHOR_NAME=name1 \
+       GIT_AUTHOR_EMAIL=email1@test.git \
+       git commit -m First --date="2010-01-01 01:00:00" &&
+       cat >expected_n <<-\EOF &&
+       (name1 2010-01-01 01:00:00 +0000 1) bin: test number 1
+       EOF
+       cat >expected_e <<-\EOF
+       (<email1@test.git> 2010-01-01 01:00:00 +0000 1) bin: test number 1
+       EOF
+'
+
+find_blame () {
+       sed -e 's/^[^(]*//'
+}
+
+test_expect_success 'blame with no options and no config' '
+       git blame one >blame &&
+       find_blame <blame >result &&
+       test_cmp expected_n result
+'
+
+test_expect_success 'blame with showemail options' '
+       git blame --show-email one >blame1 &&
+       find_blame <blame1 >result &&
+       test_cmp expected_e result &&
+       git blame -e one >blame2 &&
+       find_blame <blame2 >result &&
+       test_cmp expected_e result &&
+       git blame --no-show-email one >blame3 &&
+       find_blame <blame3 >result &&
+       test_cmp expected_n result
+'
+
+test_expect_success 'blame with showEmail config false' '
+       git config blame.showEmail false &&
+       git blame one >blame1 &&
+       find_blame <blame1 >result &&
+       test_cmp expected_n result &&
+       git blame --show-email one >blame2 &&
+       find_blame <blame2 >result &&
+       test_cmp expected_e result &&
+       git blame -e one >blame3 &&
+       find_blame <blame3 >result &&
+       test_cmp expected_e result &&
+       git blame --no-show-email one >blame4 &&
+       find_blame <blame4 >result &&
+       test_cmp expected_n result
+'
+
+test_expect_success 'blame with showEmail config true' '
+       git config blame.showEmail true &&
+       git blame one >blame1 &&
+       find_blame <blame1 >result &&
+       test_cmp expected_e result &&
+       git blame --no-show-email one >blame2 &&
+       find_blame <blame2 >result &&
+       test_cmp expected_n result
+'
+
 test_done
index 7be14a4e37f7843d9a9863a21e9181ffeceee11b..db2f45e83b14fb5ae5b5f32b4178d7cd1ce97bf8 100755 (executable)
@@ -1537,7 +1537,7 @@ test_expect_success $PREREQ 'sendemail.aliasfiletype=mailrc' '
 
 test_expect_success $PREREQ 'sendemail.aliasfile=~/.mailrc' '
        clean_fake_sendmail &&
-       echo "alias sbd  someone@example.org" >~/.mailrc &&
+       echo "alias sbd  someone@example.org" >"$HOME/.mailrc" &&
        git config --replace-all sendemail.aliasesfile "~/.mailrc" &&
        git config sendemail.aliasfiletype mailrc &&
        git send-email \
@@ -1549,6 +1549,78 @@ test_expect_success $PREREQ 'sendemail.aliasfile=~/.mailrc' '
        grep "^!someone@example\.org!$" commandline1
 '
 
+test_sendmail_aliases () {
+       msg="$1" && shift &&
+       expect="$@" &&
+       cat >.tmp-email-aliases &&
+
+       test_expect_success $PREREQ "$msg" '
+               clean_fake_sendmail && rm -fr outdir &&
+               git format-patch -1 -o outdir &&
+               git config --replace-all sendemail.aliasesfile \
+                       "$(pwd)/.tmp-email-aliases" &&
+               git config sendemail.aliasfiletype sendmail &&
+               git send-email \
+                       --from="Example <nobody@example.com>" \
+                       --to=alice --to=bcgrp \
+                       --smtp-server="$(pwd)/fake.sendmail" \
+                       outdir/0001-*.patch \
+                       2>errors >out &&
+               for i in $expect
+               do
+                       grep "^!$i!$" commandline1 || return 1
+               done
+       '
+}
+
+test_sendmail_aliases 'sendemail.aliasfiletype=sendmail' \
+       'awol@example\.com' \
+       'bob@example\.com' \
+       'chloe@example\.com' \
+       'o@example\.com' <<-\EOF
+       alice: Alice W Land <awol@example.com>
+       bob: Robert Bobbyton <bob@example.com>
+       # this is a comment
+          # this is also a comment
+       chloe: chloe@example.com
+       abgroup: alice, bob
+       bcgrp: bob, chloe, Other <o@example.com>
+       EOF
+
+test_sendmail_aliases 'sendmail aliases line folding' \
+       alice1 \
+       bob1 bob2 \
+       chuck1 chuck2 \
+       darla1 darla2 darla3 \
+       elton1 elton2 elton3 \
+       fred1 fred2 \
+       greg1 <<-\EOF
+       alice: alice1
+       bob: bob1,\
+       bob2
+       chuck: chuck1,
+           chuck2
+       darla: darla1,\
+       darla2,
+           darla3
+       elton: elton1,
+           elton2,\
+       elton3
+       fred: fred1,\
+           fred2
+       greg: greg1
+       bcgrp: bob, chuck, darla, elton, fred, greg
+       EOF
+
+test_sendmail_aliases 'sendmail aliases tolerate bogus line folding' \
+       alice1 bob1 <<-\EOF
+           alice: alice1
+       bcgrp: bob1\
+       EOF
+
+test_sendmail_aliases 'sendmail aliases empty' alice bcgrp <<-\EOF
+       EOF
+
 do_xmailer_test () {
        expected=$1 params=$2 &&
        git format-patch -1 &&
diff --git a/tree.c b/tree.c
index 58ebfce1bca609b0f7119eb210345c2065d02f40..413a5b1fa617df2f407d32ffdf78e58d9c42de58 100644 (file)
--- a/tree.c
+++ b/tree.c
@@ -204,7 +204,7 @@ int parse_tree_buffer(struct tree *item, void *buffer, unsigned long size)
        return 0;
 }
 
-int parse_tree(struct tree *item)
+int parse_tree_gently(struct tree *item, int quiet_on_missing)
 {
         enum object_type type;
         void *buffer;
@@ -214,7 +214,8 @@ int parse_tree(struct tree *item)
                return 0;
        buffer = read_sha1_file(item->object.sha1, &type, &size);
        if (!buffer)
-               return error("Could not read %s",
+               return quiet_on_missing ? -1 :
+                       error("Could not read %s",
                             sha1_to_hex(item->object.sha1));
        if (type != OBJ_TREE) {
                free(buffer);
diff --git a/tree.h b/tree.h
index d24125f84f01cddaecbd047b24e3a710014560a4..d24786cba2ca91d0bffd63490b4f71ac4a58de13 100644 (file)
--- a/tree.h
+++ b/tree.h
@@ -16,7 +16,11 @@ struct tree *lookup_tree(const unsigned char *sha1);
 
 int parse_tree_buffer(struct tree *item, void *buffer, unsigned long size);
 
-int parse_tree(struct tree *tree);
+int parse_tree_gently(struct tree *tree, int quiet_on_missing);
+static inline int parse_tree(struct tree *tree)
+{
+       return parse_tree_gently(tree, 0);
+}
 void free_tree_buffer(struct tree *tree);
 
 /* Parses and returns the tree in the given ent, chasing tags and commits. */
diff --git a/utf8.h b/utf8.h
index e7b2aa416844a0b2ccb2d2cf8a1bce68c4d60be1..5a9e94bee62fd2ed62dcbc82aee12bfc6d75e254 100644 (file)
--- a/utf8.h
+++ b/utf8.h
@@ -31,7 +31,9 @@ char *reencode_string_len(const char *in, int insz,
                          const char *in_encoding,
                          int *outsz);
 #else
-#define reencode_string_len(a,b,c,d,e) NULL
+static inline char *reencode_string_len(const char *a, int b,
+                                       const char *c, const char *d, int *e)
+{ if (e) *e = 0; return NULL; }
 #endif
 
 static inline char *reencode_string(const char *in,
index c1a663fd592133b3e26314f3d598a8bca8bcceb1..ff49807948f3c406fe73511cec4ef4e2b2df186a 100644 (file)
--- a/wrapper.c
+++ b/wrapper.c
@@ -595,3 +595,8 @@ int write_file(const char *path, int fatal, const char *fmt, ...)
        }
        return 0;
 }
+
+void sleep_millisec(int millisec)
+{
+       poll(NULL, 0, millisec);
+}
index c56c78fb6f6947a61a1d54b15da53ad967cc7d38..eaed4fed32f48f4834afaf47f87bda7ce4794308 100644 (file)
@@ -825,10 +825,11 @@ void wt_status_truncate_message_at_cut_line(struct strbuf *buf)
        const char *p;
        struct strbuf pattern = STRBUF_INIT;
 
-       strbuf_addf(&pattern, "%c %s", comment_line_char, cut_line);
-       p = strstr(buf->buf, pattern.buf);
-       if (p && (p == buf->buf || p[-1] == '\n'))
-               strbuf_setlen(buf, p - buf->buf);
+       strbuf_addf(&pattern, "\n%c %s", comment_line_char, cut_line);
+       if (starts_with(buf->buf, pattern.buf + 1))
+               strbuf_setlen(buf, 0);
+       else if ((p = strstr(buf->buf, pattern.buf)))
+               strbuf_setlen(buf, p - buf->buf + 1);
        strbuf_release(&pattern);
 }