commit: add amend suboption to --fixup to create amend! commit

`git commit --fixup=amend:<commit>` will create an "amend!" commit. The resulting commit message subject will be "amend! ..." where "..." is the subject line of <commit> and the initial message body will be <commit>'s message. The "amend!" commit when rebased with --autosquash will fixup the contents and replace the commit message of <commit> with the "amend!" commit's message body. In order to prevent rebase from creating commits with an empty message we refuse to create an "amend!" commit if commit message body is empty. Mentored-by: Christian Couder <chriscool@tuxfamily.org> Mentored-by: Phillip Wood <phillip.wood@dunelm.org.uk> Helped-by: Junio C Hamano <gitster@pobox.com> Helped-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Charvi Mendiratta <charvi077@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Charvi Mendiratta committed Mar 15, 2021 at 13:24 UTC 494d314a0526a8bee9f8af7a6e6b74b66043c9fa
1 file changed +93 -10
builtin/commit.c
+93 -10
@@ -105,7 +105,8 @@ static const char *template_file;
105 */
106 static const char *author_message, *author_message_buffer;
107 static char *edit_message, *use_message;
108 -static char *fixup_message, *squash_message;
108 +static char *fixup_message, *fixup_commit, *squash_message;
109 +static const char *fixup_prefix;
110 static int all, also, interactive, patch_interactive, only, amend, signoff;
111 static int edit_flag = -1; /* unspecified */
112 static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
@@ -357,7 +358,8 @@ static const char *prepare_index(const char **argv, const char *prefix,
358 die(_("--pathspec-file-nul requires --pathspec-from-file"));
359 }
360
360 - if (!pathspec.nr && (also || (only && !amend && !allow_empty)))
361 + if (!pathspec.nr && (also || (only && !allow_empty &&
362 + (!amend || (fixup_message && strcmp(fixup_prefix, "amend"))))))
363 die(_("No paths with --include/--only does not make sense."));
364
365 if (read_cache_preload(&pathspec) < 0)
@@ -681,6 +683,22 @@ static void adjust_comment_line_char(const struct strbuf *sb)
683 comment_line_char = *p;
684 }
685
686 +static void prepare_amend_commit(struct commit *commit, struct strbuf *sb,
687 + struct pretty_print_context *ctx)
688 +{
689 + const char *buffer, *subject, *fmt;
690 +
691 + buffer = get_commit_buffer(commit, NULL);
692 + find_commit_subject(buffer, &subject);
693 + /*
694 + * If we amend the 'amend!' commit then we don't want to
695 + * duplicate the subject line.
696 + */
697 + fmt = starts_with(subject, "amend!") ? "%b" : "%B";
698 + format_commit_message(commit, fmt, sb, ctx);
699 + unuse_commit_buffer(commit, buffer);
700 +}
701 +
702 static int prepare_to_commit(const char *index_file, const char *prefix,
703 struct commit *current_head,
704 struct wt_status *s,
@@ -745,15 +763,33 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
763 } else if (fixup_message) {
764 struct pretty_print_context ctx = {0};
765 struct commit *commit;
748 - commit = lookup_commit_reference_by_name(fixup_message);
766 + char *fmt;
767 + commit = lookup_commit_reference_by_name(fixup_commit);
768 if (!commit)
750 - die(_("could not lookup commit %s"), fixup_message);
769 + die(_("could not lookup commit %s"), fixup_commit);
770 ctx.output_encoding = get_commit_output_encoding();
752 - format_commit_message(commit, "fixup! %s\n\n",
753 - &sb, &ctx);
754 - if (have_option_m)
755 - strbuf_addbuf(&sb, &message);
771 + fmt = xstrfmt("%s! %%s\n\n", fixup_prefix);
772 + format_commit_message(commit, fmt, &sb, &ctx);
773 + free(fmt);
774 hook_arg1 = "message";
775 +
776 + /*
777 + * Only `-m` commit message option is checked here, as
778 + * it supports `--fixup` to append the commit message.
779 + *
780 + * The other commit message options `-c`/`-C`/`-F` are
781 + * incompatible with all the forms of `--fixup` and
782 + * have already errored out while parsing the `git commit`
783 + * options.
784 + */
785 + if (have_option_m && !strcmp(fixup_prefix, "fixup"))
786 + strbuf_addbuf(&sb, &message);
787 +
788 + if (!strcmp(fixup_prefix, "amend")) {
789 + if (have_option_m)
790 + die(_("cannot combine -m with --fixup:%s"), fixup_message);
791 + prepare_amend_commit(commit, &sb, &ctx);
792 + }
793 } else if (!stat(git_path_merge_msg(the_repository), &statbuf)) {
794 size_t merge_msg_start;
795
@@ -1170,7 +1206,7 @@ static int parse_and_validate_options(int argc, const char *argv[],
1206 if (force_author && renew_authorship)
1207 die(_("Using both --reset-author and --author does not make sense"));
1208
1173 - if (logfile || have_option_m || use_message || fixup_message)
1209 + if (logfile || have_option_m || use_message)
1210 use_editor = 0;
1211 if (0 <= edit_flag)
1212 use_editor = edit_flag;
@@ -1227,6 +1263,36 @@ static int parse_and_validate_options(int argc, const char *argv[],
1263
1264 if (also + only + all + interactive > 1)
1265 die(_("Only one of --include/--only/--all/--interactive/--patch can be used."));
1266 +
1267 + if (fixup_message) {
1268 + /*
1269 + * We limit --fixup's suboptions to only alpha characters.
1270 + * If the first character after a run of alpha is colon,
1271 + * then the part before the colon may be a known suboption
1272 + * name `amend` or a misspelt suboption name. In this case,
1273 + * we treat it as --fixup=<suboption>:<arg>.
1274 + *
1275 + * Otherwise, we are dealing with --fixup=<commit>.
1276 + */
1277 + char *p = fixup_message;
1278 + while (isalpha(*p))
1279 + p++;
1280 + if (p > fixup_message && *p == ':') {
1281 + *p = '\0';
1282 + fixup_commit = p + 1;
1283 + if (!strcmp("amend", fixup_message)) {
1284 + fixup_prefix = "amend";
1285 + allow_empty = 1;
1286 + } else {
1287 + die(_("unknown option: --fixup=%s:%s"), fixup_message, fixup_commit);
1288 + }
1289 + } else {
1290 + fixup_commit = fixup_message;
1291 + fixup_prefix = "fixup";
1292 + use_editor = 0;
1293 + }
1294 + }
1295 +
1296 cleanup_mode = get_cleanup_mode(cleanup_arg, use_editor);
1297
1298 handle_untracked_files_arg(s);
@@ -1504,7 +1570,11 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
1570 OPT_CALLBACK('m', "message", &message, N_("message"), N_("commit message"), opt_parse_m),
1571 OPT_STRING('c', "reedit-message", &edit_message, N_("commit"), N_("reuse and edit message from specified commit")),
1572 OPT_STRING('C', "reuse-message", &use_message, N_("commit"), N_("reuse message from specified commit")),
1507 - OPT_STRING(0, "fixup", &fixup_message, N_("commit"), N_("use autosquash formatted message to fixup specified commit")),
1573 + /*
1574 + * TRANSLATORS: Leave "[amend:]" as-is, and
1575 + * only translate <commit>.
1576 + */
1577 + OPT_STRING(0, "fixup", &fixup_message, N_("[amend:]commit"), N_("use autosquash formatted message to fixup or amend specified commit")),
1578 OPT_STRING(0, "squash", &squash_message, N_("commit"), N_("use autosquash formatted message to squash specified commit")),
1579 OPT_BOOL(0, "reset-author", &renew_authorship, N_("the commit is authored by me now (used with -C/-c/--amend)")),
1580 OPT_BOOL('s', "signoff", &signoff, N_("add a Signed-off-by trailer")),
@@ -1663,6 +1733,19 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
1733 exit(1);
1734 }
1735
1736 + if (fixup_message && starts_with(sb.buf, "amend! ") &&
1737 + !allow_empty_message) {
1738 + struct strbuf body = STRBUF_INIT;
1739 + size_t len = commit_subject_length(sb.buf);
1740 + strbuf_addstr(&body, sb.buf + len);
1741 + if (message_is_empty(&body, cleanup_mode)) {
1742 + rollback_index_files();
1743 + fprintf(stderr, _("Aborting commit due to empty commit message body.\n"));
1744 + exit(1);
1745 + }
1746 + strbuf_release(&body);
1747 + }
1748 +
1749 if (amend) {
1750 const char *exclude_gpgsig[3] = { "gpgsig", "gpgsig-sha256", NULL };
1751 extra = read_commit_extra_headers(current_head, exclude_gpgsig);