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
This commit is contained in:
Junio C Hamano 2022-01-05 14:01:28 -08:00
commit ead6767ad7
6 changed files with 211 additions and 16 deletions

View File

@ -16,8 +16,9 @@ SYNOPSIS
[--exclude=<path>] [--include=<path>] [--reject] [-q | --quiet] [--exclude=<path>] [--include=<path>] [--reject] [-q | --quiet]
[--[no-]scissors] [-S[<keyid>]] [--patch-format=<format>] [--[no-]scissors] [-S[<keyid>]] [--patch-format=<format>]
[--quoted-cr=<action>] [--quoted-cr=<action>]
[--empty=(stop|drop|keep)]
[(<mbox> | <Maildir>)...] [(<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 DESCRIPTION
----------- -----------
@ -63,6 +64,14 @@ OPTIONS
--quoted-cr=<action>:: --quoted-cr=<action>::
This flag will be passed down to 'git mailinfo' (see linkgit:git-mailinfo[1]). 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:: -m::
--message-id:: --message-id::
Pass the `-m` flag to 'git mailinfo' (see linkgit:git-mailinfo[1]), 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. the e-mail message; if `diff`, show the diff portion only.
Defaults to `raw`. 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 DISCUSSION
---------- ----------

View File

@ -18,7 +18,7 @@ SYNOPSIS
[-n | --numbered | -N | --no-numbered] [-n | --numbered | -N | --no-numbered]
[--start-number <n>] [--numbered-files] [--start-number <n>] [--numbered-files]
[--in-reply-to=<message id>] [--suffix=.<sfx>] [--in-reply-to=<message id>] [--suffix=.<sfx>]
[--ignore-if-in-upstream] [--ignore-if-in-upstream] [--always]
[--cover-from-description=<mode>] [--cover-from-description=<mode>]
[--rfc] [--subject-prefix=<subject prefix>] [--rfc] [--subject-prefix=<subject prefix>]
[(--reroll-count|-v) <n>] [(--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 patches being generated, and any patch that matches is
ignored. ignored.
--always::
Include patches for commits that do not introduce any change,
which are omitted by default.
--cover-from-description=<mode>:: --cover-from-description=<mode>::
Controls which parts of the cover letter will be automatically Controls which parts of the cover letter will be automatically
populated using the branch's description. populated using the branch's description.

View File

@ -87,6 +87,12 @@ enum show_patch_type {
SHOW_PATCH_DIFF = 1, 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 { struct am_state {
/* state directory path */ /* state directory path */
char *dir; char *dir;
@ -118,6 +124,7 @@ struct am_state {
int message_id; int message_id;
int scissors; /* enum scissors_type */ int scissors; /* enum scissors_type */
int quoted_cr; /* enum quoted_cr_action */ int quoted_cr; /* enum quoted_cr_action */
int empty_type; /* enum empty_action */
struct strvec git_apply_opts; struct strvec git_apply_opts;
const char *resolvemsg; const char *resolvemsg;
int committer_date_is_author_date; int committer_date_is_author_date;
@ -178,6 +185,25 @@ static int am_option_parse_quoted_cr(const struct option *opt,
return 0; 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. * 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(_("When you have resolved this problem, run \"%s --continue\"."), cmdline);
printf_ln(_("If you prefer to skip this patch, run \"%s --skip\" instead."), 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); 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; 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_addstr(&msg, "\n\n");
strbuf_addbuf(&msg, &mi.log_message); strbuf_addbuf(&msg, &mi.log_message);
strbuf_stripspace(&msg, 0); strbuf_stripspace(&msg, 0);
@ -1763,6 +1790,7 @@ static void am_run(struct am_state *state, int resume)
while (state->cur <= state->last) { while (state->cur <= state->last) {
const char *mail = am_path(state, msgnum(state)); const char *mail = am_path(state, msgnum(state));
int apply_status; int apply_status;
int to_keep;
reset_ident_date(); reset_ident_date();
@ -1792,8 +1820,29 @@ static void am_run(struct am_state *state, int resume)
if (state->interactive && do_interactive(state)) if (state->interactive && do_interactive(state))
goto next; 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)) if (run_applypatch_msg_hook(state))
exit(1); exit(1);
if (to_keep)
goto commit;
say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg); 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); die_user_resolve(state);
} }
commit:
do_commit(state); do_commit(state);
next: next:
@ -1856,19 +1906,24 @@ next:
/** /**
* Resume the current am session after patch application failure. The user did * 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 * 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); validate_resume_state(state);
say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg); say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg);
if (!repo_index_has_changes(the_repository, NULL, NULL)) { if (!repo_index_has_changes(the_repository, NULL, NULL)) {
printf_ln(_("No changes - did you forget to use 'git add'?\n" if (allow_empty && is_empty_or_missing_file(am_path(state, "patch"))) {
"If there is nothing left to stage, chances are that something else\n" printf_ln(_("No changes - recorded it as an empty commit."));
"already introduced the same changes; you might want to skip this patch.")); } else {
die_user_resolve(state); 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()) { if (unmerged_cache()) {
@ -2195,7 +2250,8 @@ enum resume_type {
RESUME_SKIP, RESUME_SKIP,
RESUME_ABORT, RESUME_ABORT,
RESUME_QUIT, RESUME_QUIT,
RESUME_SHOW_PATCH RESUME_SHOW_PATCH,
RESUME_ALLOW_EMPTY,
}; };
struct resume_mode { struct resume_mode {
@ -2348,6 +2404,9 @@ int cmd_am(int argc, const char **argv, const char *prefix)
N_("show the patch being applied"), N_("show the patch being applied"),
PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,
parse_opt_show_current_patch, RESUME_SHOW_PATCH }, 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", OPT_BOOL(0, "committer-date-is-author-date",
&state.committer_date_is_author_date, &state.committer_date_is_author_date,
N_("lie about committer 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"), { OPTION_STRING, 'S', "gpg-sign", &state.sign_commit, N_("key-id"),
N_("GPG-sign commits"), N_("GPG-sign commits"),
PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, 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, OPT_HIDDEN_BOOL(0, "rebasing", &state.rebasing,
N_("(internal use for git-rebase)")), N_("(internal use for git-rebase)")),
OPT_END() OPT_END()
@ -2453,7 +2515,8 @@ int cmd_am(int argc, const char **argv, const char *prefix)
am_run(&state, 1); am_run(&state, 1);
break; break;
case RESUME_RESOLVED: case RESUME_RESOLVED:
am_resolve(&state); case RESUME_ALLOW_EMPTY:
am_resolve(&state, resume.mode == RESUME_ALLOW_EMPTY ? 1 : 0);
break; break;
case RESUME_SKIP: case RESUME_SKIP:
am_skip(&state); am_skip(&state);

View File

@ -196,6 +196,12 @@ test_expect_success setup '
git format-patch -M --stdout lorem^ >rename-add.patch && 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 # reset time
sane_unset test_tick && sane_unset test_tick &&
test_tick test_tick
@ -1152,4 +1158,105 @@ test_expect_success 'apply binary blob in partial clone' '
git -C client am ../patch 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 test_done

View File

@ -659,6 +659,7 @@ On branch am_empty
You are in the middle of an am session. You are in the middle of an am session.
The current patch is empty. The current patch is empty.
(use "git am --skip" to skip this patch) (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) (use "git am --abort" to restore the original branch)
nothing to commit (use -u to show untracked files) nothing to commit (use -u to show untracked files)

View File

@ -1218,17 +1218,23 @@ static void show_merge_in_progress(struct wt_status *s,
static void show_am_in_progress(struct wt_status *s, static void show_am_in_progress(struct wt_status *s,
const char *color) const char *color)
{ {
int am_empty_patch;
status_printf_ln(s, color, status_printf_ln(s, color,
_("You are in the middle of an am session.")); _("You are in the middle of an am session."));
if (s->state.am_empty_patch) if (s->state.am_empty_patch)
status_printf_ln(s, color, status_printf_ln(s, color,
_("The current patch is empty.")); _("The current patch is empty."));
if (s->hints) { 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, status_printf_ln(s, color,
_(" (fix conflicts and then run \"git am --continue\")")); _(" (fix conflicts and then run \"git am --continue\")"));
status_printf_ln(s, color, status_printf_ln(s, color,
_(" (use \"git am --skip\" to skip this patch)")); _(" (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, status_printf_ln(s, color,
_(" (use \"git am --abort\" to restore the original branch)")); _(" (use \"git am --abort\" to restore the original branch)"));
} }