builtin/tag: add --trailer option

git-tag supports interpreting trailers from an annotated tag message, using --list --format="%(trailers)". However, the available methods to add a trailer to a tag message (namely -F or --editor) are not as ergonomic. In a previous patch, we moved git-commit's implementation of its --trailer option to the trailer.h API. Let's use that new function to teach git-tag the same --trailer option, emulating as much of git-commit's behavior as much as possible. Helped-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: John Passaro <john.a.passaro@gmail.com> Acked-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

John Passaro committed May 5, 2024 at 18:49 UTC 066cef770713026f33bb548885e3d6f66323c1f5
3 files changed +157 -11
Documentation/git-tag.txt
+14 -2
@@ -10,6 +10,7 @@ SYNOPSIS
10 --------
11 [verse]
12 'git tag' [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] [-e]
13 + [(--trailer <token>[(=|:)<value>])...]
14 <tagname> [<commit> | <object>]
15 'git tag' -d <tagname>...
16 'git tag' [-n[<num>]] -l [--contains <commit>] [--no-contains <commit>]
@@ -31,8 +32,8 @@ creates a 'tag' object, and requires a tag message. Unless
32 `-m <msg>` or `-F <file>` is given, an editor is started for the user to type
33 in the tag message.
34
34 -If `-m <msg>` or `-F <file>` is given and `-a`, `-s`, and `-u <key-id>`
35 -are absent, `-a` is implied.
35 +If `-m <msg>` or `-F <file>` or `--trailer <token>[=<value>]` is given
36 +and `-a`, `-s`, and `-u <key-id>` are absent, `-a` is implied.
37
38 Otherwise, a tag reference that points directly at the given object
39 (i.e., a lightweight tag) is created.
@@ -178,6 +179,17 @@ This option is only applicable when listing tags without annotation lines.
179 Implies `-a` if none of `-a`, `-s`, or `-u <key-id>`
180 is given.
181
182 +--trailer <token>[(=|:)<value>]::
183 + Specify a (<token>, <value>) pair that should be applied as a
184 + trailer. (e.g. `git tag --trailer "Custom-Key: value"`
185 + will add a "Custom-Key" trailer to the tag message.)
186 + The `trailer.*` configuration variables
187 + (linkgit:git-interpret-trailers[1]) can be used to define if
188 + a duplicated trailer is omitted, where in the run of trailers
189 + each trailer would appear, and other details.
190 + The trailers can be extracted in `git tag --list`, using
191 + `--format="%(trailers)"` placeholder.
192 +
193 -e::
194 --edit::
195 The message taken from file with `-F` and command line with
builtin/tag.c
+29 -9
@@ -28,9 +28,11 @@
28 #include "date.h"
29 #include "write-or-die.h"
30 #include "object-file-convert.h"
31 +#include "trailer.h"
32
33 static const char * const git_tag_usage[] = {
34 N_("git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] [-e]\n"
35 + " [(--trailer <token>[(=|:)<value>])...]\n"
36 " <tagname> [<commit> | <object>]"),
37 N_("git tag -d <tagname>..."),
38 N_("git tag [-n[<num>]] -l [--contains <commit>] [--no-contains <commit>]\n"
@@ -290,10 +292,12 @@ static const char message_advice_nested_tag[] =
292 static void create_tag(const struct object_id *object, const char *object_ref,
293 const char *tag,
294 struct strbuf *buf, struct create_tag_options *opt,
293 - struct object_id *prev, struct object_id *result, char *path)
295 + struct object_id *prev, struct object_id *result,
296 + struct strvec *trailer_args, char *path)
297 {
298 enum object_type type;
299 struct strbuf header = STRBUF_INIT;
300 + int should_edit;
301
302 type = oid_object_info(the_repository, object, NULL);
303 if (type <= OBJ_NONE)
@@ -313,13 +317,15 @@ static void create_tag(const struct object_id *object, const char *object_ref,
317 tag,
318 git_committer_info(IDENT_STRICT));
319
316 - if (!opt->message_given || opt->use_editor) {
320 + should_edit = opt->use_editor || !opt->message_given;
321 + if (should_edit || trailer_args->nr) {
322 int fd;
323
324 /* write the template message before editing: */
325 fd = xopen(path, O_CREAT | O_TRUNC | O_WRONLY, 0600);
326
322 - if (opt->message_given) {
327 + if (opt->message_given && buf->len) {
328 + strbuf_complete(buf, '\n');
329 write_or_die(fd, buf->buf, buf->len);
330 strbuf_reset(buf);
331 } else if (!is_null_oid(prev)) {
@@ -338,10 +344,19 @@ static void create_tag(const struct object_id *object, const char *object_ref,
344 }
345 close(fd);
346
341 - if (launch_editor(path, buf, NULL)) {
342 - fprintf(stderr,
343 - _("Please supply the message using either -m or -F option.\n"));
344 - exit(1);
347 + if (trailer_args->nr && amend_file_with_trailers(path, trailer_args))
348 + die(_("unable to pass trailers to --trailers"));
349 +
350 + if (should_edit) {
351 + if (launch_editor(path, buf, NULL)) {
352 + fprintf(stderr,
353 + _("Please supply the message using either -m or -F option.\n"));
354 + exit(1);
355 + }
356 + } else if (trailer_args->nr) {
357 + strbuf_reset(buf);
358 + if (strbuf_read_file(buf, path, 0) < 0)
359 + die_errno(_("failed to read '%s'"), path);
360 }
361 }
362
@@ -463,6 +478,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
478 struct ref_sorting *sorting;
479 struct string_list sorting_options = STRING_LIST_INIT_DUP;
480 struct ref_format format = REF_FORMAT_INIT;
481 + struct strvec trailer_args = STRVEC_INIT;
482 int icase = 0;
483 int edit_flag = 0;
484 struct option options[] = {
@@ -479,6 +495,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
495 OPT_CALLBACK_F('m', "message", &msg, N_("message"),
496 N_("tag message"), PARSE_OPT_NONEG, parse_msg_arg),
497 OPT_FILENAME('F', "file", &msgfile, N_("read message from file")),
498 + OPT_PASSTHRU_ARGV(0, "trailer", &trailer_args, N_("trailer"),
499 + N_("add custom trailer(s)"), PARSE_OPT_NONEG),
500 OPT_BOOL('e', "edit", &edit_flag, N_("force edit of tag message")),
501 OPT_BOOL('s', "sign", &opt.sign, N_("annotated and GPG-signed tag")),
502 OPT_CLEANUP(&cleanup_arg),
@@ -548,7 +566,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
566 opt.sign = 1;
567 set_signing_key(keyid);
568 }
551 - create_tag_object = (opt.sign || annotate || msg.given || msgfile);
569 + create_tag_object = (opt.sign || annotate || msg.given || msgfile ||
570 + edit_flag || trailer_args.nr);
571
572 if ((create_tag_object || force) && (cmdmode != 0))
573 usage_with_options(git_tag_usage, options);
@@ -654,7 +673,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
673 opt.sign = 1;
674 path = git_pathdup("TAG_EDITMSG");
675 create_tag(&object, object_ref, tag, &buf, &opt, &prev, &object,
657 - path);
676 + &trailer_args, path);
677 }
678
679 transaction = ref_transaction_begin(&err);
@@ -686,6 +705,7 @@ cleanup:
705 strbuf_release(&reflog_msg);
706 strbuf_release(&msg.buf);
707 strbuf_release(&err);
708 + strvec_clear(&trailer_args);
709 free(msgfile);
710 return ret;
711 }
t/t7004-tag.sh
+114
@@ -668,6 +668,115 @@ test_expect_success \
668 test_cmp expect actual
669 '
670
671 +# trailers
672 +
673 +test_expect_success 'create tag with -m and --trailer' '
674 + get_tag_header tag-with-inline-message-and-trailers $commit commit $time >expect &&
675 + cat >>expect <<-\EOF &&
676 + create tag with trailers
677 +
678 + my-trailer: here
679 + alt-trailer: there
680 + EOF
681 + git tag -m "create tag with trailers" \
682 + --trailer my-trailer=here \
683 + --trailer alt-trailer=there \
684 + tag-with-inline-message-and-trailers &&
685 + get_tag_msg tag-with-inline-message-and-trailers >actual &&
686 + test_cmp expect actual
687 +'
688 +
689 +test_expect_success 'list tag extracting trailers' '
690 + cat >expect <<-\EOF &&
691 + my-trailer: here
692 + alt-trailer: there
693 +
694 + EOF
695 + git tag --list --format="%(trailers)" tag-with-inline-message-and-trailers >actual &&
696 + test_cmp expect actual
697 +'
698 +
699 +test_expect_success 'create tag with -F and --trailer' '
700 + echo "create tag from message file using --trailer" >messagefilewithnotrailers &&
701 + get_tag_header tag-with-file-message-and-trailers $commit commit $time >expect &&
702 + cat >>expect <<-\EOF &&
703 + create tag from message file using --trailer
704 +
705 + my-trailer: here
706 + alt-trailer: there
707 + EOF
708 + git tag -F messagefilewithnotrailers \
709 + --trailer my-trailer=here \
710 + --trailer alt-trailer=there \
711 + tag-with-file-message-and-trailers &&
712 + get_tag_msg tag-with-file-message-and-trailers >actual &&
713 + test_cmp expect actual
714 +'
715 +
716 +test_expect_success 'create tag with -m and --trailer and --edit' '
717 + write_script fakeeditor <<-\EOF &&
718 + sed -e "1s/^/EDITED: /g" <"$1" >"$1-"
719 + mv "$1-" "$1"
720 + EOF
721 + get_tag_header tag-with-edited-inline-message-and-trailers $commit commit $time >expect &&
722 + cat >>expect <<-\EOF &&
723 + EDITED: create tag with trailers
724 +
725 + my-trailer: here
726 + alt-trailer: there
727 + EOF
728 + GIT_EDITOR=./fakeeditor git tag --edit \
729 + -m "create tag with trailers" \
730 + --trailer my-trailer=here \
731 + --trailer alt-trailer=there \
732 + tag-with-edited-inline-message-and-trailers &&
733 + get_tag_msg tag-with-edited-inline-message-and-trailers >actual &&
734 + test_cmp expect actual
735 +'
736 +
737 +test_expect_success 'create tag with -F and --trailer and --edit' '
738 + echo "create tag from message file using --trailer" >messagefilewithnotrailers &&
739 + get_tag_header tag-with-edited-file-message-and-trailers $commit commit $time >expect &&
740 + cat >>expect <<-\EOF &&
741 + EDITED: create tag from message file using --trailer
742 +
743 + my-trailer: here
744 + alt-trailer: there
745 + EOF
746 + GIT_EDITOR=./fakeeditor git tag --edit \
747 + -F messagefilewithnotrailers \
748 + --trailer my-trailer=here \
749 + --trailer alt-trailer=there \
750 + tag-with-edited-file-message-and-trailers &&
751 + get_tag_msg tag-with-edited-file-message-and-trailers >actual &&
752 + test_cmp expect actual
753 +'
754 +
755 +test_expect_success 'create annotated tag and force editor when only --trailer is given' '
756 + write_script fakeeditor <<-\EOF &&
757 + echo "add a line" >"$1-"
758 + cat <"$1" >>"$1-"
759 + mv "$1-" "$1"
760 + EOF
761 + get_tag_header tag-with-trailers-and-no-message $commit commit $time >expect &&
762 + cat >>expect <<-\EOF &&
763 + add a line
764 +
765 + my-trailer: here
766 + alt-trailer: there
767 + EOF
768 + GIT_EDITOR=./fakeeditor git tag \
769 + --trailer my-trailer=here \
770 + --trailer alt-trailer=there \
771 + tag-with-trailers-and-no-message &&
772 + get_tag_msg tag-with-trailers-and-no-message >actual &&
773 + test_cmp expect actual
774 +'
775 +
776 +test_expect_success 'bad editor causes panic when only --trailer is given' '
777 + test_must_fail env GIT_EDITOR=false git tag --trailer my-trailer=here tag-will-not-exist
778 +'
779 +
780 # listing messages for annotated non-signed tags:
781
782 test_expect_success \
@@ -810,6 +919,11 @@ test_expect_success 'git tag --format with ahead-behind' '
919 refs/tags/tag-lines 0 1 !
920 refs/tags/tag-one-line 0 1 !
921 refs/tags/tag-right 0 0 !
922 + refs/tags/tag-with-edited-file-message-and-trailers 0 1 !
923 + refs/tags/tag-with-edited-inline-message-and-trailers 0 1 !
924 + refs/tags/tag-with-file-message-and-trailers 0 1 !
925 + refs/tags/tag-with-inline-message-and-trailers 0 1 !
926 + refs/tags/tag-with-trailers-and-no-message 0 1 !
927 refs/tags/tag-zero-lines 0 1 !
928 EOF
929 git tag -l --format="%(refname) %(ahead-behind:HEAD) !" >actual 2>err &&