Merge branch 'rs/ref-transaction'
authorJunio C Hamano <gitster@pobox.com>
Tue, 21 Oct 2014 20:28:10 +0000 (13:28 -0700)
committerJunio C Hamano <gitster@pobox.com>
Tue, 21 Oct 2014 20:28:10 +0000 (13:28 -0700)
The API to update refs have been restructured to allow introducing
a true transactional updates later. We would even allow storing
refs in backends other than the traditional filesystem-based one.

* rs/ref-transaction: (25 commits)
ref_transaction_commit: bail out on failure to remove a ref
lockfile: remove unable_to_lock_error
refs.c: do not permit err == NULL
remote rm/prune: print a message when writing packed-refs fails
for-each-ref: skip and warn about broken ref names
refs.c: allow listing and deleting badly named refs
test: put tests for handling of bad ref names in one place
packed-ref cache: forbid dot-components in refnames
branch -d: simplify by using RESOLVE_REF_READING
branch -d: avoid repeated symref resolution
reflog test: test interaction with detached HEAD
refs.c: change resolve_ref_unsafe reading argument to be a flags field
refs.c: make write_ref_sha1 static
fetch.c: change s_update_ref to use a ref transaction
refs.c: ref_transaction_commit: distinguish name conflicts from other errors
refs.c: pass a list of names to skip to is_refname_available
refs.c: call lock_ref_sha1_basic directly from commit
refs.c: refuse to lock badly named refs in lock_ref_sha1_basic
rename_ref: don't ask read_ref_full where the ref came from
refs.c: pass the ref log message to _create/delete/update instead of _commit
...

56 files changed:
.gitignore
Documentation/Makefile
Documentation/RelNotes/2.2.0.txt
Documentation/everyday.txt [deleted file]
Documentation/everyday.txto [new file with mode: 0644]
Documentation/git-imap-send.txt
Documentation/git-interpret-trailers.txt [new file with mode: 0644]
Documentation/git-prune-packed.txt
Documentation/git-push.txt
Documentation/git-quiltimport.txt
Documentation/git-stage.txt
Documentation/git.txt
Documentation/gitcore-tutorial.txt
Documentation/gitcvs-migration.txt
Documentation/giteveryday.txt [new file with mode: 0644]
Documentation/gitglossary.txt
Documentation/gittutorial-2.txt
Documentation/gittutorial.txt
Makefile
README
archive-tar.c
builtin.h
builtin/branch.c
builtin/clean.c
builtin/commit.c
builtin/config.c
builtin/for-each-ref.c
builtin/help.c
builtin/interpret-trailers.c [new file with mode: 0644]
builtin/log.c
builtin/receive-pack.c
color.c
color.h
command-list.txt
contrib/completion/git-completion.bash
contrib/contacts/.gitignore [new file with mode: 0644]
contrib/contacts/Makefile [new file with mode: 0644]
contrib/subtree/.gitignore
contrib/subtree/Makefile
diff.c
git.c
gitweb/gitweb.perl
grep.c
log-tree.c
log-tree.h
pretty.c
t/README
t/t1308-config-set.sh
t/t5004-archive-corner-cases.sh
t/t5304-prune.sh
t/t7513-interpret-trailers.sh [new file with mode: 0755]
t/test-lib-functions.sh
t/test-lib.sh
trace.c
trailer.c [new file with mode: 0644]
trailer.h [new file with mode: 0644]
index 9ec40fa9fbc645532c9bc36b65521e6771d14324..a05241916c9c9a3760a6e98670a7f6427d553d77 100644 (file)
@@ -74,6 +74,7 @@
 /git-index-pack
 /git-init
 /git-init-db
+/git-interpret-trailers
 /git-instaweb
 /git-log
 /git-ls-files
index cea0e7ae3db37dcd366e094abdd2a2f32514a247..8d0f70938e6d6f04c9edfb9a3ec8ef45d38502c5 100644 (file)
@@ -5,6 +5,7 @@ MAN7_TXT =
 TECH_DOCS =
 ARTICLES =
 SP_ARTICLES =
+OBSOLETE_HTML =
 
 MAN1_TXT += $(filter-out \
                $(addsuffix .txt, $(ARTICLES) $(SP_ARTICLES)), \
@@ -26,6 +27,7 @@ MAN7_TXT += gitcore-tutorial.txt
 MAN7_TXT += gitcredentials.txt
 MAN7_TXT += gitcvs-migration.txt
 MAN7_TXT += gitdiffcore.txt
+MAN7_TXT += giteveryday.txt
 MAN7_TXT += gitglossary.txt
 MAN7_TXT += gitnamespaces.txt
 MAN7_TXT += gitrevisions.txt
@@ -37,11 +39,11 @@ MAN_TXT = $(MAN1_TXT) $(MAN5_TXT) $(MAN7_TXT)
 MAN_XML = $(patsubst %.txt,%.xml,$(MAN_TXT))
 MAN_HTML = $(patsubst %.txt,%.html,$(MAN_TXT))
 
-OBSOLETE_HTML = git-remote-helpers.html
+OBSOLETE_HTML += everyday.html
+OBSOLETE_HTML += git-remote-helpers.html
 DOC_HTML = $(MAN_HTML) $(OBSOLETE_HTML)
 
 ARTICLES += howto-index
-ARTICLES += everyday
 ARTICLES += git-tools
 ARTICLES += git-bisect-lk2009
 # with their own formatting rules.
index 95891765a139ffe6e2bd8fa524c61f2018dcb432..af876e2ce998762071cbddf2adbb845eb8d90f37 100644 (file)
@@ -58,6 +58,14 @@ UI, Workflows & Features
    public repository really point the commits the pusher wanted to,
    without having to "trust" the server.
 
+ * "git interpret-trailers" is a new filter to programatically edit
+    the tail end of the commit log messages.
+
+ * "git help everyday" shows the "Everyday Git in 20 commands or so"
+   document, whose contents have been updated to more modern Git
+   practice.
+
+
 Performance, Internal Implementation, etc.
 
  * The API to manipulate the "refs" is currently undergoing a revamp
@@ -124,6 +132,10 @@ Performance, Internal Implementation, etc.
    original before feeding the filter.  Instead, stream the file
    contents directly to the filter and process its output.
 
+ * The scripts in the test suite can be run with "-x" option to show
+   a shell-trace of each command run in them.
+
+
 Also contains various documentation updates and code clean-ups.
 
 
@@ -138,11 +150,6 @@ notes for details).
    mean the more obvious "No output whatsoever" but "Use default
    format", which was counterintuitive.
 
- * Implementations of "tar" that do not understand an extended pax
-   header would extract the contents of it in a regular file; make
-   sure the permission bits of this file follows the same tar.umask
-   configuration setting.
-
  * "git -c section.var command" and "git -c section.var= command"
    should pass the configuration differently (the former should be a
    boolean true, the latter should be an empty string).
@@ -236,3 +243,8 @@ notes for details).
  * A few documentation pages had example sections marked up not quite
    correctly, which passed AsciiDoc but failed with AsciiDoctor.
    (merge c30c43c bc/asciidoc-pretty-formats-fix later to maint).
