diff options
author | Junio C Hamano <gitster@pobox.com> | 2022-01-05 14:01:28 -0800 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2022-01-05 14:01:28 -0800 |
commit | ead6767ad7f835f4802248371709c4506ad6a4f1 (patch) | |
tree | 62ddf372299fa9c8945738e604d45a9cd062286e | |
parent | da81d473fcfa67dfbcf0504d2b5225885e51e532 (diff) | |
parent | 9e7e41bf19ed10469f9adb60669b1699c81b8ea6 (diff) | |
download | git-ead6767ad7f835f4802248371709c4506ad6a4f1.tar.gz |
Merge branch 'xw/am-empty'
"git am" learns "--empty=(stop|drop|keep)" option to tweak what is
done to a piece of e-mail without a patch in it.
* xw/am-empty:
am: support --allow-empty to record specific empty patches
am: support --empty=<option> to handle empty patches
doc: git-format-patch: describe the option --always
-rw-r--r-- | Documentation/git-am.txt | 16 | ||||
-rw-r--r-- | Documentation/git-format-patch.txt | 6 | ||||
-rw-r--r-- | builtin/am.c | 89 | ||||
-rwxr-xr-x | t/t4150-am.sh | 107 | ||||
-rwxr-xr-x | t/t7512-status-help.sh | 1 | ||||
-rw-r--r-- | wt-status.c | 8 |
6 files changed, 211 insertions, 16 deletions
diff --git a/Documentation/git-am.txt b/Documentation/git-am.txt index 0a4a984dfd..09107fb106 100644 --- a/Documentation/git-am.txt +++ b/Documentation/git-am.txt @@ -16,8 +16,9 @@ SYNOPSIS [--exclude=<path>] [--include=<path>] [--reject] [-q | --quiet] [--[no-]scissors] [-S[<keyid>]] [--patch-format=<format>] [--quoted-cr=<action>] + [--empty=(stop|drop|keep)] [(<mbox> | <Maildir>)...] -'git am' (--continue | --skip | --abort | --quit | --show-current-patch[=(diff|raw)]) +'git am' (--continue | --skip | --abort | --quit | --show-current-patch[=(diff|raw)] | --allow-empty) DESCRIPTION ----------- @@ -63,6 +64,14 @@ OPTIONS --quoted-cr=<action>:: This flag will be passed down to 'git mailinfo' (see linkgit:git-mailinfo[1]). +--empty=(stop|drop|keep):: + By default, or when the option is set to 'stop', the command + errors out on an input e-mail message lacking a patch + and stops into the middle of the current am session. When this + option is set to 'drop', skip such an e-mail message instead. + When this option is set to 'keep', create an empty commit, + recording the contents of the e-mail message as its log. + -m:: --message-id:: Pass the `-m` flag to 'git mailinfo' (see linkgit:git-mailinfo[1]), @@ -191,6 +200,11 @@ default. You can use `--no-utf8` to override this. the e-mail message; if `diff`, show the diff portion only. Defaults to `raw`. +--allow-empty:: + After a patch failure on an input e-mail message lacking a patch, + create an empty commit with the contents of the e-mail message + as its log message. + DISCUSSION ---------- diff --git a/Documentation/git-format-patch.txt b/Documentation/git-format-patch.txt index 113eabc107..be797d7a28 100644 --- a/Documentation/git-format-patch.txt +++ b/Documentation/git-format-patch.txt @@ -18,7 +18,7 @@ SYNOPSIS [-n | --numbered | -N | --no-numbered] [--start-number <n>] [--numbered-files] [--in-reply-to=<message id>] [--suffix=.<sfx>] - [--ignore-if-in-upstream] + [--ignore-if-in-upstream] [--always] [--cover-from-description=<mode>] [--rfc] [--subject-prefix=<subject prefix>] [(--reroll-count|-v) <n>] @@ -192,6 +192,10 @@ will want to ensure that threading is disabled for `git send-email`. patches being generated, and any patch that matches is ignored. +--always:: + Include patches for commits that do not introduce any change, + which are omitted by default. + --cover-from-description=<mode>:: Controls which parts of the cover letter will be automatically populated using the branch's description. diff --git a/builtin/am.c b/builtin/am.c index 8677ea2348..f2e7e53388 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -87,6 +87,12 @@ enum show_patch_type { SHOW_PATCH_DIFF = 1, }; +enum empty_action { + STOP_ON_EMPTY_COMMIT = 0, /* output errors and stop in the middle of an am session */ + DROP_EMPTY_COMMIT, /* skip with a notice message, unless "--quiet" has been passed */ + KEEP_EMPTY_COMMIT, /* keep recording as empty commits */ +}; + struct am_state { /* state directory path */ char *dir; @@ -118,6 +124,7 @@ struct am_state { int message_id; int scissors; /* enum scissors_type */ int quoted_cr; /* enum quoted_cr_action */ + int empty_type; /* enum empty_action */ struct strvec git_apply_opts; const char *resolvemsg; int committer_date_is_author_date; @@ -178,6 +185,25 @@ static int am_option_parse_quoted_cr(const struct option *opt, return 0; } +static int am_option_parse_empty(const struct option *opt, + const char *arg, int unset) +{ + int *opt_value = opt->value; + + BUG_ON_OPT_NEG(unset); + + if (!strcmp(arg, "stop")) + *opt_value = STOP_ON_EMPTY_COMMIT; + else if (!strcmp(arg, "drop")) + *opt_value = DROP_EMPTY_COMMIT; + else if (!strcmp(arg, "keep")) + *opt_value = KEEP_EMPTY_COMMIT; + else + return error(_("Invalid value for --empty: %s"), arg); + + return 0; +} + /** * Returns path relative to the am_state directory. */ @@ -1126,6 +1152,12 @@ static void NORETURN die_user_resolve(const struct am_state *state) printf_ln(_("When you have resolved this problem, run \"%s --continue\"."), cmdline); printf_ln(_("If you prefer to skip this patch, run \"%s --skip\" instead."), cmdline); + + if (advice_enabled(ADVICE_AM_WORK_DIR) && + is_empty_or_missing_file(am_path(state, "patch")) && + !repo_index_has_changes(the_repository, NULL, NULL)) + printf_ln(_("To record the empty patch as an empty commit, run \"%s --allow-empty\"."), cmdline); + printf_ln(_("To restore the original branch and stop patching, run \"%s --abort\"."), cmdline); } @@ -1248,11 +1280,6 @@ static int parse_mail(struct am_state *state, const char *mail) goto finish; } - if (is_empty_or_missing_file(am_path(state, "patch"))) { - printf_ln(_("Patch is empty.")); - die_user_resolve(state); - } - strbuf_addstr(&msg, "\n\n"); strbuf_addbuf(&msg, &mi.log_message); strbuf_stripspace(&msg, 0); @@ -1763,6 +1790,7 @@ static void am_run(struct am_state *state, int resume) while (state->cur <= state->last) { const char *mail = am_path(state, msgnum(state)); int apply_status; + int to_keep; reset_ident_date(); @@ -1792,8 +1820,29 @@ static void am_run(struct am_state *state, int resume) if (state->interactive && do_interactive(state)) goto next; + to_keep = 0; + if (is_empty_or_missing_file(am_path(state, "patch"))) { + switch (state->empty_type) { + case DROP_EMPTY_COMMIT: + say(state, stdout, _("Skipping: %.*s"), linelen(state->msg), state->msg); + goto next; + break; + case KEEP_EMPTY_COMMIT: + to_keep = 1; + say(state, stdout, _("Creating an empty commit: %.*s"), + linelen(state->msg), state->msg); + break; + case STOP_ON_EMPTY_COMMIT: + printf_ln(_("Patch is empty.")); + die_user_resolve(state); + break; + } + } + if (run_applypatch_msg_hook(state)) exit(1); + if (to_keep) + goto commit; say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg); @@ -1827,6 +1876,7 @@ static void am_run(struct am_state *state, int resume) die_user_resolve(state); } +commit: do_commit(state); next: @@ -1856,19 +1906,24 @@ next: /** * Resume the current am session after patch application failure. The user did * all the hard work, and we do not have to do any patch application. Just - * trust and commit what the user has in the index and working tree. + * trust and commit what the user has in the index and working tree. If `allow_empty` + * is true, commit as an empty commit when index has not changed and lacking a patch. */ -static void am_resolve(struct am_state *state) +static void am_resolve(struct am_state *state, int allow_empty) { validate_resume_state(state); say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg); if (!repo_index_has_changes(the_repository, NULL, NULL)) { - printf_ln(_("No changes - did you forget to use 'git add'?\n" - "If there is nothing left to stage, chances are that something else\n" - "already introduced the same changes; you might want to skip this patch.")); - die_user_resolve(state); + if (allow_empty && is_empty_or_missing_file(am_path(state, "patch"))) { + printf_ln(_("No changes - recorded it as an empty commit.")); + } else { + printf_ln(_("No changes - did you forget to use 'git add'?\n" + "If there is nothing left to stage, chances are that something else\n" + "already introduced the same changes; you might want to skip this patch.")); + die_user_resolve(state); + } } if (unmerged_cache()) { @@ -2195,7 +2250,8 @@ enum resume_type { RESUME_SKIP, RESUME_ABORT, RESUME_QUIT, - RESUME_SHOW_PATCH + RESUME_SHOW_PATCH, + RESUME_ALLOW_EMPTY, }; struct resume_mode { @@ -2348,6 +2404,9 @@ int cmd_am(int argc, const char **argv, const char *prefix) N_("show the patch being applied"), PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, parse_opt_show_current_patch, RESUME_SHOW_PATCH }, + OPT_CMDMODE(0, "allow-empty", &resume.mode, + N_("record the empty patch as an empty commit"), + RESUME_ALLOW_EMPTY), OPT_BOOL(0, "committer-date-is-author-date", &state.committer_date_is_author_date, N_("lie about committer date")), @@ -2357,6 +2416,9 @@ int cmd_am(int argc, const char **argv, const char *prefix) { OPTION_STRING, 'S', "gpg-sign", &state.sign_commit, N_("key-id"), N_("GPG-sign commits"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + OPT_CALLBACK_F(STOP_ON_EMPTY_COMMIT, "empty", &state.empty_type, "{stop,drop,keep}", + N_("how to handle empty patches"), + PARSE_OPT_NONEG, am_option_parse_empty), OPT_HIDDEN_BOOL(0, "rebasing", &state.rebasing, N_("(internal use for git-rebase)")), OPT_END() @@ -2453,7 +2515,8 @@ int cmd_am(int argc, const char **argv, const char *prefix) am_run(&state, 1); break; case RESUME_RESOLVED: - am_resolve(&state); + case RESUME_ALLOW_EMPTY: + am_resolve(&state, resume.mode == RESUME_ALLOW_EMPTY ? 1 : 0); break; case RESUME_SKIP: am_skip(&state); diff --git a/t/t4150-am.sh b/t/t4150-am.sh index 103cd39148..6caff0ca39 100755 --- a/t/t4150-am.sh +++ b/t/t4150-am.sh @@ -196,6 +196,12 @@ test_expect_success setup ' git format-patch -M --stdout lorem^ >rename-add.patch && + git checkout -b empty-commit && + git commit -m "empty commit" --allow-empty && + + : >empty.patch && + git format-patch --always --stdout empty-commit^ >empty-commit.patch && + # reset time sane_unset test_tick && test_tick @@ -1152,4 +1158,105 @@ test_expect_success 'apply binary blob in partial clone' ' git -C client am ../patch ' +test_expect_success 'an empty input file is error regardless of --empty option' ' + test_when_finished "git am --abort || :" && + test_must_fail git am --empty=drop empty.patch 2>actual && + echo "Patch format detection failed." >expected && + test_cmp expected actual +' + +test_expect_success 'invalid when passing the --empty option alone' ' + test_when_finished "git am --abort || :" && + git checkout empty-commit^ && + test_must_fail git am --empty empty-commit.patch 2>err && + echo "error: Invalid value for --empty: empty-commit.patch" >expected && + test_cmp expected err +' + +test_expect_success 'a message without a patch is an error (default)' ' + test_when_finished "git am --abort || :" && + test_must_fail git am empty-commit.patch >err && + grep "Patch is empty" err +' + +test_expect_success 'a message without a patch is an error where an explicit "--empty=stop" is given' ' + test_when_finished "git am --abort || :" && + test_must_fail git am --empty=stop empty-commit.patch >err && + grep "Patch is empty." err +' + +test_expect_success 'a message without a patch will be skipped when "--empty=drop" is given' ' + git am --empty=drop empty-commit.patch >output && + git rev-parse empty-commit^ >expected && + git rev-parse HEAD >actual && + test_cmp expected actual && + grep "Skipping: empty commit" output +' + +test_expect_success 'record as an empty commit when meeting e-mail message that lacks a patch' ' + git am --empty=keep empty-commit.patch >output && + test_path_is_missing .git/rebase-apply && + git show empty-commit --format="%B" >expected && + git show HEAD --format="%B" >actual && + grep -f actual expected && + grep "Creating an empty commit: empty commit" output +' + +test_expect_success 'skip an empty patch in the middle of an am session' ' + git checkout empty-commit^ && + test_must_fail git am empty-commit.patch >err && + grep "Patch is empty." err && + grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err && + git am --skip && + test_path_is_missing .git/rebase-apply && + git rev-parse empty-commit^ >expected && + git rev-parse HEAD >actual && + test_cmp expected actual +' + +test_expect_success 'record an empty patch as an empty commit in the middle of an am session' ' + git checkout empty-commit^ && + test_must_fail git am empty-commit.patch >err && + grep "Patch is empty." err && + grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err && + git am --allow-empty >output && + grep "No changes - recorded it as an empty commit." output && + test_path_is_missing .git/rebase-apply && + git show empty-commit --format="%B" >expected && + git show HEAD --format="%B" >actual && + grep -f actual expected +' + +test_expect_success 'create an non-empty commit when the index IS changed though "--allow-empty" is given' ' + git checkout empty-commit^ && + test_must_fail git am empty-commit.patch >err && + : >empty-file && + git add empty-file && + git am --allow-empty && + git show empty-commit --format="%B" >expected && + git show HEAD --format="%B" >actual && + grep -f actual expected && + git diff HEAD^..HEAD --name-only +' + +test_expect_success 'cannot create empty commits when there is a clean index due to merge conflicts' ' + test_when_finished "git am --abort || :" && + git rev-parse HEAD >expected && + test_must_fail git am seq.patch && + test_must_fail git am --allow-empty >err && + ! grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err && + git rev-parse HEAD >actual && + test_cmp actual expected +' + +test_expect_success 'cannot create empty commits when there is unmerged index due to merge conflicts' ' + test_when_finished "git am --abort || :" && + git rev-parse HEAD >expected && + test_must_fail git am -3 seq.patch && + test_must_fail git am --allow-empty >err && + ! grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err && + git rev-parse HEAD >actual && + test_cmp actual expected +' + test_done diff --git a/t/t7512-status-help.sh b/t/t7512-status-help.sh index 7f2956d77a..2f16d5787e 100755 --- a/t/t7512-status-help.sh +++ b/t/t7512-status-help.sh @@ -659,6 +659,7 @@ On branch am_empty You are in the middle of an am session. The current patch is empty. (use "git am --skip" to skip this patch) + (use "git am --allow-empty" to record this patch as an empty commit) (use "git am --abort" to restore the original branch) nothing to commit (use -u to show untracked files) diff --git a/wt-status.c b/wt-status.c index 5d215f4e4f..335e723a71 100644 --- a/wt-status.c +++ b/wt-status.c @@ -1218,17 +1218,23 @@ static void show_merge_in_progress(struct wt_status *s, static void show_am_in_progress(struct wt_status *s, const char *color) { + int am_empty_patch; + status_printf_ln(s, color, _("You are in the middle of an am session.")); if (s->state.am_empty_patch) status_printf_ln(s, color, _("The current patch is empty.")); if (s->hints) { - if (!s->state.am_empty_patch) + am_empty_patch = s->state.am_empty_patch; + if (!am_empty_patch) status_printf_ln(s, color, _(" (fix conflicts and then run \"git am --continue\")")); status_printf_ln(s, color, _(" (use \"git am --skip\" to skip this patch)")); + if (am_empty_patch) + status_printf_ln(s, color, + _(" (use \"git am --allow-empty\" to record this patch as an empty commit)")); status_printf_ln(s, color, _(" (use \"git am --abort\" to restore the original branch)")); } |