1
0
mirror of https://github.com/git/git.git synced 2024-11-18 23:23:51 +01:00

Merge branch 'cc/interpret-trailers'

A new filter to programatically edit the tail end of the commit log
messages.

* cc/interpret-trailers:
  Documentation: add documentation for 'git interpret-trailers'
  trailer: add tests for commands in config file
  trailer: execute command from 'trailer.<name>.command'
  trailer: add tests for "git interpret-trailers"
  trailer: add interpret-trailers command
  trailer: put all the processing together and print
  trailer: parse trailers from file or stdin
  trailer: process command line trailer arguments
  trailer: read and process config information
  trailer: process trailers from input message and arguments
  trailer: add data structures and basic functions
This commit is contained in:
Junio C Hamano 2014-10-20 12:25:30 -07:00
commit 9d04401ffe
10 changed files with 2084 additions and 0 deletions

1
.gitignore vendored

@ -74,6 +74,7 @@
/git-index-pack
/git-init
/git-init-db
/git-interpret-trailers
/git-instaweb
/git-log
/git-ls-files

@ -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

@ -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

@ -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);

@ -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;
}

@ -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

1
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 },

863
t/t7513-interpret-trailers.sh Executable file

@ -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

851
trailer.c Normal file

@ -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);
}

6
trailer.h Normal file

@ -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 */