diff options
83 files changed, 2720 insertions, 960 deletions
diff --git a/Documentation/RelNotes/2.4.3.txt b/Documentation/RelNotes/2.4.3.txt new file mode 100644 index 0000000000..914d2c1860 --- /dev/null +++ b/Documentation/RelNotes/2.4.3.txt @@ -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 index 0000000000..f1ccd001be --- /dev/null +++ b/Documentation/RelNotes/2.4.4.txt @@ -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. diff --git a/Documentation/RelNotes/2.5.0.txt b/Documentation/RelNotes/2.5.0.txt index 0b7fe2af33..e39f327341 100644 --- a/Documentation/RelNotes/2.5.0.txt +++ b/Documentation/RelNotes/2.5.0.txt @@ -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). diff --git a/Documentation/config.txt b/Documentation/config.txt index 4d21ce1647..3e37b93ed2 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -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). diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt index 3ad6404dbc..d56ca90998 100644 --- a/Documentation/diff-options.txt +++ b/Documentation/diff-options.txt @@ -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 index 0000000000..c73cfa90b7 --- /dev/null +++ b/Documentation/fmt-merge-msg-config.txt @@ -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. diff --git a/Documentation/git-am.txt b/Documentation/git-am.txt index 0d8ba48f79..dbea6e7ae9 100644 --- a/Documentation/git-am.txt +++ b/Documentation/git-am.txt @@ -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:: diff --git a/Documentation/git-blame.txt b/Documentation/git-blame.txt index 9f23a861ce..e6e947c808 100644 --- a/Documentation/git-blame.txt +++ b/Documentation/git-blame.txt @@ -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 diff --git a/Documentation/git-fmt-merge-msg.txt b/Documentation/git-fmt-merge-msg.txt index bb1232a52c..55a9a4b93a 100644 --- a/Documentation/git-fmt-merge-msg.txt +++ b/Documentation/git-fmt-merge-msg.txt @@ -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 diff --git a/Documentation/git-format-patch.txt b/Documentation/git-format-patch.txt index bb3ea9372f..0dac4e9b86 100644 --- a/Documentation/git-format-patch.txt +++ b/Documentation/git-format-patch.txt @@ -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` diff --git a/Documentation/git-pull.txt b/Documentation/git-pull.txt index 712ab4baed..93c72a29ce 100644 --- a/Documentation/git-pull.txt +++ b/Documentation/git-pull.txt @@ -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 diff --git a/Documentation/git-send-email.txt b/Documentation/git-send-email.txt index 804554609d..7ae467ba41 100644 --- a/Documentation/git-send-email.txt +++ b/Documentation/git-send-email.txt @@ -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 diff --git a/Documentation/git-submodule.txt b/Documentation/git-submodule.txt index 2c25916f8f..f17687e09d 100644 --- a/Documentation/git-submodule.txt +++ b/Documentation/git-submodule.txt @@ -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 -------- diff --git a/Documentation/git.txt b/Documentation/git.txt index ccc12b2806..dfbc2290c0 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -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]. diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt index 70899b3023..81fe586948 100644 --- a/Documentation/gitattributes.txt +++ b/Documentation/gitattributes.txt @@ -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` ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt index bf383c2e8c..ab18f4baca 100644 --- a/Documentation/glossary-content.txt +++ b/Documentation/glossary-content.txt @@ -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 diff --git a/Documentation/merge-config.txt b/Documentation/merge-config.txt index 8a0e52f8ee..002ca58c21 100644 --- a/Documentation/merge-config.txt +++ b/Documentation/merge-config.txt @@ -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 diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt index fc09c63b32..4064fc796f 100644 --- a/Documentation/technical/pack-protocol.txt +++ b/Documentation/technical/pack-protocol.txt @@ -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; @@ -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 diff --git a/builtin/apply.c b/builtin/apply.c index 146be97a1a..54aba4e351 100644 --- a/builtin/apply.c +++ b/builtin/apply.c @@ -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; diff --git a/builtin/blame.c b/builtin/blame.c index b3e948e757..a22ac17407 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -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); diff --git a/builtin/clone.c b/builtin/clone.c index b878252bc2..00535d0178 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -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"), diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c index f7e51a7fad..cb7db230d3 100644 --- a/builtin/for-each-ref.c +++ b/builtin/for-each-ref.c @@ -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); diff --git a/builtin/fsck.c b/builtin/fsck.c index 4e8e2ee5b7..2679793049 100644 --- a/builtin/fsck.c +++ b/builtin/fsck.c @@ -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; } diff --git a/builtin/index-pack.c b/builtin/index-pack.c index 7ea2020d82..48fa4724aa 100644 --- a/builtin/index-pack.c +++ b/builtin/index-pack.c @@ -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) diff --git a/builtin/log.c b/builtin/log.c index e67671e37a..878104943f 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -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; } @@ -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 */ diff --git a/combine-diff.c b/combine-diff.c index 8eb7278978..30c7eb6d3c 100644 --- a/combine-diff.c +++ b/combine-diff.c @@ -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); @@ -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); @@ -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); /* @@ -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: diff --git a/config.mak.uname b/config.mak.uname index d26665fa54..943c43965e 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -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 diff --git a/configure.ac b/configure.ac index bbdde85c3d..14012fad72 100644 --- a/configure.ac +++ b/configure.ac @@ -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. # diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index bfc74e9d57..c97c648d7e 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -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" diff --git a/contrib/completion/git-completion.tcsh b/contrib/completion/git-completion.tcsh index 6104a42a23..4a790d8f4e 100644 --- a/contrib/completion/git-completion.tcsh +++ b/contrib/completion/git-completion.tcsh @@ -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. diff --git a/contrib/hooks/multimail/CHANGES b/contrib/hooks/multimail/CHANGES index 3603d56c26..0b823d8f5f 100644 --- a/contrib/hooks/multimail/CHANGES +++ b/contrib/hooks/multimail/CHANGES @@ -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 ============= diff --git a/contrib/hooks/multimail/README b/contrib/hooks/multimail/README index 6efa4ffe17..3a33cb734a 100644 --- a/contrib/hooks/multimail/README +++ b/contrib/hooks/multimail/README @@ -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 diff --git a/contrib/hooks/multimail/README.Git b/contrib/hooks/multimail/README.Git index ab3ece5221..449d36f156 100644 --- a/contrib/hooks/multimail/README.Git +++ b/contrib/hooks/multimail/README.Git @@ -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. diff --git a/contrib/hooks/multimail/git_multimail.py b/contrib/hooks/multimail/git_multimail.py index 8b58ed6444..7cb2b36cb4 100755 --- a/contrib/hooks/multimail/git_multimail.py +++ b/contrib/hooks/multimail/git_multimail.py @@ -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: diff --git a/contrib/hooks/multimail/migrate-mailhook-config b/contrib/hooks/multimail/migrate-mailhook-config index 04eeaac413..d0e9b39201 100755 --- a/contrib/hooks/multimail/migrate-mailhook-config +++ b/contrib/hooks/multimail/migrate-mailhook-config @@ -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) ) diff --git a/contrib/hooks/multimail/post-receive b/contrib/hooks/multimail/post-receive.example index 4d46828ba5..43f7b6b635 100755 --- a/contrib/hooks/multimail/post-receive +++ b/contrib/hooks/multimail/post-receive.example @@ -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: @@ -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")) @@ -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, diff --git a/ewah/bitmap.c b/ewah/bitmap.c index 710e58c1bf..47ad6747c4 100644 --- a/ewah/bitmap.c +++ b/ewah/bitmap.c @@ -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; } } } diff --git a/ewah/ewah_bitmap.c b/ewah/ewah_bitmap.c index fccb42b52c..b522437c0a 100644 --- a/ewah/ewah_bitmap.c +++ b/ewah/ewah_bitmap.c @@ -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); } diff --git a/ewah/ewok.h b/ewah/ewok.h index e732525367..6e2c5e1e3d 100644 --- a/ewah/ewok.h +++ b/ewah/ewok.h @@ -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 @@ -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 diff --git a/git-compat-util.h b/git-compat-util.h index 17584adbd0..0cc7ae84ba 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -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); @@ -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 ] diff --git a/git-pull.sh b/git-pull.sh index 0917d0d056..a814bf61aa 100755 --- a/git-pull.sh +++ b/git-pull.sh @@ -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) diff --git a/git-send-email.perl b/git-send-email.perl index e1e9b1460c..ae9f8698c5 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -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 ]; @@ -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; } diff --git a/line-log.c b/line-log.c index a5ed9e3642..c12c69f05a 100644 --- a/line-log.c +++ b/line-log.c @@ -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); diff --git a/list-objects.c b/list-objects.c index 2a139b6ced..41736d2372 100644 --- a/list-objects.c +++ b/list-objects.c @@ -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)); diff --git a/ll-merge.c b/ll-merge.c index 8ea03e536a..fc3c049594 100644 --- a/ll-merge.c +++ b/ll-merge.c @@ -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 diff --git a/lockfile.c b/lockfile.c index 5a93bc7bc2..993bb82748 100644 --- a/lockfile.c +++ b/lockfile.c @@ -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; diff --git a/pack-bitmap.c b/pack-bitmap.c index 2b3ff23797..637770af81 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -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; } @@ -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" diff --git a/read-cache.c b/read-cache.c index 723d48dddf..89dbc0837a 100644 --- a/read-cache.c +++ b/read-cache.c @@ -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); @@ -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; diff --git a/revision.c b/revision.c index 1d903cf311..3ff8723da4 100644 --- a/revision.c +++ b/revision.c @@ -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; @@ -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); diff --git a/sha1_file.c b/sha1_file.c index 7e38148fe5..77cd81db31 100644 --- a/sha1_file.c +++ b/sha1_file.c @@ -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); } diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh index ba89f4c009..d787bf50f8 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -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 && diff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh index 33c1a587b3..13ae12dfa7 100755 --- a/t/t1510-repo-setup.sh +++ b/t/t1510-repo-setup.sh @@ -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 ' diff --git a/t/t4014-format-patch.sh b/t/t4014-format-patch.sh index c39e50028f..890db1174f 100755 --- a/t/t4014-format-patch.sh +++ b/t/t4014-format-patch.sh @@ -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 && diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh index 604a838c1a..2434157aa7 100755 --- a/t/t4015-diff-whitespace.sh +++ b/t/t4015-diff-whitespace.sh @@ -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 ' diff --git a/t/t4136-apply-check.sh b/t/t4136-apply-check.sh index a321f7c245..4b0a374b63 100755 --- a/t/t4136-apply-check.sh +++ b/t/t4136-apply-check.sh @@ -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 diff --git a/t/t4150-am.sh b/t/t4150-am.sh index 306e6f39ac..b822a3918d 100755 --- a/t/t4150-am.sh +++ b/t/t4150-am.sh @@ -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 ' diff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh index 8d90634ab8..833e7b2cea 100755 --- a/t/t4151-am-abort.sh +++ b/t/t4151-am-abort.sh @@ -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 diff --git a/t/t5520-pull.sh b/t/t5520-pull.sh index 7efd45bc27..f4a7193677 100755 --- a/t/t5520-pull.sh +++ b/t/t5520-pull.sh @@ -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 && diff --git a/t/t5521-pull-options.sh b/t/t5521-pull-options.sh index 453aba53f4..18372caa15 100755 --- a/t/t5521-pull-options.sh +++ b/t/t5521-pull-options.sh @@ -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 diff --git a/t/t6026-merge-attr.sh b/t/t6026-merge-attr.sh index 3c21938a68..04c0509c47 100755 --- a/t/t6026-merge-attr.sh +++ b/t/t6026-merge-attr.sh @@ -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 index 0000000000..cdb67a03b7 --- /dev/null +++ b/t/t6301-for-each-ref-errors.sh @@ -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 diff --git a/t/t6501-freshen-objects.sh b/t/t6501-freshen-objects.sh index 157f3f91db..2adf825076 100755 --- a/t/t6501-freshen-objects.sh +++ b/t/t6501-freshen-objects.sh @@ -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 diff --git a/t/t7502-commit.sh b/t/t7502-commit.sh index 2e0d557243..b39e313ac2 100755 --- a/t/t7502-commit.sh +++ b/t/t7502-commit.sh @@ -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)' ' diff --git a/t/t8002-blame.sh b/t/t8002-blame.sh index 5cdf3f178e..ff09aced68 100755 --- a/t/t8002-blame.sh +++ b/t/t8002-blame.sh @@ -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 diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh index 7be14a4e37..db2f45e83b 100755 --- a/t/t9001-send-email.sh +++ b/t/t9001-send-email.sh @@ -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 && @@ -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); @@ -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. */ @@ -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, @@ -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); +} diff --git a/wt-status.c b/wt-status.c index c56c78fb6f..eaed4fed32 100644 --- a/wt-status.c +++ b/wt-status.c @@ -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); } |