summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Documentation/RelNotes/2.7.0.txt211
-rw-r--r--Documentation/blame-options.txt5
-rw-r--r--Documentation/config.txt20
-rw-r--r--Documentation/git-am.txt4
-rw-r--r--Documentation/git-bisect-lk2009.txt2
-rw-r--r--Documentation/git-bisect.txt103
-rw-r--r--Documentation/git-branch.txt16
-rw-r--r--Documentation/git-cherry-pick.txt10
-rw-r--r--Documentation/git-commit-tree.txt4
-rw-r--r--Documentation/git-commit.txt6
-rw-r--r--Documentation/git-for-each-ref.txt38
-rw-r--r--Documentation/git-grep.txt9
-rw-r--r--Documentation/git-interpret-trailers.txt2
-rw-r--r--Documentation/git-log.txt8
-rw-r--r--Documentation/git-ls-remote.txt3
-rw-r--r--Documentation/git-merge.txt6
-rw-r--r--Documentation/git-notes.txt4
-rw-r--r--Documentation/git-p4.txt39
-rw-r--r--Documentation/git-quiltimport.txt11
-rw-r--r--Documentation/git-rebase.txt7
-rw-r--r--Documentation/git-remote.txt10
-rw-r--r--Documentation/git-rev-list.txt2
-rw-r--r--Documentation/git-revert.txt10
-rw-r--r--Documentation/git-stash.txt2
-rw-r--r--Documentation/git-status.txt5
-rw-r--r--Documentation/git-tag.txt45
-rw-r--r--Documentation/gitignore.txt23
-rw-r--r--Documentation/glossary-content.txt5
-rw-r--r--Documentation/pretty-options.txt5
-rw-r--r--Documentation/rev-list-options.txt23
-rw-r--r--Documentation/user-manual.txt2
-rwxr-xr-xGIT-VERSION-GEN2
-rw-r--r--Makefile14
l---------RelNotes2
-rw-r--r--advice.c2
-rw-r--r--bisect.c11
-rw-r--r--builtin.h1
-rw-r--r--builtin/am.c13
-rw-r--r--builtin/blame.c12
-rw-r--r--builtin/branch.c508
-rw-r--r--builtin/clone.c17
-rw-r--r--builtin/for-each-ref.c15
-rw-r--r--builtin/fsck.c18
-rw-r--r--builtin/gc.c56
-rw-r--r--builtin/ls-remote.c2
-rw-r--r--builtin/remote.c59
-rw-r--r--builtin/rerere.c6
-rw-r--r--builtin/submodule--helper.c282
-rw-r--r--builtin/tag.c390
-rw-r--r--cache.h4
-rw-r--r--connect.c27
-rwxr-xr-xcontrib/examples/git-pull.sh2
-rw-r--r--contrib/hooks/multimail/CHANGES41
-rw-r--r--contrib/hooks/multimail/CONTRIBUTING.rst30
-rw-r--r--contrib/hooks/multimail/README258
-rw-r--r--contrib/hooks/multimail/README.Git4
-rw-r--r--contrib/hooks/multimail/doc/gerrit.rst56
-rw-r--r--contrib/hooks/multimail/doc/gitolite.rst109
-rwxr-xr-xcontrib/hooks/multimail/git_multimail.py936
-rwxr-xr-xcontrib/hooks/multimail/migrate-mailhook-config2
-rwxr-xr-xcontrib/hooks/multimail/post-receive.example8
-rwxr-xr-xcontrib/subtree/git-subtree.sh4
-rwxr-xr-xcontrib/subtree/t/t7900-subtree.sh194
-rw-r--r--date.c74
-rw-r--r--dir.c98
-rw-r--r--fast-import.c2
-rwxr-xr-xgit-bisect.sh117
-rw-r--r--git-compat-util.h3
-rwxr-xr-xgit-filter-branch.sh34
-rwxr-xr-xgit-p4.py330
-rwxr-xr-xgit-quiltimport.sh16
-rw-r--r--git-rebase--interactive.sh66
-rwxr-xr-xgit-rebase.sh5
-rwxr-xr-xgit-send-email.perl6
-rwxr-xr-xgit-stash.sh20
-rwxr-xr-xgit-submodule.sh164
-rw-r--r--git.c3
-rw-r--r--pager.c22
-rw-r--r--parse-options-cb.c19
-rw-r--r--parse-options.h10
-rw-r--r--path.c311
-rw-r--r--pkt-line.c8
-rw-r--r--quote.c11
-rw-r--r--reachable.c8
-rw-r--r--ref-filter.c648
-rw-r--r--ref-filter.h50
-rw-r--r--refs.c41
-rw-r--r--refs.h1
-rw-r--r--rerere.c555
-rw-r--r--rerere.h12
-rw-r--r--run-command.c41
-rw-r--r--run-command.h2
-rw-r--r--setup.c17
-rw-r--r--sha1_file.c59
-rw-r--r--submodule.c9
-rw-r--r--t/annotate-tests.sh4
-rwxr-xr-xt/perf/aggregate.perl1
-rwxr-xr-xt/t0002-gitfile.sh42
-rwxr-xr-xt/t0060-path-utils.sh6
-rwxr-xr-xt/t1400-update-ref.sh19
-rwxr-xr-xt/t1430-bad-ref-name.sh31
-rwxr-xr-xt/t1450-fsck.sh22
-rwxr-xr-xt/t2025-worktree-add.sh5
-rwxr-xr-xt/t2026-worktree-prune.sh (renamed from t/t2026-prune-linked-checkouts.sh)0
-rwxr-xr-xt/t3001-ls-files-others-exclude.sh25
-rwxr-xr-xt/t3203-branch-output.sh33
-rwxr-xr-xt/t3210-pack-refs.sh7
-rwxr-xr-xt/t3301-notes.sh6
-rwxr-xr-xt/t3404-rebase-interactive.sh15
-rwxr-xr-xt/t3420-rebase-autostash.sh10
-rwxr-xr-xt/t5505-remote.sh37
-rwxr-xr-xt/t5507-remote-environment.sh34
-rwxr-xr-xt/t5560-http-backend-noserver.sh4
-rwxr-xr-xt/t5561-http-backend.sh8
-rwxr-xr-xt/t556x_common12
-rwxr-xr-xt/t5700-clone-reference.sh21
-rwxr-xr-xt/t5801-remote-helpers.sh12
-rwxr-xr-xt/t6030-bisect-porcelain.sh135
-rwxr-xr-xt/t6300-for-each-ref.sh162
-rwxr-xr-xt/t6302-for-each-ref-filter.sh258
-rwxr-xr-xt/t6500-gc.sh13
-rwxr-xr-xt/t7003-filter-branch.sh14
-rwxr-xr-xt/t7004-tag.sh47
-rwxr-xr-xt/t7410-submodule-checkout-to.sh10
-rwxr-xr-xt/t7610-mergetool.sh2
-rwxr-xr-xt/t7800-difftool.sh8
-rwxr-xr-xt/t9811-git-p4-label-import.sh45
-rwxr-xr-xt/t9822-git-p4-path-encoding.sh58
-rwxr-xr-xt/t9823-git-p4-mock-lfs.sh192
-rwxr-xr-xt/t9824-git-p4-git-lfs.sh288
-rwxr-xr-xt/t9825-git-p4-handle-utf16-without-bom.sh50
-rw-r--r--t/test-lib-functions.sh25
-rw-r--r--test-path-utils.c2
-rw-r--r--utf8.c21
-rw-r--r--utf8.h15
-rw-r--r--wt-status.c6
137 files changed, 6388 insertions, 1805 deletions
diff --git a/.gitignore b/.gitignore
index 4fd81baf85..1c2f832138 100644
--- a/.gitignore
+++ b/.gitignore
@@ -155,6 +155,7 @@
/git-status
/git-stripspace
/git-submodule
+/git-submodule--helper
/git-svn
/git-symbolic-ref
/git-tag
diff --git a/Documentation/RelNotes/2.7.0.txt b/Documentation/RelNotes/2.7.0.txt
new file mode 100644
index 0000000000..a958f853e6
--- /dev/null
+++ b/Documentation/RelNotes/2.7.0.txt
@@ -0,0 +1,211 @@
+Git 2.7 Release Notes
+=====================
+
+Updates since v2.6
+------------------
+
+UI, Workflows & Features
+
+ * "git remote" learned "get-url" subcommand to show the URL for a
+ given remote name used for fetching and pushing.
+
+ * There was no way to defeat a configured rebase.autostash variable
+ from the command line, as "git rebase --no-autostash" was missing.
+
+ * "git log --date=local" used to only show the normal (default)
+ format in the local timezone. The command learned to take 'local'
+ as an instruction to use the local timezone with other formats,
+
+ * The refs used during a "git bisect" session is now per-worktree so
+ that independent bisect sessions can be done in different worktrees
+ created with "git worktree add".
+
+ * Users who are too busy to type three extra keystrokes to ask for
+ "git stash show -p" can now set stash.showPatch configuration
+ varible to true to always see the actual patch, not just the list
+ of paths affected with feel for the extent of damage via diffstat.
+
+ * "quiltimport" allows to specify the series file by honoring the
+ $QUILT_SERIES environment and also --series command line option.
+
+ * The use of 'good/bad' in "git bisect" made it confusing to use when
+ hunting for a state change that is not a regression (e.g. bugfix).
+ The command learned 'old/new' and then allows the end user to
+ say e.g. "bisect start --term-old=fast --term=new=slow" to find a
+ performance regression.
+
+ * "git interpret-trailers" can now run outside of a Git repository.
+
+ * "git p4" learned to reencode the pathname it uses to communicate
+ with the p4 depot with a new option.
+
+ * Give progress meter to "git filter-branch".
+
+ * Allow a later "!/abc/def" to override an earlier "/abc" that
+ appears in the same .gitignore file to make it easier to express
+ "everything in /abc directory is ignored, except for ...".
+
+ * Teach "git p4" to send large blobs outside the repository by
+ talking to Git LFS.
+
+
+Performance, Internal Implementation, Development Support etc.
+
+ * The infrastructure to rewrite "git submodule" in C is being built
+ incrementally. Let's polish these early parts well enough and make
+ them graduate to 'next' and 'master', so that the more involved
+ follow-up can start cooking on a solid ground.
+
+ * Some features from "git tag -l" and "git branch -l" have been made
+ available to "git for-each-ref" so that eventually the unified
+ implementation can be shared across all three.
+
+ * Because "test_when_finished" in our test framework queues the
+ clean-up tasks to be done in a shell variable, it should not be
+ used inside a subshell. Add a mechanism to allow 'bash' to catch
+ such uses, and fix the ones that were found.
+ (merge 0968f12 jk/test-lint-forbid-when-finished-in-subshell later to maint).
+
+ * The debugging infrastructure for pkt-line based communication has
+ been improved to mark the side-band communication specifically.
+ (merge fd89433 jk/async-pkt-line later to maint).
+
+ * Update "git branch" that list existing branches, using the
+ ref-filter API that is shared with "git tag" and "git
+ for-each-ref".
+
+
+Also contains various documentation updates and code clean-ups.
+
+
+Fixes since v2.6
+----------------
+
+Unless otherwise noted, all the fixes since v2.6 in the maintenance
+track are contained in this release (see the maintenance releases'
+notes for details).
+
+ * Very small number of options take a parameter that is optional
+ (which is not a great UI element as they can only appear at the end
+ of the command line). Add notice to documentation of each and
+ every one of them.
+ (merge 2b594bf mm/keyid-docs later to maint).
+
+ * "git blame --first-parent v1.0..v2.0" was not rejected but did not
+ limit the blame to commits on the first parent chain.
+ (merge 95a4fb0 jk/blame-first-parent later to maint).
+
+ * "git subtree" (in contrib/) now can take whitespaces in the
+ pathnames, not only in the in-tree pathname but the name of the
+ directory that the repository is in. (merge 5b6ab38
+ as/subtree-with-spaces later to maint).
+
+ * The ssh transport, just like any other transport over the network,
+ did not clear GIT_* environment variables, but it is possible to
+ use SendEnv and AcceptEnv to leak them to the remote invocation of
+ Git, which is not a good idea at all. Explicitly clear them just
+ like we do for the local transport.
+ (merge a48b409 jk/connect-clear-env later to maint).
+
+ * Correct "git p4 --detect-labels" so that it does not fail to create
+ a tag that points at a commit that is also being imported.
+ (merge b43702a ld/p4-import-labels later to maint).
+
+ * The Makefile always runs the library archiver with hardcoded "crs"
+ options, which was inconvenient for exotic platforms on which
+ people want to use programs with totally different set of command
+ line options.
+ (merge ac179b4 jw/make-arflags-customizable later to maint).
+
+ * Customization to change the behaviour with "make -w" and "make -s"
+ in our Makefile was broken when they were used together.
+ (merge ef49e05 jk/make-findstring-makeflags-fix later to maint).
+
+ * Allocation related functions and stdio are unsafe things to call
+ inside a signal handler, and indeed killing the pager can cause
+ glibc to deadlock waiting on allocation mutex as our signal handler
+ tries to free() some data structures in wait_for_pager(). Reduce
+ these unsafe calls.
+ (merge 507d780 ti/glibc-stdio-mutex-from-signal-handler later to maint).
+
+ * The way how --ref/--notes to specify the notes tree reference are
+ DWIMmed was not clearly documented.
+ (merge e14c92e jk/notes-dwim-doc later to maint).
+
+ * "git gc" used to barf when a symbolic ref has gone dangling
+ (e.g. the branch that used to be your upstream's default when you
+ cloned from it is now gone, and you did "fetch --prune").
+ (merge 14886b4 js/gc-with-stale-symref later to maint).
+
+ * "git clone --dissociate" runs a big "git repack" process at the
+ end, and it helps to close file descriptors that are open on the
+ packs and their idx files before doing so on filesystems that
+ cannot remove a file that is still open.
+ (merge 786b150 js/clone-dissociate later to maint).
+
+ * Description of the "log.follow" configuration variable in "git log"
+ documentation is now also copied to "git config" documentation.
+ (merge fd8d07e dt/log-follow-config later to maint).
+
+ * "git rebase -i" had a minor regression recently, which stopped
+ considering a line that begins with an indented '#' in its insn
+ sheet not a comment, which is now fixed.
+ (merge 1db168e gr/rebase-i-drop-warn later to maint).
+
+ * After "git checkout --detach", "git status" reported a fairly
+ useless "HEAD detached at HEAD", instead of saying at which exact
+ commit.
+ (merge 0eb8548 mm/detach-at-HEAD-reflog later to maint).
+
+ * When "git send-email" wanted to talk over Net::SMTP::SSL,
+ Net::Cmd::datasend() did not like to be fed too many bytes at the
+ same time and failed to send messages. Send the payload one line
+ at a time to work around the problem.
+ (merge f60c483 sa/send-email-smtp-batch-data-limit later to maint).
+
+ * When "git am" was rewritten as a built-in, it stopped paying
+ attention to user.signingkey, which was fixed.
+ (merge 434c64d pt/am-builtin later to maint).
+
+ * It was not possible to use a repository-lookalike created by "git
+ worktree add" as a local source of "git clone".
+ (merge d78db84 nd/clone-linked-checkout later to maint).
+
+ * On a case insensitive filesystems, setting GIT_WORK_TREE variable
+ using a random cases that does not agree with what the filesystem
+ thinks confused Git that it wasn't inside the working tree.
+ (merge 63ec5e1 js/icase-wt-detection later to maint).
+
+ * Performance-measurement tests did not work without an installed Git.
+ (merge 31cd128 sb/perf-without-installed-git later to maint).
+
+ * A test script for the HTTP service had a timing dependent bug,
+ which was fixed.
+ (merge 362d8b6 sb/http-flaky-test-fix later to maint).
+
+ * There were some classes of errors that "git fsck" diagnosed to its
+ standard error that did not cause it to exit with non-zero status.
+ (merge 122f76f jc/fsck-dropped-errors later to maint).
+
+ * Work around "git p4" failing when the P4 depot records the contents
+ in UTF-16 without UTF-16 BOM.
+ (merge 1f5f390 ls/p4-translation-failure later to maint).
+
+ * When "git gc --auto" is backgrounded, its diagnosis message is
+ lost. Save it to a file in $GIT_DIR and show it next time the "gc
+ --auto" is run.
+ (merge 329e6e8 nd/gc-auto-background-fix later to maint).
+
+ * The submodule code has been taught to work better with separate
+ work trees created via "git worktree add".
+ (merge 11f9dd7 mk/submodule-gitdir-path later to maint).
+
+ * Code clean-up and minor fixes.
+ (merge 15ed07d jc/rerere later to maint).
+ (merge b744767 pt/pull-builtin later to maint).
+ (merge 29bc480 nd/ls-remote-does-not-have-u-option later to maint).
+ (merge be510e0 jk/asciidoctor-section-heading-markup-fix later to maint).
+ (merge 83e6bda tk/typofix-connect-unknown-proto-error later to maint).
+ (merge a43eb67 tk/doc-interpret-trailers-grammo later to maint).
+ (merge ba128e2 es/worktree-add-cleanup later to maint).
+ (merge 44cd91e cc/quote-comments later to maint).
diff --git a/Documentation/blame-options.txt b/Documentation/blame-options.txt
index a09969ba08..760eab7428 100644
--- a/Documentation/blame-options.txt
+++ b/Documentation/blame-options.txt
@@ -63,11 +63,10 @@ include::line-range-format.txt[]
`-` to make the command read from the standard input).
--date <format>::
- The value is one of the following alternatives:
- {relative,local,default,iso,rfc,short}. If --date is not
+ Specifies the format used to output dates. If --date is not
provided, the value of the blame.date config variable is
used. If the blame.date config variable is also not set, the
- iso format is used. For more information, See the discussion
+ iso format is used. For supported values, see the discussion
of the --date option at linkgit:git-log[1].
-M|<num>|::
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 0cc87a6f65..391a0c3c85 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -1829,9 +1829,7 @@ log.abbrevCommit::
log.date::
Set the default date-time mode for the 'log' command.
Setting a value for log.date is similar to using 'git log''s
- `--date` option. Possible values are `relative`, `local`,
- `default`, `iso`, `rfc`, and `short`; see linkgit:git-log[1]
- for details.
+ `--date` option. See linkgit:git-log[1] for details.
log.decorate::
Print out the ref names of any commits that are shown by the log
@@ -1840,6 +1838,12 @@ log.decorate::
specified, the full ref name (including prefix) will be printed.
This is the same as the log commands '--decorate' option.
+log.follow::
+ If `true`, `git log` will act as if the `--follow` option was used when
+ a single <path> is given. This has the same limitations as `--follow`,
+ i.e. it cannot be used to follow multiple files and does not work well
+ on non-linear history.
+
log.showRoot::
If true, the initial commit will be shown as a big creation event.
This is equivalent to a diff against an empty tree.
@@ -2587,6 +2591,16 @@ status.submoduleSummary::
submodule summary' command, which shows a similar output but does
not honor these settings.
+stash.showPatch::
+ If this is set to true, the `git stash show` command without an
+ option will show the stash in patch form. Defaults to false.
+ See description of 'show' command in linkgit:git-stash[1].
+
+stash.showStat::
+ If this is set to true, the `git stash show` command without an
+ option will show diffstat of the stash. Defaults to true.
+ See description of 'show' command in linkgit:git-stash[1].
+
submodule.<name>.path::
submodule.<name>.url::
The path within this project and URL for a submodule. These
diff --git a/Documentation/git-am.txt b/Documentation/git-am.txt
index dbea6e7ae9..452c1feb23 100644
--- a/Documentation/git-am.txt
+++ b/Documentation/git-am.txt
@@ -141,7 +141,9 @@ default. You can use `--no-utf8` to override this.
-S[<keyid>]::
--gpg-sign[=<keyid>]::
- GPG-sign commits.
+ GPG-sign commits. The `keyid` argument is optional and
+ defaults to the committer identity; if specified, it must be
+ stuck to the option without a space.
--continue::
-r::
diff --git a/Documentation/git-bisect-lk2009.txt b/Documentation/git-bisect-lk2009.txt
index 0f0c6ff082..c06efbd42a 100644
--- a/Documentation/git-bisect-lk2009.txt
+++ b/Documentation/git-bisect-lk2009.txt
@@ -1321,7 +1321,7 @@ So git bisect is unconditional goodness - and feel free to quote that
_____________
Acknowledgments
-----------------
+---------------
Many thanks to Junio Hamano for his help in reviewing this paper, for
reviewing the patches I sent to the Git mailing list, for discussing
diff --git a/Documentation/git-bisect.txt b/Documentation/git-bisect.txt
index e97f2de21b..2044fe6820 100644
--- a/Documentation/git-bisect.txt
+++ b/Documentation/git-bisect.txt
@@ -16,9 +16,11 @@ DESCRIPTION
The command takes various subcommands, and different options depending
on the subcommand:
- git bisect start [--no-checkout] [<bad> [<good>...]] [--] [<paths>...]
- git bisect bad [<rev>]
- git bisect good [<rev>...]
+ git bisect start [--term-{old,good}=<term> --term-{new,bad}=<term>]
+ [--no-checkout] [<bad> [<good>...]] [--] [<paths>...]
+ git bisect (bad|new) [<rev>]
+ git bisect (good|old) [<rev>...]
+ git bisect terms [--term-good | --term-bad]
git bisect skip [(<rev>|<range>)...]
git bisect reset [<commit>]
git bisect visualize
@@ -36,6 +38,13 @@ whether the selected commit is "good" or "bad". It continues narrowing
down the range until it finds the exact commit that introduced the
change.
+In fact, `git bisect` can be used to find the commit that changed
+*any* property of your project; e.g., the commit that fixed a bug, or
+the commit that caused a benchmark's performance to improve. To
+support this more general usage, the terms "old" and "new" can be used
+in place of "good" and "bad", or you can choose your own terms. See
+section "Alternate terms" below for more information.
+
Basic bisect commands: start, bad, good
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -111,6 +120,79 @@ bad revision, while `git bisect reset HEAD` will leave you on the
current bisection commit and avoid switching commits at all.
+Alternate terms
+~~~~~~~~~~~~~~~
+
+Sometimes you are not looking for the commit that introduced a
+breakage, but rather for a commit that caused a change between some
+other "old" state and "new" state. For example, you might be looking
+for the commit that introduced a particular fix. Or you might be
+looking for the first commit in which the source-code filenames were
+finally all converted to your company's naming standard. Or whatever.
+
+In such cases it can be very confusing to use the terms "good" and
+"bad" to refer to "the state before the change" and "the state after
+the change". So instead, you can use the terms "old" and "new",
+respectively, in place of "good" and "bad". (But note that you cannot
+mix "good" and "bad" with "old" and "new" in a single session.)
+
+In this more general usage, you provide `git bisect` with a "new"
+commit has some property and an "old" commit that doesn't have that
+property. Each time `git bisect` checks out a commit, you test if that
+commit has the property. If it does, mark the commit as "new";
+otherwise, mark it as "old". When the bisection is done, `git bisect`
+will report which commit introduced the property.
+
+To use "old" and "new" instead of "good" and bad, you must run `git
+bisect start` without commits as argument and then run the following
+commands to add the commits:
+
+------------------------------------------------
+git bisect old [<rev>]
+------------------------------------------------
+
+to indicate that a commit was before the sought change, or
+
+------------------------------------------------
+git bisect new [<rev>...]
+------------------------------------------------
+
+to indicate that it was after.
+
+To get a reminder of the currently used terms, use
+
+------------------------------------------------
+git bisect terms
+------------------------------------------------
+
+You can get just the old (respectively new) term with `git bisect term
+--term-old` or `git bisect term --term-good`.
+
+If you would like to use your own terms instead of "bad"/"good" or
+"new"/"old", you can choose any names you like (except existing bisect
+subcommands like `reset`, `start`, ...) by starting the
+bisection using
+
+------------------------------------------------
+git bisect start --term-old <term-old> --term-new <term-new>
+------------------------------------------------
+
+For example, if you are looking for a commit that introduced a
+performance regression, you might use
+
+------------------------------------------------
+git bisect start --term-old fast --term-new slow
+------------------------------------------------
+
+Or if you are looking for the commit that fixed a bug, you might use
+
+------------------------------------------------
+git bisect start --term-new fixed --term-old broken
+------------------------------------------------
+
+Then, use `git bisect <term-old>` and `git bisect <term-new>` instead
+of `git bisect good` and `git bisect bad` to mark commits.
+
Bisect visualize
~~~~~~~~~~~~~~~~
@@ -387,6 +469,21 @@ In this case, when 'git bisect run' finishes, bisect/bad will refer to a commit
has at least one parent whose reachable graph is fully traversable in the sense
required by 'git pack objects'.
+* Look for a fix instead of a regression in the code
++
+------------
+$ git bisect start
+$ git bisect new HEAD # current commit is marked as new
+$ git bisect old HEAD~10 # the tenth commit from now is marked as old
+------------
++
+or:
+------------
+$ git bisect start --term-old broken --term-new fixed
+$ git bisect fixed
+$ git bisect broken HEAD~10
+------------
+
Getting help
~~~~~~~~~~~~
diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt
index bbbade4f51..4a7037f1c8 100644
--- a/Documentation/git-branch.txt
+++ b/Documentation/git-branch.txt
@@ -11,7 +11,8 @@ SYNOPSIS
'git branch' [--color[=<when>] | --no-color] [-r | -a]
[--list] [-v [--abbrev=<length> | --no-abbrev]]
[--column[=<options>] | --no-column]
- [(--merged | --no-merged | --contains) [<commit>]] [<pattern>...]
+ [(--merged | --no-merged | --contains) [<commit>]] [--sort=<key>]
+ [--points-at <object>] [<pattern>...]
'git branch' [--set-upstream | --track | --no-track] [-l] [-f] <branchname> [<start-point>]
'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
'git branch' --unset-upstream [<branchname>]
@@ -231,6 +232,19 @@ start-point is either a local or remote-tracking branch.
The new name for an existing branch. The same restrictions as for
<branchname> apply.
+--sort=<key>::
+ Sort based on the key given. Prefix `-` to sort in descending
+ order of the value. You may use the --sort=<key> option
+ multiple times, in which case the last key becomes the primary
+ key. The keys supported are the same as those in `git
+ for-each-ref`. Sort order defaults to sorting based on the
+ full refname (including `refs/...` prefix). This lists
+ detached HEAD (if present) first, then local branches and
+ finally remote-tracking branches.
+
+
+--points-at <object>::
+ Only list branches of the given object.
Examples
--------
diff --git a/Documentation/git-cherry-pick.txt b/Documentation/git-cherry-pick.txt
index 1147c71da6..77da29a474 100644
--- a/Documentation/git-cherry-pick.txt
+++ b/Documentation/git-cherry-pick.txt
@@ -9,7 +9,7 @@ SYNOPSIS
--------
[verse]
'git cherry-pick' [--edit] [-n] [-m parent-number] [-s] [-x] [--ff]
- [-S[<key-id>]] <commit>...
+ [-S[<keyid>]] <commit>...
'git cherry-pick' --continue
'git cherry-pick' --quit
'git cherry-pick' --abort
@@ -101,9 +101,11 @@ effect to your index in a row.
--signoff::
Add Signed-off-by line at the end of the commit message.
--S[<key-id>]::
---gpg-sign[=<key-id>]::
- GPG-sign commits.
+-S[<keyid>]::
+--gpg-sign[=<keyid>]::
+ GPG-sign commits. The `keyid` argument is optional and
+ defaults to the committer identity; if specified, it must be
+ stuck to the option without a space.
--ff::
If the current HEAD is the same as the parent of the
diff --git a/Documentation/git-commit-tree.txt b/Documentation/git-commit-tree.txt
index f5f2a8d326..a0b5457304 100644
--- a/Documentation/git-commit-tree.txt
+++ b/Documentation/git-commit-tree.txt
@@ -56,7 +56,9 @@ OPTIONS
-S[<keyid>]::
--gpg-sign[=<keyid>]::
- GPG-sign commit.
+ GPG-sign commits. The `keyid` argument is optional and
+ defaults to the committer identity; if specified, it must be
+ stuck to the option without a space.
--no-gpg-sign::
Countermand `commit.gpgSign` configuration variable that is
diff --git a/Documentation/git-commit.txt b/Documentation/git-commit.txt
index 904dafa0f7..7f34a5b331 100644
--- a/Documentation/git-commit.txt
+++ b/Documentation/git-commit.txt
@@ -13,7 +13,7 @@ SYNOPSIS
[-F <file> | -m <msg>] [--reset-author] [--allow-empty]
[--allow-empty-message] [--no-verify] [-e] [--author=<author>]
[--date=<date>] [--cleanup=<mode>] [--[no-]status]
- [-i | -o] [-S[<key-id>]] [--] [<file>...]
+ [-i | -o] [-S[<keyid>]] [--] [<file>...]
DESCRIPTION
-----------
@@ -314,7 +314,9 @@ changes to tracked files.
-S[<keyid>]::
--gpg-sign[=<keyid>]::
- GPG-sign commit.
+ GPG-sign commits. The `keyid` argument is optional and
+ defaults to the committer identity; if specified, it must be
+ stuck to the option without a space.
--no-gpg-sign::
Countermand `commit.gpgSign` configuration variable that is
diff --git a/Documentation/git-for-each-ref.txt b/Documentation/git-for-each-ref.txt
index 7f8d9a5b5f..c6f073cea4 100644
--- a/Documentation/git-for-each-ref.txt
+++ b/Documentation/git-for-each-ref.txt
@@ -10,6 +10,8 @@ SYNOPSIS
[verse]
'git for-each-ref' [--count=<count>] [--shell|--perl|--python|--tcl]
[(--sort=<key>)...] [--format=<format>] [<pattern>...]
+ [--points-at <object>] [(--merged | --no-merged) [<object>]]
+ [--contains [<object>]]
DESCRIPTION
-----------
@@ -62,6 +64,20 @@ OPTIONS
the specified host language. This is meant to produce
a scriptlet that can directly be `eval`ed.
+--points-at <object>::
+ Only list refs which points at the given object.
+
+--merged [<object>]::
+ Only list refs whose tips are reachable from the
+ specified commit (HEAD if not specified).
+
+--no-merged [<object>]::
+ Only list refs whose tips are not reachable from the
+ specified commit (HEAD if not specified).
+
+--contains [<object>]::
+ Only list tags which contain the specified commit (HEAD if not
+ specified).
FIELD NAMES
-----------
@@ -111,6 +127,17 @@ color::
Change output color. Followed by `:<colorname>`, where names
are described in `color.branch.*`.
+align::
+ Left-, middle-, or right-align the content between
+ %(align:...) and %(end). The "align:" is followed by `<width>`
+ and `<position>` in any order separated by a comma, where the
+ `<position>` is either left, right or middle, default being
+ left and `<width>` is the total length of the content with
+ alignment. If the contents length is more than the width then
+ no alignment is performed. If used with '--quote' everything
+ in between %(align:...) and %(end) is quoted, but if nested
+ then only the topmost level performs quoting.
+
In addition to the above, for commit and tag objects, the header
field names (`tree`, `parent`, `object`, `type`, and `tag`) can
be used to specify the value in the header field.
@@ -123,20 +150,23 @@ The complete message in a commit and tag object is `contents`.
Its first line is `contents:subject`, where subject is the concatenation
of all lines of the commit message up to the first blank line. The next
line is 'contents:body', where body is all of the lines after the first
-blank line. Finally, the optional GPG signature is `contents:signature`.
+blank line. The optional GPG signature is `contents:signature`. The
+first `N` lines of the message is obtained using `contents:lines=N`.
For sorting purposes, fields with numeric values sort in numeric
order (`objectsize`, `authordate`, `committerdate`, `taggerdate`).
All other fields are used to sort in their byte-value order.
+There is also an option to sort by versions, this can be done by using
+the fieldname `version:refname` or its alias `v:refname`.
+
In any case, a field name that refers to a field inapplicable to
the object referred by the ref does not cause an error. It
returns an empty string instead.
As a special case for the date-type fields, you may specify a format for
-the date by adding one of `:default`, `:relative`, `:short`, `:local`,
-`:iso8601`, `:rfc2822` or `:raw` to the end of the fieldname; e.g.
-`%(taggerdate:relative)`.
+the date by adding `:` followed by date format name (see the
+values the `--date` option to linkgit::git-rev-list[1] takes).
EXAMPLES
diff --git a/Documentation/git-grep.txt b/Documentation/git-grep.txt
index 31811f16bd..4a44d6da13 100644
--- a/Documentation/git-grep.txt
+++ b/Documentation/git-grep.txt
@@ -160,12 +160,15 @@ OPTIONS
For better compatibility with 'git diff', `--name-only` is a
synonym for `--files-with-matches`.
--O [<pager>]::
---open-files-in-pager [<pager>]::
+-O[<pager>]::
+--open-files-in-pager[=<pager>]::
Open the matching files in the pager (not the output of 'grep').
If the pager happens to be "less" or "vi", and the user
specified only one pattern, the first file is positioned at
- the first match automatically.
+ the first match automatically. The `pager` argument is
+ optional; if specified, it must be stuck to the option
+ without a space. If `pager` is unspecified, the default pager
+ will be used (see `core.pager` in linkgit:git-config[1]).
-z::
--null::
diff --git a/Documentation/git-interpret-trailers.txt b/Documentation/git-interpret-trailers.txt
index d6d9231b50..0ecd497c4d 100644
--- a/Documentation/git-interpret-trailers.txt
+++ b/Documentation/git-interpret-trailers.txt
@@ -67,7 +67,7 @@ OPTIONS
--trim-empty::
If the <value> part of any trailer contains only whitespace,
the whole trailer will be removed from the resulting message.
- This apply to existing trailers as well as new trailers.
+ This applies to existing trailers as well as new trailers.
--trailer <token>[(=|:)<value>]::
Specify a (<token>, <value>) pair that should be applied as a
diff --git a/Documentation/git-log.txt b/Documentation/git-log.txt
index 97b9993ee8..03f958029a 100644
--- a/Documentation/git-log.txt
+++ b/Documentation/git-log.txt
@@ -185,10 +185,10 @@ log.date::
dates like `Sat May 8 19:35:34 2010 -0500`.
log.follow::
- If a single <path> is given to git log, it will act as
- if the `--follow` option was also used. This has the same
- limitations as `--follow`, i.e. it cannot be used to follow
- multiple files and does not work well on non-linear history.
+ If `true`, `git log` will act as if the `--follow` option was used when
+ a single <path> is given. This has the same limitations as `--follow`,
+ i.e. it cannot be used to follow multiple files and does not work well
+ on non-linear history.
log.showRoot::
If `false`, `git log` and related commands will not treat the
diff --git a/Documentation/git-ls-remote.txt b/Documentation/git-ls-remote.txt
index 2e22915eb8..d510c05e11 100644
--- a/Documentation/git-ls-remote.txt
+++ b/Documentation/git-ls-remote.txt
@@ -9,7 +9,7 @@ git-ls-remote - List references in a remote repository
SYNOPSIS
--------
[verse]
-'git ls-remote' [--heads] [--tags] [-u <exec> | --upload-pack <exec>]
+'git ls-remote' [--heads] [--tags] [--upload-pack=<exec>]
[--exit-code] <repository> [<refs>...]
DESCRIPTION
@@ -29,7 +29,6 @@ OPTIONS
both, references stored in refs/heads and refs/tags are
displayed.
--u <exec>::
--upload-pack=<exec>::
Specify the full path of 'git-upload-pack' on the remote
host. This allows listing references from repositories accessed via
diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt
index a62d6729b9..07f7295ec8 100644
--- a/Documentation/git-merge.txt
+++ b/Documentation/git-merge.txt
@@ -10,7 +10,7 @@ SYNOPSIS
--------
[verse]
'git merge' [-n] [--stat] [--no-commit] [--squash] [--[no-]edit]
- [-s <strategy>] [-X <strategy-option>] [-S[<key-id>]]
+ [-s <strategy>] [-X <strategy-option>] [-S[<keyid>]]
[--[no-]rerere-autoupdate] [-m <msg>] [<commit>...]
'git merge' <msg> HEAD <commit>...
'git merge' --abort
@@ -67,7 +67,9 @@ include::merge-options.txt[]
-S[<keyid>]::
--gpg-sign[=<keyid>]::
- GPG-sign the resulting merge commit.
+ GPG-sign the resulting merge commit. The `keyid` argument is
+ optional and defaults to the committer identity; if specified,
+ it must be stuck to the option without a space.
-m <msg>::
Set the commit message to be used for the merge commit (in
diff --git a/Documentation/git-notes.txt b/Documentation/git-notes.txt
index a9a916f360..8de349968a 100644
--- a/Documentation/git-notes.txt
+++ b/Documentation/git-notes.txt
@@ -162,7 +162,9 @@ OPTIONS
--ref <ref>::
Manipulate the notes tree in <ref>. This overrides
'GIT_NOTES_REF' and the "core.notesRef" configuration. The ref
- is taken to be in `refs/notes/` if it is not qualified.
+ specifies the full refname when it begins with `refs/notes/`; when it
+ begins with `notes/`, `refs/` and otherwise `refs/notes/` is prefixed
+ to form a full name of the ref.
--ignore-missing::
Do not consider it an error to request removing notes from an
diff --git a/Documentation/git-p4.txt b/Documentation/git-p4.txt
index 82aa5d6073..c3ff7d0d9b 100644
--- a/Documentation/git-p4.txt
+++ b/Documentation/git-p4.txt
@@ -510,6 +510,45 @@ git-p4.useClientSpec::
option '--use-client-spec'. See the "CLIENT SPEC" section above.
This variable is a boolean, not the name of a p4 client.
+git-p4.pathEncoding::
+ Perforce keeps the encoding of a path as given by the originating OS.
+ Git expects paths encoded as UTF-8. Use this config to tell git-p4
+ what encoding Perforce had used for the paths. This encoding is used
+ to transcode the paths to UTF-8. As an example, Perforce on Windows
+ often uses “cp1252” to encode path names.
+
+git-p4.largeFileSystem::
+ Specify the system that is used for large (binary) files. Please note
+ that large file systems do not support the 'git p4 submit' command.
+ Only Git LFS [1] is implemented right now. Download
+ and install the Git LFS command line extension to use this option
+ and configure it like this:
++
+-------------
+git config git-p4.largeFileSystem GitLFS
+-------------
++
+ [1] https://git-lfs.github.com/
+
+git-p4.largeFileExtensions::
+ All files matching a file extension in the list will be processed
+ by the large file system. Do not prefix the extensions with '.'.
+
+git-p4.largeFileThreshold::
+ All files with an uncompressed size exceeding the threshold will be
+ processed by the large file system. By default the threshold is
+ defined in bytes. Add the suffix k, m, or g to change the unit.
+
+git-p4.largeFileCompressedThreshold::
+ All files with a compressed size exceeding the threshold will be
+ processed by the large file system. This option might slow down
+ your clone/sync process. By default the threshold is defined in
+ bytes. Add the suffix k, m, or g to change the unit.
+
+git-p4.largeFilePush::
+ Boolean variable which defines if large files are automatically
+ pushed to a server.
+
Submit variables
~~~~~~~~~~~~~~~~
git-p4.detectRenames::
diff --git a/Documentation/git-quiltimport.txt b/Documentation/git-quiltimport.txt
index d64388cb8e..ff633b0db7 100644
--- a/Documentation/git-quiltimport.txt
+++ b/Documentation/git-quiltimport.txt
@@ -10,6 +10,7 @@ SYNOPSIS
--------
[verse]
'git quiltimport' [--dry-run | -n] [--author <author>] [--patches <dir>]
+ [--series <file>]
DESCRIPTION
@@ -42,13 +43,19 @@ OPTIONS
information can be found in the patch description.
--patches <dir>::
- The directory to find the quilt patches and the
- quilt series file.
+ The directory to find the quilt patches.
+
The default for the patch directory is patches
or the value of the $QUILT_PATCHES environment
variable.
+--series <file>::
+ The quilt series file.
++
+The default for the series file is <patches>/series
+or the value of the $QUILT_SERIES environment
+variable.
+
GIT
---
Part of the linkgit:git[1] suite
diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
index ca039546a4..6cca8bb51d 100644
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@ -294,7 +294,9 @@ which makes little sense.
-S[<keyid>]::
--gpg-sign[=<keyid>]::
- GPG-sign commits.
+ GPG-sign commits. The `keyid` argument is optional and
+ defaults to the committer identity; if specified, it must be
+ stuck to the option without a space.
-q::
--quiet::
@@ -432,7 +434,8 @@ If the '--autosquash' option is enabled by default using the
configuration variable `rebase.autoSquash`, this option can be
used to override and disable this setting.
---[no-]autostash::
+--autostash::
+--no-autostash::
Automatically create a temporary stash before the operation
begins, and apply it after the operation ends. This means
that you can run rebase on a dirty worktree. However, use
diff --git a/Documentation/git-remote.txt b/Documentation/git-remote.txt
index 4c6d6de7b7..3c9bf45829 100644
--- a/Documentation/git-remote.txt
+++ b/Documentation/git-remote.txt
@@ -15,6 +15,7 @@ SYNOPSIS
'git remote remove' <name>
'git remote set-head' <name> (-a | --auto | -d | --delete | <branch>)
'git remote set-branches' [--add] <name> <branch>...
+'git remote get-url' [--push] [--all] <name>
'git remote set-url' [--push] <name> <newurl> [<oldurl>]
'git remote set-url --add' [--push] <name> <newurl>
'git remote set-url --delete' [--push] <name> <url>
@@ -131,6 +132,15 @@ The named branches will be interpreted as if specified with the
With `--add`, instead of replacing the list of currently tracked
branches, adds to that list.
+'get-url'::
+
+Retrieves the URLs for a remote. Configurations for `insteadOf` and
+`pushInsteadOf` are expanded here. By default, only the first URL is listed.
++
+With '--push', push URLs are queried rather than fetch URLs.
++
+With '--all', all URLs for the remote will be listed.
+
'set-url'::
Changes URLs for the remote. Sets first URL for remote <name> that matches
diff --git a/Documentation/git-rev-list.txt b/Documentation/git-rev-list.txt
index 7b49c85347..ef22f1775b 100644
--- a/Documentation/git-rev-list.txt
+++ b/Documentation/git-rev-list.txt
@@ -45,7 +45,7 @@ SYNOPSIS
[ --regexp-ignore-case | -i ]
[ --extended-regexp | -E ]
[ --fixed-strings | -F ]
- [ --date=(local|relative|default|iso|iso-strict|rfc|short) ]
+ [ --date=<format>]
[ [ --objects | --objects-edge | --objects-edge-aggressive ]
[ --unpacked ] ]
[ --pretty | --header ]
diff --git a/Documentation/git-revert.txt b/Documentation/git-revert.txt
index cceb5f2f7f..b15139ffdc 100644
--- a/Documentation/git-revert.txt
+++ b/Documentation/git-revert.txt
@@ -8,7 +8,7 @@ git-revert - Revert some existing commits
SYNOPSIS
--------
[verse]
-'git revert' [--[no-]edit] [-n] [-m parent-number] [-s] [-S[<key-id>]] <commit>...
+'git revert' [--[no-]edit] [-n] [-m parent-number] [-s] [-S[<keyid>]] <commit>...
'git revert' --continue
'git revert' --quit
'git revert' --abort
@@ -80,9 +80,11 @@ more details.
This is useful when reverting more than one commits'
effect to your index in a row.
--S[<key-id>]::
---gpg-sign[=<key-id>]::
- GPG-sign commits.
+-S[<keyid>]::
+--gpg-sign[=<keyid>]::
+ GPG-sign commits. The `keyid` argument is optional and
+ defaults to the committer identity; if specified, it must be
+ stuck to the option without a space.
-s::
--signoff::
diff --git a/Documentation/git-stash.txt b/Documentation/git-stash.txt
index 375213fe46..92df596e5f 100644
--- a/Documentation/git-stash.txt
+++ b/Documentation/git-stash.txt
@@ -95,6 +95,8 @@ show [<stash>]::
shows the latest one. By default, the command shows the diffstat, but
it will accept any format known to 'git diff' (e.g., `git stash show
-p stash@{1}` to view the second most recent stash in patch form).
+ You can use stash.showStat and/or stash.showPatch config variables
+ to change the default behavior.
pop [--index] [-q|--quiet] [<stash>]::
diff --git a/Documentation/git-status.txt b/Documentation/git-status.txt
index 335f312335..e1e8f57cdd 100644
--- a/Documentation/git-status.txt
+++ b/Documentation/git-status.txt
@@ -53,8 +53,9 @@ OPTIONS
--untracked-files[=<mode>]::
Show untracked files.
+
-The mode parameter is optional (defaults to 'all'), and is used to
-specify the handling of untracked files.
+The mode parameter is used to specify the handling of untracked files.
+It is optional: it defaults to 'all', and if specified, it must be
+stuck to the option (e.g. `-uno`, but not `-u no`).
+
The possible options are:
+
diff --git a/Documentation/git-tag.txt b/Documentation/git-tag.txt
index 84f6496bf2..7220e5eca1 100644
--- a/Documentation/git-tag.txt
+++ b/Documentation/git-tag.txt
@@ -9,11 +9,12 @@ git-tag - Create, list, delete or verify a tag object signed with GPG
SYNOPSIS
--------
[verse]
-'git tag' [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>]
+'git tag' [-a | -s | -u <keyid>] [-f] [-m <msg> | -F <file>]
<tagname> [<commit> | <object>]
'git tag' -d <tagname>...
'git tag' [-n[<num>]] -l [--contains <commit>] [--points-at <object>]
- [--column[=<options>] | --no-column] [--create-reflog] [<pattern>...]
+ [--column[=<options>] | --no-column] [--create-reflog] [--sort=<key>]
+ [--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]
'git tag' -v <tagname>...
DESCRIPTION
@@ -24,19 +25,19 @@ to delete, list or verify tags.
Unless `-f` is given, the named tag must not yet exist.
-If one of `-a`, `-s`, or `-u <key-id>` is passed, the command
+If one of `-a`, `-s`, or `-u <keyid>` is passed, the command
creates a 'tag' object, and requires a tag message. Unless
`-m <msg>` or `-F <file>` is given, an editor is started for the user to type
in the tag message.
-If `-m <msg>` or `-F <file>` is given and `-a`, `-s`, and `-u <key-id>`
+If `-m <msg>` or `-F <file>` is given and `-a`, `-s`, and `-u <keyid>`
are absent, `-a` is implied.
Otherwise just a tag reference for the SHA-1 object name of the commit object is
created (i.e. a lightweight tag).
A GnuPG signed tag object will be created when `-s` or `-u
-<key-id>` is used. When `-u <key-id>` is not used, the
+<keyid>` is used. When `-u <keyid>` is not used, the
committer identity for the current user is used to find the
GnuPG key for signing. The configuration variable `gpg.program`
is used to specify custom GnuPG binary.
@@ -63,8 +64,8 @@ OPTIONS
--sign::
Make a GPG-signed tag, using the default e-mail address's key.
--u <key-id>::
---local-user=<key-id>::
+-u <keyid>::
+--local-user=<keyid>::
Make a GPG-signed tag, using the given key.
-f::
@@ -94,14 +95,16 @@ OPTIONS
using fnmatch(3)). Multiple patterns may be given; if any of
them matches, the tag is shown.
---sort=<type>::
- Sort in a specific order. Supported type is "refname"
- (lexicographic order), "version:refname" or "v:refname" (tag
+--sort=<key>::
+ Sort based on the key given. Prefix `-` to sort in
+ descending order of the value. You may use the --sort=<key> option
+ multiple times, in which case the last key becomes the primary
+ key. Also supports "version:refname" or "v:refname" (tag
names are treated as versions). The "version:refname" sort
order can also be affected by the
- "versionsort.prereleaseSuffix" configuration variable. Prepend
- "-" to reverse sort order. When this option is not given, the
- sort order defaults to the value configured for the 'tag.sort'
+ "versionsort.prereleaseSuffix" configuration variable.
+ The keys supported are the same as those in `git for-each-ref`.
+ Sort order defaults to the value configured for the 'tag.sort'
variable if it exists, or lexicographic order otherwise. See
linkgit:git-config[1].
@@ -125,14 +128,14 @@ This option is only applicable when listing tags without annotation lines.
Use the given tag message (instead of prompting).
If multiple `-m` options are given, their values are
concatenated as separate paragraphs.
- Implies `-a` if none of `-a`, `-s`, or `-u <key-id>`
+ Implies `-a` if none of `-a`, `-s`, or `-u <keyid>`
is given.
-F <file>::
--file=<file>::
Take the tag message from the given file. Use '-' to
read the message from the standard input.
- Implies `-a` if none of `-a`, `-s`, or `-u <key-id>`
+ Implies `-a` if none of `-a`, `-s`, or `-u <keyid>`
is given.
--cleanup=<mode>::
@@ -156,6 +159,16 @@ This option is only applicable when listing tags without annotation lines.
The object that the new tag will refer to, usually a commit.
Defaults to HEAD.
+<format>::
+ A string that interpolates `%(fieldname)` from the object
+ pointed at by a ref being shown. The format is the same as
+ that of linkgit:git-for-each-ref[1]. When unspecified,
+ defaults to `%(refname:short)`.
+
+--[no-]merged [<commit>]::
+ Only list tags whose tips are reachable, or not reachable
+ if '--no-merged' is used, from the specified commit ('HEAD'
+ if not specified).
CONFIGURATION
-------------
@@ -166,7 +179,7 @@ it in the repository configuration as follows:
-------------------------------------
[user]
- signingKey = <gpg-key-id>
+ signingKey = <gpg-keyid>
-------------------------------------
diff --git a/Documentation/gitignore.txt b/Documentation/gitignore.txt
index 473623d631..79a1948a0b 100644
--- a/Documentation/gitignore.txt
+++ b/Documentation/gitignore.txt
@@ -82,12 +82,12 @@ PATTERN FORMAT
- An optional prefix "`!`" which negates the pattern; any
matching file excluded by a previous pattern will become
- included again. It is not possible to re-include a file if a parent
- directory of that file is excluded. Git doesn't list excluded
- directories for performance reasons, so any patterns on contained
- files have no effect, no matter where they are defined.
+ included again.
Put a backslash ("`\`") in front of the first "`!`" for patterns
that begin with a literal "`!`", for example, "`\!important!.txt`".
+ It is possible to re-include a file if a parent directory of that
+ file is excluded if certain conditions are met. See section NOTES
+ for detail.
- If the pattern ends with a slash, it is removed for the
purpose of the following description, but it would only find
@@ -141,6 +141,21 @@ not tracked by Git remain untracked.
To stop tracking a file that is currently tracked, use
'git rm --cached'.
+To re-include files or directories when their parent directory is
+excluded, the following conditions must be met:
+
+ - The rules to exclude a directory and re-include a subset back must
+ be in the same .gitignore file.
+
+ - The directory part in the re-include rules must be literal (i.e. no
+ wildcards)
+
+ - The rules to exclude the parent directory must not end with a
+ trailing slash.
+
+ - The rules to exclude the parent directory must have at least one
+ slash.
+
EXAMPLES
--------
diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt
index 8c6478b2f2..e225974253 100644
--- a/Documentation/glossary-content.txt
+++ b/Documentation/glossary-content.txt
@@ -413,8 +413,9 @@ exclude;;
[[def_per_worktree_ref]]per-worktree ref::
Refs that are per-<<def_working_tree,worktree>>, rather than
- global. This is presently only <<def_HEAD,HEAD>>, but might
- later include other unusual refs.
+ global. This is presently only <<def_HEAD,HEAD>> and any refs
+ that start with `refs/bisect/`, but might later include other
+ unusual refs.
[[def_pseudoref]]pseudoref::
Pseudorefs are a class of files under `$GIT_DIR` which behave
diff --git a/Documentation/pretty-options.txt b/Documentation/pretty-options.txt
index 8d6c5cec4c..4b659ac1a6 100644
--- a/Documentation/pretty-options.txt
+++ b/Documentation/pretty-options.txt
@@ -55,8 +55,9 @@ By default, the notes shown are from the notes refs listed in the
environment overrides). See linkgit:git-config[1] for more details.
+
With an optional '<ref>' argument, show this notes ref instead of the
-default notes ref(s). The ref is taken to be in `refs/notes/` if it
-is not qualified.
+default notes ref(s). The ref specifies the full refname when it begins
+with `refs/notes/`; when it begins with `notes/`, `refs/` and otherwise
+`refs/notes/` is prefixed to form a full name of the ref.
+
Multiple --notes options can be combined to control which notes are
being displayed. Examples: "--notes=foo" will show only notes from
diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt
index f1c52208f0..4f009d4424 100644
--- a/Documentation/rev-list-options.txt
+++ b/Documentation/rev-list-options.txt
@@ -701,15 +701,19 @@ include::pretty-options.txt[]
--relative-date::
Synonym for `--date=relative`.
---date=(relative|local|default|iso|iso-strict|rfc|short|raw)::
+--date=<format>::
Only takes effect for dates shown in human-readable format, such
as when using `--pretty`. `log.date` config variable sets a default
- value for the log command's `--date` option.
+ value for the log command's `--date` option. By default, dates
+ are shown in the original time zone (either committer's or
+ author's). If `-local` is appended to the format (e.g.,
+ `iso-local`), the user's local time zone is used instead.
+
`--date=relative` shows dates relative to the current time,
-e.g. ``2 hours ago''.
+e.g. ``2 hours ago''. The `-local` option cannot be used with
+`--raw` or `--relative`.
+
-`--date=local` shows timestamps in user's local time zone.
+`--date=local` is an alias for `--date=default-local`.
+
`--date=iso` (or `--date=iso8601`) shows timestamps in a ISO 8601-like format.
The differences to the strict ISO 8601 format are:
@@ -732,10 +736,15 @@ format, often found in email messages.
`--date=format:...` feeds the format `...` to your system `strftime`.
Use `--date=format:%c` to show the date in your system locale's
preferred format. See the `strftime` manual for a complete list of
-format placeholders.
+format placeholders. When using `-local`, the correct syntax is
+`--date=format-local:...`.
+
-`--date=default` shows timestamps in the original time zone
-(either committer's or author's).
+`--date=default` is the default format, and is similar to
+`--date=rfc2822`, with a few exceptions:
+
+ - there is no comma after the day-of-week
+
+ - the time zone is omitted when the local time zone is used
ifdef::git-rev-list[]
--header::
diff --git a/Documentation/user-manual.txt b/Documentation/user-manual.txt
index 68978f5338..1b7987e737 100644
--- a/Documentation/user-manual.txt
+++ b/Documentation/user-manual.txt
@@ -3424,7 +3424,7 @@ just missing one particular blob version.
[[the-index]]
The index
------------
+---------
The index is a binary file (generally kept in `.git/index`) containing a
sorted list of path names, each with permissions and the SHA-1 of a blob
diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN
index e1aba8533f..94e40c4000 100755
--- a/GIT-VERSION-GEN
+++ b/GIT-VERSION-GEN
@@ -1,7 +1,7 @@
#!/bin/sh
GVF=GIT-VERSION-FILE
-DEF_VER=v2.6.1
+DEF_VER=v2.6.0.GIT
LF='
'
diff --git a/Makefile b/Makefile
index 8d5df7ea1e..36bc54ccd1 100644
--- a/Makefile
+++ b/Makefile
@@ -375,6 +375,9 @@ ALL_CFLAGS = $(CPPFLAGS) $(CFLAGS)
ALL_LDFLAGS = $(LDFLAGS)
STRIP ?= strip
+# Create as necessary, replace existing, make ranlib unneeded.
+ARFLAGS = rcs
+
# Among the variables below, these:
# gitexecdir
# template_dir
@@ -902,6 +905,7 @@ BUILTIN_OBJS += builtin/shortlog.o
BUILTIN_OBJS += builtin/show-branch.o
BUILTIN_OBJS += builtin/show-ref.o
BUILTIN_OBJS += builtin/stripspace.o
+BUILTIN_OBJS += builtin/submodule--helper.o
BUILTIN_OBJS += builtin/symbolic-ref.o
BUILTIN_OBJS += builtin/tag.o
BUILTIN_OBJS += builtin/unpack-file.o
@@ -1465,13 +1469,13 @@ endif
QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir
QUIET_SUBDIR1 =
-ifneq ($(findstring $(MAKEFLAGS),w),w)
+ifneq ($(findstring w,$(MAKEFLAGS)),w)
PRINT_DIR = --no-print-directory
else # "make -w"
NO_SUBDIR = :
endif
-ifneq ($(findstring $(MAKEFLAGS),s),s)
+ifneq ($(findstring s,$(MAKEFLAGS)),s)
ifndef V
QUIET_CC = @echo ' ' CC $@;
QUIET_AR = @echo ' ' AR $@;
@@ -1995,13 +1999,13 @@ $(REMOTE_CURL_PRIMARY): remote-curl.o http.o http-walker.o GIT-LDFLAGS $(GITLIBS
$(LIBS) $(CURL_LIBCURL) $(EXPAT_LIBEXPAT)
$(LIB_FILE): $(LIB_OBJS)
- $(QUIET_AR)$(RM) $@ && $(AR) rcs $@ $^
+ $(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^
$(XDIFF_LIB): $(XDIFF_OBJS)
- $(QUIET_AR)$(RM) $@ && $(AR) rcs $@ $^
+ $(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^
$(VCSSVN_LIB): $(VCSSVN_OBJS)
- $(QUIET_AR)$(RM) $@ && $(AR) rcs $@ $^
+ $(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^
export DEFAULT_EDITOR DEFAULT_PAGER
diff --git a/RelNotes b/RelNotes
index def6ebd430..3ba13ce253 120000
--- a/RelNotes
+++ b/RelNotes
@@ -1 +1 @@
-Documentation/RelNotes/2.6.1.txt \ No newline at end of file
+Documentation/RelNotes/2.7.0.txt \ No newline at end of file
diff --git a/advice.c b/advice.c
index 4965686e19..4dc5cf10a8 100644
--- a/advice.c
+++ b/advice.c
@@ -100,7 +100,7 @@ void NORETURN die_conclude_merge(void)
{
error(_("You have not concluded your merge (MERGE_HEAD exists)."));
if (advice_resolve_conflict)
- advise(_("Please, commit your changes before you can merge."));
+ advise(_("Please, commit your changes before merging."));
die(_("Exiting because of unfinished merge."));
}
diff --git a/bisect.c b/bisect.c
index 041a13d093..053d1a2ab9 100644
--- a/bisect.c
+++ b/bisect.c
@@ -730,6 +730,11 @@ static void handle_bad_merge_base(void)
"This means the bug has been fixed "
"between %s and [%s].\n",
bad_hex, bad_hex, good_hex);
+ } else if (!strcmp(term_bad, "new") && !strcmp(term_good, "old")) {
+ fprintf(stderr, "The merge base %s is new.\n"
+ "The property has changed "
+ "between %s and [%s].\n",
+ bad_hex, bad_hex, good_hex);
} else {
fprintf(stderr, "The merge base %s is %s.\n"
"This means the first '%s' commit is "
@@ -762,11 +767,11 @@ static void handle_skipped_merge_base(const unsigned char *mb)
}
/*
- * "check_merge_bases" checks that merge bases are not "bad".
+ * "check_merge_bases" checks that merge bases are not "bad" (or "new").
*
- * - If one is "bad", it means the user assumed something wrong
+ * - If one is "bad" (or "new"), it means the user assumed something wrong
* and we must exit with a non 0 error code.
- * - If one is "good", that's good, we have nothing to do.
+ * - If one is "good" (or "old"), that's good, we have nothing to do.
* - If one is "skipped", we can't know but we should warn.
* - If we don't know, we should check it out and ask the user to test.
*/
diff --git a/builtin.h b/builtin.h
index 79aaf0afe8..6b95006a0a 100644
--- a/builtin.h
+++ b/builtin.h
@@ -120,6 +120,7 @@ extern int cmd_show(int argc, const char **argv, const char *prefix);
extern int cmd_show_branch(int argc, const char **argv, const char *prefix);
extern int cmd_status(int argc, const char **argv, const char *prefix);
extern int cmd_stripspace(int argc, const char **argv, const char *prefix);
+extern int cmd_submodule__helper(int argc, const char **argv, const char *prefix);
extern int cmd_symbolic_ref(int argc, const char **argv, const char *prefix);
extern int cmd_tag(int argc, const char **argv, const char *prefix);
extern int cmd_tar_tree(int argc, const char **argv, const char *prefix);
diff --git a/builtin/am.c b/builtin/am.c
index 4f77e07b95..3bd4fd701b 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -2208,6 +2208,17 @@ enum resume_mode {
RESUME_ABORT
};
+static int git_am_config(const char *k, const char *v, void *cb)
+{
+ int status;
+
+ status = git_gpg_config(k, v, NULL);
+ if (status)
+ return status;
+
+ return git_default_config(k, v, NULL);
+}
+
int cmd_am(int argc, const char **argv, const char *prefix)
{
struct am_state state;
@@ -2308,7 +2319,7 @@ int cmd_am(int argc, const char **argv, const char *prefix)
OPT_END()
};
- git_config(git_default_config, NULL);
+ git_config(git_am_config, NULL);
am_state_init(&state, git_path("rebase-apply"));
diff --git a/builtin/blame.c b/builtin/blame.c
index 245d253d04..203a981cd0 100644
--- a/builtin/blame.c
+++ b/builtin/blame.c
@@ -1371,8 +1371,15 @@ static void pass_whole_blame(struct scoreboard *sb,
*/
static struct commit_list *first_scapegoat(struct rev_info *revs, struct commit *commit)
{
- if (!reverse)
+ if (!reverse) {
+ if (revs->first_parent_only &&
+ commit->parents &&
+ commit->parents->next) {
+ free_commit_list(commit->parents->next);
+ commit->parents->next = NULL;
+ }
return commit->parents;
+ }
return lookup_decoration(&revs->children, &commit->object);
}
@@ -2605,7 +2612,6 @@ parse_done:
fewer display columns. */
blame_date_width = utf8_strwidth(_("4 years, 11 months ago")) + 1; /* add the null */
break;
- case DATE_LOCAL:
case DATE_NORMAL:
blame_date_width = sizeof("Thu Oct 19 16:00:04 2006 -0700");
break;
@@ -2685,6 +2691,8 @@ parse_done:
}
else if (contents_from)
die("--contents and --children do not blend well.");
+ else if (revs.first_parent_only)
+ die("combining --first-parent and --reverse is not supported");
else {
final_commit_name = prepare_initial(&sb);
sb.commits.compare = compare_commits_by_reverse_commit_date;
diff --git a/builtin/branch.c b/builtin/branch.c
index ff05869949..01f9530822 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -19,18 +19,17 @@
#include "column.h"
#include "utf8.h"
#include "wt-status.h"
+#include "ref-filter.h"
static const char * const builtin_branch_usage[] = {
N_("git branch [<options>] [-r | -a] [--merged | --no-merged]"),
N_("git branch [<options>] [-l] [-f] <branch-name> [<start-point>]"),
N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"),
+ N_("git branch [<options>] [-r | -a] [--points-at]"),
NULL
};
-#define REF_LOCAL_BRANCH 0x01
-#define REF_REMOTE_BRANCH 0x02
-
static const char *head;
static unsigned char head_sha1[20];
@@ -52,13 +51,6 @@ enum color_branch {
BRANCH_COLOR_UPSTREAM = 5
};
-static enum merge_filter {
- NO_FILTER = 0,
- SHOW_NOT_MERGED,
- SHOW_MERGED
-} merge_filter;
-static unsigned char merge_filter_ref[20];
-
static struct string_list output = STRING_LIST_INIT_DUP;
static unsigned int colopts;
@@ -121,7 +113,7 @@ static int branch_merged(int kind, const char *name,
void *reference_name_to_free = NULL;
int merged;
- if (kind == REF_LOCAL_BRANCH) {
+ if (kind == FILTER_REFS_BRANCHES) {
struct branch *branch = branch_get(name);
const char *upstream = branch_get_upstream(branch, NULL);
unsigned char sha1[20];
@@ -199,14 +191,14 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
struct strbuf bname = STRBUF_INIT;
switch (kinds) {
- case REF_REMOTE_BRANCH:
+ case FILTER_REFS_REMOTES:
fmt = "refs/remotes/%s";
/* For subsequent UI messages */
remote_branch = 1;
force = 1;
break;
- case REF_LOCAL_BRANCH:
+ case FILTER_REFS_BRANCHES:
fmt = "refs/heads/%s";
break;
default:
@@ -223,7 +215,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
int flags = 0;
strbuf_branchname(&bname, argv[i]);
- if (kinds == REF_LOCAL_BRANCH && !strcmp(head, bname.buf)) {
+ if (kinds == FILTER_REFS_BRANCHES && !strcmp(head, bname.buf)) {
error(_("Cannot delete the branch '%s' "
"which you are currently on."), bname.buf);
ret = 1;
@@ -279,147 +271,6 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
return(ret);
}
-struct ref_item {
- char *name;
- char *dest;
- unsigned int kind, width;
- struct commit *commit;
- int ignore;
-};
-
-struct ref_list {
- struct rev_info revs;
- int index, alloc, maxwidth, verbose, abbrev;
- struct ref_item *list;
- struct commit_list *with_commit;
- int kinds;
-};
-
-static char *resolve_symref(const char *src, const char *prefix)
-{
- unsigned char sha1[20];
- int flag;
- const char *dst;
-
- dst = resolve_ref_unsafe(src, 0, sha1, &flag);
- if (!(dst && (flag & REF_ISSYMREF)))
- return NULL;
- if (prefix)
- skip_prefix(dst, prefix, &dst);
- return xstrdup(dst);
-}
-
-struct append_ref_cb {
- struct ref_list *ref_list;
- const char **pattern;
- int ret;
-};
-
-static int match_patterns(const char **pattern, const char *refname)
-{
- if (!*pattern)
- return 1; /* no pattern always matches */
- while (*pattern) {
- if (!wildmatch(*pattern, refname, 0, NULL))
- return 1;
- pattern++;
- }
- return 0;
-}
-
-static int append_ref(const char *refname, const struct object_id *oid, int flags, void *cb_data)
-{
- struct append_ref_cb *cb = (struct append_ref_cb *)(cb_data);
- struct ref_list *ref_list = cb->ref_list;
- struct ref_item *newitem;
- struct commit *commit;
- int kind, i;
- const char *prefix, *orig_refname = refname;
-
- static struct {
- int kind;
- const char *prefix;
- } ref_kind[] = {
- { REF_LOCAL_BRANCH, "refs/heads/" },
- { REF_REMOTE_BRANCH, "refs/remotes/" },
- };
-
- /* Detect kind */
- for (i = 0; i < ARRAY_SIZE(ref_kind); i++) {
- prefix = ref_kind[i].prefix;
- if (skip_prefix(refname, prefix, &refname)) {
- kind = ref_kind[i].kind;
- break;
- }
- }
- if (ARRAY_SIZE(ref_kind) <= i)
- return 0;
-
- /* Don't add types the caller doesn't want */
- if ((kind & ref_list->kinds) == 0)
- return 0;
-
- if (!match_patterns(cb->pattern, refname))
- return 0;
-
- commit = NULL;
- if (ref_list->verbose || ref_list->with_commit || merge_filter != NO_FILTER) {
- commit = lookup_commit_reference_gently(oid->hash, 1);
- if (!commit) {
- cb->ret = error(_("branch '%s' does not point at a commit"), refname);
- return 0;
- }
-
- /* Filter with with_commit if specified */
- if (!is_descendant_of(commit, ref_list->with_commit))
- return 0;
-
- if (merge_filter != NO_FILTER)
- add_pending_object(&ref_list->revs,
- (struct object *)commit, refname);
- }
-
- ALLOC_GROW(ref_list->list, ref_list->index + 1, ref_list->alloc);
-
- /* Record the new item */
- newitem = &(ref_list->list[ref_list->index++]);
- newitem->name = xstrdup(refname);
- newitem->kind = kind;
- newitem->commit = commit;
- newitem->width = utf8_strwidth(refname);
- newitem->dest = resolve_symref(orig_refname, prefix);
- newitem->ignore = 0;
- /* adjust for "remotes/" */
- if (newitem->kind == REF_REMOTE_BRANCH &&
- ref_list->kinds != REF_REMOTE_BRANCH)
- newitem->width += 8;
- if (newitem->width > ref_list->maxwidth)
- ref_list->maxwidth = newitem->width;
-
- return 0;
-}
-
-static void free_ref_list(struct ref_list *ref_list)
-{
- int i;
-
- for (i = 0; i < ref_list->index; i++) {
- free(ref_list->list[i].name);
- free(ref_list->list[i].dest);
- }
- free(ref_list->list);
-}
-
-static int ref_cmp(const void *r1, const void *r2)
-{
- struct ref_item *c1 = (struct ref_item *)(r1);
- struct ref_item *c2 = (struct ref_item *)(r2);
-
- if (c1->kind != c2->kind)
- return c1->kind - c2->kind;
- return strcmp(c1->name, c2->name);
-}
-
static void fill_tracking_info(struct strbuf *stat, const char *branch_name,
int show_upstream_ref)
{
@@ -482,8 +333,8 @@ static void fill_tracking_info(struct strbuf *stat, const char *branch_name,
free(ref);
}
-static void add_verbose_info(struct strbuf *out, struct ref_item *item,
- int verbose, int abbrev)
+static void add_verbose_info(struct strbuf *out, struct ref_array_item *item,
+ struct ref_filter *filter, const char *refname)
{
struct strbuf subject = STRBUF_INIT, stat = STRBUF_INIT;
const char *sub = _(" **** invalid ref ****");
@@ -494,32 +345,74 @@ static void add_verbose_info(struct strbuf *out, struct ref_item *item,
sub = subject.buf;
}
- if (item->kind == REF_LOCAL_BRANCH)
- fill_tracking_info(&stat, item->name, verbose > 1);
+ if (item->kind == FILTER_REFS_BRANCHES)
+ fill_tracking_info(&stat, refname, filter->verbose > 1);
strbuf_addf(out, " %s %s%s",
- find_unique_abbrev(item->commit->object.sha1, abbrev),
+ find_unique_abbrev(item->commit->object.sha1, filter->abbrev),
stat.buf, sub);
strbuf_release(&stat);
strbuf_release(&subject);
}
-static void print_ref_item(struct ref_item *item, int maxwidth, int verbose,
- int abbrev, int current, char *prefix)
+static char *get_head_description(void)
+{
+ struct strbuf desc = STRBUF_INIT;
+ struct wt_status_state state;
+ memset(&state, 0, sizeof(state));
+ wt_status_get_state(&state, 1);
+ if (state.rebase_in_progress ||
+ state.rebase_interactive_in_progress)
+ strbuf_addf(&desc, _("(no branch, rebasing %s)"),
+ state.branch);
+ else if (state.bisect_in_progress)
+ strbuf_addf(&desc, _("(no branch, bisect started on %s)"),
+ state.branch);
+ else if (state.detached_from) {
+ /* TRANSLATORS: make sure these match _("HEAD detached at ")
+ and _("HEAD detached from ") in wt-status.c */
+ if (state.detached_at)
+ strbuf_addf(&desc, _("(HEAD detached at %s)"),
+ state.detached_from);
+ else
+ strbuf_addf(&desc, _("(HEAD detached from %s)"),
+ state.detached_from);
+ }
+ else
+ strbuf_addstr(&desc, _("(no branch)"));
+ free(state.branch);
+ free(state.onto);
+ free(state.detached_from);
+ return strbuf_detach(&desc, NULL);
+}
+
+static void format_and_print_ref_item(struct ref_array_item *item, int maxwidth,
+ struct ref_filter *filter, const char *remote_prefix)
{
char c;
+ int current = 0;
int color;
struct strbuf out = STRBUF_INIT, name = STRBUF_INIT;
-
- if (item->ignore)
- return;
+ const char *prefix = "";
+ const char *desc = item->refname;
+ char *to_free = NULL;
switch (item->kind) {
- case REF_LOCAL_BRANCH:
- color = BRANCH_COLOR_LOCAL;
+ case FILTER_REFS_BRANCHES:
+ skip_prefix(desc, "refs/heads/", &desc);
+ if (!filter->detached && !strcmp(desc, head))
+ current = 1;
+ else
+ color = BRANCH_COLOR_LOCAL;
break;
- case REF_REMOTE_BRANCH:
+ case FILTER_REFS_REMOTES:
+ skip_prefix(desc, "refs/remotes/", &desc);
color = BRANCH_COLOR_REMOTE;
+ prefix = remote_prefix;
+ break;
+ case FILTER_REFS_DETACHED_HEAD:
+ desc = to_free = get_head_description();
+ current = 1;
break;
default:
color = BRANCH_COLOR_PLAIN;
@@ -532,8 +425,8 @@ static void print_ref_item(struct ref_item *item, int maxwidth, int verbose,
color = BRANCH_COLOR_CURRENT;
}
- strbuf_addf(&name, "%s%s", prefix, item->name);
- if (verbose) {
+ strbuf_addf(&name, "%s%s", prefix, desc);
+ if (filter->verbose) {
int utf8_compensation = strlen(name.buf) - utf8_strwidth(name.buf);
strbuf_addf(&out, "%c %s%-*s%s", c, branch_get_color(color),
maxwidth + utf8_compensation, name.buf,
@@ -542,155 +435,82 @@ static void print_ref_item(struct ref_item *item, int maxwidth, int verbose,
strbuf_addf(&out, "%c %s%s%s", c, branch_get_color(color),
name.buf, branch_get_color(BRANCH_COLOR_RESET));
- if (item->dest)
- strbuf_addf(&out, " -> %s", item->dest);
- else if (verbose)
+ if (item->symref) {
+ skip_prefix(item->symref, "refs/remotes/", &desc);
+ strbuf_addf(&out, " -> %s", desc);
+ }
+ else if (filter->verbose)
/* " f7c0c00 [ahead 58, behind 197] vcs-svn: drop obj_pool.h" */
- add_verbose_info(&out, item, verbose, abbrev);
+ add_verbose_info(&out, item, filter, desc);
if (column_active(colopts)) {
- assert(!verbose && "--column and --verbose are incompatible");
+ assert(!filter->verbose && "--column and --verbose are incompatible");
string_list_append(&output, out.buf);
} else {
printf("%s\n", out.buf);
}
strbuf_release(&name);
strbuf_release(&out);
+ free(to_free);
}
-static int calc_maxwidth(struct ref_list *refs)
-{
- int i, w = 0;
- for (i = 0; i < refs->index; i++) {
- if (refs->list[i].ignore)
- continue;
- if (refs->list[i].width > w)
- w = refs->list[i].width;
- }
- return w;
-}
-
-static char *get_head_description(void)
-{
- struct strbuf desc = STRBUF_INIT;
- struct wt_status_state state;
- memset(&state, 0, sizeof(state));
- wt_status_get_state(&state, 1);
- if (state.rebase_in_progress ||
- state.rebase_interactive_in_progress)
- strbuf_addf(&desc, _("(no branch, rebasing %s)"),
- state.branch);
- else if (state.bisect_in_progress)
- strbuf_addf(&desc, _("(no branch, bisect started on %s)"),
- state.branch);
- else if (state.detached_from) {
- /* TRANSLATORS: make sure these match _("HEAD detached at ")
- and _("HEAD detached from ") in wt-status.c */
- if (state.detached_at)
- strbuf_addf(&desc, _("(HEAD detached at %s)"),
- state.detached_from);
- else
- strbuf_addf(&desc, _("(HEAD detached from %s)"),
- state.detached_from);
- }
- else
- strbuf_addstr(&desc, _("(no branch)"));
- free(state.branch);
- free(state.onto);
- free(state.detached_from);
- return strbuf_detach(&desc, NULL);
-}
-
-static void show_detached(struct ref_list *ref_list)
+static int calc_maxwidth(struct ref_array *refs, int remote_bonus)
{
- struct commit *head_commit = lookup_commit_reference_gently(head_sha1, 1);
-
- if (head_commit && is_descendant_of(head_commit, ref_list->with_commit)) {
- struct ref_item item;
- item.name = get_head_description();
- item.width = utf8_strwidth(item.name);
- item.kind = REF_LOCAL_BRANCH;
- item.dest = NULL;
- item.commit = head_commit;
- item.ignore = 0;
- if (item.width > ref_list->maxwidth)
- ref_list->maxwidth = item.width;
- print_ref_item(&item, ref_list->maxwidth, ref_list->verbose, ref_list->abbrev, 1, "");
- free(item.name);
+ int i, max = 0;
+ for (i = 0; i < refs->nr; i++) {
+ struct ref_array_item *it = refs->items[i];
+ const char *desc = it->refname;
+ int w;
+
+ skip_prefix(it->refname, "refs/heads/", &desc);
+ skip_prefix(it->refname, "refs/remotes/", &desc);
+ w = utf8_strwidth(desc);
+
+ if (it->kind == FILTER_REFS_REMOTES)
+ w += remote_bonus;
+ if (w > max)
+ max = w;
}
+ return max;
}
-static int print_ref_list(int kinds, int detached, int verbose, int abbrev, struct commit_list *with_commit, const char **pattern)
+static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sorting)
{
int i;
- struct append_ref_cb cb;
- struct ref_list ref_list;
-
- memset(&ref_list, 0, sizeof(ref_list));
- ref_list.kinds = kinds;
- ref_list.verbose = verbose;
- ref_list.abbrev = abbrev;
- ref_list.with_commit = with_commit;
- if (merge_filter != NO_FILTER)
- init_revisions(&ref_list.revs, NULL);
- cb.ref_list = &ref_list;
- cb.pattern = pattern;
- cb.ret = 0;
- for_each_rawref(append_ref, &cb);
- if (merge_filter != NO_FILTER) {
- struct commit *filter;
- filter = lookup_commit_reference_gently(merge_filter_ref, 0);
- if (!filter)
- die(_("object '%s' does not point to a commit"),
- sha1_to_hex(merge_filter_ref));
-
- filter->object.flags |= UNINTERESTING;
- add_pending_object(&ref_list.revs,
- (struct object *) filter, "");
- ref_list.revs.limited = 1;
-
- if (prepare_revision_walk(&ref_list.revs))
- die(_("revision walk setup failed"));
-
- for (i = 0; i < ref_list.index; i++) {
- struct ref_item *item = &ref_list.list[i];
- struct commit *commit = item->commit;
- int is_merged = !!(commit->object.flags & UNINTERESTING);
- item->ignore = is_merged != (merge_filter == SHOW_MERGED);
- }
+ struct ref_array array;
+ int maxwidth = 0;
+ const char *remote_prefix = "";
- for (i = 0; i < ref_list.index; i++) {
- struct ref_item *item = &ref_list.list[i];
- clear_commit_marks(item->commit, ALL_REV_FLAGS);
- }
- clear_commit_marks(filter, ALL_REV_FLAGS);
+ /*
+ * If we are listing more than just remote branches,
+ * then remote branches will have a "remotes/" prefix.
+ * We need to account for this in the width.
+ */
+ if (filter->kind != FILTER_REFS_REMOTES)
+ remote_prefix = "remotes/";
- if (verbose)
- ref_list.maxwidth = calc_maxwidth(&ref_list);
- }
+ memset(&array, 0, sizeof(array));
- qsort(ref_list.list, ref_list.index, sizeof(struct ref_item), ref_cmp);
-
- detached = (detached && (kinds & REF_LOCAL_BRANCH));
- if (detached && match_patterns(pattern, "HEAD"))
- show_detached(&ref_list);
-
- for (i = 0; i < ref_list.index; i++) {
- int current = !detached &&
- (ref_list.list[i].kind == REF_LOCAL_BRANCH) &&
- !strcmp(ref_list.list[i].name, head);
- char *prefix = (kinds != REF_REMOTE_BRANCH &&
- ref_list.list[i].kind == REF_REMOTE_BRANCH)
- ? "remotes/" : "";
- print_ref_item(&ref_list.list[i], ref_list.maxwidth, verbose,
- abbrev, current, prefix);
- }
+ verify_ref_format("%(refname)%(symref)");
+ filter_refs(&array, filter, filter->kind | FILTER_REFS_INCLUDE_BROKEN);
- free_ref_list(&ref_list);
+ if (filter->verbose)
+ maxwidth = calc_maxwidth(&array, strlen(remote_prefix));
- if (cb.ret)
- error(_("some refs could not be read"));
+ /*
+ * If no sorting parameter is given then we default to sorting
+ * by 'refname'. This would give us an alphabetically sorted
+ * array with the 'HEAD' ref at the beginning followed by
+ * local branches 'refs/heads/...' and finally remote-tacking
+ * branches 'refs/remotes/...'.
+ */
+ if (!sorting)
+ sorting = ref_default_sorting();
+ ref_array_sort(sorting, &array);
+
+ for (i = 0; i < array.nr; i++)
+ format_and_print_ref_item(array.items[i], maxwidth, filter, remote_prefix);
- return cb.ret;
+ ref_array_clear(&array);
}
static void rename_branch(const char *oldname, const char *newname, int force)
@@ -746,20 +566,6 @@ static void rename_branch(const char *oldname, const char *newname, int force)
strbuf_release(&newsection);
}
-static int opt_parse_merge_filter(const struct option *opt, const char *arg, int unset)
-{
- merge_filter = ((opt->long_name[0] == 'n')
- ? SHOW_NOT_MERGED
- : SHOW_MERGED);
- if (unset)
- merge_filter = SHOW_NOT_MERGED; /* b/c for --no-merged */
- if (!arg)
- arg = "HEAD";
- if (get_sha1(arg, merge_filter_ref))
- die(_("malformed object name %s"), arg);
- return 0;
-}
-
static const char edit_description[] = "BRANCH_DESCRIPTION";
static int edit_branch_description(const char *branch_name)
@@ -799,17 +605,16 @@ static int edit_branch_description(const char *branch_name)
int cmd_branch(int argc, const char **argv, const char *prefix)
{
int delete = 0, rename = 0, force = 0, list = 0;
- int verbose = 0, abbrev = -1, detached = 0;
int reflog = 0, edit_description = 0;
int quiet = 0, unset_upstream = 0;
const char *new_upstream = NULL;
enum branch_track track;
- int kinds = REF_LOCAL_BRANCH;
- struct commit_list *with_commit = NULL;
+ struct ref_filter filter;
+ static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
struct option options[] = {
OPT_GROUP(N_("Generic options")),
- OPT__VERBOSE(&verbose,
+ OPT__VERBOSE(&filter.verbose,
N_("show hash and subject, give twice for upstream branch")),
OPT__QUIET(&quiet, N_("suppress informational messages")),
OPT_SET_INT('t', "track", &track, N_("set up tracking mode (see git-pull(1))"),
@@ -819,25 +624,15 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
OPT_STRING('u', "set-upstream-to", &new_upstream, "upstream", "change the upstream info"),
OPT_BOOL(0, "unset-upstream", &unset_upstream, "Unset the upstream info"),
OPT__COLOR(&branch_use_color, N_("use colored output")),
- OPT_SET_INT('r', "remotes", &kinds, N_("act on remote-tracking branches"),
- REF_REMOTE_BRANCH),
- {
- OPTION_CALLBACK, 0, "contains", &with_commit, N_("commit"),
- N_("print only branches that contain the commit"),
- PARSE_OPT_LASTARG_DEFAULT,
- parse_opt_with_commit, (intptr_t)"HEAD",
- },
- {
- OPTION_CALLBACK, 0, "with", &with_commit, N_("commit"),
- N_("print only branches that contain the commit"),
- PARSE_OPT_HIDDEN | PARSE_OPT_LASTARG_DEFAULT,
- parse_opt_with_commit, (intptr_t) "HEAD",
- },
- OPT__ABBREV(&abbrev),
+ OPT_SET_INT('r', "remotes", &filter.kind, N_("act on remote-tracking branches"),
+ FILTER_REFS_REMOTES),
+ OPT_CONTAINS(&filter.with_commit, N_("print only branches that contain the commit")),
+ OPT_WITH(&filter.with_commit, N_("print only branches that contain the commit")),
+ OPT__ABBREV(&filter.abbrev),
OPT_GROUP(N_("Specific git-branch actions:")),
- OPT_SET_INT('a', "all", &kinds, N_("list both remote-tracking and local branches"),
- REF_REMOTE_BRANCH | REF_LOCAL_BRANCH),
+ OPT_SET_INT('a', "all", &filter.kind, N_("list both remote-tracking and local branches"),
+ FILTER_REFS_REMOTES | FILTER_REFS_BRANCHES),
OPT_BIT('d', "delete", &delete, N_("delete fully merged branch"), 1),
OPT_BIT('D', NULL, &delete, N_("delete branch (even if not merged)"), 2),
OPT_BIT('m', "move", &rename, N_("move/rename a branch and its reflog"), 1),
@@ -847,22 +642,22 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
OPT_BOOL(0, "edit-description", &edit_description,
N_("edit the description for the branch")),
OPT__FORCE(&force, N_("force creation, move/rename, deletion")),
+ OPT_MERGED(&filter, N_("print only branches that are merged")),
+ OPT_NO_MERGED(&filter, N_("print only branches that are not merged")),
+ OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")),
+ OPT_CALLBACK(0 , "sort", sorting_tail, N_("key"),
+ N_("field name to sort on"), &parse_opt_ref_sorting),
{
- OPTION_CALLBACK, 0, "no-merged", &merge_filter_ref,
- N_("commit"), N_("print only not merged branches"),
- PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG,
- opt_parse_merge_filter, (intptr_t) "HEAD",
- },
- {
- OPTION_CALLBACK, 0, "merged", &merge_filter_ref,
- N_("commit"), N_("print only merged branches"),
- PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG,
- opt_parse_merge_filter, (intptr_t) "HEAD",
+ OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"),
+ N_("print only branches of the object"), 0, parse_opt_object_name
},
- OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")),
OPT_END(),
};
+ memset(&filter, 0, sizeof(filter));
+ filter.kind = FILTER_REFS_BRANCHES;
+ filter.abbrev = -1;
+
if (argc == 2 && !strcmp(argv[1], "-h"))
usage_with_options(builtin_branch_usage, options);
@@ -874,11 +669,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
if (!head)
die(_("Failed to resolve HEAD as a valid ref."));
if (!strcmp(head, "HEAD"))
- detached = 1;
+ filter.detached = 1;
else if (!skip_prefix(head, "refs/heads/", &head))
die(_("HEAD not found below refs/heads!"));
- hashcpy(merge_filter_ref, head_sha1);
-
argc = parse_options(argc, argv, prefix, options, builtin_branch_usage,
0);
@@ -886,17 +679,17 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
if (!delete && !rename && !edit_description && !new_upstream && !unset_upstream && argc == 0)
list = 1;
- if (with_commit || merge_filter != NO_FILTER)
+ if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr)
list = 1;
if (!!delete + !!rename + !!new_upstream +
list + unset_upstream > 1)
usage_with_options(builtin_branch_usage, options);
- if (abbrev == -1)
- abbrev = DEFAULT_ABBREV;
+ if (filter.abbrev == -1)
+ filter.abbrev = DEFAULT_ABBREV;
finalize_colopts(&colopts, -1);
- if (verbose) {
+ if (filter.verbose) {
if (explicitly_enable_column(colopts))
die(_("--column and --verbose are incompatible"));
colopts = 0;
@@ -910,20 +703,23 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
if (delete) {
if (!argc)
die(_("branch name required"));
- return delete_branches(argc, argv, delete > 1, kinds, quiet);
+ return delete_branches(argc, argv, delete > 1, filter.kind, quiet);
} else if (list) {
- int ret = print_ref_list(kinds, detached, verbose, abbrev,
- with_commit, argv);
+ /* git branch --local also shows HEAD when it is detached */
+ if ((filter.kind & FILTER_REFS_BRANCHES) && filter.detached)
+ filter.kind |= FILTER_REFS_DETACHED_HEAD;
+ filter.name_patterns = argv;
+ print_ref_list(&filter, sorting);
print_columns(&output, colopts, NULL);
string_list_clear(&output, 0);
- return ret;
+ return 0;
}
else if (edit_description) {
const char *branch_name;
struct strbuf branch_ref = STRBUF_INIT;
if (!argc) {
- if (detached)
+ if (filter.detached)
die(_("Cannot give description to detached HEAD"));
branch_name = head;
} else if (argc == 1)
@@ -1011,7 +807,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
if (!branch)
die(_("no such branch '%s'"), argv[0]);
- if (kinds != REF_LOCAL_BRANCH)
+ if (filter.kind != FILTER_REFS_BRANCHES)
die(_("-a and -r options to 'git branch' do not make sense with a branch name"));
if (track == BRANCH_TRACK_OVERRIDE)
diff --git a/builtin/clone.c b/builtin/clone.c
index 578da85254..9eaecd9a7c 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -294,9 +294,14 @@ static int add_one_reference(struct string_list_item *item, void *cb_data)
char *ref_git_git = mkpathdup("%s/.git", ref_git);
free(ref_git);
ref_git = ref_git_git;
- } else if (!is_directory(mkpath("%s/objects", ref_git)))
+ } else if (!is_directory(mkpath("%s/objects", ref_git))) {
+ struct strbuf sb = STRBUF_INIT;
+ if (get_common_dir(&sb, ref_git))
+ die(_("reference repository '%s' as a linked checkout is not supported yet."),
+ item->string);
die(_("reference repository '%s' is not a local repository."),
item->string);
+ }
if (!access(mkpath("%s/shallow", ref_git), F_OK))
die(_("reference repository '%s' is shallow"), item->string);
@@ -424,8 +429,10 @@ static void clone_local(const char *src_repo, const char *dest_repo)
} else {
struct strbuf src = STRBUF_INIT;
struct strbuf dest = STRBUF_INIT;
- strbuf_addf(&src, "%s/objects", src_repo);
- strbuf_addf(&dest, "%s/objects", dest_repo);
+ get_common_dir(&src, src_repo);
+ get_common_dir(&dest, dest_repo);
+ strbuf_addstr(&src, "/objects");
+ strbuf_addstr(&dest, "/objects");
copy_or_link_directory(&src, &dest, src_repo, src.len);
strbuf_release(&src);
strbuf_release(&dest);
@@ -1064,8 +1071,10 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
transport_unlock_pack(transport);
transport_disconnect(transport);
- if (option_dissociate)
+ if (option_dissociate) {
+ close_all_packs();
dissociate_from_references();
+ }
junk_mode = JUNK_LEAVE_REPO;
err = checkout();
diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c
index 7919206187..4e9f6c29bf 100644
--- a/builtin/for-each-ref.c
+++ b/builtin/for-each-ref.c
@@ -7,6 +7,9 @@
static char const * const for_each_ref_usage[] = {
N_("git for-each-ref [<options>] [<pattern>]"),
+ N_("git for-each-ref [--points-at <object>]"),
+ N_("git for-each-ref [(--merged | --no-merged) [<object>]]"),
+ N_("git for-each-ref [--contains [<object>]]"),
NULL
};
@@ -34,9 +37,18 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
OPT_STRING( 0 , "format", &format, N_("format"), N_("format to use for the output")),
OPT_CALLBACK(0 , "sort", sorting_tail, N_("key"),
N_("field name to sort on"), &parse_opt_ref_sorting),
+ OPT_CALLBACK(0, "points-at", &filter.points_at,
+ N_("object"), N_("print only refs which points at the given object"),
+ parse_opt_object_name),
+ OPT_MERGED(&filter, N_("print only refs that are merged")),
+ OPT_NO_MERGED(&filter, N_("print only refs that are not merged")),
+ OPT_CONTAINS(&filter.with_commit, N_("print only refs which contain the commit")),
OPT_END(),
};
+ memset(&array, 0, sizeof(array));
+ memset(&filter, 0, sizeof(filter));
+
parse_options(argc, argv, prefix, opts, for_each_ref_usage, 0);
if (maxcount < 0) {
error("invalid --count argument: `%d'", maxcount);
@@ -55,9 +67,8 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
/* for warn_ambiguous_refs */
git_config(git_default_config, NULL);
- memset(&array, 0, sizeof(array));
- memset(&filter, 0, sizeof(filter));
filter.name_patterns = argv;
+ filter.match_as_path = 1;
filter_refs(&array, &filter, FILTER_REFS_ALL | FILTER_REFS_INCLUDE_BROKEN);
ref_array_sort(sorting, &array);
diff --git a/builtin/fsck.c b/builtin/fsck.c
index 079470342f..b9a74f0cf6 100644
--- a/builtin/fsck.c
+++ b/builtin/fsck.c
@@ -38,6 +38,7 @@ static int show_dangling = 1;
#define ERROR_OBJECT 01
#define ERROR_REACHABLE 02
#define ERROR_PACK 04
+#define ERROR_REFS 010
#ifdef NO_D_INO_IN_DIRENT
#define SORT_DIRENT 0
@@ -521,8 +522,10 @@ static int fsck_handle_ref(const char *refname, const struct object_id *oid,
/* We'll continue with the rest despite the error.. */
return 0;
}
- if (obj->type != OBJ_COMMIT && is_branch(refname))
+ if (obj->type != OBJ_COMMIT && is_branch(refname)) {
error("%s: not a commit", refname);
+ errors_found |= ERROR_REFS;
+ }
default_refs++;
obj->used = 1;
mark_object_reachable(obj);
@@ -585,17 +588,23 @@ static int fsck_head_link(void)
fprintf(stderr, "Checking HEAD link\n");
head_points_at = resolve_ref_unsafe("HEAD", 0, head_oid.hash, &flag);
- if (!head_points_at)
+ if (!head_points_at) {
+ errors_found |= ERROR_REFS;
return error("Invalid HEAD");
+ }
if (!strcmp(head_points_at, "HEAD"))
/* detached HEAD */
null_is_error = 1;
- else if (!starts_with(head_points_at, "refs/heads/"))
+ else if (!starts_with(head_points_at, "refs/heads/")) {
+ errors_found |= ERROR_REFS;
return error("HEAD points to something strange (%s)",
head_points_at);
+ }
if (is_null_oid(&head_oid)) {
- if (null_is_error)
+ if (null_is_error) {
+ errors_found |= ERROR_REFS;
return error("HEAD: detached HEAD points at nothing");
+ }
fprintf(stderr, "notice: HEAD points to an unborn branch (%s)\n",
head_points_at + 11);
}
@@ -615,6 +624,7 @@ static int fsck_cache_tree(struct cache_tree *it)
if (!obj) {
error("%s: invalid sha1 pointer in cache-tree",
sha1_to_hex(it->sha1));
+ errors_found |= ERROR_REFS;
return 1;
}
obj->used = 1;
diff --git a/builtin/gc.c b/builtin/gc.c
index 0ad8d30b56..9ff0204940 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -44,6 +44,7 @@ static struct argv_array prune_worktrees = ARGV_ARRAY_INIT;
static struct argv_array rerere = ARGV_ARRAY_INIT;
static struct tempfile pidfile;
+static struct lock_file log_lock;
static void git_config_date_string(const char *key, const char **output)
{
@@ -56,6 +57,28 @@ static void git_config_date_string(const char *key, const char **output)
}
}
+static void process_log_file(void)
+{
+ struct stat st;
+ if (!fstat(get_lock_file_fd(&log_lock), &st) && st.st_size)
+ commit_lock_file(&log_lock);
+ else
+ rollback_lock_file(&log_lock);
+}
+
+static void process_log_file_at_exit(void)
+{
+ fflush(stderr);
+ process_log_file();
+}
+
+static void process_log_file_on_signal(int signo)
+{
+ process_log_file();
+ sigchain_pop(signo);
+ raise(signo);
+}
+
static void gc_config(void)
{
const char *value;
@@ -241,6 +264,24 @@ static const char *lock_repo_for_gc(int force, pid_t* ret_pid)
return NULL;
}
+static int report_last_gc_error(void)
+{
+ struct strbuf sb = STRBUF_INIT;
+ int ret;
+
+ ret = strbuf_read_file(&sb, git_path("gc.log"), 0);
+ if (ret > 0)
+ return error(_("The last gc run reported the following. "
+ "Please correct the root cause\n"
+ "and remove %s.\n"
+ "Automatic cleanup will not be performed "
+ "until the file is removed.\n\n"
+ "%s"),
+ git_path("gc.log"), sb.buf);
+ strbuf_release(&sb);
+ return 0;
+}
+
static int gc_before_repack(void)
{
if (pack_refs && run_command_v_opt(pack_refs_cmd.argv, RUN_GIT_CMD))
@@ -262,6 +303,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
int force = 0;
const char *name;
pid_t pid;
+ int daemonized = 0;
struct option builtin_gc_options[] = {
OPT__QUIET(&quiet, N_("suppress progress reporting")),
@@ -318,13 +360,16 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
fprintf(stderr, _("See \"git help gc\" for manual housekeeping.\n"));
}
if (detach_auto) {
+ if (report_last_gc_error())
+ return -1;
+
if (gc_before_repack())
return -1;
/*
* failure to daemonize is ok, we'll continue
* in foreground
*/
- daemonize();
+ daemonized = !daemonize();
}
} else
add_repack_all_option();
@@ -337,6 +382,15 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
name, (uintmax_t)pid);
}
+ if (daemonized) {
+ hold_lock_file_for_update(&log_lock,
+ git_path("gc.log"),
+ LOCK_DIE_ON_ERROR);
+ dup2(get_lock_file_fd(&log_lock), 2);
+ sigchain_push_common(process_log_file_on_signal);
+ atexit(process_log_file_at_exit);
+ }
+
if (gc_before_repack())
return -1;
diff --git a/builtin/ls-remote.c b/builtin/ls-remote.c
index 4554dbc8a9..5e9d5450b7 100644
--- a/builtin/ls-remote.c
+++ b/builtin/ls-remote.c
@@ -4,7 +4,7 @@
#include "remote.h"
static const char ls_remote_usage[] =
-"git ls-remote [--heads] [--tags] [-u <exec> | --upload-pack <exec>]\n"
+"git ls-remote [--heads] [--tags] [--upload-pack=<exec>]\n"
" [-q | --quiet] [--exit-code] [--get-url] [<repository> [<refs>...]]";
/*
diff --git a/builtin/remote.c b/builtin/remote.c
index 181668dedd..e4c3ea130c 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -18,6 +18,7 @@ static const char * const builtin_remote_usage[] = {
N_("git remote prune [-n | --dry-run] <name>"),
N_("git remote [-v | --verbose] update [-p | --prune] [(<group> | <remote>)...]"),
N_("git remote set-branches [--add] <name> <branch>..."),
+ N_("git remote get-url [--push] [--all] <name>"),
N_("git remote set-url [--push] <name> <newurl> [<oldurl>]"),
N_("git remote set-url --add <name> <newurl>"),
N_("git remote set-url --delete <name> <url>"),
@@ -65,6 +66,11 @@ static const char * const builtin_remote_update_usage[] = {
NULL
};
+static const char * const builtin_remote_geturl_usage[] = {
+ N_("git remote get-url [--push] [--all] <name>"),
+ NULL
+};
+
static const char * const builtin_remote_seturl_usage[] = {
N_("git remote set-url [--push] <name> <newurl> [<oldurl>]"),
N_("git remote set-url --add <name> <newurl>"),
@@ -1467,6 +1473,57 @@ static int set_branches(int argc, const char **argv)
return set_remote_branches(argv[0], argv + 1, add_mode);
}
+static int get_url(int argc, const char **argv)
+{
+ int i, push_mode = 0, all_mode = 0;
+ const char *remotename = NULL;
+ struct remote *remote;
+ const char **url;
+ int url_nr;
+ struct option options[] = {
+ OPT_BOOL('\0', "push", &push_mode,
+ N_("query push URLs rather than fetch URLs")),
+ OPT_BOOL('\0', "all", &all_mode,
+ N_("return all URLs")),
+ OPT_END()
+ };
+ argc = parse_options(argc, argv, NULL, options, builtin_remote_geturl_usage, 0);
+
+ if (argc != 1)
+ usage_with_options(builtin_remote_geturl_usage, options);
+
+ remotename = argv[0];
+
+ if (!remote_is_configured(remotename))
+ die(_("No such remote '%s'"), remotename);
+ remote = remote_get(remotename);
+
+ url_nr = 0;
+ if (push_mode) {
+ url = remote->pushurl;
+ url_nr = remote->pushurl_nr;
+ }
+ /* else fetch mode */
+
+ /* Use the fetch URL when no push URLs were found or requested. */
+ if (!url_nr) {
+ url = remote->url;
+ url_nr = remote->url_nr;
+ }
+
+ if (!url_nr)
+ die(_("no URLs configured for remote '%s'"), remotename);
+
+ if (all_mode) {
+ for (i = 0; i < url_nr; i++)
+ printf_ln("%s", url[i]);
+ } else {
+ printf_ln("%s", *url);
+ }
+
+ return 0;
+}
+
static int set_url(int argc, const char **argv)
{
int i, push_mode = 0, add_mode = 0, delete_mode = 0;
@@ -1576,6 +1633,8 @@ int cmd_remote(int argc, const char **argv, const char *prefix)
result = set_head(argc, argv);
else if (!strcmp(argv[0], "set-branches"))
result = set_branches(argc, argv);
+ else if (!strcmp(argv[0], "get-url"))
+ result = get_url(argc, argv);
else if (!strcmp(argv[0], "set-url"))
result = set_url(argc, argv);
else if (!strcmp(argv[0], "show"))
diff --git a/builtin/rerere.c b/builtin/rerere.c
index 88e1359ebc..1bf72423bf 100644
--- a/builtin/rerere.c
+++ b/builtin/rerere.c
@@ -104,9 +104,9 @@ int cmd_rerere(int argc, const char **argv, const char *prefix)
return 0;
for (i = 0; i < merge_rr.nr; i++) {
const char *path = merge_rr.items[i].string;
- const char *name = (const char *)merge_rr.items[i].util;
- if (diff_two(rerere_path(name, "preimage"), path, path, path))
- die("unable to generate diff for %s", name);
+ const struct rerere_id *id = merge_rr.items[i].util;
+ if (diff_two(rerere_path(id, "preimage"), path, path, path))
+ die("unable to generate diff for %s", rerere_path(id, NULL));
}
} else
usage_with_options(rerere_usage, options);
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
new file mode 100644
index 0000000000..f4c3eff179
--- /dev/null
+++ b/builtin/submodule--helper.c
@@ -0,0 +1,282 @@
+#include "builtin.h"
+#include "cache.h"
+#include "parse-options.h"
+#include "quote.h"
+#include "pathspec.h"
+#include "dir.h"
+#include "utf8.h"
+#include "submodule.h"
+#include "submodule-config.h"
+#include "string-list.h"
+#include "run-command.h"
+
+struct module_list {
+ const struct cache_entry **entries;
+ int alloc, nr;
+};
+#define MODULE_LIST_INIT { NULL, 0, 0 }
+
+static int module_list_compute(int argc, const char **argv,
+ const char *prefix,
+ struct pathspec *pathspec,
+ struct module_list *list)
+{
+ int i, result = 0;
+ char *max_prefix, *ps_matched = NULL;
+ int max_prefix_len;
+ parse_pathspec(pathspec, 0,
+ PATHSPEC_PREFER_FULL |
+ PATHSPEC_STRIP_SUBMODULE_SLASH_CHEAP,
+ prefix, argv);
+
+ /* Find common prefix for all pathspec's */
+ max_prefix = common_prefix(pathspec);
+ max_prefix_len = max_prefix ? strlen(max_prefix) : 0;
+
+ if (pathspec->nr)
+ ps_matched = xcalloc(pathspec->nr, 1);
+
+ if (read_cache() < 0)
+ die(_("index file corrupt"));
+
+ for (i = 0; i < active_nr; i++) {
+ const struct cache_entry *ce = active_cache[i];
+
+ if (!S_ISGITLINK(ce->ce_mode) ||
+ !match_pathspec(pathspec, ce->name, ce_namelen(ce),
+ max_prefix_len, ps_matched, 1))
+ continue;
+
+ ALLOC_GROW(list->entries, list->nr + 1, list->alloc);
+ list->entries[list->nr++] = ce;
+ while (i + 1 < active_nr &&
+ !strcmp(ce->name, active_cache[i + 1]->name))
+ /*
+ * Skip entries with the same name in different stages
+ * to make sure an entry is returned only once.
+ */
+ i++;
+ }
+ free(max_prefix);
+
+ if (ps_matched && report_path_error(ps_matched, pathspec, prefix))
+ result = -1;
+
+ free(ps_matched);
+
+ return result;
+}
+
+static int module_list(int argc, const char **argv, const char *prefix)
+{
+ int i;
+ struct pathspec pathspec;
+ struct module_list list = MODULE_LIST_INIT;
+
+ struct option module_list_options[] = {
+ OPT_STRING(0, "prefix", &prefix,
+ N_("path"),
+ N_("alternative anchor for relative paths")),
+ OPT_END()
+ };
+
+ const char *const git_submodule_helper_usage[] = {
+ N_("git submodule--helper list [--prefix=<path>] [<path>...]"),
+ NULL
+ };
+
+ argc = parse_options(argc, argv, prefix, module_list_options,
+ git_submodule_helper_usage, 0);
+
+ if (module_list_compute(argc, argv, prefix, &pathspec, &list) < 0) {
+ printf("#unmatched\n");
+ return 1;
+ }
+
+ for (i = 0; i < list.nr; i++) {
+ const struct cache_entry *ce = list.entries[i];
+
+ if (ce_stage(ce))
+ printf("%06o %s U\t", ce->ce_mode, sha1_to_hex(null_sha1));
+ else
+ printf("%06o %s %d\t", ce->ce_mode, sha1_to_hex(ce->sha1), ce_stage(ce));
+
+ utf8_fprintf(stdout, "%s\n", ce->name);
+ }
+ return 0;
+}
+
+static int module_name(int argc, const char **argv, const char *prefix)
+{
+ const struct submodule *sub;
+
+ if (argc != 2)
+ usage(_("git submodule--helper name <path>"));
+
+ gitmodules_config();
+ sub = submodule_from_path(null_sha1, argv[1]);
+
+ if (!sub)
+ die(_("no submodule mapping found in .gitmodules for path '%s'"),
+ argv[1]);
+
+ printf("%s\n", sub->name);
+
+ return 0;
+}
+static int clone_submodule(const char *path, const char *gitdir, const char *url,
+ const char *depth, const char *reference, int quiet)
+{
+ struct child_process cp;
+ child_process_init(&cp);
+
+ argv_array_push(&cp.args, "clone");
+ argv_array_push(&cp.args, "--no-checkout");
+ if (quiet)
+ argv_array_push(&cp.args, "--quiet");
+ if (depth && *depth)
+ argv_array_pushl(&cp.args, "--depth", depth, NULL);
+ if (reference && *reference)
+ argv_array_pushl(&cp.args, "--reference", reference, NULL);
+ if (gitdir && *gitdir)
+ argv_array_pushl(&cp.args, "--separate-git-dir", gitdir, NULL);
+
+ argv_array_push(&cp.args, url);
+ argv_array_push(&cp.args, path);
+
+ cp.git_cmd = 1;
+ cp.env = local_repo_env;
+ cp.no_stdin = 1;
+
+ return run_command(&cp);
+}
+
+static int module_clone(int argc, const char **argv, const char *prefix)
+{
+ const char *path = NULL, *name = NULL, *url = NULL;
+ const char *reference = NULL, *depth = NULL;
+ int quiet = 0;
+ FILE *submodule_dot_git;
+ char *sm_gitdir, *cwd, *p;
+ struct strbuf rel_path = STRBUF_INIT;
+ struct strbuf sb = STRBUF_INIT;
+
+ struct option module_clone_options[] = {
+ OPT_STRING(0, "prefix", &prefix,
+ N_("path"),
+ N_("alternative anchor for relative paths")),
+ OPT_STRING(0, "path", &path,
+ N_("path"),
+ N_("where the new submodule will be cloned to")),
+ OPT_STRING(0, "name", &name,
+ N_("string"),
+ N_("name of the new submodule")),
+ OPT_STRING(0, "url", &url,
+ N_("string"),
+ N_("url where to clone the submodule from")),
+ OPT_STRING(0, "reference", &reference,
+ N_("string"),
+ N_("reference repository")),
+ OPT_STRING(0, "depth", &depth,
+ N_("string"),
+ N_("depth for shallow clones")),
+ OPT__QUIET(&quiet, "Suppress output for cloning a submodule"),
+ OPT_END()
+ };
+
+ const char *const git_submodule_helper_usage[] = {
+ N_("git submodule--helper clone [--prefix=<path>] [--quiet] "
+ "[--reference <repository>] [--name <name>] [--url <url>]"
+ "[--depth <depth>] [--] [<path>...]"),
+ NULL
+ };
+
+ argc = parse_options(argc, argv, prefix, module_clone_options,
+ git_submodule_helper_usage, 0);
+
+ strbuf_addf(&sb, "%s/modules/%s", get_git_dir(), name);
+ sm_gitdir = strbuf_detach(&sb, NULL);
+
+ if (!file_exists(sm_gitdir)) {
+ if (safe_create_leading_directories_const(sm_gitdir) < 0)
+ die(_("could not create directory '%s'"), sm_gitdir);
+ if (clone_submodule(path, sm_gitdir, url, depth, reference, quiet))
+ die(_("clone of '%s' into submodule path '%s' failed"),
+ url, path);
+ } else {
+ if (safe_create_leading_directories_const(path) < 0)
+ die(_("could not create directory '%s'"), path);
+ strbuf_addf(&sb, "%s/index", sm_gitdir);
+ unlink_or_warn(sb.buf);
+ strbuf_reset(&sb);
+ }
+
+ /* Write a .git file in the submodule to redirect to the superproject. */
+ if (safe_create_leading_directories_const(path) < 0)
+ die(_("could not create directory '%s'"), path);
+
+ if (path && *path)
+ strbuf_addf(&sb, "%s/.git", path);
+ else
+ strbuf_addstr(&sb, ".git");
+
+ if (safe_create_leading_directories_const(sb.buf) < 0)
+ die(_("could not create leading directories of '%s'"), sb.buf);
+ submodule_dot_git = fopen(sb.buf, "w");
+ if (!submodule_dot_git)
+ die_errno(_("cannot open file '%s'"), sb.buf);
+
+ fprintf(submodule_dot_git, "gitdir: %s\n",
+ relative_path(sm_gitdir, path, &rel_path));
+ if (fclose(submodule_dot_git))
+ die(_("could not close file %s"), sb.buf);
+ strbuf_reset(&sb);
+ strbuf_reset(&rel_path);
+
+ cwd = xgetcwd();
+ /* Redirect the worktree of the submodule in the superproject's config */
+ if (!is_absolute_path(sm_gitdir)) {
+ strbuf_addf(&sb, "%s/%s", cwd, sm_gitdir);
+ free(sm_gitdir);
+ sm_gitdir = strbuf_detach(&sb, NULL);
+ }
+
+ strbuf_addf(&sb, "%s/%s", cwd, path);
+ p = git_pathdup_submodule(path, "config");
+ if (!p)
+ die(_("could not get submodule directory for '%s'"), path);
+ git_config_set_in_file(p, "core.worktree",
+ relative_path(sb.buf, sm_gitdir, &rel_path));
+ strbuf_release(&sb);
+ strbuf_release(&rel_path);
+ free(sm_gitdir);
+ free(cwd);
+ free(p);
+ return 0;
+}
+
+struct cmd_struct {
+ const char *cmd;
+ int (*fn)(int, const char **, const char *);
+};
+
+static struct cmd_struct commands[] = {
+ {"list", module_list},
+ {"name", module_name},
+ {"clone", module_clone},
+};
+
+int cmd_submodule__helper(int argc, const char **argv, const char *prefix)
+{
+ int i;
+ if (argc < 2)
+ die(_("fatal: submodule--helper subcommand must be "
+ "called with a subcommand"));
+
+ for (i = 0; i < ARRAY_SIZE(commands); i++)
+ if (!strcmp(argv[1], commands[i].cmd))
+ return commands[i].fn(argc - 1, argv + 1, prefix);
+
+ die(_("fatal: '%s' is not a valid submodule--helper "
+ "subcommand"), argv[1]);
+}
diff --git a/builtin/tag.c b/builtin/tag.c
index cba0e22666..9e17dca7ca 100644
--- a/builtin/tag.c
+++ b/builtin/tag.c
@@ -17,271 +17,49 @@
#include "gpg-interface.h"
#include "sha1-array.h"
#include "column.h"
+#include "ref-filter.h"
static const char * const git_tag_usage[] = {
N_("git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] <tagname> [<head>]"),
N_("git tag -d <tagname>..."),
N_("git tag -l [-n[<num>]] [--contains <commit>] [--points-at <object>]"
- "\n\t\t[<pattern>...]"),
+ "\n\t\t[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]"),
N_("git tag -v <tagname>..."),
NULL
};
-#define STRCMP_SORT 0 /* must be zero */
-#define VERCMP_SORT 1
-#define SORT_MASK 0x7fff
-#define REVERSE_SORT 0x8000
-
-static int tag_sort;
-
-struct tag_filter {
- const char **patterns;
- int lines;
- int sort;
- struct string_list tags;
- struct commit_list *with_commit;
-};
-
-static struct sha1_array points_at;
static unsigned int colopts;
-static int match_pattern(const char **patterns, const char *ref)
-{
- /* no pattern means match everything */
- if (!*patterns)
- return 1;
- for (; *patterns; patterns++)
- if (!wildmatch(*patterns, ref, 0, NULL))
- return 1;
- return 0;
-}
-
-static const unsigned char *match_points_at(const char *refname,
- const unsigned char *sha1)
-{
- const unsigned char *tagged_sha1 = NULL;
- struct object *obj;
-
- if (sha1_array_lookup(&points_at, sha1) >= 0)
- return sha1;
- obj = parse_object(sha1);
- if (!obj)
- die(_("malformed object at '%s'"), refname);
- if (obj->type == OBJ_TAG)
- tagged_sha1 = ((struct tag *)obj)->tagged->sha1;
- if (tagged_sha1 && sha1_array_lookup(&points_at, tagged_sha1) >= 0)
- return tagged_sha1;
- return NULL;
-}
-
-static int in_commit_list(const struct commit_list *want, struct commit *c)
-{
- for (; want; want = want->next)
- if (!hashcmp(want->item->object.sha1, c->object.sha1))
- return 1;
- return 0;
-}
-
-enum contains_result {
- CONTAINS_UNKNOWN = -1,
- CONTAINS_NO = 0,
- CONTAINS_YES = 1
-};
-
-/*
- * Test whether the candidate or one of its parents is contained in the list.
- * Do not recurse to find out, though, but return -1 if inconclusive.
- */
-static enum contains_result contains_test(struct commit *candidate,
- const struct commit_list *want)
-{
- /* was it previously marked as containing a want commit? */
- if (candidate->object.flags & TMP_MARK)
- return 1;
- /* or marked as not possibly containing a want commit? */
- if (candidate->object.flags & UNINTERESTING)
- return 0;
- /* or are we it? */
- if (in_commit_list(want, candidate)) {
- candidate->object.flags |= TMP_MARK;
- return 1;
- }
-
- if (parse_commit(candidate) < 0)
- return 0;
-
- return -1;
-}
-
-/*
- * Mimicking the real stack, this stack lives on the heap, avoiding stack
- * overflows.
- *
- * At each recursion step, the stack items points to the commits whose
- * ancestors are to be inspected.
- */
-struct stack {
- int nr, alloc;
- struct stack_entry {
- struct commit *commit;
- struct commit_list *parents;
- } *stack;
-};
-
-static void push_to_stack(struct commit *candidate, struct stack *stack)
-{
- int index = stack->nr++;
- ALLOC_GROW(stack->stack, stack->nr, stack->alloc);
- stack->stack[index].commit = candidate;
- stack->stack[index].parents = candidate->parents;
-}
-
-static enum contains_result contains(struct commit *candidate,
- const struct commit_list *want)
-{
- struct stack stack = { 0, 0, NULL };
- int result = contains_test(candidate, want);
-
- if (result != CONTAINS_UNKNOWN)
- return result;
-
- push_to_stack(candidate, &stack);
- while (stack.nr) {
- struct stack_entry *entry = &stack.stack[stack.nr - 1];
- struct commit *commit = entry->commit;
- struct commit_list *parents = entry->parents;
-
- if (!parents) {
- commit->object.flags |= UNINTERESTING;
- stack.nr--;
- }
- /*
- * If we just popped the stack, parents->item has been marked,
- * therefore contains_test will return a meaningful 0 or 1.
- */
- else switch (contains_test(parents->item, want)) {
- case CONTAINS_YES:
- commit->object.flags |= TMP_MARK;
- stack.nr--;
- break;
- case CONTAINS_NO:
- entry->parents = parents->next;
- break;
- case CONTAINS_UNKNOWN:
- push_to_stack(parents->item, &stack);
- break;
- }
- }
- free(stack.stack);
- return contains_test(candidate, want);
-}
-
-static void show_tag_lines(const struct object_id *oid, int lines)
+static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting, const char *format)
{
+ struct ref_array array;
+ char *to_free = NULL;
int i;
- unsigned long size;
- enum object_type type;
- char *buf, *sp, *eol;
- size_t len;
-
- buf = read_sha1_file(oid->hash, &type, &size);
- if (!buf)
- die_errno("unable to read object %s", oid_to_hex(oid));
- if (type != OBJ_COMMIT && type != OBJ_TAG)
- goto free_return;
- if (!size)
- die("an empty %s object %s?",
- typename(type), oid_to_hex(oid));
-
- /* skip header */
- sp = strstr(buf, "\n\n");
- if (!sp)
- goto free_return;
-
- /* only take up to "lines" lines, and strip the signature from a tag */
- if (type == OBJ_TAG)
- size = parse_signature(buf, size);
- for (i = 0, sp += 2; i < lines && sp < buf + size; i++) {
- if (i)
- printf("\n ");
- eol = memchr(sp, '\n', size - (sp - buf));
- len = eol ? eol - sp : size - (sp - buf);
- fwrite(sp, len, 1, stdout);
- if (!eol)
- break;
- sp = eol + 1;
- }
-free_return:
- free(buf);
-}
-
-static int show_reference(const char *refname, const struct object_id *oid,
- int flag, void *cb_data)
-{
- struct tag_filter *filter = cb_data;
- if (match_pattern(filter->patterns, refname)) {
- if (filter->with_commit) {
- struct commit *commit;
+ memset(&array, 0, sizeof(array));
- commit = lookup_commit_reference_gently(oid->hash, 1);
- if (!commit)
- return 0;
- if (!contains(commit, filter->with_commit))
- return 0;
- }
-
- if (points_at.nr && !match_points_at(refname, oid->hash))
- return 0;
+ if (filter->lines == -1)
+ filter->lines = 0;
- if (!filter->lines) {
- if (filter->sort)
- string_list_append(&filter->tags, refname);
- else
- printf("%s\n", refname);
- return 0;
- }
- printf("%-15s ", refname);
- show_tag_lines(oid, filter->lines);
- putchar('\n');
+ if (!format) {
+ if (filter->lines) {
+ to_free = xstrfmt("%s %%(contents:lines=%d)",
+ "%(align:15)%(refname:short)%(end)",
+ filter->lines);
+ format = to_free;
+ } else
+ format = "%(refname:short)";
}
- return 0;
-}
+ verify_ref_format(format);
+ filter_refs(&array, filter, FILTER_REFS_TAGS);
+ ref_array_sort(sorting, &array);
-static int sort_by_version(const void *a_, const void *b_)
-{
- const struct string_list_item *a = a_;
- const struct string_list_item *b = b_;
- return versioncmp(a->string, b->string);
-}
+ for (i = 0; i < array.nr; i++)
+ show_ref_array_item(array.items[i], format, 0);
+ ref_array_clear(&array);
+ free(to_free);
-static int list_tags(const char **patterns, int lines,
- struct commit_list *with_commit, int sort)
-{
- struct tag_filter filter;
-
- filter.patterns = patterns;
- filter.lines = lines;
- filter.sort = sort;
- filter.with_commit = with_commit;
- memset(&filter.tags, 0, sizeof(filter.tags));
- filter.tags.strdup_strings = 1;
-
- for_each_tag_ref(show_reference, (void *)&filter);
- if (sort) {
- int i;
- if ((sort & SORT_MASK) == VERCMP_SORT)
- qsort(filter.tags.items, filter.tags.nr,
- sizeof(struct string_list_item), sort_by_version);
- if (sort & REVERSE_SORT)
- for (i = filter.tags.nr - 1; i >= 0; i--)
- printf("%s\n", filter.tags.items[i].string);
- else
- for (i = 0; i < filter.tags.nr; i++)
- printf("%s\n", filter.tags.items[i].string);
- string_list_clear(&filter.tags, 0);
- }
return 0;
}
@@ -348,35 +126,26 @@ static const char tag_template_nocleanup[] =
"Lines starting with '%c' will be kept; you may remove them"
" yourself if you want to.\n");
-/*
- * Parse a sort string, and return 0 if parsed successfully. Will return
- * non-zero when the sort string does not parse into a known type. If var is
- * given, the error message becomes a warning and includes information about
- * the configuration value.
- */
-static int parse_sort_string(const char *var, const char *arg, int *sort)
+/* Parse arg given and add it the ref_sorting array */
+static int parse_sorting_string(const char *arg, struct ref_sorting **sorting_tail)
{
- int type = 0, flags = 0;
+ struct ref_sorting *s;
+ int len;
- if (skip_prefix(arg, "-", &arg))
- flags |= REVERSE_SORT;
+ s = xcalloc(1, sizeof(*s));
+ s->next = *sorting_tail;
+ *sorting_tail = s;
- if (skip_prefix(arg, "version:", &arg) || skip_prefix(arg, "v:", &arg))
- type = VERCMP_SORT;
- else
- type = STRCMP_SORT;
-
- if (strcmp(arg, "refname")) {
- if (!var)
- return error(_("unsupported sort specification '%s'"), arg);
- else {
- warning(_("unsupported sort specification '%s' in variable '%s'"),
- var, arg);
- return -1;
- }
+ if (*arg == '-') {
+ s->reverse = 1;
+ arg++;
}
+ if (skip_prefix(arg, "version:", &arg) ||
+ skip_prefix(arg, "v:", &arg))
+ s->version = 1;
- *sort = (type | flags);
+ len = strlen(arg);
+ s->atom = parse_ref_filter_atom(arg, arg+len);
return 0;
}
@@ -384,11 +153,12 @@ static int parse_sort_string(const char *var, const char *arg, int *sort)
static int git_tag_config(const char *var, const char *value, void *cb)
{
int status;
+ struct ref_sorting **sorting_tail = (struct ref_sorting **)cb;
if (!strcmp(var, "tag.sort")) {
if (!value)
return config_error_nonbool(var);
- parse_sort_string(var, value, &tag_sort);
+ parse_sorting_string(value, sorting_tail);
return 0;
}
@@ -546,30 +316,6 @@ static int strbuf_check_tag_ref(struct strbuf *sb, const char *name)
return check_refname_format(sb->buf, 0);
}
-static int parse_opt_points_at(const struct option *opt __attribute__((unused)),
- const char *arg, int unset)
-{
- unsigned char sha1[20];
-
- if (unset) {
- sha1_array_clear(&points_at);
- return 0;
- }
- if (!arg)
- return error(_("switch 'points-at' requires an object"));
- if (get_sha1(arg, sha1))
- return error(_("malformed object name '%s'"), arg);
- sha1_array_append(&points_at, sha1);
- return 0;
-}
-
-static int parse_opt_sort(const struct option *opt, const char *arg, int unset)
-{
- int *sort = opt->value;
-
- return parse_sort_string(NULL, arg, sort);
-}
-
int cmd_tag(int argc, const char **argv, const char *prefix)
{
struct strbuf buf = STRBUF_INIT;
@@ -578,17 +324,19 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
const char *object_ref, *tag;
struct create_tag_options opt;
char *cleanup_arg = NULL;
- int annotate = 0, force = 0, lines = -1;
int create_reflog = 0;
+ int annotate = 0, force = 0;
int cmdmode = 0;
const char *msgfile = NULL, *keyid = NULL;
struct msg_arg msg = { 0, STRBUF_INIT };
- struct commit_list *with_commit = NULL;
struct ref_transaction *transaction;
struct strbuf err = STRBUF_INIT;
+ struct ref_filter filter;
+ static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
+ const char *format = NULL;
struct option options[] = {
OPT_CMDMODE('l', "list", &cmdmode, N_("list tag names"), 'l'),
- { OPTION_INTEGER, 'n', NULL, &lines, N_("n"),
+ { OPTION_INTEGER, 'n', NULL, &filter.lines, N_("n"),
N_("print <n> lines of each tag message"),
PARSE_OPT_OPTARG, NULL, 1 },
OPT_CMDMODE('d', "delete", &cmdmode, N_("delete tags"), 'd'),
@@ -610,32 +358,25 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
OPT_GROUP(N_("Tag listing options")),
OPT_COLUMN(0, "column", &colopts, N_("show tag list in columns")),
+ OPT_CONTAINS(&filter.with_commit, N_("print only tags that contain the commit")),
+ OPT_WITH(&filter.with_commit, N_("print only tags that contain the commit")),
+ OPT_MERGED(&filter, N_("print only tags that are merged")),
+ OPT_NO_MERGED(&filter, N_("print only tags that are not merged")),
+ OPT_CALLBACK(0 , "sort", sorting_tail, N_("key"),
+ N_("field name to sort on"), &parse_opt_ref_sorting),
{
- OPTION_CALLBACK, 0, "sort", &tag_sort, N_("type"), N_("sort tags"),
- PARSE_OPT_NONEG, parse_opt_sort
- },
- {
- OPTION_CALLBACK, 0, "contains", &with_commit, N_("commit"),
- N_("print only tags that contain the commit"),
- PARSE_OPT_LASTARG_DEFAULT,
- parse_opt_with_commit, (intptr_t)"HEAD",
- },
- {
- OPTION_CALLBACK, 0, "with", &with_commit, N_("commit"),
- N_("print only tags that contain the commit"),
- PARSE_OPT_HIDDEN | PARSE_OPT_LASTARG_DEFAULT,
- parse_opt_with_commit, (intptr_t)"HEAD",
- },
- {
- OPTION_CALLBACK, 0, "points-at", NULL, N_("object"),
- N_("print only tags of the object"), 0, parse_opt_points_at
+ OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"),
+ N_("print only tags of the object"), 0, parse_opt_object_name
},
+ OPT_STRING( 0 , "format", &format, N_("format"), N_("format to use for the output")),
OPT_END()
};
- git_config(git_tag_config, NULL);
+ git_config(git_tag_config, sorting_tail);
memset(&opt, 0, sizeof(opt));
+ memset(&filter, 0, sizeof(filter));
+ filter.lines = -1;
argc = parse_options(argc, argv, prefix, options, git_tag_usage, 0);
@@ -652,11 +393,13 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
usage_with_options(git_tag_usage, options);
finalize_colopts(&colopts, -1);
- if (cmdmode == 'l' && lines != -1) {
+ if (cmdmode == 'l' && filter.lines != -1) {
if (explicitly_enable_column(colopts))
die(_("--column and -n are incompatible"));
colopts = 0;
}
+ if (!sorting)
+ sorting = ref_default_sorting();
if (cmdmode == 'l') {
int ret;
if (column_active(colopts)) {
@@ -665,19 +408,20 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
copts.padding = 2;
run_column_filter(colopts, &copts);
}
- if (lines != -1 && tag_sort)
- die(_("--sort and -n are incompatible"));
- ret = list_tags(argv, lines == -1 ? 0 : lines, with_commit, tag_sort);
+ filter.name_patterns = argv;
+ ret = list_tags(&filter, sorting, format);
if (column_active(colopts))
stop_column_filter();
return ret;
}
- if (lines != -1)
+ if (filter.lines != -1)
die(_("-n option is only allowed with -l."));
- if (with_commit)
+ if (filter.with_commit)
die(_("--contains option is only allowed with -l."));
- if (points_at.nr)
+ if (filter.points_at.nr)
die(_("--points-at option is only allowed with -l."));
+ if (filter.merge_commit)
+ die(_("--merged and --no-merged option are only allowed with -l"));
if (cmdmode == 'd')
return for_each_tag_name(argv, delete_tag);
if (cmdmode == 'v')
diff --git a/cache.h b/cache.h
index 79066e57dc..44c0260000 100644
--- a/cache.h
+++ b/cache.h
@@ -443,6 +443,7 @@ extern char *get_object_directory(void);
extern char *get_index_file(void);
extern char *get_graft_file(void);
extern int set_git_dir(const char *path);
+extern int get_common_dir_noenv(struct strbuf *sb, const char *gitdir);
extern int get_common_dir(struct strbuf *sb, const char *gitdir);
extern const char *get_git_namespace(void);
extern const char *strip_namespace(const char *namespaced_ref);
@@ -1091,7 +1092,6 @@ struct date_mode {
DATE_NORMAL = 0,
DATE_RELATIVE,
DATE_SHORT,
- DATE_LOCAL,
DATE_ISO8601,
DATE_ISO8601_STRICT,
DATE_RFC2822,
@@ -1099,6 +1099,7 @@ struct date_mode {
DATE_RAW
} type;
const char *strftime_fmt;
+ int local;
};
/*
@@ -1275,6 +1276,7 @@ extern void close_pack_index(struct packed_git *);
extern unsigned char *use_pack(struct packed_git *, struct pack_window **, off_t, unsigned long *);
extern void close_pack_windows(struct packed_git *);
+extern void close_all_packs(void);
extern void unuse_pack(struct pack_window **);
extern void free_pack_by_name(const char *);
extern void clear_delta_base_cache(void);
diff --git a/connect.c b/connect.c
index 27a706f766..d3283b8a4f 100644
--- a/connect.c
+++ b/connect.c
@@ -255,7 +255,7 @@ static const char *prot_name(enum protocol protocol)
case PROTO_GIT:
return "git";
default:
- return "unkown protocol";
+ return "unknown protocol";
}
}
@@ -724,10 +724,13 @@ struct child_process *git_connect(int fd[2], const char *url,
strbuf_addch(&cmd, ' ');
sq_quote_buf(&cmd, path);
+ /* remove repo-local variables from the environment */
+ conn->env = local_repo_env;
+ conn->use_shell = 1;
conn->in = conn->out = -1;
if (protocol == PROTO_SSH) {
const char *ssh;
- int putty, tortoiseplink = 0;
+ int putty = 0, tortoiseplink = 0;
char *ssh_host = hostandport;
const char *port = NULL;
transport_check_allowed("ssh");
@@ -750,13 +753,17 @@ struct child_process *git_connect(int fd[2], const char *url,
}
ssh = getenv("GIT_SSH_COMMAND");
- if (ssh) {
- conn->use_shell = 1;
- putty = 0;
- } else {
+ if (!ssh) {
const char *base;
char *ssh_dup;
+ /*
+ * GIT_SSH is the no-shell version of
+ * GIT_SSH_COMMAND (and must remain so for
+ * historical compatibility).
+ */
+ conn->use_shell = 0;
+
ssh = getenv("GIT_SSH");
if (!ssh)
ssh = "ssh";
@@ -766,8 +773,9 @@ struct child_process *git_connect(int fd[2], const char *url,
tortoiseplink = !strcasecmp(base, "tortoiseplink") ||
!strcasecmp(base, "tortoiseplink.exe");
- putty = !strcasecmp(base, "plink") ||
- !strcasecmp(base, "plink.exe") || tortoiseplink;
+ putty = tortoiseplink ||
+ !strcasecmp(base, "plink") ||
+ !strcasecmp(base, "plink.exe");
free(ssh_dup);
}
@@ -782,9 +790,6 @@ struct child_process *git_connect(int fd[2], const char *url,
}
argv_array_push(&conn->args, ssh_host);
} else {
- /* remove repo-local variables from the environment */
- conn->env = local_repo_env;
- conn->use_shell = 1;
transport_check_allowed("file");
}
argv_array_push(&conn->args, cmd.buf);
diff --git a/contrib/examples/git-pull.sh b/contrib/examples/git-pull.sh
index e8dc2e0e7d..6b3a03f9b0 100755
--- a/contrib/examples/git-pull.sh
+++ b/contrib/examples/git-pull.sh
@@ -69,7 +69,7 @@ as appropriate to mark resolution and make a commit.")"
die_merge () {
if [ $(git config --bool --get advice.resolveConflict || echo true) = "true" ]; then
die "$(gettext "You have not concluded your merge (MERGE_HEAD exists).
-Please, commit your changes before you can merge.")"
+Please, commit your changes before merging.")"
else
die "$(gettext "You have not concluded your merge (MERGE_HEAD exists).")"
fi
diff --git a/contrib/hooks/multimail/CHANGES b/contrib/hooks/multimail/CHANGES
index 6bb12306b8..bc77e66b85 100644
--- a/contrib/hooks/multimail/CHANGES
+++ b/contrib/hooks/multimail/CHANGES
@@ -1,3 +1,44 @@
+Release 1.2.0
+=============
+
+* It is now possible to exclude some refs (e.g. exclude some branches
+ or tags). See refFilterDoSendRegex, refFilterDontSendRegex,
+ refFilterInclusionRegex and refFilterExclusionRegex.
+
+* New commitEmailFormat option which can be set to "html" to generate
+ simple colorized diffs using HTML for the commit emails.
+
+* git-multimail can now be ran as a Gerrit ref-updated hook, or from
+ Atlassian BitBucket Server (formerly known as Atlassian Stash).
+
+* The From: field is now more customizeable. It can be set
+ independently for refchange emails and commit emails (see
+ fromCommit, fromRefChange). The special values pusher and author can
+ be used in these configuration variable.
+
+* A new command-line option, --version, was added. The version is also
+ available in the X-Git-Multimail-Version header of sent emails.
+
+* Set X-Git-NotificationType header to differentiate the various types
+ of notifications. Current values are: diff, ref_changed_plus_diff,
+ ref_changed.
+
+* Preliminary support for Python 3. The testsuite passes with Python 3,
+ but it has not received as much testing as the Python 2 version yet.
+
+* Several encoding-related fixes. UTF-8 characters work in more
+ situations (but non-ascii characters in email address are still not
+ supported).
+
+* The testsuite and its documentation has been greatly improved.
+
+Plus all the bugfixes from version 1.1.1.
+
+This version has been tested with Python 2.4 and 2.6 to 3.5, and Git
+v1.7.10-406-gdc801e7, git-1.8.2.3 and 2.6.0. Git versions prior to
+v1.7.10-406-gdc801e7 probably work, but cannot run the testsuite
+properly.
+
Release 1.1.1 (bugfix-only release)
===================================
diff --git a/contrib/hooks/multimail/CONTRIBUTING.rst b/contrib/hooks/multimail/CONTRIBUTING.rst
new file mode 100644
index 0000000000..09efdb059c
--- /dev/null
+++ b/contrib/hooks/multimail/CONTRIBUTING.rst
@@ -0,0 +1,30 @@
+git-multimail is an open-source project, built by volunteers. We would
+welcome your help!
+
+The current maintainers are Michael Haggerty <mhagger@alum.mit.edu>
+and Matthieu Moy <matthieu.moy@grenoble-inp.fr>.
+
+Please note that although a copy of git-multimail is distributed in
+the "contrib" section of the main Git project, development takes place
+in a separate git-multimail repository on GitHub:
+
+ https://github.com/git-multimail/git-multimail
+
+Whenever enough changes to git-multimail have accumulated, a new
+code-drop of git-multimail will be submitted for inclusion in the Git
+project.
+
+We use the GitHub issue tracker to keep track of bugs and feature
+requests, and we use GitHub pull requests to exchange patches (though,
+if you prefer, you can send patches via the Git mailing list with CC
+to the maintainers). Please sign off your patches as per the `Git
+project practice
+<https://github.com/git/git/blob/master/Documentation/SubmittingPatches#L234>`__.
+
+General discussion of git-multimail can take place on the main Git
+mailing list,
+
+ git@vger.kernel.org
+
+Please CC emails regarding git-multimail to the maintainers so that we
+don't overlook them.
diff --git a/contrib/hooks/multimail/README b/contrib/hooks/multimail/README
index e552c90c45..55120685f0 100644
--- a/contrib/hooks/multimail/README
+++ b/contrib/hooks/multimail/README
@@ -1,5 +1,5 @@
-git-multimail Version 1.1.1
-===========================
+git-multimail (version 1.2.0)
+=============================
.. image:: https://travis-ci.org/git-multimail/git-multimail.svg?branch=master
:target: https://travis-ci.org/git-multimail/git-multimail
@@ -53,11 +53,13 @@ By default, for each push received by the repository, git-multimail:
+ [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
- into another branch, then a one-line summary of the commit is
- included in the reference change email (as usual), but no
- additional commit email is generated.
+ By default, each commit appears in exactly one commit email, the
+ first time that it is pushed to the repository. If a commit is later
+ merged into another branch, then a one-line summary of the commit
+ is included in the reference change email (as usual), but no
+ additional commit email is generated. See
+ `multimailhook.refFilter(Inclusion|Exclusion|DoSend|DontSend)Regex`
+ below to configure which branches and tags are watched by the hook.
By default, reference change emails have their "Reply-To" field set
to the person who pushed the change, and commit emails have their
@@ -73,21 +75,8 @@ Requirements
------------
* Python 2.x, version 2.4 or later. No non-standard Python modules
- are required. git-multimail does *not* currently work with Python
- 3.x.
-
- The example scripts invoke Python using the following shebang line
- (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
- invoke the Python interpreter explicitly, for example via a tiny
- shell script like::
-
- #! /bin/sh
- /usr/local/bin/python /path/to/git_multimail.py "$@"
+ are required. git-multimail has preliminary support for Python 3
+ (but it has been better tested with Python 2).
* 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
@@ -146,7 +135,9 @@ following ``git config`` settings:
multimailhook.environment
- This describes the general environment of the repository.
+ This describes the general environment of the repository. In most
+ cases, you do not need to specify a value for this variable:
+ `git-multimail` will autodetect which environment to use.
Currently supported values:
* generic
@@ -161,18 +152,57 @@ multimailhook.environment
optionally read from gitolite.conf (see multimailhook.from).
For more information about gitolite and git-multimail, read
- doc/gitolite.rst
+ `<doc/gitolite.rst>`__
+
+ * stash
+
+ Environment to use when ``git-multimail`` is ran as an Atlassian
+ BitBucket Server (formerly known as Atlassian Stash) hook.
+
+ **Warning:** this mode was provided by a third-party contributor
+ and never tested by the git-multimail maintainers. It is
+ provided as-is and may or may not work for you.
+
+ This value is automatically assumed when the stash-specific
+ flags (``--stash-user`` and ``--stash-repo``) are specified on
+ the command line. When this environment is active, the username
+ and repo come from these two command line flags, which must be
+ specified.
+
+ * gerrit
+
+ Environment to use when ``git-multimail`` is ran as a
+ ``ref-updated`` Gerrit hook.
+
+ This value is used when the gerrit-specific command line flags
+ (``--oldrev``, ``--newrev``, ``--refname``, ``--project``) for
+ gerrit's ref-updated hook are present. When this environment is
+ active, the username of the pusher is taken from the
+ ``--submitter`` argument if that command line option is passed,
+ otherwise 'Gerrit' is used. The repository name is taken from
+ the ``--project`` option on the command line, which must be passed.
+
+ For more information about gerrit and git-multimail, read
+ `<doc/gerrit.rst>`__
- If neither of these environments is suitable for your setup, then
- you can implement a Python class that inherits from Environment
- and instantiate it via a script that looks like the example
+ If none of these environments is suitable for your setup, then you
+ can implement a Python class that inherits from Environment and
+ instantiate it via a script that looks like the example
post-receive script.
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``.
+ the ``--environment`` option. If it is not specified on the
+ command line or by ``multimailhook.environment``, the value is
+ guessed as follows:
+
+ * If stash-specific (respectively gerrit-specific) command flags
+ are present on the command-line, then ``stash`` (respectively
+ ``gerrit``) is used.
+
+ * If the environment variables $GL_USER and $GL_REPO are set, then
+ ``gitolite`` is used.
+
+ * If none of the above apply, then ``generic`` is used.
multimailhook.repoName
@@ -196,8 +226,8 @@ multimailhook.refchangeList
reference changes should be sent, as RFC 2822 email addresses
separated by commas. This configuration option can be
multivalued. The default is the value in
- multimailhook.mailingList. Set this value to the empty string to
- prevent reference change emails from being sent even if
+ multimailhook.mailingList. Set this value to "none" (or the empty
+ string) to prevent reference change emails from being sent even if
multimailhook.mailingList is set.
multimailhook.announceList
@@ -206,9 +236,9 @@ multimailhook.announceList
tags should be sent, as RFC 2822 email addresses separated by
commas. This configuration option can be multivalued. The
default is the value in multimailhook.refchangeList or
- multimailhook.mailingList. Set this value to the empty string to
- prevent annotated tag announcement emails from being sent even if
- one of the other values is set.
+ multimailhook.mailingList. Set this value to "none" (or the empty
+ string) to prevent annotated tag announcement emails from being sent
+ even if one of the other values is set.
multimailhook.commitList
@@ -216,7 +246,7 @@ multimailhook.commitList
commits should be sent, as RFC 2822 email addresses separated by
commas. This configuration option can be multivalued. The
default is the value in multimailhook.mailingList. Set this value
- to the empty string to prevent notification emails about
+ to "none" (or the empty string) to prevent notification emails about
individual commits from being sent even if
multimailhook.mailingList is set.
@@ -230,6 +260,20 @@ multimailhook.announceShortlog
not so straightforward, then the shortlog might be confusing
rather than useful. Default is false.
+multimailhook.commitEmailFormat
+
+ The format of email messages for the individual commits, can be "text" or
+ "html". In the latter case, the emails will include diffs using colorized
+ HTML instead of plain text used by default. Note that this currently the
+ ref change emails are always sent in plain text.
+
+ Note that when using "html", the formatting is done by parsing the
+ output of ``git log`` with ``-p``. When using
+ ``multimailhook.commitLogOpts`` to specify a ``--format`` for
+ ``git log``, one may get false positive (e.g. lines in the body of
+ the message starting with ``+++`` or ``---`` colored in red or
+ green).
+
multimailhook.refchangeShowGraph
If this option is set to true, then summary emails about reference
@@ -305,7 +349,7 @@ multimailhook.mailer
* multimailhook.smtpEncryption
- Set the security type. Allowed values: none, ssl.
+ Set the security type. Allowed values: none, ssl, tls.
Default=none.
* multimailhook.smtpServerDebugLevel
@@ -313,9 +357,26 @@ multimailhook.mailer
Integer number. Set to greater than 0 to activate debugging.
multimailhook.from
+multimailhook.fromCommit
+multimailhook.fromRefchange
+
+ If set, use this value in the From: field of generated emails.
+ ``fromCommit`` is used for commit emails, ``fromRefchange`` is
+ used for refchange emails, and ``from`` is used as fall-back in
+ all cases.
+
+ The value for these variables can be either:
+
+ - An email address, which will be used directly.
+
+ - The value ``pusher``, in which case the pusher's address (if
+ available) will be used.
- If set, use this value in the From: field of generated emails. If
- unset, the value of the From: header is determined as follows:
+ - The value ``author`` (meaningful only for replyToCommit), in which
+ case the commit author's address will be used.
+
+ If config values are 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::
@@ -425,6 +486,15 @@ multimailhook.commitLogOpts
--stat -p --cc``. Shell quoting is allowed; see
multimailhook.logOpts for details.
+multimailhook.dateSubstitute
+
+ String to use as a substitute for ``Date:`` in the output of ``git
+ log`` while formatting commit messages. This is usefull to avoid
+ emitting a line that can be interpreted by mailers as the start of
+ a cited message (Zimbra webmail in particular). Defaults to
+ ``CommitDate: ``. Set to an empty string or ``none`` to deactivate
+ the behavior.
+
multimailhook.emailDomain
Domain name appended to the username of the person doing the push
@@ -440,21 +510,13 @@ multimailhook.replyToRefchange
Addresses to use in the Reply-To: field for commit emails
(replyToCommit) and refchange emails (replyToRefchange).
multimailhook.replyTo is used as default when replyToCommit or
- replyToRefchange is not set. The value for these variables can be
- either:
-
- - An email address, which will be used directly.
-
- - The value `pusher`, in which case the pusher's address (if
- available) will be used. This is the default for refchange
- emails.
+ replyToRefchange is not set. The shortcuts ``pusher`` and
+ ``author`` are allowed with the same semantics as for
+ ``multimailhook.from``. In addition, the value ``none`` can be
+ used to omit the ``Reply-To:`` field.
- - 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
- omitted.
+ The default is ``pusher`` for refchange emails, and ``author`` for
+ commit emails.
multimailhook.quiet
@@ -478,6 +540,63 @@ multimailhook.combineWhenSingleCommit
single email.
Default: true
+multimailhook.refFilterInclusionRegex
+multimailhook.refFilterExclusionRegex
+multimailhook.refFilterDoSendRegex
+multimailhook.refFilterDontSendRegex
+
+ **Warning:** these options are experimental. They should work, but
+ the user-interface is not stable yet (in particular, the option
+ names may change). If you want to participate in stabilizing the
+ feature, please contact the maintainers and/or send pull-requests.
+
+ Regular expressions that can be used to limit refs for which email
+ updates will be sent. It is an error to specify both an inclusion
+ and an exclusion regex. If a ``refFilterInclusionRegex`` is
+ specified, emails will only be sent for refs which match this
+ regex. If a ``refFilterExclusionRegex`` regex is specified,
+ emails will be sent for all refs except those that match this
+ regex (or that match a predefined regex specific to the
+ environment, such as "^refs/notes" for most environments and
+ "^refs/notes|^refs/changes" for the gerrit environment).
+
+ The expressions are matched against the complete refname, and is
+ considered to match if any substring matches. For example, to
+ filter-out all tags, set ``refFilterExclusionRegex`` to
+ ``^refs/tags/`` (note the leading ``^`` but no trailing ``$``). If
+ you set ``refFilterExclusionRegex`` to ``master``, then any ref
+ containing ``master`` will be excluded (the ``master`` branch, but
+ also ``refs/tags/master`` or ``refs/heads/foo-master-bar``).
+
+ ``refFilterDoSendRegex`` and ``refFilterDontSendRegex`` are
+ analogous to ``refFilterInclusionRegex`` and
+ ``refFilterExclusionRegex`` with one difference: with
+ ``refFilterDoSendRegex`` and ``refFilterDontSendRegex``, commits
+ introduced by one excluded ref will not be considered as new when
+ they reach an included ref. Typically, if you add a branch ``foo``
+ to ``refFilterDontSendRegex``, push commits to this branch, and
+ later merge branch ``foo`` into ``master``, then the notification
+ email for ``master`` will contain a commit email only for the
+ merge commit. If you include ``foo`` in
+ ``refFilterExclusionRegex``, then at the time of merge, you will
+ receive one commit email per commit in the branch.
+
+ These variables can be multi-valued, like::
+
+ [multimailhook]
+ refFilterExclusionRegex = ^refs/tags/
+ refFilterExclusionRegex = ^refs/heads/master$
+
+ You can also provide a whitespace-separated list like::
+
+ [multimailhook]
+ refFilterExclusionRegex = ^refs/tags/ ^refs/heads/master$
+
+ Both examples exclude tags and the master branch, and are
+ equivalent to::
+
+ [multimailhook]
+ refFilterExclusionRegex = ^refs/tags/|^refs/heads/master$
Email filtering aids
--------------------
@@ -547,35 +666,8 @@ consider sharing them with the community!
Getting involved
----------------
-git-multimail is an open-source project, built by volunteers. We would
-welcome your help!
-
-The current maintainers are Michael Haggerty <mhagger@alum.mit.edu>
-and Matthieu Moy <matthieu.moy@grenoble-inp.fr>.
-
-Please note that although a copy of git-multimail is distributed in
-the "contrib" section of the main Git project, development takes place
-in a separate git-multimail repository on GitHub:
-
- https://github.com/git-multimail/git-multimail
-
-Whenever enough changes to git-multimail have accumulated, a new
-code-drop of git-multimail will be submitted for inclusion in the Git
-project.
-
-We use the GitHub issue tracker to keep track of bugs and feature
-requests, and we use GitHub pull requests to exchange patches (though,
-if you prefer, you can send patches via the Git mailing list with CC
-to the maintainers). Please sign off your patches as per the Git
-project practice.
-
-General discussion of git-multimail can take place on the main Git
-mailing list,
-
- git@vger.kernel.org
-
-Please CC emails regarding git-multimail to the maintainers so that we
-don't overlook them.
+Please, read `<CONTRIBUTING.rst>`__ for instructions on how to
+contribute to git-multimail.
Footnotes
diff --git a/contrib/hooks/multimail/README.Git b/contrib/hooks/multimail/README.Git
index f5d59a8d31..300a2a4d2d 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 July 03 2015 and consists of the "git-multimail" subdirectory from
+on October 11 2015 and consists of the "git-multimail" subdirectory from
revision
- 6d6c9eb62a054143322cfaecde3949189c065b46 refs/tags/1.1.1
+ c0791b9ef5821a746fc3475c25765e640452eaae refs/tags/1.2.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/doc/gerrit.rst b/contrib/hooks/multimail/doc/gerrit.rst
new file mode 100644
index 0000000000..8011d05dec
--- /dev/null
+++ b/contrib/hooks/multimail/doc/gerrit.rst
@@ -0,0 +1,56 @@
+Setting up git-multimail on Gerrit
+==================================
+
+Gerrit has its own email-sending system, but you may prefer using
+``git-multimail`` instead. It supports Gerrit natively as a Gerrit
+``ref-updated`` hook (Warning: `Gerrit hooks
+<https://gerrit-review.googlesource.com/Documentation/config-hooks.html>`__
+are distinct from Git hooks). Setting up ``git-multimail`` on a Gerrit
+installation can be done following the instructions below.
+
+The explanations show an easy way to set up ``git-multimail``,
+but leave ``git-multimail`` installed and unconfigured for a while. If
+you run Gerrit on a production server, it is advised that you
+execute the step "Set up the hook" last to avoid confusing your users
+in the meantime.
+
+Set up the hook
+---------------
+
+Create a directory ``$site_path/hooks/`` if it does not exist (if you
+don't know what ``$site_path`` is, run ``gerrit.sh status`` and look
+for a ``GERRIT_SITE`` line). Either copy ``git_multimail.py`` to
+``$site_path/hooks/ref-updated`` or create a wrapper script like
+this::
+
+ #! /bin/sh
+ exec /path/to/git_multimail.py "$@"
+
+In both cases, make sure the file is named exactly
+``$site_path/hooks/ref-updated`` and is executable.
+
+(Alternatively, you may configure the ``[hooks]`` section of
+gerrit.config)
+
+Configuration
+-------------
+
+Log on the gerrit server and edit ``$site_path/git/$project/config``
+to configure ``git-multimail``.
+
+Troubleshooting
+---------------
+
+Warning: this will disable ``git-multimail`` during the debug, and
+could confuse your users. Don't run on a production server.
+
+To debug configuration issues with ``git-multimail``, you can add the
+``--stdout`` option when calling ``git_multimail.py`` like this::
+
+ #!/bin/sh
+ exec /path/to/git-multimail/git-multimail/git_multimail.py \
+ --stdout "$@" >> /tmp/log.txt
+
+and try pushing from a test repository. You should see the source of
+the email that would have been sent in the output of ``git push`` in
+the file ``/tmp/log.txt``.
diff --git a/contrib/hooks/multimail/doc/gitolite.rst b/contrib/hooks/multimail/doc/gitolite.rst
new file mode 100644
index 0000000000..00aedd9c57
--- /dev/null
+++ b/contrib/hooks/multimail/doc/gitolite.rst
@@ -0,0 +1,109 @@
+Setting up git-multimail on gitolite
+====================================
+
+``git-multimail`` supports gitolite 3 natively.
+The explanations below show an easy way to set up ``git-multimail``,
+but leave ``git-multimail`` installed and unconfigured for a while. If
+you run gitolite on a production server, it is advised that you
+execute the step "Set up the hook" last to avoid confusing your users
+in the meantime.
+
+Set up the hook
+---------------
+
+Log in as your gitolite user.
+
+Create a file ``.gitolite/hooks/common/post-receive`` on your gitolite
+account containing (adapt the path, obviously)::
+
+ #!/bin/sh
+ exec /path/to/git-multimail/git-multimail/git_multimail.py "$@"
+
+Make sure it's executable (``chmod +x``). Record the hook in
+gitolite::
+
+ gitolite setup
+
+Configuration
+-------------
+
+First, you have to allow the admin to set Git configuration variables.
+
+As gitolite user, edit the line containing ``GIT_CONFIG_KEYS`` in file
+``.gitolite.rc``, to make it look like::
+
+ GIT_CONFIG_KEYS => 'multimailhook\..*',
+
+You can now log out and return to your normal user.
+
+In the ``gitolite-admin`` clone, edit the file ``conf/gitolite.conf``
+and add::
+
+ repo @all
+ # Not strictly needed as git_multimail.py will chose gitolite if
+ # $GL_USER is set.
+ config multimailhook.environment = gitolite
+ config multimailhook.mailingList = # Where emails should be sent
+ config multimailhook.from = # From address to use
+
+Obviously, you can customize all parameters on a per-repository basis by
+adding these ``config multimailhook.*`` lines in the section
+corresponding to a repository or set of repositories.
+
+To activate ``git-multimail`` on a per-repository basis, do not set
+``multimailhook.mailingList`` in the ``@all`` section and set it only
+for repositories for which you want ``git-multimail``.
+
+Alternatively, you can set up the ``From:`` field on a per-user basis
+by adding a ``BEGIN USER EMAILS``/``END USER EMAILS`` section (see
+``../README``).
+
+Specificities of Gitolite for Configuration
+-------------------------------------------
+
+Empty configuration variables
+.............................
+
+With gitolite, the syntax ``config multimailhook.commitList = ""``
+unsets the variable instead of setting it to an empty string (see
+`here
+<http://gitolite.com/gitolite/git-config.html#an-important-warning-about-deleting-a-config-line>`__).
+As a result, there is no way to set a variable to the empty string.
+In all most places where an empty value is required, git-multimail
+now allows to specify special ``"none"`` value (case-sensitive) to
+mean the same.
+
+Alternatively, one can use ``" "`` (a single space) instead of ``""``.
+In most cases (in particular ``multimailhook.*List`` variables), this
+will be equivalent to an empty string.
+
+If you have a use-case where ``"none"`` is not an acceptable value and
+you need ``" "`` or ``""`` instead, please report it as a bug to
+git-multimail.
+
+Allowing Regular Expressions in Configuration
+.............................................
+
+gitolite has a mechanism to prevent unsafe configuration variable
+values, which prevent characters like ``|`` commonly used in regular
+expressions. If you do not need the safety feature of gitolite and
+need to use regular expressions in your configuration (e.g. for
+``multimailhook.refFilter*`` variables), set
+`UNSAFE_PATT
+<http://gitolite.com/gitolite/git-config.html#unsafe-patt>`__ to a
+less restrictive value.
+
+Troubleshooting
+---------------
+
+Warning: this will disable ``git-multimail`` during the debug, and
+could confuse your users. Don't run on a production server.
+
+To debug configuration issues with ``git-multimail``, you can add the
+``--stdout`` option when calling ``git_multimail.py`` like this::
+
+ #!/bin/sh
+ exec /path/to/git-multimail/git-multimail/git_multimail.py --stdout "$@"
+
+and try pushing from a test repository. You should see the source of
+the email that would have been sent in the output of ``git push``.
diff --git a/contrib/hooks/multimail/git_multimail.py b/contrib/hooks/multimail/git_multimail.py
index c06ce7a515..0180dba431 100755
--- a/contrib/hooks/multimail/git_multimail.py
+++ b/contrib/hooks/multimail/git_multimail.py
@@ -1,4 +1,6 @@
-#! /usr/bin/env python2
+#! /usr/bin/env python
+
+__version__ = '1.2.0'
# Copyright (c) 2015 Matthieu Moy and others
# Copyright (c) 2012-2014 Michael Haggerty and others
@@ -56,8 +58,54 @@ import shlex
import optparse
import smtplib
import time
+import cgi
+
+PYTHON3 = sys.version_info >= (3, 0)
+
+if sys.version_info <= (2, 5):
+ def all(iterable):
+ for element in iterable:
+ if not element:
+ return False
+ return True
+
+
+def is_ascii(s):
+ return all(ord(c) < 128 and ord(c) > 0 for c in s)
+
+
+if PYTHON3:
+ def str_to_bytes(s):
+ return s.encode(ENCODING)
+
+ def bytes_to_str(s):
+ return s.decode(ENCODING)
+
+ unicode = str
+
+ def write_str(f, msg):
+ # Try outputing with the default encoding. If it fails,
+ # try UTF-8.
+ try:
+ f.buffer.write(msg.encode(sys.getdefaultencoding()))
+ except UnicodeEncodeError:
+ f.buffer.write(msg.encode(ENCODING))
+else:
+ def str_to_bytes(s):
+ return s
+
+ def bytes_to_str(s):
+ return s
+
+ def write_str(f, msg):
+ f.write(msg)
+
+ def next(it):
+ return it.next()
+
try:
+ from email.charset import Charset
from email.utils import make_msgid
from email.utils import getaddresses
from email.utils import formataddr
@@ -65,6 +113,7 @@ try:
from email.header import Header
except ImportError:
# Prior to Python 2.5, the email module used different names:
+ from email.Charset import Charset
from email.Utils import make_msgid
from email.Utils import getaddresses
from email.Utils import formataddr
@@ -109,7 +158,7 @@ Date: %(send_date)s
To: %(recipients)s
Subject: %(subject)s
MIME-Version: 1.0
-Content-Type: text/plain; charset=%(charset)s
+Content-Type: text/%(contenttype)s; charset=%(charset)s
Content-Transfer-Encoding: 8bit
Message-ID: %(msgid)s
From: %(fromaddr)s
@@ -120,6 +169,8 @@ X-Git-Refname: %(refname)s
X-Git-Reftype: %(refname_type)s
X-Git-Oldrev: %(oldrev)s
X-Git-Newrev: %(newrev)s
+X-Git-NotificationType: ref_changed
+X-Git-Multimail-Version: %(multimail_version)s
Auto-Submitted: auto-generated
"""
@@ -238,7 +289,7 @@ 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
+Content-Type: text/%(contenttype)s; charset=%(charset)s
Content-Transfer-Encoding: 8bit
From: %(fromaddr)s
Reply-To: %(reply_to)s
@@ -249,6 +300,8 @@ X-Git-Repo: %(repo_shortname)s
X-Git-Refname: %(refname)s
X-Git-Reftype: %(refname_type)s
X-Git-Rev: %(rev)s
+X-Git-NotificationType: diff
+X-Git-Multimail-Version: %(multimail_version)s
Auto-Submitted: auto-generated
"""
@@ -270,7 +323,7 @@ Date: %(send_date)s
To: %(recipients)s
Subject: %(subject)s
MIME-Version: 1.0
-Content-Type: text/plain; charset=%(charset)s
+Content-Type: text/%(contenttype)s; charset=%(charset)s
Content-Transfer-Encoding: 8bit
Message-ID: %(msgid)s
From: %(fromaddr)s
@@ -282,6 +335,8 @@ X-Git-Reftype: %(refname_type)s
X-Git-Oldrev: %(oldrev)s
X-Git-Newrev: %(newrev)s
X-Git-Rev: %(rev)s
+X-Git-NotificationType: ref_changed_plus_diff
+X-Git-Multimail-Version: %(multimail_version)s
Auto-Submitted: auto-generated
"""
@@ -352,12 +407,14 @@ def read_git_output(args, input=None, keepends=False, **kw):
def read_output(cmd, input=None, keepends=False, **kw):
if input:
stdin = subprocess.PIPE
+ input = str_to_bytes(input)
else:
stdin = None
p = subprocess.Popen(
cmd, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw
)
(out, err) = p.communicate(input)
+ out = bytes_to_str(out)
retcode = p.wait()
if retcode:
raise CommandError(cmd, retcode)
@@ -418,26 +475,37 @@ def git_log(spec, **kw):
def header_encode(text, header_name=None):
"""Encode and line-wrap the value of an email header field."""
- try:
- if isinstance(text, str):
- text = text.decode(ENCODING, 'replace')
- return Header(text, header_name=header_name).encode()
- except UnicodeEncodeError:
- return Header(text, header_name=header_name, charset=CHARSET,
- errors='replace').encode()
+ # Convert to unicode, if required.
+ if not isinstance(text, unicode):
+ text = unicode(text, 'utf-8')
+
+ if is_ascii(text):
+ charset = 'ascii'
+ else:
+ charset = 'utf-8'
+
+ return Header(text, header_name=header_name, charset=Charset(charset)).encode()
def addr_header_encode(text, header_name=None):
"""Encode and line-wrap the value of an email header field containing
email addresses."""
- return Header(
- ', '.join(
- formataddr((header_encode(name), emailaddr))
- for name, emailaddr in getaddresses([text])
- ),
- header_name=header_name
- ).encode()
+ # Convert to unicode, if required.
+ if not isinstance(text, unicode):
+ text = unicode(text, 'utf-8')
+
+ text = ', '.join(
+ formataddr((header_encode(name), emailaddr))
+ for name, emailaddr in getaddresses([text])
+ )
+
+ if is_ascii(text):
+ charset = 'ascii'
+ else:
+ charset = 'utf-8'
+
+ return Header(text, header_name=header_name, charset=Charset(charset)).encode()
class Config(object):
@@ -496,7 +564,8 @@ class Config(object):
['config', '--get-all', '--null', '%s.%s' % (self.section, name)],
env=self.env, keepends=True,
))
- except CommandError, e:
+ except CommandError:
+ t, e, traceback = sys.exc_info()
if e.retcode == 1:
# "the section or key is invalid"; i.e., there is no
# value for the specified key.
@@ -504,18 +573,6 @@ class Config(object):
else:
raise
- def get_recipients(self, name, default=None):
- """Read a recipients list from the configuration.
-
- Return the result as a comma-separated list of email
- addresses, or default if the option is unset. If the setting
- has multiple values, concatenate them with comma separators."""
-
- lines = self.get_all(name, default=None)
- if lines is None:
- return default
- return ', '.join(line.strip() for line in lines)
-
def set(self, name, value):
read_git_output(
['config', '%s.%s' % (self.section, name), value],
@@ -542,7 +599,8 @@ class Config(object):
['config', '--unset-all', '%s.%s' % (self.section, name)],
env=self.env,
)
- except CommandError, e:
+ except CommandError:
+ t, e, traceback = sys.exc_info()
if e.retcode == 5:
# The name doesn't exist, which is what we wanted anyway...
pass
@@ -636,7 +694,7 @@ class GitObject(object):
if not self.sha1:
raise ValueError('Empty commit has no summary')
- return iter(generate_summaries('--no-walk', self.sha1)).next()
+ return next(iter(generate_summaries('--no-walk', self.sha1)))
def __eq__(self, other):
return isinstance(other, GitObject) and self.sha1 == other.sha1
@@ -647,6 +705,10 @@ class GitObject(object):
def __nonzero__(self):
return bool(self.sha1)
+ def __bool__(self):
+ """Python 2 backward compatibility"""
+ return self.__nonzero__()
+
def __str__(self):
return self.sha1 or ZEROS
@@ -661,6 +723,12 @@ class Change(object):
def __init__(self, environment):
self.environment = environment
self._values = None
+ self._contains_html_diff = False
+
+ def _contains_diff(self):
+ # We do contain a diff, should it be rendered in HTML?
+ if self.environment.commit_email_format == "html":
+ self._contains_html_diff = True
def _compute_values(self):
"""Return a dictionary {keyword: expansion} for this Change.
@@ -670,7 +738,12 @@ class Change(object):
get_values(). The return value should always be a new
dictionary."""
- return self.environment.get_values()
+ values = self.environment.get_values()
+ fromaddr = self.environment.get_fromaddr(change=self)
+ if fromaddr is not None:
+ values['fromaddr'] = fromaddr
+ values['multimail_version'] = get_version()
+ return values
def get_values(self, **extra_values):
"""Return a dictionary {keyword: expansion} for this Change.
@@ -713,12 +786,18 @@ class Change(object):
skip lines that contain references to unknown variables."""
values = self.get_values(**extra_values)
+ if self._contains_html_diff:
+ values['contenttype'] = 'html'
+ else:
+ values['contenttype'] = 'plain'
+
for line in template.splitlines():
- (name, value) = line.split(':', 1)
+ (name, value) = line.split(': ', 1)
try:
value = value % values
- except KeyError, e:
+ except KeyError:
+ t, e, traceback = sys.exc_info()
if DEBUG:
self.environment.log_warning(
'Warning: unknown variable %r in the following line; line skipped:\n'
@@ -764,6 +843,24 @@ class Change(object):
raise NotImplementedError()
+ def _wrap_for_html(self, lines):
+ """Wrap the lines in HTML <pre> tag when using HTML format.
+
+ Escape special HTML characters and add <pre> and </pre> tags around
+ the given lines if we should be generating HTML as indicated by
+ self._contains_html_diff being set to true.
+ """
+ if self._contains_html_diff:
+ yield "<pre style='margin:0'>\n"
+
+ for line in lines:
+ yield cgi.escape(line)
+
+ yield '</pre>\n'
+ else:
+ for line in lines:
+ yield line
+
def generate_email(self, push, body_filter=None, extra_header_values={}):
"""Generate an email describing this change.
@@ -779,18 +876,76 @@ class Change(object):
for line in self.generate_email_header(**extra_header_values):
yield line
yield '\n'
- for line in self.generate_email_intro():
+ for line in self._wrap_for_html(self.generate_email_intro()):
yield line
body = self.generate_email_body(push)
if body_filter is not None:
body = body_filter(body)
+
+ diff_started = False
+ if self._contains_html_diff:
+ # "white-space: pre" is the default, but we need to
+ # specify it again in case the message is viewed in a
+ # webmail which wraps it in an element setting white-space
+ # to something else (Zimbra does this and sets
+ # white-space: pre-line).
+ yield '<pre style="white-space: pre; background: #F8F8F8">'
for line in body:
+ if self._contains_html_diff:
+ # This is very, very naive. It would be much better to really
+ # parse the diff, i.e. look at how many lines do we have in
+ # the hunk headers instead of blindly highlighting everything
+ # that looks like it might be part of a diff.
+ bgcolor = ''
+ fgcolor = ''
+ if line.startswith('--- a/'):
+ diff_started = True
+ bgcolor = 'e0e0ff'
+ elif line.startswith('diff ') or line.startswith('index '):
+ diff_started = True
+ fgcolor = '808080'
+ elif diff_started:
+ if line.startswith('+++ '):
+ bgcolor = 'e0e0ff'
+ elif line.startswith('@@'):
+ bgcolor = 'e0e0e0'
+ elif line.startswith('+'):
+ bgcolor = 'e0ffe0'
+ elif line.startswith('-'):
+ bgcolor = 'ffe0e0'
+ elif line.startswith('commit '):
+ fgcolor = '808000'
+ elif line.startswith(' '):
+ fgcolor = '404040'
+
+ # Chop the trailing LF, we don't want it inside <pre>.
+ line = cgi.escape(line[:-1])
+
+ if bgcolor or fgcolor:
+ style = 'display:block; white-space:pre;'
+ if bgcolor:
+ style += 'background:#' + bgcolor + ';'
+ if fgcolor:
+ style += 'color:#' + fgcolor + ';'
+ # Use a <span style='display:block> to color the
+ # whole line. The newline must be inside the span
+ # to display properly both in Firefox and in
+ # text-based browser.
+ line = "<span style='%s'>%s\n</span>" % (style, line)
+ else:
+ line = line + '\n'
+
yield line
+ if self._contains_html_diff:
+ yield '</pre>'
- for line in self.generate_email_footer():
+ for line in self._wrap_for_html(self.generate_email_footer()):
yield line
+ def get_alt_fromaddr(self):
+ return None
+
class Revision(Change):
"""A Change consisting of a single git commit."""
@@ -867,14 +1022,25 @@ class Revision(Change):
def generate_email_body(self, push):
"""Show this revision."""
- return read_git_lines(
- ['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1],
- keepends=True,
- )
+ for line in read_git_lines(
+ ['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1],
+ keepends=True,
+ ):
+ if line.startswith('Date: ') and self.environment.date_substitute:
+ yield self.environment.date_substitute + line[len('Date: '):]
+ else:
+ yield line
def generate_email_footer(self):
return self.expand_lines(REVISION_FOOTER_TEMPLATE)
+ def generate_email(self, push, body_filter=None, extra_header_values={}):
+ self._contains_diff()
+ return Change.generate_email(self, push, body_filter, extra_header_values)
+
+ def get_alt_fromaddr(self):
+ return self.environment.from_commit
+
class ReferenceChange(Change):
"""A Change to a Git reference.
@@ -1096,10 +1262,10 @@ class ReferenceChange(Change):
yield '\n'
yield 'Detailed log of new commits:\n\n'
for line in read_git_lines(
- ['log', '--no-walk']
- + self.logopts
- + new_commits_list
- + ['--'],
+ ['log', '--no-walk'] +
+ self.logopts +
+ new_commits_list +
+ ['--'],
keepends=True,
):
yield line
@@ -1253,9 +1419,9 @@ 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,)],
+ ['diff-tree'] +
+ self.diffopts +
+ ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)],
keepends=True,
):
yield line
@@ -1316,6 +1482,9 @@ class ReferenceChange(Change):
)
yield '\n'
+ def get_alt_fromaddr(self):
+ return self.environment.from_refchange
+
class BranchChange(ReferenceChange):
refname_type = 'branch'
@@ -1397,9 +1566,9 @@ class BranchChange(ReferenceChange):
# 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
+ len(new_commits) == 1 and
+ len(new_commits[0][1]) == 1 and
+ new_commits[0][0] in known_added_sha1s
):
return None
@@ -1432,6 +1601,7 @@ class BranchChange(ReferenceChange):
values['subject'] = self.expand(COMBINED_REFCHANGE_REVISION_SUBJECT_TEMPLATE, **values)
self._single_revision = revision
+ self._contains_diff()
self.header_template = COMBINED_HEADER_TEMPLATE
self.intro_template = COMBINED_INTRO_TEMPLATE
self.footer_template = COMBINED_FOOTER_TEMPLATE
@@ -1690,17 +1860,18 @@ class SendMailer(Mailer):
def send(self, lines, to_addrs):
try:
p = subprocess.Popen(self.command, stdin=subprocess.PIPE)
- except OSError, e:
+ except OSError:
sys.stderr.write(
- '*** Cannot execute command: %s\n' % ' '.join(self.command)
- + '*** %s\n' % str(e)
- + '*** Try setting multimailhook.mailer to "smtp"\n'
+ '*** Cannot execute command: %s\n' % ' '.join(self.command) +
+ '*** %s\n' % sys.exc_info()[1] +
+ '*** Try setting multimailhook.mailer to "smtp"\n' +
'*** to send emails without using the sendmail command.\n'
)
sys.exit(1)
try:
+ lines = (str_to_bytes(line) for line in lines)
p.stdin.writelines(lines)
- except Exception, e:
+ except Exception:
sys.stderr.write(
'*** Error while generating commit email\n'
'*** - mail sending aborted.\n'
@@ -1710,7 +1881,7 @@ class SendMailer(Mailer):
p.terminate()
except AttributeError:
pass
- raise e
+ raise
else:
p.stdin.close()
retcode = p.wait()
@@ -1770,11 +1941,11 @@ class SMTPMailer(Mailer):
"*** Setting debug on for SMTP server connection (%s) ***\n"
% self.smtpserverdebuglevel)
self.smtp.set_debuglevel(self.smtpserverdebuglevel)
- except Exception, e:
+ except Exception:
sys.stderr.write(
'*** Error establishing SMTP connection to %s ***\n'
% self.smtpserver)
- sys.stderr.write('*** %s\n' % str(e))
+ sys.stderr.write('*** %s\n' % sys.exc_info()[1])
sys.exit(1)
def __del__(self):
@@ -1784,16 +1955,15 @@ class SMTPMailer(Mailer):
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:
+ except Exception:
sys.stderr.write('*** Error sending email ***\n')
- sys.stderr.write('*** %s\n' % str(e))
+ sys.stderr.write('*** %s\n' % sys.exc_info()[1])
self.smtp.quit()
sys.exit(1)
@@ -1809,9 +1979,10 @@ class OutputMailer(Mailer):
self.f = f
def send(self, lines, to_addrs):
- self.f.write(self.SEPARATOR)
- self.f.writelines(lines)
- self.f.write(self.SEPARATOR)
+ write_str(self.f, self.SEPARATOR)
+ for line in lines:
+ write_str(self.f, line)
+ write_str(self.f, self.SEPARATOR)
def get_git_dir():
@@ -1877,11 +2048,13 @@ class Environment(object):
Return the address to be used as the 'From' email address
in the email envelope.
- get_fromaddr()
+ get_fromaddr(change=None)
Return the 'From' email address used in the email 'From:'
- headers. (May be a full RFC 2822 email address like 'Joe
- User <user@example.com>'.)
+ headers. If the change is known when this function is
+ called, it is passed in as the 'change' parameter. (May
+ be a full RFC 2822 email address like 'Joe User
+ <user@example.com>'.)
get_administrator()
@@ -1901,12 +2074,29 @@ class Environment(object):
get_reply_to_commit() is used for individual commit
emails.
+ get_ref_filter_regex()
+
+ Return a tuple -- a compiled regex, and a boolean indicating
+ whether the regex picks refs to include (if False, the regex
+ matches on refs to exclude).
+
+ get_default_ref_ignore_regex()
+
+ Return a regex that should be ignored for both what emails
+ to send and when computing what commits are considered new
+ to the repository. Default is "^refs/notes/".
+
They should also define the following attributes:
announce_show_shortlog (bool)
True iff announce emails should include a shortlog.
+ commit_email_format (string)
+
+ If "html", generate commit emails in HTML instead of plain text
+ used by default.
+
refchange_showgraph (bool)
True iff refchanges emails should include a detailed graph.
@@ -1939,6 +2129,11 @@ class Environment(object):
commit mail. The value should be a list of strings
representing words to be passed to the command.
+ date_substitute (string)
+
+ String to be used in substitution for 'Date:' at start of
+ line in the output of 'git log'.
+
quiet (bool)
On success do not write to stderr
@@ -1950,6 +2145,13 @@ class Environment(object):
True if a combined email should be produced when a single
new commit is pushed to a branch, False otherwise.
+ from_refchange, from_commit (strings)
+
+ Addresses to use for the From: field for refchange emails
+ and commit emails respectively. Set from
+ multimailhook.fromRefchange and multimailhook.fromCommit
+ by ConfigEnvironmentMixin.
+
"""
REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\.git)$')
@@ -1957,6 +2159,7 @@ class Environment(object):
def __init__(self, osenv=None):
self.osenv = osenv or os.environ
self.announce_show_shortlog = False
+ self.commit_email_format = "text"
self.maxcommitemails = 500
self.diffopts = ['--stat', '--summary', '--find-copies-harder']
self.graphopts = ['--oneline', '--decorate']
@@ -1964,6 +2167,7 @@ class Environment(object):
self.refchange_showgraph = False
self.refchange_showlog = False
self.commitlogopts = ['-C', '--stat', '-p', '--cc']
+ self.date_substitute = 'AuthorDate: '
self.quiet = False
self.stdout = False
self.combine_when_single_commit = True
@@ -1972,7 +2176,6 @@ class Environment(object):
'administrator',
'charset',
'emailprefix',
- 'fromaddr',
'pusher',
'pusher_email',
'repo_path',
@@ -1998,7 +2201,7 @@ class Environment(object):
def get_pusher_email(self):
return None
- def get_fromaddr(self):
+ def get_fromaddr(self, change=None):
config = Config('user')
fromname = config.get('name', default='')
fromemail = config.get('email', default='')
@@ -2080,6 +2283,15 @@ class Environment(object):
def get_reply_to_commit(self, revision):
return revision.author
+ def get_default_ref_ignore_regex(self):
+ # The commit messages of git notes are essentially meaningless
+ # and "filenames" in git notes commits are an implementational
+ # detail that might surprise users at first. As such, we
+ # would need a completely different method for handling emails
+ # of git notes in order for them to be of benefit for users,
+ # which we simply do not have right now.
+ return "^refs/notes/"
+
def filter_body(self, lines):
"""Filter the lines intended for an email body.
@@ -2095,19 +2307,19 @@ class Environment(object):
"""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)
+ write_str(sys.stderr, 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)
+ write_str(sys.stderr, 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)
+ write_str(sys.stderr, msg)
class ConfigEnvironmentMixin(Environment):
@@ -2128,6 +2340,14 @@ class ConfigEnvironmentMixin(Environment):
class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
"""An Environment that reads most of its information from "git config"."""
+ @staticmethod
+ def forbid_field_values(name, value, forbidden):
+ for forbidden_val in forbidden:
+ if value is not None and value.lower() == forbidden:
+ raise ConfigurationException(
+ '"%s" is not an allowed setting for %s' % (value, name)
+ )
+
def __init__(self, config, **kw):
super(ConfigOptionsEnvironmentMixin, self).__init__(
config=config, **kw
@@ -2144,14 +2364,26 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
if val is not None:
setattr(self, var, val)
+ commit_email_format = config.get('commitEmailFormat')
+ if commit_email_format is not None:
+ if commit_email_format != "html" and commit_email_format != "text":
+ self.log_warning(
+ '*** Unknown value for multimailhook.commitEmailFormat: %s\n' %
+ commit_email_format +
+ '*** Expected either "text" or "html". Ignoring.\n'
+ )
+ else:
+ self.commit_email_format = commit_email_format
+
maxcommitemails = config.get('maxcommitemails')
if maxcommitemails is not None:
try:
self.maxcommitemails = int(maxcommitemails)
except ValueError:
self.log_warning(
- '*** Malformed value for multimailhook.maxCommitEmails: %s\n' % maxcommitemails
- + '*** Expected a number. Ignoring.\n'
+ '*** Malformed value for multimailhook.maxCommitEmails: %s\n'
+ % maxcommitemails +
+ '*** Expected a number. Ignoring.\n'
)
diffopts = config.get('diffopts')
@@ -2170,32 +2402,44 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
if commitlogopts is not None:
self.commitlogopts = shlex.split(commitlogopts)
+ date_substitute = config.get('dateSubstitute')
+ if date_substitute == 'none':
+ self.date_substitute = None
+ elif date_substitute is not None:
+ self.date_substitute = date_substitute
+
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'
- ):
- raise ConfigurationException(
- '"author" is not an allowed setting for replyToRefchange'
- )
+ self.forbid_field_values('replyToRefchange',
+ self.__reply_to_refchange,
+ ['author'])
self.__reply_to_commit = config.get('replyToCommit', default=reply_to)
+ from_addr = self.config.get('from')
+ self.from_refchange = config.get('fromRefchange')
+ self.forbid_field_values('fromRefchange',
+ self.from_refchange,
+ ['author', 'none'])
+ self.from_commit = config.get('fromCommit')
+ self.forbid_field_values('fromCommit',
+ self.from_commit,
+ ['none'])
+
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')
- or self.get_sender()
- or super(ConfigOptionsEnvironmentMixin, self).get_administrator()
+ self.config.get('administrator') or
+ self.get_sender() or
+ super(ConfigOptionsEnvironmentMixin, self).get_administrator()
)
def get_repo_shortname(self):
return (
- self.config.get('reponame')
- or super(ConfigOptionsEnvironmentMixin, self).get_repo_shortname()
+ self.config.get('reponame') or
+ super(ConfigOptionsEnvironmentMixin, self).get_repo_shortname()
)
def get_emailprefix(self):
@@ -2212,33 +2456,42 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
def get_sender(self):
return self.config.get('envelopesender')
- def get_fromaddr(self):
+ def process_addr(self, addr, change):
+ if addr.lower() == 'author':
+ if hasattr(change, 'author'):
+ return change.author
+ else:
+ return None
+ elif addr.lower() == 'pusher':
+ return self.get_pusher_email()
+ elif addr.lower() == 'none':
+ return None
+ else:
+ return addr
+
+ def get_fromaddr(self, change=None):
fromaddr = self.config.get('from')
+ if change:
+ alt_fromaddr = change.get_alt_fromaddr()
+ if alt_fromaddr:
+ fromaddr = alt_fromaddr
+ if fromaddr:
+ fromaddr = self.process_addr(fromaddr, change)
if fromaddr:
return fromaddr
- return super(ConfigOptionsEnvironmentMixin, self).get_fromaddr()
+ return super(ConfigOptionsEnvironmentMixin, self).get_fromaddr(change)
def get_reply_to_refchange(self, refchange):
if self.__reply_to_refchange is None:
return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_refchange(refchange)
- elif self.__reply_to_refchange.lower() == 'pusher':
- return self.get_pusher_email()
- elif self.__reply_to_refchange.lower() == 'none':
- return None
else:
- return self.__reply_to_refchange
+ return self.process_addr(self.__reply_to_refchange, refchange)
def get_reply_to_commit(self, revision):
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.author
- elif self.__reply_to_commit.lower() == 'pusher':
- return self.get_pusher_email()
- elif self.__reply_to_commit.lower() == 'none':
- return None
else:
- return self.__reply_to_commit
+ return self.process_addr(self.__reply_to_commit, revision)
def get_scancommitforcc(self):
return self.config.get('scancommitforcc')
@@ -2270,12 +2523,14 @@ class FilterLinesEnvironmentMixin(Environment):
def filter_body(self, lines):
lines = super(FilterLinesEnvironmentMixin, self).filter_body(lines)
if self.__strict_utf8:
- lines = (line.decode(ENCODING, 'replace') for line in lines)
+ if not PYTHON3:
+ lines = (line.decode(ENCODING, 'replace') for line in lines)
# Limit the line length in Unicode-space to avoid
# splitting characters:
if self.__emailmaxlinelength:
lines = limit_linelength(lines, self.__emailmaxlinelength)
- lines = (line.encode(ENCODING, 'replace') for line in lines)
+ if not PYTHON3:
+ lines = (line.encode(ENCODING, 'replace') for line in lines)
elif self.__emailmaxlinelength:
lines = limit_linelength(lines, self.__emailmaxlinelength)
@@ -2404,10 +2659,10 @@ class StaticRecipientsEnvironmentMixin(Environment):
# actual *contents* of the change being reported, we only
# choose based on the *type* of the change. Therefore we can
# compute them once and for all:
- if not (refchange_recipients
- or announce_recipients
- or revision_recipients
- or scancommitforcc):
+ if not (refchange_recipients or
+ announce_recipients or
+ revision_recipients or
+ scancommitforcc):
raise ConfigurationException('No email recipients configured!')
self.__refchange_recipients = refchange_recipients
self.__announce_recipients = announce_recipients
@@ -2457,13 +2712,104 @@ class ConfigRecipientsEnvironmentMixin(
found, raise a ConfigurationException."""
for name in names:
- retval = config.get_recipients(name)
- if retval is not None:
- return retval
+ lines = config.get_all(name)
+ if lines is not None:
+ lines = [line.strip() for line in lines]
+ # Single "none" is a special value equivalen to empty string.
+ if lines == ['none']:
+ lines = ['']
+ return ', '.join(lines)
else:
return ''
+class StaticRefFilterEnvironmentMixin(Environment):
+ """Set branch filter statically based on constructor parameters."""
+
+ def __init__(self, ref_filter_incl_regex, ref_filter_excl_regex,
+ ref_filter_do_send_regex, ref_filter_dont_send_regex,
+ **kw):
+ super(StaticRefFilterEnvironmentMixin, self).__init__(**kw)
+
+ if ref_filter_incl_regex and ref_filter_excl_regex:
+ raise ConfigurationException(
+ "Cannot specify both a ref inclusion and exclusion regex.")
+ self.__is_inclusion_filter = bool(ref_filter_incl_regex)
+ default_exclude = self.get_default_ref_ignore_regex()
+ if ref_filter_incl_regex:
+ ref_filter_regex = ref_filter_incl_regex
+ elif ref_filter_excl_regex:
+ ref_filter_regex = ref_filter_excl_regex + '|' + default_exclude
+ else:
+ ref_filter_regex = default_exclude
+ try:
+ self.__compiled_regex = re.compile(ref_filter_regex)
+ except Exception:
+ raise ConfigurationException(
+ 'Invalid Ref Filter Regex "%s": %s' % (ref_filter_regex, sys.exc_info()[1]))
+
+ if ref_filter_do_send_regex and ref_filter_dont_send_regex:
+ raise ConfigurationException(
+ "Cannot specify both a ref doSend and dontSend regex.")
+ if ref_filter_do_send_regex or ref_filter_dont_send_regex:
+ self.__is_do_send_filter = bool(ref_filter_do_send_regex)
+ if ref_filter_incl_regex:
+ ref_filter_send_regex = ref_filter_incl_regex
+ elif ref_filter_excl_regex:
+ ref_filter_send_regex = ref_filter_excl_regex
+ else:
+ ref_filter_send_regex = '.*'
+ self.__is_do_send_filter = True
+ try:
+ self.__send_compiled_regex = re.compile(ref_filter_send_regex)
+ except Exception:
+ raise ConfigurationException(
+ 'Invalid Ref Filter Regex "%s": %s' %
+ (ref_filter_send_regex, sys.exc_info()[1]))
+ else:
+ self.__send_compiled_regex = self.__compiled_regex
+ self.__is_do_send_filter = self.__is_inclusion_filter
+
+ def get_ref_filter_regex(self, send_filter=False):
+ if send_filter:
+ return self.__send_compiled_regex, self.__is_do_send_filter
+ else:
+ return self.__compiled_regex, self.__is_inclusion_filter
+
+
+class ConfigRefFilterEnvironmentMixin(
+ ConfigEnvironmentMixin,
+ StaticRefFilterEnvironmentMixin
+ ):
+ """Determine branch filtering statically based on config."""
+
+ def _get_regex(self, config, key):
+ """Get a list of whitespace-separated regex. The refFilter* config
+ variables are multivalued (hence the use of get_all), and we
+ allow each entry to be a whitespace-separated list (hence the
+ split on each line). The whole thing is glued into a single regex."""
+ values = config.get_all(key)
+ if values is None:
+ return values
+ items = []
+ for line in values:
+ for i in line.split():
+ items.append(i)
+ if items == []:
+ return None
+ return '|'.join(items)
+
+ def __init__(self, config, **kw):
+ super(ConfigRefFilterEnvironmentMixin, self).__init__(
+ config=config,
+ ref_filter_incl_regex=self._get_regex(config, 'refFilterInclusionRegex'),
+ ref_filter_excl_regex=self._get_regex(config, 'refFilterExclusionRegex'),
+ ref_filter_do_send_regex=self._get_regex(config, 'refFilterDoSendRegex'),
+ ref_filter_dont_send_regex=self._get_regex(config, 'refFilterDontSendRegex'),
+ **kw
+ )
+
+
class ProjectdescEnvironmentMixin(Environment):
"""Make a "projectdesc" value available for templates.
@@ -2499,6 +2845,7 @@ class GenericEnvironment(
ComputeFQDNEnvironmentMixin,
ConfigFilterLinesEnvironmentMixin,
ConfigRecipientsEnvironmentMixin,
+ ConfigRefFilterEnvironmentMixin,
PusherDomainEnvironmentMixin,
ConfigOptionsEnvironmentMixin,
GenericEnvironmentMixin,
@@ -2513,14 +2860,14 @@ class GitoliteEnvironmentMixin(Environment):
# repo_shortname (though it's probably not as good as a value
# the user might have explicitly put in his config).
return (
- self.osenv.get('GL_REPO', None)
- or super(GitoliteEnvironmentMixin, self).get_repo_shortname()
+ self.osenv.get('GL_REPO', None) or
+ super(GitoliteEnvironmentMixin, self).get_repo_shortname()
)
def get_pusher(self):
return self.osenv.get('GL_USER', 'unknown user')
- def get_fromaddr(self):
+ def get_fromaddr(self, change=None):
GL_USER = self.osenv.get('GL_USER')
if GL_USER is not None:
# Find the path to gitolite.conf. Note that gitolite v3
@@ -2536,9 +2883,9 @@ class GitoliteEnvironmentMixin(Environment):
f = open(GL_CONF, 'rU')
try:
in_user_emails_section = False
- re_template = r'^\s*#\s*{}\s*$'
+ re_template = r'^\s*#\s*%s\s*$'
re_begin, re_user, re_end = (
- re.compile(re_template.format(x))
+ re.compile(re_template % x)
for x in (
r'BEGIN\s+USER\s+EMAILS',
re.escape(GL_USER) + r'\s+(.*)',
@@ -2557,7 +2904,7 @@ class GitoliteEnvironmentMixin(Environment):
return m.group(1)
finally:
f.close()
- return super(GitoliteEnvironmentMixin, self).get_fromaddr()
+ return super(GitoliteEnvironmentMixin, self).get_fromaddr(change)
class IncrementalDateTime(object):
@@ -2570,8 +2917,9 @@ class IncrementalDateTime(object):
def __init__(self):
self.time = time.time()
+ self.next = self.__next__ # Python 2 backward compatibility
- def next(self):
+ def __next__(self):
formatted = formatdate(self.time, True)
self.time += 1
return formatted
@@ -2583,6 +2931,7 @@ class GitoliteEnvironment(
ComputeFQDNEnvironmentMixin,
ConfigFilterLinesEnvironmentMixin,
ConfigRecipientsEnvironmentMixin,
+ ConfigRefFilterEnvironmentMixin,
PusherDomainEnvironmentMixin,
ConfigOptionsEnvironmentMixin,
GitoliteEnvironmentMixin,
@@ -2591,6 +2940,117 @@ class GitoliteEnvironment(
pass
+class StashEnvironmentMixin(Environment):
+ def __init__(self, user=None, repo=None, **kw):
+ super(StashEnvironmentMixin, self).__init__(**kw)
+ self.__user = user
+ self.__repo = repo
+
+ def get_repo_shortname(self):
+ return self.__repo
+
+ def get_pusher(self):
+ return re.match('(.*?)\s*<', self.__user).group(1)
+
+ def get_pusher_email(self):
+ return self.__user
+
+ def get_fromaddr(self, change=None):
+ return self.__user
+
+
+class StashEnvironment(
+ StashEnvironmentMixin,
+ ProjectdescEnvironmentMixin,
+ ConfigMaxlinesEnvironmentMixin,
+ ComputeFQDNEnvironmentMixin,
+ ConfigFilterLinesEnvironmentMixin,
+ ConfigRecipientsEnvironmentMixin,
+ ConfigRefFilterEnvironmentMixin,
+ PusherDomainEnvironmentMixin,
+ ConfigOptionsEnvironmentMixin,
+ Environment,
+ ):
+ pass
+
+
+class GerritEnvironmentMixin(Environment):
+ def __init__(self, project=None, submitter=None, update_method=None, **kw):
+ super(GerritEnvironmentMixin, self).__init__(**kw)
+ self.__project = project
+ self.__submitter = submitter
+ self.__update_method = update_method
+ "Make an 'update_method' value available for templates."
+ self.COMPUTED_KEYS += ['update_method']
+
+ def get_repo_shortname(self):
+ return self.__project
+
+ def get_pusher(self):
+ if self.__submitter:
+ if self.__submitter.find('<') != -1:
+ # Submitter has a configured email, we transformed
+ # __submitter into an RFC 2822 string already.
+ return re.match('(.*?)\s*<', self.__submitter).group(1)
+ else:
+ # Submitter has no configured email, it's just his name.
+ return self.__submitter
+ else:
+ # If we arrive here, this means someone pushed "Submit" from
+ # the gerrit web UI for the CR (or used one of the programmatic
+ # APIs to do the same, such as gerrit review) and the
+ # merge/push was done by the Gerrit user. It was technically
+ # triggered by someone else, but sadly we have no way of
+ # determining who that someone else is at this point.
+ return 'Gerrit' # 'unknown user'?
+
+ def get_pusher_email(self):
+ if self.__submitter:
+ return self.__submitter
+ else:
+ return super(GerritEnvironmentMixin, self).get_pusher_email()
+
+ def get_fromaddr(self, change=None):
+ if self.__submitter and self.__submitter.find('<') != -1:
+ return self.__submitter
+ else:
+ return super(GerritEnvironmentMixin, self).get_fromaddr(change)
+
+ def get_default_ref_ignore_regex(self):
+ default = super(GerritEnvironmentMixin, self).get_default_ref_ignore_regex()
+ return default + '|^refs/changes/|^refs/cache-automerge/|^refs/meta/'
+
+ def get_revision_recipients(self, revision):
+ # Merge commits created by Gerrit when users hit "Submit this patchset"
+ # in the Web UI (or do equivalently with REST APIs or the gerrit review
+ # command) are not something users want to see an individual email for.
+ # Filter them out.
+ committer = read_git_output(['log', '--no-walk', '--format=%cN',
+ revision.rev.sha1])
+ if committer == 'Gerrit Code Review':
+ return []
+ else:
+ return super(GerritEnvironmentMixin, self).get_revision_recipients(revision)
+
+ def get_update_method(self):
+ return self.__update_method
+
+
+class GerritEnvironment(
+ GerritEnvironmentMixin,
+ ProjectdescEnvironmentMixin,
+ ConfigMaxlinesEnvironmentMixin,
+ ComputeFQDNEnvironmentMixin,
+ ConfigFilterLinesEnvironmentMixin,
+ ConfigRecipientsEnvironmentMixin,
+ ConfigRefFilterEnvironmentMixin,
+ PusherDomainEnvironmentMixin,
+ ConfigOptionsEnvironmentMixin,
+ Environment,
+ ):
+ pass
+
+
class Push(object):
"""Represent an entire push (i.e., a group of ReferenceChanges).
@@ -2673,10 +3133,11 @@ class Push(object):
])
)
- def __init__(self, changes, ignore_other_refs=False):
+ def __init__(self, environment, changes, ignore_other_refs=False):
self.changes = sorted(changes, key=self._sort_key)
self.__other_ref_sha1s = None
self.__cached_commits_spec = {}
+ self.environment = environment
if ignore_other_refs:
self.__other_ref_sha1s = set()
@@ -2703,10 +3164,14 @@ class Push(object):
'%(objectname) %(objecttype) %(refname)\n'
'%(*objectname) %(*objecttype) %(refname)'
)
+ ref_filter_regex, is_inclusion_filter = \
+ self.environment.get_ref_filter_regex()
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:
+ if (sha1 and type == 'commit' and
+ name not in updated_refs and
+ include_ref(name, ref_filter_regex, is_inclusion_filter)):
sha1s.add(sha1)
self.__other_ref_sha1s = sha1s
@@ -2856,7 +3321,7 @@ class Push(object):
if not change.environment.quiet:
change.environment.log_msg(
'Sending notification emails to: %s\n' % (change.recipients,))
- extra_values = {'send_date': send_date.next()}
+ extra_values = {'send_date': next(send_date)}
rev = change.send_single_combined_email(sha1s)
if rev:
@@ -2876,9 +3341,9 @@ class Push(object):
max_emails = change.environment.maxcommitemails
if max_emails and len(sha1s) > max_emails:
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
+ '*** 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
)
return
@@ -2889,7 +3354,7 @@ class Push(object):
rev.recipients = rev.cc_recipients
rev.cc_recipients = None
if rev.recipients:
- extra_values = {'send_date': send_date.next()}
+ extra_values = {'send_date': next(send_date)}
mailer.send(
rev.generate_email(self, body_filter, extra_values),
rev.recipients,
@@ -2904,18 +3369,33 @@ class Push(object):
)
+def include_ref(refname, ref_filter_regex, is_inclusion_filter):
+ does_match = bool(ref_filter_regex.search(refname))
+ if is_inclusion_filter:
+ return does_match
+ else: # exclusion filter -- we include the ref if the regex doesn't match
+ return not does_match
+
+
def run_as_post_receive_hook(environment, mailer):
+ ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(True)
changes = []
for line in sys.stdin:
(oldrev, newrev, refname) = line.strip().split(' ', 2)
+ if not include_ref(refname, ref_filter_regex, is_inclusion_filter):
+ continue
changes.append(
ReferenceChange.create(environment, oldrev, newrev, refname)
)
- push = Push(changes)
- push.send_emails(mailer, body_filter=environment.filter_body)
+ if changes:
+ push = Push(environment, changes)
+ push.send_emails(mailer, body_filter=environment.filter_body)
def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False):
+ ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(True)
+ if not include_ref(refname, ref_filter_regex, is_inclusion_filter):
+ return
changes = [
ReferenceChange.create(
environment,
@@ -2924,7 +3404,7 @@ def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=
refname,
),
]
- push = Push(changes, force_send)
+ push = Push(environment, changes, force_send)
push.send_emails(mailer, body_filter=environment.filter_body)
@@ -2953,8 +3433,8 @@ def choose_mailer(config, environment):
mailer = SendMailer(command=command, envelopesender=environment.get_sender())
else:
environment.log_error(
- 'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer
- + 'please use one of "smtp" or "sendmail".\n'
+ 'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer +
+ 'please use one of "smtp" or "sendmail".\n'
)
sys.exit(1)
return mailer
@@ -2963,14 +3443,18 @@ def choose_mailer(config, environment):
KNOWN_ENVIRONMENTS = {
'generic': GenericEnvironmentMixin,
'gitolite': GitoliteEnvironmentMixin,
+ 'stash': StashEnvironmentMixin,
+ 'gerrit': GerritEnvironmentMixin,
}
-def choose_environment(config, osenv=None, env=None, recipients=None):
+def choose_environment(config, osenv=None, env=None, recipients=None,
+ hook_info=None):
if not osenv:
osenv = os.environ
environment_mixins = [
+ ConfigRefFilterEnvironmentMixin,
ProjectdescEnvironmentMixin,
ConfigMaxlinesEnvironmentMixin,
ComputeFQDNEnvironmentMixin,
@@ -2992,7 +3476,15 @@ def choose_environment(config, osenv=None, env=None, recipients=None):
else:
env = 'generic'
- environment_mixins.append(KNOWN_ENVIRONMENTS[env])
+ environment_mixins.insert(0, KNOWN_ENVIRONMENTS[env])
+
+ if env == 'stash':
+ environment_kw['user'] = hook_info['stash_user']
+ environment_kw['repo'] = hook_info['stash_repo']
+ elif env == 'gerrit':
+ environment_kw['project'] = hook_info['project']
+ environment_kw['submitter'] = hook_info['submitter']
+ environment_kw['update_method'] = hook_info['update_method']
if recipients:
environment_mixins.insert(0, StaticRecipientsEnvironmentMixin)
@@ -3011,6 +3503,116 @@ def choose_environment(config, osenv=None, env=None, recipients=None):
return environment_klass(**environment_kw)
+def get_version():
+ oldcwd = os.getcwd()
+ try:
+ try:
+ os.chdir(os.path.dirname(os.path.realpath(__file__)))
+ git_version = read_git_output(['describe', '--tags', 'HEAD'])
+ if git_version == __version__:
+ return git_version
+ else:
+ return '%s (%s)' % (__version__, git_version)
+ except:
+ pass
+ finally:
+ os.chdir(oldcwd)
+ return __version__
+
+
+def compute_gerrit_options(options, args, required_gerrit_options):
+ if None in required_gerrit_options:
+ raise SystemExit("Error: Specify all of --oldrev, --newrev, --refname, "
+ "and --project; or none of them.")
+
+ if options.environment not in (None, 'gerrit'):
+ raise SystemExit("Non-gerrit environments incompatible with --oldrev, "
+ "--newrev, --refname, and --project")
+ options.environment = 'gerrit'
+
+ if args:
+ raise SystemExit("Error: Positional parameters not allowed with "
+ "--oldrev, --newrev, and --refname.")
+
+ # Gerrit oddly omits 'refs/heads/' in the refname when calling
+ # ref-updated hook; put it back.
+ git_dir = get_git_dir()
+ if (not os.path.exists(os.path.join(git_dir, options.refname)) and
+ os.path.exists(os.path.join(git_dir, 'refs', 'heads',
+ options.refname))):
+ options.refname = 'refs/heads/' + options.refname
+
+ # Convert each string option unicode for Python3.
+ if PYTHON3:
+ opts = ['environment', 'recipients', 'oldrev', 'newrev', 'refname',
+ 'project', 'submitter', 'stash-user', 'stash-repo']
+ for opt in opts:
+ if not hasattr(options, opt):
+ continue
+ obj = getattr(options, opt)
+ if obj:
+ enc = obj.encode('utf-8', 'surrogateescape')
+ dec = enc.decode('utf-8', 'replace')
+ setattr(options, opt, dec)
+
+ # New revisions can appear in a gerrit repository either due to someone
+ # pushing directly (in which case options.submitter will be set), or they
+ # can press "Submit this patchset" in the web UI for some CR (in which
+ # case options.submitter will not be set and gerrit will not have provided
+ # us the information about who pressed the button).
+ #
+ # Note for the nit-picky: I'm lumping in REST API calls and the ssh
+ # gerrit review command in with "Submit this patchset" button, since they
+ # have the same effect.
+ if options.submitter:
+ update_method = 'pushed'
+ # The submitter argument is almost an RFC 2822 email address; change it
+ # from 'User Name (email@domain)' to 'User Name <email@domain>' so it is
+ options.submitter = options.submitter.replace('(', '<').replace(')', '>')
+ else:
+ update_method = 'submitted'
+ # Gerrit knew who submitted this patchset, but threw that information
+ # away when it invoked this hook. However, *IF* Gerrit created a
+ # merge to bring the patchset in (project 'Submit Type' is either
+ # "Always Merge", or is "Merge if Necessary" and happens to be
+ # necessary for this particular CR), then it will have the committer
+ # of that merge be 'Gerrit Code Review' and the author will be the
+ # person who requested the submission of the CR. Since this is fairly
+ # likely for most gerrit installations (of a reasonable size), it's
+ # worth the extra effort to try to determine the actual submitter.
+ rev_info = read_git_lines(['log', '--no-walk', '--merges',
+ '--format=%cN%n%aN <%aE>', options.newrev])
+ if rev_info and rev_info[0] == 'Gerrit Code Review':
+ options.submitter = rev_info[1]
+
+ # We pass back refname, oldrev, newrev as args because then the
+ # gerrit ref-updated hook is much like the git update hook
+ return (options,
+ [options.refname, options.oldrev, options.newrev],
+ {'project': options.project, 'submitter': options.submitter,
+ 'update_method': update_method})
+
+
+def check_hook_specific_args(options, args):
+ # First check for stash arguments
+ if (options.stash_user is None) != (options.stash_repo is None):
+ raise SystemExit("Error: Specify both of --stash-user and "
+ "--stash-repo or neither.")
+ if options.stash_user:
+ options.environment = 'stash'
+ return options, args, {'stash_user': options.stash_user,
+ 'stash_repo': options.stash_repo}
+
+ # Finally, check for gerrit specific arguments
+ required_gerrit_options = (options.oldrev, options.newrev, options.refname,
+ options.project)
+ if required_gerrit_options != (None,) * 4:
+ return compute_gerrit_options(options, args, required_gerrit_options)
+
+ # No special options in use, just return what we started with
+ return options, args, {}
+
+
def main(args):
parser = optparse.OptionParser(
description=__doc__,
@@ -3019,7 +3621,7 @@ def main(args):
parser.add_option(
'--environment', '--env', action='store', type='choice',
- choices=['generic', 'gitolite'], default=None,
+ choices=list(KNOWN_ENVIRONMENTS.keys()), default=None,
help=(
'Choose type of environment is in use. Default is taken from '
'multimailhook.environment if set; otherwise "generic".'
@@ -3048,8 +3650,58 @@ def main(args):
'detection in this mode.'
),
)
+ parser.add_option(
+ '-c', metavar="<name>=<value>", action='append',
+ help=(
+ 'Pass a configuration parameter through to git. The value given '
+ 'will override values from configuration files. See the -c option '
+ 'of git(1) for more details. (Only works with git >= 1.7.3)'
+ ),
+ )
+ parser.add_option(
+ '--version', '-v', action='store_true', default=False,
+ help=(
+ "Display git-multimail's version"
+ ),
+ )
+ # The following options permit this script to be run as a gerrit
+ # ref-updated hook. See e.g.
+ # code.google.com/p/gerrit/source/browse/Documentation/config-hooks.txt
+ # We suppress help for these items, since these are specific to gerrit,
+ # and we don't want users directly using them any way other than how the
+ # gerrit ref-updated hook is called.
+ parser.add_option('--oldrev', action='store', help=optparse.SUPPRESS_HELP)
+ parser.add_option('--newrev', action='store', help=optparse.SUPPRESS_HELP)
+ parser.add_option('--refname', action='store', help=optparse.SUPPRESS_HELP)
+ parser.add_option('--project', action='store', help=optparse.SUPPRESS_HELP)
+ parser.add_option('--submitter', action='store', help=optparse.SUPPRESS_HELP)
+
+ # The following allow this to be run as a stash asynchronous post-receive
+ # hook (almost identical to a git post-receive hook but triggered also for
+ # merges of pull requests from the UI). We suppress help for these items,
+ # since these are specific to stash.
+ parser.add_option('--stash-user', action='store', help=optparse.SUPPRESS_HELP)
+ parser.add_option('--stash-repo', action='store', help=optparse.SUPPRESS_HELP)
(options, args) = parser.parse_args(args)
+ (options, args, hook_info) = check_hook_specific_args(options, args)
+
+ if options.version:
+ sys.stdout.write('git-multimail version ' + get_version() + '\n')
+ return
+
+ if options.c:
+ parameters = os.environ.get('GIT_CONFIG_PARAMETERS', '')
+ if parameters:
+ parameters += ' '
+ # git expects GIT_CONFIG_PARAMETERS to be of the form
+ # "'name1=value1' 'name2=value2' 'name3=value3'"
+ # including everything inside the double quotes (but not the double
+ # quotes themselves). Spacing is critical. Also, if a value contains
+ # a literal single quote that quote must be represented using the
+ # four character sequence: '\''
+ parameters += ' '.join("'" + x.replace("'", "'\\''") + "'" for x in options.c)
+ os.environ['GIT_CONFIG_PARAMETERS'] = parameters
config = Config('multimailhook')
@@ -3058,6 +3710,7 @@ def main(args):
config, osenv=os.environ,
env=options.environment,
recipients=options.recipients,
+ hook_info=hook_info,
)
if options.show_env:
@@ -3080,9 +3733,20 @@ def main(args):
run_as_update_hook(environment, mailer, refname, oldrev, newrev, options.force_send)
else:
run_as_post_receive_hook(environment, mailer)
- except ConfigurationException, e:
- sys.exit(str(e))
-
+ except ConfigurationException:
+ sys.exit(sys.exc_info()[1])
+ except Exception:
+ t, e, tb = sys.exc_info()
+ import traceback
+ sys.stdout.write('\n')
+ sys.stdout.write('Exception \'' + t.__name__ +
+ '\' raised. Please report this as a bug to\n')
+ sys.stdout.write('https://github.com/git-multimail/git-multimail/issues\n')
+ sys.stdout.write('with the information below:\n\n')
+ sys.stdout.write('git-multimail version ' + get_version() + '\n')
+ sys.stdout.write('Python version ' + sys.version + '\n')
+ traceback.print_exc(file=sys.stdout)
+ sys.exit(1)
if __name__ == '__main__':
main(sys.argv[1:])
diff --git a/contrib/hooks/multimail/migrate-mailhook-config b/contrib/hooks/multimail/migrate-mailhook-config
index d0e9b39201..992657bbdc 100755
--- a/contrib/hooks/multimail/migrate-mailhook-config
+++ b/contrib/hooks/multimail/migrate-mailhook-config
@@ -1,4 +1,4 @@
-#! /usr/bin/env python2
+#! /usr/bin/env python
"""Migrate a post-receive-email configuration to be usable with git_multimail.py.
diff --git a/contrib/hooks/multimail/post-receive.example b/contrib/hooks/multimail/post-receive.example
index 43f7b6b635..9975df7107 100755
--- a/contrib/hooks/multimail/post-receive.example
+++ b/contrib/hooks/multimail/post-receive.example
@@ -1,4 +1,4 @@
-#! /usr/bin/env python2
+#! /usr/bin/env python
"""Example post-receive hook based on git-multimail.
@@ -42,7 +42,6 @@ import os
import git_multimail
-
# It is possible to modify the output templates here; e.g.:
#git_multimail.FOOTER_TEMPLATE = """\
@@ -61,8 +60,9 @@ config = git_multimail.Config('multimailhook')
try:
environment = git_multimail.GenericEnvironment(config=config)
#environment = git_multimail.GitoliteEnvironment(config=config)
-except git_multimail.ConfigurationException, e:
- sys.exit(str(e))
+except git_multimail.ConfigurationException:
+ sys.stderr.write('*** %s\n' % sys.exc_info()[1])
+ sys.exit(1)
# Choose the method of sending emails based on the git config:
diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh
index 9f06571851..308b777b0a 100755
--- a/contrib/subtree/git-subtree.sh
+++ b/contrib/subtree/git-subtree.sh
@@ -648,7 +648,7 @@ cmd_split()
debug "Merging split branch into HEAD..."
latest_old=$(cache_get latest_old)
git merge -s ours \
- -m "$(rejoin_msg $dir $latest_old $latest_new)" \
+ -m "$(rejoin_msg "$dir" $latest_old $latest_new)" \
$latest_new >&2 || exit $?
fi
if [ -n "$branch" ]; then
@@ -735,7 +735,7 @@ cmd_push()
refspec=$2
echo "git push using: " $repository $refspec
localrev=$(git subtree split --prefix="$prefix") || die
- git push $repository $localrev:refs/heads/$refspec
+ git push "$repository" $localrev:refs/heads/$refspec
else
die "'$dir' must already exist. Try 'git subtree add'."
fi
diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh
index 90519823be..dfbe443dea 100755
--- a/contrib/subtree/t/t7900-subtree.sh
+++ b/contrib/subtree/t/t7900-subtree.sh
@@ -1,6 +1,7 @@
#!/bin/sh
#
# Copyright (c) 2012 Avery Pennaraum
+# Copyright (c) 2015 Alexey Shumkin
#
test_description='Basic porcelain support for subtrees
@@ -32,25 +33,6 @@ check_equal()
fi
}
-fixnl()
-{
- t=""
- while read x; do
- t="$t$x "
- done
- echo $t
-}
-
-multiline()
-{
- while read x; do
- set -- $x
- for d in "$@"; do
- echo "$d"
- done
- done
-}
-
undo()
{
git reset --hard HEAD~
@@ -62,11 +44,11 @@ last_commit_message()
}
test_expect_success 'init subproj' '
- test_create_repo subproj
+ test_create_repo "sub proj"
'
# To the subproject!
-cd subproj
+cd ./"sub proj"
test_expect_success 'add sub1' '
create sub1 &&
@@ -106,39 +88,39 @@ test_expect_success 'add main4' '
'
test_expect_success 'fetch subproj history' '
- git fetch ./subproj sub1 &&
+ git fetch ./"sub proj" sub1 &&
git branch sub1 FETCH_HEAD
'
test_expect_success 'no subtree exists in main tree' '
- test_must_fail git subtree merge --prefix=subdir sub1
+ test_must_fail git subtree merge --prefix="sub dir" sub1
'
test_expect_success 'no pull from non-existant subtree' '
- test_must_fail git subtree pull --prefix=subdir ./subproj sub1
+ test_must_fail git subtree pull --prefix="sub dir" ./"sub proj" sub1
'
test_expect_success 'check if --message works for add' '
- git subtree add --prefix=subdir --message="Added subproject" sub1 &&
+ git subtree add --prefix="sub dir" --message="Added subproject" sub1 &&
check_equal ''"$(last_commit_message)"'' "Added subproject" &&
undo
'
test_expect_success 'check if --message works as -m and --prefix as -P' '
- git subtree add -P subdir -m "Added subproject using git subtree" sub1 &&
+ git subtree add -P "sub dir" -m "Added subproject using git subtree" sub1 &&
check_equal ''"$(last_commit_message)"'' "Added subproject using git subtree" &&
undo
'
test_expect_success 'check if --message works with squash too' '
- git subtree add -P subdir -m "Added subproject with squash" --squash sub1 &&
+ git subtree add -P "sub dir" -m "Added subproject with squash" --squash sub1 &&
check_equal ''"$(last_commit_message)"'' "Added subproject with squash" &&
undo
'
test_expect_success 'add subproj to mainline' '
- git subtree add --prefix=subdir/ FETCH_HEAD &&
- check_equal ''"$(last_commit_message)"'' "Add '"'subdir/'"' from commit '"'"'''"$(git rev-parse sub1)"'''"'"'"
+ git subtree add --prefix="sub dir"/ FETCH_HEAD &&
+ check_equal ''"$(last_commit_message)"'' "Add '"'sub dir/'"' from commit '"'"'''"$(git rev-parse sub1)"'''"'"'"
'
# this shouldn't actually do anything, since FETCH_HEAD is already a parent
@@ -147,7 +129,7 @@ test_expect_success 'merge fetched subproj' '
'
test_expect_success 'add main-sub5' '
- create subdir/main-sub5 &&
+ create "sub dir/main-sub5" &&
git commit -m "main-sub5"
'
@@ -157,29 +139,29 @@ test_expect_success 'add main6' '
'
test_expect_success 'add main-sub7' '
- create subdir/main-sub7 &&
+ create "sub dir/main-sub7" &&
git commit -m "main-sub7"
'
test_expect_success 'fetch new subproj history' '
- git fetch ./subproj sub2 &&
+ git fetch ./"sub proj" sub2 &&
git branch sub2 FETCH_HEAD
'
test_expect_success 'check if --message works for merge' '
- git subtree merge --prefix=subdir -m "Merged changes from subproject" sub2 &&
+ git subtree merge --prefix="sub dir" -m "Merged changes from subproject" sub2 &&
check_equal ''"$(last_commit_message)"'' "Merged changes from subproject" &&
undo
'
test_expect_success 'check if --message for merge works with squash too' '
- git subtree merge --prefix subdir -m "Merged changes from subproject using squash" --squash sub2 &&
+ git subtree merge --prefix "sub dir" -m "Merged changes from subproject using squash" --squash sub2 &&
check_equal ''"$(last_commit_message)"'' "Merged changes from subproject using squash" &&
undo
'
test_expect_success 'merge new subproj history into subdir' '
- git subtree merge --prefix=subdir FETCH_HEAD &&
+ git subtree merge --prefix="sub dir" FETCH_HEAD &&
git branch pre-split &&
check_equal ''"$(last_commit_message)"'' "Merge commit '"'"'"$(git rev-parse sub2)"'"'"' into mainline" &&
undo
@@ -208,53 +190,53 @@ test_expect_success 'Check that the <prefix> exists for a split' '
'
test_expect_success 'check if --message works for split+rejoin' '
- spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' &&
+ spl1=''"$(git subtree split --annotate='"'*'"' --prefix "sub dir" --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' &&
git branch spl1 "$spl1" &&
check_equal ''"$(last_commit_message)"'' "Split & rejoin" &&
undo
'
test_expect_success 'check split with --branch' '
- spl1=$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin) &&
+ spl1=$(git subtree split --annotate='"'*'"' --prefix "sub dir" --onto FETCH_HEAD --message "Split & rejoin" --rejoin) &&
undo &&
- git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --branch splitbr1 &&
+ git subtree split --annotate='"'*'"' --prefix "sub dir" --onto FETCH_HEAD --branch splitbr1 &&
check_equal ''"$(git rev-parse splitbr1)"'' "$spl1"
'
test_expect_success 'check hash of split' '
- spl1=$(git subtree split --prefix subdir) &&
- git subtree split --prefix subdir --branch splitbr1test &&
+ spl1=$(git subtree split --prefix "sub dir") &&
+ git subtree split --prefix "sub dir" --branch splitbr1test &&
check_equal ''"$(git rev-parse splitbr1test)"'' "$spl1" &&
new_hash=$(git rev-parse splitbr1test~2) &&
check_equal ''"$new_hash"'' "$subdir_hash"
'
test_expect_success 'check split with --branch for an existing branch' '
- spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' &&
+ spl1=''"$(git subtree split --annotate='"'*'"' --prefix "sub dir" --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' &&
undo &&
git branch splitbr2 sub1 &&
- git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --branch splitbr2 &&
+ git subtree split --annotate='"'*'"' --prefix "sub dir" --onto FETCH_HEAD --branch splitbr2 &&
check_equal ''"$(git rev-parse splitbr2)"'' "$spl1"
'
test_expect_success 'check split with --branch for an incompatible branch' '
- test_must_fail git subtree split --prefix subdir --onto FETCH_HEAD --branch subdir
+ test_must_fail git subtree split --prefix "sub dir" --onto FETCH_HEAD --branch subdir
'
test_expect_success 'check split+rejoin' '
- spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' &&
+ spl1=''"$(git subtree split --annotate='"'*'"' --prefix "sub dir" --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' &&
undo &&
- git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --rejoin &&
- check_equal ''"$(last_commit_message)"'' "Split '"'"'subdir/'"'"' into commit '"'"'"$spl1"'"'"'"
+ git subtree split --annotate='"'*'"' --prefix "sub dir" --onto FETCH_HEAD --rejoin &&
+ check_equal ''"$(last_commit_message)"'' "Split '"'"'sub dir/'"'"' into commit '"'"'"$spl1"'"'"'"
'
test_expect_success 'add main-sub8' '
- create subdir/main-sub8 &&
+ create "sub dir/main-sub8" &&
git commit -m "main-sub8"
'
# To the subproject!
-cd ./subproj
+cd ./"sub proj"
test_expect_success 'merge split into subproj' '
git fetch .. spl1 &&
@@ -271,22 +253,22 @@ test_expect_success 'add sub9' '
cd ..
test_expect_success 'split for sub8' '
- split2=''"$(git subtree split --annotate='"'*'"' --prefix subdir/ --rejoin)"'' &&
+ split2=''"$(git subtree split --annotate='"'*'"' --prefix "sub dir/" --rejoin)"'' &&
git branch split2 "$split2"
'
test_expect_success 'add main-sub10' '
- create subdir/main-sub10 &&
+ create "sub dir/main-sub10" &&
git commit -m "main-sub10"
'
test_expect_success 'split for sub10' '
- spl3=''"$(git subtree split --annotate='"'*'"' --prefix subdir --rejoin)"'' &&
+ spl3=''"$(git subtree split --annotate='"'*'"' --prefix "sub dir" --rejoin)"'' &&
git branch spl3 "$spl3"
'
# To the subproject!
-cd ./subproj
+cd ./"sub proj"
test_expect_success 'merge split into subproj' '
git fetch .. spl3 &&
@@ -295,42 +277,64 @@ test_expect_success 'merge split into subproj' '
git branch subproj-merge-spl3
'
-chkm="main4 main6"
-chkms="main-sub10 main-sub5 main-sub7 main-sub8"
-chkms_sub=$(echo $chkms | multiline | sed 's,^,subdir/,' | fixnl)
-chks="sub1 sub2 sub3 sub9"
-chks_sub=$(echo $chks | multiline | sed 's,^,subdir/,' | fixnl)
+chkm="main4
+main6"
+chkms="main-sub10
+main-sub5
+main-sub7
+main-sub8"
+chkms_sub=$(cat <<TXT | sed 's,^,sub dir/,'
+$chkms
+TXT
+)
+chks="sub1
+sub2
+sub3
+sub9"
+chks_sub=$(cat <<TXT | sed 's,^,sub dir/,'
+$chks
+TXT
+)
test_expect_success 'make sure exactly the right set of files ends up in the subproj' '
- subfiles=''"$(git ls-files | fixnl)"'' &&
- check_equal "$subfiles" "$chkms $chks"
+ subfiles="$(git ls-files)" &&
+ check_equal "$subfiles" "$chkms
+$chks"
'
-
test_expect_success 'make sure the subproj history *only* contains commits that affect the subdir' '
- allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | fixnl)"'' &&
- check_equal "$allchanges" "$chkms $chks"
+ allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | sed "/^$/d")"'' &&
+ check_equal "$allchanges" "$chkms
+$chks"
'
# Back to mainline
cd ..
test_expect_success 'pull from subproj' '
- git fetch ./subproj subproj-merge-spl3 &&
+ git fetch ./"sub proj" subproj-merge-spl3 &&
git branch subproj-merge-spl3 FETCH_HEAD &&
- git subtree pull --prefix=subdir ./subproj subproj-merge-spl3
+ git subtree pull --prefix="sub dir" ./"sub proj" subproj-merge-spl3
'
test_expect_success 'make sure exactly the right set of files ends up in the mainline' '
- mainfiles=''"$(git ls-files | fixnl)"'' &&
- check_equal "$mainfiles" "$chkm $chkms_sub $chks_sub"
+ mainfiles=$(git ls-files) &&
+ check_equal "$mainfiles" "$chkm
+$chkms_sub
+$chks_sub"
'
test_expect_success 'make sure each filename changed exactly once in the entire history' '
# main-sub?? and /subdir/main-sub?? both change, because those are the
# changes that were split into their own history. And subdir/sub?? never
# change, since they were *only* changed in the subtree branch.
- allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | fixnl)"'' &&
- check_equal "$allchanges" ''"$(echo $chkms $chkm $chks $chkms_sub | multiline | sort | fixnl)"''
+ allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | sed "/^$/d")"'' &&
+ check_equal "$allchanges" ''"$(cat <<TXT | sort
+$chkms
+$chkm
+$chks
+$chkms_sub
+TXT
+)"''
'
test_expect_success 'make sure the --rejoin commits never make it into subproj' '
@@ -377,7 +381,7 @@ cd ../main
test_expect_success 'add sub as subdir in main' '
git fetch ../sub master &&
git branch sub2 FETCH_HEAD &&
- git subtree add --prefix subdir sub2
+ git subtree add --prefix "sub dir" sub2
'
cd ../sub
@@ -392,16 +396,16 @@ cd ../main
test_expect_success 'merge from sub' '
git fetch ../sub master &&
git branch sub3 FETCH_HEAD &&
- git subtree merge --prefix subdir sub3
+ git subtree merge --prefix "sub dir" sub3
'
test_expect_success 'add main-sub4' '
- create subdir/main-sub4 &&
+ create "sub dir/main-sub4" &&
git commit -m "main-sub4"
'
test_expect_success 'split for main-sub4 without --onto' '
- git subtree split --prefix subdir --branch mainsub4
+ git subtree split --prefix "sub dir" --branch mainsub4
'
# at this point, the new commit parent should be sub3 if it is not,
@@ -468,4 +472,50 @@ test_expect_success 'verify one file change per commit' '
))
'
+# test push
+
+cd ../..
+
+mkdir test-push
+
+cd test-push
+
+test_expect_success 'init main' '
+ test_create_repo main
+'
+
+test_expect_success 'init sub' '
+ test_create_repo "sub project"
+'
+
+cd ./"sub project"
+
+test_expect_success 'add subproject' '
+ create "sub project" &&
+ git commit -m "Sub project: 1" &&
+ git branch sub-branch-1
+'
+
+cd ../main
+
+test_expect_success 'make first commit and add subproject' '
+ create "main-1" &&
+ git commit -m "main: 1" &&
+ git subtree add "../sub project" --prefix "sub dir" --message "Added subproject" sub-branch-1 &&
+ check_equal "$(last_commit_message)" "Added subproject"
+'
+
+test_expect_success 'make second commit to a subproject file and push it into a sub project' '
+ create "sub dir/sub1" &&
+ git commit -m "Sub project: 2" &&
+ git subtree push "../sub project" --prefix "sub dir" sub-branch-1
+'
+
+cd ../"sub project"
+
+test_expect_success 'Test second commit is pushed' '
+ git checkout sub-branch-1 &&
+ check_equal "$(last_commit_message)" "Sub project: 2"
+'
+
test_done
diff --git a/date.c b/date.c
index 8f9156909b..7c9f76998a 100644
--- a/date.c
+++ b/date.c
@@ -166,6 +166,7 @@ struct date_mode *date_mode_from_type(enum date_mode_type type)
if (type == DATE_STRFTIME)
die("BUG: cannot create anonymous strftime date_mode struct");
mode.type = type;
+ mode.local = 0;
return &mode;
}
@@ -174,6 +175,9 @@ const char *show_date(unsigned long time, int tz, const struct date_mode *mode)
struct tm *tm;
static struct strbuf timebuf = STRBUF_INIT;
+ if (mode->local)
+ tz = local_tzoffset(time);
+
if (mode->type == DATE_RAW) {
strbuf_reset(&timebuf);
strbuf_addf(&timebuf, "%lu %+05d", time, tz);
@@ -189,9 +193,6 @@ const char *show_date(unsigned long time, int tz, const struct date_mode *mode)
return timebuf.buf;
}
- if (mode->type == DATE_LOCAL)
- tz = local_tzoffset(time);
-
tm = time_to_tm(time, tz);
if (!tm) {
tm = time_to_tm(0, 0);
@@ -232,7 +233,7 @@ const char *show_date(unsigned long time, int tz, const struct date_mode *mode)
tm->tm_mday,
tm->tm_hour, tm->tm_min, tm->tm_sec,
tm->tm_year + 1900,
- (mode->type == DATE_LOCAL) ? 0 : ' ',
+ mode->local ? 0 : ' ',
tz);
return timebuf.buf;
}
@@ -770,31 +771,50 @@ int parse_date(const char *date, struct strbuf *result)
return 0;
}
+static enum date_mode_type parse_date_type(const char *format, const char **end)
+{
+ if (skip_prefix(format, "relative", end))
+ return DATE_RELATIVE;
+ if (skip_prefix(format, "iso8601-strict", end) ||
+ skip_prefix(format, "iso-strict", end))
+ return DATE_ISO8601_STRICT;
+ if (skip_prefix(format, "iso8601", end) ||
+ skip_prefix(format, "iso", end))
+ return DATE_ISO8601;
+ if (skip_prefix(format, "rfc2822", end) ||
+ skip_prefix(format, "rfc", end))
+ return DATE_RFC2822;
+ if (skip_prefix(format, "short", end))
+ return DATE_SHORT;
+ if (skip_prefix(format, "default", end))
+ return DATE_NORMAL;
+ if (skip_prefix(format, "raw", end))
+ return DATE_RAW;
+ if (skip_prefix(format, "format", end))
+ return DATE_STRFTIME;
+
+ die("unknown date format %s", format);
+}
+
void parse_date_format(const char *format, struct date_mode *mode)
{
- if (!strcmp(format, "relative"))
- mode->type = DATE_RELATIVE;
- else if (!strcmp(format, "iso8601") ||
- !strcmp(format, "iso"))
- mode->type = DATE_ISO8601;
- else if (!strcmp(format, "iso8601-strict") ||
- !strcmp(format, "iso-strict"))
- mode->type = DATE_ISO8601_STRICT;
- else if (!strcmp(format, "rfc2822") ||
- !strcmp(format, "rfc"))
- mode->type = DATE_RFC2822;
- else if (!strcmp(format, "short"))
- mode->type = DATE_SHORT;
- else if (!strcmp(format, "local"))
- mode->type = DATE_LOCAL;
- else if (!strcmp(format, "default"))
- mode->type = DATE_NORMAL;
- else if (!strcmp(format, "raw"))
- mode->type = DATE_RAW;
- else if (skip_prefix(format, "format:", &format)) {
- mode->type = DATE_STRFTIME;
- mode->strftime_fmt = xstrdup(format);
- } else
+ const char *p;
+
+ /* historical alias */
+ if (!strcmp(format, "local"))
+ format = "default-local";
+
+ mode->type = parse_date_type(format, &p);
+ mode->local = 0;
+
+ if (skip_prefix(p, "-local", &p))
+ mode->local = 1;
+
+ if (mode->type == DATE_STRFTIME) {
+ if (!skip_prefix(p, ":", &p))
+ die("date format missing colon separator: %s", format);
+ mode->strftime_fmt = xstrdup(p);
+ } else if (*p)
die("unknown date format %s", format);
}
diff --git a/dir.c b/dir.c
index 7b25634832..fba938b701 100644
--- a/dir.c
+++ b/dir.c
@@ -882,6 +882,25 @@ int match_pathname(const char *pathname, int pathlen,
*/
if (!patternlen && !namelen)
return 1;
+ /*
+ * This can happen when we ignore some exclude rules
+ * on directories in other to see if negative rules
+ * may match. E.g.
+ *
+ * /abc
+ * !/abc/def/ghi
+ *
+ * The pattern of interest is "/abc". On the first
+ * try, we should match path "abc" with this pattern
+ * in the "if" statement right above, but the caller
+ * ignores it.
+ *
+ * On the second try with paths within "abc",
+ * e.g. "abc/xyz", we come here and try to match it
+ * with "/abc".
+ */
+ if (!patternlen && namelen && *name == '/')
+ return 1;
}
return fnmatch_icase_mem(pattern, patternlen,
@@ -890,6 +909,48 @@ int match_pathname(const char *pathname, int pathlen,
}
/*
+ * Return non-zero if pathname is a directory and an ancestor of the
+ * literal path in a (negative) pattern. This is used to keep
+ * descending in "foo" and "foo/bar" when the pattern is
+ * "!foo/bar/.gitignore". "foo/notbar" will not be descended however.
+ */
+static int match_neg_path(const char *pathname, int pathlen, int *dtype,
+ const char *base, int baselen,
+ const char *pattern, int prefix, int patternlen,
+ int flags)
+{
+ assert((flags & EXC_FLAG_NEGATIVE) && !(flags & EXC_FLAG_NODIR));
+
+ if (*dtype == DT_UNKNOWN)
+ *dtype = get_dtype(NULL, pathname, pathlen);
+ if (*dtype != DT_DIR)
+ return 0;
+
+ if (*pattern == '/') {
+ pattern++;
+ patternlen--;
+ prefix--;
+ }
+
+ if (baselen) {
+ if (((pathlen < baselen && base[pathlen] == '/') ||
+ pathlen == baselen) &&
+ !strncmp_icase(pathname, base, pathlen))
+ return 1;
+ pathname += baselen + 1;
+ pathlen -= baselen + 1;
+ }
+
+
+ if (prefix &&
+ ((pathlen < prefix && pattern[pathlen] == '/') &&
+ !strncmp_icase(pathname, pattern, pathlen)))
+ return 1;
+
+ return 0;
+}
+
+/*
* Scan the given exclude list in reverse to see whether pathname
* should be ignored. The first match (i.e. the last on the list), if
* any, determines the fate. Returns the exclude_list element which
@@ -901,7 +962,8 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
int *dtype,
struct exclude_list *el)
{
- int i;
+ struct exclude *exc = NULL; /* undecided */
+ int i, matched_negative_path = 0;
if (!el->nr)
return NULL; /* undefined */
@@ -922,18 +984,33 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
if (match_basename(basename,
pathlen - (basename - pathname),
exclude, prefix, x->patternlen,
- x->flags))
- return x;
+ x->flags)) {
+ exc = x;
+ break;
+ }
continue;
}
assert(x->baselen == 0 || x->base[x->baselen - 1] == '/');
if (match_pathname(pathname, pathlen,
x->base, x->baselen ? x->baselen - 1 : 0,
+ exclude, prefix, x->patternlen, x->flags)) {
+ exc = x;
+ break;
+ }
+
+ if ((x->flags & EXC_FLAG_NEGATIVE) && !matched_negative_path &&
+ match_neg_path(pathname, pathlen, dtype, x->base,
+ x->baselen ? x->baselen - 1 : 0,
exclude, prefix, x->patternlen, x->flags))
- return x;
+ matched_negative_path = 1;
}
- return NULL; /* undecided */
+ if (exc &&
+ !(exc->flags & EXC_FLAG_NEGATIVE) &&
+ !(exc->flags & EXC_FLAG_NODIR) &&
+ matched_negative_path)
+ exc = NULL;
+ return exc;
}
/*
@@ -2030,6 +2107,15 @@ int file_exists(const char *f)
return lstat(f, &sb) == 0;
}
+static int cmp_icase(char a, char b)
+{
+ if (a == b)
+ return 0;
+ if (ignore_case)
+ return toupper(a) - toupper(b);
+ return a - b;
+}
+
/*
* Given two normalized paths (a trailing slash is ok), if subdir is
* outside dir, return -1. Otherwise return the offset in subdir that
@@ -2041,7 +2127,7 @@ int dir_inside_of(const char *subdir, const char *dir)
assert(dir && subdir && *dir && *subdir);
- while (*dir && *subdir && *dir == *subdir) {
+ while (*dir && *subdir && !cmp_icase(*dir, *subdir)) {
dir++;
subdir++;
offset++;
diff --git a/fast-import.c b/fast-import.c
index 6c7c3c9b66..adcbfc67dc 100644
--- a/fast-import.c
+++ b/fast-import.c
@@ -424,7 +424,7 @@ static void write_crash_report(const char *err)
fprintf(rpt, "fast-import crash report:\n");
fprintf(rpt, " fast-import process: %"PRIuMAX"\n", (uintmax_t) getpid());
fprintf(rpt, " parent process : %"PRIuMAX"\n", (uintmax_t) getppid());
- fprintf(rpt, " at %s\n", show_date(time(NULL), 0, DATE_MODE(LOCAL)));
+ fprintf(rpt, " at %s\n", show_date(time(NULL), 0, DATE_MODE(ISO8601)));
fputc('\n', rpt);
fputs("fatal: ", rpt);
diff --git a/git-bisect.sh b/git-bisect.sh
index ea63223ab3..5d1cb00d86 100755
--- a/git-bisect.sh
+++ b/git-bisect.sh
@@ -1,14 +1,19 @@
#!/bin/sh
-USAGE='[help|start|bad|good|skip|next|reset|visualize|replay|log|run]'
+USAGE='[help|start|bad|good|new|old|terms|skip|next|reset|visualize|replay|log|run]'
LONG_USAGE='git bisect help
print this long help message.
-git bisect start [--no-checkout] [<bad> [<good>...]] [--] [<pathspec>...]
+git bisect start [--term-{old,good}=<term> --term-{new,bad}=<term>]
+ [--no-checkout] [<bad> [<good>...]] [--] [<pathspec>...]
reset bisect state and start bisection.
-git bisect bad [<rev>]
- mark <rev> a known-bad revision.
-git bisect good [<rev>...]
- mark <rev>... known-good revisions.
+git bisect (bad|new) [<rev>]
+ mark <rev> a known-bad revision/
+ a revision after change in a given property.
+git bisect (good|old) [<rev>...]
+ mark <rev>... known-good revisions/
+ revisions before change in a given property.
+git bisect terms [--term-good | --term-bad]
+ show the terms used for old and new commits (default: bad, good)
git bisect skip [(<rev>|<range>)...]
mark <rev>... untestable revisions.
git bisect next
@@ -95,6 +100,24 @@ bisect_start() {
--no-checkout)
mode=--no-checkout
shift ;;
+ --term-good|--term-old)
+ shift
+ must_write_terms=1
+ TERM_GOOD=$1
+ shift ;;
+ --term-good=*|--term-old=*)
+ must_write_terms=1
+ TERM_GOOD=${1#*=}
+ shift ;;
+ --term-bad|--term-new)
+ shift
+ must_write_terms=1
+ TERM_BAD=$1
+ shift ;;
+ --term-bad=*|--term-new=*)
+ must_write_terms=1
+ TERM_BAD=${1#*=}
+ shift ;;
--*)
die "$(eval_gettext "unrecognised option: '\$arg'")" ;;
*)
@@ -294,7 +317,7 @@ bisect_next_check() {
false
;;
t,,"$TERM_GOOD")
- # have bad but not good. we could bisect although
+ # have bad (or new) but not good (or old). we could bisect although
# this is less optimum.
eval_gettextln "Warning: bisecting only with a \$TERM_BAD commit." >&2
if test -t 0
@@ -451,6 +474,8 @@ bisect_replay () {
eval "$cmd" ;;
"$TERM_GOOD"|"$TERM_BAD"|skip)
bisect_write "$command" "$rev" ;;
+ terms)
+ bisect_terms $rev ;;
*)
die "$(gettext "?? what are you talking about?")" ;;
esac
@@ -535,9 +560,42 @@ get_terms () {
write_terms () {
TERM_BAD=$1
TERM_GOOD=$2
+ if test "$TERM_BAD" = "$TERM_GOOD"
+ then
+ die "$(gettext "please use two different terms")"
+ fi
+ check_term_format "$TERM_BAD" bad
+ check_term_format "$TERM_GOOD" good
printf '%s\n%s\n' "$TERM_BAD" "$TERM_GOOD" >"$GIT_DIR/BISECT_TERMS"
}
+check_term_format () {
+ term=$1
+ git check-ref-format refs/bisect/"$term" ||
+ die "$(eval_gettext "'\$term' is not a valid term")"
+ case "$term" in
+ help|start|terms|skip|next|reset|visualize|replay|log|run)
+ die "$(eval_gettext "can't use the builtin command '\$term' as a term")"
+ ;;
+ bad|new)
+ if test "$2" != bad
+ then
+ # In theory, nothing prevents swapping
+ # completely good and bad, but this situation
+ # could be confusing and hasn't been tested
+ # enough. Forbid it for now.
+ die "$(eval_gettext "can't change the meaning of term '\$term'")"
+ fi
+ ;;
+ good|old)
+ if test "$2" != good
+ then
+ die "$(eval_gettext "can't change the meaning of term '\$term'")"
+ fi
+ ;;
+ esac
+}
+
check_and_set_terms () {
cmd="$1"
case "$cmd" in
@@ -554,14 +612,51 @@ check_and_set_terms () {
write_terms bad good
fi
;;
+ new|old)
+ if ! test -s "$GIT_DIR/BISECT_TERMS"
+ then
+ write_terms new old
+ fi
+ ;;
esac ;;
esac
}
bisect_voc () {
case "$1" in
- bad) echo "bad" ;;
- good) echo "good" ;;
+ bad) echo "bad|new" ;;
+ good) echo "good|old" ;;
+ esac
+}
+
+bisect_terms () {
+ get_terms
+ if ! test -s "$GIT_DIR/BISECT_TERMS"
+ then
+ die "$(gettext "no terms defined")"
+ fi
+ case "$#" in
+ 0)
+ gettextln "Your current terms are $TERM_GOOD for the old state
+and $TERM_BAD for the new state."
+ ;;
+ 1)
+ arg=$1
+ case "$arg" in
+ --term-good|--term-old)
+ printf '%s\n' "$TERM_GOOD"
+ ;;
+ --term-bad|--term-new)
+ printf '%s\n' "$TERM_BAD"
+ ;;
+ *)
+ die "$(eval_gettext "invalid argument \$arg for 'git bisect terms'.
+Supported options are: --term-good|--term-old and --term-bad|--term-new.")"
+ ;;
+ esac
+ ;;
+ *)
+ usage ;;
esac
}
@@ -577,7 +672,7 @@ case "$#" in
git bisect -h ;;
start)
bisect_start "$@" ;;
- bad|good|"$TERM_BAD"|"$TERM_GOOD")
+ bad|good|new|old|"$TERM_BAD"|"$TERM_GOOD")
bisect_state "$cmd" "$@" ;;
skip)
bisect_skip "$@" ;;
@@ -594,6 +689,8 @@ case "$#" in
bisect_log ;;
run)
bisect_run "$@" ;;
+ terms)
+ bisect_terms "$@" ;;
*)
usage ;;
esac
diff --git a/git-compat-util.h b/git-compat-util.h
index f649e81f11..1df82fa598 100644
--- a/git-compat-util.h
+++ b/git-compat-util.h
@@ -814,6 +814,9 @@ static inline int strtoul_ui(char const *s, int base, unsigned int *result)
char *p;
errno = 0;
+ /* negative values would be accepted by strtoul */
+ if (strchr(s, '-'))
+ return -1;
ul = strtoul(s, &p, base);
if (errno || *p || p == s || (unsigned int) ul != ul)
return -1;
diff --git a/git-filter-branch.sh b/git-filter-branch.sh
index 5b3f63d8bb..27c9c54fbd 100755
--- a/git-filter-branch.sh
+++ b/git-filter-branch.sh
@@ -275,11 +275,41 @@ commits=$(wc -l <../revs | tr -d " ")
test $commits -eq 0 && die "Found nothing to rewrite"
# Rewrite the commits
+report_progress ()
+{
+ if test -n "$progress" &&
+ test $git_filter_branch__commit_count -gt $next_sample_at
+ then
+ count=$git_filter_branch__commit_count
+
+ now=$(date +%s)
+ elapsed=$(($now - $start_timestamp))
+ remaining=$(( ($commits - $count) * $elapsed / $count ))
+ if test $elapsed -gt 0
+ then
+ next_sample_at=$(( ($elapsed + 1) * $count / $elapsed ))
+ else
+ next_sample_at=$(($next_sample_at + 1))
+ fi
+ progress=" ($elapsed seconds passed, remaining $remaining predicted)"
+ fi
+ printf "\rRewrite $commit ($count/$commits)$progress "
+}
git_filter_branch__commit_count=0
+
+progress= start_timestamp=
+if date '+%s' 2>/dev/null | grep -q '^[0-9][0-9]*$'
+then
+ next_sample_at=0
+ progress="dummy to ensure this is not empty"
+ start_timestamp=$(date '+%s')
+fi
+
while read commit parents; do
git_filter_branch__commit_count=$(($git_filter_branch__commit_count+1))
- printf "\rRewrite $commit ($git_filter_branch__commit_count/$commits)"
+
+ report_progress
case "$filter_subdir" in
"")
@@ -347,7 +377,7 @@ while read commit parents; do
fi
{
- while read -r header_line && test -n "$header_line"
+ while IFS='' read -r header_line && test -n "$header_line"
do
# skip header lines...
:;
diff --git a/git-p4.py b/git-p4.py
index 0093fa3d83..daa60c60d6 100755
--- a/git-p4.py
+++ b/git-p4.py
@@ -22,6 +22,8 @@ import platform
import re
import shutil
import stat
+import zipfile
+import zlib
try:
from subprocess import CalledProcessError
@@ -104,6 +106,16 @@ def chdir(path, is_client_path=False):
path = os.getcwd()
os.environ['PWD'] = path
+def calcDiskFree():
+ """Return free space in bytes on the disk of the given dirname."""
+ if platform.system() == 'Windows':
+ free_bytes = ctypes.c_ulonglong(0)
+ ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes))
+ return free_bytes.value
+ else:
+ st = os.statvfs(os.getcwd())
+ return st.f_bavail * st.f_frsize
+
def die(msg):
if verbose:
raise Exception(msg)
@@ -134,13 +146,11 @@ def read_pipe(c, ignore_error=False):
sys.stderr.write('Reading pipe: %s\n' % str(c))
expand = isinstance(c,basestring)
- p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
- pipe = p.stdout
- val = pipe.read()
- if p.wait() and not ignore_error:
- die('Command failed: %s' % str(c))
-
- return val
+ p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand)
+ (out, err) = p.communicate()
+ if p.returncode != 0 and not ignore_error:
+ die('Command failed: %s\nError: %s' % (str(c), err))
+ return out
def p4_read_pipe(c, ignore_error=False):
real_cmd = p4_build_cmd(c)
@@ -604,9 +614,12 @@ def gitBranchExists(branch):
_gitConfig = {}
-def gitConfig(key):
+def gitConfig(key, typeSpecifier=None):
if not _gitConfig.has_key(key):
- cmd = [ "git", "config", key ]
+ cmd = [ "git", "config" ]
+ if typeSpecifier:
+ cmd += [ typeSpecifier ]
+ cmd += [ key ]
s = read_pipe(cmd, ignore_error=True)
_gitConfig[key] = s.strip()
return _gitConfig[key]
@@ -617,16 +630,26 @@ def gitConfigBool(key):
in the config."""
if not _gitConfig.has_key(key):
- cmd = [ "git", "config", "--bool", key ]
+ _gitConfig[key] = gitConfig(key, '--bool') == "true"
+ return _gitConfig[key]
+
+def gitConfigInt(key):
+ if not _gitConfig.has_key(key):
+ cmd = [ "git", "config", "--int", key ]
s = read_pipe(cmd, ignore_error=True)
v = s.strip()
- _gitConfig[key] = v == "true"
+ try:
+ _gitConfig[key] = int(gitConfig(key, '--int'))
+ except ValueError:
+ _gitConfig[key] = None
return _gitConfig[key]
def gitConfigList(key):
if not _gitConfig.has_key(key):
s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
_gitConfig[key] = s.strip().split(os.linesep)
+ if _gitConfig[key] == ['']:
+ _gitConfig[key] = []
return _gitConfig[key]
def p4BranchesInGit(branchesAreInRemotes=True):
@@ -909,6 +932,182 @@ def wildcard_present(path):
m = re.search("[*#@%]", path)
return m is not None
+class LargeFileSystem(object):
+ """Base class for large file system support."""
+
+ def __init__(self, writeToGitStream):
+ self.largeFiles = set()
+ self.writeToGitStream = writeToGitStream
+
+ def generatePointer(self, cloneDestination, contentFile):
+ """Return the content of a pointer file that is stored in Git instead of
+ the actual content."""
+ assert False, "Method 'generatePointer' required in " + self.__class__.__name__
+
+ def pushFile(self, localLargeFile):
+ """Push the actual content which is not stored in the Git repository to
+ a server."""
+ assert False, "Method 'pushFile' required in " + self.__class__.__name__
+
+ def hasLargeFileExtension(self, relPath):
+ return reduce(
+ lambda a, b: a or b,
+ [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
+ False
+ )
+
+ def generateTempFile(self, contents):
+ contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
+ for d in contents:
+ contentFile.write(d)
+ contentFile.close()
+ return contentFile.name
+
+ def exceedsLargeFileThreshold(self, relPath, contents):
+ if gitConfigInt('git-p4.largeFileThreshold'):
+ contentsSize = sum(len(d) for d in contents)
+ if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
+ return True
+ if gitConfigInt('git-p4.largeFileCompressedThreshold'):
+ contentsSize = sum(len(d) for d in contents)
+ if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
+ return False
+ contentTempFile = self.generateTempFile(contents)
+ compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
+ zf = zipfile.ZipFile(compressedContentFile.name, mode='w')
+ zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
+ zf.close()
+ compressedContentsSize = zf.infolist()[0].compress_size
+ os.remove(contentTempFile)
+ os.remove(compressedContentFile.name)
+ if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
+ return True
+ return False
+
+ def addLargeFile(self, relPath):
+ self.largeFiles.add(relPath)
+
+ def removeLargeFile(self, relPath):
+ self.largeFiles.remove(relPath)
+
+ def isLargeFile(self, relPath):
+ return relPath in self.largeFiles
+
+ def processContent(self, git_mode, relPath, contents):
+ """Processes the content of git fast import. This method decides if a
+ file is stored in the large file system and handles all necessary
+ steps."""
+ if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
+ contentTempFile = self.generateTempFile(contents)
+ (git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)
+
+ # Move temp file to final location in large file system
+ largeFileDir = os.path.dirname(localLargeFile)
+ if not os.path.isdir(largeFileDir):
+ os.makedirs(largeFileDir)
+ shutil.move(contentTempFile, localLargeFile)
+ self.addLargeFile(relPath)
+ if gitConfigBool('git-p4.largeFilePush'):
+ self.pushFile(localLargeFile)
+ if verbose:
+ sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
+ return (git_mode, contents)
+
+class MockLFS(LargeFileSystem):
+ """Mock large file system for testing."""
+
+ def generatePointer(self, contentFile):
+ """The pointer content is the original content prefixed with "pointer-".
+ The local filename of the large file storage is derived from the file content.
+ """
+ with open(contentFile, 'r') as f:
+ content = next(f)
+ gitMode = '100644'
+ pointerContents = 'pointer-' + content
+ localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
+ return (gitMode, pointerContents, localLargeFile)
+
+ def pushFile(self, localLargeFile):
+ """The remote filename of the large file storage is the same as the local
+ one but in a different directory.
+ """
+ remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
+ if not os.path.exists(remotePath):
+ os.makedirs(remotePath)
+ shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
+
+class GitLFS(LargeFileSystem):
+ """Git LFS as backend for the git-p4 large file system.
+ See https://git-lfs.github.com/ for details."""
+
+ def __init__(self, *args):
+ LargeFileSystem.__init__(self, *args)
+ self.baseGitAttributes = []
+
+ def generatePointer(self, contentFile):
+ """Generate a Git LFS pointer for the content. Return LFS Pointer file
+ mode and content which is stored in the Git repository instead of
+ the actual content. Return also the new location of the actual
+ content.
+ """
+ pointerProcess = subprocess.Popen(
+ ['git', 'lfs', 'pointer', '--file=' + contentFile],
+ stdout=subprocess.PIPE
+ )
+ pointerFile = pointerProcess.stdout.read()
+ if pointerProcess.wait():
+ os.remove(contentFile)
+ die('git-lfs pointer command failed. Did you install the extension?')
+ pointerContents = [i+'\n' for i in pointerFile.split('\n')[2:][:-1]]
+ oid = pointerContents[1].split(' ')[1].split(':')[1][:-1]
+ localLargeFile = os.path.join(
+ os.getcwd(),
+ '.git', 'lfs', 'objects', oid[:2], oid[2:4],
+ oid,
+ )
+ # LFS Spec states that pointer files should not have the executable bit set.
+ gitMode = '100644'
+ return (gitMode, pointerContents, localLargeFile)
+
+ def pushFile(self, localLargeFile):
+ uploadProcess = subprocess.Popen(
+ ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
+ )
+ if uploadProcess.wait():
+ die('git-lfs push command failed. Did you define a remote?')
+
+ def generateGitAttributes(self):
+ return (
+ self.baseGitAttributes +
+ [
+ '\n',
+ '#\n',
+ '# Git LFS (see https://git-lfs.github.com/)\n',
+ '#\n',
+ ] +
+ ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs -text\n'
+ for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
+ ] +
+ ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs -text\n'
+ for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
+ ]
+ )
+
+ def addLargeFile(self, relPath):
+ LargeFileSystem.addLargeFile(self, relPath)
+ self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
+
+ def removeLargeFile(self, relPath):
+ LargeFileSystem.removeLargeFile(self, relPath)
+ self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
+
+ def processContent(self, git_mode, relPath, contents):
+ if relPath == '.gitattributes':
+ self.baseGitAttributes = contents
+ return (git_mode, self.generateGitAttributes())
+ else:
+ return LargeFileSystem.processContent(self, git_mode, relPath, contents)
+
class Command:
def __init__(self):
self.usage = "usage: %prog [options]"
@@ -1082,6 +1281,9 @@ class P4Submit(Command, P4UserMap):
self.p4HasMoveCommand = p4_has_move_command()
self.branch = None
+ if gitConfig('git-p4.largeFileSystem'):
+ die("Large file system not supported for git-p4 submit command. Please remove it from config.")
+
def check(self):
if len(p4CmdList("opened ...")) > 0:
die("You have files opened with perforce! Close them before starting the sync.")
@@ -2032,6 +2234,13 @@ class P4Sync(Command, P4UserMap):
self.clientSpecDirs = None
self.tempBranches = []
self.tempBranchLocation = "git-p4-tmp"
+ self.largeFileSystem = None
+
+ if gitConfig('git-p4.largeFileSystem'):
+ largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
+ self.largeFileSystem = largeFileSystemConstructor(
+ lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
+ )
if gitConfig("git-p4.syncFromOrigin") == "false":
self.syncWithOrigin = False
@@ -2152,13 +2361,22 @@ class P4Sync(Command, P4UserMap):
return branches
+ def writeToGitStream(self, gitMode, relPath, contents):
+ self.gitStream.write('M %s inline %s\n' % (gitMode, relPath))
+ self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
+ for d in contents:
+ self.gitStream.write(d)
+ self.gitStream.write('\n')
+
# output one file from the P4 stream
# - helper for streamP4Files
def streamOneP4File(self, file, contents):
relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
if verbose:
- sys.stderr.write("%s\n" % relPath)
+ size = int(self.stream_file['fileSize'])
+ sys.stdout.write('\r%s --> %s (%i MB)\n' % (file['depotFile'], relPath, size/1024/1024))
+ sys.stdout.flush()
(type_base, type_mods) = split_p4_type(file["type"])
@@ -2193,10 +2411,17 @@ 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', '-', "%s@%s" % (file['depotFile'], file['change']) ])
- if p4_version_string().find("/NT") >= 0:
- text = text.replace("\r\n", "\n")
- contents = [ text ]
+ try:
+ text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
+ except Exception as e:
+ if 'Translation of file content failed' in str(e):
+ type_base = 'binary'
+ else:
+ raise e
+ else:
+ if p4_version_string().find('/NT') >= 0:
+ text = text.replace('\r\n', '\n')
+ contents = [ text ]
if type_base == "apple":
# Apple filetype files will be streamed as a concatenation of
@@ -2220,24 +2445,31 @@ class P4Sync(Command, P4UserMap):
text = regexp.sub(r'$\1$', text)
contents = [ text ]
- self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
+ try:
+ relPath.decode('ascii')
+ except:
+ encoding = 'utf8'
+ if gitConfig('git-p4.pathEncoding'):
+ encoding = gitConfig('git-p4.pathEncoding')
+ relPath = relPath.decode(encoding, 'replace').encode('utf8', 'replace')
+ if self.verbose:
+ print 'Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, relPath)
- # total length...
- length = 0
- for d in contents:
- length = length + len(d)
+ if self.largeFileSystem:
+ (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
- self.gitStream.write("data %d\n" % length)
- for d in contents:
- self.gitStream.write(d)
- self.gitStream.write("\n")
+ self.writeToGitStream(git_mode, relPath, contents)
def streamOneP4Deletion(self, file):
relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
if verbose:
- sys.stderr.write("delete %s\n" % relPath)
+ sys.stdout.write("delete %s\n" % relPath)
+ sys.stdout.flush()
self.gitStream.write("D %s\n" % relPath)
+ if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
+ self.largeFileSystem.removeLargeFile(relPath)
+
# handle another chunk of streaming data
def streamP4FilesCb(self, marshalled):
@@ -2247,6 +2479,14 @@ class P4Sync(Command, P4UserMap):
if marshalled["code"] == "error":
if "data" in marshalled:
err = marshalled["data"].rstrip()
+
+ if not err and 'fileSize' in self.stream_file:
+ required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
+ if required_bytes > 0:
+ err = 'Not enough space left on %s! Free at least %i MB.' % (
+ os.getcwd(), required_bytes/1024/1024
+ )
+
if err:
f = None
if self.stream_have_file_info:
@@ -2275,10 +2515,23 @@ class P4Sync(Command, P4UserMap):
# 'data' field we need to append to our array
for k in marshalled.keys():
if k == 'data':
+ if 'streamContentSize' not in self.stream_file:
+ self.stream_file['streamContentSize'] = 0
+ self.stream_file['streamContentSize'] += len(marshalled['data'])
self.stream_contents.append(marshalled['data'])
else:
self.stream_file[k] = marshalled[k]
+ if (verbose and
+ 'streamContentSize' in self.stream_file and
+ 'fileSize' in self.stream_file and
+ 'depotFile' in self.stream_file):
+ size = int(self.stream_file["fileSize"])
+ if size > 0:
+ progress = 100*self.stream_file['streamContentSize']/size
+ sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
+ sys.stdout.flush()
+
self.stream_have_file_info = True
# Stream directly from "p4 files" into "git fast-import"
@@ -2329,8 +2582,11 @@ class P4Sync(Command, P4UserMap):
else:
return "%s <a@b>" % userid
- # Stream a p4 tag
def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
+ """ Stream a p4 tag.
+ commit is either a git commit, or a fast-import mark, ":<p4commit>"
+ """
+
if verbose:
print "writing tag %s for commit %s" % (labelName, commit)
gitStream.write("tag %s\n" % labelName)
@@ -2381,7 +2637,7 @@ class P4Sync(Command, P4UserMap):
self.clientSpecDirs.update_client_spec_path_cache(files)
self.gitStream.write("commit %s\n" % branch)
-# gitStream.write("mark :%s\n" % details["change"])
+ self.gitStream.write("mark :%s\n" % details["change"])
self.committedChanges.add(int(details["change"]))
committer = ""
if author not in self.users:
@@ -2500,13 +2756,19 @@ class P4Sync(Command, P4UserMap):
if change.has_key('change'):
# find the corresponding git commit; take the oldest commit
changelist = int(change['change'])
- gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
- "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
- if len(gitCommit) == 0:
- print "could not find git commit for changelist %d" % changelist
- else:
- gitCommit = gitCommit.strip()
+ if changelist in self.committedChanges:
+ gitCommit = ":%d" % changelist # use a fast-import mark
commitFound = True
+ else:
+ gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
+ "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
+ if len(gitCommit) == 0:
+ print "importing label %s: could not find git commit for changelist %d" % (name, changelist)
+ else:
+ commitFound = True
+ gitCommit = gitCommit.strip()
+
+ if commitFound:
# Convert from p4 time format
try:
tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
diff --git a/git-quiltimport.sh b/git-quiltimport.sh
index 167d79fea8..6d3a88decd 100755
--- a/git-quiltimport.sh
+++ b/git-quiltimport.sh
@@ -6,7 +6,8 @@ git quiltimport [options]
--
n,dry-run dry run
author= author name and email address for patches without any
-patches= path to the quilt series and patches
+patches= path to the quilt patches
+series= path to the quilt series file
"
SUBDIRECTORY_ON=Yes
. git-sh-setup
@@ -27,6 +28,10 @@ do
shift
QUILT_PATCHES="$1"
;;
+ --series)
+ shift
+ QUILT_SERIES="$1"
+ ;;
--)
shift
break;;
@@ -53,6 +58,13 @@ if ! [ -d "$QUILT_PATCHES" ] ; then
exit 1
fi
+# Quilt series file
+: ${QUILT_SERIES:=$QUILT_PATCHES/series}
+if ! [ -e "$QUILT_SERIES" ] ; then
+ echo "The \"$QUILT_SERIES\" file does not exist."
+ exit 1
+fi
+
# Temporary directories
tmp_dir="$GIT_DIR"/rebase-apply
tmp_msg="$tmp_dir/msg"
@@ -135,5 +147,5 @@ do
commit=$( (echo "$SUBJECT"; echo; cat "$tmp_msg") | git commit-tree $tree -p $commit) &&
git update-ref -m "quiltimport: $patch_name" HEAD $commit || exit 4
fi
-done 3<"$QUILT_PATCHES/series"
+done 3<"$QUILT_SERIES"
rm -rf $tmp_dir || exit 5
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index f01637b1fd..d65c06eff3 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -729,8 +729,8 @@ transform_todo_ids () {
# that do not have a SHA-1 at the beginning of $rest.
;;
*)
- sha1=$(git rev-parse --verify --quiet "$@" ${rest%% *}) &&
- rest="$sha1 ${rest#* }"
+ sha1=$(git rev-parse --verify --quiet "$@" ${rest%%[ ]*}) &&
+ rest="$sha1 ${rest#*[ ]}"
;;
esac
printf '%s\n' "$command${rest:+ }$rest"
@@ -857,7 +857,8 @@ add_exec_commands () {
# Check if the SHA-1 passed as an argument is a
# correct one, if not then print $2 in "$todo".badsha
# $1: the SHA-1 to test
-# $2: the line to display if incorrect SHA-1
+# $2: the line number of the input
+# $3: the input filename
check_commit_sha () {
badsha=0
if test -z $1
@@ -873,9 +874,10 @@ check_commit_sha () {
if test $badsha -ne 0
then
+ line="$(sed -n -e "${2}p" "$3")"
warn "Warning: the SHA-1 is missing or isn't" \
"a commit in the following line:"
- warn " - $2"
+ warn " - $line"
warn
fi
@@ -886,37 +888,31 @@ check_commit_sha () {
# from the todolist in stdin
check_bad_cmd_and_sha () {
retval=0
- git stripspace --strip-comments |
- (
- while read -r line
- do
- IFS=' '
- set -- $line
- command=$1
- sha1=$2
-
- case $command in
- ''|noop|x|"exec")
- # Doesn't expect a SHA-1
- ;;
- pick|p|drop|d|reword|r|edit|e|squash|s|fixup|f)
- if ! check_commit_sha $sha1 "$line"
- then
- retval=1
- fi
- ;;
- *)
- warn "Warning: the command isn't recognized" \
- "in the following line:"
- warn " - $line"
- warn
+ lineno=0
+ while read -r command rest
+ do
+ lineno=$(( $lineno + 1 ))
+ case $command in
+ "$comment_char"*|''|noop|x|exec)
+ # Doesn't expect a SHA-1
+ ;;
+ pick|p|drop|d|reword|r|edit|e|squash|s|fixup|f)
+ if ! check_commit_sha "${rest%%[ ]*}" "$lineno" "$1"
+ then
retval=1
- ;;
- esac
- done
-
- return $retval
- )
+ fi
+ ;;
+ *)
+ line="$(sed -n -e "${lineno}p" "$1")"
+ warn "Warning: the command isn't recognized" \
+ "in the following line:"
+ warn " - $line"
+ warn
+ retval=1
+ ;;
+ esac
+ done <"$1"
+ return $retval
}
# Print the list of the SHA-1 of the commits
@@ -1010,7 +1006,7 @@ check_todo_list () {
;;
esac
- if ! check_bad_cmd_and_sha <"$todo"
+ if ! check_bad_cmd_and_sha "$todo"
then
raise_error=t
fi
diff --git a/git-rebase.sh b/git-rebase.sh
index 1757404bc2..af7ba5fd90 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -14,7 +14,7 @@ git-rebase --continue | --abort | --skip | --edit-todo
Available options are
v,verbose! display a diffstat of what changed upstream
q,quiet! be quiet. implies --no-stat
-autostash! automatically stash/stash pop before and after
+autostash automatically stash/stash pop before and after
fork-point use 'merge-base --fork-point' to refine upstream
onto=! rebase onto given branch instead of upstream
p,preserve-merges! try to recreate merges instead of ignoring them
@@ -292,6 +292,9 @@ do
--autostash)
autostash=true
;;
+ --no-autostash)
+ autostash=false
+ ;;
--verbose)
verbose=t
diffstat=t
diff --git a/git-send-email.perl b/git-send-email.perl
index e3ff44b4d0..e907e0eacf 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -1365,7 +1365,11 @@ Message-Id: $message_id
$smtp->mail( $raw_from ) or die $smtp->message;
$smtp->to( @recipients ) or die $smtp->message;
$smtp->data or die $smtp->message;
- $smtp->datasend("$header\n$message") or die $smtp->message;
+ $smtp->datasend("$header\n") or die $smtp->message;
+ my @lines = split /^/, $message;
+ foreach my $line (@lines) {
+ $smtp->datasend("$line") or die $smtp->message;
+ }
$smtp->dataend() or die $smtp->message;
$smtp->code =~ /250|200/ or die "Failed to send $subject\n".$smtp->message;
}
diff --git a/git-stash.sh b/git-stash.sh
index 1d5ba7a4f9..c7c65e25f5 100755
--- a/git-stash.sh
+++ b/git-stash.sh
@@ -305,7 +305,25 @@ show_stash () {
ALLOW_UNKNOWN_FLAGS=t
assert_stash_like "$@"
- git diff ${FLAGS:---stat} $b_commit $w_commit
+ if test -z "$FLAGS"
+ then
+ if test "$(git config --bool stash.showStat || echo true)" = "true"
+ then
+ FLAGS=--stat
+ fi
+
+ if test "$(git config --bool stash.showPatch || echo false)" = "true"
+ then
+ FLAGS=${FLAGS}${FLAGS:+ }-p
+ fi
+
+ if test -z "$FLAGS"
+ then
+ return 0
+ fi
+ fi
+
+ git diff ${FLAGS} $b_commit $w_commit
}
show_help () {
diff --git a/git-submodule.sh b/git-submodule.sh
index 82e35582cd..9bc5c5f94d 100755
--- a/git-submodule.sh
+++ b/git-submodule.sh
@@ -154,48 +154,6 @@ relative_path ()
echo "$result$target"
}
-#
-# Get submodule info for registered submodules
-# $@ = path to limit submodule list
-#
-module_list()
-{
- eval "set $(git rev-parse --sq --prefix "$wt_prefix" -- "$@")"
- (
- git ls-files -z --error-unmatch --stage -- "$@" ||
- echo "unmatched pathspec exists"
- ) |
- @@PERL@@ -e '
- my %unmerged = ();
- my ($null_sha1) = ("0" x 40);
- my @out = ();
- my $unmatched = 0;
- $/ = "\0";
- while (<STDIN>) {
- if (/^unmatched pathspec/) {
- $unmatched = 1;
- next;
- }
- chomp;
- my ($mode, $sha1, $stage, $path) =
- /^([0-7]+) ([0-9a-f]{40}) ([0-3])\t(.*)$/;
- next unless $mode eq "160000";
- if ($stage ne "0") {
- if (!$unmerged{$path}++) {
- push @out, "$mode $null_sha1 U\t$path\n";
- }
- next;
- }
- push @out, "$_\n";
- }
- if ($unmatched) {
- print "#unmatched\n";
- } else {
- print for (@out);
- }
- '
-}
-
die_if_unmatched ()
{
if test "$1" = "#unmatched"
@@ -229,98 +187,6 @@ get_submodule_config () {
printf '%s' "${value:-$default}"
}
-
-#
-# Map submodule path to submodule name
-#
-# $1 = path
-#
-module_name()
-{
- # Do we have "submodule.<something>.path = $1" defined in .gitmodules file?
- sm_path="$1"
- re=$(printf '%s\n' "$1" | sed -e 's/[].[^$\\*]/\\&/g')
- name=$( git config -f .gitmodules --get-regexp '^submodule\..*\.path$' |
- sed -n -e 's|^submodule\.\(.*\)\.path '"$re"'$|\1|p' )
- test -z "$name" &&
- die "$(eval_gettext "No submodule mapping found in .gitmodules for path '\$sm_path'")"
- printf '%s\n' "$name"
-}
-
-#
-# Clone a submodule
-#
-# $1 = submodule path
-# $2 = submodule name
-# $3 = URL to clone
-# $4 = reference repository to reuse (empty for independent)
-# $5 = depth argument for shallow clones (empty for deep)
-#
-# Prior to calling, cmd_update checks that a possibly existing
-# path is not a git repository.
-# Likewise, cmd_add checks that path does not exist at all,
-# since it is the location of a new submodule.
-#
-module_clone()
-{
- sm_path=$1
- name=$2
- url=$3
- reference="$4"
- depth="$5"
- quiet=
- if test -n "$GIT_QUIET"
- then
- quiet=-q
- fi
-
- gitdir=
- gitdir_base=
- base_name=$(dirname "$name")
-
- gitdir=$(git rev-parse --git-dir)
- gitdir_base="$gitdir/modules/$base_name"
- gitdir="$gitdir/modules/$name"
-
- if test -d "$gitdir"
- then
- mkdir -p "$sm_path"
- rm -f "$gitdir/index"
- else
- mkdir -p "$gitdir_base"
- (
- clear_local_git_env
- git clone $quiet ${depth:+"$depth"} -n ${reference:+"$reference"} \
- --separate-git-dir "$gitdir" "$url" "$sm_path"
- ) ||
- die "$(eval_gettext "Clone of '\$url' into submodule path '\$sm_path' failed")"
- fi
-
- # We already are at the root of the work tree but cd_to_toplevel will
- # resolve any symlinks that might be present in $PWD
- a=$(cd_to_toplevel && cd "$gitdir" && pwd)/
- b=$(cd_to_toplevel && cd "$sm_path" && pwd)/
- # Remove all common leading directories after a sanity check
- if test "${a#$b}" != "$a" || test "${b#$a}" != "$b"; then
- die "$(eval_gettext "Gitdir '\$a' is part of the submodule path '\$b' or vice versa")"
- fi
- while test "${a%%/*}" = "${b%%/*}"
- do
- a=${a#*/}
- b=${b#*/}
- done
- # Now chop off the trailing '/'s that were added in the beginning
- a=${a%/}
- b=${b%/}
-
- # Turn each leading "*/" component into "../"
- rel=$(printf '%s\n' "$b" | sed -e 's|[^/][^/]*|..|g')
- printf '%s\n' "gitdir: $rel/$a" >"$sm_path/.git"
-
- rel=$(printf '%s\n' "$a" | sed -e 's|[^/][^/]*|..|g')
- (clear_local_git_env; cd "$sm_path" && GIT_WORK_TREE=. git config core.worktree "$rel/$b")
-}
-
isnumber()
{
n=$(($1 + 0)) 2>/dev/null && test "$n" = "$1"
@@ -481,7 +347,7 @@ Use -f if you really want to add it." >&2
echo "$(eval_gettext "Reactivating local git directory for submodule '\$sm_name'.")"
fi
fi
- module_clone "$sm_path" "$sm_name" "$realrepo" "$reference" "$depth" || exit
+ git submodule--helper clone ${GIT_QUIET:+--quiet} --prefix "$wt_prefix" --path "$sm_path" --name "$sm_name" --url "$realrepo" "$reference" "$depth" || exit
(
clear_local_git_env
cd "$sm_path" &&
@@ -541,7 +407,7 @@ cmd_foreach()
# command in the subshell (and a recursive call to this function)
exec 3<&0
- module_list |
+ git submodule--helper list --prefix "$wt_prefix"|
while read mode sha1 stage sm_path
do
die_if_unmatched "$mode"
@@ -549,7 +415,7 @@ cmd_foreach()
then
displaypath=$(relative_path "$sm_path")
say "$(eval_gettext "Entering '\$prefix\$displaypath'")"
- name=$(module_name "$sm_path")
+ name=$(git submodule--helper name "$sm_path")
(
prefix="$prefix$sm_path/"
clear_local_git_env
@@ -601,11 +467,11 @@ cmd_init()
shift
done
- module_list "$@" |
+ git submodule--helper list --prefix "$wt_prefix" "$@" |
while read mode sha1 stage sm_path
do
die_if_unmatched "$mode"
- name=$(module_name "$sm_path") || exit
+ name=$(git submodule--helper name "$sm_path") || exit
displaypath=$(relative_path "$sm_path")
@@ -683,11 +549,11 @@ cmd_deinit()
die "$(eval_gettext "Use '.' if you really want to deinitialize all submodules")"
fi
- module_list "$@" |
+ git submodule--helper list --prefix "$wt_prefix" "$@" |
while read mode sha1 stage sm_path
do
die_if_unmatched "$mode"
- name=$(module_name "$sm_path") || exit
+ name=$(git submodule--helper name "$sm_path") || exit
displaypath=$(relative_path "$sm_path")
@@ -799,7 +665,7 @@ cmd_update()
fi
cloned_modules=
- module_list "$@" | {
+ git submodule--helper list --prefix "$wt_prefix" "$@" | {
err=
while read mode sha1 stage sm_path
do
@@ -809,7 +675,7 @@ cmd_update()
echo >&2 "Skipping unmerged submodule $prefix$sm_path"
continue
fi
- name=$(module_name "$sm_path") || exit
+ name=$(git submodule--helper name "$sm_path") || exit
url=$(git config submodule."$name".url)
branch=$(get_submodule_config "$name" branch master)
if ! test -z "$update"
@@ -843,7 +709,7 @@ Maybe you want to use 'update --init'?")"
if ! test -d "$sm_path"/.git && ! test -f "$sm_path"/.git
then
- module_clone "$sm_path" "$name" "$url" "$reference" "$depth" || exit
+ git submodule--helper clone ${GIT_QUIET:+--quiet} --prefix "$prefix" --path "$sm_path" --name "$name" --url "$url" "$reference" "$depth" || exit
cloned_modules="$cloned_modules;$name"
subsha1=
else
@@ -1073,7 +939,7 @@ cmd_summary() {
# Respect the ignore setting for --for-status.
if test -n "$for_status"
then
- name=$(module_name "$sm_path")
+ name=$(git submodule--helper name "$sm_path")
ignore_config=$(get_submodule_config "$name" ignore none)
test $status != A && test $ignore_config = all && continue
fi
@@ -1231,11 +1097,11 @@ cmd_status()
shift
done
- module_list "$@" |
+ git submodule--helper list --prefix "$wt_prefix" "$@" |
while read mode sha1 stage sm_path
do
die_if_unmatched "$mode"
- name=$(module_name "$sm_path") || exit
+ name=$(git submodule--helper name "$sm_path") || exit
url=$(git config submodule."$name".url)
displaypath=$(relative_path "$prefix$sm_path")
if test "$stage" = U
@@ -1308,11 +1174,11 @@ cmd_sync()
esac
done
cd_to_toplevel
- module_list "$@" |
+ git submodule--helper list --prefix "$wt_prefix" "$@" |
while read mode sha1 stage sm_path
do
die_if_unmatched "$mode"
- name=$(module_name "$sm_path")
+ name=$(git submodule--helper name "$sm_path")
url=$(git config -f .gitmodules --get submodule."$name".url)
# Possibly a url relative to parent
diff --git a/git.c b/git.c
index 5feba410ca..6ed824cacf 100644
--- a/git.c
+++ b/git.c
@@ -417,7 +417,7 @@ static struct cmd_struct commands[] = {
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY },
{ "init", cmd_init_db, NO_SETUP },
{ "init-db", cmd_init_db, NO_SETUP },
- { "interpret-trailers", cmd_interpret_trailers, RUN_SETUP },
+ { "interpret-trailers", cmd_interpret_trailers, RUN_SETUP_GENTLY },
{ "log", cmd_log, RUN_SETUP },
{ "ls-files", cmd_ls_files, RUN_SETUP },
{ "ls-remote", cmd_ls_remote, RUN_SETUP_GENTLY },
@@ -470,6 +470,7 @@ static struct cmd_struct commands[] = {
{ "stage", cmd_add, RUN_SETUP | NEED_WORK_TREE },
{ "status", cmd_status, RUN_SETUP | NEED_WORK_TREE },
{ "stripspace", cmd_stripspace },
+ { "submodule--helper", cmd_submodule__helper, RUN_SETUP },
{ "symbolic-ref", cmd_symbolic_ref, RUN_SETUP },
{ "tag", cmd_tag, RUN_SETUP },
{ "unpack-file", cmd_unpack_file, RUN_SETUP },
diff --git a/pager.c b/pager.c
index 27d4c8a17a..e425070528 100644
--- a/pager.c
+++ b/pager.c
@@ -14,19 +14,29 @@
static const char *pager_argv[] = { NULL, NULL };
static struct child_process pager_process = CHILD_PROCESS_INIT;
-static void wait_for_pager(void)
+static void wait_for_pager(int in_signal)
{
- fflush(stdout);
- fflush(stderr);
+ if (!in_signal) {
+ fflush(stdout);
+ fflush(stderr);
+ }
/* signal EOF to pager */
close(1);
close(2);
- finish_command(&pager_process);
+ if (in_signal)
+ finish_command_in_signal(&pager_process);
+ else
+ finish_command(&pager_process);
+}
+
+static void wait_for_pager_atexit(void)
+{
+ wait_for_pager(0);
}
static void wait_for_pager_signal(int signo)
{
- wait_for_pager();
+ wait_for_pager(1);
sigchain_pop(signo);
raise(signo);
}
@@ -90,7 +100,7 @@ void setup_pager(void)
/* this makes sure that the parent terminates after the pager */
sigchain_push_common(wait_for_pager_signal);
- atexit(wait_for_pager);
+ atexit(wait_for_pager_atexit);
}
int pager_in_use(void)
diff --git a/parse-options-cb.c b/parse-options-cb.c
index 5ab6ed6b08..239898d946 100644
--- a/parse-options-cb.c
+++ b/parse-options-cb.c
@@ -5,6 +5,7 @@
#include "color.h"
#include "string-list.h"
#include "argv-array.h"
+#include "sha1-array.h"
/*----- some often used options -----*/
@@ -77,7 +78,7 @@ int parse_opt_verbosity_cb(const struct option *opt, const char *arg,
return 0;
}
-int parse_opt_with_commit(const struct option *opt, const char *arg, int unset)
+int parse_opt_commits(const struct option *opt, const char *arg, int unset)
{
unsigned char sha1[20];
struct commit *commit;
@@ -93,6 +94,22 @@ int parse_opt_with_commit(const struct option *opt, const char *arg, int unset)
return 0;
}
+int parse_opt_object_name(const struct option *opt, const char *arg, int unset)
+{
+ unsigned char sha1[20];
+
+ if (unset) {
+ sha1_array_clear(opt->value);
+ return 0;
+ }
+ if (!arg)
+ return -1;
+ if (get_sha1(arg, sha1))
+ return error(_("malformed object name '%s'"), arg);
+ sha1_array_append(opt->value, sha1);
+ return 0;
+}
+
int parse_opt_tertiary(const struct option *opt, const char *arg, int unset)
{
int *target = opt->value;
diff --git a/parse-options.h b/parse-options.h
index 3f1cc3aee0..e8b55ea87a 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -223,7 +223,8 @@ extern int parse_opt_approxidate_cb(const struct option *, const char *, int);
extern int parse_opt_expiry_date_cb(const struct option *, const char *, int);
extern int parse_opt_color_flag_cb(const struct option *, const char *, int);
extern int parse_opt_verbosity_cb(const struct option *, const char *, int);
-extern int parse_opt_with_commit(const struct option *, const char *, int);
+extern int parse_opt_object_name(const struct option *, const char *, int);
+extern int parse_opt_commits(const struct option *, const char *, int);
extern int parse_opt_tertiary(const struct option *, const char *, int);
extern int parse_opt_string_list(const struct option *, const char *, int);
extern int parse_opt_noop_cb(const struct option *, const char *, int);
@@ -251,5 +252,12 @@ extern int parse_opt_passthru_argv(const struct option *, const char *, int);
{ OPTION_CALLBACK, (s), (l), (v), (a), (h), (f), parse_opt_passthru }
#define OPT_PASSTHRU_ARGV(s, l, v, a, h, f) \
{ OPTION_CALLBACK, (s), (l), (v), (a), (h), (f), parse_opt_passthru_argv }
+#define _OPT_CONTAINS_OR_WITH(name, variable, help, flag) \
+ { OPTION_CALLBACK, 0, name, (variable), N_("commit"), (help), \
+ PARSE_OPT_LASTARG_DEFAULT | flag, \
+ parse_opt_commits, (intptr_t) "HEAD" \
+ }
+#define OPT_CONTAINS(v, h) _OPT_CONTAINS_OR_WITH("contains", v, h, 0)
+#define OPT_WITH(v, h) _OPT_CONTAINS_OR_WITH("with", v, h, PARSE_OPT_HIDDEN)
#endif
diff --git a/path.c b/path.c
index 95acbafa68..1352952616 100644
--- a/path.c
+++ b/path.c
@@ -91,54 +91,274 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
buf->buf[newlen] = '/';
}
-static const char *common_list[] = {
- "/branches", "/hooks", "/info", "!/logs", "/lost-found",
- "/objects", "/refs", "/remotes", "/worktrees", "/rr-cache", "/svn",
- "config", "!gc.pid", "packed-refs", "shallow",
- NULL
+struct common_dir {
+ /* Not considered garbage for report_linked_checkout_garbage */
+ unsigned ignore_garbage:1;
+ unsigned is_dir:1;
+ /* Not common even though its parent is */
+ unsigned exclude:1;
+ const char *dirname;
};
-static void update_common_dir(struct strbuf *buf, int git_dir_len)
+static struct common_dir common_list[] = {
+ { 0, 1, 0, "branches" },
+ { 0, 1, 0, "hooks" },
+ { 0, 1, 0, "info" },
+ { 0, 0, 1, "info/sparse-checkout" },
+ { 1, 1, 0, "logs" },
+ { 1, 1, 1, "logs/HEAD" },
+ { 0, 1, 1, "logs/refs/bisect" },
+ { 0, 1, 0, "lost-found" },
+ { 0, 1, 0, "objects" },
+ { 0, 1, 0, "refs" },
+ { 0, 1, 1, "refs/bisect" },
+ { 0, 1, 0, "remotes" },
+ { 0, 1, 0, "worktrees" },
+ { 0, 1, 0, "rr-cache" },
+ { 0, 1, 0, "svn" },
+ { 0, 0, 0, "config" },
+ { 1, 0, 0, "gc.pid" },
+ { 0, 0, 0, "packed-refs" },
+ { 0, 0, 0, "shallow" },
+ { 0, 0, 0, NULL }
+};
+
+/*
+ * A compressed trie. A trie node consists of zero or more characters that
+ * are common to all elements with this prefix, optionally followed by some
+ * children. If value is not NULL, the trie node is a terminal node.
+ *
+ * For example, consider the following set of strings:
+ * abc
+ * def
+ * definite
+ * definition
+ *
+ * The trie would look look like:
+ * root: len = 0, children a and d non-NULL, value = NULL.
+ * a: len = 2, contents = bc, value = (data for "abc")
+ * d: len = 2, contents = ef, children i non-NULL, value = (data for "def")
+ * i: len = 3, contents = nit, children e and i non-NULL, value = NULL
+ * e: len = 0, children all NULL, value = (data for "definite")
+ * i: len = 2, contents = on, children all NULL,
+ * value = (data for "definition")
+ */
+struct trie {
+ struct trie *children[256];
+ int len;
+ char *contents;
+ void *value;
+};
+
+static struct trie *make_trie_node(const char *key, void *value)
{
- char *base = buf->buf + git_dir_len;
- const char **p;
-
- if (is_dir_file(base, "logs", "HEAD") ||
- is_dir_file(base, "info", "sparse-checkout"))
- return; /* keep this in $GIT_DIR */
- for (p = common_list; *p; p++) {
- const char *path = *p;
- int is_dir = 0;
- if (*path == '!')
- path++;
- if (*path == '/') {
- path++;
- is_dir = 1;
+ struct trie *new_node = xcalloc(1, sizeof(*new_node));
+ new_node->len = strlen(key);
+ if (new_node->len) {
+ new_node->contents = xmalloc(new_node->len);
+ memcpy(new_node->contents, key, new_node->len);
+ }
+ new_node->value = value;
+ return new_node;
+}
+
+/*
+ * Add a key/value pair to a trie. The key is assumed to be \0-terminated.
+ * If there was an existing value for this key, return it.
+ */
+static void *add_to_trie(struct trie *root, const char *key, void *value)
+{
+ struct trie *child;
+ void *old;
+ int i;
+
+ if (!*key) {
+ /* we have reached the end of the key */
+ old = root->value;
+ root->value = value;
+ return old;
+ }
+
+ for (i = 0; i < root->len; i++) {
+ if (root->contents[i] == key[i])
+ continue;
+
+ /*
+ * Split this node: child will contain this node's
+ * existing children.
+ */
+ child = malloc(sizeof(*child));
+ memcpy(child->children, root->children, sizeof(root->children));
+
+ child->len = root->len - i - 1;
+ if (child->len) {
+ child->contents = xstrndup(root->contents + i + 1,
+ child->len);
}
- if (is_dir && dir_prefix(base, path)) {
- replace_dir(buf, git_dir_len, get_git_common_dir());
- return;
+ child->value = root->value;
+ root->value = NULL;
+ root->len = i;
+
+ memset(root->children, 0, sizeof(root->children));
+ root->children[(unsigned char)root->contents[i]] = child;
+
+ /* This is the newly-added child. */
+ root->children[(unsigned char)key[i]] =
+ make_trie_node(key + i + 1, value);
+ return NULL;
+ }
+
+ /* We have matched the entire compressed section */
+ if (key[i]) {
+ child = root->children[(unsigned char)key[root->len]];
+ if (child) {
+ return add_to_trie(child, key + root->len + 1, value);
+ } else {
+ child = make_trie_node(key + root->len + 1, value);
+ root->children[(unsigned char)key[root->len]] = child;
+ return NULL;
}
- if (!is_dir && !strcmp(base, path)) {
- replace_dir(buf, git_dir_len, get_git_common_dir());
- return;
+ }
+
+ old = root->value;
+ root->value = value;
+ return old;
+}
+
+typedef int (*match_fn)(const char *unmatched, void *data, void *baton);
+
+/*
+ * Search a trie for some key. Find the longest /-or-\0-terminated
+ * prefix of the key for which the trie contains a value. Call fn
+ * with the unmatched portion of the key and the found value, and
+ * return its return value. If there is no such prefix, return -1.
+ *
+ * The key is partially normalized: consecutive slashes are skipped.
+ *
+ * For example, consider the trie containing only [refs,
+ * refs/worktree] (both with values).
+ *
+ * | key | unmatched | val from node | return value |
+ * |-----------------|------------|---------------|--------------|
+ * | a | not called | n/a | -1 |
+ * | refs | \0 | refs | as per fn |
+ * | refs/ | / | refs | as per fn |
+ * | refs/w | /w | refs | as per fn |
+ * | refs/worktree | \0 | refs/worktree | as per fn |
+ * | refs/worktree/ | / | refs/worktree | as per fn |
+ * | refs/worktree/a | /a | refs/worktree | as per fn |
+ * |-----------------|------------|---------------|--------------|
+ *
+ */
+static int trie_find(struct trie *root, const char *key, match_fn fn,
+ void *baton)
+{
+ int i;
+ int result;
+ struct trie *child;
+
+ if (!*key) {
+ /* we have reached the end of the key */
+ if (root->value && !root->len)
+ return fn(key, root->value, baton);
+ else
+ return -1;
+ }
+
+ for (i = 0; i < root->len; i++) {
+ /* Partial path normalization: skip consecutive slashes. */
+ if (key[i] == '/' && key[i+1] == '/') {
+ key++;
+ continue;
}
+ if (root->contents[i] != key[i])
+ return -1;
}
+
+ /* Matched the entire compressed section */
+ key += i;
+ if (!*key)
+ /* End of key */
+ return fn(key, root->value, baton);
+
+ /* Partial path normalization: skip consecutive slashes */
+ while (key[0] == '/' && key[1] == '/')
+ key++;
+
+ child = root->children[(unsigned char)*key];
+ if (child)
+ result = trie_find(child, key + 1, fn, baton);
+ else
+ result = -1;
+
+ if (result >= 0 || (*key != '/' && *key != 0))
+ return result;
+ if (root->value)
+ return fn(key, root->value, baton);
+ else
+ return -1;
+}
+
+static struct trie common_trie;
+static int common_trie_done_setup;
+
+static void init_common_trie(void)
+{
+ struct common_dir *p;
+
+ if (common_trie_done_setup)
+ return;
+
+ for (p = common_list; p->dirname; p++)
+ add_to_trie(&common_trie, p->dirname, p);
+
+ common_trie_done_setup = 1;
+}
+
+/*
+ * Helper function for update_common_dir: returns 1 if the dir
+ * prefix is common.
+ */
+static int check_common(const char *unmatched, void *value, void *baton)
+{
+ struct common_dir *dir = value;
+
+ if (!dir)
+ return 0;
+
+ if (dir->is_dir && (unmatched[0] == 0 || unmatched[0] == '/'))
+ return !dir->exclude;
+
+ if (!dir->is_dir && unmatched[0] == 0)
+ return !dir->exclude;
+
+ return 0;
+}
+
+static void update_common_dir(struct strbuf *buf, int git_dir_len,
+ const char *common_dir)
+{
+ char *base = buf->buf + git_dir_len;
+ init_common_trie();
+ if (!common_dir)
+ common_dir = get_git_common_dir();
+ if (trie_find(&common_trie, base, check_common, NULL) > 0)
+ replace_dir(buf, git_dir_len, common_dir);
}
void report_linked_checkout_garbage(void)
{
struct strbuf sb = STRBUF_INIT;
- const char **p;
+ const struct common_dir *p;
int len;
if (!git_common_dir_env)
return;
strbuf_addf(&sb, "%s/", get_git_dir());
len = sb.len;
- for (p = common_list; *p; p++) {
- const char *path = *p;
- if (*path == '!')
+ for (p = common_list; p->dirname; p++) {
+ const char *path = p->dirname;
+ if (p->ignore_garbage)
continue;
strbuf_setlen(&sb, len);
strbuf_addstr(&sb, path);
@@ -160,7 +380,7 @@ static void adjust_git_path(struct strbuf *buf, int git_dir_len)
else if (git_db_env && dir_prefix(base, "objects"))
replace_dir(buf, git_dir_len + 7, get_object_directory());
else if (git_common_dir_env)
- update_common_dir(buf, git_dir_len);
+ update_common_dir(buf, git_dir_len, NULL);
}
static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)
@@ -228,6 +448,8 @@ static void do_submodule_path(struct strbuf *buf, const char *path,
const char *fmt, va_list args)
{
const char *git_dir;
+ struct strbuf git_submodule_common_dir = STRBUF_INIT;
+ struct strbuf git_submodule_dir = STRBUF_INIT;
strbuf_addstr(buf, path);
if (buf->len && buf->buf[buf->len - 1] != '/')
@@ -240,9 +462,17 @@ static void do_submodule_path(struct strbuf *buf, const char *path,
strbuf_addstr(buf, git_dir);
}
strbuf_addch(buf, '/');
+ strbuf_addstr(&git_submodule_dir, buf->buf);
strbuf_vaddf(buf, fmt, args);
+
+ if (get_common_dir_noenv(&git_submodule_common_dir, git_submodule_dir.buf))
+ update_common_dir(buf, git_submodule_dir.len, git_submodule_common_dir.buf);
+
strbuf_cleanup_path(buf);
+
+ strbuf_release(&git_submodule_dir);
+ strbuf_release(&git_submodule_common_dir);
}
char *git_pathdup_submodule(const char *path, const char *fmt, ...)
@@ -431,18 +661,22 @@ const char *enter_repo(const char *path, int strict)
}
if (!suffix[i])
return NULL;
- gitfile = read_gitfile(used_path) ;
+ gitfile = read_gitfile(used_path);
if (gitfile)
strcpy(used_path, gitfile);
if (chdir(used_path))
return NULL;
path = validated_path;
}
- else if (chdir(path))
- return NULL;
+ else {
+ const char *gitfile = read_gitfile(path);
+ if (gitfile)
+ path = gitfile;
+ if (chdir(path))
+ return NULL;
+ }
- if (access("objects", X_OK) == 0 && access("refs", X_OK) == 0 &&
- validate_headref("HEAD") == 0) {
+ if (is_git_directory(".")) {
set_git_dir(".");
check_repository_format();
return path;
@@ -676,6 +910,11 @@ const char *remove_leading_path(const char *in, const char *prefix)
* normalized, any time "../" eats up to the prefix_len part,
* prefix_len is reduced. In the end prefix_len is the remaining
* prefix that has not been overridden by user pathspec.
+ *
+ * NEEDSWORK: This function doesn't perform normalization w.r.t. trailing '/'.
+ * For everything but the root folder itself, the normalized path should not
+ * end with a '/', then the callers need to be fixed up accordingly.
+ *
*/
int normalize_path_copy_len(char *dst, const char *src, int *prefix_len)
{
diff --git a/pkt-line.c b/pkt-line.c
index 08a1427c0d..62fdb37079 100644
--- a/pkt-line.c
+++ b/pkt-line.c
@@ -1,5 +1,6 @@
#include "cache.h"
#include "pkt-line.h"
+#include "run-command.h"
char packet_buffer[LARGE_PACKET_MAX];
static const char *packet_trace_prefix = "git";
@@ -11,6 +12,11 @@ void packet_trace_identity(const char *prog)
packet_trace_prefix = xstrdup(prog);
}
+static const char *get_trace_prefix(void)
+{
+ return in_async() ? "sideband" : packet_trace_prefix;
+}
+
static int packet_trace_pack(const char *buf, unsigned int len, int sideband)
{
if (!sideband) {
@@ -57,7 +63,7 @@ static void packet_trace(const char *buf, unsigned int len, int write)
strbuf_init(&out, len+32);
strbuf_addf(&out, "packet: %12s%c ",
- packet_trace_prefix, write ? '>' : '<');
+ get_trace_prefix(), write ? '>' : '<');
/* XXX we should really handle printable utf8 */
for (i = 0; i < len; i++) {
diff --git a/quote.c b/quote.c
index 7920e18e44..fe884d2452 100644
--- a/quote.c
+++ b/quote.c
@@ -4,9 +4,15 @@
int quote_path_fully = 1;
+static inline int need_bs_quote(char c)
+{
+ return (c == '\'' || c == '!');
+}
+
/* Help to copy the thing properly quoted for the shell safety.
* any single quote is replaced with '\'', any exclamation point
* is replaced with '\!', and the whole thing is enclosed in a
+ * single quote pair.
*
* E.g.
* original sq_quote result
@@ -15,11 +21,6 @@ int quote_path_fully = 1;
* a'b ==> a'\''b ==> 'a'\''b'
* a!b ==> a'\!'b ==> 'a'\!'b'
*/
-static inline int need_bs_quote(char c)
-{
- return (c == '\'' || c == '!');
-}
-
void sq_quote_buf(struct strbuf *dst, const char *src)
{
char *to_free = NULL;
diff --git a/reachable.c b/reachable.c
index 9cff25b490..43616d49c7 100644
--- a/reachable.c
+++ b/reachable.c
@@ -25,9 +25,15 @@ static void update_progress(struct connectivity_progress *cp)
static int add_one_ref(const char *path, const struct object_id *oid,
int flag, void *cb_data)
{
- struct object *object = parse_object_or_die(oid->hash, path);
struct rev_info *revs = (struct rev_info *)cb_data;
+ struct object *object;
+ if ((flag & REF_ISSYMREF) && (flag & REF_ISBROKEN)) {
+ warning("symbolic ref is dangling: %s", path);
+ return 0;
+ }
+
+ object = parse_object_or_die(oid->hash, path);
add_pending_object(revs, object, "");
return 0;
diff --git a/ref-filter.c b/ref-filter.c
index f38dee4f60..dbd8fcec24 100644
--- a/ref-filter.c
+++ b/ref-filter.c
@@ -9,6 +9,10 @@
#include "tag.h"
#include "quote.h"
#include "ref-filter.h"
+#include "revision.h"
+#include "utf8.h"
+#include "git-compat-util.h"
+#include "version.h"
typedef enum { FIELD_STR, FIELD_ULONG, FIELD_TIME } cmp_type;
@@ -43,15 +47,48 @@ static struct {
{ "subject" },
{ "body" },
{ "contents" },
- { "contents:subject" },
- { "contents:body" },
- { "contents:signature" },
{ "upstream" },
{ "push" },
{ "symref" },
{ "flag" },
{ "HEAD" },
{ "color" },
+ { "align" },
+ { "end" },
+};
+
+#define REF_FORMATTING_STATE_INIT { 0, NULL }
+
+struct align {
+ align_type position;
+ unsigned int width;
+};
+
+struct contents {
+ unsigned int lines;
+ struct object_id oid;
+};
+
+struct ref_formatting_stack {
+ struct ref_formatting_stack *prev;
+ struct strbuf output;
+ void (*at_end)(struct ref_formatting_stack *stack);
+ void *at_end_data;
+};
+
+struct ref_formatting_state {
+ int quote_style;
+ struct ref_formatting_stack *stack;
+};
+
+struct atom_value {
+ const char *s;
+ union {
+ struct align align;
+ struct contents contents;
+ } u;
+ void (*handler)(struct atom_value *atomv, struct ref_formatting_state *state);
+ unsigned long ul; /* used for sorting when not FIELD_STR */
};
/*
@@ -123,6 +160,120 @@ int parse_ref_filter_atom(const char *atom, const char *ep)
return at;
}
+static void quote_formatting(struct strbuf *s, const char *str, int quote_style)
+{
+ switch (quote_style) {
+ case QUOTE_NONE:
+ strbuf_addstr(s, str);
+ break;
+ case QUOTE_SHELL:
+ sq_quote_buf(s, str);
+ break;
+ case QUOTE_PERL:
+ perl_quote_buf(s, str);
+ break;
+ case QUOTE_PYTHON:
+ python_quote_buf(s, str);
+ break;
+ case QUOTE_TCL:
+ tcl_quote_buf(s, str);
+ break;
+ }
+}
+
+static void append_atom(struct atom_value *v, struct ref_formatting_state *state)
+{
+ /*
+ * Quote formatting is only done when the stack has a single
+ * element. Otherwise quote formatting is done on the
+ * element's entire output strbuf when the %(end) atom is
+ * encountered.
+ */
+ if (!state->stack->prev)
+ quote_formatting(&state->stack->output, v->s, state->quote_style);
+ else
+ strbuf_addstr(&state->stack->output, v->s);
+}
+
+static void push_stack_element(struct ref_formatting_stack **stack)
+{
+ struct ref_formatting_stack *s = xcalloc(1, sizeof(struct ref_formatting_stack));
+
+ strbuf_init(&s->output, 0);
+ s->prev = *stack;
+ *stack = s;
+}
+
+static void pop_stack_element(struct ref_formatting_stack **stack)
+{
+ struct ref_formatting_stack *current = *stack;
+ struct ref_formatting_stack *prev = current->prev;
+
+ if (prev)
+ strbuf_addbuf(&prev->output, &current->output);
+ strbuf_release(&current->output);
+ free(current);
+ *stack = prev;
+}
+
+static void end_align_handler(struct ref_formatting_stack *stack)
+{
+ struct align *align = (struct align *)stack->at_end_data;
+ struct strbuf s = STRBUF_INIT;
+
+ strbuf_utf8_align(&s, align->position, align->width, stack->output.buf);
+ strbuf_swap(&stack->output, &s);
+ strbuf_release(&s);
+}
+
+static void align_atom_handler(struct atom_value *atomv, struct ref_formatting_state *state)
+{
+ struct ref_formatting_stack *new;
+
+ push_stack_element(&state->stack);
+ new = state->stack;
+ new->at_end = end_align_handler;
+ new->at_end_data = &atomv->u.align;
+}
+
+static void end_atom_handler(struct atom_value *atomv, struct ref_formatting_state *state)
+{
+ struct ref_formatting_stack *current = state->stack;
+ struct strbuf s = STRBUF_INIT;
+
+ if (!current->at_end)
+ die(_("format: %%(end) atom used without corresponding atom"));
+ current->at_end(current);
+
+ /*
+ * Perform quote formatting when the stack element is that of
+ * a supporting atom. If nested then perform quote formatting
+ * only on the topmost supporting atom.
+ */
+ if (!state->stack->prev->prev) {
+ quote_formatting(&s, current->output.buf, state->quote_style);
+ strbuf_swap(&current->output, &s);
+ }
+ strbuf_release(&s);
+ pop_stack_element(&state->stack);
+}
+
+static int match_atom_name(const char *name, const char *atom_name, const char **val)
+{
+ const char *body;
+
+ if (!skip_prefix(name, atom_name, &body))
+ return 0; /* doesn't even begin with "atom_name" */
+ if (!body[0]) {
+ *val = NULL; /* %(atom_name) and no customization */
+ return 1;
+ }
+ if (body[0] != ':')
+ return 0; /* "atom_namefoo" is not "atom_name" or "atom_name:..." */
+ *val = body + 1; /* "atom_name:val" */
+ return 1;
+}
+
/*
* In a format string, find the next occurrence of %(atom).
*/
@@ -497,6 +648,30 @@ static void find_subpos(const char *buf, unsigned long sz,
*nonsiglen = *sig - buf;
}
+/*
+ * If 'lines' is greater than 0, append that many lines from the given
+ * 'buf' of length 'size' to the given strbuf.
+ */
+static void append_lines(struct strbuf *out, const char *buf, unsigned long size, int lines)
+{
+ int i;
+ const char *sp, *eol;
+ size_t len;
+
+ sp = buf;
+
+ for (i = 0; i < lines && sp < buf + size; i++) {
+ if (i)
+ strbuf_addstr(out, "\n ");
+ eol = memchr(sp, '\n', size - (sp - buf));
+ len = eol ? eol - sp : size - (sp - buf);
+ strbuf_add(out, sp, len);
+ if (!eol)
+ break;
+ sp = eol + 1;
+ }
+}
+
/* See grab_values */
static void grab_sub_body_contents(struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz)
{
@@ -507,6 +682,7 @@ static void grab_sub_body_contents(struct atom_value *val, int deref, struct obj
for (i = 0; i < used_atom_cnt; i++) {
const char *name = used_atom[i];
struct atom_value *v = &val[i];
+ const char *valp = NULL;
if (!!deref != (*name == '*'))
continue;
if (deref)
@@ -516,7 +692,8 @@ static void grab_sub_body_contents(struct atom_value *val, int deref, struct obj
strcmp(name, "contents") &&
strcmp(name, "contents:subject") &&
strcmp(name, "contents:body") &&
- strcmp(name, "contents:signature"))
+ strcmp(name, "contents:signature") &&
+ !starts_with(name, "contents:lines="))
continue;
if (!subpos)
find_subpos(buf, sz,
@@ -536,6 +713,16 @@ static void grab_sub_body_contents(struct atom_value *val, int deref, struct obj
v->s = xmemdupz(sigpos, siglen);
else if (!strcmp(name, "contents"))
v->s = xstrdup(subpos);
+ else if (skip_prefix(name, "contents:lines=", &valp)) {
+ struct strbuf s = STRBUF_INIT;
+ const char *contents_end = bodylen + bodypos - siglen;
+
+ if (strtoul_ui(valp, 10, &v->u.contents.lines))
+ die(_("positive value expected contents:lines=%s"), valp);
+ /* Size is the length of the message after removing the signature */
+ append_lines(&s, subpos, contents_end - subpos, v->u.contents.lines);
+ v->s = strbuf_detach(&s, NULL);
+ }
}
}
@@ -621,8 +808,11 @@ static void populate_value(struct ref_array_item *ref)
int deref = 0;
const char *refname;
const char *formatp;
+ const char *valp;
struct branch *branch = NULL;
+ v->handler = append_atom;
+
if (*name == '*') {
deref = 1;
name++;
@@ -653,10 +843,12 @@ static void populate_value(struct ref_array_item *ref)
refname = branch_get_push(branch, NULL);
if (!refname)
continue;
- } else if (starts_with(name, "color:")) {
+ } else if (match_atom_name(name, "color", &valp)) {
char color[COLOR_MAXLEN] = "";
- if (color_parse(name + 6, color) < 0)
+ if (!valp)
+ die(_("expected format: %%(color:<color>)"));
+ if (color_parse(valp, color) < 0)
die(_("unable to parse format"));
v->s = xstrdup(color);
continue;
@@ -686,6 +878,48 @@ static void populate_value(struct ref_array_item *ref)
else
v->s = " ";
continue;
+ } else if (match_atom_name(name, "align", &valp)) {
+ struct align *align = &v->u.align;
+ struct strbuf **s, **to_free;
+ int width = -1;
+
+ if (!valp)
+ die(_("expected format: %%(align:<width>,<position>)"));
+
+ /*
+ * TODO: Implement a function similar to strbuf_split_str()
+ * which would omit the separator from the end of each value.
+ */
+ s = to_free = strbuf_split_str(valp, ',', 0);
+
+ align->position = ALIGN_LEFT;
+
+ while (*s) {
+ /* Strip trailing comma */
+ if (s[1])
+ strbuf_setlen(s[0], s[0]->len - 1);
+ if (!strtoul_ui(s[0]->buf, 10, (unsigned int *)&width))
+ ;
+ else if (!strcmp(s[0]->buf, "left"))
+ align->position = ALIGN_LEFT;
+ else if (!strcmp(s[0]->buf, "right"))
+ align->position = ALIGN_RIGHT;
+ else if (!strcmp(s[0]->buf, "middle"))
+ align->position = ALIGN_MIDDLE;
+ else
+ die(_("improper format entered align:%s"), s[0]->buf);
+ s++;
+ }
+
+ if (width < 0)
+ die(_("positive width expected with the %%(align) atom"));
+ align->width = width;
+ strbuf_list_free(to_free);
+ v->handler = align_atom_handler;
+ continue;
+ } else if (!strcmp(name, "end")) {
+ v->handler = end_atom_handler;
+ continue;
} else
continue;
@@ -817,11 +1051,143 @@ static void get_ref_atom_value(struct ref_array_item *ref, int atom, struct atom
*v = &ref->value[atom];
}
+enum contains_result {
+ CONTAINS_UNKNOWN = -1,
+ CONTAINS_NO = 0,
+ CONTAINS_YES = 1
+};
+
+/*
+ * Mimicking the real stack, this stack lives on the heap, avoiding stack
+ * overflows.
+ *
+ * At each recursion step, the stack items points to the commits whose
+ * ancestors are to be inspected.
+ */
+struct contains_stack {
+ int nr, alloc;
+ struct contains_stack_entry {
+ struct commit *commit;
+ struct commit_list *parents;
+ } *contains_stack;
+};
+
+static int in_commit_list(const struct commit_list *want, struct commit *c)
+{
+ for (; want; want = want->next)
+ if (!hashcmp(want->item->object.sha1, c->object.sha1))
+ return 1;
+ return 0;
+}
+
+/*
+ * Test whether the candidate or one of its parents is contained in the list.
+ * Do not recurse to find out, though, but return -1 if inconclusive.
+ */
+static enum contains_result contains_test(struct commit *candidate,
+ const struct commit_list *want)
+{
+ /* was it previously marked as containing a want commit? */
+ if (candidate->object.flags & TMP_MARK)
+ return 1;
+ /* or marked as not possibly containing a want commit? */
+ if (candidate->object.flags & UNINTERESTING)
+ return 0;
+ /* or are we it? */
+ if (in_commit_list(want, candidate)) {
+ candidate->object.flags |= TMP_MARK;
+ return 1;
+ }
+
+ if (parse_commit(candidate) < 0)
+ return 0;
+
+ return -1;
+}
+
+static void push_to_contains_stack(struct commit *candidate, struct contains_stack *contains_stack)
+{
+ ALLOC_GROW(contains_stack->contains_stack, contains_stack->nr + 1, contains_stack->alloc);
+ contains_stack->contains_stack[contains_stack->nr].commit = candidate;
+ contains_stack->contains_stack[contains_stack->nr++].parents = candidate->parents;
+}
+
+static enum contains_result contains_tag_algo(struct commit *candidate,
+ const struct commit_list *want)
+{
+ struct contains_stack contains_stack = { 0, 0, NULL };
+ int result = contains_test(candidate, want);
+
+ if (result != CONTAINS_UNKNOWN)
+ return result;
+
+ push_to_contains_stack(candidate, &contains_stack);
+ while (contains_stack.nr) {
+ struct contains_stack_entry *entry = &contains_stack.contains_stack[contains_stack.nr - 1];
+ struct commit *commit = entry->commit;
+ struct commit_list *parents = entry->parents;
+
+ if (!parents) {
+ commit->object.flags |= UNINTERESTING;
+ contains_stack.nr--;
+ }
+ /*
+ * If we just popped the stack, parents->item has been marked,
+ * therefore contains_test will return a meaningful 0 or 1.
+ */
+ else switch (contains_test(parents->item, want)) {
+ case CONTAINS_YES:
+ commit->object.flags |= TMP_MARK;
+ contains_stack.nr--;
+ break;
+ case CONTAINS_NO:
+ entry->parents = parents->next;
+ break;
+ case CONTAINS_UNKNOWN:
+ push_to_contains_stack(parents->item, &contains_stack);
+ break;
+ }
+ }
+ free(contains_stack.contains_stack);
+ return contains_test(candidate, want);
+}
+
+static int commit_contains(struct ref_filter *filter, struct commit *commit)
+{
+ if (filter->with_commit_tag_algo)
+ return contains_tag_algo(commit, filter->with_commit);
+ return is_descendant_of(commit, filter->with_commit);
+}
+
+/*
+ * Return 1 if the refname matches one of the patterns, otherwise 0.
+ * A pattern can be a literal prefix (e.g. a refname "refs/heads/master"
+ * matches a pattern "refs/heads/mas") or a wildcard (e.g. the same ref
+ * matches "refs/heads/mas*", too).
+ */
+static int match_pattern(const char **patterns, const char *refname)
+{
+ /*
+ * When no '--format' option is given we need to skip the prefix
+ * for matching refs of tags and branches.
+ */
+ (void)(skip_prefix(refname, "refs/tags/", &refname) ||
+ skip_prefix(refname, "refs/heads/", &refname) ||
+ skip_prefix(refname, "refs/remotes/", &refname) ||
+ skip_prefix(refname, "refs/", &refname));
+
+ for (; *patterns; patterns++) {
+ if (!wildmatch(*patterns, refname, 0, NULL))
+ return 1;
+ }
+ return 0;
+}
+
/*
* Return 1 if the refname matches one of the patterns, otherwise 0.
* A pattern can be path prefix (e.g. a refname "refs/heads/master"
- * matches a pattern "refs/heads/") or a wildcard (e.g. the same ref
- * matches "refs/heads/m*",too).
+ * matches a pattern "refs/heads/" but not "refs/heads/m") or a
+ * wildcard (e.g. the same ref matches "refs/heads/m*", too).
*/
static int match_name_as_path(const char **pattern, const char *refname)
{
@@ -842,6 +1208,48 @@ static int match_name_as_path(const char **pattern, const char *refname)
return 0;
}
+/* Return 1 if the refname matches one of the patterns, otherwise 0. */
+static int filter_pattern_match(struct ref_filter *filter, const char *refname)
+{
+ if (!*filter->name_patterns)
+ return 1; /* No pattern always matches */
+ if (filter->match_as_path)
+ return match_name_as_path(filter->name_patterns, refname);
+ return match_pattern(filter->name_patterns, refname);
+}
+
+/*
+ * Given a ref (sha1, refname), check if the ref belongs to the array
+ * of sha1s. If the given ref is a tag, check if the given tag points
+ * at one of the sha1s in the given sha1 array.
+ * the given sha1_array.
+ * NEEDSWORK:
+ * 1. Only a single level of inderection is obtained, we might want to
+ * change this to account for multiple levels (e.g. annotated tags
+ * pointing to annotated tags pointing to a commit.)
+ * 2. As the refs are cached we might know what refname peels to without
+ * the need to parse the object via parse_object(). peel_ref() might be a
+ * more efficient alternative to obtain the pointee.
+ */
+static const unsigned char *match_points_at(struct sha1_array *points_at,
+ const unsigned char *sha1,
+ const char *refname)
+{
+ const unsigned char *tagged_sha1 = NULL;
+ struct object *obj;
+
+ if (sha1_array_lookup(points_at, sha1) >= 0)
+ return sha1;
+ obj = parse_object(sha1);
+ if (!obj)
+ die(_("malformed object at '%s'"), refname);
+ if (obj->type == OBJ_TAG)
+ tagged_sha1 = ((struct tag *)obj)->tagged->sha1;
+ if (tagged_sha1 && sha1_array_lookup(points_at, tagged_sha1) >= 0)
+ return tagged_sha1;
+ return NULL;
+}
+
/* Allocate space for a new ref_array_item and copy the objectname and flag to it */
static struct ref_array_item *new_ref_array_item(const char *refname,
const unsigned char *objectname,
@@ -857,6 +1265,34 @@ static struct ref_array_item *new_ref_array_item(const char *refname,
return ref;
}
+static int filter_ref_kind(struct ref_filter *filter, const char *refname)
+{
+ unsigned int i;
+
+ static struct {
+ const char *prefix;
+ unsigned int kind;
+ } ref_kind[] = {
+ { "refs/heads/" , FILTER_REFS_BRANCHES },
+ { "refs/remotes/" , FILTER_REFS_REMOTES },
+ { "refs/tags/", FILTER_REFS_TAGS}
+ };
+
+ if (filter->kind == FILTER_REFS_BRANCHES ||
+ filter->kind == FILTER_REFS_REMOTES ||
+ filter->kind == FILTER_REFS_TAGS)
+ return filter->kind;
+ else if (!strcmp(refname, "HEAD"))
+ return FILTER_REFS_DETACHED_HEAD;
+
+ for (i = 0; i < ARRAY_SIZE(ref_kind); i++) {
+ if (starts_with(refname, ref_kind[i].prefix))
+ return ref_kind[i].kind;
+ }
+
+ return FILTER_REFS_OTHERS;
+}
+
/*
* A call-back given to for_each_ref(). Filter refs and keep them for
* later object processing.
@@ -866,6 +1302,8 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid,
struct ref_filter_cbdata *ref_cbdata = cb_data;
struct ref_filter *filter = ref_cbdata->filter;
struct ref_array_item *ref;
+ struct commit *commit = NULL;
+ unsigned int kind;
if (flag & REF_BAD_NAME) {
warning("ignoring ref with broken name %s", refname);
@@ -877,18 +1315,43 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid,
return 0;
}
- if (*filter->name_patterns && !match_name_as_path(filter->name_patterns, refname))
+ /* Obtain the current ref kind from filter_ref_kind() and ignore unwanted refs. */
+ kind = filter_ref_kind(filter, refname);
+ if (!(kind & filter->kind))
+ return 0;
+
+ if (!filter_pattern_match(filter, refname))
return 0;
+ if (filter->points_at.nr && !match_points_at(&filter->points_at, oid->hash, refname))
+ return 0;
+
+ /*
+ * A merge filter is applied on refs pointing to commits. Hence
+ * obtain the commit using the 'oid' available and discard all
+ * non-commits early. The actual filtering is done later.
+ */
+ if (filter->merge_commit || filter->with_commit || filter->verbose) {
+ commit = lookup_commit_reference_gently(oid->hash, 1);
+ if (!commit)
+ return 0;
+ /* We perform the filtering for the '--contains' option */
+ if (filter->with_commit &&
+ !commit_contains(filter, commit))
+ return 0;
+ }
+
/*
* We do not open the object yet; sort may only need refname
* to do its job and the resulting list may yet to be pruned
* by maxcount logic.
*/
ref = new_ref_array_item(refname, oid->hash, flag);
+ ref->commit = commit;
REALLOC_ARRAY(ref_cbdata->array->items, ref_cbdata->array->nr + 1);
ref_cbdata->array->items[ref_cbdata->array->nr++] = ref;
+ ref->kind = kind;
return 0;
}
@@ -911,6 +1374,50 @@ void ref_array_clear(struct ref_array *array)
array->nr = array->alloc = 0;
}
+static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata)
+{
+ struct rev_info revs;
+ int i, old_nr;
+ struct ref_filter *filter = ref_cbdata->filter;
+ struct ref_array *array = ref_cbdata->array;
+ struct commit **to_clear = xcalloc(sizeof(struct commit *), array->nr);
+
+ init_revisions(&revs, NULL);
+
+ for (i = 0; i < array->nr; i++) {
+ struct ref_array_item *item = array->items[i];
+ add_pending_object(&revs, &item->commit->object, item->refname);
+ to_clear[i] = item->commit;
+ }
+
+ filter->merge_commit->object.flags |= UNINTERESTING;
+ add_pending_object(&revs, &filter->merge_commit->object, "");
+
+ revs.limited = 1;
+ if (prepare_revision_walk(&revs))
+ die(_("revision walk setup failed"));
+
+ old_nr = array->nr;
+ array->nr = 0;
+
+ for (i = 0; i < old_nr; i++) {
+ struct ref_array_item *item = array->items[i];
+ struct commit *commit = item->commit;
+
+ int is_merged = !!(commit->object.flags & UNINTERESTING);
+
+ if (is_merged == (filter->merge == REF_FILTER_MERGED_INCLUDE))
+ array->items[array->nr++] = array->items[i];
+ else
+ free_array_item(item);
+ }
+
+ for (i = 0; i < old_nr; i++)
+ clear_commit_marks(to_clear[i], ALL_REV_FLAGS);
+ clear_commit_marks(filter->merge_commit, ALL_REV_FLAGS);
+ free(to_clear);
+}
+
/*
* API for filtering a set of refs. Based on the type of refs the user
* has requested, we iterate through those refs and apply filters
@@ -920,17 +1427,44 @@ void ref_array_clear(struct ref_array *array)
int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int type)
{
struct ref_filter_cbdata ref_cbdata;
+ int ret = 0;
+ unsigned int broken = 0;
ref_cbdata.array = array;
ref_cbdata.filter = filter;
- if (type & (FILTER_REFS_ALL | FILTER_REFS_INCLUDE_BROKEN))
- return for_each_rawref(ref_filter_handler, &ref_cbdata);
- else if (type & FILTER_REFS_ALL)
- return for_each_ref(ref_filter_handler, &ref_cbdata);
- else
+ if (type & FILTER_REFS_INCLUDE_BROKEN)
+ broken = 1;
+ filter->kind = type & FILTER_REFS_KIND_MASK;
+
+ /* Simple per-ref filtering */
+ if (!filter->kind)
die("filter_refs: invalid type");
- return 0;
+ else {
+ /*
+ * For common cases where we need only branches or remotes or tags,
+ * we only iterate through those refs. If a mix of refs is needed,
+ * we iterate over all refs and filter out required refs with the help
+ * of filter_ref_kind().
+ */
+ if (filter->kind == FILTER_REFS_BRANCHES)
+ ret = for_each_fullref_in("refs/heads/", ref_filter_handler, &ref_cbdata, broken);
+ else if (filter->kind == FILTER_REFS_REMOTES)
+ ret = for_each_fullref_in("refs/remotes/", ref_filter_handler, &ref_cbdata, broken);
+ else if (filter->kind == FILTER_REFS_TAGS)
+ ret = for_each_fullref_in("refs/tags/", ref_filter_handler, &ref_cbdata, broken);
+ else if (filter->kind & FILTER_REFS_ALL)
+ ret = for_each_fullref_in("", ref_filter_handler, &ref_cbdata, broken);
+ if (!ret && (filter->kind & FILTER_REFS_DETACHED_HEAD))
+ head_ref(ref_filter_handler, &ref_cbdata);
+ }
+
+
+ /* Filters that need revision walking */
+ if (filter->merge_commit)
+ do_merge_filter(&ref_cbdata);
+
+ return ret;
}
static int cmp_ref_sorting(struct ref_sorting *s, struct ref_array_item *a, struct ref_array_item *b)
@@ -941,19 +1475,19 @@ static int cmp_ref_sorting(struct ref_sorting *s, struct ref_array_item *a, stru
get_ref_atom_value(a, s->atom, &va);
get_ref_atom_value(b, s->atom, &vb);
- switch (cmp_type) {
- case FIELD_STR:
+ if (s->version)
+ cmp = versioncmp(va->s, vb->s);
+ else if (cmp_type == FIELD_STR)
cmp = strcmp(va->s, vb->s);
- break;
- default:
+ else {
if (va->ul < vb->ul)
cmp = -1;
else if (va->ul == vb->ul)
cmp = 0;
else
cmp = 1;
- break;
}
+
return (s->reverse) ? -cmp : cmp;
}
@@ -978,32 +1512,6 @@ void ref_array_sort(struct ref_sorting *sorting, struct ref_array *array)
qsort(array->items, array->nr, sizeof(struct ref_array_item *), compare_refs);
}
-static void print_value(struct atom_value *v, int quote_style)
-{
- struct strbuf sb = STRBUF_INIT;
- switch (quote_style) {
- case QUOTE_NONE:
- fputs(v->s, stdout);
- break;
- case QUOTE_SHELL:
- sq_quote_buf(&sb, v->s);
- break;
- case QUOTE_PERL:
- perl_quote_buf(&sb, v->s);
- break;
- case QUOTE_PYTHON:
- python_quote_buf(&sb, v->s);
- break;
- case QUOTE_TCL:
- tcl_quote_buf(&sb, v->s);
- break;
- }
- if (quote_style != QUOTE_NONE) {
- fputs(sb.buf, stdout);
- strbuf_release(&sb);
- }
-}
-
static int hex1(char ch)
{
if ('0' <= ch && ch <= '9')
@@ -1022,8 +1530,10 @@ static int hex2(const char *cp)
return -1;
}
-static void emit(const char *cp, const char *ep)
+static void append_literal(const char *cp, const char *ep, struct ref_formatting_state *state)
{
+ struct strbuf *s = &state->stack->output;
+
while (*cp && (!ep || cp < ep)) {
if (*cp == '%') {
if (cp[1] == '%')
@@ -1031,13 +1541,13 @@ static void emit(const char *cp, const char *ep)
else {
int ch = hex2(cp + 1);
if (0 <= ch) {
- putchar(ch);
+ strbuf_addch(s, ch);
cp += 3;
continue;
}
}
}
- putchar(*cp);
+ strbuf_addch(s, *cp);
cp++;
}
}
@@ -1045,19 +1555,24 @@ static void emit(const char *cp, const char *ep)
void show_ref_array_item(struct ref_array_item *info, const char *format, int quote_style)
{
const char *cp, *sp, *ep;
+ struct strbuf *final_buf;
+ struct ref_formatting_state state = REF_FORMATTING_STATE_INIT;
+
+ state.quote_style = quote_style;
+ push_stack_element(&state.stack);
for (cp = format; *cp && (sp = find_next(cp)); cp = ep + 1) {
struct atom_value *atomv;
ep = strchr(sp, ')');
if (cp < sp)
- emit(cp, sp);
+ append_literal(cp, sp, &state);
get_ref_atom_value(info, parse_ref_filter_atom(sp + 2, ep), &atomv);
- print_value(atomv, quote_style);
+ atomv->handler(atomv, &state);
}
if (*cp) {
sp = cp + strlen(cp);
- emit(cp, sp);
+ append_literal(cp, sp, &state);
}
if (need_color_reset_at_eol) {
struct atom_value resetv;
@@ -1066,8 +1581,13 @@ void show_ref_array_item(struct ref_array_item *info, const char *format, int qu
if (color_parse("reset", color) < 0)
die("BUG: couldn't parse 'reset' as a color");
resetv.s = color;
- print_value(&resetv, quote_style);
+ append_atom(&resetv, &state);
}
+ if (state.stack->prev)
+ die(_("format: %%(end) atom missing"));
+ final_buf = &state.stack->output;
+ fwrite(final_buf->buf, 1, final_buf->len, stdout);
+ pop_stack_element(&state.stack);
putchar('\n');
}
@@ -1100,7 +1620,29 @@ int parse_opt_ref_sorting(const struct option *opt, const char *arg, int unset)
s->reverse = 1;
arg++;
}
+ if (skip_prefix(arg, "version:", &arg) ||
+ skip_prefix(arg, "v:", &arg))
+ s->version = 1;
len = strlen(arg);
s->atom = parse_ref_filter_atom(arg, arg+len);
return 0;
}
+
+int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset)
+{
+ struct ref_filter *rf = opt->value;
+ unsigned char sha1[20];
+
+ rf->merge = starts_with(opt->long_name, "no")
+ ? REF_FILTER_MERGED_OMIT
+ : REF_FILTER_MERGED_INCLUDE;
+
+ if (get_sha1(arg, sha1))
+ die(_("malformed object name %s"), arg);
+
+ rf->merge_commit = lookup_commit_reference_gently(sha1, 0);
+ if (!rf->merge_commit)
+ return opterror(opt, "must point to a commit", 0);
+
+ return 0;
+}
diff --git a/ref-filter.h b/ref-filter.h
index 699798400b..14d435e2cc 100644
--- a/ref-filter.h
+++ b/ref-filter.h
@@ -13,24 +13,31 @@
#define QUOTE_PYTHON 4
#define QUOTE_TCL 8
-#define FILTER_REFS_INCLUDE_BROKEN 0x1
-#define FILTER_REFS_ALL 0x2
+#define FILTER_REFS_INCLUDE_BROKEN 0x0001
+#define FILTER_REFS_TAGS 0x0002
+#define FILTER_REFS_BRANCHES 0x0004
+#define FILTER_REFS_REMOTES 0x0008
+#define FILTER_REFS_OTHERS 0x0010
+#define FILTER_REFS_ALL (FILTER_REFS_TAGS | FILTER_REFS_BRANCHES | \
+ FILTER_REFS_REMOTES | FILTER_REFS_OTHERS)
+#define FILTER_REFS_DETACHED_HEAD 0x0020
+#define FILTER_REFS_KIND_MASK (FILTER_REFS_ALL | FILTER_REFS_DETACHED_HEAD)
-struct atom_value {
- const char *s;
- unsigned long ul; /* used for sorting when not FIELD_STR */
-};
+struct atom_value;
struct ref_sorting {
struct ref_sorting *next;
int atom; /* index into used_atom array (internal) */
- unsigned reverse : 1;
+ unsigned reverse : 1,
+ version : 1;
};
struct ref_array_item {
unsigned char objectname[20];
int flag;
+ unsigned int kind;
const char *symref;
+ struct commit *commit;
struct atom_value *value;
char refname[FLEX_ARRAY];
};
@@ -38,10 +45,28 @@ struct ref_array_item {
struct ref_array {
int nr, alloc;
struct ref_array_item **items;
+ struct rev_info *revs;
};
struct ref_filter {
const char **name_patterns;
+ struct sha1_array points_at;
+ struct commit_list *with_commit;
+
+ enum {
+ REF_FILTER_MERGED_NONE = 0,
+ REF_FILTER_MERGED_INCLUDE,
+ REF_FILTER_MERGED_OMIT
+ } merge;
+ struct commit *merge_commit;
+
+ unsigned int with_commit_tag_algo : 1,
+ match_as_path : 1,
+ detached : 1;
+ unsigned int kind,
+ lines;
+ int abbrev,
+ verbose;
};
struct ref_filter_cbdata {
@@ -49,6 +74,15 @@ struct ref_filter_cbdata {
struct ref_filter *filter;
};
+/* Macros for checking --merged and --no-merged options */
+#define _OPT_MERGED_NO_MERGED(option, filter, h) \
+ { OPTION_CALLBACK, 0, option, (filter), N_("commit"), (h), \
+ PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \
+ parse_opt_merge_filter, (intptr_t) "HEAD" \
+ }
+#define OPT_MERGED(f, h) _OPT_MERGED_NO_MERGED("merged", f, h)
+#define OPT_NO_MERGED(f, h) _OPT_MERGED_NO_MERGED("no-merged", f, h)
+
/*
* API for filtering a set of refs. Based on the type of refs the user
* has requested, we iterate through those refs and apply filters
@@ -70,5 +104,7 @@ void show_ref_array_item(struct ref_array_item *info, const char *format, int qu
int parse_opt_ref_sorting(const struct option *opt, const char *arg, int unset);
/* Default sort option based on refname */
struct ref_sorting *ref_default_sorting(void);
+/* Function to parse --merged and --no-merged options */
+int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset);
#endif /* REF_FILTER_H */
diff --git a/refs.c b/refs.c
index 4e15f60d98..91c88bad4a 100644
--- a/refs.c
+++ b/refs.c
@@ -304,6 +304,11 @@ struct ref_entry {
};
static void read_loose_refs(const char *dirname, struct ref_dir *dir);
+static int search_ref_dir(struct ref_dir *dir, const char *refname, size_t len);
+static struct ref_entry *create_dir_entry(struct ref_cache *ref_cache,
+ const char *dirname, size_t len,
+ int incomplete);
+static void add_entry_to_dir(struct ref_dir *dir, struct ref_entry *entry);
static struct ref_dir *get_ref_dir(struct ref_entry *entry)
{
@@ -312,6 +317,24 @@ static struct ref_dir *get_ref_dir(struct ref_entry *entry)
dir = &entry->u.subdir;
if (entry->flag & REF_INCOMPLETE) {
read_loose_refs(entry->name, dir);
+
+ /*
+ * Manually add refs/bisect, which, being
+ * per-worktree, might not appear in the directory
+ * listing for refs/ in the main repo.
+ */
+ if (!strcmp(entry->name, "refs/")) {
+ int pos = search_ref_dir(dir, "refs/bisect/", 12);
+ if (pos < 0) {
+ struct ref_entry *child_entry;
+ child_entry = create_dir_entry(dir->ref_cache,
+ "refs/bisect/",
+ 12, 1);
+ add_entry_to_dir(dir, child_entry);
+ read_loose_refs("refs/bisect",
+ &child_entry->u.subdir);
+ }
+ }
entry->flag &= ~REF_INCOMPLETE;
}
return dir;
@@ -2108,6 +2131,15 @@ int for_each_ref_in(const char *prefix, each_ref_fn fn, void *cb_data)
return do_for_each_ref(&ref_cache, prefix, fn, strlen(prefix), 0, cb_data);
}
+int for_each_fullref_in(const char *prefix, each_ref_fn fn, void *cb_data, unsigned int broken)
+{
+ unsigned int flag = 0;
+
+ if (broken)
+ flag = DO_FOR_EACH_INCLUDE_BROKEN;
+ return do_for_each_ref(&ref_cache, prefix, fn, 0, flag, cb_data);
+}
+
int for_each_ref_in_submodule(const char *submodule, const char *prefix,
each_ref_fn fn, void *cb_data)
{
@@ -2649,6 +2681,8 @@ struct pack_refs_cb_data {
struct ref_to_prune *ref_to_prune;
};
+static int is_per_worktree_ref(const char *refname);
+
/*
* An each_ref_entry_fn that is run over loose references only. If
* the loose reference can be packed, add an entry in the packed ref
@@ -2662,6 +2696,10 @@ static int pack_if_possible_fn(struct ref_entry *entry, void *cb_data)
struct ref_entry *packed_entry;
int is_tag_ref = starts_with(entry->name, "refs/tags/");
+ /* Do not pack per-worktree refs: */
+ if (is_per_worktree_ref(entry->name))
+ return 0;
+
/* ALWAYS pack tags */
if (!(cb->flags & PACK_REFS_ALL) && !is_tag_ref)
return 0;
@@ -2856,7 +2894,8 @@ static int delete_ref_loose(struct ref_lock *lock, int flag, struct strbuf *err)
static int is_per_worktree_ref(const char *refname)
{
- return !strcmp(refname, "HEAD");
+ return !strcmp(refname, "HEAD") ||
+ starts_with(refname, "refs/bisect/");
}
static int is_pseudoref_syntax(const char *refname)
diff --git a/refs.h b/refs.h
index e9a5f3230a..6d30c980d1 100644
--- a/refs.h
+++ b/refs.h
@@ -173,6 +173,7 @@ typedef int each_ref_fn(const char *refname,
extern int head_ref(each_ref_fn fn, void *cb_data);
extern int for_each_ref(each_ref_fn fn, void *cb_data);
extern int for_each_ref_in(const char *prefix, each_ref_fn fn, void *cb_data);
+extern int for_each_fullref_in(const char *prefix, each_ref_fn fn, void *cb_data, unsigned int broken);
extern int for_each_tag_ref(each_ref_fn fn, void *cb_data);
extern int for_each_branch_ref(each_ref_fn fn, void *cb_data);
extern int for_each_remote_ref(each_ref_fn fn, void *cb_data);
diff --git a/rerere.c b/rerere.c
index d90057b769..403c700c32 100644
--- a/rerere.c
+++ b/rerere.c
@@ -20,45 +20,74 @@ static int rerere_enabled = -1;
/* automatically update cleanly resolved paths to the index */
static int rerere_autoupdate;
-const char *rerere_path(const char *hex, const char *file)
+static void free_rerere_id(struct string_list_item *item)
{
- return git_path("rr-cache/%s/%s", hex, file);
+ free(item->util);
}
-static int has_rerere_resolution(const char *hex)
+static const char *rerere_id_hex(const struct rerere_id *id)
+{
+ return id->hex;
+}
+
+const char *rerere_path(const struct rerere_id *id, const char *file)
+{
+ if (!file)
+ return git_path("rr-cache/%s", rerere_id_hex(id));
+
+ return git_path("rr-cache/%s/%s", rerere_id_hex(id), file);
+}
+
+static int has_rerere_resolution(const struct rerere_id *id)
{
struct stat st;
- return !stat(rerere_path(hex, "postimage"), &st);
+
+ return !stat(rerere_path(id, "postimage"), &st);
+}
+
+static struct rerere_id *new_rerere_id_hex(char *hex)
+{
+ struct rerere_id *id = xmalloc(sizeof(*id));
+ strcpy(id->hex, hex);
+ return id;
+}
+
+static struct rerere_id *new_rerere_id(unsigned char *sha1)
+{
+ return new_rerere_id_hex(sha1_to_hex(sha1));
}
+/*
+ * $GIT_DIR/MERGE_RR file is a collection of records, each of which is
+ * "conflict ID", a HT and pathname, terminated with a NUL, and is
+ * used to keep track of the set of paths that "rerere" may need to
+ * work on (i.e. what is left by the previous invocation of "git
+ * rerere" during the current conflict resolution session).
+ */
static void read_rr(struct string_list *rr)
{
- unsigned char sha1[20];
- char buf[PATH_MAX];
+ struct strbuf buf = STRBUF_INIT;
FILE *in = fopen(git_path_merge_rr(), "r");
+
if (!in)
return;
- while (fread(buf, 40, 1, in) == 1) {
- int i;
- char *name;
- if (get_sha1_hex(buf, sha1))
+ while (!strbuf_getwholeline(&buf, in, '\0')) {
+ char *path;
+ unsigned char sha1[20];
+ struct rerere_id *id;
+
+ /* There has to be the hash, tab, path and then NUL */
+ if (buf.len < 42 || get_sha1_hex(buf.buf, sha1))
die("corrupt MERGE_RR");
- buf[40] = '\0';
- name = xstrdup(buf);
- if (fgetc(in) != '\t')
+
+ if (buf.buf[40] != '\t')
die("corrupt MERGE_RR");
- for (i = 0; i < sizeof(buf); i++) {
- int c = fgetc(in);
- if (c < 0)
- die("corrupt MERGE_RR");
- buf[i] = c;
- if (c == 0)
- break;
- }
- if (i == sizeof(buf))
- die("filename too long");
- string_list_insert(rr, buf)->util = name;
+ buf.buf[40] = '\0';
+ path = buf.buf + 41;
+ id = new_rerere_id_hex(buf.buf);
+ string_list_insert(rr, path)->util = id;
}
+ strbuf_release(&buf);
fclose(in);
}
@@ -68,22 +97,42 @@ static int write_rr(struct string_list *rr, int out_fd)
{
int i;
for (i = 0; i < rr->nr; i++) {
- const char *path;
- int length;
- if (!rr->items[i].util)
+ struct strbuf buf = STRBUF_INIT;
+ struct rerere_id *id;
+
+ assert(rr->items[i].util != RERERE_RESOLVED);
+
+ id = rr->items[i].util;
+ if (!id)
continue;
- path = rr->items[i].string;
- length = strlen(path) + 1;
- if (write_in_full(out_fd, rr->items[i].util, 40) != 40 ||
- write_str_in_full(out_fd, "\t") != 1 ||
- write_in_full(out_fd, path, length) != length)
+ strbuf_addf(&buf, "%s\t%s%c",
+ rerere_id_hex(id),
+ rr->items[i].string, 0);
+ if (write_in_full(out_fd, buf.buf, buf.len) != buf.len)
die("unable to write rerere record");
+
+ strbuf_release(&buf);
}
if (commit_lock_file(&write_lock) != 0)
die("unable to write rerere record");
return 0;
}
+/*
+ * "rerere" interacts with conflicted file contents using this I/O
+ * abstraction. It reads a conflicted contents from one place via
+ * "getline()" method, and optionally can write it out after
+ * normalizing the conflicted hunks to the "output". Subclasses of
+ * rerere_io embed this structure at the beginning of their own
+ * rerere_io object.
+ */
+struct rerere_io {
+ int (*getline)(struct strbuf *, struct rerere_io *);
+ FILE *output;
+ int wrerror;
+ /* some more stuff */
+};
+
static void ferr_write(const void *p, size_t count, FILE *fp, int *err)
{
if (!count || *err)
@@ -97,31 +146,34 @@ static inline void ferr_puts(const char *s, FILE *fp, int *err)
ferr_write(s, strlen(s), fp, err);
}
-struct rerere_io {
- int (*getline)(struct strbuf *, struct rerere_io *);
- FILE *output;
- int wrerror;
- /* some more stuff */
-};
-
static void rerere_io_putstr(const char *str, struct rerere_io *io)
{
if (io->output)
ferr_puts(str, io->output, &io->wrerror);
}
+/*
+ * Write a conflict marker to io->output (if defined).
+ */
static void rerere_io_putconflict(int ch, int size, struct rerere_io *io)
{
char buf[64];
while (size) {
- if (size < sizeof(buf) - 2) {
+ if (size <= sizeof(buf) - 2) {
memset(buf, ch, size);
buf[size] = '\n';
buf[size + 1] = '\0';
size = 0;
} else {
int sz = sizeof(buf) - 1;
+
+ /*
+ * Make sure we will not write everything out
+ * in this round by leaving at least 1 byte
+ * for the next round, giving the next round
+ * a chance to add the terminating LF. Yuck.
+ */
if (size <= sz)
sz -= (sz - size) + 1;
memset(buf, ch, sz);
@@ -138,19 +190,42 @@ static void rerere_io_putmem(const char *mem, size_t sz, struct rerere_io *io)
ferr_write(mem, sz, io->output, &io->wrerror);
}
+/*
+ * Subclass of rerere_io that reads from an on-disk file
+ */
struct rerere_io_file {
struct rerere_io io;
FILE *input;
};
+/*
+ * ... and its getline() method implementation
+ */
static int rerere_file_getline(struct strbuf *sb, struct rerere_io *io_)
{
struct rerere_io_file *io = (struct rerere_io_file *)io_;
return strbuf_getwholeline(sb, io->input, '\n');
}
-static int is_cmarker(char *buf, int marker_char, int marker_size, int want_sp)
+/*
+ * Require the exact number of conflict marker letters, no more, no
+ * less, followed by SP or any whitespace
+ * (including LF).
+ */
+static int is_cmarker(char *buf, int marker_char, int marker_size)
{
+ int want_sp;
+
+ /*
+ * The beginning of our version and the end of their version
+ * always are labeled like "<<<<< ours" or ">>>>> theirs",
+ * hence we set want_sp for them. Note that the version from
+ * the common ancestor in diff3-style output is not always
+ * labelled (e.g. "||||| common" is often seen but "|||||"
+ * alone is also valid), so we do not set want_sp.
+ */
+ want_sp = (marker_char == '<') || (marker_char == '>');
+
while (marker_size--)
if (*buf++ != marker_char)
return 0;
@@ -159,6 +234,21 @@ static int is_cmarker(char *buf, int marker_char, int marker_size, int want_sp)
return isspace(*buf);
}
+/*
+ * Read contents a file with conflicts, normalize the conflicts
+ * by (1) discarding the common ancestor version in diff3-style,
+ * (2) reordering our side and their side so that whichever sorts
+ * alphabetically earlier comes before the other one, while
+ * computing the "conflict ID", which is just an SHA-1 hash of
+ * one side of the conflict, NUL, the other side of the conflict,
+ * and NUL concatenated together.
+ *
+ * Return the number of conflict hunks found.
+ *
+ * NEEDSWORK: the logic and theory of operation behind this conflict
+ * normalization may deserve to be documented somewhere, perhaps in
+ * Documentation/technical/rerere.txt.
+ */
static int handle_path(unsigned char *sha1, struct rerere_io *io, int marker_size)
{
git_SHA_CTX ctx;
@@ -173,19 +263,19 @@ static int handle_path(unsigned char *sha1, struct rerere_io *io, int marker_siz
git_SHA1_Init(&ctx);
while (!io->getline(&buf, io)) {
- if (is_cmarker(buf.buf, '<', marker_size, 1)) {
+ if (is_cmarker(buf.buf, '<', marker_size)) {
if (hunk != RR_CONTEXT)
goto bad;
hunk = RR_SIDE_1;
- } else if (is_cmarker(buf.buf, '|', marker_size, 0)) {
+ } else if (is_cmarker(buf.buf, '|', marker_size)) {
if (hunk != RR_SIDE_1)
goto bad;
hunk = RR_ORIGINAL;
- } else if (is_cmarker(buf.buf, '=', marker_size, 0)) {
+ } else if (is_cmarker(buf.buf, '=', marker_size)) {
if (hunk != RR_SIDE_1 && hunk != RR_ORIGINAL)
goto bad;
hunk = RR_SIDE_2;
- } else if (is_cmarker(buf.buf, '>', marker_size, 1)) {
+ } else if (is_cmarker(buf.buf, '>', marker_size)) {
if (hunk != RR_SIDE_2)
goto bad;
if (strbuf_cmp(&one, &two) > 0)
@@ -229,6 +319,10 @@ static int handle_path(unsigned char *sha1, struct rerere_io *io, int marker_siz
return hunk_no;
}
+/*
+ * Scan the path for conflicts, do the "handle_path()" thing above, and
+ * return the number of conflict hunks found.
+ */
static int handle_file(const char *path, unsigned char *sha1, const char *output)
{
int hunk_no = 0;
@@ -270,11 +364,18 @@ static int handle_file(const char *path, unsigned char *sha1, const char *output
return hunk_no;
}
+/*
+ * Subclass of rerere_io that reads from an in-core buffer that is a
+ * strbuf
+ */
struct rerere_io_mem {
struct rerere_io io;
struct strbuf input;
};
+/*
+ * ... and its getline() method implementation
+ */
static int rerere_mem_getline(struct strbuf *sb, struct rerere_io *io_)
{
struct rerere_io_mem *io = (struct rerere_io_mem *)io_;
@@ -313,24 +414,23 @@ static int handle_cache(const char *path, unsigned char *sha1, const char *outpu
return -1;
pos = -pos - 1;
- for (i = 0; i < 3; i++) {
+ while (pos < active_nr) {
enum object_type type;
unsigned long size;
- int j;
- if (active_nr <= pos)
- break;
ce = active_cache[pos++];
if (ce_namelen(ce) != len || memcmp(ce->name, path, len))
- continue;
- j = ce_stage(ce) - 1;
- mmfile[j].ptr = read_sha1_file(ce->sha1, &type, &size);
- mmfile[j].size = size;
+ break;
+ i = ce_stage(ce) - 1;
+ if (!mmfile[i].ptr) {
+ mmfile[i].ptr = read_sha1_file(ce->sha1, &type, &size);
+ mmfile[i].size = size;
+ }
}
- for (i = 0; i < 3; i++) {
+ for (i = 0; i < 3; i++)
if (!mmfile[i].ptr && !mmfile[i].size)
mmfile[i].ptr = xstrdup("");
- }
+
/*
* NEEDSWORK: handle conflicts from merges with
* merge.renormalize set, too
@@ -350,6 +450,10 @@ static int handle_cache(const char *path, unsigned char *sha1, const char *outpu
strbuf_init(&io.input, 0);
strbuf_attach(&io.input, result.ptr, result.size, result.size);
+ /*
+ * Grab the conflict ID and optionally write the original
+ * contents with conflict markers out.
+ */
hunk_no = handle_path(sha1, (struct rerere_io *)&io, marker_size);
strbuf_release(&io.input);
if (io.io.output)
@@ -357,6 +461,14 @@ static int handle_cache(const char *path, unsigned char *sha1, const char *outpu
return hunk_no;
}
+/*
+ * Look at a cache entry at "i" and see if it is not conflicting,
+ * conflicting and we are willing to handle, or conflicting and
+ * we are unable to handle, and return the determination in *type.
+ * Return the cache index to be looked at next, by skipping the
+ * stages we have already looked at in this invocation of this
+ * function.
+ */
static int check_one_conflict(int i, int *type)
{
const struct cache_entry *e = active_cache[i];
@@ -367,10 +479,8 @@ static int check_one_conflict(int i, int *type)
}
*type = PUNTED;
- if (ce_stage(e) == 1) {
- if (active_nr <= ++i)
- return i + 1;
- }
+ while (ce_stage(active_cache[i]) == 1)
+ i++;
/* Only handle regular files with both stages #2 and #3 */
if (i + 1 < active_nr) {
@@ -390,6 +500,17 @@ static int check_one_conflict(int i, int *type)
return i;
}
+/*
+ * Scan the index and find paths that have conflicts that rerere can
+ * handle, i.e. the ones that has both stages #2 and #3.
+ *
+ * NEEDSWORK: we do not record or replay a previous "resolve by
+ * deletion" for a delete-modify conflict, as that is inherently risky
+ * without knowing what modification is being discarded. The only
+ * safe case, i.e. both side doing the deletion and modification that
+ * are identical to the previous round, might want to be handled,
+ * though.
+ */
static int find_conflict(struct string_list *conflict)
{
int i;
@@ -406,6 +527,21 @@ static int find_conflict(struct string_list *conflict)
return 0;
}
+/*
+ * The merge_rr list is meant to hold outstanding conflicted paths
+ * that rerere could handle. Abuse the list by adding other types of
+ * entries to allow the caller to show "rerere remaining".
+ *
+ * - Conflicted paths that rerere does not handle are added
+ * - Conflicted paths that have been resolved are marked as such
+ * by storing RERERE_RESOLVED to .util field (where conflict ID
+ * is expected to be stored).
+ *
+ * Do *not* write MERGE_RR file out after calling this function.
+ *
+ * NEEDSWORK: we may want to fix the caller that implements "rerere
+ * remaining" to do this without abusing merge_rr.
+ */
int rerere_remaining(struct string_list *merge_rr)
{
int i;
@@ -424,7 +560,7 @@ int rerere_remaining(struct string_list *merge_rr)
struct string_list_item *it;
it = string_list_lookup(merge_rr, (const char *)e->name);
if (it != NULL) {
- free(it->util);
+ free_rerere_id(it);
it->util = RERERE_RESOLVED;
}
}
@@ -432,39 +568,66 @@ int rerere_remaining(struct string_list *merge_rr)
return 0;
}
-static int merge(const char *name, const char *path)
+/*
+ * Find the conflict identified by "id"; the change between its
+ * "preimage" (i.e. a previous contents with conflict markers) and its
+ * "postimage" (i.e. the corresponding contents with conflicts
+ * resolved) may apply cleanly to the contents stored in "path", i.e.
+ * the conflict this time around.
+ *
+ * Returns 0 for successful replay of recorded resolution, or non-zero
+ * for failure.
+ */
+static int merge(const struct rerere_id *id, const char *path)
{
+ FILE *f;
int ret;
mmfile_t cur = {NULL, 0}, base = {NULL, 0}, other = {NULL, 0};
mmbuffer_t result = {NULL, 0};
- if (handle_file(path, NULL, rerere_path(name, "thisimage")) < 0)
- return 1;
+ /*
+ * Normalize the conflicts in path and write it out to
+ * "thisimage" temporary file.
+ */
+ if (handle_file(path, NULL, rerere_path(id, "thisimage")) < 0) {
+ ret = 1;
+ goto out;
+ }
- if (read_mmfile(&cur, rerere_path(name, "thisimage")) ||
- read_mmfile(&base, rerere_path(name, "preimage")) ||
- read_mmfile(&other, rerere_path(name, "postimage"))) {
+ if (read_mmfile(&cur, rerere_path(id, "thisimage")) ||
+ read_mmfile(&base, rerere_path(id, "preimage")) ||
+ read_mmfile(&other, rerere_path(id, "postimage"))) {
ret = 1;
goto out;
}
+
+ /*
+ * A three-way merge. Note that this honors user-customizable
+ * low-level merge driver settings.
+ */
ret = ll_merge(&result, path, &base, NULL, &cur, "", &other, "", NULL);
- if (!ret) {
- FILE *f;
-
- if (utime(rerere_path(name, "postimage"), NULL) < 0)
- warning("failed utime() on %s: %s",
- rerere_path(name, "postimage"),
- strerror(errno));
- f = fopen(path, "w");
- if (!f)
- return error("Could not open %s: %s", path,
- strerror(errno));
- if (fwrite(result.ptr, result.size, 1, f) != 1)
- error("Could not write %s: %s", path, strerror(errno));
- if (fclose(f))
- return error("Writing %s failed: %s", path,
- strerror(errno));
- }
+ if (ret)
+ goto out;
+
+ /*
+ * A successful replay of recorded resolution.
+ * Mark that "postimage" was used to help gc.
+ */
+ if (utime(rerere_path(id, "postimage"), NULL) < 0)
+ warning("failed utime() on %s: %s",
+ rerere_path(id, "postimage"),
+ strerror(errno));
+
+ /* Update "path" with the resolution */
+ f = fopen(path, "w");
+ if (!f)
+ return error("Could not open %s: %s", path,
+ strerror(errno));
+ if (fwrite(result.ptr, result.size, 1, f) != 1)
+ error("Could not write %s: %s", path, strerror(errno));
+ if (fclose(f))
+ return error("Writing %s failed: %s", path,
+ strerror(errno));
out:
free(cur.ptr);
@@ -487,6 +650,8 @@ static void update_paths(struct string_list *update)
struct string_list_item *item = &update->items[i];
if (add_file_to_cache(item->string, 0))
exit(128);
+ fprintf(stderr, "Staged '%s' using previous resolution.\n",
+ item->string);
}
if (active_cache_changed) {
@@ -496,6 +661,41 @@ static void update_paths(struct string_list *update)
rollback_lock_file(&index_lock);
}
+/*
+ * The path indicated by rr_item may still have conflict for which we
+ * have a recorded resolution, in which case replay it and optionally
+ * update it. Or it may have been resolved by the user and we may
+ * only have the preimage for that conflict, in which case the result
+ * needs to be recorded as a resolution in a postimage file.
+ */
+static void do_rerere_one_path(struct string_list_item *rr_item,
+ struct string_list *update)
+{
+ const char *path = rr_item->string;
+ const struct rerere_id *id = rr_item->util;
+
+ /* Is there a recorded resolution we could attempt to apply? */
+ if (has_rerere_resolution(id)) {
+ if (merge(id, path))
+ return; /* failed to replay */
+
+ if (rerere_autoupdate)
+ string_list_insert(update, path);
+ else
+ fprintf(stderr,
+ "Resolved '%s' using previous resolution.\n",
+ path);
+ } else if (!handle_file(path, NULL, NULL)) {
+ /* The user has resolved it. */
+ copy_file(rerere_path(id, "postimage"), path, 0666);
+ fprintf(stderr, "Recorded resolution for '%s'.\n", path);
+ } else {
+ return;
+ }
+ free_rerere_id(rr_item);
+ rr_item->util = NULL;
+}
+
static int do_plain_rerere(struct string_list *rr, int fd)
{
struct string_list conflict = STRING_LIST_INIT_DUP;
@@ -505,65 +705,55 @@ static int do_plain_rerere(struct string_list *rr, int fd)
find_conflict(&conflict);
/*
- * MERGE_RR records paths with conflicts immediately after merge
- * failed. Some of the conflicted paths might have been hand resolved
- * in the working tree since then, but the initial run would catch all
- * and register their preimages.
+ * MERGE_RR records paths with conflicts immediately after
+ * merge failed. Some of the conflicted paths might have been
+ * hand resolved in the working tree since then, but the
+ * initial run would catch all and register their preimages.
*/
-
for (i = 0; i < conflict.nr; i++) {
+ struct rerere_id *id;
+ unsigned char sha1[20];
const char *path = conflict.items[i].string;
- if (!string_list_has_string(rr, path)) {
- unsigned char sha1[20];
- char *hex;
- int ret;
- ret = handle_file(path, sha1, NULL);
- if (ret < 1)
- continue;
- hex = xstrdup(sha1_to_hex(sha1));
- string_list_insert(rr, path)->util = hex;
- if (mkdir_in_gitdir(git_path("rr-cache/%s", hex)))
- continue;
- handle_file(path, NULL, rerere_path(hex, "preimage"));
- fprintf(stderr, "Recorded preimage for '%s'\n", path);
- }
- }
+ int ret;
- /*
- * Now some of the paths that had conflicts earlier might have been
- * hand resolved. Others may be similar to a conflict already that
- * was resolved before.
- */
+ if (string_list_has_string(rr, path))
+ continue;
- for (i = 0; i < rr->nr; i++) {
- int ret;
- const char *path = rr->items[i].string;
- const char *name = (const char *)rr->items[i].util;
-
- if (has_rerere_resolution(name)) {
- if (!merge(name, path)) {
- const char *msg;
- if (rerere_autoupdate) {
- string_list_insert(&update, path);
- msg = "Staged '%s' using previous resolution.\n";
- } else
- msg = "Resolved '%s' using previous resolution.\n";
- fprintf(stderr, msg, path);
- goto mark_resolved;
- }
- }
+ /*
+ * Ask handle_file() to scan and assign a
+ * conflict ID. No need to write anything out
+ * yet.
+ */
+ ret = handle_file(path, sha1, NULL);
+ if (ret < 1)
+ continue;
- /* Let's see if we have resolved it. */
- ret = handle_file(path, NULL, NULL);
- if (ret)
+ id = new_rerere_id(sha1);
+ string_list_insert(rr, path)->util = id;
+
+ /*
+ * If the directory does not exist, create
+ * it. mkdir_in_gitdir() will fail with
+ * EEXIST if there already is one.
+ *
+ * NEEDSWORK: make sure "gc" does not remove
+ * preimage without removing the directory.
+ */
+ if (mkdir_in_gitdir(rerere_path(id, NULL)))
continue;
- fprintf(stderr, "Recorded resolution for '%s'.\n", path);
- copy_file(rerere_path(name, "postimage"), path, 0666);
- mark_resolved:
- rr->items[i].util = NULL;
+ /*
+ * We are the first to encounter this
+ * conflict. Ask handle_file() to write the
+ * normalized contents to the "preimage" file.
+ */
+ handle_file(path, NULL, rerere_path(id, "preimage"));
+ fprintf(stderr, "Recorded preimage for '%s'\n", path);
}
+ for (i = 0; i < rr->nr; i++)
+ do_rerere_one_path(&rr->items[i], &update);
+
if (update.nr)
update_paths(&update);
@@ -614,6 +804,11 @@ int setup_rerere(struct string_list *merge_rr, int flags)
return fd;
}
+/*
+ * The main entry point that is called internally from codepaths that
+ * perform mergy operations, possibly leaving conflicted index entries
+ * and working tree files.
+ */
int rerere(int flags)
{
struct string_list merge_rr = STRING_LIST_INIT_DUP;
@@ -628,25 +823,42 @@ int rerere(int flags)
static int rerere_forget_one_path(const char *path, struct string_list *rr)
{
const char *filename;
- char *hex;
+ struct rerere_id *id;
unsigned char sha1[20];
int ret;
+ struct string_list_item *item;
+ /*
+ * Recreate the original conflict from the stages in the
+ * index and compute the conflict ID
+ */
ret = handle_cache(path, sha1, NULL);
if (ret < 1)
return error("Could not parse conflict hunks in '%s'", path);
- hex = xstrdup(sha1_to_hex(sha1));
- filename = rerere_path(hex, "postimage");
+
+ /* Nuke the recorded resolution for the conflict */
+ id = new_rerere_id(sha1);
+ filename = rerere_path(id, "postimage");
if (unlink(filename))
return (errno == ENOENT
? error("no remembered resolution for %s", path)
: error("cannot unlink %s: %s", filename, strerror(errno)));
- handle_cache(path, sha1, rerere_path(hex, "preimage"));
+ /*
+ * Update the preimage so that the user can resolve the
+ * conflict in the working tree, run us again to record
+ * the postimage.
+ */
+ handle_cache(path, sha1, rerere_path(id, "preimage"));
fprintf(stderr, "Updated preimage for '%s'\n", path);
-
- string_list_insert(rr, path)->util = hex;
+ /*
+ * And remember that we can record resolution for this
+ * conflict when the user is done.
+ */
+ item = string_list_insert(rr, path);
+ free_rerere_id(item);
+ item->util = id;
fprintf(stderr, "Forgot resolution for %s\n", path);
return 0;
}
@@ -664,6 +876,11 @@ int rerere_forget(struct pathspec *pathspec)
if (fd < 0)
return 0;
+ /*
+ * The paths may have been resolved (incorrectly);
+ * recover the original conflicted state and then
+ * find the conflicted paths.
+ */
unmerge_cache(pathspec);
find_conflict(&conflict);
for (i = 0; i < conflict.nr; i++) {
@@ -676,24 +893,51 @@ int rerere_forget(struct pathspec *pathspec)
return write_rr(&merge_rr, fd);
}
-static time_t rerere_created_at(const char *name)
+/*
+ * Garbage collection support
+ */
+
+/*
+ * Note that this is not reentrant but is used only one-at-a-time
+ * so it does not matter right now.
+ */
+static struct rerere_id *dirname_to_id(const char *name)
+{
+ static struct rerere_id id;
+ strcpy(id.hex, name);
+ return &id;
+}
+
+static time_t rerere_created_at(const char *dir_name)
{
struct stat st;
- return stat(rerere_path(name, "preimage"), &st) ? (time_t) 0 : st.st_mtime;
+ struct rerere_id *id = dirname_to_id(dir_name);
+
+ return stat(rerere_path(id, "preimage"), &st) ? (time_t) 0 : st.st_mtime;
}
-static time_t rerere_last_used_at(const char *name)
+static time_t rerere_last_used_at(const char *dir_name)
{
struct stat st;
- return stat(rerere_path(name, "postimage"), &st) ? (time_t) 0 : st.st_mtime;
+ struct rerere_id *id = dirname_to_id(dir_name);
+
+ return stat(rerere_path(id, "postimage"), &st) ? (time_t) 0 : st.st_mtime;
}
-static void unlink_rr_item(const char *name)
+/*
+ * Remove the recorded resolution for a given conflict ID
+ */
+static void unlink_rr_item(struct rerere_id *id)
{
- unlink(rerere_path(name, "thisimage"));
- unlink(rerere_path(name, "preimage"));
- unlink(rerere_path(name, "postimage"));
- rmdir(git_path("rr-cache/%s", name));
+ unlink(rerere_path(id, "thisimage"));
+ unlink(rerere_path(id, "preimage"));
+ unlink(rerere_path(id, "postimage"));
+ /*
+ * NEEDSWORK: what if this rmdir() fails? Wouldn't we then
+ * assume that we already have preimage recorded in
+ * do_plain_rerere()?
+ */
+ rmdir(rerere_path(id, NULL));
}
void rerere_gc(struct string_list *rr)
@@ -715,6 +959,7 @@ void rerere_gc(struct string_list *rr)
dir = opendir(git_path("rr-cache"));
if (!dir)
die_errno("unable to open rr-cache directory");
+ /* Collect stale conflict IDs ... */
while ((e = readdir(dir))) {
if (is_dot_or_dotdot(e->d_name))
continue;
@@ -732,12 +977,20 @@ void rerere_gc(struct string_list *rr)
string_list_append(&to_remove, e->d_name);
}
closedir(dir);
+ /* ... and then remove them one-by-one */
for (i = 0; i < to_remove.nr; i++)
- unlink_rr_item(to_remove.items[i].string);
+ unlink_rr_item(dirname_to_id(to_remove.items[i].string));
string_list_clear(&to_remove, 0);
rollback_lock_file(&write_lock);
}
+/*
+ * During a conflict resolution, after "rerere" recorded the
+ * preimages, abandon them if the user did not resolve them or
+ * record their resolutions. And drop $GIT_DIR/MERGE_RR.
+ *
+ * NEEDSWORK: shouldn't we be calling this from "reset --hard"?
+ */
void rerere_clear(struct string_list *merge_rr)
{
int i;
@@ -746,9 +999,9 @@ void rerere_clear(struct string_list *merge_rr)
return;
for (i = 0; i < merge_rr->nr; i++) {
- const char *name = (const char *)merge_rr->items[i].util;
- if (!has_rerere_resolution(name))
- unlink_rr_item(name);
+ struct rerere_id *id = merge_rr->items[i].util;
+ if (!has_rerere_resolution(id))
+ unlink_rr_item(id);
}
unlink_or_warn(git_path_merge_rr());
rollback_lock_file(&write_lock);
diff --git a/rerere.h b/rerere.h
index 407d59996d..1222e91921 100644
--- a/rerere.h
+++ b/rerere.h
@@ -16,9 +16,19 @@ struct pathspec;
*/
extern void *RERERE_RESOLVED;
+struct rerere_id {
+ char hex[41];
+};
+
extern int setup_rerere(struct string_list *, int);
extern int rerere(int);
-extern const char *rerere_path(const char *hex, const char *file);
+/*
+ * Given the conflict ID and the name of a "file" used for replaying
+ * the recorded resolution (e.g. "preimage", "postimage"), return the
+ * path to that filesystem entity. With "file" specified with NULL,
+ * return the path to the directory that houses these files.
+ */
+extern const char *rerere_path(const struct rerere_id *, const char *file);
extern int rerere_forget(struct pathspec *);
extern int rerere_remaining(struct string_list *);
extern void rerere_clear(struct string_list *);
diff --git a/run-command.c b/run-command.c
index 3277cf797e..e17e456cda 100644
--- a/run-command.c
+++ b/run-command.c
@@ -18,26 +18,27 @@ struct child_to_clean {
static struct child_to_clean *children_to_clean;
static int installed_child_cleanup_handler;
-static void cleanup_children(int sig)
+static void cleanup_children(int sig, int in_signal)
{
while (children_to_clean) {
struct child_to_clean *p = children_to_clean;
children_to_clean = p->next;
kill(p->pid, sig);
- free(p);
+ if (!in_signal)
+ free(p);
}
}
static void cleanup_children_on_signal(int sig)
{
- cleanup_children(sig);
+ cleanup_children(sig, 1);
sigchain_pop(sig);
raise(sig);
}
static void cleanup_children_on_exit(void)
{
- cleanup_children(SIGTERM);
+ cleanup_children(SIGTERM, 0);
}
static void mark_child_for_cleanup(pid_t pid)
@@ -220,7 +221,7 @@ static inline void set_cloexec(int fd)
fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
}
-static int wait_or_whine(pid_t pid, const char *argv0)
+static int wait_or_whine(pid_t pid, const char *argv0, int in_signal)
{
int status, code = -1;
pid_t waiting;
@@ -228,6 +229,8 @@ static int wait_or_whine(pid_t pid, const char *argv0)
while ((waiting = waitpid(pid, &status, 0)) < 0 && errno == EINTR)
; /* nothing */
+ if (in_signal)
+ return 0;
if (waiting < 0) {
failed_errno = errno;
@@ -437,7 +440,7 @@ fail_pipe:
* At this point we know that fork() succeeded, but execvp()
* failed. Errors have been reported to our stderr.
*/
- wait_or_whine(cmd->pid, cmd->argv[0]);
+ wait_or_whine(cmd->pid, cmd->argv[0], 0);
failed_errno = errno;
cmd->pid = -1;
}
@@ -536,12 +539,18 @@ fail_pipe:
int finish_command(struct child_process *cmd)
{
- int ret = wait_or_whine(cmd->pid, cmd->argv[0]);
+ int ret = wait_or_whine(cmd->pid, cmd->argv[0], 0);
argv_array_clear(&cmd->args);
argv_array_clear(&cmd->env_array);
return ret;
}
+int finish_command_in_signal(struct child_process *cmd)
+{
+ return wait_or_whine(cmd->pid, cmd->argv[0], 1);
+}
+
+
int run_command(struct child_process *cmd)
{
int code;
@@ -595,7 +604,7 @@ static NORETURN void die_async(const char *err, va_list params)
{
vreportf("fatal: ", err, params);
- if (!pthread_equal(main_thread, pthread_self())) {
+ if (in_async()) {
struct async *async = pthread_getspecific(async_key);
if (async->proc_in >= 0)
close(async->proc_in);
@@ -614,6 +623,13 @@ static int async_die_is_recursing(void)
return ret != NULL;
}
+int in_async(void)
+{
+ if (!main_thread_set)
+ return 0; /* no asyncs started yet */
+ return !pthread_equal(main_thread, pthread_self());
+}
+
#else
static struct {
@@ -653,6 +669,12 @@ int git_atexit(void (*handler)(void))
}
#define atexit git_atexit
+static int process_is_async;
+int in_async(void)
+{
+ return process_is_async;
+}
+
#endif
int start_async(struct async *async)
@@ -712,6 +734,7 @@ int start_async(struct async *async)
if (need_out)
close(fdout[0]);
git_atexit_clear();
+ process_is_async = 1;
exit(!!async->proc(proc_in, proc_out, async->data));
}
@@ -772,7 +795,7 @@ error:
int finish_async(struct async *async)
{
#ifdef NO_PTHREADS
- return wait_or_whine(async->pid, "child process");
+ return wait_or_whine(async->pid, "child process", 0);
#else
void *ret = (void *)(intptr_t)(-1);
diff --git a/run-command.h b/run-command.h
index 5b4425a3cb..5428b048e2 100644
--- a/run-command.h
+++ b/run-command.h
@@ -50,6 +50,7 @@ void child_process_init(struct child_process *);
int start_command(struct child_process *);
int finish_command(struct child_process *);
+int finish_command_in_signal(struct child_process *);
int run_command(struct child_process *);
/*
@@ -118,5 +119,6 @@ struct async {
int start_async(struct async *async);
int finish_async(struct async *async);
+int in_async(void);
#endif
diff --git a/setup.c b/setup.c
index a17c51e61d..e41e5e1a82 100644
--- a/setup.c
+++ b/setup.c
@@ -229,14 +229,21 @@ void verify_non_filename(const char *prefix, const char *arg)
int get_common_dir(struct strbuf *sb, const char *gitdir)
{
+ const char *git_env_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
+ if (git_env_common_dir) {
+ strbuf_addstr(sb, git_env_common_dir);
+ return 1;
+ } else {
+ return get_common_dir_noenv(sb, gitdir);
+ }
+}
+
+int get_common_dir_noenv(struct strbuf *sb, const char *gitdir)
+{
struct strbuf data = STRBUF_INIT;
struct strbuf path = STRBUF_INIT;
- const char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
int ret = 0;
- if (git_common_dir) {
- strbuf_addstr(sb, git_common_dir);
- return 1;
- }
+
strbuf_addf(&path, "%s/commondir", gitdir);
if (file_exists(path.buf)) {
if (strbuf_read_file(&data, path.buf, 0) <= 0)
diff --git a/sha1_file.c b/sha1_file.c
index d295a3225a..ca699d7beb 100644
--- a/sha1_file.c
+++ b/sha1_file.c
@@ -786,6 +786,37 @@ void close_pack_windows(struct packed_git *p)
}
}
+static int close_pack_fd(struct packed_git *p)
+{
+ if (p->pack_fd < 0)
+ return 0;
+
+ close(p->pack_fd);
+ pack_open_fds--;
+ p->pack_fd = -1;
+
+ return 1;
+}
+
+static void close_pack(struct packed_git *p)
+{
+ close_pack_windows(p);
+ close_pack_fd(p);
+ close_pack_index(p);
+}
+
+void close_all_packs(void)
+{
+ struct packed_git *p;
+
+ for (p = packed_git; p; p = p->next)
+ if (p->do_not_close)
+ die("BUG! Want to close pack marked 'do-not-close'");
+ else
+ close_pack(p);
+}
+
+
/*
* The LRU pack is the one with the oldest MRU window, preferring packs
* with no used windows, or the oldest mtime if it has no windows allocated.
@@ -853,12 +884,8 @@ static int close_one_pack(void)
find_lru_pack(p, &lru_p, &mru_w, &accept_windows_inuse);
}
- if (lru_p) {
- close(lru_p->pack_fd);
- pack_open_fds--;
- lru_p->pack_fd = -1;
- return 1;
- }
+ if (lru_p)
+ return close_pack_fd(lru_p);
return 0;
}
@@ -898,12 +925,7 @@ void free_pack_by_name(const char *pack_name)
p = *pp;
if (strcmp(pack_name, p->pack_name) == 0) {
clear_delta_base_cache();
- close_pack_windows(p);
- if (p->pack_fd != -1) {
- close(p->pack_fd);
- pack_open_fds--;
- }
- close_pack_index(p);
+ close_pack(p);
free(p->bad_object_sha1);
*pp = p->next;
if (last_found_pack == p)
@@ -1037,11 +1059,7 @@ static int open_packed_git(struct packed_git *p)
{
if (!open_packed_git_1(p))
return 0;
- if (p->pack_fd != -1) {
- close(p->pack_fd);
- pack_open_fds--;
- p->pack_fd = -1;
- }
+ close_pack_fd(p);
return -1;
}
@@ -1107,11 +1125,8 @@ unsigned char *use_pack(struct packed_git *p,
p->pack_name,
strerror(errno));
if (!win->offset && win->len == p->pack_size
- && !p->do_not_close) {
- close(p->pack_fd);
- pack_open_fds--;
- p->pack_fd = -1;
- }
+ && !p->do_not_close)
+ close_pack_fd(p);
pack_mmap_calls++;
pack_open_windows++;
if (pack_mapped > peak_pack_mapped)
diff --git a/submodule.c b/submodule.c
index 245ed4dfbb..5e5a46fe2a 100644
--- a/submodule.c
+++ b/submodule.c
@@ -122,15 +122,8 @@ static int add_submodule_odb(const char *path)
struct strbuf objects_directory = STRBUF_INIT;
struct alternate_object_database *alt_odb;
int ret = 0;
- const char *git_dir;
- strbuf_addf(&objects_directory, "%s/.git", path);
- git_dir = read_gitfile(objects_directory.buf);
- if (git_dir) {
- strbuf_reset(&objects_directory);
- strbuf_addstr(&objects_directory, git_dir);
- }
- strbuf_addstr(&objects_directory, "/objects/");
+ strbuf_git_path_submodule(&objects_directory, path, "objects/");
if (!is_directory(objects_directory.buf)) {
ret = -1;
goto done;
diff --git a/t/annotate-tests.sh b/t/annotate-tests.sh
index f5c01758ca..b1673b3e8f 100644
--- a/t/annotate-tests.sh
+++ b/t/annotate-tests.sh
@@ -111,6 +111,10 @@ test_expect_success 'blame 2 authors + 2 merged-in authors' '
check_count A 2 B 1 B1 2 B2 1
'
+test_expect_success 'blame --first-parent blames merge for branch1' '
+ check_count --first-parent A 2 B 1 "A U Thor" 2 B2 1
+'
+
test_expect_success 'blame ancestor' '
check_count -h master A 2 B 2
'
diff --git a/t/perf/aggregate.perl b/t/perf/aggregate.perl
index 15f7fc1b80..924b19dab4 100755
--- a/t/perf/aggregate.perl
+++ b/t/perf/aggregate.perl
@@ -1,5 +1,6 @@
#!/usr/bin/perl
+use lib '../../perl/blib/lib';
use strict;
use warnings;
use Git;
diff --git a/t/t0002-gitfile.sh b/t/t0002-gitfile.sh
index 9393322c3e..9670e8cbe6 100755
--- a/t/t0002-gitfile.sh
+++ b/t/t0002-gitfile.sh
@@ -116,4 +116,46 @@ test_expect_success 'setup_git_dir twice in subdir' '
)
'
+test_expect_success 'enter_repo non-strict mode' '
+ test_create_repo enter_repo &&
+ (
+ cd enter_repo &&
+ test_tick &&
+ test_commit foo &&
+ mv .git .realgit &&
+ echo "gitdir: .realgit" >.git
+ ) &&
+ git ls-remote enter_repo >actual &&
+ cat >expected <<-\EOF &&
+ 946e985ab20de757ca5b872b16d64e92ff3803a9 HEAD
+ 946e985ab20de757ca5b872b16d64e92ff3803a9 refs/heads/master
+ 946e985ab20de757ca5b872b16d64e92ff3803a9 refs/tags/foo
+ EOF
+ test_cmp expected actual
+'
+
+test_expect_success 'enter_repo linked checkout' '
+ (
+ cd enter_repo &&
+ git worktree add ../foo refs/tags/foo
+ ) &&
+ git ls-remote foo >actual &&
+ cat >expected <<-\EOF &&
+ 946e985ab20de757ca5b872b16d64e92ff3803a9 HEAD
+ 946e985ab20de757ca5b872b16d64e92ff3803a9 refs/heads/master
+ 946e985ab20de757ca5b872b16d64e92ff3803a9 refs/tags/foo
+ EOF
+ test_cmp expected actual
+'
+
+test_expect_success 'enter_repo strict mode' '
+ git ls-remote --upload-pack="git upload-pack --strict" foo/.git >actual &&
+ cat >expected <<-\EOF &&
+ 946e985ab20de757ca5b872b16d64e92ff3803a9 HEAD
+ 946e985ab20de757ca5b872b16d64e92ff3803a9 refs/heads/master
+ 946e985ab20de757ca5b872b16d64e92ff3803a9 refs/tags/foo
+ EOF
+ test_cmp expected actual
+'
+
test_done
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index 93605f42f2..627ef854d5 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -266,15 +266,21 @@ test_expect_success 'setup common repository' 'git --git-dir=bar init'
test_git_path GIT_COMMON_DIR=bar index .git/index
test_git_path GIT_COMMON_DIR=bar HEAD .git/HEAD
test_git_path GIT_COMMON_DIR=bar logs/HEAD .git/logs/HEAD
+test_git_path GIT_COMMON_DIR=bar logs/refs/bisect/foo .git/logs/refs/bisect/foo
+test_git_path GIT_COMMON_DIR=bar logs/refs/bisec/foo bar/logs/refs/bisec/foo
+test_git_path GIT_COMMON_DIR=bar logs/refs/bisec bar/logs/refs/bisec
+test_git_path GIT_COMMON_DIR=bar logs/refs/bisectfoo bar/logs/refs/bisectfoo
test_git_path GIT_COMMON_DIR=bar objects bar/objects
test_git_path GIT_COMMON_DIR=bar objects/bar bar/objects/bar
test_git_path GIT_COMMON_DIR=bar info/exclude bar/info/exclude
test_git_path GIT_COMMON_DIR=bar info/grafts bar/info/grafts
test_git_path GIT_COMMON_DIR=bar info/sparse-checkout .git/info/sparse-checkout
+test_git_path GIT_COMMON_DIR=bar info//sparse-checkout .git/info//sparse-checkout
test_git_path GIT_COMMON_DIR=bar remotes/bar bar/remotes/bar
test_git_path GIT_COMMON_DIR=bar branches/bar bar/branches/bar
test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master bar/logs/refs/heads/master
test_git_path GIT_COMMON_DIR=bar refs/heads/master bar/refs/heads/master
+test_git_path GIT_COMMON_DIR=bar refs/bisect/foo .git/refs/bisect/foo
test_git_path GIT_COMMON_DIR=bar hooks/me bar/hooks/me
test_git_path GIT_COMMON_DIR=bar config bar/config
test_git_path GIT_COMMON_DIR=bar packed-refs bar/packed-refs
diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh
index 97406fa4b1..af1b20dd5c 100755
--- a/t/t1400-update-ref.sh
+++ b/t/t1400-update-ref.sh
@@ -1130,4 +1130,23 @@ test_expect_success ULIMIT_FILE_DESCRIPTORS 'large transaction deleting branches
)
'
+test_expect_success 'handle per-worktree refs in refs/bisect' '
+ git commit --allow-empty -m "initial commit" &&
+ git worktree add -b branch worktree &&
+ (
+ cd worktree &&
+ git commit --allow-empty -m "test commit" &&
+ git for-each-ref >for-each-ref.out &&
+ ! grep refs/bisect for-each-ref.out &&
+ git update-ref refs/bisect/something HEAD &&
+ git rev-parse refs/bisect/something >../worktree-head &&
+ git for-each-ref | grep refs/bisect/something
+ ) &&
+ test_path_is_missing .git/refs/bisect &&
+ test_must_fail git rev-parse refs/bisect/something &&
+ git update-ref refs/bisect/something HEAD &&
+ git rev-parse refs/bisect/something >main-head &&
+ ! test_cmp main-head worktree-head
+'
+
test_done
diff --git a/t/t1430-bad-ref-name.sh b/t/t1430-bad-ref-name.sh
index 16d0b8bd1a..c465abe8e3 100755
--- a/t/t1430-bad-ref-name.sh
+++ b/t/t1430-bad-ref-name.sh
@@ -38,18 +38,20 @@ test_expect_success 'fast-import: fail on invalid branch name "bad[branch]name"'
test_must_fail git fast-import <input
'
-test_expect_success 'git branch shows badly named ref' '
+test_expect_success 'git branch shows badly named ref as warning' '
cp .git/refs/heads/master .git/refs/heads/broken...ref &&
test_when_finished "rm -f .git/refs/heads/broken...ref" &&
- git branch >output &&
- grep -e "broken\.\.\.ref" output
+ git branch >output 2>error &&
+ grep -e "broken\.\.\.ref" error &&
+ ! grep -e "broken\.\.\.ref" output
'
test_expect_success 'branch -d can delete badly named ref' '
cp .git/refs/heads/master .git/refs/heads/broken...ref &&
test_when_finished "rm -f .git/refs/heads/broken...ref" &&
git branch -d broken...ref &&
- git branch >output &&
+ git branch >output 2>error &&
+ ! grep -e "broken\.\.\.ref" error &&
! grep -e "broken\.\.\.ref" output
'
@@ -57,7 +59,8 @@ test_expect_success 'branch -D can delete badly named ref' '
cp .git/refs/heads/master .git/refs/heads/broken...ref &&
test_when_finished "rm -f .git/refs/heads/broken...ref" &&
git branch -D broken...ref &&
- git branch >output &&
+ git branch >output 2>error &&
+ ! grep -e "broken\.\.\.ref" error &&
! grep -e "broken\.\.\.ref" output
'
@@ -85,7 +88,8 @@ test_expect_success 'branch -D cannot delete absolute path' '
test_expect_success 'git branch cannot create a badly named ref' '
test_when_finished "rm -f .git/refs/heads/broken...ref" &&
test_must_fail git branch broken...ref &&
- git branch >output &&
+ git branch >output 2>error &&
+ ! grep -e "broken\.\.\.ref" error &&
! grep -e "broken\.\.\.ref" output
'
@@ -95,7 +99,8 @@ test_expect_success 'branch -m cannot rename to a bad ref name' '
git branch goodref &&
test_must_fail git branch -m goodref broken...ref &&
test_cmp_rev master goodref &&
- git branch >output &&
+ git branch >output 2>error &&
+ ! grep -e "broken\.\.\.ref" error &&
! grep -e "broken\.\.\.ref" output
'
@@ -104,14 +109,16 @@ test_expect_failure 'branch -m can rename from a bad ref name' '
test_when_finished "rm -f .git/refs/heads/broken...ref" &&
git branch -m broken...ref renamed &&
test_cmp_rev master renamed &&
- git branch >output &&
+ git branch >output 2>error &&
+ ! grep -e "broken\.\.\.ref" error &&
! grep -e "broken\.\.\.ref" output
'
test_expect_success 'push cannot create a badly named ref' '
test_when_finished "rm -f .git/refs/heads/broken...ref" &&
test_must_fail git push "file://$(pwd)" HEAD:refs/heads/broken...ref &&
- git branch >output &&
+ git branch >output 2>error &&
+ ! grep -e "broken\.\.\.ref" error &&
! grep -e "broken\.\.\.ref" output
'
@@ -131,7 +138,8 @@ test_expect_failure 'push --mirror can delete badly named ref' '
cp .git/refs/heads/master .git/refs/heads/broken...ref
) &&
git -C src push --mirror "file://$top/dest" &&
- git -C dest branch >output &&
+ git -C dest branch >output 2>error &&
+ ! grep -e "broken\.\.\.ref" error &&
! grep -e "broken\.\.\.ref" output
'
@@ -159,7 +167,8 @@ test_expect_success 'update-ref -d can delete broken name' '
cp .git/refs/heads/master .git/refs/heads/broken...ref &&
test_when_finished "rm -f .git/refs/heads/broken...ref" &&
git update-ref -d refs/heads/broken...ref &&
- git branch >output &&
+ git branch >output 2>error &&
+ ! grep -e "broken\.\.\.ref" error &&
! grep -e "broken\.\.\.ref" output
'
diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh
index 956673b8a1..dc09797021 100755
--- a/t/t1450-fsck.sh
+++ b/t/t1450-fsck.sh
@@ -77,11 +77,31 @@ test_expect_success 'object with bad sha1' '
test_expect_success 'branch pointing to non-commit' '
git rev-parse HEAD^{tree} >.git/refs/heads/invalid &&
test_when_finished "git update-ref -d refs/heads/invalid" &&
- git fsck 2>out &&
+ test_must_fail git fsck 2>out &&
cat out &&
grep "not a commit" out
'
+test_expect_success 'HEAD link pointing at a funny object' '
+ test_when_finished "mv .git/SAVED_HEAD .git/HEAD" &&
+ mv .git/HEAD .git/SAVED_HEAD &&
+ echo 0000000000000000000000000000000000000000 >.git/HEAD &&
+ # avoid corrupt/broken HEAD from interfering with repo discovery
+ test_must_fail env GIT_DIR=.git git fsck 2>out &&
+ cat out &&
+ grep "detached HEAD points" out
+'
+
+test_expect_success 'HEAD link pointing at a funny place' '
+ test_when_finished "mv .git/SAVED_HEAD .git/HEAD" &&
+ mv .git/HEAD .git/SAVED_HEAD &&
+ echo "ref: refs/funny/place" >.git/HEAD &&
+ # avoid corrupt/broken HEAD from interfering with repo discovery
+ test_must_fail env GIT_DIR=.git git fsck 2>out &&
+ cat out &&
+ grep "HEAD points to something strange" out
+'
+
test_expect_success 'email without @ is okay' '
git cat-file commit HEAD >basis &&
sed "s/@/AT/" basis >okay &&
diff --git a/t/t2025-worktree-add.sh b/t/t2025-worktree-add.sh
index 8267411a0e..3694174989 100755
--- a/t/t2025-worktree-add.sh
+++ b/t/t2025-worktree-add.sh
@@ -193,4 +193,9 @@ test_expect_success '"add" -B/--detach mutually exclusive' '
test_must_fail git worktree add -B poodle --detach bamboo master
'
+test_expect_success 'local clone from linked checkout' '
+ git clone --local here here-clone &&
+ ( cd here-clone && git fsck )
+'
+
test_done
diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-worktree-prune.sh
index a0f1e3bb80..a0f1e3bb80 100755
--- a/t/t2026-prune-linked-checkouts.sh
+++ b/t/t2026-worktree-prune.sh
diff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh
index 3fc484e8c3..da257c020f 100755
--- a/t/t3001-ls-files-others-exclude.sh
+++ b/t/t3001-ls-files-others-exclude.sh
@@ -305,4 +305,29 @@ test_expect_success 'ls-files with "**" patterns and no slashes' '
test_cmp expect actual
'
+test_expect_success 'negative patterns' '
+ git init reinclude &&
+ (
+ cd reinclude &&
+ cat >.gitignore <<-\EOF &&
+ /fooo
+ /foo
+ !foo/bar/bar
+ EOF
+ mkdir fooo &&
+ cat >fooo/.gitignore <<-\EOF &&
+ !/*
+ EOF
+ mkdir -p foo/bar &&
+ touch abc foo/def foo/bar/ghi foo/bar/bar &&
+ git ls-files -o --exclude-standard >../actual &&
+ cat >../expected <<-\EOF &&
+ .gitignore
+ abc
+ foo/bar/bar
+ EOF
+ test_cmp ../expected ../actual
+ )
+'
+
test_done
diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh
index f51d0f3cad..9454423ca0 100755
--- a/t/t3203-branch-output.sh
+++ b/t/t3203-branch-output.sh
@@ -106,6 +106,19 @@ EOF
test_i18ncmp expect actual
'
+test_expect_success 'git branch shows detached HEAD properly after checkout --detach' '
+ git checkout master &&
+ cat >expect <<EOF &&
+* (HEAD detached at $(git rev-parse --short HEAD^0))
+ branch-one
+ branch-two
+ master
+EOF
+ git checkout --detach &&
+ git branch >actual &&
+ test_i18ncmp expect actual
+'
+
test_expect_success 'git branch shows detached HEAD properly after moving' '
cat >expect <<EOF &&
* (HEAD detached from $(git rev-parse --short HEAD))
@@ -143,4 +156,24 @@ EOF
test_i18ncmp expect actual
'
+test_expect_success 'git branch `--sort` option' '
+ cat >expect <<-\EOF &&
+ branch-two
+ * (HEAD detached from fromtag)
+ branch-one
+ master
+ EOF
+ git branch --sort=objectsize >actual &&
+ test_i18ncmp expect actual
+'
+
+test_expect_success 'git branch --points-at option' '
+ cat >expect <<-\EOF &&
+ branch-one
+ master
+ EOF
+ git branch --points-at=branch-one >actual &&
+ test_cmp expect actual
+'
+
test_done
diff --git a/t/t3210-pack-refs.sh b/t/t3210-pack-refs.sh
index 7b5b6d452e..db244d2f88 100755
--- a/t/t3210-pack-refs.sh
+++ b/t/t3210-pack-refs.sh
@@ -160,6 +160,13 @@ test_expect_success 'pack ref directly below refs/' '
test_path_is_missing .git/refs/top
'
+test_expect_success 'do not pack ref in refs/bisect' '
+ git update-ref refs/bisect/local HEAD &&
+ git pack-refs --all --prune &&
+ ! grep refs/bisect/local .git/packed-refs >/dev/null &&
+ test_path_is_file .git/refs/bisect/local
+'
+
test_expect_success 'disable reflogs' '
git config core.logallrefupdates false &&
rm -rf .git/logs
diff --git a/t/t3301-notes.sh b/t/t3301-notes.sh
index 8cffd35fb0..cd70274ea5 100755
--- a/t/t3301-notes.sh
+++ b/t/t3301-notes.sh
@@ -1122,6 +1122,12 @@ test_expect_success 'git notes copy diagnoses too many or too few parameters' '
test_must_fail git notes copy one two three
'
+test_expect_success 'git notes get-ref expands refs/heads/master to refs/notes/refs/heads/master' '
+ test_unconfig core.notesRef &&
+ sane_unset GIT_NOTES_REF &&
+ test "$(git notes --ref=refs/heads/master get-ref)" = "refs/notes/refs/heads/master"
+'
+
test_expect_success 'git notes get-ref (no overrides)' '
test_unconfig core.notesRef &&
sane_unset GIT_NOTES_REF &&
diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh
index d26e3f57dc..3de0b1dcfd 100755
--- a/t/t3404-rebase-interactive.sh
+++ b/t/t3404-rebase-interactive.sh
@@ -1227,6 +1227,21 @@ test_expect_success 'static check of bad command' '
test C = $(git cat-file commit HEAD^ | sed -ne \$p)
'
+test_expect_success 'tabs and spaces are accepted in the todolist' '
+ rebase_setup_and_clean indented-comment &&
+ write_script add-indent.sh <<-\EOF &&
+ (
+ # Turn single spaces into space/tab mix
+ sed "1s/ / /g; 2s/ / /g; 3s/ / /g" "$1"
+ printf "\n\t# comment\n #more\n\t # comment\n"
+ ) >$1.new
+ mv "$1.new" "$1"
+ EOF
+ test_set_editor "$(pwd)/add-indent.sh" &&
+ git rebase -i HEAD^^^ &&
+ test E = $(git cat-file commit HEAD | sed -ne \$p)
+'
+
cat >expect <<EOF
Warning: the SHA-1 is missing or isn't a commit in the following line:
- edit XXXXXXX False commit
diff --git a/t/t3420-rebase-autostash.sh b/t/t3420-rebase-autostash.sh
index d783f03d3f..944154b2e0 100755
--- a/t/t3420-rebase-autostash.sh
+++ b/t/t3420-rebase-autostash.sh
@@ -37,6 +37,16 @@ testrebase() {
type=$1
dotest=$2
+ test_expect_success "rebase$type: dirty worktree, --no-autostash" '
+ test_config rebase.autostash true &&
+ git reset --hard &&
+ git checkout -b rebased-feature-branch feature-branch &&
+ test_when_finished git branch -D rebased-feature-branch &&
+ test_when_finished git checkout feature-branch &&
+ echo dirty >>file3 &&
+ test_must_fail git rebase$type --no-autostash unrelated-onto-branch
+ '
+
test_expect_success "rebase$type: dirty worktree, non-conflicting rebase" '
test_config rebase.autostash true &&
git reset --hard &&
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 7a8499ce66..dfaf9d9f68 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -919,6 +919,19 @@ test_expect_success 'new remote' '
cmp expect actual
'
+get_url_test () {
+ cat >expect &&
+ git remote get-url "$@" >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success 'get-url on new remote' '
+ echo foo | get_url_test someremote &&
+ echo foo | get_url_test --all someremote &&
+ echo foo | get_url_test --push someremote &&
+ echo foo | get_url_test --push --all someremote
+'
+
test_expect_success 'remote set-url bar' '
git remote set-url someremote bar &&
echo bar >expect &&
@@ -961,6 +974,13 @@ test_expect_success 'remote set-url --push zot' '
cmp expect actual
'
+test_expect_success 'get-url with different urls' '
+ echo baz | get_url_test someremote &&
+ echo baz | get_url_test --all someremote &&
+ echo zot | get_url_test --push someremote &&
+ echo zot | get_url_test --push --all someremote
+'
+
test_expect_success 'remote set-url --push qux zot' '
git remote set-url --push someremote qux zot &&
echo qux >expect &&
@@ -995,6 +1015,14 @@ test_expect_success 'remote set-url --push --add aaa' '
cmp expect actual
'
+test_expect_success 'get-url on multi push remote' '
+ echo foo | get_url_test --push someremote &&
+ get_url_test --push --all someremote <<-\EOF
+ foo
+ aaa
+ EOF
+'
+
test_expect_success 'remote set-url --push bar aaa' '
git remote set-url --push someremote bar aaa &&
echo foo >expect &&
@@ -1039,6 +1067,14 @@ test_expect_success 'remote set-url --add bbb' '
cmp expect actual
'
+test_expect_success 'get-url on multi fetch remote' '
+ echo baz | get_url_test someremote &&
+ get_url_test --all someremote <<-\EOF
+ baz
+ bbb
+ EOF
+'
+
test_expect_success 'remote set-url --delete .*' '
test_must_fail git remote set-url --delete someremote .\* &&
echo "YYY" >expect &&
@@ -1108,6 +1144,7 @@ test_extra_arg rename origin newname
test_extra_arg remove origin
test_extra_arg set-head origin master
# set-branches takes any number of args
+test_extra_arg get-url origin newurl
test_extra_arg set-url origin newurl oldurl
# show takes any number of args
# prune takes any number of args
diff --git a/t/t5507-remote-environment.sh b/t/t5507-remote-environment.sh
new file mode 100755
index 0000000000..e6149295b1
--- /dev/null
+++ b/t/t5507-remote-environment.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+
+test_description='check environment showed to remote side of transports'
+. ./test-lib.sh
+
+test_expect_success 'set up "remote" push situation' '
+ test_commit one &&
+ git config push.default current &&
+ git init remote
+'
+
+test_expect_success 'set up fake ssh' '
+ GIT_SSH_COMMAND="f() {
+ cd \"\$TRASH_DIRECTORY\" &&
+ eval \"\$2\"
+ }; f" &&
+ export GIT_SSH_COMMAND &&
+ export TRASH_DIRECTORY
+'
+
+# due to receive.denyCurrentBranch=true
+test_expect_success 'confirm default push fails' '
+ test_must_fail git push remote
+'
+
+test_expect_success 'config does not travel over same-machine push' '
+ test_must_fail git -c receive.denyCurrentBranch=false push remote
+'
+
+test_expect_success 'config does not travel over ssh push' '
+ test_must_fail git -c receive.denyCurrentBranch=false push host:remote
+'
+
+test_done
diff --git a/t/t5560-http-backend-noserver.sh b/t/t5560-http-backend-noserver.sh
index aa73eeaef8..9fafcf1945 100755
--- a/t/t5560-http-backend-noserver.sh
+++ b/t/t5560-http-backend-noserver.sh
@@ -44,10 +44,6 @@ POST() {
test_cmp exp act
}
-log_div() {
- return 0
-}
-
. "$TEST_DIRECTORY"/t556x_common
expect_aliased() {
diff --git a/t/t5561-http-backend.sh b/t/t5561-http-backend.sh
index 19afe96698..90e0d6f0fe 100755
--- a/t/t5561-http-backend.sh
+++ b/t/t5561-http-backend.sh
@@ -29,15 +29,9 @@ POST() {
test_cmp exp act
}
-log_div() {
- echo >>"$HTTPD_ROOT_PATH"/access.log
- echo "### $1" >>"$HTTPD_ROOT_PATH"/access.log
- echo "###" >>"$HTTPD_ROOT_PATH"/access.log
-}
-
. "$TEST_DIRECTORY"/t556x_common
-cat >exp <<EOF
+grep '^[^#]' >exp <<EOF
### refs/heads/master
###
diff --git a/t/t556x_common b/t/t556x_common
index 82926cfdb7..359fcfe32b 100755
--- a/t/t556x_common
+++ b/t/t556x_common
@@ -52,21 +52,17 @@ get_static_files() {
SMART=smart
GIT_HTTP_EXPORT_ALL=1 && export GIT_HTTP_EXPORT_ALL
test_expect_success 'direct refs/heads/master not found' '
- log_div "refs/heads/master" &&
GET refs/heads/master "404 Not Found"
'
test_expect_success 'static file is ok' '
- log_div "getanyfile default" &&
get_static_files "200 OK"
'
SMART=smart_noexport
unset GIT_HTTP_EXPORT_ALL
test_expect_success 'no export by default' '
- log_div "no git-daemon-export-ok" &&
get_static_files "404 Not Found"
'
test_expect_success 'export if git-daemon-export-ok' '
- log_div "git-daemon-export-ok" &&
(cd "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" &&
touch git-daemon-export-ok
) &&
@@ -75,47 +71,39 @@ test_expect_success 'export if git-daemon-export-ok' '
SMART=smart
GIT_HTTP_EXPORT_ALL=1 && export GIT_HTTP_EXPORT_ALL
test_expect_success 'static file if http.getanyfile true is ok' '
- log_div "getanyfile true" &&
config http.getanyfile true &&
get_static_files "200 OK"
'
test_expect_success 'static file if http.getanyfile false fails' '
- log_div "getanyfile false" &&
config http.getanyfile false &&
get_static_files "403 Forbidden"
'
test_expect_success 'http.uploadpack default enabled' '
- log_div "uploadpack default" &&
GET info/refs?service=git-upload-pack "200 OK" &&
POST git-upload-pack 0000 "200 OK"
'
test_expect_success 'http.uploadpack true' '
- log_div "uploadpack true" &&
config http.uploadpack true &&
GET info/refs?service=git-upload-pack "200 OK" &&
POST git-upload-pack 0000 "200 OK"
'
test_expect_success 'http.uploadpack false' '
- log_div "uploadpack false" &&
config http.uploadpack false &&
GET info/refs?service=git-upload-pack "403 Forbidden" &&
POST git-upload-pack 0000 "403 Forbidden"
'
test_expect_success 'http.receivepack default disabled' '
- log_div "receivepack default" &&
GET info/refs?service=git-receive-pack "403 Forbidden" &&
POST git-receive-pack 0000 "403 Forbidden"
'
test_expect_success 'http.receivepack true' '
- log_div "receivepack true" &&
config http.receivepack true &&
GET info/refs?service=git-receive-pack "200 OK" &&
POST git-receive-pack 0000 "200 OK"
'
test_expect_success 'http.receivepack false' '
- log_div "receivepack false" &&
config http.receivepack false &&
GET info/refs?service=git-receive-pack "403 Forbidden" &&
POST git-receive-pack 0000 "403 Forbidden"
diff --git a/t/t5700-clone-reference.sh b/t/t5700-clone-reference.sh
index ef1779f5ca..2250ef4fe2 100755
--- a/t/t5700-clone-reference.sh
+++ b/t/t5700-clone-reference.sh
@@ -188,5 +188,26 @@ test_expect_success 'clone and dissociate from reference' '
test_must_fail git -C R fsck &&
git -C S fsck
'
+test_expect_success 'clone, dissociate from partial reference and repack' '
+ rm -fr P Q R &&
+ git init P &&
+ (
+ cd P &&
+ test_commit one &&
+ git repack &&
+ test_commit two &&
+ git repack
+ ) &&
+ git clone --bare P Q &&
+ (
+ cd P &&
+ git checkout -b second &&
+ test_commit three &&
+ git repack
+ ) &&
+ git clone --bare --dissociate --reference=P Q R &&
+ ls R/objects/pack/*.pack >packs.txt &&
+ test_line_count = 1 packs.txt
+'
test_done
diff --git a/t/t5801-remote-helpers.sh b/t/t5801-remote-helpers.sh
index c9d3ed14c3..362b1581e0 100755
--- a/t/t5801-remote-helpers.sh
+++ b/t/t5801-remote-helpers.sh
@@ -242,13 +242,6 @@ clean_mark () {
sort >$(basename "$1")
}
-cmp_marks () {
- test_when_finished "rm -rf git.marks testgit.marks" &&
- clean_mark ".git/testgit/$1/git.marks" &&
- clean_mark ".git/testgit/$1/testgit.marks" &&
- test_cmp git.marks testgit.marks
-}
-
test_expect_success 'proper failure checks for fetching' '
(cd local &&
test_must_fail env GIT_REMOTE_TESTGIT_FAILURE=1 git fetch 2>error &&
@@ -258,12 +251,15 @@ test_expect_success 'proper failure checks for fetching' '
'
test_expect_success 'proper failure checks for pushing' '
+ test_when_finished "rm -rf local/git.marks local/testgit.marks" &&
(cd local &&
git checkout -b crash master &&
echo crash >>file &&
git commit -a -m crash &&
test_must_fail env GIT_REMOTE_TESTGIT_FAILURE=1 git push --all &&
- cmp_marks origin
+ clean_mark ".git/testgit/origin/git.marks" &&
+ clean_mark ".git/testgit/origin/testgit.marks" &&
+ test_cmp git.marks testgit.marks
)
'
diff --git a/t/t6030-bisect-porcelain.sh b/t/t6030-bisect-porcelain.sh
index 9e2c203747..e74662ba5c 100755
--- a/t/t6030-bisect-porcelain.sh
+++ b/t/t6030-bisect-porcelain.sh
@@ -759,4 +759,139 @@ test_expect_success '"git bisect bad HEAD" behaves as "git bisect bad"' '
git bisect reset
'
+test_expect_success 'bisect starts with only one new' '
+ git bisect reset &&
+ git bisect start &&
+ git bisect new $HASH4 &&
+ git bisect next
+'
+
+test_expect_success 'bisect does not start with only one old' '
+ git bisect reset &&
+ git bisect start &&
+ git bisect old $HASH1 &&
+ test_must_fail git bisect next
+'
+
+test_expect_success 'bisect start with one new and old' '
+ git bisect reset &&
+ git bisect start &&
+ git bisect old $HASH1 &&
+ git bisect new $HASH4 &&
+ git bisect new &&
+ git bisect new >bisect_result &&
+ grep "$HASH2 is the first new commit" bisect_result &&
+ git bisect log >log_to_replay.txt &&
+ git bisect reset
+'
+
+test_expect_success 'bisect replay with old and new' '
+ git bisect replay log_to_replay.txt >bisect_result &&
+ grep "$HASH2 is the first new commit" bisect_result &&
+ git bisect reset
+'
+
+test_expect_success 'bisect cannot mix old/new and good/bad' '
+ git bisect start &&
+ git bisect bad $HASH4 &&
+ test_must_fail git bisect old $HASH1
+'
+
+test_expect_success 'bisect terms needs 0 or 1 argument' '
+ git bisect reset &&
+ test_must_fail git bisect terms only-one &&
+ test_must_fail git bisect terms 1 2 &&
+ test_must_fail git bisect terms 2>actual &&
+ echo "no terms defined" >expected &&
+ test_cmp expected actual
+'
+
+test_expect_success 'bisect terms shows good/bad after start' '
+ git bisect reset &&
+ git bisect start HEAD $HASH1 &&
+ git bisect terms --term-good >actual &&
+ echo good >expected &&
+ test_cmp expected actual &&
+ git bisect terms --term-bad >actual &&
+ echo bad >expected &&
+ test_cmp expected actual
+'
+
+test_expect_success 'bisect start with one term1 and term2' '
+ git bisect reset &&
+ git bisect start --term-old term2 --term-new term1 &&
+ git bisect term2 $HASH1 &&
+ git bisect term1 $HASH4 &&
+ git bisect term1 &&
+ git bisect term1 >bisect_result &&
+ grep "$HASH2 is the first term1 commit" bisect_result &&
+ git bisect log >log_to_replay.txt &&
+ git bisect reset
+'
+
+test_expect_success 'bisect replay with term1 and term2' '
+ git bisect replay log_to_replay.txt >bisect_result &&
+ grep "$HASH2 is the first term1 commit" bisect_result &&
+ git bisect reset
+'
+
+test_expect_success 'bisect start term1 term2' '
+ git bisect reset &&
+ git bisect start --term-new term1 --term-old term2 $HASH4 $HASH1 &&
+ git bisect term1 &&
+ git bisect term1 >bisect_result &&
+ grep "$HASH2 is the first term1 commit" bisect_result &&
+ git bisect log >log_to_replay.txt &&
+ git bisect reset
+'
+
+test_expect_success 'bisect cannot mix terms' '
+ git bisect reset &&
+ git bisect start --term-good term1 --term-bad term2 $HASH4 $HASH1 &&
+ test_must_fail git bisect a &&
+ test_must_fail git bisect b &&
+ test_must_fail git bisect bad &&
+ test_must_fail git bisect good &&
+ test_must_fail git bisect new &&
+ test_must_fail git bisect old
+'
+
+test_expect_success 'bisect terms rejects invalid terms' '
+ git bisect reset &&
+ test_must_fail git bisect start --term-good invalid..term &&
+ test_must_fail git bisect terms --term-bad invalid..term &&
+ test_must_fail git bisect terms --term-good bad &&
+ test_must_fail git bisect terms --term-good old &&
+ test_must_fail git bisect terms --term-good skip &&
+ test_must_fail git bisect terms --term-good reset &&
+ test_path_is_missing .git/BISECT_TERMS
+'
+
+test_expect_success 'bisect start --term-* does store terms' '
+ git bisect reset &&
+ git bisect start --term-bad=one --term-good=two &&
+ git bisect terms >actual &&
+ cat <<-EOF >expected &&
+ Your current terms are two for the old state
+ and one for the new state.
+ EOF
+ test_cmp expected actual &&
+ git bisect terms --term-bad >actual &&
+ echo one >expected &&
+ test_cmp expected actual &&
+ git bisect terms --term-good >actual &&
+ echo two >expected &&
+ test_cmp expected actual
+'
+
+test_expect_success 'bisect start takes options and revs in any order' '
+ git bisect reset &&
+ git bisect start --term-good one $HASH4 \
+ --term-good two --term-bad bad-term \
+ $HASH1 --term-good three -- &&
+ (git bisect terms --term-bad && git bisect terms --term-good) >actual &&
+ printf "%s\n%s\n" bad-term three >expected &&
+ test_cmp expected actual
+'
+
test_done
diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh
index 7c9bec7630..03873b09d1 100755
--- a/t/t6300-for-each-ref.sh
+++ b/t/t6300-for-each-ref.sh
@@ -8,8 +8,8 @@ test_description='for-each-ref test'
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-gpg.sh
-# Mon Jul 3 15:18:43 2006 +0000
-datestamp=1151939923
+# Mon Jul 3 23:18:43 2006 +0000
+datestamp=1151968723
setdate_and_increment () {
GIT_COMMITTER_DATE="$datestamp +0200"
datestamp=$(expr "$datestamp" + 1)
@@ -61,21 +61,21 @@ test_atom head object ''
test_atom head type ''
test_atom head '*objectname' ''
test_atom head '*objecttype' ''
-test_atom head author 'A U Thor <author@example.com> 1151939924 +0200'
+test_atom head author 'A U Thor <author@example.com> 1151968724 +0200'
test_atom head authorname 'A U Thor'
test_atom head authoremail '<author@example.com>'
-test_atom head authordate 'Mon Jul 3 17:18:44 2006 +0200'
-test_atom head committer 'C O Mitter <committer@example.com> 1151939923 +0200'
+test_atom head authordate 'Tue Jul 4 01:18:44 2006 +0200'
+test_atom head committer 'C O Mitter <committer@example.com> 1151968723 +0200'
test_atom head committername 'C O Mitter'
test_atom head committeremail '<committer@example.com>'
-test_atom head committerdate 'Mon Jul 3 17:18:43 2006 +0200'
+test_atom head committerdate 'Tue Jul 4 01:18:43 2006 +0200'
test_atom head tag ''
test_atom head tagger ''
test_atom head taggername ''
test_atom head taggeremail ''
test_atom head taggerdate ''
-test_atom head creator 'C O Mitter <committer@example.com> 1151939923 +0200'
-test_atom head creatordate 'Mon Jul 3 17:18:43 2006 +0200'
+test_atom head creator 'C O Mitter <committer@example.com> 1151968723 +0200'
+test_atom head creatordate 'Tue Jul 4 01:18:43 2006 +0200'
test_atom head subject 'Initial'
test_atom head contents:subject 'Initial'
test_atom head body ''
@@ -96,7 +96,7 @@ test_atom tag parent ''
test_atom tag numparent ''
test_atom tag object $(git rev-parse refs/tags/testtag^0)
test_atom tag type 'commit'
-test_atom tag '*objectname' '67a36f10722846e891fbada1ba48ed035de75581'
+test_atom tag '*objectname' 'ea122842f48be4afb2d1fc6a4b96c05885ab7463'
test_atom tag '*objecttype' 'commit'
test_atom tag author ''
test_atom tag authorname ''
@@ -107,18 +107,18 @@ test_atom tag committername ''
test_atom tag committeremail ''
test_atom tag committerdate ''
test_atom tag tag 'testtag'
-test_atom tag tagger 'C O Mitter <committer@example.com> 1151939925 +0200'
+test_atom tag tagger 'C O Mitter <committer@example.com> 1151968725 +0200'
test_atom tag taggername 'C O Mitter'
test_atom tag taggeremail '<committer@example.com>'
-test_atom tag taggerdate 'Mon Jul 3 17:18:45 2006 +0200'
-test_atom tag creator 'C O Mitter <committer@example.com> 1151939925 +0200'
-test_atom tag creatordate 'Mon Jul 3 17:18:45 2006 +0200'
-test_atom tag subject 'Tagging at 1151939927'
-test_atom tag contents:subject 'Tagging at 1151939927'
+test_atom tag taggerdate 'Tue Jul 4 01:18:45 2006 +0200'
+test_atom tag creator 'C O Mitter <committer@example.com> 1151968725 +0200'
+test_atom tag creatordate 'Tue Jul 4 01:18:45 2006 +0200'
+test_atom tag subject 'Tagging at 1151968727'
+test_atom tag contents:subject 'Tagging at 1151968727'
test_atom tag body ''
test_atom tag contents:body ''
test_atom tag contents:signature ''
-test_atom tag contents 'Tagging at 1151939927
+test_atom tag contents 'Tagging at 1151968727
'
test_atom tag HEAD ' '
@@ -146,95 +146,123 @@ test_expect_success 'Check invalid format specifiers are errors' '
test_must_fail git for-each-ref --format="%(authordate:INVALID)" refs/heads
'
-cat >expected <<\EOF
-'refs/heads/master' 'Mon Jul 3 17:18:43 2006 +0200' 'Mon Jul 3 17:18:44 2006 +0200'
-'refs/tags/testtag' 'Mon Jul 3 17:18:45 2006 +0200'
-EOF
+test_date () {
+ f=$1 &&
+ committer_date=$2 &&
+ author_date=$3 &&
+ tagger_date=$4 &&
+ cat >expected <<-EOF &&
+ 'refs/heads/master' '$committer_date' '$author_date'
+ 'refs/tags/testtag' '$tagger_date'
+ EOF
+ (
+ git for-each-ref --shell \
+ --format="%(refname) %(committerdate${f:+:$f}) %(authordate${f:+:$f})" \
+ refs/heads &&
+ git for-each-ref --shell \
+ --format="%(refname) %(taggerdate${f:+:$f})" \
+ refs/tags
+ ) >actual &&
+ test_cmp expected actual
+}
test_expect_success 'Check unformatted date fields output' '
- (git for-each-ref --shell --format="%(refname) %(committerdate) %(authordate)" refs/heads &&
- git for-each-ref --shell --format="%(refname) %(taggerdate)" refs/tags) >actual &&
- test_cmp expected actual
+ test_date "" \
+ "Tue Jul 4 01:18:43 2006 +0200" \
+ "Tue Jul 4 01:18:44 2006 +0200" \
+ "Tue Jul 4 01:18:45 2006 +0200"
'
test_expect_success 'Check format "default" formatted date fields output' '
- f=default &&
- (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads &&
- git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual &&
- test_cmp expected actual
+ test_date default \
+ "Tue Jul 4 01:18:43 2006 +0200" \
+ "Tue Jul 4 01:18:44 2006 +0200" \
+ "Tue Jul 4 01:18:45 2006 +0200"
+'
+
+test_expect_success 'Check format "default-local" date fields output' '
+ test_date default-local "Mon Jul 3 23:18:43 2006" "Mon Jul 3 23:18:44 2006" "Mon Jul 3 23:18:45 2006"
'
# Don't know how to do relative check because I can't know when this script
# is going to be run and can't fake the current time to git, and hence can't
# provide expected output. Instead, I'll just make sure that "relative"
# doesn't exit in error
-#
-#cat >expected <<\EOF
-#
-#EOF
-#
test_expect_success 'Check format "relative" date fields output' '
f=relative &&
(git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads &&
git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual
'
-cat >expected <<\EOF
-'refs/heads/master' '2006-07-03' '2006-07-03'
-'refs/tags/testtag' '2006-07-03'
-EOF
+# We just check that this is the same as "relative" for now.
+test_expect_success 'Check format "relative-local" date fields output' '
+ test_date relative-local \
+ "$(git for-each-ref --format="%(committerdate:relative)" refs/heads)" \
+ "$(git for-each-ref --format="%(authordate:relative)" refs/heads)" \
+ "$(git for-each-ref --format="%(taggerdate:relative)" refs/tags)"
+'
test_expect_success 'Check format "short" date fields output' '
- f=short &&
- (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads &&
- git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual &&
- test_cmp expected actual
+ test_date short 2006-07-04 2006-07-04 2006-07-04
'
-cat >expected <<\EOF
-'refs/heads/master' 'Mon Jul 3 15:18:43 2006' 'Mon Jul 3 15:18:44 2006'
-'refs/tags/testtag' 'Mon Jul 3 15:18:45 2006'
-EOF
+test_expect_success 'Check format "short-local" date fields output' '
+ test_date short-local 2006-07-03 2006-07-03 2006-07-03
+'
test_expect_success 'Check format "local" date fields output' '
- f=local &&
- (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads &&
- git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual &&
- test_cmp expected actual
+ test_date local \
+ "Mon Jul 3 23:18:43 2006" \
+ "Mon Jul 3 23:18:44 2006" \
+ "Mon Jul 3 23:18:45 2006"
'
-cat >expected <<\EOF
-'refs/heads/master' '2006-07-03 17:18:43 +0200' '2006-07-03 17:18:44 +0200'
-'refs/tags/testtag' '2006-07-03 17:18:45 +0200'
-EOF
-
test_expect_success 'Check format "iso8601" date fields output' '
- f=iso8601 &&
- (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads &&
- git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual &&
- test_cmp expected actual
+ test_date iso8601 \
+ "2006-07-04 01:18:43 +0200" \
+ "2006-07-04 01:18:44 +0200" \
+ "2006-07-04 01:18:45 +0200"
'
-cat >expected <<\EOF
-'refs/heads/master' 'Mon, 3 Jul 2006 17:18:43 +0200' 'Mon, 3 Jul 2006 17:18:44 +0200'
-'refs/tags/testtag' 'Mon, 3 Jul 2006 17:18:45 +0200'
-EOF
+test_expect_success 'Check format "iso8601-local" date fields output' '
+ test_date iso8601-local "2006-07-03 23:18:43 +0000" "2006-07-03 23:18:44 +0000" "2006-07-03 23:18:45 +0000"
+'
test_expect_success 'Check format "rfc2822" date fields output' '
- f=rfc2822 &&
- (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads &&
- git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual &&
- test_cmp expected actual
+ test_date rfc2822 \
+ "Tue, 4 Jul 2006 01:18:43 +0200" \
+ "Tue, 4 Jul 2006 01:18:44 +0200" \
+ "Tue, 4 Jul 2006 01:18:45 +0200"
+'
+
+test_expect_success 'Check format "rfc2822-local" date fields output' '
+ test_date rfc2822-local "Mon, 3 Jul 2006 23:18:43 +0000" "Mon, 3 Jul 2006 23:18:44 +0000" "Mon, 3 Jul 2006 23:18:45 +0000"
+'
+
+test_expect_success 'Check format "raw" date fields output' '
+ test_date raw "1151968723 +0200" "1151968724 +0200" "1151968725 +0200"
+'
+
+test_expect_success 'Check format "raw-local" date fields output' '
+ test_date raw-local "1151968723 +0000" "1151968724 +0000" "1151968725 +0000"
'
test_expect_success 'Check format of strftime date fields' '
- echo "my date is 2006-07-03" >expected &&
+ echo "my date is 2006-07-04" >expected &&
git for-each-ref \
--format="%(authordate:format:my date is %Y-%m-%d)" \
refs/heads >actual &&
test_cmp expected actual
'
+test_expect_success 'Check format of strftime-local date fields' '
+ echo "my date is 2006-07-03" >expected &&
+ git for-each-ref \
+ --format="%(authordate:format-local:my date is %Y-%m-%d)" \
+ refs/heads >actual &&
+ test_cmp expected actual
+'
+
test_expect_success 'exercise strftime with odd fields' '
echo >expected &&
git for-each-ref --format="%(authordate:format:)" refs/heads >actual &&
@@ -546,8 +574,8 @@ body contents
$sig"
cat >expected <<EOF
-$(git rev-parse refs/tags/master) <committer@example.com> refs/tags/master
$(git rev-parse refs/tags/bogo) <committer@example.com> refs/tags/bogo
+$(git rev-parse refs/tags/master) <committer@example.com> refs/tags/master
EOF
test_expect_success 'Verify sort with multiple keys' '
diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh
new file mode 100755
index 0000000000..fe4796cc9c
--- /dev/null
+++ b/t/t6302-for-each-ref-filter.sh
@@ -0,0 +1,258 @@
+#!/bin/sh
+
+test_description='test for-each-refs usage of ref-filter APIs'
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-gpg.sh
+
+if ! test_have_prereq GPG
+then
+ skip_all="skipping for-each-ref tests, GPG not available"
+ test_done
+fi
+
+test_expect_success 'setup some history and refs' '
+ test_commit one &&
+ test_commit two &&
+ test_commit three &&
+ git checkout -b side &&
+ test_commit four &&
+ git tag -s -m "A signed tag message" signed-tag &&
+ git tag -s -m "Annonated doubly" double-tag signed-tag &&
+ git checkout master &&
+ git update-ref refs/odd/spot master
+'
+
+test_expect_success 'filtering with --points-at' '
+ cat >expect <<-\EOF &&
+ refs/heads/master
+ refs/odd/spot
+ refs/tags/three
+ EOF
+ git for-each-ref --format="%(refname)" --points-at=master >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'check signed tags with --points-at' '
+ sed -e "s/Z$//" >expect <<-\EOF &&
+ refs/heads/side Z
+ refs/tags/four Z
+ refs/tags/signed-tag four
+ EOF
+ git for-each-ref --format="%(refname) %(*subject)" --points-at=side >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'filtering with --merged' '
+ cat >expect <<-\EOF &&
+ refs/heads/master
+ refs/odd/spot
+ refs/tags/one
+ refs/tags/three
+ refs/tags/two
+ EOF
+ git for-each-ref --format="%(refname)" --merged=master >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'filtering with --no-merged' '
+ cat >expect <<-\EOF &&
+ refs/heads/side
+ refs/tags/double-tag
+ refs/tags/four
+ refs/tags/signed-tag
+ EOF
+ git for-each-ref --format="%(refname)" --no-merged=master >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'filtering with --contains' '
+ cat >expect <<-\EOF &&
+ refs/heads/master
+ refs/heads/side
+ refs/odd/spot
+ refs/tags/double-tag
+ refs/tags/four
+ refs/tags/signed-tag
+ refs/tags/three
+ refs/tags/two
+ EOF
+ git for-each-ref --format="%(refname)" --contains=two >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success '%(color) must fail' '
+ test_must_fail git for-each-ref --format="%(color)%(refname)"
+'
+
+test_expect_success 'left alignment is default' '
+ cat >expect <<-\EOF &&
+ refname is refs/heads/master |refs/heads/master
+ refname is refs/heads/side |refs/heads/side
+ refname is refs/odd/spot |refs/odd/spot
+ refname is refs/tags/double-tag|refs/tags/double-tag
+ refname is refs/tags/four |refs/tags/four
+ refname is refs/tags/one |refs/tags/one
+ refname is refs/tags/signed-tag|refs/tags/signed-tag
+ refname is refs/tags/three |refs/tags/three
+ refname is refs/tags/two |refs/tags/two
+ EOF
+ git for-each-ref --format="%(align:30)refname is %(refname)%(end)|%(refname)" >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'middle alignment' '
+ cat >expect <<-\EOF &&
+ | refname is refs/heads/master |refs/heads/master
+ | refname is refs/heads/side |refs/heads/side
+ | refname is refs/odd/spot |refs/odd/spot
+ |refname is refs/tags/double-tag|refs/tags/double-tag
+ | refname is refs/tags/four |refs/tags/four
+ | refname is refs/tags/one |refs/tags/one
+ |refname is refs/tags/signed-tag|refs/tags/signed-tag
+ | refname is refs/tags/three |refs/tags/three
+ | refname is refs/tags/two |refs/tags/two
+ EOF
+ git for-each-ref --format="|%(align:middle,30)refname is %(refname)%(end)|%(refname)" >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'right alignment' '
+ cat >expect <<-\EOF &&
+ | refname is refs/heads/master|refs/heads/master
+ | refname is refs/heads/side|refs/heads/side
+ | refname is refs/odd/spot|refs/odd/spot
+ |refname is refs/tags/double-tag|refs/tags/double-tag
+ | refname is refs/tags/four|refs/tags/four
+ | refname is refs/tags/one|refs/tags/one
+ |refname is refs/tags/signed-tag|refs/tags/signed-tag
+ | refname is refs/tags/three|refs/tags/three
+ | refname is refs/tags/two|refs/tags/two
+ EOF
+ git for-each-ref --format="|%(align:30,right)refname is %(refname)%(end)|%(refname)" >actual &&
+ test_cmp expect actual
+'
+
+# Individual atoms inside %(align:...) and %(end) must not be quoted.
+
+test_expect_success 'alignment with format quote' "
+ cat >expect <<-\EOF &&
+ |' '\''master| A U Thor'\'' '|
+ |' '\''side| A U Thor'\'' '|
+ |' '\''odd/spot| A U Thor'\'' '|
+ |' '\''double-tag| '\'' '|
+ |' '\''four| A U Thor'\'' '|
+ |' '\''one| A U Thor'\'' '|
+ |' '\''signed-tag| '\'' '|
+ |' '\''three| A U Thor'\'' '|
+ |' '\''two| A U Thor'\'' '|
+ EOF
+ git for-each-ref --shell --format=\"|%(align:30,middle)'%(refname:short)| %(authorname)'%(end)|\" >actual &&
+ test_cmp expect actual
+"
+
+test_expect_success 'nested alignment with quote formatting' "
+ cat >expect <<-\EOF &&
+ |' master '|
+ |' side '|
+ |' odd/spot '|
+ |' double-tag '|
+ |' four '|
+ |' one '|
+ |' signed-tag '|
+ |' three '|
+ |' two '|
+ EOF
+ git for-each-ref --shell --format='|%(align:30,left)%(align:15,right)%(refname:short)%(end)%(end)|' >actual &&
+ test_cmp expect actual
+"
+
+test_expect_success 'check `%(contents:lines=1)`' '
+ cat >expect <<-\EOF &&
+ master |three
+ side |four
+ odd/spot |three
+ double-tag |Annonated doubly
+ four |four
+ one |one
+ signed-tag |A signed tag message
+ three |three
+ two |two
+ EOF
+ git for-each-ref --format="%(refname:short) |%(contents:lines=1)" >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'check `%(contents:lines=0)`' '
+ cat >expect <<-\EOF &&
+ master |
+ side |
+ odd/spot |
+ double-tag |
+ four |
+ one |
+ signed-tag |
+ three |
+ two |
+ EOF
+ git for-each-ref --format="%(refname:short) |%(contents:lines=0)" >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'check `%(contents:lines=99999)`' '
+ cat >expect <<-\EOF &&
+ master |three
+ side |four
+ odd/spot |three
+ double-tag |Annonated doubly
+ four |four
+ one |one
+ signed-tag |A signed tag message
+ three |three
+ two |two
+ EOF
+ git for-each-ref --format="%(refname:short) |%(contents:lines=99999)" >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success '`%(contents:lines=-1)` should fail' '
+ test_must_fail git for-each-ref --format="%(refname:short) |%(contents:lines=-1)"
+'
+
+test_expect_success 'setup for version sort' '
+ test_commit foo1.3 &&
+ test_commit foo1.6 &&
+ test_commit foo1.10
+'
+
+test_expect_success 'version sort' '
+ git for-each-ref --sort=version:refname --format="%(refname:short)" refs/tags/ | grep "foo" >actual &&
+ cat >expect <<-\EOF &&
+ foo1.3
+ foo1.6
+ foo1.10
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'version sort (shortened)' '
+ git for-each-ref --sort=v:refname --format="%(refname:short)" refs/tags/ | grep "foo" >actual &&
+ cat >expect <<-\EOF &&
+ foo1.3
+ foo1.6
+ foo1.10
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'reverse version sort' '
+ git for-each-ref --sort=-version:refname --format="%(refname:short)" refs/tags/ | grep "foo" >actual &&
+ cat >expect <<-\EOF &&
+ foo1.10
+ foo1.6
+ foo1.3
+ EOF
+ test_cmp expect actual
+'
+
+test_done
diff --git a/t/t6500-gc.sh b/t/t6500-gc.sh
index 63194d819e..5d7d414617 100755
--- a/t/t6500-gc.sh
+++ b/t/t6500-gc.sh
@@ -30,4 +30,17 @@ test_expect_success 'gc -h with invalid configuration' '
test_i18ngrep "[Uu]sage" broken/usage
'
+test_expect_success 'gc is not aborted due to a stale symref' '
+ git init remote &&
+ (
+ cd remote &&
+ test_commit initial &&
+ git clone . ../client &&
+ git branch -m develop &&
+ cd ../client &&
+ git fetch --prune &&
+ git gc
+ )
+'
+
test_done
diff --git a/t/t7003-filter-branch.sh b/t/t7003-filter-branch.sh
index 855afda80a..377c648e04 100755
--- a/t/t7003-filter-branch.sh
+++ b/t/t7003-filter-branch.sh
@@ -2,6 +2,7 @@
test_description='git filter-branch'
. ./test-lib.sh
+. "$TEST_DIRECTORY/lib-gpg.sh"
test_expect_success 'setup' '
test_commit A &&
@@ -292,6 +293,19 @@ test_expect_success 'Tag name filtering strips gpg signature' '
test_cmp expect actual
'
+test_expect_success GPG 'Filtering retains message of gpg signed commit' '
+ mkdir gpg &&
+ touch gpg/foo &&
+ git add gpg &&
+ test_tick &&
+ git commit -S -m "Adding gpg" &&
+
+ git log -1 --format="%s" > expect &&
+ git filter-branch -f --msg-filter "cat" &&
+ git log -1 --format="%s" > actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'Tag name filtering allows slashes in tag names' '
git tag -m tag-with-slash X/1 &&
git cat-file tag X/1 | sed -e s,X/1,X/2, > expect &&
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index d31788cc6c..3dd2f51e49 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -1462,13 +1462,7 @@ test_expect_success 'invalid sort parameter on command line' '
test_expect_success 'invalid sort parameter in configuratoin' '
git config tag.sort "v:notvalid" &&
- git tag -l "foo*" >actual &&
- cat >expect <<-\EOF &&
- foo1.10
- foo1.3
- foo1.6
- EOF
- test_cmp expect actual
+ test_must_fail git tag -l "foo*"
'
test_expect_success 'version sort with prerelease reordering' '
@@ -1525,4 +1519,43 @@ EOF"
test_cmp expect actual
'
+test_expect_success '--format should list tags as per format given' '
+ cat >expect <<-\EOF &&
+ refname : refs/tags/foo1.10
+ refname : refs/tags/foo1.3
+ refname : refs/tags/foo1.6
+ refname : refs/tags/foo1.6-rc1
+ refname : refs/tags/foo1.6-rc2
+ EOF
+ git tag -l --format="refname : %(refname)" "foo*" >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'setup --merged test tags' '
+ git tag mergetest-1 HEAD~2 &&
+ git tag mergetest-2 HEAD~1 &&
+ git tag mergetest-3 HEAD
+'
+
+test_expect_success '--merged cannot be used in non-list mode' '
+ test_must_fail git tag --merged=mergetest-2 foo
+'
+
+test_expect_success '--merged shows merged tags' '
+ cat >expect <<-\EOF &&
+ mergetest-1
+ mergetest-2
+ EOF
+ git tag -l --merged=mergetest-2 mergetest-* >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success '--no-merged show unmerged tags' '
+ cat >expect <<-\EOF &&
+ mergetest-3
+ EOF
+ git tag -l --no-merged=mergetest-2 mergetest-* >actual &&
+ test_cmp expect actual
+'
+
test_done
diff --git a/t/t7410-submodule-checkout-to.sh b/t/t7410-submodule-checkout-to.sh
index 3f609e8909..1acef32647 100755
--- a/t/t7410-submodule-checkout-to.sh
+++ b/t/t7410-submodule-checkout-to.sh
@@ -47,4 +47,14 @@ test_expect_success 'checkout main and initialize independed clones' \
test_expect_success 'can see submodule diffs after independed cloning' \
'(cd fully_cloned_submodule/main && git diff --submodule master"^!" | grep "file1 updated")'
+test_expect_success 'checkout sub manually' \
+ 'mkdir linked_submodule &&
+ (cd clone/main &&
+ git worktree add "$base_path/linked_submodule/main" "$rev1_hash_main") &&
+ (cd clone/main/sub &&
+ git worktree add "$base_path/linked_submodule/main/sub" "$rev1_hash_sub")'
+
+test_expect_success 'can see submodule diffs after manual checkout of linked submodule' \
+ '(cd linked_submodule/main && git diff --submodule master"^!" | grep "file1 updated")'
+
test_done
diff --git a/t/t7610-mergetool.sh b/t/t7610-mergetool.sh
index 7eeb207b32..6f12b235b3 100755
--- a/t/t7610-mergetool.sh
+++ b/t/t7610-mergetool.sh
@@ -174,9 +174,9 @@ test_expect_success 'mergetool skips autoresolved' '
'
test_expect_success 'mergetool merges all from subdir' '
+ test_config rerere.enabled false &&
(
cd subdir &&
- test_config rerere.enabled false &&
test_must_fail git merge master &&
( yes "r" | git mergetool ../submod ) &&
( yes "d" "d" | git mergetool --no-prompt ) &&
diff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh
index ea35a0241c..48c6e2bc83 100755
--- a/t/t7800-difftool.sh
+++ b/t/t7800-difftool.sh
@@ -492,12 +492,12 @@ test_expect_success PERL 'difftool --no-symlinks detects conflict ' '
test_expect_success PERL 'difftool properly honors gitlink and core.worktree' '
git submodule add ./. submod/ule &&
+ test_config -C submod/ule diff.tool checktrees &&
+ test_config -C submod/ule difftool.checktrees.cmd '\''
+ test -d "$LOCAL" && test -d "$REMOTE" && echo good
+ '\'' &&
(
cd submod/ule &&
- test_config diff.tool checktrees &&
- test_config difftool.checktrees.cmd '\''
- test -d "$LOCAL" && test -d "$REMOTE" && echo good
- '\'' &&
echo good >expect &&
git difftool --tool=checktrees --dir-diff HEAD~ >actual &&
test_cmp expect actual
diff --git a/t/t9811-git-p4-label-import.sh b/t/t9811-git-p4-label-import.sh
index 095238fffe..decb66ba30 100755
--- a/t/t9811-git-p4-label-import.sh
+++ b/t/t9811-git-p4-label-import.sh
@@ -214,6 +214,51 @@ test_expect_success 'use git config to enable import/export of tags' '
)
'
+p4_head_revision() {
+ p4 changes -m 1 "$@" | awk '{print $2}'
+}
+
+# Importing a label that references a P4 commit that
+# has not been seen. The presence of a label on a commit
+# we haven't seen should not cause git-p4 to fail. It should
+# merely skip that label, and still import other labels.
+test_expect_success 'importing labels with missing revisions' '
+ test_when_finished cleanup_git &&
+ (
+ rm -fr "$cli" "$git" &&
+ mkdir "$cli" &&
+ P4CLIENT=missing-revision &&
+ client_view "//depot/missing-revision/... //missing-revision/..." &&
+ cd "$cli" &&
+ >f1 && p4 add f1 && p4 submit -d "start" &&
+
+ p4 tag -l TAG_S0 ... &&
+
+ >f2 && p4 add f2 && p4 submit -d "second" &&
+
+ startrev=$(p4_head_revision //depot/missing-revision/...) &&
+
+ >f3 && p4 add f3 && p4 submit -d "third" &&
+
+ p4 edit f2 && date >f2 && p4 submit -d "change" f2 &&
+
+ endrev=$(p4_head_revision //depot/missing-revision/...) &&
+
+ p4 tag -l TAG_S1 ... &&
+
+ # we should skip TAG_S0 since it is before our startpoint,
+ # but pick up TAG_S1.
+
+ git p4 clone --dest="$git" --import-labels -v \
+ //depot/missing-revision/...@$startrev,$endrev &&
+ (
+ cd "$git" &&
+ git rev-parse TAG_S1 &&
+ ! git rev-parse TAG_S0
+ )
+ )
+'
+
test_expect_success 'kill p4d' '
kill_p4d
diff --git a/t/t9822-git-p4-path-encoding.sh b/t/t9822-git-p4-path-encoding.sh
new file mode 100755
index 0000000000..7b83e696a9
--- /dev/null
+++ b/t/t9822-git-p4-path-encoding.sh
@@ -0,0 +1,58 @@
+#!/bin/sh
+
+test_description='Clone repositories with non ASCII paths'
+
+. ./lib-git-p4.sh
+
+UTF8_ESCAPED="a-\303\244_o-\303\266_u-\303\274.txt"
+ISO8859_ESCAPED="a-\344_o-\366_u-\374.txt"
+
+test_expect_success 'start p4d' '
+ start_p4d
+'
+
+test_expect_success 'Create a repo containing iso8859-1 encoded paths' '
+ (
+ cd "$cli" &&
+ ISO8859="$(printf "$ISO8859_ESCAPED")" &&
+ echo content123 >"$ISO8859" &&
+ p4 add "$ISO8859" &&
+ p4 submit -d "test commit"
+ )
+'
+
+test_expect_failure 'Clone auto-detects depot with iso8859-1 paths' '
+ git p4 clone --destination="$git" //depot &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ UTF8="$(printf "$UTF8_ESCAPED")" &&
+ echo "$UTF8" >expect &&
+ git -c core.quotepath=false ls-files >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'Clone repo containing iso8859-1 encoded paths with git-p4.pathEncoding' '
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init . &&
+ git config git-p4.pathEncoding iso8859-1 &&
+ git p4 clone --use-client-spec --destination="$git" //depot &&
+ UTF8="$(printf "$UTF8_ESCAPED")" &&
+ echo "$UTF8" >expect &&
+ git -c core.quotepath=false ls-files >actual &&
+ test_cmp expect actual &&
+
+ echo content123 >expect &&
+ cat "$UTF8" >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'kill p4d' '
+ kill_p4d
+'
+
+test_done
diff --git a/t/t9823-git-p4-mock-lfs.sh b/t/t9823-git-p4-mock-lfs.sh
new file mode 100755
index 0000000000..1f2dc369bf
--- /dev/null
+++ b/t/t9823-git-p4-mock-lfs.sh
@@ -0,0 +1,192 @@
+#!/bin/sh
+
+test_description='Clone repositories and store files in Mock LFS'
+
+. ./lib-git-p4.sh
+
+test_file_is_not_in_mock_lfs () {
+ FILE="$1" &&
+ CONTENT="$2" &&
+ echo "$CONTENT" >expect_content &&
+ test_path_is_file "$FILE" &&
+ test_cmp expect_content "$FILE"
+}
+
+test_file_is_in_mock_lfs () {
+ FILE="$1" &&
+ CONTENT="$2" &&
+ LOCAL_STORAGE=".git/mock-storage/local/$CONTENT" &&
+ SERVER_STORAGE=".git/mock-storage/remote/$CONTENT" &&
+ echo "pointer-$CONTENT" >expect_pointer &&
+ echo "$CONTENT" >expect_content &&
+ test_path_is_file "$FILE" &&
+ test_path_is_file "$LOCAL_STORAGE" &&
+ test_path_is_file "$SERVER_STORAGE" &&
+ test_cmp expect_pointer "$FILE" &&
+ test_cmp expect_content "$LOCAL_STORAGE" &&
+ test_cmp expect_content "$SERVER_STORAGE"
+}
+
+test_file_is_deleted_in_mock_lfs () {
+ FILE="$1" &&
+ CONTENT="$2" &&
+ LOCAL_STORAGE=".git/mock-storage/local/$CONTENT" &&
+ SERVER_STORAGE=".git/mock-storage/remote/$CONTENT" &&
+ echo "pointer-$CONTENT" >expect_pointer &&
+ echo "$CONTENT" >expect_content &&
+ test_path_is_missing "$FILE" &&
+ test_path_is_file "$LOCAL_STORAGE" &&
+ test_path_is_file "$SERVER_STORAGE" &&
+ test_cmp expect_content "$LOCAL_STORAGE" &&
+ test_cmp expect_content "$SERVER_STORAGE"
+}
+
+test_file_count_in_dir () {
+ DIR="$1" &&
+ EXPECTED_COUNT="$2" &&
+ find "$DIR" -type f >actual &&
+ test_line_count = $EXPECTED_COUNT actual
+}
+
+test_expect_success 'start p4d' '
+ start_p4d
+'
+
+test_expect_success 'Create repo with binary files' '
+ client_view "//depot/... //client/..." &&
+ (
+ cd "$cli" &&
+
+ echo "content 1 txt 23 bytes" >file1.txt &&
+ p4 add file1.txt &&
+ echo "content 2-3 bin 25 bytes" >file2.dat &&
+ p4 add file2.dat &&
+ p4 submit -d "Add text and binary file" &&
+
+ mkdir "path with spaces" &&
+ echo "content 2-3 bin 25 bytes" >"path with spaces/file3.bin" &&
+ p4 add "path with spaces/file3.bin" &&
+ p4 submit -d "Add another binary file with same content and spaces in path" &&
+
+ echo "content 4 bin 26 bytes XX" >file4.bin &&
+ p4 add file4.bin &&
+ p4 submit -d "Add another binary file with different content"
+ )
+'
+
+test_expect_success 'Store files in Mock LFS based on size (>24 bytes)' '
+ client_view "//depot/... //client/..." &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init . &&
+ git config git-p4.useClientSpec true &&
+ git config git-p4.largeFileSystem MockLFS &&
+ git config git-p4.largeFileThreshold 24 &&
+ git config git-p4.largeFilePush True &&
+ git p4 clone --destination="$git" //depot@all &&
+
+ test_file_is_not_in_mock_lfs file1.txt "content 1 txt 23 bytes" &&
+ test_file_is_in_mock_lfs file2.dat "content 2-3 bin 25 bytes" &&
+ test_file_is_in_mock_lfs "path with spaces/file3.bin" "content 2-3 bin 25 bytes" &&
+ test_file_is_in_mock_lfs file4.bin "content 4 bin 26 bytes XX" &&
+
+ test_file_count_in_dir ".git/mock-storage/local" 2 &&
+ test_file_count_in_dir ".git/mock-storage/remote" 2
+ )
+'
+
+test_expect_success 'Store files in Mock LFS based on extension (dat)' '
+ client_view "//depot/... //client/..." &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init . &&
+ git config git-p4.useClientSpec true &&
+ git config git-p4.largeFileSystem MockLFS &&
+ git config git-p4.largeFileExtensions dat &&
+ git config git-p4.largeFilePush True &&
+ git p4 clone --destination="$git" //depot@all &&
+
+ test_file_is_not_in_mock_lfs file1.txt "content 1 txt 23 bytes" &&
+ test_file_is_in_mock_lfs file2.dat "content 2-3 bin 25 bytes" &&
+ test_file_is_not_in_mock_lfs "path with spaces/file3.bin" "content 2-3 bin 25 bytes" &&
+ test_file_is_not_in_mock_lfs file4.bin "content 4 bin 26 bytes XX" &&
+
+ test_file_count_in_dir ".git/mock-storage/local" 1 &&
+ test_file_count_in_dir ".git/mock-storage/remote" 1
+ )
+'
+
+test_expect_success 'Store files in Mock LFS based on extension (dat) and use git p4 sync and no client spec' '
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init &&
+ git config git-p4.useClientSpec true &&
+ git config git-p4.largeFileSystem MockLFS &&
+ git config git-p4.largeFileExtensions dat &&
+ git config git-p4.largeFilePush True &&
+ git p4 sync //depot &&
+ git checkout p4/master &&
+
+ test_file_is_not_in_mock_lfs file1.txt "content 1 txt 23 bytes" &&
+ test_file_is_in_mock_lfs file2.dat "content 2-3 bin 25 bytes" &&
+ test_file_is_not_in_mock_lfs "path with spaces/file3.bin" "content 2-3 bin 25 bytes" &&
+ test_file_is_not_in_mock_lfs file4.bin "content 4 bin 26 bytes XX" &&
+
+ test_file_count_in_dir ".git/mock-storage/local" 1 &&
+ test_file_count_in_dir ".git/mock-storage/remote" 1
+ )
+'
+
+test_expect_success 'Remove file from repo and store files in Mock LFS based on size (>24 bytes)' '
+ client_view "//depot/... //client/..." &&
+ (
+ cd "$cli" &&
+ p4 delete file4.bin &&
+ p4 submit -d "Remove file"
+ ) &&
+
+ client_view "//depot/... //client/..." &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init . &&
+ git config git-p4.useClientSpec true &&
+ git config git-p4.largeFileSystem MockLFS &&
+ git config git-p4.largeFileThreshold 24 &&
+ git config git-p4.largeFilePush True &&
+ git p4 clone --destination="$git" //depot@all &&
+
+ test_file_is_not_in_mock_lfs file1.txt "content 1 txt 23 bytes" &&
+ test_file_is_in_mock_lfs file2.dat "content 2-3 bin 25 bytes" &&
+ test_file_is_in_mock_lfs "path with spaces/file3.bin" "content 2-3 bin 25 bytes" &&
+ test_file_is_deleted_in_mock_lfs file4.bin "content 4 bin 26 bytes XX" &&
+
+ test_file_count_in_dir ".git/mock-storage/local" 2 &&
+ test_file_count_in_dir ".git/mock-storage/remote" 2
+ )
+'
+
+test_expect_success 'Run git p4 submit in repo configured with large file system' '
+ client_view "//depot/... //client/..." &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init . &&
+ git config git-p4.useClientSpec true &&
+ git config git-p4.largeFileSystem MockLFS &&
+ git config git-p4.largeFileThreshold 24 &&
+ git config git-p4.largeFilePush True &&
+ git p4 clone --destination="$git" //depot@all &&
+
+ test_must_fail git p4 submit
+ )
+'
+
+test_expect_success 'kill p4d' '
+ kill_p4d
+'
+
+test_done
diff --git a/t/t9824-git-p4-git-lfs.sh b/t/t9824-git-p4-git-lfs.sh
new file mode 100755
index 0000000000..0b664a377c
--- /dev/null
+++ b/t/t9824-git-p4-git-lfs.sh
@@ -0,0 +1,288 @@
+#!/bin/sh
+
+test_description='Clone repositories and store files in Git LFS'
+
+. ./lib-git-p4.sh
+
+git lfs help >/dev/null 2>&1 || {
+ skip_all='skipping git p4 Git LFS tests; Git LFS not found'
+ test_done
+}
+
+test_file_in_lfs () {
+ FILE="$1" &&
+ SIZE="$2" &&
+ EXPECTED_CONTENT="$3" &&
+ cat "$FILE" | grep "size $SIZE" &&
+ HASH=$(cat "$FILE" | grep "oid sha256:" | sed -e "s/oid sha256://g") &&
+ LFS_FILE=".git/lfs/objects/$(echo "$HASH" | cut -c1-2)/$(echo "$HASH" | cut -c3-4)/$HASH" &&
+ echo $EXPECTED_CONTENT >expect &&
+ test_path_is_file "$FILE" &&
+ test_path_is_file "$LFS_FILE" &&
+ test_cmp expect "$LFS_FILE"
+}
+
+test_file_count_in_dir () {
+ DIR="$1" &&
+ EXPECTED_COUNT="$2" &&
+ find "$DIR" -type f >actual &&
+ test_line_count = $EXPECTED_COUNT actual
+}
+
+test_expect_success 'start p4d' '
+ start_p4d
+'
+
+test_expect_success 'Create repo with binary files' '
+ client_view "//depot/... //client/..." &&
+ (
+ cd "$cli" &&
+
+ echo "content 1 txt 23 bytes" >file1.txt &&
+ p4 add file1.txt &&
+ echo "content 2-3 bin 25 bytes" >file2.dat &&
+ p4 add file2.dat &&
+ p4 submit -d "Add text and binary file" &&
+
+ mkdir "path with spaces" &&
+ echo "content 2-3 bin 25 bytes" >"path with spaces/file3.bin" &&
+ p4 add "path with spaces/file3.bin" &&
+ p4 submit -d "Add another binary file with same content and spaces in path" &&
+
+ echo "content 4 bin 26 bytes XX" >file4.bin &&
+ p4 add file4.bin &&
+ p4 submit -d "Add another binary file with different content"
+ )
+'
+
+test_expect_success 'Store files in LFS based on size (>24 bytes)' '
+ client_view "//depot/... //client/..." &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init . &&
+ git config git-p4.useClientSpec true &&
+ git config git-p4.largeFileSystem GitLFS &&
+ git config git-p4.largeFileThreshold 24 &&
+ git p4 clone --destination="$git" //depot@all &&
+
+ test_file_in_lfs file2.dat 25 "content 2-3 bin 25 bytes" &&
+ test_file_in_lfs "path with spaces/file3.bin" 25 "content 2-3 bin 25 bytes" &&
+ test_file_in_lfs file4.bin 26 "content 4 bin 26 bytes XX" &&
+
+ test_file_count_in_dir ".git/lfs/objects" 2 &&
+
+ cat >expect <<-\EOF &&
+
+ #
+ # Git LFS (see https://git-lfs.github.com/)
+ #
+ /file2.dat filter=lfs -text
+ /file4.bin filter=lfs -text
+ /path[[:space:]]with[[:space:]]spaces/file3.bin filter=lfs -text
+ EOF
+ test_path_is_file .gitattributes &&
+ test_cmp expect .gitattributes
+ )
+'
+
+test_expect_success 'Store files in LFS based on size (>25 bytes)' '
+ client_view "//depot/... //client/..." &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init . &&
+ git config git-p4.useClientSpec true &&
+ git config git-p4.largeFileSystem GitLFS &&
+ git config git-p4.largeFileThreshold 25 &&
+ git p4 clone --destination="$git" //depot@all &&
+
+ test_file_in_lfs file4.bin 26 "content 4 bin 26 bytes XX" &&
+ test_file_count_in_dir ".git/lfs/objects" 1 &&
+
+ cat >expect <<-\EOF &&
+
+ #
+ # Git LFS (see https://git-lfs.github.com/)
+ #
+ /file4.bin filter=lfs -text
+ EOF
+ test_path_is_file .gitattributes &&
+ test_cmp expect .gitattributes
+ )
+'
+
+test_expect_success 'Store files in LFS based on extension (dat)' '
+ client_view "//depot/... //client/..." &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init . &&
+ git config git-p4.useClientSpec true &&
+ git config git-p4.largeFileSystem GitLFS &&
+ git config git-p4.largeFileExtensions dat &&
+ git p4 clone --destination="$git" //depot@all &&
+
+ test_file_in_lfs file2.dat 25 "content 2-3 bin 25 bytes" &&
+ test_file_count_in_dir ".git/lfs/objects" 1 &&
+
+ cat >expect <<-\EOF &&
+
+ #
+ # Git LFS (see https://git-lfs.github.com/)
+ #
+ *.dat filter=lfs -text
+ EOF
+ test_path_is_file .gitattributes &&
+ test_cmp expect .gitattributes
+ )
+'
+
+test_expect_success 'Store files in LFS based on size (>25 bytes) and extension (dat)' '
+ client_view "//depot/... //client/..." &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init . &&
+ git config git-p4.useClientSpec true &&
+ git config git-p4.largeFileSystem GitLFS &&
+ git config git-p4.largeFileExtensions dat &&
+ git config git-p4.largeFileThreshold 25 &&
+ git p4 clone --destination="$git" //depot@all &&
+
+ test_file_in_lfs file2.dat 25 "content 2-3 bin 25 bytes" &&
+ test_file_in_lfs file4.bin 26 "content 4 bin 26 bytes XX" &&
+ test_file_count_in_dir ".git/lfs/objects" 2 &&
+
+ cat >expect <<-\EOF &&
+
+ #
+ # Git LFS (see https://git-lfs.github.com/)
+ #
+ *.dat filter=lfs -text
+ /file4.bin filter=lfs -text
+ EOF
+ test_path_is_file .gitattributes &&
+ test_cmp expect .gitattributes
+ )
+'
+
+test_expect_success 'Remove file from repo and store files in LFS based on size (>24 bytes)' '
+ client_view "//depot/... //client/..." &&
+ (
+ cd "$cli" &&
+ p4 delete file4.bin &&
+ p4 submit -d "Remove file"
+ ) &&
+
+ client_view "//depot/... //client/..." &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init . &&
+ git config git-p4.useClientSpec true &&
+ git config git-p4.largeFileSystem GitLFS &&
+ git config git-p4.largeFileThreshold 24 &&
+ git p4 clone --destination="$git" //depot@all &&
+
+ test_file_in_lfs file2.dat 25 "content 2-3 bin 25 bytes" &&
+ test_file_in_lfs "path with spaces/file3.bin" 25 "content 2-3 bin 25 bytes" &&
+ test_path_is_missing file4.bin &&
+ test_file_count_in_dir ".git/lfs/objects" 2 &&
+
+ cat >expect <<-\EOF &&
+
+ #
+ # Git LFS (see https://git-lfs.github.com/)
+ #
+ /file2.dat filter=lfs -text
+ /path[[:space:]]with[[:space:]]spaces/file3.bin filter=lfs -text
+ EOF
+ test_path_is_file .gitattributes &&
+ test_cmp expect .gitattributes
+ )
+'
+
+test_expect_success 'Add .gitattributes and store files in LFS based on size (>24 bytes)' '
+ client_view "//depot/... //client/..." &&
+ (
+ cd "$cli" &&
+ echo "*.txt text" >.gitattributes &&
+ p4 add .gitattributes &&
+ p4 submit -d "Add .gitattributes"
+ ) &&
+
+ client_view "//depot/... //client/..." &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init . &&
+ git config git-p4.useClientSpec true &&
+ git config git-p4.largeFileSystem GitLFS &&
+ git config git-p4.largeFileThreshold 24 &&
+ git p4 clone --destination="$git" //depot@all &&
+
+ test_file_in_lfs file2.dat 25 "content 2-3 bin 25 bytes" &&
+ test_file_in_lfs "path with spaces/file3.bin" 25 "content 2-3 bin 25 bytes" &&
+ test_path_is_missing file4.bin &&
+ test_file_count_in_dir ".git/lfs/objects" 2 &&
+
+ cat >expect <<-\EOF &&
+ *.txt text
+
+ #
+ # Git LFS (see https://git-lfs.github.com/)
+ #
+ /file2.dat filter=lfs -text
+ /path[[:space:]]with[[:space:]]spaces/file3.bin filter=lfs -text
+ EOF
+ test_path_is_file .gitattributes &&
+ test_cmp expect .gitattributes
+ )
+'
+
+test_expect_success 'Add big files to repo and store files in LFS based on compressed size (>28 bytes)' '
+ client_view "//depot/... //client/..." &&
+ (
+ cd "$cli" &&
+ echo "content 5 bin 40 bytes XXXXXXXXXXXXXXXX" >file5.bin &&
+ p4 add file5.bin &&
+ p4 submit -d "Add file with small footprint after compression" &&
+
+ echo "content 6 bin 39 bytes XXXXXYYYYYZZZZZ" >file6.bin &&
+ p4 add file6.bin &&
+ p4 submit -d "Add file with large footprint after compression"
+ ) &&
+
+ client_view "//depot/... //client/..." &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ git init . &&
+ git config git-p4.useClientSpec true &&
+ git config git-p4.largeFileSystem GitLFS &&
+ git config git-p4.largeFileCompressedThreshold 28 &&
+ # We only import HEAD here ("@all" is missing!)
+ git p4 clone --destination="$git" //depot &&
+
+ test_file_in_lfs file6.bin 13 "content 6 bin 39 bytes XXXXXYYYYYZZZZZ"
+ test_file_count_in_dir ".git/lfs/objects" 1 &&
+
+ cat >expect <<-\EOF &&
+ *.txt text
+
+ #
+ # Git LFS (see https://git-lfs.github.com/)
+ #
+ /file6.bin filter=lfs -text
+ EOF
+ test_path_is_file .gitattributes &&
+ test_cmp expect .gitattributes
+ )
+'
+
+test_expect_success 'kill p4d' '
+ kill_p4d
+'
+
+test_done
diff --git a/t/t9825-git-p4-handle-utf16-without-bom.sh b/t/t9825-git-p4-handle-utf16-without-bom.sh
new file mode 100755
index 0000000000..1551845dc1
--- /dev/null
+++ b/t/t9825-git-p4-handle-utf16-without-bom.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+test_description='git p4 handling of UTF-16 files without BOM'
+
+. ./lib-git-p4.sh
+
+UTF16="\227\000\227\000"
+
+test_expect_success 'start p4d' '
+ start_p4d
+'
+
+test_expect_success 'init depot with UTF-16 encoded file and artificially remove BOM' '
+ (
+ cd "$cli" &&
+ printf "$UTF16" >file1 &&
+ p4 add -t utf16 file1 &&
+ p4 submit -d "file1"
+ ) &&
+
+ (
+ cd db &&
+ p4d -jc &&
+ # P4D automatically adds a BOM. Remove it here to make the file invalid.
+ sed -e "\$d" depot/file1,v >depot/file1,v.new &&
+ mv depot/file1,v.new depot/file1,v &&
+ printf "@$UTF16@" >>depot/file1,v &&
+ p4d -jrF checkpoint.1
+ )
+'
+
+test_expect_success 'clone depot with invalid UTF-16 file in verbose mode' '
+ git p4 clone --dest="$git" --verbose //depot &&
+ test_when_finished cleanup_git &&
+ (
+ cd "$git" &&
+ printf "$UTF16" >expect &&
+ test_cmp_bin expect file1
+ )
+'
+
+test_expect_failure 'clone depot with invalid UTF-16 file in non-verbose mode' '
+ git p4 clone --dest="$git" //depot
+'
+
+test_expect_success 'kill p4d' '
+ kill_p4d
+'
+
+test_done
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index e8d3c0fdbc..6dffb8bcde 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -201,7 +201,14 @@ test_chmod () {
# Unset a configuration variable, but don't fail if it doesn't exist.
test_unconfig () {
- git config --unset-all "$@"
+ config_dir=
+ if test "$1" = -C
+ then
+ shift
+ config_dir=$1
+ shift
+ fi
+ git ${config_dir:+-C "$config_dir"} config --unset-all "$@"
config_status=$?
case "$config_status" in
5) # ok, nothing to unset
@@ -213,8 +220,15 @@ test_unconfig () {
# Set git config, automatically unsetting it after the test is over.
test_config () {
- test_when_finished "test_unconfig '$1'" &&
- git config "$@"
+ config_dir=
+ if test "$1" = -C
+ then
+ shift
+ config_dir=$1
+ shift
+ fi
+ test_when_finished "test_unconfig ${config_dir:+-C '$config_dir'} '$1'" &&
+ git ${config_dir:+-C "$config_dir"} config "$@"
}
test_config_global () {
@@ -722,6 +736,11 @@ test_seq () {
# what went wrong.
test_when_finished () {
+ # We cannot detect when we are in a subshell in general, but by
+ # doing so on Bash is better than nothing (the test will
+ # silently pass on other shells).
+ test "${BASH_SUBSHELL-0}" = 0 ||
+ error "bug in test script: test_when_finished does nothing in a subshell"
test_cleanup="{ $*
} && (exit \"\$eval_ret\"); eval_ret=\$?; $test_cleanup"
}
diff --git a/test-path-utils.c b/test-path-utils.c
index 3dd3744a57..c67bf65b34 100644
--- a/test-path-utils.c
+++ b/test-path-utils.c
@@ -21,8 +21,6 @@ static int normalize_ceiling_entry(struct string_list_item *item, void *unused)
if (normalize_path_copy(buf, ceil) < 0)
die("Path \"%s\" could not be normalized", ceil);
len = strlen(buf);
- if (len > 1 && buf[len-1] == '/')
- die("Normalized path \"%s\" ended with slash", buf);
free(item->string);
item->string = xstrdup(buf);
return 1;
diff --git a/utf8.c b/utf8.c
index 28e6d76a42..00e10c86ad 100644
--- a/utf8.c
+++ b/utf8.c
@@ -644,3 +644,24 @@ int skip_utf8_bom(char **text, size_t len)
*text += strlen(utf8_bom);
return 1;
}
+
+void strbuf_utf8_align(struct strbuf *buf, align_type position, unsigned int width,
+ const char *s)
+{
+ int slen = strlen(s);
+ int display_len = utf8_strnwidth(s, slen, 0);
+ int utf8_compensation = slen - display_len;
+
+ if (display_len >= width) {
+ strbuf_addstr(buf, s);
+ return;
+ }
+
+ if (position == ALIGN_LEFT)
+ strbuf_addf(buf, "%-*s", width + utf8_compensation, s);
+ else if (position == ALIGN_MIDDLE) {
+ int left = (width - display_len) / 2;
+ strbuf_addf(buf, "%*s%-*s", left, "", width - left + utf8_compensation, s);
+ } else if (position == ALIGN_RIGHT)
+ strbuf_addf(buf, "%*s", width + utf8_compensation, s);
+}
diff --git a/utf8.h b/utf8.h
index 5a9e94bee6..7930b44f19 100644
--- a/utf8.h
+++ b/utf8.h
@@ -55,4 +55,19 @@ int mbs_chrlen(const char **text, size_t *remainder_p, const char *encoding);
*/
int is_hfs_dotgit(const char *path);
+typedef enum {
+ ALIGN_LEFT,
+ ALIGN_MIDDLE,
+ ALIGN_RIGHT
+} align_type;
+
+/*
+ * Align the string given and store it into a strbuf as per the
+ * 'position' and 'width'. If the given string length is larger than
+ * 'width' than then the input string is not truncated and no
+ * alignment is done.
+ */
+void strbuf_utf8_align(struct strbuf *buf, align_type position, unsigned int width,
+ const char *s);
+
#endif
diff --git a/wt-status.c b/wt-status.c
index c327fe8128..3e3b8c0989 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -1319,6 +1319,12 @@ static int grab_1st_switch(unsigned char *osha1, unsigned char *nsha1,
hashcpy(cb->nsha1, nsha1);
for (end = target; *end && *end != '\n'; end++)
;
+ if (!memcmp(target, "HEAD", end - target)) {
+ /* HEAD is relative. Resolve it to the right reflog entry. */
+ strbuf_addstr(&cb->buf,
+ find_unique_abbrev(nsha1, DEFAULT_ABBREV));
+ return 1;
+ }
strbuf_add(&cb->buf, target, end - target);
return 1;
}