+   (merge f8a48af bc/asciidoc later to maint).
+
+ * "gitweb" used deprecated CGI::startfrom, which was removed from
+   CGI.pm as of 4.04; use CGI::start_from instead.
+   (merge 4750f4b rm/gitweb-start-form later to maint).
diff --git a/Documentation/everyday.txt b/Documentation/everyday.txt
deleted file mode 100644 (file)
index b2548ef..0000000
+++ /dev/null
@@ -1,413 +0,0 @@
-Everyday Git With 20 Commands Or So
-===================================
-
-<<Individual Developer (Standalone)>> commands are essential for
-anybody who makes a commit, even for somebody who works alone.
-
-If you work with other people, you will need commands listed in
-the <<Individual Developer (Participant)>> section as well.
-
-People who play the <<Integrator>> role need to learn some more
-commands in addition to the above.
-
-<<Repository Administration>> commands are for system
-administrators who are responsible for the care and feeding
-of Git repositories.
-
-
-Individual Developer (Standalone)[[Individual Developer (Standalone)]]
-----------------------------------------------------------------------
-
-A standalone individual developer does not exchange patches with
-other people, and works alone in a single repository, using the
-following commands.
-
-  * linkgit:git-init[1] to create a new repository.
-
-  * linkgit:git-show-branch[1] to see where you are.
-
-  * linkgit:git-log[1] to see what happened.
-
-  * linkgit:git-checkout[1] and linkgit:git-branch[1] to switch
-    branches.
-
-  * linkgit:git-add[1] to manage the index file.
-
-  * linkgit:git-diff[1] and linkgit:git-status[1] to see what
-    you are in the middle of doing.
-
-  * linkgit:git-commit[1] to advance the current branch.
-
-  * linkgit:git-reset[1] and linkgit:git-checkout[1] (with
-    pathname parameters) to undo changes.
-
-  * linkgit:git-merge[1] to merge between local branches.
-
-  * linkgit:git-rebase[1] to maintain topic branches.
-
-  * linkgit:git-tag[1] to mark known point.
-
-Examples
-~~~~~~~~
-
-Use a tarball as a starting point for a new repository.::
-+
-------------
-$ tar zxf frotz.tar.gz
-$ cd frotz
-$ git init
-$ git add . <1>
-$ git commit -m "import of frotz source tree."
-$ git tag v2.43 <2>
-------------
-+
-<1> add everything under the current directory.
-<2> make a lightweight, unannotated tag.
-
-Create a topic branch and develop.::
-+
-------------
-$ git checkout -b alsa-audio <1>
-$ edit/compile/test
-$ git checkout -- curses/ux_audio_oss.c <2>
-$ git add curses/ux_audio_alsa.c <3>
-$ edit/compile/test
-$ git diff HEAD <4>
-$ git commit -a -s <5>
-$ edit/compile/test
-$ git reset --soft HEAD^ <6>
-$ edit/compile/test
-$ git diff ORIG_HEAD <7>
-$ git commit -a -c ORIG_HEAD <8>
-$ git checkout master <9>
-$ git merge alsa-audio <10>
-$ git log --since='3 days ago' <11>
-$ git log v2.43.. curses/ <12>
-------------
-+
-<1> create a new topic branch.
-<2> revert your botched changes in `curses/ux_audio_oss.c`.
-<3> you need to tell Git if you added a new file; removal and
-modification will be caught if you do `git commit -a` later.
-<4> to see what changes you are committing.
-<5> commit everything as you have tested, with your sign-off.
-<6> take the last commit back, keeping what is in the working tree.
-<7> look at the changes since the premature commit we took back.
-<8> redo the commit undone in the previous step, using the message
-you originally wrote.
-<9> switch to the master branch.
-<10> merge a topic branch into your master branch.
-<11> review commit logs; other forms to limit output can be
-combined and include `--max-count=10` (show 10 commits),
-`--until=2005-12-10`, etc.
-<12> view only the changes that touch what's in `curses/`
-directory, since `v2.43` tag.
-
-
-Individual Developer (Participant)[[Individual Developer (Participant)]]
-------------------------------------------------------------------------
-
-A developer working as a participant in a group project needs to
-learn how to communicate with others, and uses these commands in
-addition to the ones needed by a standalone developer.
-
-  * linkgit:git-clone[1] from the upstream to prime your local
-    repository.
-
-  * linkgit:git-pull[1] and linkgit:git-fetch[1] from "origin"
-    to keep up-to-date with the upstream.
-
-  * linkgit:git-push[1] to shared repository, if you adopt CVS
-    style shared repository workflow.
-
-  * linkgit:git-format-patch[1] to prepare e-mail submission, if
-    you adopt Linux kernel-style public forum workflow.
-
-Examples
-~~~~~~~~
-
-Clone the upstream and work on it.  Feed changes to upstream.::
-+
-------------
-$ git clone git://git.kernel.org/pub/scm/.../torvalds/linux-2.6 my2.6
-$ cd my2.6
-$ edit/compile/test; git commit -a -s <1>
-$ git format-patch origin <2>
-$ git pull <3>
-$ git log -p ORIG_HEAD.. arch/i386 include/asm-i386 <4>
-$ git pull git://git.kernel.org/pub/.../jgarzik/libata-dev.git ALL <5>
-$ git reset --hard ORIG_HEAD <6>
-$ git gc <7>
-$ git fetch --tags <8>
-------------
-+
-<1> repeat as needed.
-<2> extract patches from your branch for e-mail submission.
-<3> `git pull` fetches from `origin` by default and merges into the
-current branch.
-<4> immediately after pulling, look at the changes done upstream
-since last time we checked, only in the
-area we are interested in.
-<5> fetch from a specific branch from a specific repository and merge.
-<6> revert the pull.
-<7> garbage collect leftover objects from reverted pull.
-<8> from time to time, obtain official tags from the `origin`
-and store them under `.git/refs/tags/`.
-
-
-Push into another repository.::
-+
-------------
-satellite$ git clone mothership:frotz frotz <1>
-satellite$ cd frotz
-satellite$ git config --get-regexp '^(remote|branch)\.' <2>
-remote.origin.url mothership:frotz
-remote.origin.fetch refs/heads/*:refs/remotes/origin/*
-branch.master.remote origin
-branch.master.merge refs/heads/master
-satellite$ git config remote.origin.push \
-           master:refs/remotes/satellite/master <3>
-satellite$ edit/compile/test/commit
-satellite$ git push origin <4>
-
-mothership$ cd frotz
-mothership$ git checkout master
-mothership$ git merge satellite/master <5>
-------------
-+
-<1> mothership machine has a frotz repository under your home
-directory; clone from it to start a repository on the satellite
-machine.
-<2> clone sets these configuration variables by default.
-It arranges `git pull` to fetch and store the branches of mothership
-machine to local `remotes/origin/*` remote-tracking branches.
-<3> arrange `git push` to push local `master` branch to
-`remotes/satellite/master` branch of the mothership machine.
-<4> push will stash our work away on `remotes/satellite/master`
-remote-tracking branch on the mothership machine.  You could use this
-as a back-up method.
-<5> on mothership machine, merge the work done on the satellite
-machine into the master branch.
-
-Branch off of a specific tag.::
-+
-------------
-$ git checkout -b private2.6.14 v2.6.14 <1>
-$ edit/compile/test; git commit -a
-$ git checkout master
-$ git format-patch -k -m --stdout v2.6.14..private2.6.14 |
-  git am -3 -k <2>
-------------
-+
-<1> create a private branch based on a well known (but somewhat behind)
-tag.
-<2> forward port all changes in `private2.6.14` branch to `master` branch
-without a formal "merging".
-
-
-Integrator[[Integrator]]
-------------------------
-
-A fairly central person acting as the integrator in a group
-project receives changes made by others, reviews and integrates
-them and publishes the result for others to use, using these
-commands in addition to the ones needed by participants.
-
-  * linkgit:git-am[1] to apply patches e-mailed in from your
-    contributors.
-
-  * linkgit:git-pull[1] to merge from your trusted lieutenants.
-
-  * linkgit:git-format-patch[1] to prepare and send suggested
-    alternative to contributors.
-
-  * linkgit:git-revert[1] to undo botched commits.
-
-  * linkgit:git-push[1] to publish the bleeding edge.
-
-
-Examples
-~~~~~~~~
-
-My typical Git day.::
-+
-------------
-$ git status <1>
-$ git show-branch <2>
-$ mailx <3>
-& s 2 3 4 5 ./+to-apply
-& s 7 8 ./+hold-linus
-& q
-$ git checkout -b topic/one master
-$ git am -3 -i -s -u ./+to-apply <4>
-$ compile/test
-$ git checkout -b hold/linus && git am -3 -i -s -u ./+hold-linus <5>
-$ git checkout topic/one && git rebase master <6>
-$ git checkout pu && git reset --hard next <7>
-$ git merge topic/one topic/two && git merge hold/linus <8>
-$ git checkout maint
-$ git cherry-pick master~4 <9>
-$ compile/test
-$ git tag -s -m "GIT 0.99.9x" v0.99.9x <10>
-$ git fetch ko && git show-branch master maint 'tags/ko-*' <11>
-$ git push ko <12>
-$ git push ko v0.99.9x <13>
-------------
-+
-<1> see what I was in the middle of doing, if any.
-<2> see what topic branches I have and think about how ready
-they are.
-<3> read mails, save ones that are applicable, and save others
-that are not quite ready.
-<4> apply them, interactively, with my sign-offs.
-<5> create topic branch as needed and apply, again with my
-sign-offs.
-<6> rebase internal topic branch that has not been merged to the
-master or exposed as a part of a stable branch.
-<7> restart `pu` every time from the next.
-<8> and bundle topic branches still cooking.
-<9> backport a critical fix.
-<10> create a signed tag.
-<11> make sure I did not accidentally rewind master beyond what I
-already pushed out.  `ko` shorthand points at the repository I have
-at kernel.org, and looks like this:
-+
-------------
-$ cat .git/remotes/ko
-URL: kernel.org:/pub/scm/git/git.git
-Pull: master:refs/tags/ko-master
-Pull: next:refs/tags/ko-next
-Pull: maint:refs/tags/ko-maint
-Push: master
-Push: next
-Push: +pu
-Push: maint
-------------
-+
-In the output from `git show-branch`, `master` should have
-everything `ko-master` has, and `next` should have
-everything `ko-next` has.
-
-<12> push out the bleeding edge.
-<13> push the tag out, too.
-
-
-Repository Administration[[Repository Administration]]
-------------------------------------------------------
-
-A repository administrator uses the following tools to set up
-and maintain access to the repository by developers.
-
-  * linkgit:git-daemon[1] to allow anonymous download from
-    repository.
-
-  * linkgit:git-shell[1] can be used as a 'restricted login shell'
-    for shared central repository users.
-
-link:howto/update-hook-example.html[update hook howto] has a good
-example of managing a shared central repository.
-
-
-Examples
-~~~~~~~~
-We assume the following in /etc/services::
-+
-------------
-$ grep 9418 /etc/services
-git            9418/tcp                # Git Version Control System
-------------
-
-Run git-daemon to serve /pub/scm from inetd.::
-+
-------------
-$ grep git /etc/inetd.conf
-git    stream  tcp     nowait  nobody \
-  /usr/bin/git-daemon git-daemon --inetd --export-all /pub/scm
-------------
-+
-The actual configuration line should be on one line.
-
-Run git-daemon to serve /pub/scm from xinetd.::
-+
-------------
-$ cat /etc/xinetd.d/git-daemon
-# default: off
-# description: The Git server offers access to Git repositories
-service git
-{
-        disable = no
-        type            = UNLISTED
-        port            = 9418
-        socket_type     = stream
-        wait            = no
-        user            = nobody
-        server          = /usr/bin/git-daemon
-        server_args     = --inetd --export-all --base-path=/pub/scm
-        log_on_failure  += USERID
-}
-------------
-+
-Check your xinetd(8) documentation and setup, this is from a Fedora system.
-Others might be different.
-
-Give push/pull only access to developers.::
-+
-------------
-$ grep git /etc/passwd <1>
-alice:x:1000:1000::/home/alice:/usr/bin/git-shell
-bob:x:1001:1001::/home/bob:/usr/bin/git-shell
-cindy:x:1002:1002::/home/cindy:/usr/bin/git-shell
-david:x:1003:1003::/home/david:/usr/bin/git-shell
-$ grep git /etc/shells <2>
-/usr/bin/git-shell
-------------
-+
-<1> log-in shell is set to /usr/bin/git-shell, which does not
-allow anything but `git push` and `git pull`.  The users should
-get an ssh access to the machine.
-<2> in many distributions /etc/shells needs to list what is used
-as the login shell.
-
-CVS-style shared repository.::
-+
-------------
-$ grep git /etc/group <1>
-git:x:9418:alice,bob,cindy,david
-$ cd /home/devo.git
-$ ls -l <2>
-  lrwxrwxrwx   1 david git    17 Dec  4 22:40 HEAD -> refs/heads/master
-  drwxrwsr-x   2 david git  4096 Dec  4 22:40 branches
-  -rw-rw-r--   1 david git    84 Dec  4 22:40 config
-  -rw-rw-r--   1 david git    58 Dec  4 22:40 description
-  drwxrwsr-x   2 david git  4096 Dec  4 22:40 hooks
-  -rw-rw-r--   1 david git 37504 Dec  4 22:40 index
-  drwxrwsr-x   2 david git  4096 Dec  4 22:40 info
-  drwxrwsr-x   4 david git  4096 Dec  4 22:40 objects
-  drwxrwsr-x   4 david git  4096 Nov  7 14:58 refs
-  drwxrwsr-x   2 david git  4096 Dec  4 22:40 remotes
-$ ls -l hooks/update <3>
-  -r-xr-xr-x   1 david git  3536 Dec  4 22:40 update
-$ cat info/allowed-users <4>
-refs/heads/master      alice\|cindy
-refs/heads/doc-update  bob
-refs/tags/v[0-9]*      david
-------------
-+
-<1> place the developers into the same git group.
-<2> and make the shared repository writable by the group.
-<3> use update-hook example by Carl from Documentation/howto/
-for branch policy control.
-<4> alice and cindy can push into master, only bob can push into doc-update.
-david is the release manager and is the only person who can
-create and push version tags.
-
-HTTP server to support dumb protocol transfer.::
-+
-------------
-dev$ git update-server-info <1>
-dev$ ftp user@isp.example.com <2>
-ftp> cp -r .git /home/user/myproject.git
-------------
-+
-<1> make sure your info/refs and objects/info/packs are up-to-date
-<2> upload to public HTTP server hosted by your ISP.
diff --git a/Documentation/everyday.txto b/Documentation/everyday.txto
new file mode 100644 (file)
index 0000000..c5047d8
--- /dev/null
@@ -0,0 +1,9 @@
+Everyday Git With 20 Commands Or So
+===================================
+
+This document has been moved to linkgit:giteveryday[1].
+
+Please let the owners of the referring site know so that they can update the
+link you clicked to get here.
+
+Thanks.
index 7d991d919ccf3378fdc924418aede45f8cb38632..c7c0d21429bb745d1abcfc775308e8ccac8f499a 100644 (file)
@@ -97,7 +97,7 @@ Using direct mode:
     host = imap://imap.example.com
     user = bob
     pass = p4ssw0rd
-..........................
+.........................
 
 Using direct mode with SSL:
 
@@ -109,7 +109,7 @@ Using direct mode with SSL:
     pass = p4ssw0rd
     port = 123
     sslverify = false
-..........................
+.........................
 
 
 EXAMPLE
diff --git a/Documentation/git-interpret-trailers.txt b/Documentation/git-interpret-trailers.txt
new file mode 100644 (file)
index 0000000..81fac3d
--- /dev/null
@@ -0,0 +1,314 @@
+git-interpret-trailers(1)
+=========================
+
+NAME
+----
+git-interpret-trailers - help add stuctured information into commit messages
+
+SYNOPSIS
+--------
+[verse]
+'git interpret-trailers' [--trim-empty] [(--trailer <token>[(=|:)<value>])...] [<file>...]
+
+DESCRIPTION
+-----------
+Help adding 'trailers' lines, that look similar to RFC 822 e-mail
+headers, at the end of the otherwise free-form part of a commit
+message.
+
+This command reads some patches or commit messages from either the
+<file> arguments or the standard input if no <file> is specified. Then
+this command applies the arguments passed using the `--trailer`
+option, if any, to the commit message part of each input file. The
+result is emitted on the standard output.
+
+Some configuration variables control the way the `--trailer` arguments
+are applied to each commit message and the way any existing trailer in
+the commit message is changed. They also make it possible to
+automatically add some trailers.
+
+By default, a '<token>=<value>' or '<token>:<value>' argument given
+using `--trailer` will be appended after the existing trailers only if
+the last trailer has a different (<token>, <value>) pair (or if there
+is no existing trailer). The <token> and <value> parts will be trimmed
+to remove starting and trailing whitespace, and the resulting trimmed
+<token> and <value> will appear in the message like this:
+
+------------------------------------------------
+token: value
+------------------------------------------------
+
+This means that the trimmed <token> and <value> will be separated by
+`': '` (one colon followed by one space).
+
+By default the new trailer will appear at the end of all the existing
+trailers. If there is no existing trailer, the new trailer will appear
+after the commit message part of the ouput, and, if there is no line
+with only spaces at the end of the commit message part, one blank line
+will be added before the new trailer.
+
+Existing trailers are extracted from the input message by looking for
+a group of one or more lines that contain a colon (by default), where
+the group is preceded by one or more empty (or whitespace-only) lines.
+The group must either be at the end of the message or be the last
+non-whitespace lines before a line that starts with '---'. Such three
+minus signs start the patch part of the message.
+
+When reading trailers, there can be whitespaces before and after the
+token, the separator and the value. There can also be whitespaces
+indide the token and the value.
+
+Note that 'trailers' do not follow and are not intended to follow many
+rules for RFC 822 headers. For example they do not follow the line
+folding rules, the encoding rules and probably many other rules.
+
+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.
+
+--trailer <token>[(=|:)<value>]::
+       Specify a (<token>, <value>) pair that should be applied as a
+       trailer to the input messages. See the description of this
+       command.
+
+CONFIGURATION VARIABLES
+-----------------------
+
+trailer.separators::
+       This option tells which characters are recognized as trailer
+       separators. By default only ':' is recognized as a trailer
+       separator, except that '=' is always accepted on the command
+       line for compatibility with other git commands.
++
+The first character given by this option will be the default character
+used when another separator is not specified in the config for this
+trailer.
++
+For example, if the value for this option is "%=$", then only lines
+using the format '<token><sep><value>' with <sep> containing '%', '='
+or '$' and then spaces will be considered trailers. And '%' will be
+the default separator used, so by default trailers will appear like:
+'<token>% <value>' (one percent sign and one space will appear between
+the token and the value).
+
+trailer.where::
+       This option tells where a new trailer will be added.
++
+This can be `end`, which is the default, `start`, `after` or `before`.
++
+If it is `end`, then each new trailer will appear at the end of the
+existing trailers.
++
+If it is `start`, then each new trailer will appear at the start,
+instead of the end, of the existing trailers.
++
+If it is `after`, then each new trailer will appear just after the
+last trailer with the same <token>.
++
+If it is `before`, then each new trailer will appear just before the
+first trailer with the same <token>.
+
+trailer.ifexists::
+       This option makes it possible to choose what action will be
+       performed when there is already at least one trailer with the
+       same <token> in the message.
++
+The valid values for this option are: `addIfDifferentNeighbor` (this
+is the default), `addIfDifferent`, `add`, `overwrite` or `doNothing`.
++
+With `addIfDifferentNeighbor`, a new trailer will be added only if no
+trailer with the same (<token>, <value>) pair is above or below the line
+where the new trailer will be added.
++
+With `addIfDifferent`, a new trailer will be added only if no trailer
+with the same (<token>, <value>) pair is already in the message.
++
+With `add`, a new trailer will be added, even if some trailers with
+the same (<token>, <value>) pair are already in the message.
++
+With `replace`, an existing trailer with the same <token> will be
+deleted and the new trailer will be added. The deleted trailer will be
+the closest one (with the same <token>) to the place where the new one
+will be added.
++
+With `doNothing`, nothing will be done; that is no new trailer will be
+added if there is already one with the same <token> in the message.
+
+trailer.ifmissing::
+       This option makes it possible to choose what action will be
+       performed when there is not yet any trailer with the same
+       <token> in the message.
++
+The valid values for this option are: `add` (this is the default) and
+`doNothing`.
++
+With `add`, a new trailer will be added.
++
+With `doNothing`, nothing will be done.
+
+trailer.<token>.key::
+       This `key` will be used instead of <token> in the trailer. At
+       the end of this key, a separator can appear and then some
+       space characters. By default the only valid separator is ':',
+       but this can be changed using the `trailer.separators` config
+       variable.
++
+If there is a separator, then the key will be used instead of both the
+<token> and the default separator when adding the trailer.
+
+trailer.<token>.where::
+       This option takes the same values as the 'trailer.where'
+       configuration variable and it overrides what is specified by
+       that option for trailers with the specified <token>.
+
+trailer.<token>.ifexist::
+       This option takes the same values as the 'trailer.ifexist'
+       configuration variable and it overrides what is specified by
+       that option for trailers with the specified <token>.
+
+trailer.<token>.ifmissing::
+       This option takes the same values as the 'trailer.ifmissing'
+       configuration variable and it overrides what is specified by
+       that option for trailers with the specified <token>.
+
+trailer.<token>.command::
+       This option can be used to specify a shell command that will
+       be called to automatically add or modify a trailer with the
+       specified <token>.
++
+When this option is specified, the behavior is as if a special
+'<token>=<value>' argument were added at the beginning of the command
+line, where <value> is taken to be the standard output of the
+specified command with any leading and trailing whitespace trimmed
+off.
++
+If the command contains the `$ARG` string, this string will be
+replaced with the <value> part of an existing trailer with the same
+<token>, if any, before the command is launched.
++
+If some '<token>=<value>' arguments are also passed on the command
+line, when a 'trailer.<token>.command' is configured, the command will
+also be executed for each of these arguments. And the <value> part of
+these arguments, if any, will be used to replace the `$ARG` string in
+the command.
+
+EXAMPLES
+--------
+
+* Configure a 'sign' trailer with a 'Signed-off-by' key, and then
+  add two of these trailers to a message:
++
+------------
+$ git config trailer.sign.key "Signed-off-by"
+$ cat msg.txt
+subject
+
+message
+$ cat msg.txt | git interpret-trailers --trailer 'sign: Alice <alice@example.com>' --trailer 'sign: Bob <bob@example.com>'
+subject
+
+message
+
+Signed-off-by: Alice <alice@example.com>
+Signed-off-by: Bob <bob@example.com>
+------------
+
+* Extract the last commit as a patch, and add a 'Cc' and a
+  'Reviewed-by' trailer to it:
++
+------------
+$ git format-patch -1
+0001-foo.patch
+$ git interpret-trailers --trailer 'Cc: Alice <alice@example.com>' --trailer 'Reviewed-by: Bob <bob@example.com>' 0001-foo.patch >0001-bar.patch
+------------
+
+* Configure a 'sign' trailer with a command to automatically add a
+  'Signed-off-by: ' with the author information only if there is no
+  'Signed-off-by: ' already, and show how it works:
++
+------------
+$ git config trailer.sign.key "Signed-off-by: "
+$ git config trailer.sign.ifmissing add
+$ git config trailer.sign.ifexists doNothing
+$ git config trailer.sign.command 'echo "$(git config user.name) <$(git config user.email)>"'
+$ git interpret-trailers <<EOF
+> EOF
+
+Signed-off-by: Bob <bob@example.com>
+$ git interpret-trailers <<EOF
+> Signed-off-by: Alice <alice@example.com>
+> EOF
+
+Signed-off-by: Alice <alice@example.com>
+------------
+
+* Configure a 'fix' trailer with a key that contains a '#' and no
+  space after this character, and show how it works:
++
+------------
+$ git config trailer.separators ":#"
+$ git config trailer.fix.key "Fix #"
+$ echo "subject" | git interpret-trailers --trailer fix=42
+subject
+
+Fix #42
+------------
+
+* Configure a 'see' trailer with a command to show the subject of a
+  commit that is related, and show how it works:
++
+------------
+$ git config trailer.see.key "See-also: "
+$ git config trailer.see.ifExists "replace"
+$ git config trailer.see.ifMissing "doNothing"
+$ git config trailer.see.command "git log -1 --oneline --format=\"%h (%s)\" --abbrev-commit --abbrev=14 \$ARG"
+$ git interpret-trailers <<EOF
+> subject
+> 
+> message
+> 
+> see: HEAD~2
+> EOF
+subject
+
+message
+
+See-also: fe3187489d69c4 (subject of related commit)
+------------
+
+* Configure a commit template with some trailers with empty values
+  (using sed to show and keep the trailing spaces at the end of the
+  trailers), then configure a commit-msg hook that uses
+  'git interpret-trailers' to remove trailers with empty values and
+  to add a 'git-version' trailer:
++
+------------
+$ sed -e 's/ Z$/ /' >commit_template.txt <<EOF
+> ***subject***
+> 
+> ***message***
+> 
+> Fixes: Z
+> Cc: Z
+> Reviewed-by: Z
+> Signed-off-by: Z
+> EOF
+$ git config commit.template commit_template.txt
+$ cat >.git/hooks/commit-msg <<EOF
+> #!/bin/sh
+> git interpret-trailers --trim-empty --trailer "git-version: \$(git describe)" "\$1" > "\$1.new"
+> mv "\$1.new" "\$1"
+> EOF
+$ chmod +x .git/hooks/commit-msg
+------------
+
+SEE ALSO
+--------
+linkgit:git-commit[1], linkgit:git-format-patch[1], linkgit:git-config[1]
+
+GIT
+---
+Part of the linkgit:git[1] suite
index 6738055bd3083825c06c82d1b7678ef453854253..9fed59a31724c4dccd7364c03c32c72d9c8d4664 100644 (file)
@@ -1,5 +1,5 @@
 git-prune-packed(1)
-=====================
+===================
 
 NAME
 ----
index b17283ab7a1cc73c5ec741e0da1128bea2a57a65..21b3f29c3bc603df74e07226d27ad63faa6fae24 100644 (file)
@@ -34,7 +34,7 @@ When the command line does not specify what to push with `<refspec>...`
 arguments or `--all`, `--mirror`, `--tags` options, the command finds
 the default `<refspec>` by consulting `remote.*.push` configuration,
 and if it is not found, honors `push.default` configuration to decide
-what to push (See linkgit:git-config[1] for the meaning of `push.default`).
+what to push (See gitlink:git-config[1] for the meaning of `push.default`).
 
 
 OPTIONS[[OPTIONS]]
index a356196586e2cfd472e1e087fb12e981b3a1480f..d64388cb8e454be17e4c20caf95897a71619c11f 100644 (file)
@@ -1,5 +1,5 @@
 git-quiltimport(1)
-================
+==================
 
 NAME
 ----
index ba3fe0d7f59b1ae4c9ae9e3d32675d2e281ccf13..25bcda936dbe8b171e0195a569d58ba8ce42e714 100644 (file)
@@ -1,5 +1,5 @@
 git-stage(1)
-==============
+============
 
 NAME
 ----
index c6175d45e4257efa96995853f00e42baf4b221f9..9e0a42ce5673c1fc11c311c63b5452181110fd8f 100644 (file)
@@ -22,7 +22,7 @@ unusually rich command set that provides both high-level operations
 and full access to internals.
 
 See linkgit:gittutorial[7] to get started, then see
-link:everyday.html[Everyday Git] for a useful minimum set of
+linkgit:giteveryday[7] for a useful minimum set of
 commands.  The link:user-manual.html[Git User's Manual] has a more
 in-depth introduction.
 
@@ -1098,7 +1098,7 @@ subscribed to the list to send a message there.
 SEE ALSO
 --------
 linkgit:gittutorial[7], linkgit:gittutorial-2[7],
-link:everyday.html[Everyday Git], linkgit:gitcvs-migration[7],
+linkgit:giteveryday[7], linkgit:gitcvs-migration[7],
 linkgit:gitglossary[7], linkgit:gitcore-tutorial[7],
 linkgit:gitcli[7], link:user-manual.html[The Git User's Manual],
 linkgit:gitworkflows[7]
index d2d7c213dd56f3886b7ab7e3651e1e80eec3b2ef..8475c079325103fe102027fccd5f5f02818667f3 100644 (file)
@@ -1667,7 +1667,7 @@ linkgit:gittutorial[7],
 linkgit:gittutorial-2[7],
 linkgit:gitcvs-migration[7],
 linkgit:git-help[1],
-link:everyday.html[Everyday git],
+linkgit:giteveryday[7],
 link:user-manual.html[The Git User's Manual]
 
 GIT
index 5f4e89005c5e554353920fe858bc74399e438a14..b06e852a85587ffd46820c9dfb832784cbf1dfd9 100644 (file)
@@ -194,7 +194,7 @@ linkgit:gittutorial[7],
 linkgit:gittutorial-2[7],
 linkgit:gitcore-tutorial[7],
 linkgit:gitglossary[7],
-link:everyday.html[Everyday Git],
+linkgit:giteveryday[7],
 link:user-manual.html[The Git User's Manual]
 
 GIT
diff --git a/Documentation/giteveryday.txt b/Documentation/giteveryday.txt
new file mode 100644 (file)
index 0000000..7be6e64
--- /dev/null
@@ -0,0 +1,455 @@
+giteveryday(7)
+===============
+
+NAME
+----
+giteveryday - A useful minimum set of commands for Everyday Git
+
+SYNOPSIS
+--------
+
+Everyday Git With 20 Commands Or So
+
+DESCRIPTION
+-----------
+
+Git users can broadly be grouped into four categories for the purposes of
+describing here a small set of useful command for everyday Git.
+
+*      <<STANDALONE,Individual Developer (Standalone)>> commands are essential
+       for anybody who makes a commit, even for somebody who works alone.
+
+*      If you work with other people, you will need commands listed in
+       the <<PARTICIPANT,Individual Developer (Participant)>> section as well.
+
+*      People who play the <<INTEGRATOR,Integrator>> role need to learn some
+       more commands in addition to the above.
+
+*      <<ADMINISTRATION,Repository Administration>> commands are for system
+       administrators who are responsible for the care and feeding
+       of Git repositories.
+
+
+Individual Developer (Standalone)[[STANDALONE]]
+-----------------------------------------------
+
+A standalone individual developer does not exchange patches with
+other people, and works alone in a single repository, using the
+following commands.
+
+  * linkgit:git-init[1] to create a new repository.
+
+  * linkgit:git-log[1] to see what happened.
+
+  * linkgit:git-checkout[1] and linkgit:git-branch[1] to switch
+    branches.
+
+  * linkgit:git-add[1] to manage the index file.
+
+  * linkgit:git-diff[1] and linkgit:git-status[1] to see what
+    you are in the middle of doing.
+
+  * linkgit:git-commit[1] to advance the current branch.
+
+  * linkgit:git-reset[1] and linkgit:git-checkout[1] (with
+    pathname parameters) to undo changes.
+
+  * linkgit:git-merge[1] to merge between local branches.
+
+  * linkgit:git-rebase[1] to maintain topic branches.
+
+  * linkgit:git-tag[1] to mark a known point.
+
+Examples
+~~~~~~~~
+
+Use a tarball as a starting point for a new repository.::
++
+------------
+$ tar zxf frotz.tar.gz
+$ cd frotz
+$ git init
+$ git add . <1>
+$ git commit -m "import of frotz source tree."
+$ git tag v2.43 <2>
+------------
++
+<1> add everything under the current directory.
+<2> make a lightweight, unannotated tag.
+
+Create a topic branch and develop.::
++
+------------
+$ git checkout -b alsa-audio <1>
+$ edit/compile/test
+$ git checkout -- curses/ux_audio_oss.c <2>
+$ git add curses/ux_audio_alsa.c <3>
+$ edit/compile/test
+$ git diff HEAD <4>
+$ git commit -a -s <5>
+$ edit/compile/test
+$ git diff HEAD^ <6>
+$ git commit -a --amend <7>
+$ git checkout master <8>
+$ git merge alsa-audio <9>
+$ git log --since='3 days ago' <10>
+$ git log v2.43.. curses/ <11>
+------------
++
+<1> create a new topic branch.
+<2> revert your botched changes in `curses/ux_audio_oss.c`.
+<3> you need to tell Git if you added a new file; removal and
+modification will be caught if you do `git commit -a` later.
+<4> to see what changes you are committing.
+<5> commit everything, as you have tested, with your sign-off.
+<6> look at all your changes including the previous commit.
+<7> amend the previous commit, adding all your new changes,
+using your original message.
+<8> switch to the master branch.
+<9> merge a topic branch into your master branch.
+<10> review commit logs; other forms to limit output can be
+combined and include `-10` (to show up to 10 commits),
+`--until=2005-12-10`, etc.
+<11> view only the changes that touch what's in `curses/`
+directory, since `v2.43` tag.
+
+
+Individual Developer (Participant)[[PARTICIPANT]]
+-------------------------------------------------
+
+A developer working as a participant in a group project needs to
+learn how to communicate with others, and uses these commands in
+addition to the ones needed by a standalone developer.
+
+  * linkgit:git-clone[1] from the upstream to prime your local
+    repository.
+
+  * linkgit:git-pull[1] and linkgit:git-fetch[1] from "origin"
+    to keep up-to-date with the upstream.
+
+  * linkgit:git-push[1] to shared repository, if you adopt CVS
+    style shared repository workflow.
+
+  * linkgit:git-format-patch[1] to prepare e-mail submission, if
+    you adopt Linux kernel-style public forum workflow.
+
+  * linkgit:git-send-email[1] to send your e-mail submission without
+    corruption by your MUA.
+
+  * linkgit:git-request-pull[1] to create a summary of changes
+    for your upstream to pull.
+
+
+Examples
+~~~~~~~~
+
+Clone the upstream and work on it.  Feed changes to upstream.::
++
+------------
+$ git clone git://git.kernel.org/pub/scm/.../torvalds/linux-2.6 my2.6
+$ cd my2.6
+$ git checkout -b mine master <1>
+$ edit/compile/test; git commit -a -s <2>
+$ git format-patch master <3>
+$ git send-email --to="person <email@example.com>" 00*.patch <4>
+$ git checkout master <5>
+$ git pull <6>
+$ git log -p ORIG_HEAD.. arch/i386 include/asm-i386 <7>
+$ git ls-remote --heads http://git.kernel.org/.../jgarzik/libata-dev.git <8>
+$ git pull git://git.kernel.org/pub/.../jgarzik/libata-dev.git ALL <9>
+$ git reset --hard ORIG_HEAD <10>
+$ git gc <11>
+------------
++
+<1> checkout a new branch `mine` from master.
+<2> repeat as needed.
+<3> extract patches from your branch, relative to master,
+<4> and email them.
+<5> return to `master`, ready to see what's new
+<6> `git pull` fetches from `origin` by default and merges into the
+current branch.
+<7> immediately after pulling, look at the changes done upstream
+since last time we checked, only in the
+area we are interested in.
+<8> check the branch names in an external repository (if not known).
+<9> fetch from a specific branch `ALL` from a specific repository
+and merge it.
+<10> revert the pull.
+<11> garbage collect leftover objects from reverted pull.
+
+
+Push into another repository.::
++
+------------
+satellite$ git clone mothership:frotz frotz <1>
+satellite$ cd frotz
+satellite$ git config --get-regexp '^(remote|branch)\.' <2>
+remote.origin.url mothership:frotz
+remote.origin.fetch refs/heads/*:refs/remotes/origin/*
+branch.master.remote origin
+branch.master.merge refs/heads/master
+satellite$ git config remote.origin.push \
+          +refs/heads/*:refs/remotes/satellite/* <3>
+satellite$ edit/compile/test/commit
+satellite$ git push origin <4>
+
+mothership$ cd frotz
+mothership$ git checkout master
+mothership$ git merge satellite/master <5>
+------------
++
+<1> mothership machine has a frotz repository under your home
+directory; clone from it to start a repository on the satellite
+machine.
+<2> clone sets these configuration variables by default.
+It arranges `git pull` to fetch and store the branches of mothership
+machine to local `remotes/origin/*` remote-tracking branches.
+<3> arrange `git push` to push all local branches to
+their corresponding branch of the mothership machine.
+<4> push will stash all our work away on `remotes/satellite/*`
+remote-tracking branches on the mothership machine.  You could use this
+as a back-up method. Likewise, you can pretend that mothership
+"fetched" from you (useful when access is one sided).
+<5> on mothership machine, merge the work done on the satellite
+machine into the master branch.
+
+Branch off of a specific tag.::
++
+------------
+$ git checkout -b private2.6.14 v2.6.14 <1>
+$ edit/compile/test; git commit -a
+$ git checkout master
+$ git cherry-pick v2.6.14..private2.6.14 <2>
+------------
++
+<1> create a private branch based on a well known (but somewhat behind)
+tag.
+<2> forward port all changes in `private2.6.14` branch to `master` branch
+without a formal "merging". Or longhand +
+`git format-patch -k -m --stdout v2.6.14..private2.6.14 |
+  git am -3 -k`
+
+An alternate participant submission mechanism is using the
+`git request-pull` or pull-request mechanisms (e.g as used on
+GitHub (www.github.com) to notify your upstream of your
+contribution.
+
+Integrator[[INTEGRATOR]]
+------------------------
+
+A fairly central person acting as the integrator in a group
+project receives changes made by others, reviews and integrates
+them and publishes the result for others to use, using these
+commands in addition to the ones needed by participants.
+
+This section can also be used by those who respond to `git
+request-pull` or pull-request on GitHub (www.github.com) to
+integrate the work of others into their history. An sub-area
+lieutenant for a repository will act both as a participant and
+as an integrator.
+
+
+  * linkgit:git-am[1] to apply patches e-mailed in from your
+    contributors.
+
+  * linkgit:git-pull[1] to merge from your trusted lieutenants.
+
+  * linkgit:git-format-patch[1] to prepare and send suggested
+    alternative to contributors.
+
+  * linkgit:git-revert[1] to undo botched commits.
+
+  * linkgit:git-push[1] to publish the bleeding edge.
+
+
+Examples
+~~~~~~~~
+
+A typical integrator's Git day.::
++
+------------
+$ git status <1>
+$ git branch --no-merged master <2>
+$ mailx <3>
+& s 2 3 4 5 ./+to-apply
+& s 7 8 ./+hold-linus
+& q
+$ git checkout -b topic/one master
+$ git am -3 -i -s ./+to-apply <4>
+$ compile/test
+$ git checkout -b hold/linus && git am -3 -i -s ./+hold-linus <5>
+$ git checkout topic/one && git rebase master <6>
+$ git checkout pu && git reset --hard next <7>
+$ git merge topic/one topic/two && git merge hold/linus <8>
+$ git checkout maint
+$ git cherry-pick master~4 <9>
+$ compile/test
+$ git tag -s -m "GIT 0.99.9x" v0.99.9x <10>
+$ git fetch ko && for branch in master maint next pu <11>
+    do
+       git show-branch ko/$branch $branch <12>
+    done
+$ git push --follow-tags ko <13>
+------------
++
+<1> see what you were in the middle of doing, if anything.
+<2> see which branches haven't been merged into `master` yet.
+Likewise for any other integration branches e.g. `maint`, `next`
+and `pu` (potential updates).
+<3> read mails, save ones that are applicable, and save others
+that are not quite ready (other mail readers are available).
+<4> apply them, interactively, with your sign-offs.
+<5> create topic branch as needed and apply, again with sign-offs.
+<6> rebase internal topic branch that has not been merged to the
+master or exposed as a part of a stable branch.
+<7> restart `pu` every time from the next.
+<8> and bundle topic branches still cooking.
+<9> backport a critical fix.
+<10> create a signed tag.
+<11> make sure master was not accidentally rewound beyond that
+already pushed out.  `ko` shorthand points at the Git maintainer's
+repository at kernel.org, and looks like this:
++
+------------
+(in .git/config)
+[remote "ko"]
+       url = kernel.org:/pub/scm/git/git.git
+       fetch = refs/heads/*:refs/remotes/ko/*
+       push = refs/heads/master
+       push = refs/heads/next
+       push = +refs/heads/pu
+       push = refs/heads/maint
+------------
++
+<12> In the output from `git show-branch`, `master` should have
+everything `ko/master` has, and `next` should have
+everything `ko/next` has, etc.
+<13> push out the bleeding edge, together with new tags that point
+into the pushed history.
+
+
+Repository Administration[[ADMINISTRATION]]
+-------------------------------------------
+
+A repository administrator uses the following tools to set up
+and maintain access to the repository by developers.
+
+  * linkgit:git-daemon[1] to allow anonymous download from
+    repository.
+
+  * linkgit:git-shell[1] can be used as a 'restricted login shell'
+    for shared central repository users.
+
+  * linkgit:git-http-backend[1] provides a server side implementation
+    of Git-over-HTTP ("Smart http") allowing both fetch and push services.
+
+  * linkgit:gitweb[1] provides a web front-end to Git repositories,
+    which can be set-up using the linkgit:git-instaweb[1] script.
+
+link:howto/update-hook-example.html[update hook howto] has a good
+example of managing a shared central repository.
+
+In addition there are a number of other widely deployed hosting, browsing
+and reviewing solutions such as:
+
+  * gitolite, gerrit code review, cgit and others.
+
+Examples
+~~~~~~~~
+We assume the following in /etc/services::
++
+------------
+$ grep 9418 /etc/services
+git            9418/tcp                # Git Version Control System
+------------
+
+Run git-daemon to serve /pub/scm from inetd.::
++
+------------
+$ grep git /etc/inetd.conf
+git    stream  tcp     nowait  nobody \
+  /usr/bin/git-daemon git-daemon --inetd --export-all /pub/scm
+------------
++
+The actual configuration line should be on one line.
+
+Run git-daemon to serve /pub/scm from xinetd.::
++
+------------
+$ cat /etc/xinetd.d/git-daemon
+# default: off
+# description: The Git server offers access to Git repositories
+service git
+{
+       disable = no
+       type            = UNLISTED
+       port            = 9418
+       socket_type     = stream
+       wait            = no
+       user            = nobody
+       server          = /usr/bin/git-daemon
+       server_args     = --inetd --export-all --base-path=/pub/scm
+       log_on_failure  += USERID
+}
+------------
++
+Check your xinetd(8) documentation and setup, this is from a Fedora system.
+Others might be different.
+
+Give push/pull only access to developers using git-over-ssh.::
+
+e.g. those using:
+`$ git push/pull ssh://host.xz/pub/scm/project`
++
+------------
+$ grep git /etc/passwd <1>
+alice:x:1000:1000::/home/alice:/usr/bin/git-shell
+bob:x:1001:1001::/home/bob:/usr/bin/git-shell
+cindy:x:1002:1002::/home/cindy:/usr/bin/git-shell
+david:x:1003:1003::/home/david:/usr/bin/git-shell
+$ grep git /etc/shells <2>
+/usr/bin/git-shell
+------------
++
+<1> log-in shell is set to /usr/bin/git-shell, which does not
+allow anything but `git push` and `git pull`.  The users require
+ssh access to the machine.
+<2> in many distributions /etc/shells needs to list what is used
+as the login shell.
+
+CVS-style shared repository.::
++
+------------
+$ grep git /etc/group <1>
+git:x:9418:alice,bob,cindy,david
+$ cd /home/devo.git
+$ ls -l <2>
+  lrwxrwxrwx   1 david git    17 Dec  4 22:40 HEAD -> refs/heads/master
+  drwxrwsr-x   2 david git  4096 Dec  4 22:40 branches
+  -rw-rw-r--   1 david git    84 Dec  4 22:40 config
+  -rw-rw-r--   1 david git    58 Dec  4 22:40 description
+  drwxrwsr-x   2 david git  4096 Dec  4 22:40 hooks
+  -rw-rw-r--   1 david git 37504 Dec  4 22:40 index
+  drwxrwsr-x   2 david git  4096 Dec  4 22:40 info
+  drwxrwsr-x   4 david git  4096 Dec  4 22:40 objects
+  drwxrwsr-x   4 david git  4096 Nov  7 14:58 refs
+  drwxrwsr-x   2 david git  4096 Dec  4 22:40 remotes
+$ ls -l hooks/update <3>
+  -r-xr-xr-x   1 david git  3536 Dec  4 22:40 update
+$ cat info/allowed-users <4>
+refs/heads/master      alice\|cindy
+refs/heads/doc-update  bob
+refs/tags/v[0-9]*      david
+------------
++
+<1> place the developers into the same git group.
+<2> and make the shared repository writable by the group.
+<3> use update-hook example by Carl from Documentation/howto/
+for branch policy control.
+<4> alice and cindy can push into master, only bob can push into doc-update.
+david is the release manager and is the only person who can
+create and push version tags.
+
+GIT
+---
+Part of the linkgit:git[1] suite
index e52de7dbb48ce84e3a82d84953a01bf0e6bfcdb4..212e254adc057fa3f67bc50cc23a995059a24585 100644 (file)
@@ -19,7 +19,7 @@ SEE ALSO
 linkgit:gittutorial[7],
 linkgit:gittutorial-2[7],
 linkgit:gitcvs-migration[7],
-link:everyday.html[Everyday Git],
+linkgit:giteveryday[7],
 link:user-manual.html[The Git User's Manual]
 
 GIT
index 3109ea8aade1ceb47de67823388d865a1505e616..f6fbf814fba14d230f623aa9b2e44a7bde88adbd 100644 (file)
@@ -403,7 +403,7 @@ What next?
 
 At this point you should know everything necessary to read the man
 pages for any of the git commands; one good place to start would be
-with the commands mentioned in link:everyday.html[Everyday Git].  You
+with the commands mentioned in linkgit:giteveryday[7].  You
 should be able to find any unknown jargon in linkgit:gitglossary[7].
 
 The link:user-manual.html[Git User's Manual] provides a more
@@ -427,7 +427,7 @@ linkgit:gitcvs-migration[7],
 linkgit:gitcore-tutorial[7],
 linkgit:gitglossary[7],
 linkgit:git-help[1],
-link:everyday.html[Everyday Git],
+linkgit:giteveryday[7],
 link:user-manual.html[The Git User's Manual]
 
 GIT
index 82621963189d1800087c8eca63add642c0743a96..af9f709ccf36b37ab4b143eaad218372af3e0ee4 100644 (file)
@@ -656,7 +656,7 @@ digressions that may be interesting at this point are:
   * linkgit:gitworkflows[7]: Gives an overview of recommended
     workflows.
 
-  * link:everyday.html[Everyday Git with 20 Commands Or So]
+  * linkgit:giteveryday[7]: Everyday Git with 20 Commands Or So.
 
   * linkgit:gitcvs-migration[7]: Git for CVS users.
 
@@ -668,7 +668,7 @@ linkgit:gitcore-tutorial[7],
 linkgit:gitglossary[7],
 linkgit:git-help[1],
 linkgit:gitworkflows[7],
-link:everyday.html[Everyday Git],
+linkgit:giteveryday[7],
 link:user-manual.html[The Git User's Manual]
 
 GIT
index 356feb5c86023d00a41ac8bca7d44f9779c39ad0..fcd51ac463a2f4ae2d748b30df36604d6190fb90 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -764,6 +764,7 @@ LIB_OBJS += submodule.o
 LIB_OBJS += symlinks.o
 LIB_OBJS += tag.o
 LIB_OBJS += trace.o
+LIB_OBJS += trailer.o
 LIB_OBJS += transport.o
 LIB_OBJS += transport-helper.o
 LIB_OBJS += tree-diff.o
@@ -828,6 +829,7 @@ BUILTIN_OBJS += builtin/hash-object.o
 BUILTIN_OBJS += builtin/help.o
 BUILTIN_OBJS += builtin/index-pack.o
 BUILTIN_OBJS += builtin/init-db.o
+BUILTIN_OBJS += builtin/interpret-trailers.o
 BUILTIN_OBJS += builtin/log.o
 BUILTIN_OBJS += builtin/ls-files.o
 BUILTIN_OBJS += builtin/ls-remote.o
diff --git a/README b/README
index 15a8e235012a247e6f22abd2c298a2e43c940c61..1083735d1cddaba394eb5c99ce470a31050dad4b 100644 (file)
--- a/README
+++ b/README
@@ -27,7 +27,7 @@ Torvalds with help of a group of hackers around the net.
 Please read the file INSTALL for installation instructions.
 
 See Documentation/gittutorial.txt to get started, then see
-Documentation/everyday.txt for a useful minimum set of commands, and
+Documentation/giteveryday.txt for a useful minimum set of commands, and
 Documentation/git-commandname.txt for documentation of each command.
 If git has been correctly installed, then the tutorial can also be
 read with "man gittutorial" or "git help tutorial", and the
index df2f4c8a643796ee96a31185f2a49fa7e00ca991..0d1e6bd7542dd7c76d2f349de0d0238a8d1b55af 100644 (file)
@@ -192,7 +192,7 @@ static int write_extended_header(struct archiver_args *args,
        unsigned int mode;
        memset(&header, 0, sizeof(header));
        *header.typeflag = TYPEFLAG_EXT_HEADER;
-       mode = 0100666 & ~tar_umask;
+       mode = 0100666;
        sprintf(header.name, "%s.paxheader", sha1_to_hex(sha1));
        prepare_header(args, &header, mode, size);
        write_blocked(&header, sizeof(header));
@@ -300,7 +300,7 @@ static int write_global_extended_header(struct archiver_args *args)
        strbuf_append_ext_header(&ext_header, "comment", sha1_to_hex(sha1), 40);
        memset(&header, 0, sizeof(header));
        *header.typeflag = TYPEFLAG_GLOBAL_HEADER;
-       mode = 0100666 & ~tar_umask;
+       mode = 0100666;
        strcpy(header.name, "pax_global_header");
        prepare_header(args, &header, mode, ext_header.len);
        write_blocked(&header, sizeof(header));
index 5d91f31ca25e0c16fca0e8c23d83706840bf6c57..b87df70f96d0f43e4b838f34d7492d116d4d97db 100644 (file)
--- a/builtin.h
+++ b/builtin.h
@@ -73,6 +73,7 @@ extern int cmd_hash_object(int argc, const char **argv, const char *prefix);
 extern int cmd_help(int argc, const char **argv, const char *prefix);
 extern int cmd_index_pack(int argc, const char **argv, const char *prefix);
 extern int cmd_init_db(int argc, const char **argv, const char *prefix);
+extern int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);
 extern int cmd_log(int argc, const char **argv, const char *prefix);
 extern int cmd_log_reflog(int argc, const char **argv, const char *prefix);
 extern int cmd_ls_files(int argc, const char **argv, const char *prefix);
index 7e113d6c7df7ac602333c81aac84aee5a7e35f2d..3b79c5087fbf1157d1bdfa2cccc99547f8abdb09 100644 (file)
@@ -62,19 +62,19 @@ static unsigned char merge_filter_ref[20];
 static struct string_list output = STRING_LIST_INIT_DUP;
 static unsigned int colopts;
 
-static int parse_branch_color_slot(const char *var, int ofs)
+static int parse_branch_color_slot(const char *slot)
 {
-       if (!strcasecmp(var+ofs, "plain"))
+       if (!strcasecmp(slot, "plain"))
                return BRANCH_COLOR_PLAIN;
-       if (!strcasecmp(var+ofs, "reset"))
+       if (!strcasecmp(slot, "reset"))
                return BRANCH_COLOR_RESET;
-       if (!strcasecmp(var+ofs, "remote"))
+       if (!strcasecmp(slot, "remote"))
                return BRANCH_COLOR_REMOTE;
-       if (!strcasecmp(var+ofs, "local"))
+       if (!strcasecmp(slot, "local"))
                return BRANCH_COLOR_LOCAL;
-       if (!strcasecmp(var+ofs, "current"))
+       if (!strcasecmp(slot, "current"))
                return BRANCH_COLOR_CURRENT;
-       if (!strcasecmp(var+ofs, "upstream"))
+       if (!strcasecmp(slot, "upstream"))
                return BRANCH_COLOR_UPSTREAM;
        return -1;
 }
@@ -90,13 +90,12 @@ static int git_branch_config(const char *var, const char *value, void *cb)
                return 0;
        }
        if (skip_prefix(var, "color.branch.", &slot_name)) {
-               int slot = parse_branch_color_slot(var, slot_name - var);
+               int slot = parse_branch_color_slot(slot_name);
                if (slot < 0)
                        return 0;
                if (!value)
                        return config_error_nonbool(var);
-               color_parse(value, var, branch_colors[slot]);
-               return 0;
+               return color_parse(value, branch_colors[slot]);
        }
        return git_color_default_config(var, value, cb);
 }
index c35505ee6b4030eedc61861081fd21e655b2b3e4..77846762b55bd42f4ab97823ee1b6850dff81714 100644 (file)
@@ -117,8 +117,7 @@ static int git_clean_config(const char *var, const char *value, void *cb)
                        return 0;
                if (!value)
                        return config_error_nonbool(var);
-               color_parse(value, var, clean_colors[slot]);
-               return 0;
+               return color_parse(value, clean_colors[slot]);
        }
 
        if (!strcmp(var, "clean.requireforce")) {
index aa0940e38f7f2c4e562956e5cdb184392b7c7661..e108c5301564a86d396d72169f40cd24a9916a1f 100644 (file)
@@ -1272,22 +1272,21 @@ static int dry_run_commit(int argc, const char **argv, const char *prefix,
        return commitable ? 0 : 1;
 }
 
-static int parse_status_slot(const char *var, int offset)
+static int parse_status_slot(const char *slot)
 {
-       if (!strcasecmp(var+offset, "header"))
+       if (!strcasecmp(slot, "header"))
                return WT_STATUS_HEADER;
-       if (!strcasecmp(var+offset, "branch"))
+       if (!strcasecmp(slot, "branch"))
                return WT_STATUS_ONBRANCH;
-       if (!strcasecmp(var+offset, "updated")
-               || !strcasecmp(var+offset, "added"))
+       if (!strcasecmp(slot, "updated") || !strcasecmp(slot, "added"))
                return WT_STATUS_UPDATED;
-       if (!strcasecmp(var+offset, "changed"))
+       if (!strcasecmp(slot, "changed"))
                return WT_STATUS_CHANGED;
-       if (!strcasecmp(var+offset, "untracked"))
+       if (!strcasecmp(slot, "untracked"))
                return WT_STATUS_UNTRACKED;
-       if (!strcasecmp(var+offset, "nobranch"))
+       if (!strcasecmp(slot, "nobranch"))
                return WT_STATUS_NOBRANCH;
-       if (!strcasecmp(var+offset, "unmerged"))
+       if (!strcasecmp(slot, "unmerged"))
                return WT_STATUS_UNMERGED;
        return -1;
 }
@@ -1327,13 +1326,12 @@ static int git_status_config(const char *k, const char *v, void *cb)
        }
        if (skip_prefix(k, "status.color.", &slot_name) ||
            skip_prefix(k, "color.status.", &slot_name)) {
-               int slot = parse_status_slot(k, slot_name - k);
+               int slot = parse_status_slot(slot_name);
                if (slot < 0)
                        return 0;
                if (!v)
                        return config_error_nonbool(k);
-               color_parse(v, k, s->color_palette[slot]);
-               return 0;
+               return color_parse(v, s->color_palette[slot]);
        }
        if (!strcmp(k, "status.relativepaths")) {
                s->relative_paths = git_config_bool(k, v);
index 37305e93e937ade83072501df6b5d07033ee89d3..8cc2604069cd008297081988ac807412ba9901a7 100644 (file)
@@ -296,7 +296,8 @@ static int git_get_color_config(const char *var, const char *value, void *cb)
        if (!strcmp(var, get_color_slot)) {
                if (!value)
                        config_error_nonbool(var);
-               color_parse(value, var, parsed_color);
+               if (color_parse(value, parsed_color) < 0)
+                       return -1;
                get_color_found = 1;
        }
        return 0;
@@ -309,8 +310,10 @@ static void get_color(const char *def_color)
        git_config_with_options(git_get_color_config, NULL,
                                &given_config_source, respect_includes);
 
-       if (!get_color_found && def_color)
-               color_parse(def_color, "command line", parsed_color);
+       if (!get_color_found && def_color) {
+               if (color_parse(def_color, parsed_color) < 0)
+                       die(_("unable to parse default color value"));
+       }
 
        fputs(parsed_color, stdout);
 }
index 3ee22b96a561ecba2fcd344379fbfb861530a3ae..603a90e29b808a86fa39f031d13a1ca94f5d236e 100644 (file)
@@ -672,7 +672,8 @@ static void populate_value(struct refinfo *ref)
                } else if (starts_with(name, "color:")) {
                        char color[COLOR_MAXLEN] = "";
 
-                       color_parse(name + 6, "--format", color);
+                       if (color_parse(name + 6, color) < 0)
+                               die(_("unable to parse format"));
                        v->s = xstrdup(color);
                        continue;
                } else if (!strcmp(name, "flag")) {
@@ -1011,7 +1012,8 @@ static void show_ref(struct refinfo *info, const char *format, int quote_style)
                struct atom_value resetv;
                char color[COLOR_MAXLEN] = "";
 
-               color_parse("reset", "--format", color);
+               if (color_parse("reset", color) < 0)
+                       die("BUG: couldn't parse 'reset' as a color");
                resetv.s = color;
                print_value(&resetv, quote_style);
        }
index 8343b4027d458d77a720f79d64c9a4c116208f84..b3c818ee01b6f64c7cf00f2e58d372f4dc8f0250 100644 (file)
@@ -421,6 +421,7 @@ static struct {
        const char *help;
 } common_guides[] = {
        { "attributes", N_("Defining attributes per path") },
+       { "everyday", N_("Everyday Git With 20 Commands Or So") },
        { "glossary", N_("A Git glossary") },
        { "ignore", N_("Specifies intentionally untracked files to ignore") },
        { "modules", N_("Defining submodule properties") },
diff --git a/builtin/interpret-trailers.c b/builtin/interpret-trailers.c
new file mode 100644 (file)
index 0000000..46838d2
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Builtin "git interpret-trailers"
+ *
+ * Copyright (c) 2013, 2014 Christian Couder <chriscool@tuxfamily.org>
+ *
+ */
+
+#include "cache.h"
+#include "builtin.h"
+#include "parse-options.h"
+#include "string-list.h"
+#include "trailer.h"
+
+static const char * const git_interpret_trailers_usage[] = {
+       N_("git interpret-trailers [--trim-empty] [(--trailer <token>[(=|:)<value>])...] [<file>...]"),
+       NULL
+};
+
+int cmd_interpret_trailers(int argc, const char **argv, const char *prefix)
+{
+       int trim_empty = 0;
+       struct string_list trailers = STRING_LIST_INIT_DUP;
+
+       struct option options[] = {
+               OPT_BOOL(0, "trim-empty", &trim_empty, N_("trim empty trailers")),
+               OPT_STRING_LIST(0, "trailer", &trailers, N_("trailer"),
+                               N_("trailer(s) to add")),
+               OPT_END()
+       };
+
+       argc = parse_options(argc, argv, prefix, options,
+                            git_interpret_trailers_usage, 0);
+
+       if (argc) {
+               int i;
+               for (i = 0; i < argc; i++)
+                       process_trailers(argv[i], trim_empty, &trailers);
+       } else
+               process_trailers(NULL, trim_empty, &trailers);
+
+       string_list_clear(&trailers, 0);
+
+       return 0;
+}
index 83c0b927070793ed421992a1647e3c0e71721cc3..734aab3a73185964d18c82fe5eb31008bbc409b7 100644 (file)
@@ -391,7 +391,7 @@ static int git_log_config(const char *var, const char *value, void *cb)
                return 0;
        }
        if (skip_prefix(var, "color.decorate.", &slot_name))
