Subject: [PATCH v7 0/2] am: support --empty=(die|drop|keep) option to handle empty patches

From: "Aleen via GitGitGadget" <[email-masked]>
Date: Fri, 19 Nov 2021 05:04:56 +0000

Since that git has supported the --always option for the git-format-patch
command to create a patch with an empty commit message, git-am should
support applying and committing with empty patches.

----------------------------------------------------------------------------

Changes since v1:

 1. add a case when not passing the --always option.
 2. rename the --always option to --allow-empty.

----------------------------------------------------------------------------

Changes since v2:

 1. rename the --allow-empty option to --empty-commit.
 2. introduce three different strategies (die|skip|asis) when trying to
    record empty patches as empty commits.

----------------------------------------------------------------------------

Changes since v3:

 1. generate the missed file for test cases.
 2. grep -f cannot be used under Mac OS.

----------------------------------------------------------------------------

Changes since v4:

 1. rename the --empty-commit option to --empty.
 2. rename three different strategies (die|skip|asis) to die, drop and keep
    correspondingly.

----------------------------------------------------------------------------

Changes since v5:

 1. throw an error when passing --empty option without value.

----------------------------------------------------------------------------

cc: René Scharfe [email-masked] cc: Phillip Wood [email-masked] cc:
Aleen 徐沛文 [email-masked]

Aleen (2):
  doc: git-format-patch: describe the option --always
  am: support --empty=<option> to handle empty patches

 Documentation/git-am.txt           |  8 +++++
 Documentation/git-format-patch.txt |  6 +++-
 builtin/am.c                       | 55 +++++++++++++++++++++++++++---
 po/bg.po                           |  2 +-
 po/ca.po                           |  2 +-
 po/de.po                           |  2 +-
 po/el.po                           |  2 +-
 po/es.po                           |  2 +-
 po/fr.po                           |  2 +-
 po/git.pot                         |  2 +-
 po/id.po                           |  2 +-
 po/it.po                           |  2 +-
 po/ko.po                           |  2 +-
 po/pl.po                           |  2 +-
 po/pt_PT.po                        |  8 ++---
 po/ru.po                           |  4 +--
 po/sv.po                           |  2 +-
 po/tr.po                           |  2 +-
 po/vi.po                           |  2 +-
 po/zh_CN.po                        |  9 +++--
 po/zh_TW.po                        |  7 +++-
 t/t4150-am.sh                      | 49 ++++++++++++++++++++++++++
 22 files changed, 145 insertions(+), 29 deletions(-)

base-commit: ca35af825273b98fc8dc11527488952f5db8eb80
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1076%2Faleen42%2Fnext-v7
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1076/aleen42/next-v7
Pull-Request: https://github.com/gitgitgadget/git/pull/1076

