trailer: add new .cmd config option

The `trailer.<token>.command` configuration variable
specifies a command (run via the shell, so it does not have
to be a single name or path to the command, but can be a
shell script), and the first occurrence of substring $ARG is
replaced with the value given to the `interpret-trailer`
command for the token in a '--trailer <token>=<value>' argument.

This has three downsides:

* The use of $ARG in the mechanism misleads the users that
the value is passed in the shell variable, and tempt them
to use $ARG more than once, but that would not work, as
the second and subsequent $ARG are not replaced.

* Because $ARG is textually replaced without regard to the
shell language syntax, even '$ARG' (inside a single-quote
pair), which a user would expect to stay intact, would be
replaced, and worse, if the value had an unmatched single
quote (imagine a name like "O'Connor", substituted into
NAME='$ARG' to make it NAME='O'Connor'), it would result in
a broken command that is not syntactically correct (or
worse).

* The first occurrence of substring `$ARG` will be replaced
with the empty string, in the command when the command is
first called to add a trailer with the specified <token>.
This is a bad design, the nature of automatic execution
causes it to add a trailer that we don't expect.

Introduce a new `trailer.<token>.cmd` configuration that
takes higher precedence to deprecate and eventually remove
`trailer.<token>.command`, which passes the value as an
argument to the command.  Instead of "$ARG", users can
refer to the value as positional argument, $1, in their
scripts. At the same time, in order to allow
`git interpret-trailers` to better simulate the behavior
of `git command -s`, 'trailer.<token>.cmd' will not
automatically execute.

Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Christian Couder <christian.couder@gmail.com>
Signed-off-by: ZheNing Hu <adlternative@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
ZheNing Hu 2021-05-03 15:41:05 +00:00 committed by Junio C Hamano
parent 57dcb6575b
commit c364b7ef51
3 changed files with 175 additions and 19 deletions

View File

@ -232,6 +232,20 @@ trailer.<token>.ifmissing::
that option for trailers with the specified <token>. that option for trailers with the specified <token>.
trailer.<token>.command:: trailer.<token>.command::
This option behaves in the same way as 'trailer.<token>.cmd', except
that it doesn't pass anything as argument to the specified command.
Instead the first occurrence of substring $ARG is replaced by the
value that would be passed as argument.
+
The 'trailer.<token>.command' option has been deprecated in favor of
'trailer.<token>.cmd' due to the fact that $ARG in the user's command is
only replaced once and that the original way of replacing $ARG is not safe.
+
When both 'trailer.<token>.cmd' and 'trailer.<token>.command' are given
for the same <token>, 'trailer.<token>.cmd' is used and
'trailer.<token>.command' is ignored.
trailer.<token>.cmd::
This option can be used to specify a shell command that will be called: This option can be used to specify a shell command that will be called:
once to automatically add a trailer with the specified <token>, and then once to automatically add a trailer with the specified <token>, and then
each time a '--trailer <token>=<value>' argument to modify the <value> of each time a '--trailer <token>=<value>' argument to modify the <value> of
@ -247,15 +261,9 @@ leading and trailing whitespace trimmed off.
If some '--trailer <token>=<value>' arguments are also passed If some '--trailer <token>=<value>' arguments are also passed
on the command line, the command is called again once for each on the command line, the command is called again once for each
of these arguments with the same <token>. And the <value> part of these arguments with the same <token>. And the <value> part
of these arguments, if any, will be used to replace the first of these arguments, if any, will be passed to the command as its
occurrence of substring `$ARG` in the command. This way the first argument. This way the command can produce a <value> computed
command can produce a <value> computed from the <value> passed from the <value> passed in the '--trailer <token>=<value>' argument.
in the '--trailer <token>=<value>' argument.
+
For consistency, the first occurrence of substring `$ARG` is
also replaced, this time with the empty string, in the command
when the command is first called to add a trailer with the
specified <token>.
EXAMPLES EXAMPLES
-------- --------
@ -338,6 +346,55 @@ subject
Fix #42 Fix #42
------------ ------------
* Configure a 'help' trailer with a cmd use a script `glog-find-author`
which search specified author identity from git log in git repository
and show how it works:
+
------------
$ cat ~/bin/glog-find-author
#!/bin/sh
test -n "$1" && git log --author="$1" --pretty="%an <%ae>" -1 || true
$ git config trailer.help.key "Helped-by: "
$ git config trailer.help.ifExists "addIfDifferentNeighbor"
$ git config trailer.help.cmd "~/bin/glog-find-author"
$ git interpret-trailers --trailer="help:Junio" --trailer="help:Couder" <<EOF
> subject
>
> message
>
> EOF
subject
message
Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Christian Couder <christian.couder@gmail.com>
------------
* Configure a 'ref' trailer with a cmd use a script `glog-grep`
to grep last relevant commit from git log in the git repository
and show how it works:
+
------------
$ cat ~/bin/glog-grep
#!/bin/sh
test -n "$1" && git log --grep "$1" --pretty=reference -1 || true
$ git config trailer.ref.key "Reference-to: "
$ git config trailer.ref.ifExists "replace"
$ git config trailer.ref.cmd "~/bin/glog-grep"
$ git interpret-trailers --trailer="ref:Add copyright notices." <<EOF
> subject
>
> message
>
> EOF
subject
message
Reference-to: 8bc9a0c769 (Add copyright notices., 2005-04-07)
------------
* Configure a 'see' trailer with a command to show the subject of a * Configure a 'see' trailer with a command to show the subject of a
commit that is related, and show how it works: commit that is related, and show how it works:
+ +

View File

@ -51,6 +51,69 @@ test_expect_success 'setup' '
EOF EOF
' '
test_expect_success 'with cmd' '
test_when_finished "git config --remove-section trailer.bug" &&
git config trailer.bug.key "Bug-maker: " &&
git config trailer.bug.ifExists "add" &&
git config trailer.bug.cmd "echo \"maybe is\"" &&
cat >expected2 <<-EOF &&
Bug-maker: maybe is him
Bug-maker: maybe is me
EOF
git interpret-trailers --trailer "bug: him" --trailer "bug:me" \
>actual2 &&
test_cmp expected2 actual2
'
test_expect_success 'with cmd and $1' '
test_when_finished "git config --remove-section trailer.bug" &&
git config trailer.bug.key "Bug-maker: " &&
git config trailer.bug.ifExists "add" &&
git config trailer.bug.cmd "echo \"\$1\" is" &&
cat >expected2 <<-EOF &&
Bug-maker: him is him
Bug-maker: me is me
EOF
git interpret-trailers --trailer "bug: him" --trailer "bug:me" \
>actual2 &&
test_cmp expected2 actual2
'
test_expect_success 'with cmd and $1 with sh -c' '
test_when_finished "git config --remove-section trailer.bug" &&
git config trailer.bug.key "Bug-maker: " &&
git config trailer.bug.ifExists "replace" &&
git config trailer.bug.cmd "sh -c \"echo who is \"\$1\"\"" &&
cat >expected2 <<-EOF &&
Bug-maker: who is me
EOF
git interpret-trailers --trailer "bug: him" --trailer "bug:me" \
>actual2 &&
test_cmp expected2 actual2
'
test_expect_success 'with cmd and $1 with shell script' '
test_when_finished "git config --remove-section trailer.bug" &&
git config trailer.bug.key "Bug-maker: " &&
git config trailer.bug.ifExists "replace" &&
git config trailer.bug.cmd "./echoscript" &&
cat >expected2 <<-EOF &&
Bug-maker: who is me
EOF
cat >echoscript <<-EOF &&
#!/bin/sh
echo who is "\$1"
EOF
chmod +x echoscript &&
git interpret-trailers --trailer "bug: him" --trailer "bug:me" \
>actual2 &&
test_cmp expected2 actual2
'
test_expect_success 'without config' ' test_expect_success 'without config' '
sed -e "s/ Z\$/ /" >expected <<-\EOF && sed -e "s/ Z\$/ /" >expected <<-\EOF &&
@ -1274,6 +1337,27 @@ test_expect_success 'setup a commit' '
git commit -m "Add file a.txt" git commit -m "Add file a.txt"
' '
test_expect_success 'cmd takes precedence over command' '
test_when_finished "git config --unset trailer.fix.cmd" &&
git config trailer.fix.ifExists "replace" &&
git config trailer.fix.cmd "test -n \"\$1\" && git log -1 --oneline --format=\"%h (%aN)\" \
--abbrev-commit --abbrev=14 \"\$1\" || true" &&
git config trailer.fix.command "git log -1 --oneline --format=\"%h (%s)\" \
--abbrev-commit --abbrev=14 \$ARG" &&
FIXED=$(git log -1 --oneline --format="%h (%aN)" --abbrev-commit --abbrev=14 HEAD) &&
cat complex_message_body >expected2 &&
sed -e "s/ Z\$/ /" >>expected2 <<-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 >actual2 &&
test_cmp expected2 actual2
'
test_expect_success 'with command using $ARG' ' test_expect_success 'with command using $ARG' '
git config trailer.fix.ifExists "replace" && git config trailer.fix.ifExists "replace" &&
git config trailer.fix.command "git log -1 --oneline --format=\"%h (%s)\" --abbrev-commit --abbrev=14 \$ARG" && git config trailer.fix.command "git log -1 --oneline --format=\"%h (%s)\" --abbrev-commit --abbrev=14 \$ARG" &&

View File

@ -14,6 +14,7 @@ struct conf_info {
char *name; char *name;
char *key; char *key;
char *command; char *command;
char *cmd;
enum trailer_where where; enum trailer_where where;
enum trailer_if_exists if_exists; enum trailer_if_exists if_exists;
enum trailer_if_missing if_missing; enum trailer_if_missing if_missing;
@ -127,6 +128,7 @@ static void free_arg_item(struct arg_item *item)
free(item->conf.name); free(item->conf.name);
free(item->conf.key); free(item->conf.key);
free(item->conf.command); free(item->conf.command);
free(item->conf.cmd);
free(item->token); free(item->token);
free(item->value); free(item->value);
free(item); free(item);
@ -216,18 +218,24 @@ static int check_if_different(struct trailer_item *in_tok,
return 1; return 1;
} }
static char *apply_command(const char *command, const char *arg) static char *apply_command(struct conf_info *conf, const char *arg)
{ {
struct strbuf cmd = STRBUF_INIT; struct strbuf cmd = STRBUF_INIT;
struct strbuf buf = STRBUF_INIT; struct strbuf buf = STRBUF_INIT;
struct child_process cp = CHILD_PROCESS_INIT; struct child_process cp = CHILD_PROCESS_INIT;
char *result; char *result;
strbuf_addstr(&cmd, command); if (conf->cmd) {
if (arg) strbuf_addstr(&cmd, conf->cmd);
strbuf_replace(&cmd, TRAILER_ARG_STRING, arg); strvec_push(&cp.args, cmd.buf);
if (arg)
strvec_push(&cp.args, cmd.buf); strvec_push(&cp.args, arg);
} else if (conf->command) {
strbuf_addstr(&cmd, conf->command);
if (arg)
strbuf_replace(&cmd, TRAILER_ARG_STRING, arg);
strvec_push(&cp.args, cmd.buf);
}
cp.env = local_repo_env; cp.env = local_repo_env;
cp.no_stdin = 1; cp.no_stdin = 1;
cp.use_shell = 1; cp.use_shell = 1;
@ -247,7 +255,7 @@ static char *apply_command(const char *command, const char *arg)
static void apply_item_command(struct trailer_item *in_tok, struct arg_item *arg_tok) static void apply_item_command(struct trailer_item *in_tok, struct arg_item *arg_tok)
{ {
if (arg_tok->conf.command) { if (arg_tok->conf.command || arg_tok->conf.cmd) {
const char *arg; const char *arg;
if (arg_tok->value && arg_tok->value[0]) { if (arg_tok->value && arg_tok->value[0]) {
arg = arg_tok->value; arg = arg_tok->value;
@ -257,7 +265,7 @@ static void apply_item_command(struct trailer_item *in_tok, struct arg_item *arg
else else
arg = xstrdup(""); arg = xstrdup("");
} }
arg_tok->value = apply_command(arg_tok->conf.command, arg); arg_tok->value = apply_command(&arg_tok->conf, arg);
free((char *)arg); free((char *)arg);
} }
} }
@ -430,6 +438,7 @@ static void duplicate_conf(struct conf_info *dst, const struct conf_info *src)
dst->name = xstrdup_or_null(src->name); dst->name = xstrdup_or_null(src->name);
dst->key = xstrdup_or_null(src->key); dst->key = xstrdup_or_null(src->key);
dst->command = xstrdup_or_null(src->command); dst->command = xstrdup_or_null(src->command);
dst->cmd = xstrdup_or_null(src->cmd);
} }
static struct arg_item *get_conf_item(const char *name) static struct arg_item *get_conf_item(const char *name)
@ -454,8 +463,8 @@ static struct arg_item *get_conf_item(const char *name)
return item; return item;
} }
enum trailer_info_type { TRAILER_KEY, TRAILER_COMMAND, TRAILER_WHERE, enum trailer_info_type { TRAILER_KEY, TRAILER_COMMAND, TRAILER_CMD,
TRAILER_IF_EXISTS, TRAILER_IF_MISSING }; TRAILER_WHERE, TRAILER_IF_EXISTS, TRAILER_IF_MISSING };
static struct { static struct {
const char *name; const char *name;
@ -463,6 +472,7 @@ static struct {
} trailer_config_items[] = { } trailer_config_items[] = {
{ "key", TRAILER_KEY }, { "key", TRAILER_KEY },
{ "command", TRAILER_COMMAND }, { "command", TRAILER_COMMAND },
{ "cmd", TRAILER_CMD },
{ "where", TRAILER_WHERE }, { "where", TRAILER_WHERE },
{ "ifexists", TRAILER_IF_EXISTS }, { "ifexists", TRAILER_IF_EXISTS },
{ "ifmissing", TRAILER_IF_MISSING } { "ifmissing", TRAILER_IF_MISSING }
@ -542,6 +552,11 @@ static int git_trailer_config(const char *conf_key, const char *value, void *cb)
warning(_("more than one %s"), conf_key); warning(_("more than one %s"), conf_key);
conf->command = xstrdup(value); conf->command = xstrdup(value);
break; break;
case TRAILER_CMD:
if (conf->cmd)
warning(_("more than one %s"), conf_key);
conf->cmd = xstrdup(value);
break;
case TRAILER_WHERE: case TRAILER_WHERE:
if (trailer_set_where(&conf->where, value)) if (trailer_set_where(&conf->where, value))
warning(_("unknown value '%s' for key '%s'"), value, conf_key); warning(_("unknown value '%s' for key '%s'"), value, conf_key);