-               return parse_decorate_color_config(var, slot_name - var, value);
+               return parse_decorate_color_config(var, slot_name, value);
        if (!strcmp(var, "log.mailmap")) {
                use_mailmap_config = git_config_bool(var, value);
                return 0;
index 27e9dc2c5b37150da7012a794827d331d90380a6..916315ff5c2f08bed1e52c8188939405c0b7a0e1 100644 (file)
@@ -1231,7 +1231,6 @@ static const char *pack_lockfile;
 static const char *unpack(int err_fd, struct shallow_info *si)
 {
        struct pack_header hdr;
-       struct argv_array av = ARGV_ARRAY_INIT;
        const char *hdr_err;
        int status;
        char hdr_arg[38];
@@ -1254,16 +1253,16 @@ static const char *unpack(int err_fd, struct shallow_info *si)
 
        if (si->nr_ours || si->nr_theirs) {
                alt_shallow_file = setup_temporary_shallow(si->shallow);
-               argv_array_pushl(&av, "--shallow-file", alt_shallow_file, NULL);
+               argv_array_push(&child.args, "--shallow-file");
+               argv_array_push(&child.args, alt_shallow_file);
        }
 
        if (ntohl(hdr.hdr_entries) < unpack_limit) {
-               argv_array_pushl(&av, "unpack-objects", hdr_arg, NULL);
+               argv_array_pushl(&child.args, "unpack-objects", hdr_arg, NULL);
                if (quiet)
-                       argv_array_push(&av, "-q");
+                       argv_array_push(&child.args, "-q");
                if (fsck_objects)
-                       argv_array_push(&av, "--strict");
-               child.argv = av.argv;
+                       argv_array_push(&child.args, "--strict");
                child.no_stdout = 1;
                child.err = err_fd;
                child.git_cmd = 1;
@@ -1278,13 +1277,12 @@ static const char *unpack(int err_fd, struct shallow_info *si)
                if (gethostname(keep_arg + s, sizeof(keep_arg) - s))
                        strcpy(keep_arg + s, "localhost");
 
-               argv_array_pushl(&av, "index-pack",
+               argv_array_pushl(&child.args, "index-pack",
                                 "--stdin", hdr_arg, keep_arg, NULL);
                if (fsck_objects)
-                       argv_array_push(&av, "--strict");
+                       argv_array_push(&child.args, "--strict");
                if (fix_thin)
-                       argv_array_push(&av, "--fix-thin");
-               child.argv = av.argv;
+                       argv_array_push(&child.args, "--fix-thin");
                child.out = -1;
                child.err = err_fd;
                child.git_cmd = 1;
diff --git a/color.c b/color.c
index f672885b71acef4108c8cefc9cf887c220f614d3..7941e932d2fb7cb0af5d15dcb2754bc8c96ec657 100644 (file)
--- a/color.c
+++ b/color.c
@@ -60,13 +60,12 @@ static int parse_attr(const char *name, int len)
        return -1;
 }
 
-void color_parse(const char *value, const char *var, char *dst)
+int color_parse(const char *value, char *dst)
 {
-       color_parse_mem(value, strlen(value), var, dst);
+       return color_parse_mem(value, strlen(value), dst);
 }
 
-void color_parse_mem(const char *value, int value_len, const char *var,
-               char *dst)
+int color_parse_mem(const char *value, int value_len, char *dst)
 {
        const char *ptr = value;
        int len = value_len;
@@ -76,7 +75,7 @@ void color_parse_mem(const char *value, int value_len, const char *var,
 
        if (!strncasecmp(value, "reset", len)) {
                strcpy(dst, GIT_COLOR_RESET);
-               return;
+               return 0;
        }
 
        /* [fg [bg]] [attr]... */
@@ -153,9 +152,9 @@ void color_parse_mem(const char *value, int value_len, const char *var,
                *dst++ = 'm';
        }
        *dst = 0;
-       return;
+       return 0;
 bad:
-       die("bad color value '%.*s' for variable '%s'", value_len, value, var);
+       return error(_("invalid color value: %.*s"), value_len, value);
 }
 
 int git_config_colorbool(const char *var, const char *value)
diff --git a/color.h b/color.h
index 9a8495bb7ff06eb4e94e190d902b48c23fc021f9..f5beab1ed782549eb08ad11392ed8e6331bdd653 100644 (file)
--- a/color.h
+++ b/color.h
@@ -77,8 +77,8 @@ int git_color_default_config(const char *var, const char *value, void *cb);
 
 int git_config_colorbool(const char *var, const char *value);
 int want_color(int var);
-void color_parse(const char *value, const char *var, char *dst);
-void color_parse_mem(const char *value, int len, const char *var, char *dst);
+int color_parse(const char *value, char *dst);
+int color_parse_mem(const char *value, int len, char *dst);
 __attribute__((format (printf, 3, 4)))
 int color_fprintf(FILE *fp, const char *color, const char *fmt, ...);
 __attribute__((format (printf, 3, 4)))
index a3ff0c9e60148e6dc98a6bd0d71d98cba4a7eae2..f1eae0810de9d7e405759190221742534c293745 100644 (file)
@@ -62,6 +62,7 @@ git-imap-send                           foreignscminterface
 git-index-pack                          plumbingmanipulators
 git-init                                mainporcelain common
 git-instaweb                            ancillaryinterrogators
+git-interpret-trailers                  purehelpers
 gitk                                    mainporcelain
 git-log                                 mainporcelain common
 git-ls-files                            plumbinginterrogators
index 2ed230a861248f510e8e9d4597faef6f14ffd552..d548e99ebc213cc107b61acbdafa065bbb46dc30 100644 (file)
@@ -281,16 +281,12 @@ __gitcomp_file ()
 # argument, and using the options specified in the second argument.
 __git_ls_files_helper ()
 {
-       (
-               test -n "${CDPATH+set}" && unset CDPATH
-               cd "$1"
-               if [ "$2" == "--committable" ]; then
-                       git diff-index --name-only --relative HEAD
-               else
-                       # NOTE: $2 is not quoted in order to support multiple options
-                       git ls-files --exclude-standard $2
-               fi
-       ) 2>/dev/null
+       if [ "$2" == "--committable" ]; then
+               git -C "$1" diff-index --name-only --relative HEAD
+       else
+               # NOTE: $2 is not quoted in order to support multiple options
+               git -C "$1" ls-files --exclude-standard $2
+       fi 2>/dev/null
 }
 
 
@@ -522,7 +518,7 @@ __git_complete_index_file ()
                ;;
        esac
 
-       __gitcomp_file "$(__git_index_files "$1" "$pfx")" "$pfx" "$cur_"
+       __gitcomp_file "$(__git_index_files "$1" ${pfx:+"$pfx"})" "$pfx" "$cur_"
 }
 
 __git_complete_file ()
diff --git a/contrib/contacts/.gitignore b/contrib/contacts/.gitignore
new file mode 100644 (file)
index 0000000..f385ee6
--- /dev/null
@@ -0,0 +1,3 @@
+git-contacts.1
+git-contacts.html
+git-contacts.xml
diff --git a/contrib/contacts/Makefile b/contrib/contacts/Makefile
new file mode 100644 (file)
index 0000000..a2990f0
--- /dev/null
@@ -0,0 +1,71 @@
+# The default target of this Makefile is...
+all::
+
+-include ../../config.mak.autogen
+-include ../../config.mak
+
+prefix ?= /usr/local
+gitexecdir ?= $(prefix)/libexec/git-core
+mandir ?= $(prefix)/share/man
+man1dir ?= $(mandir)/man1
+htmldir ?= $(prefix)/share/doc/git-doc
+
+../../GIT-VERSION-FILE: FORCE
+       $(MAKE) -C ../../ GIT-VERSION-FILE
+
+-include ../../GIT-VERSION-FILE
+
+# this should be set to a 'standard' bsd-type install program
+INSTALL  ?= install
+RM       ?= rm -f
+
+ASCIIDOC = asciidoc
+XMLTO    = xmlto
+
+ifndef SHELL_PATH
+       SHELL_PATH = /bin/sh
+endif
+SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
+
+ASCIIDOC_CONF = ../../Documentation/asciidoc.conf
+MANPAGE_XSL   = ../../Documentation/manpage-normal.xsl
+
+GIT_CONTACTS := git-contacts
+
+GIT_CONTACTS_DOC := git-contacts.1
+GIT_CONTACTS_XML := git-contacts.xml
+GIT_CONTACTS_TXT := git-contacts.txt
+GIT_CONTACTS_HTML := git-contacts.html
+
+doc: $(GIT_CONTACTS_DOC) $(GIT_CONTACTS_HTML)
+
+install: $(GIT_CONTACTS)
+       $(INSTALL) -d -m 755 $(DESTDIR)$(gitexecdir)
+       $(INSTALL) -m 755 $(GIT_CONTACTS) $(DESTDIR)$(gitexecdir)
+
+install-doc: install-man install-html
+
+install-man: $(GIT_CONTACTS_DOC)
+       $(INSTALL) -d -m 755 $(DESTDIR)$(man1dir)
+       $(INSTALL) -m 644 $^ $(DESTDIR)$(man1dir)
+
+install-html: $(GIT_CONTACTS_HTML)
+       $(INSTALL) -d -m 755 $(DESTDIR)$(htmldir)
+       $(INSTALL) -m 644 $^ $(DESTDIR)$(htmldir)
+
+$(GIT_CONTACTS_DOC): $(GIT_CONTACTS_XML)
+       $(XMLTO) -m $(MANPAGE_XSL) man $^
+
+$(GIT_CONTACTS_XML): $(GIT_CONTACTS_TXT)
+       $(ASCIIDOC) -b docbook -d manpage -f $(ASCIIDOC_CONF) \
+               -agit_version=$(GIT_VERSION) $^
+
+$(GIT_CONTACTS_HTML): $(GIT_CONTACTS_TXT)
+       $(ASCIIDOC) -b xhtml11 -d manpage -f $(ASCIIDOC_CONF) \
+               -agit_version=$(GIT_VERSION) $^
+
+clean:
+       $(RM) $(GIT_CONTACTS)
+       $(RM) *.xml *.html *.1
+
+.PHONY: FORCE
index 91360a3d7f0ad2d589dfe0365d4456d56853e7c0..0b9381abcad34e79a1cdfa129bb9b3e4e187a62e 100644 (file)
@@ -1,6 +1,7 @@
 *~
 git-subtree
-git-subtree.xml
 git-subtree.1
+git-subtree.html
+git-subtree.xml
 mainline
 subproj
index c2bd703ee3aa0fd734c71dc50c01085455d33773..3071baf493442e8bfb3f34252f9c0d13801343b1 100644 (file)
@@ -5,9 +5,10 @@ all::
 -include ../../config.mak
 
 prefix ?= /usr/local
-mandir ?= $(prefix)/share/man
 gitexecdir ?= $(prefix)/libexec/git-core
+mandir ?= $(prefix)/share/man
 man1dir ?= $(mandir)/man1
+htmldir ?= $(prefix)/share/doc/git-doc
 
 ../../GIT-VERSION-FILE: FORCE
        $(MAKE) -C ../../ GIT-VERSION-FILE
@@ -49,12 +50,16 @@ install: $(GIT_SUBTREE)
        $(INSTALL) -d -m 755 $(DESTDIR)$(gitexecdir)
        $(INSTALL) -m 755 $(GIT_SUBTREE) $(DESTDIR)$(gitexecdir)
 
-install-doc: install-man
+install-doc: install-man install-html
 
 install-man: $(GIT_SUBTREE_DOC)
        $(INSTALL) -d -m 755 $(DESTDIR)$(man1dir)
        $(INSTALL) -m 644 $^ $(DESTDIR)$(man1dir)
 
+install-html: $(GIT_SUBTREE_HTML)
+       $(INSTALL) -d -m 755 $(DESTDIR)$(htmldir)
+       $(INSTALL) -m 644 $^ $(DESTDIR)$(htmldir)
+
 $(GIT_SUBTREE_DOC): $(GIT_SUBTREE_XML)
        $(XMLTO) -m $(MANPAGE_XSL) man $^
 
diff --git a/diff.c b/diff.c
index d7a5c81bb8545584ce5fe652dc42f2ee8bc1e2fd..d1bd534caeaf662b0ee0d547d3aa2012310fff57 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -248,8 +248,7 @@ int git_diff_basic_config(const char *var, const char *value, void *cb)
                        return 0;
                if (!value)
                        return config_error_nonbool(var);
-               color_parse(value, var, diff_colors[slot]);
-               return 0;
+               return color_parse(value, diff_colors[slot]);
        }
 
        /* like GNU diff's --suppress-blank-empty option  */
diff --git a/git.c b/git.c
index 4cebf32126014938533b66cb57164c6602a1675c..18fbf79430687a473e9306f2bb65abbaec161bb4 100644 (file)
--- a/git.c
+++ b/git.c
@@ -417,6 +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 },
        { "log", cmd_log, RUN_SETUP },
        { "ls-files", cmd_ls_files, RUN_SETUP },
        { "ls-remote", cmd_ls_remote, RUN_SETUP_GENTLY },
index a9f57d6f9024b2c23d6a2a67d33d84638e3bfb07..ccf75169dd6fb50e2f8ce584af8fa7841fa55fd6 100755 (executable)
@@ -4100,7 +4100,7 @@ sub print_search_form {
        if ($use_pathinfo) {
                $action .= "/".esc_url($project);
        }
-       print $cgi->startform(-method => "get", -action => $action) .
+       print $cgi->start_form(-method => "get", -action => $action) .
              "<div class=\"search\">\n" .
              (!$use_pathinfo &&
              $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
@@ -5510,7 +5510,7 @@ sub git_project_search_form {
        }
 
        print "<div class=\"projsearch\">\n";
-       print $cgi->startform(-method => 'get', -action => $my_uri) .
+       print $cgi->start_form(-method => 'get', -action => $my_uri) .
              $cgi->hidden(-name => 'a', -value => 'project_list')  . "\n";
        print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n"
                if (defined $project_filter);
diff --git a/grep.c b/grep.c
index 99217dc04f5d04c761f094069f1053cb090baaf1..4dc31ea38656f72c51bae96155053cd1cd0f6d64 100644 (file)
--- a/grep.c
+++ b/grep.c
@@ -111,7 +111,7 @@ int grep_config(const char *var, const char *value, void *cb)
        if (color) {
                if (!value)
                        return config_error_nonbool(var);
-               color_parse(value, var, color);
+               return color_parse(value, color);
        }
        return 0;
 }
index cff7ac1dbd8942f3013e6fa91d30d2ea33495854..7f0890e4ac14348e78f7f1e305629fa745a79392 100644 (file)
@@ -56,15 +56,14 @@ static int parse_decorate_color_slot(const char *slot)
        return -1;
 }
 
-int parse_decorate_color_config(const char *var, const int ofs, const char *value)
+int parse_decorate_color_config(const char *var, const char *slot_name, const char *value)
 {
-       int slot = parse_decorate_color_slot(var + ofs);
+       int slot = parse_decorate_color_slot(slot_name);
        if (slot < 0)
                return 0;
        if (!value)
                return config_error_nonbool(var);
-       color_parse(value, var, decoration_colors[slot]);
-       return 0;
+       return color_parse(value, decoration_colors[slot]);
 }
 
 /*
index b26160c4d64b041ed31826a61d4b898c8608c0a2..c8116e60cde34032da6f2044881d5a4970cd96fe 100644 (file)
@@ -7,7 +7,7 @@ struct log_info {
        struct commit *commit, *parent;
 };
 
-int parse_decorate_color_config(const char *var, const int ofs, const char *value);
+int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);
 void init_log_tree_opt(struct rev_info *);
 int log_tree_diff_flush(struct rev_info *);
 int log_tree_commit(struct rev_info *, struct commit *);
index a181ac66875a158514bba48a21911adf9c5cd8be..9d34d02db11bd6761d48484327fbc6d1704ad555 100644 (file)
--- a/pretty.c
+++ b/pretty.c
@@ -964,9 +964,8 @@ static size_t parse_color(struct strbuf *sb, /* in UTF-8 */
                        if (!want_color(c->pretty_ctx->color))
                                return end - placeholder + 1;
                }