Range-diff vs v6:

 1:  9f1b3dd6d0b ! 1:  d612bc49d57 doc: git-format-patch: describe the option --always
     @@ Metadata
       ## Commit message ##
          doc: git-format-patch: describe the option --always
      
     +    This commit has described how to use '--always' option in the command
     +    'git-format-patch' to include patches for commits that emit no changes.
     +
          Signed-off-by: Aleen <[email-masked]>
      
       ## Documentation/git-format-patch.txt ##
 2:  96d8573dc80 ! 2:  9e60e77c041 am: support --empty option to handle empty patches
     @@ Metadata
      Author: Aleen <[email-masked]>
      
       ## Commit message ##
     -    am: support --empty option to handle empty patches
     +    am: support --empty=<option> to handle empty patches
     +
     +    Since that the command 'git-format-patch' can include patches of
     +    commits that emit no changes, the 'git-am' command should also
     +    support an option, named as '--empty', to specify how to handle
     +    those empty patches. In this commit, we have implemented three
     +    valid options ('die', 'drop' and 'keep').
      
          Signed-off-by: Aleen <[email-masked]>
      
     @@ Documentation/git-am.txt: OPTIONS
       	This flag will be passed down to 'git mailinfo' (see linkgit:git-mailinfo[1]).
       
      +--empty-commit=(die|drop|keep)::
     -+	The command usually errors out when seeing an input e-mail
     -+	message that lacks a patch. When this option is set to
     -+	'drop', skip such an e-mail message without outputting error.
     ++	By default, or when the option is set to 'die', the command
     ++	errors out on an input e-mail message that lacks a patch. 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.
     -+	'die' is specified by default.
      +
       -m::
       --message-id::
     @@ builtin/am.c: enum show_patch_type {
       
      +enum empty_action {
      +	DIE_EMPTY_COMMIT = 0,  /* output errors */
     -+	DROP_EMPTY_COMMIT,     /* skip without outputting errors */
     ++	DROP_EMPTY_COMMIT,     /* skip with a notice message, unless "--quiet" has been passed */
      +	KEEP_EMPTY_COMMIT      /* keep recording as empty commits */
      +};
      +
     @@ builtin/am.c: static int am_option_parse_quoted_cr(const struct option *opt,
       	return 0;
       }
       
     -+static int am_option_parse_empty_commit(const struct option *opt,
     ++static int am_option_parse_empty(const struct option *opt,
      +				     const char *arg, int unset)
      +{
      +	int *opt_value = opt->value;
      +
     -+	if (unset || !strcmp(arg, "die"))
     ++	BUG_ON_OPT_NEG(unset);
     ++
     ++	if (!strcmp(arg, "die"))
      +		*opt_value = DIE_EMPTY_COMMIT;
      +	else if (!strcmp(arg, "drop"))
      +		*opt_value = DROP_EMPTY_COMMIT;
     @@ builtin/am.c: static int parse_mail(struct am_state *state, const char *mail)
       	strbuf_addstr(&msg, "\n\n");
       	strbuf_addbuf(&msg, &mi.log_message);
       	strbuf_stripspace(&msg, 0);
     +@@ builtin/am.c: 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();
     + 
      @@ builtin/am.c: 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"))) {
     -+			if (state->empty_type == DROP_EMPTY_COMMIT)
     ++			switch (state->empty_type) {
     ++			case DROP_EMPTY_COMMIT:
     ++				say(state, stdout, _("Skipping: %.*s"), linelen(state->msg), state->msg);
      +				goto next;
     -+			else if (state->empty_type == KEEP_EMPTY_COMMIT) {
     -+				if (run_applypatch_msg_hook(state))
     -+					exit(1);
     -+				else
     -+					goto commit;
     -+			} else if (state->empty_type == DIE_EMPTY_COMMIT) {
     ++				break;
     ++			case KEEP_EMPTY_COMMIT:
     ++				to_keep = 1;
     ++				break;
     ++			case DIE_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);
       
      @@ builtin/am.c: static void am_run(struct am_state *state, int resume)
       			die_user_resolve(state);
     @@ builtin/am.c: 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) "" },
     -+		{ OPTION_CALLBACK, 0, "empty", &state.empty_type,
     -+		  "(die|drop|keep)",
     -+		  N_("specify how to handle empty patches"),
     -+		  PARSE_OPT_OPTARG, am_option_parse_empty_commit },
     ++		OPT_CALLBACK_F(0, "empty", &state.empty_type, "{drop,keep,die}",
     ++		  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()
      
     + ## po/bg.po ##
     +@@ po/bg.po: msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr ""
     + "Индексът не е чист: кръпките не може да бъдат приложени (замърсени са: %s)"
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "Прилагане: %.*s"
     +
     + ## po/ca.po ##
     +@@ po/ca.po: msgstr "no s'ha pogut escriure el fitxer d'índex"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "Índex brut: no es poden aplicar pedaços (bruts: %s)"
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "S'està aplicant: %.*s"
     +
     + ## po/de.po ##
     +@@ po/de.po: msgstr "Konnte Index-Datei nicht schreiben."
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "Geänderter Index: kann Patches nicht anwenden (geändert: %s)"
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "Wende an: %.*s"
     +
     + ## po/el.po ##
     +@@ po/el.po: msgstr "Να γίνει εφαρμογή; [y]es/[n]o/[e]dit/[v]iew patch/[a]ccep
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr ""
     + 
     +-#: builtin/am.c:1749 builtin/am.c:1817
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr ""
     +
     + ## po/es.po ##
     +@@ po/es.po: msgstr "no es posible escribir el archivo índice"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "Índice sucio: no se puede aplicar parches (sucio: %s)"
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "Aplicando: %.*s"
     +
     + ## po/fr.po ##
     +@@ po/fr.po: msgstr "impossible d'écrire le fichier d'index"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "Index sale : impossible d'appliquer des patchs (sales : %s)"
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "Application de  %.*s"
     +
     + ## po/git.pot ##
     +@@ po/git.pot: msgstr ""
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr ""
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr ""
     +
     + ## po/id.po ##
     +@@ po/id.po: msgstr "tidak dapat menulis berkas indeks"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "Indeks kotor: tidak dapat menerapkan tambalan (kotor: %s)"
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "Menerapkan: %.*s"
     +
     + ## po/it.po ##
     +@@ po/it.po: msgstr "impossibile scrivere il file indice"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "Indice sporco: impossibile applicare le patch (elemento sporco: %s)"
     + 
     +-#: builtin/am.c:1761 builtin/am.c:1829
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "Applicazione in corso: %.*s"
     +
     + ## po/ko.po ##
     +@@ po/ko.po: msgstr "적용? 예[y]/아니오[n]/편집[e]/패치 보기[v]/모두 적용[a]:
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "변경된 인덱스: 패치를 적용할 수 없습니다 (dirty: %s)"
     + 
     +-#: builtin/am.c:1808 builtin/am.c:1879
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "적용하는 중: %.*s"
     +
     + ## po/pl.po ##
     +@@ po/pl.po: msgstr "nie można zapisać pliku indeksu"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "Brudny indeks: nie można zastosować łatek (brudny: %s)"
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "Stosowanie: %.*s"
     +
     + ## po/pt_PT.po ##
     +@@
     + #   bisect                           |  bisetar
     + #   blame                            |  blame
     + #   blob object                      |  objeto-blob
     +-#   branch                           |  ramo 
     ++#   branch                           |  ramo
     + #   bug                              |  bug
     + #   bundle                           |  conjunto
     + #   bypass                           |  desviar
     +@@
     + #   loose refs                       |  refs soltas
     + #   mark                             |  marca
     + #   master                           |  master
     +-#   merge                            |  junção 
     ++#   merge                            |  junção
     + #   mergetag                         |  etiqueta-junção
     + #   object                           |  objeto
     + #   object database                  |  base dados de objeto
     +@@
     + #   token                            |  token
     + #   unset                            |  desdefinir
     + #   untrack                          |  desmonitorizar
     +-#   
     ++#
     + #
     + msgid ""
     + msgstr ""
     +@@ po/pt_PT.po: msgstr "incapaz escrever ficheiro de index"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "Index sujo: incapaz aplicar patches (sujo: %s)"
     + 
     +-#: builtin/am.c:1797 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "A aplicar: %.*s"
     +
     + ## po/ru.po ##
     +@@
     + # SOME DESCRIPTIVE TITLE.
     + # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
     + # This file is distributed under the same license as the PACKAGE package.
     +-# 
     ++#
     + # Translators:
     + # Alexander Golubev <[email-masked]>, 2020
     + # Dimitriy Ryazantcev <[email-masked]>, 2014-2021
     +@@ po/ru.po: msgstr "не удалось записать индекс"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "Индекс изменён: нельзя применять патчи (изменено: %s)"
     + 
     +-#: builtin/am.c:1748 builtin/am.c:1816
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "Применение: %.*s"
     +
     + ## po/sv.po ##
     +@@ po/sv.po: msgstr "kan inte skriva indexfil"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "Smutsigt index: kan inte tillämpa patchar (smutsiga: %s)"
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "Tillämpar: %.*s"
     +
     + ## po/tr.po ##
     +@@ po/tr.po: msgstr "indeks dosyası yazılamıyor"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "Kirli indeks: Yamalar uygulanamıyor (kirli: %s)"
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "Uygulanıyor: %.*s"
     +
     + ## po/vi.po ##
     +@@ po/vi.po: msgstr "không thể ghi tập tin lưu mục lục"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "Bảng mục lục bẩn: không thể áp dụng các miếng vá (bẩn: %s)"
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "Áp dụng: %.*s"
     +
     + ## po/zh_CN.po ##
     +@@ po/zh_CN.po: msgstr "坏的索引文件 sha1 签名"
     + msgid "index uses %.4s extension, which we do not understand"
     + msgstr "索引使用不被支持的 %.4s 扩展"
     + 
     +-# 	
     ++#
     + #: read-cache.c:1834
     + #, c-format
     + msgid "ignoring %.4s extension"
     +@@ po/zh_CN.po: msgstr "无法写入索引文件"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "脏索引：不能应用补丁（脏文件：%s）"
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "应用：%.*s"
     + 
     ++#: builtin/am.c:1818
     ++#, c-format
     ++msgid "Skipping: %.*s"
     ++msgstr "跳过：%.*s"
     ++
     + #: builtin/am.c:1815
     + msgid "No changes -- Patch already applied."
     + msgstr "没有变更 —— 补丁已经应用过。"
     +
     + ## po/zh_TW.po ##
     +@@ po/zh_TW.po: msgstr "無法寫入索引檔案"
     + msgid "Dirty index: cannot apply patches (dirty: %s)"
     + msgstr "髒索引：不能套用修補檔（髒檔案：%s）"
     + 
     +-#: builtin/am.c:1798 builtin/am.c:1865
     ++#: builtin/am.c:1834 builtin/am.c:1902
     + #, c-format
     + msgid "Applying: %.*s"
     + msgstr "套用：%.*s"
     + 
     ++#: builtin/am.c:1818
     ++#, c-format
     ++msgid "Skipping: %.*s"
     ++msgstr "忽略：%.*s"
     ++
     + #: builtin/am.c:1815
     + msgid "No changes -- Patch already applied."
     + msgstr "沒有變更——修補檔已經套用過。"
     +
       ## t/t4150-am.sh ##
      @@ t/t4150-am.sh: test_expect_success setup '
       
     @@ t/t4150-am.sh: test_expect_success setup '
      +	git checkout -b empty-commit &&
      +	git commit -m "empty commit" --allow-empty &&
      +
     -+	git format-patch --stdout empty-commit^ >empty.patch &&
     -+	git format-patch --stdout --cover-letter empty-commit^ >cover-letter.patch &&
     ++	: >empty.patch &&
      +	git format-patch --always --stdout empty-commit^ >empty-commit.patch &&
      +
       	# reset time
     @@ t/t4150-am.sh: test_expect_success 'apply binary blob in partial clone' '
       	git -C client am ../patch
       '
       
     -+test_expect_success 'still output error with --empty when meeting empty files' '
     ++test_expect_success 'An empty input file is error regardless of --empty option' '
      +	test_must_fail git am --empty=drop empty.patch 2>actual &&
      +	echo Patch format detection failed. >expected &&
      +	test_cmp expected actual
      +'
      +
     -+test_expect_success 'error when meeting e-mail message that lacks a patch by default' '
     ++test_expect_success 'invalid when passing the --empty option alone' '
      +	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 &&
     -+	test_path_is_dir .git/rebase-apply &&
     -+	test_i18ngrep "Patch is empty." err &&
     -+	rm -fr .git/rebase-apply &&
     ++	grep "Patch is empty" err &&
     ++	rm -fr .git/rebase-apply
     ++'
      +
     ++test_expect_success 'a message without a patch is an error where an explicit "--empty=die" is given' '
     ++	test_when_finished "git am --abort || :" &&
      +	test_must_fail git am --empty=die empty-commit.patch >err &&
     -+	test_path_is_dir .git/rebase-apply &&
     -+	test_i18ngrep "Patch is empty." err &&
     -+	rm -fr .git/rebase-apply &&
     -+
     -+	test_must_fail git am --empty=die cover-letter.patch >err &&
     -+	test_path_is_dir .git/rebase-apply &&
     -+	test_i18ngrep "Patch is empty." err &&
     ++	grep "Patch is empty." err &&
      +	rm -fr .git/rebase-apply
      +'
      +
     -+test_expect_success 'skip without error when meeting e-mail message that lacks a patch' '
     -+	git am --empty=drop empty-commit.patch >err &&
     -+	test_path_is_missing .git/rebase-apply &&
     ++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 &&
     -+
     -+	git am --empty=drop cover-letter.patch >err &&
     -+	test_path_is_missing .git/rebase-apply &&
     -+	test_cmp_rev empty-commit^ HEAD
     ++	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 &&
      +	test_path_is_missing .git/rebase-apply &&
     -+	{
     -+		git show empty-commit --format="%B" &&
     -+		echo "--" &&
     -+		git version | sed -e "s/^git version //" &&
     -+		echo
     -+	} >expected &&
     -+	git show HEAD --format="%B" >actual &&
     -+	test_cmp actual expected &&
     -+
     -+	git am --empty=keep cover-letter.patch &&
     -+	test_path_is_missing .git/rebase-apply &&
     -+	{
     -+		echo "*** SUBJECT HERE ***" &&
     -+		echo &&
     -+		echo "*** BLURB HERE ***" &&
     -+		echo &&
     -+		echo "A U Thor (1):" &&
     -+		printf "  " &&
     -+		git show empty-commit --format="%B" &&
     -+		echo "--" &&
     -+		git version | sed -e "s/^git version //" &&
     -+		echo
     -+	} >expected &&
     -+	git show HEAD --format="%B" >actual &&
     ++	git show empty-commit --format="%s" >expected &&
     ++	git show HEAD --format="%s" >actual &&
      +	test_cmp actual expected
      +'
      +
 3:  e907a2b2faa < -:  ----------- am: throw an error when passing --empty option without value

-- 
gitgitgadget