-               color_parse_mem(begin,
-                               end - begin,
-                               "--pretty format", color);
+               if (color_parse_mem(begin, end - begin, color) < 0)
+                       die(_("unable to parse --pretty format"));
                strbuf_addstr(sb, color);
                return end - placeholder + 1;
        }
index 52c77ae936d9cb3ecc4ff42a2c4667fca4373577..9952261299e62c88d04954a2a8e738cd6a2adb30 100644 (file)
--- a/t/README
+++ b/t/README
@@ -82,6 +82,12 @@ appropriately before running "make".
        numbers matching <pattern>.  The number matched against is
        simply the running count of the test within the file.
 
+-x::
+       Turn on shell tracing (i.e., `set -x`) during the tests
+       themselves. Implies `--verbose`. Note that this can cause
+       failures in some tests which redirect and test the
+       output of shell functions. Use with caution.
+
 -d::
 --debug::
        This may help the person who is developing a new test.
index ea0bce2dc64b6fe3cac3e00f48b7a17a341af647..91235b76ba76f7e7e9338e37da7106eb550969e6 100755 (executable)
@@ -23,7 +23,7 @@ check_config () {
 }
 
 test_expect_success 'setup default config' '
-       cat >.git/config <<\EOF
+       cat >.git/config <<-\EOF
        [case]
                penguin = very blue
                Movie = BadPhysics
@@ -195,7 +195,7 @@ test_expect_success 'proper error on error in default config files' '
        cp .git/config .git/config.old &&
        test_when_finished "mv .git/config.old .git/config" &&
        echo "[" >>.git/config &&
-       echo "fatal: bad config file line 35 in .git/config" >expect &&
+       echo "fatal: bad config file line 34 in .git/config" >expect &&
        test_expect_code 128 test-config get_value foo.bar 2>actual &&
        test_cmp expect actual
 '
index 83d20c4ba9640fff27777917da0342940c33e0e3..305bcac6b765111106887d4d24d99657c0554b5b 100755 (executable)
@@ -113,9 +113,4 @@ test_expect_success 'archive empty subtree by direct pathspec' '
        check_dir extract sub
 '
 
-test_expect_success 'archive applies umask even for pax headers' '
-       git archive --format=tar HEAD >archive.tar &&
-       ! grep 0666 archive.tar
-'
-
 test_done
index 01c6a3fc1dc3ef028cfdaad749e9e68955f00226..e32e46dee10e96a0b0e51df0eeefd343373f662e 100755 (executable)
@@ -13,8 +13,8 @@ add_blob() {
        before=$(git count-objects | sed "s/ .*//") &&
        BLOB=$(echo aleph_0 | git hash-object -w --stdin) &&
        BLOB_FILE=.git/objects/$(echo $BLOB | sed "s/^../&\//") &&
-       test $((1 + $before)) = $(git count-objects | sed "s/ .*//") &&
-       test -f $BLOB_FILE &&
+       verbose test $((1 + $before)) = $(git count-objects | sed "s/ .*//") &&
+       test_path_is_file $BLOB_FILE &&
        test-chmtime =+0 $BLOB_FILE
 }
 
@@ -35,9 +35,9 @@ test_expect_success 'prune stale packs' '
        : > .git/objects/tmp_2.pack &&
        test-chmtime =-86501 .git/objects/tmp_1.pack &&
        git prune --expire 1.day &&
-       test -f $orig_pack &&
-       test -f .git/objects/tmp_2.pack &&
-       ! test -f .git/objects/tmp_1.pack
+       test_path_is_file $orig_pack &&
+       test_path_is_file .git/objects/tmp_2.pack &&
+       test_path_is_missing .git/objects/tmp_1.pack
 
 '
 
@@ -45,12 +45,12 @@ test_expect_success 'prune --expire' '
 
        add_blob &&
        git prune --expire=1.hour.ago &&
-       test $((1 + $before)) = $(git count-objects | sed "s/ .*//") &&
-       test -f $BLOB_FILE &&
+       verbose test $((1 + $before)) = $(git count-objects | sed "s/ .*//") &&
+       test_path_is_file $BLOB_FILE &&
        test-chmtime =-86500 $BLOB_FILE &&
        git prune --expire 1.day &&
-       test $before = $(git count-objects | sed "s/ .*//") &&
-       ! test -f $BLOB_FILE
+       verbose test $before = $(git count-objects | sed "s/ .*//") &&
+       test_path_is_missing $BLOB_FILE
 
 '
 
@@ -59,12 +59,12 @@ test_expect_success 'gc: implicit prune --expire' '
        add_blob &&
        test-chmtime =-$((2*$week-30)) $BLOB_FILE &&
        git gc &&
-       test $((1 + $before)) = $(git count-objects | sed "s/ .*//") &&
-       test -f $BLOB_FILE &&
+       verbose test $((1 + $before)) = $(git count-objects | sed "s/ .*//") &&
+       test_path_is_file $BLOB_FILE &&
        test-chmtime =-$((2*$week+1)) $BLOB_FILE &&
        git gc &&
-       test $before = $(git count-objects | sed "s/ .*//") &&
-       ! test -f $BLOB_FILE
+       verbose test $before = $(git count-objects | sed "s/ .*//") &&
+       test_path_is_missing $BLOB_FILE
 
 '
 
@@ -110,7 +110,7 @@ test_expect_success 'prune: do not prune detached HEAD with no reflog' '
        git commit --allow-empty -m "detached commit" &&
        # verify that there is no reflogs
        # (should be removed and disabled by previous test)
-       test ! -e .git/logs &&
+       test_path_is_missing .git/logs &&
        git prune -n >prune_actual &&
        : >prune_expected &&
        test_cmp prune_actual prune_expected
@@ -144,8 +144,8 @@ test_expect_success 'gc --no-prune' '
        test-chmtime =-$((5001*$day)) $BLOB_FILE &&
        git config gc.pruneExpire 2.days.ago &&
        git gc --no-prune &&
-       test 1 = $(git count-objects | sed "s/ .*//") &&
-       test -f $BLOB_FILE
+       verbose test 1 = $(git count-objects | sed "s/ .*//") &&
+       test_path_is_file $BLOB_FILE
 
 '
 
@@ -153,10 +153,10 @@ test_expect_success 'gc respects gc.pruneExpire' '
 
        git config gc.pruneExpire 5002.days.ago &&
        git gc &&
-       test -f $BLOB_FILE &&
+       test_path_is_file $BLOB_FILE &&
        git config gc.pruneExpire 5000.days.ago &&
        git gc &&
-       test ! -f $BLOB_FILE
+       test_path_is_missing $BLOB_FILE
 
 '
 
@@ -165,9 +165,9 @@ test_expect_success 'gc --prune=<date>' '
        add_blob &&
        test-chmtime =-$((5001*$day)) $BLOB_FILE &&
        git gc --prune=5002.days.ago &&
-       test -f $BLOB_FILE &&
+       test_path_is_file $BLOB_FILE &&
        git gc --prune=5000.days.ago &&
-       test ! -f $BLOB_FILE
+       test_path_is_missing $BLOB_FILE
 
 '
 
@@ -175,9 +175,9 @@ test_expect_success 'gc --prune=never' '
 
        add_blob &&
        git gc --prune=never &&
-       test -f $BLOB_FILE &&
+       test_path_is_file $BLOB_FILE &&
        git gc --prune=now &&
-       test ! -f $BLOB_FILE
+       test_path_is_missing $BLOB_FILE
 
 '
 
@@ -186,10 +186,10 @@ test_expect_success 'gc respects gc.pruneExpire=never' '
        git config gc.pruneExpire never &&
        add_blob &&
        git gc &&
-       test -f $BLOB_FILE &&
+       test_path_is_file $BLOB_FILE &&
        git config gc.pruneExpire now &&
        git gc &&
-       test ! -f $BLOB_FILE
+       test_path_is_missing $BLOB_FILE
 
 '
 
@@ -197,9 +197,9 @@ test_expect_success 'prune --expire=never' '
 
        add_blob &&
        git prune --expire=never &&
-       test -f $BLOB_FILE &&
+       test_path_is_file $BLOB_FILE &&
        git prune &&
-       test ! -f $BLOB_FILE
+       test_path_is_missing $BLOB_FILE
 
 '
 
@@ -209,11 +209,11 @@ test_expect_success 'gc: prune old objects after local clone' '
        git clone --no-hardlinks . aclone &&
        (
                cd aclone &&
-               test 1 = $(git count-objects | sed "s/ .*//") &&
-               test -f $BLOB_FILE &&
+               verbose test 1 = $(git count-objects | sed "s/ .*//") &&
+               test_path_is_file $BLOB_FILE &&
                git gc --prune &&
-               test 0 = $(git count-objects | sed "s/ .*//") &&
-               ! test -f $BLOB_FILE
+               verbose test 0 = $(git count-objects | sed "s/ .*//") &&
+               test_path_is_missing $BLOB_FILE
        )
 '
 
@@ -250,7 +250,7 @@ test_expect_success 'prune .git/shallow' '
        grep $SHA1 .git/shallow &&
        grep $SHA1 out &&
        git prune &&
-       ! test -f .git/shallow
+       test_path_is_missing .git/shallow
 '
 
 test_done
diff --git a/t/t7513-interpret-trailers.sh b/t/t7513-interpret-trailers.sh
new file mode 100755 (executable)
index 0000000..1efb880
--- /dev/null
@@ -0,0 +1,863 @@
+#!/bin/sh
+#
+# Copyright (c) 2013, 2014 Christian Couder
+#
+
+test_description='git interpret-trailers'
+
+. ./test-lib.sh
+
+# When we want one trailing space at the end of each line, let's use sed
+# to make sure that these spaces are not removed by any automatic tool.
+
+test_expect_success 'setup' '
+       : >empty &&
+       cat >basic_message <<-\EOF &&
+               subject
+
+               body
+       EOF
+       cat >complex_message_body <<-\EOF &&
+               my subject
+
+               my body which is long
+               and contains some special
+               chars like : = ? !
+
+       EOF
+       sed -e "s/ Z\$/ /" >complex_message_trailers <<-\EOF &&
+               Fixes: Z
+               Acked-by: Z
+               Reviewed-by: Z
+               Signed-off-by: Z
+       EOF
+       cat >basic_patch <<-\EOF
+               ---
+                foo.txt | 2 +-
+                1 file changed, 1 insertion(+), 1 deletion(-)
+
+               diff --git a/foo.txt b/foo.txt
+               index 0353767..1d91aa1 100644
+               --- a/foo.txt
+               +++ b/foo.txt
+               @@ -1,3 +1,3 @@
+
+               -bar
+               +baz
+
+               --
+               1.9.rc0.11.ga562ddc
+
+       EOF
+'
+
+test_expect_success 'without config' '
+       sed -e "s/ Z\$/ /" >expected <<-\EOF &&
+
+               ack: Peff
+               Reviewed-by: Z
+               Acked-by: Johan
+       EOF
+       git interpret-trailers --trailer "ack = Peff" --trailer "Reviewed-by" \
+               --trailer "Acked-by: Johan" empty >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'without config in another order' '
+       sed -e "s/ Z\$/ /" >expected <<-\EOF &&
+
+               Acked-by: Johan
+               Reviewed-by: Z
+               ack: Peff
+       EOF
+       git interpret-trailers --trailer "Acked-by: Johan" --trailer "Reviewed-by" \
+               --trailer "ack = Peff" empty >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success '--trim-empty without config' '
+       cat >expected <<-\EOF &&
+
+               ack: Peff
+               Acked-by: Johan
+       EOF
+       git interpret-trailers --trim-empty --trailer ack=Peff \
+               --trailer "Reviewed-by" --trailer "Acked-by: Johan" \
+               --trailer "sob:" empty >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with config option on the command line' '
+       cat >expected <<-\EOF &&
+
+               Acked-by: Johan
+               Reviewed-by: Peff
+       EOF
+       echo "Acked-by: Johan" |
+       git -c "trailer.Acked-by.ifexists=addifdifferent" interpret-trailers \
+               --trailer "Reviewed-by: Peff" --trailer "Acked-by: Johan" >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with config setup' '
+       git config trailer.ack.key "Acked-by: " &&
+       cat >expected <<-\EOF &&
+
+               Acked-by: Peff
+       EOF
+       git interpret-trailers --trim-empty --trailer "ack = Peff" empty >actual &&
+       test_cmp expected actual &&
+       git interpret-trailers --trim-empty --trailer "Acked-by = Peff" empty >actual &&
+       test_cmp expected actual &&
+       git interpret-trailers --trim-empty --trailer "Acked-by :Peff" empty >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with config setup and ":=" as separators' '
+       git config trailer.separators ":=" &&
+       git config trailer.ack.key "Acked-by= " &&
+       cat >expected <<-\EOF &&
+
+               Acked-by= Peff
+       EOF
+       git interpret-trailers --trim-empty --trailer "ack = Peff" empty >actual &&
+       test_cmp expected actual &&
+       git interpret-trailers --trim-empty --trailer "Acked-by= Peff" empty >actual &&
+       test_cmp expected actual &&
+       git interpret-trailers --trim-empty --trailer "Acked-by : Peff" empty >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with config setup and "%" as separators' '
+       git config trailer.separators "%" &&
+       cat >expected <<-\EOF &&
+
+               bug% 42
+               count% 10
+               bug% 422
+       EOF
+       git interpret-trailers --trim-empty --trailer "bug = 42" \
+               --trailer count%10 --trailer "test: stuff" \
+               --trailer "bug % 422" empty >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with "%" as separators and a message with trailers' '
+       cat >special_message <<-\EOF &&
+               Special Message
+
+               bug% 42
+               count% 10
+               bug% 422
+       EOF
+       cat >expected <<-\EOF &&
+               Special Message
+
+               bug% 42
+               count% 10
+               bug% 422
+               count% 100
+       EOF
+       git interpret-trailers --trailer count%100 \
+               special_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with config setup and ":=#" as separators' '
+       git config trailer.separators ":=#" &&
+       git config trailer.bug.key "Bug #" &&
+       cat >expected <<-\EOF &&
+
+               Bug #42
+       EOF
+       git interpret-trailers --trim-empty --trailer "bug = 42" empty >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with commit basic message' '
+       cat basic_message >expected &&
+       echo >>expected &&
+       git interpret-trailers <basic_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with basic patch' '
+       cat basic_message >input &&
+       cat basic_patch >>input &&
+       cat basic_message >expected &&
+       echo >>expected &&
+       cat basic_patch >>expected &&
+       git interpret-trailers <input >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with commit complex message as argument' '
+       cat complex_message_body complex_message_trailers >complex_message &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Fixes: Z
+               Acked-by= Z
+               Reviewed-by: Z
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with 2 files arguments' '
+       cat basic_message >>expected &&
+       echo >>expected &&
+       cat basic_patch >>expected &&
+       git interpret-trailers complex_message input >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with message that has comments' '
+       cat basic_message >>message_with_comments &&
+       sed -e "s/ Z\$/ /" >>message_with_comments <<-\EOF &&
+               # comment
+
+               # other comment
+               Cc: Z
+               # yet another comment
+               Reviewed-by: Johan
+               Reviewed-by: Z
+               # last comment
+
+       EOF
+       cat basic_patch >>message_with_comments &&
+       cat basic_message >expected &&
+       cat >>expected <<-\EOF &&
+               # comment
+
+               Reviewed-by: Johan
+               Cc: Peff
+       EOF
+       cat basic_patch >>expected &&
+       git interpret-trailers --trim-empty --trailer "Cc: Peff" message_with_comments >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with commit complex message and trailer args' '
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Fixes: Z
+               Acked-by= Z
+               Reviewed-by: Z
+               Signed-off-by: Z
+               Acked-by= Peff
+               Bug #42
+       EOF
+       git interpret-trailers --trailer "ack: Peff" \
+               --trailer "bug: 42" <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with complex patch, args and --trim-empty' '
+       cat complex_message >complex_patch &&
+       cat basic_patch >>complex_patch &&
+       cat complex_message_body >expected &&
+       cat >>expected <<-\EOF &&
+               Acked-by= Peff
+               Bug #42
+       EOF
+       cat basic_patch >>expected &&
+       git interpret-trailers --trim-empty --trailer "ack: Peff" \
+               --trailer "bug: 42" <complex_patch >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "where = before"' '
+       git config trailer.bug.where "before" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Reviewed-by: Z
+               Signed-off-by: Z
+               Acked-by= Peff
+       EOF
+       git interpret-trailers --trailer "ack: Peff" \
+               --trailer "bug: 42" complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "where = after"' '
+       git config trailer.ack.where "after" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Peff
+               Reviewed-by: Z
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "ack: Peff" \
+               --trailer "bug: 42" complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "where = end"' '
+       git config trailer.review.key "Reviewed-by" &&
+       git config trailer.review.where "end" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Peff
+               Reviewed-by: Z
+               Signed-off-by: Z
+               Reviewed-by: Junio
+               Reviewed-by: Johannes
+       EOF
+       git interpret-trailers --trailer "ack: Peff" \
+               --trailer "Reviewed-by: Junio" --trailer "Reviewed-by: Johannes" \
+               complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "where = start"' '
+       git config trailer.review.key "Reviewed-by" &&
+       git config trailer.review.where "start" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Reviewed-by: Johannes
+               Reviewed-by: Junio
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Peff
+               Reviewed-by: Z
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "ack: Peff" \
+               --trailer "Reviewed-by: Junio" --trailer "Reviewed-by: Johannes" \
+               complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "where = before" for a token in the middle of the message' '
+       git config trailer.review.key "Reviewed-by:" &&
+       git config trailer.review.where "before" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Peff
+               Reviewed-by:Johan
+               Reviewed-by:
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "ack: Peff" --trailer "bug: 42" \
+               --trailer "review: Johan" <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "where = before" and --trim-empty' '
+       cat complex_message_body >expected &&
+       cat >>expected <<-\EOF &&
+               Bug #46
+               Bug #42
+               Acked-by= Peff
+               Reviewed-by:Johan
+       EOF
+       git interpret-trailers --trim-empty --trailer "ack: Peff" \
+               --trailer "bug: 42" --trailer "review: Johan" \
+               --trailer "Bug: 46" <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'the default is "ifExists = addIfDifferentNeighbor"' '
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Peff
+               Acked-by= Junio
+               Acked-by= Peff
+               Reviewed-by:
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "ack: Peff" --trailer "review:" \
+               --trailer "ack: Junio" --trailer "bug: 42" --trailer "ack: Peff" \
+               --trailer "ack: Peff" <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'default "ifExists" is now "addIfDifferent"' '
+       git config trailer.ifexists "addIfDifferent" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Peff
+               Acked-by= Junio
+               Reviewed-by:
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "ack: Peff" --trailer "review:" \
+               --trailer "ack: Junio" --trailer "bug: 42" --trailer "ack: Peff" \
+               --trailer "ack: Peff" <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "ifExists = addIfDifferent" with "where = end"' '
+       git config trailer.ack.ifExists "addIfDifferent" &&
+       git config trailer.ack.where "end" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Reviewed-by:
+               Signed-off-by: Z
+               Acked-by= Peff
+       EOF
+       git interpret-trailers --trailer "ack: Peff" --trailer "review:" \
+               --trailer "bug: 42" --trailer "ack: Peff" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "ifExists = addIfDifferent" with "where = before"' '
+       git config trailer.ack.ifExists "addIfDifferent" &&
+       git config trailer.ack.where "before" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Peff
+               Acked-by= Z
+               Reviewed-by:
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "ack: Peff" --trailer "review:" \
+               --trailer "bug: 42" --trailer "ack: Peff" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "ifExists = addIfDifferentNeighbor" with "where = end"' '
+       git config trailer.ack.ifExists "addIfDifferentNeighbor" &&
+       git config trailer.ack.where "end" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Reviewed-by:
+               Signed-off-by: Z
+               Acked-by= Peff
+               Acked-by= Junio
+               Tested-by: Jakub
+               Acked-by= Junio
+               Acked-by= Peff
+       EOF
+       git interpret-trailers --trailer "ack: Peff" --trailer "review:" \
+               --trailer "ack: Junio" --trailer "bug: 42" \
+               --trailer "Tested-by: Jakub" --trailer "ack: Junio" \
+               --trailer "ack: Junio" --trailer "ack: Peff" <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "ifExists = addIfDifferentNeighbor"  with "where = after"' '
+       git config trailer.ack.ifExists "addIfDifferentNeighbor" &&
+       git config trailer.ack.where "after" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Peff
+               Acked-by= Junio
+               Acked-by= Peff
+               Reviewed-by:
+               Signed-off-by: Z
+               Tested-by: Jakub
+       EOF
+       git interpret-trailers --trailer "ack: Peff" --trailer "review:" \
+               --trailer "ack: Junio" --trailer "bug: 42" \
+               --trailer "Tested-by: Jakub" --trailer "ack: Junio" \
+               --trailer "ack: Junio" --trailer "ack: Peff" <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "ifExists = addIfDifferentNeighbor" and --trim-empty' '
+       git config trailer.ack.ifExists "addIfDifferentNeighbor" &&
+       cat complex_message_body >expected &&
+       cat >>expected <<-\EOF &&
+               Bug #42
+               Acked-by= Peff
+               Acked-by= Junio
+               Acked-by= Peff
+       EOF
+       git interpret-trailers --trim-empty --trailer "ack: Peff" \
+               --trailer "Acked-by= Peff" --trailer "review:" \
+               --trailer "ack: Junio" --trailer "bug: 42" \
+               --trailer "ack: Peff" <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "ifExists = add" with "where = end"' '
+       git config trailer.ack.ifExists "add" &&
+       git config trailer.ack.where "end" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Reviewed-by:
+               Signed-off-by: Z
+               Acked-by= Peff
+               Acked-by= Peff
+               Tested-by: Jakub
+               Acked-by= Junio
+               Tested-by: Johannes
+               Acked-by= Peff
+       EOF
+       git interpret-trailers --trailer "ack: Peff" \
+               --trailer "Acked-by= Peff" --trailer "review:" \
+               --trailer "Tested-by: Jakub" --trailer "ack: Junio" \
+               --trailer "bug: 42" --trailer "Tested-by: Johannes" \
+               --trailer "ack: Peff" <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "ifExists = add" with "where = after"' '
+       git config trailer.ack.ifExists "add" &&
+       git config trailer.ack.where "after" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Peff
+               Acked-by= Peff
+               Acked-by= Junio
+               Acked-by= Peff
+               Reviewed-by:
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "ack: Peff" \
+               --trailer "Acked-by= Peff" --trailer "review:" \
+               --trailer "ack: Junio" --trailer "bug: 42" \
+               --trailer "ack: Peff" <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "ifExists = replace"' '
+       git config trailer.fix.key "Fixes: " &&
+       git config trailer.fix.ifExists "replace" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Acked-by= Z
+               Acked-by= Junio
+               Acked-by= Peff
+               Reviewed-by:
+               Signed-off-by: Z
+               Fixes: 22
+       EOF
+       git interpret-trailers --trailer "review:" \
+               --trailer "fix=53" --trailer "ack: Junio" --trailer "fix=22" \
+               --trailer "bug: 42" --trailer "ack: Peff" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "ifExists = replace" with "where = after"' '
+       git config trailer.fix.where "after" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: 22
+               Acked-by= Z
+               Acked-by= Junio
+               Acked-by= Peff
+               Reviewed-by:
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "review:" \
+               --trailer "fix=53" --trailer "ack: Junio" --trailer "fix=22" \
+               --trailer "bug: 42" --trailer "ack: Peff" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "ifExists = doNothing"' '
+       git config trailer.fix.ifExists "doNothing" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Junio
+               Acked-by= Peff
+               Reviewed-by:
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "review:" --trailer "fix=53" \
+               --trailer "ack: Junio" --trailer "fix=22" \
+               --trailer "bug: 42" --trailer "ack: Peff" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'the default is "ifMissing = add"' '
+       git config trailer.cc.key "Cc: " &&
+       git config trailer.cc.where "before" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Cc: Linus
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Junio
+               Acked-by= Peff
+               Reviewed-by:
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "review:" --trailer "fix=53" \
+               --trailer "cc=Linus" --trailer "ack: Junio" \
+               --trailer "fix=22" --trailer "bug: 42" --trailer "ack: Peff" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'when default "ifMissing" is "doNothing"' '
+       git config trailer.ifmissing "doNothing" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Junio
+               Acked-by= Peff
+               Reviewed-by:
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "review:" --trailer "fix=53" \
+               --trailer "cc=Linus" --trailer "ack: Junio" \
+               --trailer "fix=22" --trailer "bug: 42" --trailer "ack: Peff" \
+               <complex_message >actual &&
+       test_cmp expected actual &&
+       git config trailer.ifmissing "add"
+'
+
+test_expect_success 'using "ifMissing = add" with "where = end"' '
+       git config trailer.cc.key "Cc: " &&
+       git config trailer.cc.where "end" &&
+       git config trailer.cc.ifMissing "add" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Junio
+               Acked-by= Peff
+               Reviewed-by:
+               Signed-off-by: Z
+               Cc: Linus
+       EOF
+       git interpret-trailers --trailer "review:" --trailer "fix=53" \
+               --trailer "ack: Junio" --trailer "fix=22" \
+               --trailer "bug: 42" --trailer "cc=Linus" --trailer "ack: Peff" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "ifMissing = add" with "where = before"' '
+       git config trailer.cc.key "Cc: " &&
+       git config trailer.cc.where "before" &&
+       git config trailer.cc.ifMissing "add" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Cc: Linus
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Junio
+               Acked-by= Peff
+               Reviewed-by:
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "review:" --trailer "fix=53" \
+               --trailer "ack: Junio" --trailer "fix=22" \
+               --trailer "bug: 42" --trailer "cc=Linus" --trailer "ack: Peff" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'using "ifMissing = doNothing"' '
+       git config trailer.cc.ifMissing "doNothing" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Junio
+               Acked-by= Peff
+               Reviewed-by:
+               Signed-off-by: Z
+       EOF
+       git interpret-trailers --trailer "review:" --trailer "fix=53" \
+               --trailer "cc=Linus" --trailer "ack: Junio" \
+               --trailer "fix=22" --trailer "bug: 42" --trailer "ack: Peff" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'default "where" is now "after"' '
+       git config trailer.where "after" &&
+       git config --unset trailer.ack.where &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Bug #42
+               Fixes: Z
+               Acked-by= Z
+               Acked-by= Peff
+               Acked-by= Peff
+               Acked-by= Junio
+               Acked-by= Peff
+               Reviewed-by:
+               Signed-off-by: Z
+               Tested-by: Jakub
+               Tested-by: Johannes
+       EOF
+       git interpret-trailers --trailer "ack: Peff" \
+               --trailer "Acked-by= Peff" --trailer "review:" \
+               --trailer "Tested-by: Jakub" --trailer "ack: Junio" \
+               --trailer "bug: 42" --trailer "Tested-by: Johannes" \
+               --trailer "ack: Peff" <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with simple command' '
+       git config trailer.sign.key "Signed-off-by: " &&
+       git config trailer.sign.where "after" &&
+       git config trailer.sign.ifExists "addIfDifferentNeighbor" &&
+       git config trailer.sign.command "echo \"A U Thor <author@example.com>\"" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Fixes: Z
+               Acked-by= Z
+               Reviewed-by:
+               Signed-off-by: Z
+               Signed-off-by: A U Thor <author@example.com>
+       EOF
+       git interpret-trailers --trailer "review:" --trailer "fix=22" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with command using commiter information' '
+       git config trailer.sign.ifExists "addIfDifferent" &&
+       git config trailer.sign.command "echo \"\$GIT_COMMITTER_NAME <\$GIT_COMMITTER_EMAIL>\"" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Fixes: Z
+               Acked-by= Z
+               Reviewed-by:
+               Signed-off-by: Z
+               Signed-off-by: C O Mitter <committer@example.com>
+       EOF
+       git interpret-trailers --trailer "review:" --trailer "fix=22" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with command using author information' '
+       git config trailer.sign.key "Signed-off-by: " &&
+       git config trailer.sign.where "after" &&
+       git config trailer.sign.ifExists "addIfDifferentNeighbor" &&
+       git config trailer.sign.command "echo \"\$GIT_AUTHOR_NAME <\$GIT_AUTHOR_EMAIL>\"" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-\EOF &&
+               Fixes: Z
+               Acked-by= Z
+               Reviewed-by:
+               Signed-off-by: Z
+               Signed-off-by: A U Thor <author@example.com>
+       EOF
+       git interpret-trailers --trailer "review:" --trailer "fix=22" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'setup a commit' '
+       echo "Content of the first commit." > a.txt &&
+       git add a.txt &&
+       git commit -m "Add file a.txt"
+'
+
+test_expect_success 'with command using $ARG' '
+       git config trailer.fix.ifExists "replace" &&
+       git config trailer.fix.command "git log -1 --oneline --format=\"%h (%s)\" --abbrev-commit --abbrev=14 \$ARG" &&
+       FIXED=$(git log -1 --oneline --format="%h (%s)" --abbrev-commit --abbrev=14 HEAD) &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-EOF &&
+               Fixes: $FIXED
+               Acked-by= Z
+               Reviewed-by:
+               Signed-off-by: Z
+               Signed-off-by: A U Thor <author@example.com>
+       EOF
+       git interpret-trailers --trailer "review:" --trailer "fix=HEAD" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with failing command using $ARG' '
+       git config trailer.fix.ifExists "replace" &&
+       git config trailer.fix.command "false \$ARG" &&
+       cat complex_message_body >expected &&
+       sed -e "s/ Z\$/ /" >>expected <<-EOF &&
+               Fixes: Z
+               Acked-by= Z
+               Reviewed-by:
+               Signed-off-by: Z
+               Signed-off-by: A U Thor <author@example.com>
+       EOF
+       git interpret-trailers --trailer "review:" --trailer "fix=HEAD" \
+               <complex_message >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'with empty tokens' '
+       git config --unset trailer.fix.command &&
+       cat >expected <<-EOF &&
+
+               Signed-off-by: A U Thor <author@example.com>
+       EOF
+       git interpret-trailers --trailer ":" --trailer ":test" >actual <<-EOF &&
+       EOF
+       test_cmp expected actual
+'
+
+test_expect_success 'with command but no key' '
+       git config --unset trailer.sign.key &&
+       cat >expected <<-EOF &&
+
+               sign: A U Thor <author@example.com>
+       EOF
+       git interpret-trailers >actual <<-EOF &&
+       EOF
+       test_cmp expected actual
+'
+
+test_expect_success 'with no command and no key' '
+       git config --unset trailer.review.key &&
+       cat >expected <<-EOF &&
+
+               review: Junio
+               sign: A U Thor <author@example.com>
+       EOF
+       git interpret-trailers --trailer "review:Junio" >actual <<-EOF &&
+       EOF
+       test_cmp expected actual
+'
+
+test_done
index dafd6ad21a92bac48155af39ebb9a4baaf3c4970..b7957b87bb9602748d6f84eeaaeb3c6820b1142e 100644 (file)
@@ -634,6 +634,15 @@ test_cmp_bin() {
        cmp "$@"
 }
 
+# Call any command "$@" but be more verbose about its
+# failure. This is handy for commands like "test" which do
+# not output anything when they fail.
+verbose () {
+       "$@" && return 0
+       echo >&2 "command failed: $(git rev-parse --sq-quote "$@")"
+       return 1
+}
+
 # Check if the file expected to be empty is indeed empty, and barfs
 # otherwise.
 
index 0f4a67bfc63a989883007911f889ba5aed1c0aac..cf19339ccebd0ee5f55822ebe11386101f358f41 100644 (file)
@@ -233,6 +233,10 @@ do
        --root=*)
                root=$(expr "z$1" : 'z[^=]*=\(.*\)')
                shift ;;
+       -x)
+               trace=t
+               verbose=t
+               shift ;;
        *)
                echo "error: unknown test option '$1'" >&2; exit 1 ;;
        esac
@@ -517,10 +521,39 @@ maybe_setup_valgrind () {
        fi
 }
 
+# This is a separate function because some tests use
+# "return" to end a test_expect_success block early
+# (and we want to make sure we run any cleanup like
+# "set +x").
+test_eval_inner_ () {
+       # Do not add anything extra (including LF) after '$*'
+       eval "
+               test \"$trace\" = t && set -x
+               $*"
+}
+
 test_eval_ () {
-       # This is a separate function because some tests use
-       # "return" to end a test_expect_success block early.
-       eval </dev/null >&3 2>&4 "$*"
+       # We run this block with stderr redirected to avoid extra cruft
+       # during a "-x" trace. Once in "set -x" mode, we cannot prevent
+       # the shell from printing the "set +x" to turn it off (nor the saving
+       # of $? before that). But we can make sure that the output goes to
+       # /dev/null.
+       #
+       # The test itself is run with stderr put back to &4 (so either to
+       # /dev/null, or to the original stderr if --verbose was used).
+       {
+               test_eval_inner_ "$@" </dev/null >&3 2>&4
+               test_eval_ret_=$?
+               if test "$trace" = t
+               then
+                       set +x
+                       if test "$test_eval_ret_" != 0
+                       then
+                               say_color error >&4 "error: last command exited with \$?=$test_eval_ret_"
+                       fi
+               fi
+       } 2>/dev/null
+       return $test_eval_ret_
 }
 
 test_run_ () {
@@ -531,7 +564,8 @@ test_run_ () {
        eval_ret=$?
        teardown_malloc_check
 
-       if test -z "$immediate" || test $eval_ret = 0 || test -n "$expecting_failure"
+       if test -z "$immediate" || test $eval_ret = 0 ||
+          test -n "$expecting_failure" && test "$test_cleanup" != ":"
        then
                setup_malloc_check
                test_eval_ "$test_cleanup"
diff --git a/trace.c b/trace.c
index b6f25a23fdf8b6eec64181b5d6b56909bbe4ae7b..477860894196e32d7db990459cef47175277c441 100644 (file)
--- a/trace.c
+++ b/trace.c
@@ -385,7 +385,7 @@ static inline uint64_t gettimeofday_nanos(void)
  * Returns nanoseconds since the epoch (01/01/1970), for performance tracing
  * (i.e. favoring high precision over wall clock time accuracy).
  */
-inline uint64_t getnanotime(void)
+uint64_t getnanotime(void)
 {
        static uint64_t offset;
        if (offset > 1) {
diff --git a/trailer.c b/trailer.c
new file mode 100644 (file)
index 0000000..8514566
--- /dev/null
+++ b/trailer.c
@@ -0,0 +1,851 @@
+#include "cache.h"
+#include "string-list.h"
+#include "run-command.h"
+#include "string-list.h"
+#include "trailer.h"
+/*
+ * Copyright (c) 2013, 2014 Christian Couder <chriscool@tuxfamily.org>
+ */
+
+enum action_where { WHERE_END, WHERE_AFTER, WHERE_BEFORE, WHERE_START };
+enum action_if_exists { EXISTS_ADD_IF_DIFFERENT_NEIGHBOR, EXISTS_ADD_IF_DIFFERENT,
+                       EXISTS_ADD, EXISTS_REPLACE, EXISTS_DO_NOTHING };
+enum action_if_missing { MISSING_ADD, MISSING_DO_NOTHING };
+
+struct conf_info {
+       char *name;
+       char *key;
+       char *command;
+       enum action_where where;
+       enum action_if_exists if_exists;
+       enum action_if_missing if_missing;
+};
+
+static struct conf_info default_conf_info;
+
+struct trailer_item {
+       struct trailer_item *previous;
+       struct trailer_item *next;
+       const char *token;
+       const char *value;
+       struct conf_info conf;
+};
+
+static struct trailer_item *first_conf_item;
+
+static char *separators = ":";
+
+#define TRAILER_ARG_STRING "$ARG"
+
+static int after_or_end(enum action_where where)
+{
+       return (where == WHERE_AFTER) || (where == WHERE_END);
+}
+
+/*
+ * Return the length of the string not including any final
+ * punctuation. E.g., the input "Signed-off-by:" would return
+ * 13, stripping the trailing punctuation but retaining
+ * internal punctuation.
+ */
+static size_t token_len_without_separator(const char *token, size_t len)
+{
+       while (len > 0 && !isalnum(token[len - 1]))
+               len--;
+       return len;
+}
+
+static int same_token(struct trailer_item *a, struct trailer_item *b)
+{
+       size_t a_len = token_len_without_separator(a->token, strlen(a->token));
+       size_t b_len = token_len_without_separator(b->token, strlen(b->token));
+       size_t min_len = (a_len > b_len) ? b_len : a_len;
+
+       return !strncasecmp(a->token, b->token, min_len);
+}
+
+static int same_value(struct trailer_item *a, struct trailer_item *b)
+{
+       return !strcasecmp(a->value, b->value);
+}
+
+static int same_trailer(struct trailer_item *a, struct trailer_item *b)
+{
+       return same_token(a, b) && same_value(a, b);
+}
+
+static inline int contains_only_spaces(const char *str)
+{
+       const char *s = str;
+       while (*s && isspace(*s))
+               s++;
+       return !*s;
+}
+
+static inline void strbuf_replace(struct strbuf *sb, const char *a, const char *b)
+{
+       const char *ptr = strstr(sb->buf, a);
+       if (ptr)
+               strbuf_splice(sb, ptr - sb->buf, strlen(a), b, strlen(b));
+}
+
+static void free_trailer_item(struct trailer_item *item)
+{
+       free(item->conf.name);
+       free(item->conf.key);
+       free(item->conf.command);
+       free((char *)item->token);
+       free((char *)item->value);
+       free(item);
+}
+
+static char last_non_space_char(const char *s)
+{
+       int i;
+       for (i = strlen(s) - 1; i >= 0; i--)
+               if (!isspace(s[i]))
+                       return s[i];
+       return '\0';
+}
+
+static void print_tok_val(const char *tok, const char *val)
+{
+       char c = last_non_space_char(tok);
+       if (!c)
+               return;
+       if (strchr(separators, c))
+               printf("%s%s\n", tok, val);
+       else
+               printf("%s%c %s\n", tok, separators[0], val);
+}
+
+static void print_all(struct trailer_item *first, int trim_empty)
+{
+       struct trailer_item *item;
+       for (item = first; item; item = item->next) {
+               if (!trim_empty || strlen(item->value) > 0)
+                       print_tok_val(item->token, item->value);
+       }
+}
+
+static void update_last(struct trailer_item **last)
+{
+       if (*last)
+               while ((*last)->next != NULL)
+                       *last = (*last)->next;
+}
+
+static void update_first(struct trailer_item **first)
+{
+       if (*first)
+               while ((*first)->previous != NULL)
+                       *first = (*first)->previous;
+}
+
+static void add_arg_to_input_list(struct trailer_item *on_tok,
+                                 struct trailer_item *arg_tok,
+                                 struct trailer_item **first,
+                                 struct trailer_item **last)
+{
+       if (after_or_end(arg_tok->conf.where)) {
+               arg_tok->next = on_tok->next;
+               on_tok->next = arg_tok;
+               arg_tok->previous = on_tok;
+               if (arg_tok->next)
+                       arg_tok->next->previous = arg_tok;
+               update_last(last);
+       } else {
+               arg_tok->previous = on_tok->previous;
+               on_tok->previous = arg_tok;
+               arg_tok->next = on_tok;
+               if (arg_tok->previous)
+                       arg_tok->previous->next = arg_tok;
+               update_first(first);
+       }
+}
+
+static int check_if_different(struct trailer_item *in_tok,
+                             struct trailer_item *arg_tok,
+                             int check_all)
+{
+       enum action_where where = arg_tok->conf.where;
+       do {
+               if (!in_tok)
+                       return 1;
+               if (same_trailer(in_tok, arg_tok))
+                       return 0;
+               /*
+                * if we want to add a trailer after another one,
+                * we have to check those before this one
+                */
+               in_tok = after_or_end(where) ? in_tok->previous : in_tok->next;
+       } while (check_all);
+       return 1;
+}
+
+static void remove_from_list(struct trailer_item *item,
+                            struct trailer_item **first,
+                            struct trailer_item **last)
+{
+       struct trailer_item *next = item->next;
+       struct trailer_item *previous = item->previous;
+
+       if (next) {
+               item->next->previous = previous;
+               item->next = NULL;
+       } else if (last)
+               *last = previous;
+
+       if (previous) {
+               item->previous->next = next;
+               item->previous = NULL;
+       } else if (first)
+               *first = next;
+}
+
+static struct trailer_item *remove_first(struct trailer_item **first)
+{
+       struct trailer_item *item = *first;
+       *first = item->next;
+       if (item->next) {
+               item->next->previous = NULL;
+               item->next = NULL;
+       }
+       return item;
+}
+
+static int read_from_command(struct child_process *cp, struct strbuf *buf)
+{
+       if (run_command(cp))
+               return error("running trailer command '%s' failed", cp->argv[0]);
+       if (strbuf_read(buf, cp->out, 1024) < 1)
+               return error("reading from trailer command '%s' failed", cp->argv[0]);
+       strbuf_trim(buf);
+       return 0;
+}
+
+static const char *apply_command(const char *command, const char *arg)
+{
+       struct strbuf cmd = STRBUF_INIT;
+       struct strbuf buf = STRBUF_INIT;
+       struct child_process cp;
+       const char *argv[] = {NULL, NULL};
+       const char *result;
+
+       strbuf_addstr(&cmd, command);
+       if (arg)
+               strbuf_replace(&cmd, TRAILER_ARG_STRING, arg);
+
+       argv[0] = cmd.buf;
+       memset(&cp, 0, sizeof(cp));
+       cp.argv = argv;
+       cp.env = local_repo_env;
+       cp.no_stdin = 1;
+       cp.out = -1;
+       cp.use_shell = 1;
+
+       if (read_from_command(&cp, &buf)) {
+               strbuf_release(&buf);
+               result = xstrdup("");
+       } else
+               result = strbuf_detach(&buf, NULL);
+
+       strbuf_release(&cmd);
+       return result;
+}
+
+static void apply_item_command(struct trailer_item *in_tok, struct trailer_item *arg_tok)
+{
+       if (arg_tok->conf.command) {
+               const char *arg;
+               if (arg_tok->value && arg_tok->value[0]) {
+                       arg = arg_tok->value;
+               } else {
+                       if (in_tok && in_tok->value)
+                               arg = xstrdup(in_tok->value);
+                       else
+                               arg = xstrdup("");
+               }
+               arg_tok->value = apply_command(arg_tok->conf.command, arg);
+               free((char *)arg);
+       }
+}
+
+static void apply_arg_if_exists(struct trailer_item *in_tok,
+                               struct trailer_item *arg_tok,
+                               struct trailer_item *on_tok,
+                               struct trailer_item **in_tok_first,
+                               struct trailer_item **in_tok_last)
+{
+       switch (arg_tok->conf.if_exists) {
+       case EXISTS_DO_NOTHING:
+               free_trailer_item(arg_tok);
+               break;
+       case EXISTS_REPLACE:
+               apply_item_command(in_tok, arg_tok);
+               add_arg_to_input_list(on_tok, arg_tok,
+                                     in_tok_first, in_tok_last);
+               remove_from_list(in_tok, in_tok_first, in_tok_last);
+               free_trailer_item(in_tok);
+               break;
+       case EXISTS_ADD:
+               apply_item_command(in_tok, arg_tok);
+               add_arg_to_input_list(on_tok, arg_tok,
+                                     in_tok_first, in_tok_last);
+               break;
+       case EXISTS_ADD_IF_DIFFERENT:
+               apply_item_command(in_tok, arg_tok);
+               if (check_if_different(in_tok, arg_tok, 1))
+                       add_arg_to_input_list(on_tok, arg_tok,
+                                             in_tok_first, in_tok_last);
+               else
+                       free_trailer_item(arg_tok);
+               break;
+       case EXISTS_ADD_IF_DIFFERENT_NEIGHBOR:
+               apply_item_command(in_tok, arg_tok);
+               if (check_if_different(on_tok, arg_tok, 0))
+                       add_arg_to_input_list(on_tok, arg_tok,
+                                             in_tok_first, in_tok_last);
+               else
+                       free_trailer_item(arg_tok);
+               break;
+       }
+}
+
+static void apply_arg_if_missing(struct trailer_item **in_tok_first,
+                                struct trailer_item **in_tok_last,
+                                struct trailer_item *arg_tok)
+{
+       struct trailer_item **in_tok;
+       enum action_where where;
+
+       switch (arg_tok->conf.if_missing) {
+       case MISSING_DO_NOTHING:
+               free_trailer_item(arg_tok);
+               break;
+       case MISSING_ADD:
+               where = arg_tok->conf.where;
+               in_tok = after_or_end(where) ? in_tok_last : in_tok_first;
+               apply_item_command(NULL, arg_tok);
+               if (*in_tok) {
+                       add_arg_to_input_list(*in_tok, arg_tok,
+                                             in_tok_first, in_tok_last);
+               } else {
+                       *in_tok_first = arg_tok;
+                       *in_tok_last = arg_tok;
+               }
+               break;
+       }
+}
+
+static int find_same_and_apply_arg(struct trailer_item **in_tok_first,
+                                  struct trailer_item **in_tok_last,
+                                  struct trailer_item *arg_tok)
+{
+       struct trailer_item *in_tok;
+       struct trailer_item *on_tok;
+       struct trailer_item *following_tok;
+
+       enum action_where where = arg_tok->conf.where;
+       int middle = (where == WHERE_AFTER) || (where == WHERE_BEFORE);
+       int backwards = after_or_end(where);
+       struct trailer_item *start_tok = backwards ? *in_tok_last : *in_tok_first;
+
+       for (in_tok = start_tok; in_tok; in_tok = following_tok) {
+               following_tok = backwards ? in_tok->previous : in_tok->next;
+               if (!same_token(in_tok, arg_tok))
+                       continue;
+               on_tok = middle ? in_tok : start_tok;
+               apply_arg_if_exists(in_tok, arg_tok, on_tok,
+                                   in_tok_first, in_tok_last);
+               return 1;
+       }
+       return 0;
+}
+
+static void process_trailers_lists(struct trailer_item **in_tok_first,
+                                  struct trailer_item **in_tok_last,
+                                  struct trailer_item **arg_tok_first)
+{
+       struct trailer_item *arg_tok;
+       struct trailer_item *next_arg;
+
+       if (!*arg_tok_first)
+               return;
+
+       for (arg_tok = *arg_tok_first; arg_tok; arg_tok = next_arg) {
+               int applied = 0;
+
+               next_arg = arg_tok->next;
+               remove_from_list(arg_tok, arg_tok_first, NULL);
+
+               applied = find_same_and_apply_arg(in_tok_first,
+                                                 in_tok_last,
+                                                 arg_tok);
+
+               if (!applied)
+                       apply_arg_if_missing(in_tok_first,
+                                            in_tok_last,
+                                            arg_tok);
+       }
+}
+
+static int set_where(struct conf_info *item, const char *value)
+{
+       if (!strcasecmp("after", value))
+               item->where = WHERE_AFTER;
+       else if (!strcasecmp("before", value))
+               item->where = WHERE_BEFORE;
+       else if (!strcasecmp("end", value))
+               item->where = WHERE_END;
+       else if (!strcasecmp("start", value))
+               item->where = WHERE_START;
+       else
+               return -1;
+       return 0;
+}
+
+static int set_if_exists(struct conf_info *item, const char *value)
+{
+       if (!strcasecmp("addIfDifferent", value))
+               item->if_exists = EXISTS_ADD_IF_DIFFERENT;
+       else if (!strcasecmp("addIfDifferentNeighbor", value))
+               item->if_exists = EXISTS_ADD_IF_DIFFERENT_NEIGHBOR;
+       else if (!strcasecmp("add", value))
+               item->if_exists = EXISTS_ADD;
+       else if (!strcasecmp("replace", value))
+               item->if_exists = EXISTS_REPLACE;
+       else if (!strcasecmp("doNothing", value))
+               item->if_exists = EXISTS_DO_NOTHING;
+       else
+               return -1;
+       return 0;
+}
+
+static int set_if_missing(struct conf_info *item, const char *value)
+{
+       if (!strcasecmp("doNothing", value))
+               item->if_missing = MISSING_DO_NOTHING;
+       else if (!strcasecmp("add", value))
+               item->if_missing = MISSING_ADD;
+       else
+               return -1;
+       return 0;
+}
+
+static void duplicate_conf(struct conf_info *dst, struct conf_info *src)
+{
+       *dst = *src;
+       if (src->name)
+               dst->name = xstrdup(src->name);
+       if (src->key)
+               dst->key = xstrdup(src->key);
+       if (src->command)
+               dst->command = xstrdup(src->command);
+}
+
+static struct trailer_item *get_conf_item(const char *name)
+{
+       struct trailer_item *item;
+       struct trailer_item *previous;
+
+       /* Look up item with same name */
+       for (previous = NULL, item = first_conf_item;
+            item;
+            previous = item, item = item->next) {
+               if (!strcasecmp(item->conf.name, name))
+                       return item;
+       }
+
+       /* Item does not already exists, create it */
+       item = xcalloc(sizeof(struct trailer_item), 1);
+       duplicate_conf(&item->conf, &default_conf_info);
+       item->conf.name = xstrdup(name);
+
+       if (!previous)
+               first_conf_item = item;
+       else {
+               previous->next = item;
+               item->previous = previous;
+       }
+
+       return item;
+}
+
+enum trailer_info_type { TRAILER_KEY, TRAILER_COMMAND, TRAILER_WHERE,
+                        TRAILER_IF_EXISTS, TRAILER_IF_MISSING };
+
+static struct {
+       const char *name;
+       enum trailer_info_type type;
+} trailer_config_items[] = {
+       { "key", TRAILER_KEY },
+       { "command", TRAILER_COMMAND },
+       { "where", TRAILER_WHERE },
+       { "ifexists", TRAILER_IF_EXISTS },
+       { "ifmissing", TRAILER_IF_MISSING }
+};
+
+static int git_trailer_default_config(const char *conf_key, const char *value, void *cb)
+{
+       const char *trailer_item, *variable_name;
+
+       if (!skip_prefix(conf_key, "trailer.", &trailer_item))
+               return 0;
+
+       variable_name = strrchr(trailer_item, '.');
+       if (!variable_name) {
+               if (!strcmp(trailer_item, "where")) {
+                       if (set_where(&default_conf_info, value) < 0)
+                               warning(_("unknown value '%s' for key '%s'"),
+                                       value, conf_key);
+               } else if (!strcmp(trailer_item, "ifexists")) {
+                       if (set_if_exists(&default_conf_info, value) < 0)
+                               warning(_("unknown value '%s' for key '%s'"),
+                                       value, conf_key);
+               } else if (!strcmp(trailer_item, "ifmissing")) {
+                       if (set_if_missing(&default_conf_info, value) < 0)
+                               warning(_("unknown value '%s' for key '%s'"),
+                                       value, conf_key);
+               } else if (!strcmp(trailer_item, "separators")) {
+                       separators = xstrdup(value);
+               }
+       }
+       return 0;
+}
+
+static int git_trailer_config(const char *conf_key, const char *value, void *cb)
+{
+       const char *trailer_item, *variable_name;
+       struct trailer_item *item;
+       struct conf_info *conf;
+       char *name = NULL;
+       enum trailer_info_type type;
+       int i;
+
+       if (!skip_prefix(conf_key, "trailer.", &trailer_item))
+               return 0;
+
+       variable_name = strrchr(trailer_item, '.');
+       if (!variable_name)
+               return 0;
+
+       variable_name++;
+       for (i = 0; i < ARRAY_SIZE(trailer_config_items); i++) {
+               if (strcmp(trailer_config_items[i].name, variable_name))
+                       continue;
+               name = xstrndup(trailer_item,  variable_name - trailer_item - 1);
+               type = trailer_config_items[i].type;
+               break;
+       }
+
+       if (!name)
+               return 0;
+
+       item = get_conf_item(name);
+       conf = &item->conf;
+       free(name);
+
+       switch (type) {
+       case TRAILER_KEY:
+               if (conf->key)
+                       warning(_("more than one %s"), conf_key);
+               conf->key = xstrdup(value);
+               break;
+       case TRAILER_COMMAND:
+               if (conf->command)
+                       warning(_("more than one %s"), conf_key);
+               conf->command = xstrdup(value);
+               break;
+       case TRAILER_WHERE:
+               if (set_where(conf, value))
+                       warning(_("unknown value '%s' for key '%s'"), value, conf_key);
+               break;
+       case TRAILER_IF_EXISTS:
+               if (set_if_exists(conf, value))
+                       warning(_("unknown value '%s' for key '%s'"), value, conf_key);
+               break;
+       case TRAILER_IF_MISSING:
+               if (set_if_missing(conf, value))
+                       warning(_("unknown value '%s' for key '%s'"), value, conf_key);
+               break;
+       default:
+               die("internal bug in trailer.c");
+       }
+       return 0;
+}
+
+static int parse_trailer(struct strbuf *tok, struct strbuf *val, const char *trailer)
+{
+       size_t len;
+       struct strbuf seps = STRBUF_INIT;
+       strbuf_addstr(&seps, separators);
+       strbuf_addch(&seps, '=');
+       len = strcspn(trailer, seps.buf);
+       strbuf_release(&seps);
+       if (len == 0)
+               return error(_("empty trailer token in trailer '%s'"), trailer);
+       if (len < strlen(trailer)) {
+               strbuf_add(tok, trailer, len);
+               strbuf_trim(tok);
+               strbuf_addstr(val, trailer + len + 1);
+               strbuf_trim(val);
+       } else {
+               strbuf_addstr(tok, trailer);
+               strbuf_trim(tok);
+       }
+       return 0;
+}
+
+static const char *token_from_item(struct trailer_item *item, char *tok)
+{
+       if (item->conf.key)
+               return item->conf.key;
+       if (tok)
+               return tok;
+       return item->conf.name;
+}
+
+static struct trailer_item *new_trailer_item(struct trailer_item *conf_item,
+                                            char *tok, char *val)
+{
+       struct trailer_item *new = xcalloc(sizeof(*new), 1);
+       new->value = val ? val : xstrdup("");
+
+       if (conf_item) {
+               duplicate_conf(&new->conf, &conf_item->conf);
+               new->token = xstrdup(token_from_item(conf_item, tok));
+               free(tok);
+       } else {
+               duplicate_conf(&new->conf, &default_conf_info);
+               new->token = tok;
+       }
+
+       return new;
+}
+
+static int token_matches_item(const char *tok, struct trailer_item *item, int tok_len)
+{
+       if (!strncasecmp(tok, item->conf.name, tok_len))
+               return 1;
+       return item->conf.key ? !strncasecmp(tok, item->conf.key, tok_len) : 0;
+}
+
+static struct trailer_item *create_trailer_item(const char *string)
+{
+       struct strbuf tok = STRBUF_INIT;
+       struct strbuf val = STRBUF_INIT;
+       struct trailer_item *item;
+       int tok_len;
+
+       if (parse_trailer(&tok, &val, string))
+               return NULL;
+
+       tok_len = token_len_without_separator(tok.buf, tok.len);
+
+       /* Lookup if the token matches something in the config */
+       for (item = first_conf_item; item; item = item->next) {
+               if (token_matches_item(tok.buf, item, tok_len))
+                       return new_trailer_item(item,
+                                               strbuf_detach(&tok, NULL),
+                                               strbuf_detach(&val, NULL));
+       }
+
+       return new_trailer_item(NULL,
+                               strbuf_detach(&tok, NULL),
+                               strbuf_detach(&val, NULL));
+}
+
+static void add_trailer_item(struct trailer_item **first,
+                            struct trailer_item **last,
+                            struct trailer_item *new)
+{
+       if (!new)
+               return;
+       if (!*last) {
+               *first = new;
+               *last = new;
+       } else {
+               (*last)->next = new;
+               new->previous = *last;
+               *last = new;
+       }
+}
+
+static struct trailer_item *process_command_line_args(struct string_list *trailers)
+{
+       struct trailer_item *arg_tok_first = NULL;
+       struct trailer_item *arg_tok_last = NULL;
+       struct string_list_item *tr;
+       struct trailer_item *item;
+
+       /* Add a trailer item for each configured trailer with a command */
+       for (item = first_conf_item; item; item = item->next) {
+               if (item->conf.command) {
+                       struct trailer_item *new = new_trailer_item(item, NULL, NULL);
+                       add_trailer_item(&arg_tok_first, &arg_tok_last, new);
+               }
+       }
+
+       /* Add a trailer item for each trailer on the command line */
+       for_each_string_list_item(tr, trailers) {
+               struct trailer_item *new = create_trailer_item(tr->string);
+               add_trailer_item(&arg_tok_first, &arg_tok_last, new);
+       }
+
+       return arg_tok_first;
+}
+
+static struct strbuf **read_input_file(const char *file)
+{
+       struct strbuf **lines;
+       struct strbuf sb = STRBUF_INIT;
+
+       if (file) {
+               if (strbuf_read_file(&sb, file, 0) < 0)
+                       die_errno(_("could not read input file '%s'"), file);
+       } else {
+               if (strbuf_read(&sb, fileno(stdin), 0) < 0)
+                       die_errno(_("could not read from stdin"));
+       }
+
+       lines = strbuf_split(&sb, '\n');
+
+       strbuf_release(&sb);
+
+       return lines;
+}
+
+/*
+ * Return the (0 based) index of the start of the patch or the line
+ * count if there is no patch in the message.
+ */
+static int find_patch_start(struct strbuf **lines, int count)
+{
+       int i;
+
+       /* Get the start of the patch part if any */
+       for (i = 0; i < count; i++) {
+               if (starts_with(lines[i]->buf, "---"))
+                       return i;
+       }
+
+       return count;
+}
+
+/*
+ * Return the (0 based) index of the first trailer line or count if
+ * there are no trailers. Trailers are searched only in the lines from
+ * index (count - 1) down to index 0.
+ */
+static int find_trailer_start(struct strbuf **lines, int count)
+{
+       int start, only_spaces = 1;
+
+       /*
+        * Get the start of the trailers by looking starting from the end
+        * for a line with only spaces before lines with one separator.
+        */
+       for (start = count - 1; start >= 0; start--) {
+               if (lines[start]->buf[0] == comment_line_char)
+                       continue;
+               if (contains_only_spaces(lines[start]->buf)) {
+                       if (only_spaces)
+                               continue;
+                       return start + 1;
+               }
+               if (strcspn(lines[start]->buf, separators) < lines[start]->len) {
+                       if (only_spaces)
+                               only_spaces = 0;
+                       continue;
+               }
+               return count;
+       }
+
+       return only_spaces ? count : 0;
+}
+
+static int has_blank_line_before(struct strbuf **lines, int start)
+{
+       for (;start >= 0; start--) {
+               if (lines[start]->buf[0] == comment_line_char)
+                       continue;
+               return contains_only_spaces(lines[start]->buf);
+       }
+       return 0;
+}
+
+static void print_lines(struct strbuf **lines, int start, int end)
+{
+       int i;
+       for (i = start; lines[i] && i < end; i++)
+               printf("%s", lines[i]->buf);
+}
+
+static int process_input_file(struct strbuf **lines,
+                             struct trailer_item **in_tok_first,
+                             struct trailer_item **in_tok_last)
+{
+       int count = 0;
+       int patch_start, trailer_start, i;
+
+       /* Get the line count */
+       while (lines[count])
+               count++;
+
+       patch_start = find_patch_start(lines, count);
+       trailer_start = find_trailer_start(lines, patch_start);
+
+       /* Print lines before the trailers as is */
+       print_lines(lines, 0, trailer_start);
+
+       if (!has_blank_line_before(lines, trailer_start - 1))
+               printf("\n");
+
+       /* Parse trailer lines */
+       for (i = trailer_start; i < patch_start; i++) {
+               struct trailer_item *new = create_trailer_item(lines[i]->buf);
+               add_trailer_item(in_tok_first, in_tok_last, new);
+       }
+
+       return patch_start;
+}
+
+static void free_all(struct trailer_item **first)
+{
+       while (*first) {
+               struct trailer_item *item = remove_first(first);
+               free_trailer_item(item);
+       }
+}
+
+void process_trailers(const char *file, int trim_empty, struct string_list *trailers)
+{
+       struct trailer_item *in_tok_first = NULL;
+       struct trailer_item *in_tok_last = NULL;
+       struct trailer_item *arg_tok_first;
+       struct strbuf **lines;
+       int patch_start;
+
+       /* Default config must be setup first */
+       git_config(git_trailer_default_config, NULL);
+       git_config(git_trailer_config, NULL);
+
+       lines = read_input_file(file);
+
+       /* Print the lines before the trailers */
+       patch_start = process_input_file(lines, &in_tok_first, &in_tok_last);
+
+       arg_tok_first = process_command_line_args(trailers);
+
+       process_trailers_lists(&in_tok_first, &in_tok_last, &arg_tok_first);
+
+       print_all(in_tok_first, trim_empty);
+
+       free_all(&in_tok_first);
+
+       /* Print the lines after the trailers as is */
+       print_lines(lines, patch_start, INT_MAX);
+
+       strbuf_list_free(lines);
+}
diff --git a/trailer.h b/trailer.h
new file mode 100644 (file)
index 0000000..8eb25d5
--- /dev/null
+++ b/trailer.h
@@ -0,0 +1,6 @@
+#ifndef TRAILER_H
+#define TRAILER_H
+
+void process_trailers(const char *file, int trim_empty, struct string_list *trailers);
+
+#endif /* TRAILER_H */