threads / patch / 64460

patchcommit: add --committer option

Subject: [PATCH] commit: add --committer option

## tl;dr

44 messages between Nov 9, 2025 and Nov 17, 2025. Diffs are folded; open one to read it.

replies: 43people: 7as markdown or json

ZheNing Hu via GitGitGadget· Nov 9, 2025, 10:22 UTC · lore
From: ZheNing Hu <adlternative@gmail.com>

Add --committer option to git-commit, allowing users to override the committer identity similar to how --author works. This provides a more convenient alternative to setting GIT_COMMITTER_* environment variables.

Signed-off-by: ZheNing Hu <adlternative@gmail.com>
Co-authored-by: Aone-Agent <aone-agent@alibaba-inc.com>
---
    commit: add --committer option
    
    Currently, when users need to override the committer identity in
    git-commit, they have to set GIT_COMMITTER_NAME and GIT_COMMITTER_EMAIL
    environment variables, which can be cumbersome in scripting scenarios or
    when frequently switching committer identities.
    
    While git-commit already provides the --author option to conveniently
    override the author identity, there's no equivalent --committer option
    for the committer identity. This asymmetry creates an inconsistent user
    experience.
    
    This patch introduces the --committer option to git-commit, providing:
    
     1. Consistency with the existing --author option
     2. A more convenient alternative to environment variables
     3. Better support for automated workflows and scripts
     4. Improved user experience when managing multiple identities
    
    The implementation follows the same pattern as the --author option,
    accepting the format "Name " and properly validating the input.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1997%2Fadlternative%2Fzh%2Fimplement-committer-option-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1997/adlternative/zh/implement-committer-option-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1997
 Documentation/git-commit.adoc |  9 +++-
 builtin/commit.c              | 58 ++++++++++++++++++++++++-
 t/t7509-commit-authorship.sh  | 80 +++++++++++++++++++++++++++++++++++
 3 files changed, 144 insertions(+), 3 deletions(-)
Show changes to 3 files +144 −3

Documentation/git-commit.adoc, builtin/commit.c, t/t7509-commit-authorship.sh

diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
index 54c207ad45..a015c8328e 100644
--- a/Documentation/git-commit.adoc
+++ b/Documentation/git-commit.adoc
@@ -12,7 +12,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
 	   [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]
 	   [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
 	   [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
-	   [--date=<date>] [--cleanup=<mode>] [--[no-]status]
+	   [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]
 	   [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
 	   [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
 	   [--] [<pathspec>...]
@@ -181,6 +181,13 @@ See linkgit:git-rebase[1] for details.
 `--date=<date>`::
 	Override the author date used in the commit.
 
+`--committer=<committer>`::
+	Override the committer for the commit. Specify an explicit committer using the
+	standard `A U Thor <committer@example.com>` format. Otherwise _<committer>_
+	is assumed to be a pattern and is used to search for an existing
+	commit by that author (i.e. `git rev-list --all -i --author=<committer>`);
+	the commit author is then copied from the first such commit found.
+
 `-m <msg>`::
 `--message=<msg>`::
 	Use _<msg>_ as the commit message.
diff --git a/builtin/commit.c b/builtin/commit.c
index 0243f17d53..88e77cbaab 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -49,7 +49,7 @@ static const char * const builtin_commit_usage[] = {
 	   "           [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]\n"
 	   "           [-F <file> | -m <msg>] [--reset-author] [--allow-empty]\n"
 	   "           [--allow-empty-message] [--no-verify] [-e] [--author=<author>]\n"
-	   "           [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
+	   "           [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]\n"
 	   "           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
 	   "           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n"
 	   "           [--] [<pathspec>...]"),
@@ -112,6 +112,7 @@ static enum {
 } commit_style;
 
 static const char *force_author;
+static const char *force_committer;
 static char *logfile;
 static char *template_file;
 /*
@@ -690,6 +691,48 @@ static void determine_author_info(struct strbuf *author_ident)
 	free(date);
 }
 
+static void determine_committer_info(struct strbuf *committer_ident)
+{
+	char *name, *email, *date;
+	struct ident_split committer;
+
+	name = xstrdup_or_null(getenv("GIT_COMMITTER_NAME"));
+	email = xstrdup_or_null(getenv("GIT_COMMITTER_EMAIL"));
+	date = xstrdup_or_null(getenv("GIT_COMMITTER_DATE"));
+
+	if (force_committer) {
+		struct ident_split ident;
+
+		if (split_ident_line(&ident, force_committer, strlen(force_committer)) < 0)
+			die(_("malformed --committer parameter"));
+		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
+		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
+
+		if (ident.date_begin) {
+			struct strbuf date_buf = STRBUF_INIT;
+			strbuf_addch(&date_buf, '@');
+			strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
+			strbuf_addch(&date_buf, ' ');
+			strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
+			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
+		}
+	}
+
+	if (force_date) {
+		struct strbuf date_buf = STRBUF_INIT;
+		if (parse_force_date(force_date, &date_buf))
+			die(_("invalid date format: %s"), force_date);
+		set_ident_var(&date, strbuf_detach(&date_buf, NULL));
+	}
+
+	strbuf_addstr(committer_ident, fmt_ident(name, email, WANT_COMMITTER_IDENT, date,
+				IDENT_STRICT));
+	assert_split_ident(&committer, committer_ident);
+	free(name);
+	free(email);
+	free(date);
+}
+
 static int author_date_is_interesting(void)
 {
 	return author_message || force_date;
@@ -1321,6 +1364,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
 	if (force_author && renew_authorship)
 		die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author");
 
+	if (force_committer && !strchr(force_committer, '>'))
+		force_committer = find_author_by_nickname(force_committer);
+
 	if (logfile || have_option_m || use_message)
 		use_editor = 0;
 
@@ -1709,6 +1755,7 @@ int cmd_commit(int argc,
 		OPT_FILENAME('F', "file", &logfile, N_("read message from file")),
 		OPT_STRING(0, "author", &force_author, N_("author"), N_("override author for commit")),
 		OPT_STRING(0, "date", &force_date, N_("date"), N_("override date for commit")),
+		OPT_STRING(0, "committer", &force_committer, N_("committer"), N_("override committer for commit")),
 		OPT_CALLBACK('m', "message", &message, N_("message"), N_("commit message"), opt_parse_m),
 		OPT_STRING('c', "reedit-message", &edit_message, N_("commit"), N_("reuse and edit message from specified commit")),
 		OPT_STRING('C', "reuse-message", &use_message, N_("commit"), N_("reuse message from specified commit")),
@@ -1785,6 +1832,7 @@ int cmd_commit(int argc,
 
 	struct strbuf sb = STRBUF_INIT;
 	struct strbuf author_ident = STRBUF_INIT;
+	struct strbuf committer_ident = STRBUF_INIT;
 	const char *index_file, *reflog_msg;
 	struct object_id oid;
 	struct commit_list *parents = NULL;
@@ -1930,8 +1978,13 @@ int cmd_commit(int argc,
 		append_merge_tag_headers(parents, &tail);
 	}
 
+	if (force_committer) {
+		determine_committer_info(&committer_ident);
+	}
+
 	if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,
-				 parents, &oid, author_ident.buf, NULL,
+				 parents, &oid, author_ident.buf,
+				 force_committer ? committer_ident.buf : NULL,
 				 sign_commit, extra)) {
 		rollback_index_files();
 		die(_("failed to write commit object"));
@@ -1980,6 +2033,7 @@ cleanup:
 	free_commit_extra_headers(extra);
 	free_commit_list(parents);
 	strbuf_release(&author_ident);
+	strbuf_release(&committer_ident);
 	strbuf_release(&err);
 	strbuf_release(&sb);
 	free(logfile);
diff --git a/t/t7509-commit-authorship.sh b/t/t7509-commit-authorship.sh
index 8e373b566b..45527f6a70 100755
--- a/t/t7509-commit-authorship.sh
+++ b/t/t7509-commit-authorship.sh
@@ -12,6 +12,11 @@ author_header () {
 	sed -n -e '/^$/q' -e '/^author /p'
 }
 
+committer_header () {
+	git cat-file commit "$1" |
+	sed -n -e '/^$/q' -e '/^committer /p'
+}
+
 message_body () {
 	git cat-file commit "$1" |
 	sed -e '1,/^$/d'
@@ -171,4 +176,79 @@ test_expect_success '--reset-author with CHERRY_PICK_HEAD' '
 	test_cmp expect actual
 '
 
+test_expect_success '--committer option overrides committer' '
+	git checkout Initial &&
+	echo "Test --committer" >>foo &&
+	test_tick &&
+	git commit -a -m "test committer" --committer="Custom Committer <custom@committer.example>" &&
+	committer_header HEAD >actual &&
+	grep "Custom Committer <custom@committer.example>" actual
+'
+
+test_expect_success '--committer with pattern search' '
+	echo "Test committer pattern" >>foo &&
+	test_tick &&
+	git commit -a -m "test committer pattern" --committer="Frigate" &&
+	committer_header HEAD >actual &&
+	grep "Frigate <flying@over.world>" actual
+'
+
+test_expect_success '--committer malformed parameter' '
+	echo "Test malformed" >>foo &&
+	test_tick &&
+	test_must_fail git commit -a -m "test malformed" --committer="malformed committer"
+'
+
+test_expect_success '--committer with --amend option' '
+	git checkout -f Initial &&
+	echo "Test committer with amend" >>foo &&
+	test_tick &&
+	git commit -a -m "initial commit for amend test" &&
+	echo "Modified for amend" >>foo &&
+	test_tick &&
+	git commit -a --amend --no-edit \
+		--author="Test Author <test@author.example>" \
+		--committer="Test Committer <test@committer.example>" &&
+	author_header HEAD >actual_author &&
+	grep "Test Author <test@author.example>" actual_author &&
+	committer_header HEAD >actual_committer &&
+	grep "Test Committer <test@committer.example>" actual_committer
+'
+
+test_expect_success 'GIT_COMMITTER_* environment variables' '
+	git checkout -f Initial &&
+	echo "Test env vars" >>foo &&
+	test_tick &&
+	GIT_COMMITTER_NAME="Env Committer" \
+	GIT_COMMITTER_EMAIL="env@test.example" \
+	git commit -a -m "test committer env vars" &&
+	committer_header HEAD >actual &&
+	grep "Env Committer <env@test.example>" actual
+'
+
+test_expect_success '--committer overrides GIT_COMMITTER_* environment variables' '
+	echo "Test override" >>foo &&
+	test_tick &&
+	GIT_COMMITTER_NAME="Env Committer" \
+	GIT_COMMITTER_EMAIL="env@test.example" \
+	git commit -a -m "test override" \
+		--committer="Override Committer <override@test.example>" &&
+	committer_header HEAD >actual &&
+	grep "Override Committer <override@test.example>" actual
+'
+
+test_expect_success '--date with --committer changes both author and committer dates' '
+	git checkout -f Initial &&
+	echo "Test date override" >>foo &&
+	test_tick &&
+	git commit -a -m "test date" \
+		--author="Date Author <date@author.example>" \
+		--committer="Date Committer <date@committer.example>" \
+		--date="2024-06-15 10:30:00 +0800" &&
+	git log -1 --format="%ai" >author_date &&
+	git log -1 --format="%ci" >committer_date &&
+	grep "2024-06-15 10:30:00 +0800" author_date &&
+	grep "2024-06-15 10:30:00 +0800" committer_date
+'
+
 test_done

base-commit: 4badef0c3503dc29059d678abba7fac0f042bc84
-- 
gitgitgadget
Patrick Steinhardt· Nov 10, 2025, 09:24 UTC · re: ZheNing Hu via GitGitGadget · lore

Re: [PATCH] commit: add --committer option

On Sun, Nov 09, 2025 at 10:22:54AM +0000, ZheNing Hu via GitGitGadget wrote:
Show 5 quoted lines
> From: ZheNing Hu <adlternative@gmail.com>
> 
> Add --committer option to git-commit, allowing users to override the
> committer identity similar to how --author works. This provides a more
> convenient alternative to setting GIT_COMMITTER_* environment variables.
Yeah, I can see how that's useful.
Show 13 quoted lines
> diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
> index 54c207ad45..a015c8328e 100644
> --- a/Documentation/git-commit.adoc
> +++ b/Documentation/git-commit.adoc
> @@ -12,7 +12,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
>  	   [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]
>  	   [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
>  	   [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
> -	   [--date=<date>] [--cleanup=<mode>] [--[no-]status]
> +	   [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]
>  	   [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
>  	   [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
>  	   [--] [<pathspec>...]
Nit: I'd move `--committer` before `--date` so that it comes directly
after `--author`.
Show 10 quoted lines
> @@ -181,6 +181,13 @@ See linkgit:git-rebase[1] for details.
>  `--date=<date>`::
>  	Override the author date used in the commit.
>  
> +`--committer=<committer>`::
> +	Override the committer for the commit. Specify an explicit committer using the
> +	standard `A U Thor <committer@example.com>` format. Otherwise _<committer>_
> +	is assumed to be a pattern and is used to search for an existing
> +	commit by that author (i.e. `git rev-list --all -i --author=<committer>`);
> +	the commit author is then copied from the first such commit found.
This matches the description of `--author`.
Show 53 quoted lines
> diff --git a/builtin/commit.c b/builtin/commit.c
> index 0243f17d53..88e77cbaab 100644
> --- a/builtin/commit.c
> +++ b/builtin/commit.c
> @@ -690,6 +691,48 @@ static void determine_author_info(struct strbuf *author_ident)
>  	free(date);
>  }
>  
> +static void determine_committer_info(struct strbuf *committer_ident)
> +{
> +	char *name, *email, *date;
> +	struct ident_split committer;
> +
> +	name = xstrdup_or_null(getenv("GIT_COMMITTER_NAME"));
> +	email = xstrdup_or_null(getenv("GIT_COMMITTER_EMAIL"));
> +	date = xstrdup_or_null(getenv("GIT_COMMITTER_DATE"));
> +
> +	if (force_committer) {
> +		struct ident_split ident;
> +
> +		if (split_ident_line(&ident, force_committer, strlen(force_committer)) < 0)
> +			die(_("malformed --committer parameter"));
> +		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
> +		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
> +
> +		if (ident.date_begin) {
> +			struct strbuf date_buf = STRBUF_INIT;
> +			strbuf_addch(&date_buf, '@');
> +			strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
> +			strbuf_addch(&date_buf, ' ');
> +			strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
> +			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
> +		}
> +	}
> +
> +	if (force_date) {
> +		struct strbuf date_buf = STRBUF_INIT;
> +		if (parse_force_date(force_date, &date_buf))
> +			die(_("invalid date format: %s"), force_date);
> +		set_ident_var(&date, strbuf_detach(&date_buf, NULL));
> +	}
> +
> +	strbuf_addstr(committer_ident, fmt_ident(name, email, WANT_COMMITTER_IDENT, date,
> +				IDENT_STRICT));
> +	assert_split_ident(&committer, committer_ident);
> +	free(name);
> +	free(email);
> +	free(date);
> +}
> +
>  static int author_date_is_interesting(void)
>  {
>  	return author_message || force_date;

A lot of the infra in this new function is shared with `determine_author_info()`. It would be great if we could refactor it so that the common parts are shared given that this all is quite non-trivial.

Maybe we could have something like `determine_identity()` that contains the common bits between both functions? It might ultimately not really be worth it, but at least the functionality in the `force_committer` condition feels like it should be pulled out.

Show 10 quoted lines
> @@ -1321,6 +1364,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
>  	if (force_author && renew_authorship)
>  		die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author");
>  
> +	if (force_committer && !strchr(force_committer, '>'))
> +		force_committer = find_author_by_nickname(force_committer);
> +
>  	if (logfile || have_option_m || use_message)
>  		use_editor = 0;
>  

Is it the right thing to search by author here? Shouldn't we rather be searching by committer?

Show 8 quoted lines
> @@ -1930,8 +1978,13 @@ int cmd_commit(int argc,
>  		append_merge_tag_headers(parents, &tail);
>  	}
>  
> +	if (force_committer) {
> +		determine_committer_info(&committer_ident);
> +	}
> +
Nit: we tend to not use braces around single-line bodies.
Thanks!
Patrick
ZheNing Hu· Nov 10, 2025, 14:17 UTC · re: Patrick Steinhardt · lore

Re: [PATCH] commit: add --committer option

Patrick Steinhardt <ps@pks.im> 于2025年11月10日周一 17:24写道:
Show 10 quoted lines
>
> On Sun, Nov 09, 2025 at 10:22:54AM +0000, ZheNing Hu via GitGitGadget wrote:
> > From: ZheNing Hu <adlternative@gmail.com>
> >
> > Add --committer option to git-commit, allowing users to override the
> > committer identity similar to how --author works. This provides a more
> > convenient alternative to setting GIT_COMMITTER_* environment variables.
>
> Yeah, I can see how that's useful.
>
I'm glad we're aligned on this.
Show 17 quoted lines
> > diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
> > index 54c207ad45..a015c8328e 100644
> > --- a/Documentation/git-commit.adoc
> > +++ b/Documentation/git-commit.adoc
> > @@ -12,7 +12,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
> >          [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]
> >          [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
> >          [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
> > -        [--date=<date>] [--cleanup=<mode>] [--[no-]status]
> > +        [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]
> >          [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
> >          [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
> >          [--] [<pathspec>...]
>
> Nit: I'd move `--committer` before `--date` so that it comes directly
> after `--author`.
>
Agreed, will fix.
Show 13 quoted lines
> > @@ -181,6 +181,13 @@ See linkgit:git-rebase[1] for details.
> >  `--date=<date>`::
> >       Override the author date used in the commit.
> >
> > +`--committer=<committer>`::
> > +     Override the committer for the commit. Specify an explicit committer using the
> > +     standard `A U Thor <committer@example.com>` format. Otherwise _<committer>_
> > +     is assumed to be a pattern and is used to search for an existing
> > +     commit by that author (i.e. `git rev-list --all -i --author=<committer>`);
> > +     the commit author is then copied from the first such commit found.
>
> This matches the description of `--author`.
>
Agreed, I will fix it to use committer description.
Show 64 quoted lines
> > diff --git a/builtin/commit.c b/builtin/commit.c
> > index 0243f17d53..88e77cbaab 100644
> > --- a/builtin/commit.c
> > +++ b/builtin/commit.c
> > @@ -690,6 +691,48 @@ static void determine_author_info(struct strbuf *author_ident)
> >       free(date);
> >  }
> >
> > +static void determine_committer_info(struct strbuf *committer_ident)
> > +{
> > +     char *name, *email, *date;
> > +     struct ident_split committer;
> > +
> > +     name = xstrdup_or_null(getenv("GIT_COMMITTER_NAME"));
> > +     email = xstrdup_or_null(getenv("GIT_COMMITTER_EMAIL"));
> > +     date = xstrdup_or_null(getenv("GIT_COMMITTER_DATE"));
> > +
> > +     if (force_committer) {
> > +             struct ident_split ident;
> > +
> > +             if (split_ident_line(&ident, force_committer, strlen(force_committer)) < 0)
> > +                     die(_("malformed --committer parameter"));
> > +             set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
> > +             set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
> > +
> > +             if (ident.date_begin) {
> > +                     struct strbuf date_buf = STRBUF_INIT;
> > +                     strbuf_addch(&date_buf, '@');
> > +                     strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
> > +                     strbuf_addch(&date_buf, ' ');
> > +                     strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
> > +                     set_ident_var(&date, strbuf_detach(&date_buf, NULL));
> > +             }
> > +     }
> > +
> > +     if (force_date) {
> > +             struct strbuf date_buf = STRBUF_INIT;
> > +             if (parse_force_date(force_date, &date_buf))
> > +                     die(_("invalid date format: %s"), force_date);
> > +             set_ident_var(&date, strbuf_detach(&date_buf, NULL));
> > +     }
> > +
> > +     strbuf_addstr(committer_ident, fmt_ident(name, email, WANT_COMMITTER_IDENT, date,
> > +                             IDENT_STRICT));
> > +     assert_split_ident(&committer, committer_ident);
> > +     free(name);
> > +     free(email);
> > +     free(date);
> > +}
> > +
> >  static int author_date_is_interesting(void)
> >  {
> >       return author_message || force_date;
>
> A lot of the infra in this new function is shared with
> `determine_author_info()`. It would be great if we could refactor it so
> that the common parts are shared given that this all is quite
> non-trivial.
>
> Maybe we could have something like `determine_identity()` that contains
> the common bits between both functions? It might ultimately not really
> be worth it, but at least the functionality in the `force_committer`
> condition feels like it should be pulled out.
>

Good suggestion, I will refactor them to use `determine_identity()`, which should be more generic, and it even caught that I missed updating `GIT_COMMITTER_*`.

Show 14 quoted lines
> > @@ -1321,6 +1364,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
> >       if (force_author && renew_authorship)
> >               die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author");
> >
> > +     if (force_committer && !strchr(force_committer, '>'))
> > +             force_committer = find_author_by_nickname(force_committer);
> > +
> >       if (logfile || have_option_m || use_message)
> >               use_editor = 0;
> >
>
> Is it the right thing to search by author here? Shouldn't we rather be
> searching by committer?
>

Yes, there should also be a `find_identity_by_nickname()` or `find_committer_by_nickname()` here.

Show 11 quoted lines
> > @@ -1930,8 +1978,13 @@ int cmd_commit(int argc,
> >               append_merge_tag_headers(parents, &tail);
> >       }
> >
> > +     if (force_committer) {
> > +             determine_committer_info(&committer_ident);
> > +     }
> > +
>
> Nit: we tend to not use braces around single-line bodies.
>
Agree.
> Thanks!
>
> Patrick
Junio C Hamano· Nov 10, 2025, 17:38 UTC · re: Patrick Steinhardt · lore

Re: [PATCH] commit: add --committer option

Patrick Steinhardt <ps@pks.im> writes:
Show 8 quoted lines
> On Sun, Nov 09, 2025 at 10:22:54AM +0000, ZheNing Hu via GitGitGadget wrote:
>> From: ZheNing Hu <adlternative@gmail.com>
>> 
>> Add --committer option to git-commit, allowing users to override the
>> committer identity similar to how --author works. This provides a more
>> convenient alternative to setting GIT_COMMITTER_* environment variables.
>
> Yeah, I can see how that's useful.

Well, I don't. Naming somebody other than yourself as the author may be something that is needed from time to time by human users, but lying about the committer who made commits? Our tradition is to give long rope to let users hang themselves, but we already have the environment variable override specifically designed for scripted uses, where there may be very legit uses of recording arbitrary committer identity that has nothing to do with the identity the current user who is running the Git processes usually uses. I do not think it is a "useful" change to make it more it ergonomic to perform certain operations that we may not want to encourage.

So I dunno.
ZheNing Hu· Nov 11, 2025, 13:19 UTC · re: Junio C Hamano · lore

Re: [PATCH] commit: add --committer option

Junio C Hamano <gitster@pobox.com> 于2025年11月11日周二 01:38写道:
Show 25 quoted lines
>
> Patrick Steinhardt <ps@pks.im> writes:
>
> > On Sun, Nov 09, 2025 at 10:22:54AM +0000, ZheNing Hu via GitGitGadget wrote:
> >> From: ZheNing Hu <adlternative@gmail.com>
> >>
> >> Add --committer option to git-commit, allowing users to override the
> >> committer identity similar to how --author works. This provides a more
> >> convenient alternative to setting GIT_COMMITTER_* environment variables.
> >
> > Yeah, I can see how that's useful.
>
> Well, I don't.  Naming somebody other than yourself as the author
> may be something that is needed from time to time by human users,
> but lying about the committer who made commits?  Our tradition is to
> give long rope to let users hang themselves, but we already have the
> environment variable override specifically designed for scripted uses,
> where there may be very legit uses of recording arbitrary committer
> identity that has nothing to do with the identity the current user
> who is running the Git processes usually uses.  I do not think it is
> a "useful" change to make it more it ergonomic to perform certain
> operations that we may not want to encourage.
>
> So I dunno.
>

I understand your concern about not wanting to encourage users to misrepresent their committer identity. However, I'd like to point out that if someone truly wants to "lie" about the committer identity, there's nothing stopping them today. They can already do this through multiple methods.

My primary motivation for this patch is not to make it easier to falsify identity, but rather to help legitimate users who work across different repositories with different identities. These users occasionally need to fix accidentally misconfigured committer information, and the current methods (setting environment variables or editing config files) can be cumbersome for this use case.

You can see from [1] how many people have struggled with modifying commits committer.

>
[1]: https://stackoverflow.com/questions/750172/how-do-i-change-the-author-and-committer-name-email-for-multiple-commits?page=1&tab=scoredesc#tab-top
Phillip Wood· Nov 10, 2025, 16:50 UTC · re: ZheNing Hu via GitGitGadget · lore

Re: [PATCH] commit: add --committer option

Hi ZheNing
On 09/11/2025 10:22, ZheNing Hu via GitGitGadget wrote:
Show 9 quoted lines
> From: ZheNing Hu <adlternative@gmail.com>
> 
>      
>      This patch introduces the --committer option to git-commit, providing:
>      
>       1. Consistency with the existing --author option
>       2. A more convenient alternative to environment variables
>       3. Better support for automated workflows and scripts
>       4. Improved user experience when managing multiple identities

What's the use case for the same person committing under different identities? We already have a config mechanism to set different identities for different repositories but I'm struggling to see why someone would want to create commits under multiple identities in a single repository. For scripts it easy enough to set the relevant environment variables if a tool wants to create commits under its own identity.

Thanks
Phillip
Show 256 quoted lines
>      The implementation follows the same pattern as the --author option,
>      accepting the format "Name " and properly validating the input.
> 
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1997%2Fadlternative%2Fzh%2Fimplement-committer-option-v1
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1997/adlternative/zh/implement-committer-option-v1
> Pull-Request: https://github.com/gitgitgadget/git/pull/1997
> 
>   Documentation/git-commit.adoc |  9 +++-
>   builtin/commit.c              | 58 ++++++++++++++++++++++++-
>   t/t7509-commit-authorship.sh  | 80 +++++++++++++++++++++++++++++++++++
>   3 files changed, 144 insertions(+), 3 deletions(-)
> 
> diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
> index 54c207ad45..a015c8328e 100644
> --- a/Documentation/git-commit.adoc
> +++ b/Documentation/git-commit.adoc
> @@ -12,7 +12,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
>   	   [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]
>   	   [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
>   	   [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
> -	   [--date=<date>] [--cleanup=<mode>] [--[no-]status]
> +	   [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]
>   	   [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
>   	   [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
>   	   [--] [<pathspec>...]
> @@ -181,6 +181,13 @@ See linkgit:git-rebase[1] for details.
>   `--date=<date>`::
>   	Override the author date used in the commit.
>   
> +`--committer=<committer>`::
> +	Override the committer for the commit. Specify an explicit committer using the
> +	standard `A U Thor <committer@example.com>` format. Otherwise _<committer>_
> +	is assumed to be a pattern and is used to search for an existing
> +	commit by that author (i.e. `git rev-list --all -i --author=<committer>`);
> +	the commit author is then copied from the first such commit found.
> +
>   `-m <msg>`::
>   `--message=<msg>`::
>   	Use _<msg>_ as the commit message.
> diff --git a/builtin/commit.c b/builtin/commit.c
> index 0243f17d53..88e77cbaab 100644
> --- a/builtin/commit.c
> +++ b/builtin/commit.c
> @@ -49,7 +49,7 @@ static const char * const builtin_commit_usage[] = {
>   	   "           [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]\n"
>   	   "           [-F <file> | -m <msg>] [--reset-author] [--allow-empty]\n"
>   	   "           [--allow-empty-message] [--no-verify] [-e] [--author=<author>]\n"
> -	   "           [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
> +	   "           [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]\n"
>   	   "           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
>   	   "           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n"
>   	   "           [--] [<pathspec>...]"),
> @@ -112,6 +112,7 @@ static enum {
>   } commit_style;
>   
>   static const char *force_author;
> +static const char *force_committer;
>   static char *logfile;
>   static char *template_file;
>   /*
> @@ -690,6 +691,48 @@ static void determine_author_info(struct strbuf *author_ident)
>   	free(date);
>   }
>   
> +static void determine_committer_info(struct strbuf *committer_ident)
> +{
> +	char *name, *email, *date;
> +	struct ident_split committer;
> +
> +	name = xstrdup_or_null(getenv("GIT_COMMITTER_NAME"));
> +	email = xstrdup_or_null(getenv("GIT_COMMITTER_EMAIL"));
> +	date = xstrdup_or_null(getenv("GIT_COMMITTER_DATE"));
> +
> +	if (force_committer) {
> +		struct ident_split ident;
> +
> +		if (split_ident_line(&ident, force_committer, strlen(force_committer)) < 0)
> +			die(_("malformed --committer parameter"));
> +		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
> +		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
> +
> +		if (ident.date_begin) {
> +			struct strbuf date_buf = STRBUF_INIT;
> +			strbuf_addch(&date_buf, '@');
> +			strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
> +			strbuf_addch(&date_buf, ' ');
> +			strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
> +			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
> +		}
> +	}
> +
> +	if (force_date) {
> +		struct strbuf date_buf = STRBUF_INIT;
> +		if (parse_force_date(force_date, &date_buf))
> +			die(_("invalid date format: %s"), force_date);
> +		set_ident_var(&date, strbuf_detach(&date_buf, NULL));
> +	}
> +
> +	strbuf_addstr(committer_ident, fmt_ident(name, email, WANT_COMMITTER_IDENT, date,
> +				IDENT_STRICT));
> +	assert_split_ident(&committer, committer_ident);
> +	free(name);
> +	free(email);
> +	free(date);
> +}
> +
>   static int author_date_is_interesting(void)
>   {
>   	return author_message || force_date;
> @@ -1321,6 +1364,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
>   	if (force_author && renew_authorship)
>   		die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author");
>   
> +	if (force_committer && !strchr(force_committer, '>'))
> +		force_committer = find_author_by_nickname(force_committer);
> +
>   	if (logfile || have_option_m || use_message)
>   		use_editor = 0;
>   
> @@ -1709,6 +1755,7 @@ int cmd_commit(int argc,
>   		OPT_FILENAME('F', "file", &logfile, N_("read message from file")),
>   		OPT_STRING(0, "author", &force_author, N_("author"), N_("override author for commit")),
>   		OPT_STRING(0, "date", &force_date, N_("date"), N_("override date for commit")),
> +		OPT_STRING(0, "committer", &force_committer, N_("committer"), N_("override committer for commit")),
>   		OPT_CALLBACK('m', "message", &message, N_("message"), N_("commit message"), opt_parse_m),
>   		OPT_STRING('c', "reedit-message", &edit_message, N_("commit"), N_("reuse and edit message from specified commit")),
>   		OPT_STRING('C', "reuse-message", &use_message, N_("commit"), N_("reuse message from specified commit")),
> @@ -1785,6 +1832,7 @@ int cmd_commit(int argc,
>   
>   	struct strbuf sb = STRBUF_INIT;
>   	struct strbuf author_ident = STRBUF_INIT;
> +	struct strbuf committer_ident = STRBUF_INIT;
>   	const char *index_file, *reflog_msg;
>   	struct object_id oid;
>   	struct commit_list *parents = NULL;
> @@ -1930,8 +1978,13 @@ int cmd_commit(int argc,
>   		append_merge_tag_headers(parents, &tail);
>   	}
>   
> +	if (force_committer) {
> +		determine_committer_info(&committer_ident);
> +	}
> +
>   	if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,
> -				 parents, &oid, author_ident.buf, NULL,
> +				 parents, &oid, author_ident.buf,
> +				 force_committer ? committer_ident.buf : NULL,
>   				 sign_commit, extra)) {
>   		rollback_index_files();
>   		die(_("failed to write commit object"));
> @@ -1980,6 +2033,7 @@ cleanup:
>   	free_commit_extra_headers(extra);
>   	free_commit_list(parents);
>   	strbuf_release(&author_ident);
> +	strbuf_release(&committer_ident);
>   	strbuf_release(&err);
>   	strbuf_release(&sb);
>   	free(logfile);
> diff --git a/t/t7509-commit-authorship.sh b/t/t7509-commit-authorship.sh
> index 8e373b566b..45527f6a70 100755
> --- a/t/t7509-commit-authorship.sh
> +++ b/t/t7509-commit-authorship.sh
> @@ -12,6 +12,11 @@ author_header () {
>   	sed -n -e '/^$/q' -e '/^author /p'
>   }
>   
> +committer_header () {
> +	git cat-file commit "$1" |
> +	sed -n -e '/^$/q' -e '/^committer /p'
> +}
> +
>   message_body () {
>   	git cat-file commit "$1" |
>   	sed -e '1,/^$/d'
> @@ -171,4 +176,79 @@ test_expect_success '--reset-author with CHERRY_PICK_HEAD' '
>   	test_cmp expect actual
>   '
>   
> +test_expect_success '--committer option overrides committer' '
> +	git checkout Initial &&
> +	echo "Test --committer" >>foo &&
> +	test_tick &&
> +	git commit -a -m "test committer" --committer="Custom Committer <custom@committer.example>" &&
> +	committer_header HEAD >actual &&
> +	grep "Custom Committer <custom@committer.example>" actual
> +'
> +
> +test_expect_success '--committer with pattern search' '
> +	echo "Test committer pattern" >>foo &&
> +	test_tick &&
> +	git commit -a -m "test committer pattern" --committer="Frigate" &&
> +	committer_header HEAD >actual &&
> +	grep "Frigate <flying@over.world>" actual
> +'
> +
> +test_expect_success '--committer malformed parameter' '
> +	echo "Test malformed" >>foo &&
> +	test_tick &&
> +	test_must_fail git commit -a -m "test malformed" --committer="malformed committer"
> +'
> +
> +test_expect_success '--committer with --amend option' '
> +	git checkout -f Initial &&
> +	echo "Test committer with amend" >>foo &&
> +	test_tick &&
> +	git commit -a -m "initial commit for amend test" &&
> +	echo "Modified for amend" >>foo &&
> +	test_tick &&
> +	git commit -a --amend --no-edit \
> +		--author="Test Author <test@author.example>" \
> +		--committer="Test Committer <test@committer.example>" &&
> +	author_header HEAD >actual_author &&
> +	grep "Test Author <test@author.example>" actual_author &&
> +	committer_header HEAD >actual_committer &&
> +	grep "Test Committer <test@committer.example>" actual_committer
> +'
> +
> +test_expect_success 'GIT_COMMITTER_* environment variables' '
> +	git checkout -f Initial &&
> +	echo "Test env vars" >>foo &&
> +	test_tick &&
> +	GIT_COMMITTER_NAME="Env Committer" \
> +	GIT_COMMITTER_EMAIL="env@test.example" \
> +	git commit -a -m "test committer env vars" &&
> +	committer_header HEAD >actual &&
> +	grep "Env Committer <env@test.example>" actual
> +'
> +
> +test_expect_success '--committer overrides GIT_COMMITTER_* environment variables' '
> +	echo "Test override" >>foo &&
> +	test_tick &&
> +	GIT_COMMITTER_NAME="Env Committer" \
> +	GIT_COMMITTER_EMAIL="env@test.example" \
> +	git commit -a -m "test override" \
> +		--committer="Override Committer <override@test.example>" &&
> +	committer_header HEAD >actual &&
> +	grep "Override Committer <override@test.example>" actual
> +'
> +
> +test_expect_success '--date with --committer changes both author and committer dates' '
> +	git checkout -f Initial &&
> +	echo "Test date override" >>foo &&
> +	test_tick &&
> +	git commit -a -m "test date" \
> +		--author="Date Author <date@author.example>" \
> +		--committer="Date Committer <date@committer.example>" \
> +		--date="2024-06-15 10:30:00 +0800" &&
> +	git log -1 --format="%ai" >author_date &&
> +	git log -1 --format="%ci" >committer_date &&
> +	grep "2024-06-15 10:30:00 +0800" author_date &&
> +	grep "2024-06-15 10:30:00 +0800" committer_date
> +'
> +
>   test_done
> 
> base-commit: 4badef0c3503dc29059d678abba7fac0f042bc84
brian m. carlson· Nov 10, 2025, 18:01 UTC · re: Phillip Wood · lore

Re: [PATCH] commit: add --committer option

On 2025-11-10 at 16:50:04, Phillip Wood wrote:
Show 15 quoted lines
> On 09/11/2025 10:22, ZheNing Hu via GitGitGadget wrote:
> > From: ZheNing Hu <adlternative@gmail.com>
> > 
> >      This patch introduces the --committer option to git-commit, providing:
> >       1. Consistency with the existing --author option
> >       2. A more convenient alternative to environment variables
> >       3. Better support for automated workflows and scripts
> >       4. Improved user experience when managing multiple identities
> 
> What's the use case for the same person committing under different
> identities? We already have a config mechanism to set different identities
> for different repositories but I'm struggling to see why someone would want
> to create commits under multiple identities in a single repository. For
> scripts it easy enough to set the relevant environment variables if a tool
> wants to create commits under its own identity.

Someone who works on the same project under both their personal and corporate identities. For instance, me working on the Git project.

Some open source projects also require a CLA and you have to use a particular address to match the one that's listed on the CLA. For example, Google requires an address with a Google account, so in the hypothetical state where I was going to contribute to one of their projects, I'd need to use a different committer identity with my Gmail address.

I've also kept business logs in Git when I had a small business and I might well need to log approving a profit distribution (with my corporate address) and log accepting a profit distribution (with my personal address). Those would need separate digital signatures from my two different email addresses.

-- 
brian m. carlson (they/them)
Toronto, Ontario, CA
Jeff King· Nov 10, 2025, 20:11 UTC · re: brian m. carlson · lore

Re: [PATCH] commit: add --committer option

On Mon, Nov 10, 2025 at 06:01:57PM +0000, brian m. carlson wrote:
Show 32 quoted lines
> On 2025-11-10 at 16:50:04, Phillip Wood wrote:
> > On 09/11/2025 10:22, ZheNing Hu via GitGitGadget wrote:
> > > From: ZheNing Hu <adlternative@gmail.com>
> > > 
> > >      This patch introduces the --committer option to git-commit, providing:
> > >       1. Consistency with the existing --author option
> > >       2. A more convenient alternative to environment variables
> > >       3. Better support for automated workflows and scripts
> > >       4. Improved user experience when managing multiple identities
> > 
> > What's the use case for the same person committing under different
> > identities? We already have a config mechanism to set different identities
> > for different repositories but I'm struggling to see why someone would want
> > to create commits under multiple identities in a single repository. For
> > scripts it easy enough to set the relevant environment variables if a tool
> > wants to create commits under its own identity.
> 
> Someone who works on the same project under both their personal and
> corporate identities.  For instance, me working on the Git project.
> 
> Some open source projects also require a CLA and you have to use a
> particular address to match the one that's listed on the CLA.  For
> example, Google requires an address with a Google account, so in the
> hypothetical state where I was going to contribute to one of their
> projects, I'd need to use a different committer identity with my Gmail
> address.
> 
> I've also kept business logs in Git when I had a small business and I
> might well need to log approving a profit distribution (with my
> corporate address) and log accepting a profit distribution (with my
> personal address).  Those would need separate digital signatures from my
> two different email addresses.

Is a "--committer" option the best solution there, though? I'd think you'd want to set user.* in the repo-level .git/config (or using a dir-specific include) would be less error-prone.

That doesn't help for using two identities for the same repo, but in my experience it is easier to use two separate repositories for that to match the organization of the work (even if you may sometimes fetch between them).

I'm not totally opposed to the new flag, and in general I'd defer to people who say they find a new feature useful. I'm just having a hard time imagining a scenario where it's the best option.

-Peff
Junio C Hamano· Nov 10, 2025, 22:06 UTC · re: Jeff King · lore

Re: [PATCH] commit: add --committer option

Jeff King <peff@peff.net> writes:
Show 8 quoted lines
> Is a "--committer" option the best solution there, though? I'd think
> you'd want to set user.* in the repo-level .git/config (or using a
> dir-specific include) would be less error-prone.
>
> That doesn't help for using two identities for the same repo, but in my
> experience it is easier to use two separate repositories for that to
> match the organization of the work (even if you may sometimes fetch
> between them).

This happens to match my experience, but my use case may be rather skewed. The two sets of contents managed by my two clones that push into the same repository (of course to two different branches) are rather disjoint and they never merge into each other (in fact they do not even share the root commit).

If you are working under two identities on the same codebase, I would imagine that two repo arrangement may be more cumbersome than working in a single repository and switching between the identities, and may be preferrable as long as you are confident that you won't commit a change under the "other" (wrong) identity. Of course, your reflogs and notes would also follow the "then-current" committer identity, so if I were to flip between two identities while working on the same codebase in a single repository, I am very likely to export GIT_{AUTHOR,COMMITTER}_* environment variables, dedicate that shell/window to the work done under that identity, and switch the environment variables if/when I want to switch (or have another shell/window with the other identities exported---perhaps I'd do that in a secondary worktree). I cannot imagine myself keep giving --author and --committer between my two identities without mistakes.

> I'm not totally opposed to the new flag, and in general I'd defer to
> people who say they find a new feature useful. I'm just having a hard
> time imagining a scenario where it's the best option.

Same here. The "give them long enough rope" principle tells me that this may be worth having if even only to have symmetry with existing "--author" option, but these two are not inherently symmetric to begin with, and I am not sure if there is a scenario in which this new option is the best thing to use.

Patrick Steinhardt· Nov 11, 2025, 06:54 UTC · re: Jeff King · lore

Re: [PATCH] commit: add --committer option

On Mon, Nov 10, 2025 at 03:11:36PM -0500, Jeff King wrote:
Show 47 quoted lines
> On Mon, Nov 10, 2025 at 06:01:57PM +0000, brian m. carlson wrote:
> 
> > On 2025-11-10 at 16:50:04, Phillip Wood wrote:
> > > On 09/11/2025 10:22, ZheNing Hu via GitGitGadget wrote:
> > > > From: ZheNing Hu <adlternative@gmail.com>
> > > > 
> > > >      This patch introduces the --committer option to git-commit, providing:
> > > >       1. Consistency with the existing --author option
> > > >       2. A more convenient alternative to environment variables
> > > >       3. Better support for automated workflows and scripts
> > > >       4. Improved user experience when managing multiple identities
> > > 
> > > What's the use case for the same person committing under different
> > > identities? We already have a config mechanism to set different identities
> > > for different repositories but I'm struggling to see why someone would want
> > > to create commits under multiple identities in a single repository. For
> > > scripts it easy enough to set the relevant environment variables if a tool
> > > wants to create commits under its own identity.
> > 
> > Someone who works on the same project under both their personal and
> > corporate identities.  For instance, me working on the Git project.
> > 
> > Some open source projects also require a CLA and you have to use a
> > particular address to match the one that's listed on the CLA.  For
> > example, Google requires an address with a Google account, so in the
> > hypothetical state where I was going to contribute to one of their
> > projects, I'd need to use a different committer identity with my Gmail
> > address.
> > 
> > I've also kept business logs in Git when I had a small business and I
> > might well need to log approving a profit distribution (with my
> > corporate address) and log accepting a profit distribution (with my
> > personal address).  Those would need separate digital signatures from my
> > two different email addresses.
> 
> Is a "--committer" option the best solution there, though? I'd think
> you'd want to set user.* in the repo-level .git/config (or using a
> dir-specific include) would be less error-prone.
> 
> That doesn't help for using two identities for the same repo, but in my
> experience it is easier to use two separate repositories for that to
> match the organization of the work (even if you may sometimes fetch
> between them).
> 
> I'm not totally opposed to the new flag, and in general I'd defer to
> people who say they find a new feature useful. I'm just having a hard
> time imagining a scenario where it's the best option.

The reason why I find it useful is mostly scripted uses. Sure, you can already set environment variables there. But from my experience, environment variables tend to be a significantly worse API compared to command line options:

  - They are harder to discover in the manual page.
  - You don't have any "guarantees" that Git actually interprets them,
    as there won't be an error if you mistype the name.
  - Cause and effect may be detached with environment variables, but
    with command line options that's never the case.

So I myself would prefer using "--committer" over its accompanying environment variable any point in time when I have a scripted use case for it.

Patrick
Phillip Wood· Nov 11, 2025, 14:53 UTC · re: Patrick Steinhardt · lore

Re: [PATCH] commit: add --committer option

On 11/11/2025 06:54, Patrick Steinhardt wrote:
Show 48 quoted lines
> On Mon, Nov 10, 2025 at 03:11:36PM -0500, Jeff King wrote:
>> On Mon, Nov 10, 2025 at 06:01:57PM +0000, brian m. carlson wrote:
>>
>>> On 2025-11-10 at 16:50:04, Phillip Wood wrote:
>>>> On 09/11/2025 10:22, ZheNing Hu via GitGitGadget wrote:
>>>>> From: ZheNing Hu <adlternative@gmail.com>
>>>>>
>>>>>       This patch introduces the --committer option to git-commit, providing:
>>>>>        1. Consistency with the existing --author option
>>>>>        2. A more convenient alternative to environment variables
>>>>>        3. Better support for automated workflows and scripts
>>>>>        4. Improved user experience when managing multiple identities
>>>>
>>>> What's the use case for the same person committing under different
>>>> identities? We already have a config mechanism to set different identities
>>>> for different repositories but I'm struggling to see why someone would want
>>>> to create commits under multiple identities in a single repository. For
>>>> scripts it easy enough to set the relevant environment variables if a tool
>>>> wants to create commits under its own identity.
>>>
>>> Someone who works on the same project under both their personal and
>>> corporate identities.  For instance, me working on the Git project.
>>>
>>> Some open source projects also require a CLA and you have to use a
>>> particular address to match the one that's listed on the CLA.  For
>>> example, Google requires an address with a Google account, so in the
>>> hypothetical state where I was going to contribute to one of their
>>> projects, I'd need to use a different committer identity with my Gmail
>>> address.
>>>
>>> I've also kept business logs in Git when I had a small business and I
>>> might well need to log approving a profit distribution (with my
>>> corporate address) and log accepting a profit distribution (with my
>>> personal address).  Those would need separate digital signatures from my
>>> two different email addresses.
>>
>> Is a "--committer" option the best solution there, though? I'd think
>> you'd want to set user.* in the repo-level .git/config (or using a
>> dir-specific include) would be less error-prone.
>>
>> That doesn't help for using two identities for the same repo, but in my
>> experience it is easier to use two separate repositories for that to
>> match the organization of the work (even if you may sometimes fetch
>> between them).
>>
>> I'm not totally opposed to the new flag, and in general I'd defer to
>> people who say they find a new feature useful. I'm just having a hard
>> time imagining a scenario where it's the best option.

Yes, it strikes me as very inconvenient to have to specify "--committer" each time. I'd have though you'd either want to (i) set up an alias in which case you can start your alias with "-c user.name=..." or "!GIT_COMMITTER_NAME=...", or (ii) set GIT_COMMITTER_NAME in your shell.

Show 6 quoted lines
> The reason why I find it useful is mostly scripted uses. Sure, you can
> already set environment variables there. But from my experience,
> environment variables tend to be a significantly worse API compared to
> command line options:
> 
>    - They are harder to discover in the manual page.

They're documented in the COMMIT INFORMATION section of the "git commit" man page, admittedly that comes after the options and examples but overriding the committer is a fairly niche requirement.

>    - You don't have any "guarantees" that Git actually interprets them,
>      as there won't be an error if you mistype the name.

Playing devil's advocate even if you use "--committer" you still need to check the result to make sure there were no typo's in the committer info just as you would if you were setting GIT_COMMITTER_NAME.

Show 6 quoted lines
>    - Cause and effect may be detached with environment variables, but
>      with command line options that's never the case.
> 
> So I myself would prefer using "--committer" over its accompanying
> environment variable any point in time when I have a scripted use case
> for it.

I'm wary of cluttering the UI of one of our core porcelain command with options for use with scripting.

Thanks
Phillip
ZheNing Hu· Nov 12, 2025, 16:11 UTC · re: Phillip Wood · lore

Re: [PATCH] commit: add --committer option

Phillip Wood <phillip.wood123@gmail.com> 于2025年11月11日周二 22:53写道:
Show 55 quoted lines
>
> On 11/11/2025 06:54, Patrick Steinhardt wrote:
> > On Mon, Nov 10, 2025 at 03:11:36PM -0500, Jeff King wrote:
> >> On Mon, Nov 10, 2025 at 06:01:57PM +0000, brian m. carlson wrote:
> >>
> >>> On 2025-11-10 at 16:50:04, Phillip Wood wrote:
> >>>> On 09/11/2025 10:22, ZheNing Hu via GitGitGadget wrote:
> >>>>> From: ZheNing Hu <adlternative@gmail.com>
> >>>>>
> >>>>>       This patch introduces the --committer option to git-commit, providing:
> >>>>>        1. Consistency with the existing --author option
> >>>>>        2. A more convenient alternative to environment variables
> >>>>>        3. Better support for automated workflows and scripts
> >>>>>        4. Improved user experience when managing multiple identities
> >>>>
> >>>> What's the use case for the same person committing under different
> >>>> identities? We already have a config mechanism to set different identities
> >>>> for different repositories but I'm struggling to see why someone would want
> >>>> to create commits under multiple identities in a single repository. For
> >>>> scripts it easy enough to set the relevant environment variables if a tool
> >>>> wants to create commits under its own identity.
> >>>
> >>> Someone who works on the same project under both their personal and
> >>> corporate identities.  For instance, me working on the Git project.
> >>>
> >>> Some open source projects also require a CLA and you have to use a
> >>> particular address to match the one that's listed on the CLA.  For
> >>> example, Google requires an address with a Google account, so in the
> >>> hypothetical state where I was going to contribute to one of their
> >>> projects, I'd need to use a different committer identity with my Gmail
> >>> address.
> >>>
> >>> I've also kept business logs in Git when I had a small business and I
> >>> might well need to log approving a profit distribution (with my
> >>> corporate address) and log accepting a profit distribution (with my
> >>> personal address).  Those would need separate digital signatures from my
> >>> two different email addresses.
> >>
> >> Is a "--committer" option the best solution there, though? I'd think
> >> you'd want to set user.* in the repo-level .git/config (or using a
> >> dir-specific include) would be less error-prone.
> >>
> >> That doesn't help for using two identities for the same repo, but in my
> >> experience it is easier to use two separate repositories for that to
> >> match the organization of the work (even if you may sometimes fetch
> >> between them).
> >>
> >> I'm not totally opposed to the new flag, and in general I'd defer to
> >> people who say they find a new feature useful. I'm just having a hard
> >> time imagining a scenario where it's the best option.
>
> Yes, it strikes me as very inconvenient to have to specify "--committer"
> each time. I'd have though you'd either want to (i) set up an alias in
> which case you can start your alias with "-c user.name=..." or
> "!GIT_COMMITTER_NAME=...", or (ii) set GIT_COMMITTER_NAME in your shell.

Since modifying the committer is a low-frequency operation, there's no real need to specifically configure an alias for it.

Show 11 quoted lines
> > The reason why I find it useful is mostly scripted uses. Sure, you can
> > already set environment variables there. But from my experience,
> > environment variables tend to be a significantly worse API compared to
> > command line options:
> >
> >    - They are harder to discover in the manual page.
>
> They're documented in the COMMIT INFORMATION section of the "git commit"
> man page, admittedly that comes after the options and examples but
> overriding the committer is a fairly niche requirement.
>

Although it's niche or infrequent, when configuration errors do occur, the current fix process is quite painful for users. Previously, countless users have gone through great trouble seeking solutions because they didn't know how to resolve incorrect user.name and email configurations, which prevented them from pushing code to the company's servers.

This may also suggest that Git lacks simpler ways to fix their identity. In fact, many users end up needing to use more complex tools like git filter-repo to attempt fixing their commits, whereas git commit --committer and --author could become a relatively simple and memorable fix method.

Show 7 quoted lines
> >    - You don't have any "guarantees" that Git actually interprets them,
> >      as there won't be an error if you mistype the name.
>
> Playing devil's advocate even if you use "--committer" you still need to
> check the result to make sure there were no typo's in the committer info
> just as you would if you were setting GIT_COMMITTER_NAME.
>

GIT_COMMITTER_NAME and GIT_COMMITTER_EMAIL are split into two parts. Sometimes I even forget whether GIT_COMMITTER_EMAIL should be "<email.example.com>" or "email.example.com", whereas --author or --committer provides a more compact and memorable "user <email.example.com>" pair.

Show 13 quoted lines
> >    - Cause and effect may be detached with environment variables, but
> >      with command line options that's never the case.
> >
> > So I myself would prefer using "--committer" over its accompanying
> > environment variable any point in time when I have a scripted use case
> > for it.
>
> I'm wary of cluttering the UI of one of our core porcelain command with
> options for use with scripting.
>
> Thanks
>
> Phillip
Thanks
ZheNing Hu
ZheNing Hu· Nov 11, 2025, 13:42 UTC · re: Jeff King · lore

Re: [PATCH] commit: add --committer option

Jeff King <peff@peff.net> 于2025年11月11日周二 04:11写道:
Show 49 quoted lines
>
> On Mon, Nov 10, 2025 at 06:01:57PM +0000, brian m. carlson wrote:
>
> > On 2025-11-10 at 16:50:04, Phillip Wood wrote:
> > > On 09/11/2025 10:22, ZheNing Hu via GitGitGadget wrote:
> > > > From: ZheNing Hu <adlternative@gmail.com>
> > > >
> > > >      This patch introduces the --committer option to git-commit, providing:
> > > >       1. Consistency with the existing --author option
> > > >       2. A more convenient alternative to environment variables
> > > >       3. Better support for automated workflows and scripts
> > > >       4. Improved user experience when managing multiple identities
> > >
> > > What's the use case for the same person committing under different
> > > identities? We already have a config mechanism to set different identities
> > > for different repositories but I'm struggling to see why someone would want
> > > to create commits under multiple identities in a single repository. For
> > > scripts it easy enough to set the relevant environment variables if a tool
> > > wants to create commits under its own identity.
> >
> > Someone who works on the same project under both their personal and
> > corporate identities.  For instance, me working on the Git project.
> >
> > Some open source projects also require a CLA and you have to use a
> > particular address to match the one that's listed on the CLA.  For
> > example, Google requires an address with a Google account, so in the
> > hypothetical state where I was going to contribute to one of their
> > projects, I'd need to use a different committer identity with my Gmail
> > address.
> >
> > I've also kept business logs in Git when I had a small business and I
> > might well need to log approving a profit distribution (with my
> > corporate address) and log accepting a profit distribution (with my
> > personal address).  Those would need separate digital signatures from my
> > two different email addresses.
>
> Is a "--committer" option the best solution there, though? I'd think
> you'd want to set user.* in the repo-level .git/config (or using a
> dir-specific include) would be less error-prone.
>
> That doesn't help for using two identities for the same repo, but in my
> experience it is easier to use two separate repositories for that to
> match the organization of the work (even if you may sometimes fetch
> between them).
>
> I'm not totally opposed to the new flag, and in general I'd defer to
> people who say they find a new feature useful. I'm just having a hard
> time imagining a scenario where it's the best option.
>

Sometimes it's because I forgot to configure the repository-level git user config and started development first. Only when I tried to correct the committer did I feel the pain.

> -Peff
Jeff King· Nov 11, 2025, 19:15 UTC · re: ZheNing Hu · lore

Re: [PATCH] commit: add --committer option

On Tue, Nov 11, 2025 at 09:42:38PM +0800, ZheNing Hu wrote:
Show 16 quoted lines
> > Is a "--committer" option the best solution there, though? I'd think
> > you'd want to set user.* in the repo-level .git/config (or using a
> > dir-specific include) would be less error-prone.
> >
> > That doesn't help for using two identities for the same repo, but in my
> > experience it is easier to use two separate repositories for that to
> > match the organization of the work (even if you may sometimes fetch
> > between them).
> >
> > I'm not totally opposed to the new flag, and in general I'd defer to
> > people who say they find a new feature useful. I'm just having a hard
> > time imagining a scenario where it's the best option.
> 
> Sometimes it's because I forgot to configure the repository-level git user
> config and started development first. Only when I tried to correct the
> committer did I feel the pain.

OK, this workflow does make sense to me. Fixing up an earlier mistake is inherently a one-off thing, and a command-line option is more ergonomic than using the environment variables.

Two small thoughts:
  - I suspect what you'd usually want there is for the committer and the
    author to match. We have --committer-date-is-author-date for rebase,
    and conceptually I think something like --committer-is-author would
    do what you want here. But obviously it's less flexible, and I don't
    know if it's that much easier to use.
  - Because it's easy to make such mistakes, when you override the
    author (so that it doesn't match the committer), git-commit prints
    an extra "Author:" line in the output to make that more obvious.
    Should we do the same with committer when you've overridden it?  We
    already do print "Committer:" when the ident was guessed from system
    info, but I wonder if it would make sense to print when it was
    forced. I dunno. I guess the time you most need the hint is when you
    meant to use --committer and --author together, but only used
    --author. But I don't know how Git would infer that case (versus the
    normal case of you applying someone else's work and crediting them
    with --author).

I'm not sure if either is a useful direction, but they seemed sufficiently not-dumb for me to at least type them out. ;)

-Peff
Junio C Hamano· Nov 11, 2025, 20:16 UTC · re: Jeff King · lore

Re: [PATCH] commit: add --committer option

Jeff King <peff@peff.net> writes:
Show 7 quoted lines
>> Sometimes it's because I forgot to configure the repository-level git user
>> config and started development first. Only when I tried to correct the
>> committer did I feel the pain.
>
> OK, this workflow does make sense to me. Fixing up an earlier mistake is
> inherently a one-off thing, and a command-line option is more ergonomic
> than using the environment variables.

Not very much, at least to me. Fixing up an earlier mistake may be one-shot thing but it is to correct multiple commits in one go, which would be error prone if you do so with "git commit --option". Either "fast-export | fast-import" pipe, or "git rebase" (which this patch does not give --committer option, but it already knows how to honor existing environment variables) would be used for that, no?

Show 7 quoted lines
> Two small thoughts:
>
>   - I suspect what you'd usually want there is for the committer and the
>     author to match. We have --committer-date-is-author-date for rebase,
>     and conceptually I think something like --committer-is-author would
>     do what you want here. But obviously it's less flexible, and I don't
>     know if it's that much easier to use.
I am not sure how the user experience of this would look like.
Show 11 quoted lines
>   - Because it's easy to make such mistakes, when you override the
>     author (so that it doesn't match the committer), git-commit prints
>     an extra "Author:" line in the output to make that more obvious.
>     Should we do the same with committer when you've overridden it?  We
>     already do print "Committer:" when the ident was guessed from system
>     info, but I wonder if it would make sense to print when it was
>     forced. I dunno. I guess the time you most need the hint is when you
>     meant to use --committer and --author together, but only used
>     --author. But I don't know how Git would infer that case (versus the
>     normal case of you applying someone else's work and crediting them
>     with --author).

Yup, guessing based on what you did _not_ give is always a hard task for any tool ;-).

> I'm not sure if either is a useful direction, but they seemed
> sufficiently not-dumb for me to at least type them out. ;)
>
> -Peff
Jeff King· Nov 11, 2025, 21:33 UTC · re: Junio C Hamano · lore

Re: [PATCH] commit: add --committer option

On Tue, Nov 11, 2025 at 12:16:40PM -0800, Junio C Hamano wrote:
Show 16 quoted lines
> Jeff King <peff@peff.net> writes:
> 
> >> Sometimes it's because I forgot to configure the repository-level git user
> >> config and started development first. Only when I tried to correct the
> >> committer did I feel the pain.
> >
> > OK, this workflow does make sense to me. Fixing up an earlier mistake is
> > inherently a one-off thing, and a command-line option is more ergonomic
> > than using the environment variables.
> 
> Not very much, at least to me.  Fixing up an earlier mistake may be
> one-shot thing but it is to correct multiple commits in one go,
> which would be error prone if you do so with "git commit --option".
> Either "fast-export | fast-import" pipe, or "git rebase" (which this
> patch does not give --committer option, but it already knows how to
> honor existing environment variables) would be used for that, no?

I usually lean on "commit --amend" for this, coupled with rebase if there are multiple commits. So I've used:

  git rebase -x "git commit --no-edit --amend --author=..."
 
or similar when fixing up incorrect application of somebody else's
patches (e.g., if I ended up using "git apply" and tweaking the commit
message myself, rather than using "git am").
Show 9 quoted lines
> > Two small thoughts:
> >
> >   - I suspect what you'd usually want there is for the committer and the
> >     author to match. We have --committer-date-is-author-date for rebase,
> >     and conceptually I think something like --committer-is-author would
> >     do what you want here. But obviously it's less flexible, and I don't
> >     know if it's that much easier to use.
> 
> I am not sure how the user experience of this would look like.
I just mean being able to do:
  git commit --amend --author='Foo Bar <foo@example.com>' --committer-is-author
instead of:
  git commit --amend --author='Foo Bar <foo@example.com>' --committer='Foo Bar <foo@example.com>'
-Peff
Junio C Hamano· Nov 11, 2025, 21:58 UTC · re: Jeff King · lore

Re: [PATCH] commit: add --committer option

Jeff King <peff@peff.net> writes:
Show 7 quoted lines
> I just mean being able to do:
>
>   git commit --amend --author='Foo Bar <foo@example.com>' --committer-is-author
>
> instead of:
>
>   git commit --amend --author='Foo Bar <foo@example.com>' --committer='Foo Bar <foo@example.com>'
Ah, I see.  Like
    git -c user.name='Foo Bar' -c user.email=foo@example.com commit --amend
Makes me wonder if we want user.ident that covers them both ;-)
Jeff King· Nov 11, 2025, 22:23 UTC · re: Junio C Hamano · lore

Re: [PATCH] commit: add --committer option

On Tue, Nov 11, 2025 at 01:58:21PM -0800, Junio C Hamano wrote:
Show 15 quoted lines
> Jeff King <peff@peff.net> writes:
> 
> > I just mean being able to do:
> >
> >   git commit --amend --author='Foo Bar <foo@example.com>' --committer-is-author
> >
> > instead of:
> >
> >   git commit --amend --author='Foo Bar <foo@example.com>' --committer='Foo Bar <foo@example.com>'
> 
> Ah, I see.  Like
> 
>     git -c user.name='Foo Bar' -c user.email=foo@example.com commit --amend
> 
> Makes me wonder if we want user.ident that covers them both ;-)

Hmm, I hadn't thought to use "-c" config for this. That makes me question the utility of --committer a little bit. ;) I guess it is slightly more convenient than "-c" in that it will trigger the find_author_by_nickname() magic.

-Peff
ZheNing Hu· Nov 12, 2025, 16:51 UTC · re: Jeff King · lore

Re: [PATCH] commit: add --committer option

Jeff King <peff@peff.net> 于2025年11月12日周三 06:23写道:
Show 24 quoted lines
>
> On Tue, Nov 11, 2025 at 01:58:21PM -0800, Junio C Hamano wrote:
>
> > Jeff King <peff@peff.net> writes:
> >
> > > I just mean being able to do:
> > >
> > >   git commit --amend --author='Foo Bar <foo@example.com>' --committer-is-author
> > >
> > > instead of:
> > >
> > >   git commit --amend --author='Foo Bar <foo@example.com>' --committer='Foo Bar <foo@example.com>'
> >
> > Ah, I see.  Like
> >
> >     git -c user.name='Foo Bar' -c user.email=foo@example.com commit --amend
> >
> > Makes me wonder if we want user.ident that covers them both ;-)
>
> Hmm, I hadn't thought to use "-c" config for this. That makes me
> question the utility of --committer a little bit. ;) I guess it is
> slightly more convenient than "-c" in that it will trigger the
> find_author_by_nickname() magic.
>
The advantages of --committer over -c user.name -c user.email:
1. It's symmetric with --author
2. A single option is sufficient
3. As you mentioned, it can search for the committer in commit
history (although that wasn't my original intention)
> -Peff
ZheNing Hu· Nov 12, 2025, 16:48 UTC · re: Junio C Hamano · lore

Re: [PATCH] commit: add --committer option

Junio C Hamano <gitster@pobox.com> 于2025年11月12日周三 05:58写道:
Show 15 quoted lines
>
> Jeff King <peff@peff.net> writes:
>
> > I just mean being able to do:
> >
> >   git commit --amend --author='Foo Bar <foo@example.com>' --committer-is-author
> >
> > instead of:
> >
> >   git commit --amend --author='Foo Bar <foo@example.com>' --committer='Foo Bar <foo@example.com>'
>
> Ah, I see.  Like
>
>     git -c user.name='Foo Bar' -c user.email=foo@example.com commit --amend
>
You still need:

git -c user.name ='Foo Bar' -c user.email=foo@example.com commit --amend --author

It's hard to remember and not symmetric enough.
> Makes me wonder if we want user.ident that covers them both ;-)
ZheNing Hu· Nov 12, 2025, 16:46 UTC · re: Jeff King · lore

Re: [PATCH] commit: add --committer option

Jeff King <peff@peff.net> 于2025年11月12日周三 05:33写道:
Show 47 quoted lines
>
> On Tue, Nov 11, 2025 at 12:16:40PM -0800, Junio C Hamano wrote:
>
> > Jeff King <peff@peff.net> writes:
> >
> > >> Sometimes it's because I forgot to configure the repository-level git user
> > >> config and started development first. Only when I tried to correct the
> > >> committer did I feel the pain.
> > >
> > > OK, this workflow does make sense to me. Fixing up an earlier mistake is
> > > inherently a one-off thing, and a command-line option is more ergonomic
> > > than using the environment variables.
> >
> > Not very much, at least to me.  Fixing up an earlier mistake may be
> > one-shot thing but it is to correct multiple commits in one go,
> > which would be error prone if you do so with "git commit --option".
> > Either "fast-export | fast-import" pipe, or "git rebase" (which this
> > patch does not give --committer option, but it already knows how to
> > honor existing environment variables) would be used for that, no?
>
> I usually lean on "commit --amend" for this, coupled with rebase if
> there are multiple commits. So I've used:
>
>   git rebase -x "git commit --no-edit --amend --author=..."
>
> or similar when fixing up incorrect application of somebody else's
> patches (e.g., if I ended up using "git apply" and tweaking the commit
> message myself, rather than using "git am").
>
> > > Two small thoughts:
> > >
> > >   - I suspect what you'd usually want there is for the committer and the
> > >     author to match. We have --committer-date-is-author-date for rebase,
> > >     and conceptually I think something like --committer-is-author would
> > >     do what you want here. But obviously it's less flexible, and I don't
> > >     know if it's that much easier to use.
> >
> > I am not sure how the user experience of this would look like.
>
> I just mean being able to do:
>
>   git commit --amend --author='Foo Bar <foo@example.com>' --committer-is-author
>
> instead of:
>
>   git commit --amend --author='Foo Bar <foo@example.com>' --committer='Foo Bar <foo@example.com>'
>

If I wanted an elegant parameter myself, it would definitely be `git commit --amend --user='Foo Bar <foo@example.com>'`, instead of `-A -B-is-A`

> -Peff
ZheNing Hu· Nov 12, 2025, 16:41 UTC · re: Junio C Hamano · lore

Re: [PATCH] commit: add --committer option

Junio C Hamano <gitster@pobox.com> 于2025年11月12日周三 04:16写道:
Show 18 quoted lines
>
> Jeff King <peff@peff.net> writes:
>
> >> Sometimes it's because I forgot to configure the repository-level git user
> >> config and started development first. Only when I tried to correct the
> >> committer did I feel the pain.
> >
> > OK, this workflow does make sense to me. Fixing up an earlier mistake is
> > inherently a one-off thing, and a command-line option is more ergonomic
> > than using the environment variables.
>
> Not very much, at least to me.  Fixing up an earlier mistake may be
> one-shot thing but it is to correct multiple commits in one go,
> which would be error prone if you do so with "git commit --option".
> Either "fast-export | fast-import" pipe, or "git rebase" (which this
> patch does not give --committer option, but it already knows how to
> honor existing environment variables) would be used for that, no?
>

Ha, perhaps this should be left to git rebase --committer --author in the future. GIT_AUTHOR_* and GIT_COMMITTER_* are indeed a bit cumbersome to use.

Show 29 quoted lines
> > Two small thoughts:
> >
> >   - I suspect what you'd usually want there is for the committer and the
> >     author to match. We have --committer-date-is-author-date for rebase,
> >     and conceptually I think something like --committer-is-author would
> >     do what you want here. But obviously it's less flexible, and I don't
> >     know if it's that much easier to use.
>
> I am not sure how the user experience of this would look like.
>
> >   - Because it's easy to make such mistakes, when you override the
> >     author (so that it doesn't match the committer), git-commit prints
> >     an extra "Author:" line in the output to make that more obvious.
> >     Should we do the same with committer when you've overridden it?  We
> >     already do print "Committer:" when the ident was guessed from system
> >     info, but I wonder if it would make sense to print when it was
> >     forced. I dunno. I guess the time you most need the hint is when you
> >     meant to use --committer and --author together, but only used
> >     --author. But I don't know how Git would infer that case (versus the
> >     normal case of you applying someone else's work and crediting them
> >     with --author).
>
> Yup, guessing based on what you did _not_ give is always a hard task
> for any tool ;-).
>
> > I'm not sure if either is a useful direction, but they seemed
> > sufficiently not-dumb for me to at least type them out. ;)
> >
> > -Peff
ZheNing Hu· Nov 12, 2025, 16:37 UTC · re: Jeff King · lore

Re: [PATCH] commit: add --committer option

Jeff King <peff@peff.net> 于2025年11月12日周三 03:15写道:
Show 32 quoted lines
>
> On Tue, Nov 11, 2025 at 09:42:38PM +0800, ZheNing Hu wrote:
>
> > > Is a "--committer" option the best solution there, though? I'd think
> > > you'd want to set user.* in the repo-level .git/config (or using a
> > > dir-specific include) would be less error-prone.
> > >
> > > That doesn't help for using two identities for the same repo, but in my
> > > experience it is easier to use two separate repositories for that to
> > > match the organization of the work (even if you may sometimes fetch
> > > between them).
> > >
> > > I'm not totally opposed to the new flag, and in general I'd defer to
> > > people who say they find a new feature useful. I'm just having a hard
> > > time imagining a scenario where it's the best option.
> >
> > Sometimes it's because I forgot to configure the repository-level git user
> > config and started development first. Only when I tried to correct the
> > committer did I feel the pain.
>
> OK, this workflow does make sense to me. Fixing up an earlier mistake is
> inherently a one-off thing, and a command-line option is more ergonomic
> than using the environment variables.
>
> Two small thoughts:
>
>   - I suspect what you'd usually want there is for the committer and the
>     author to match. We have --committer-date-is-author-date for rebase,
>     and conceptually I think something like --committer-is-author would
>     do what you want here. But obviously it's less flexible, and I don't
>     know if it's that much easier to use.
>

Well, to be honest, I don't really like this kind of -A --BaseA option style. On the contrary, -A -B is actually simple and easy enough for me (or more symmetrical). However, it's true that git rebase probably doesn't have --author and --committer options. Perhaps this is also a pain point, or maybe something to look into for potential future contributions.

Show 12 quoted lines
>   - Because it's easy to make such mistakes, when you override the
>     author (so that it doesn't match the committer), git-commit prints
>     an extra "Author:" line in the output to make that more obvious.
>     Should we do the same with committer when you've overridden it?  We
>     already do print "Committer:" when the ident was guessed from system
>     info, but I wonder if it would make sense to print when it was
>     forced. I dunno. I guess the time you most need the hint is when you
>     meant to use --committer and --author together, but only used
>     --author. But I don't know how Git would infer that case (versus the
>     normal case of you applying someone else's work and crediting them
>     with --author).
>

Ah, you have a point. Perhaps the Commit should also be output, though it might be trivial (if you hadn't reminded me, I wouldn't have even noticed that git commit outputs Author when using --author).

> I'm not sure if either is a useful direction, but they seemed
> sufficiently not-dumb for me to at least type them out. ;)
>
> -Peff
ZheNing Hu· Nov 11, 2025, 13:01 UTC · re: Phillip Wood · lore

Re: [PATCH] commit: add --committer option

Phillip Wood <phillip.wood123@gmail.com> 于2025年11月11日周二 00:50写道:
Show 22 quoted lines
>
> Hi ZheNing
>
> On 09/11/2025 10:22, ZheNing Hu via GitGitGadget wrote:
> > From: ZheNing Hu <adlternative@gmail.com>
> >
> >
> >      This patch introduces the --committer option to git-commit, providing:
> >
> >       1. Consistency with the existing --author option
> >       2. A more convenient alternative to environment variables
> >       3. Better support for automated workflows and scripts
> >       4. Improved user experience when managing multiple identities
>
> What's the use case for the same person committing under different
> identities? We already have a config mechanism to set different
> identities for different repositories but I'm struggling to see why
> someone would want to create commits under multiple identities in a
> single repository. For scripts it easy enough to set the relevant
> environment variables if a tool wants to create commits under its own
> identity.
>

I frequently need to distinguish between different user.name and user.email configurations on our company's internal GitHub.

The current problems are:

When I misconfigure (which happens occasionally), git commit --author only fixes the author part, I still need to additionally set GIT_COMMITTER_NAME and GIT_COMMITTER_EMAIL environment variables to fix the committer information These environment variables are painful to use, requiring manual setup every time

If a --committer option could be provided to align with --author, users wouldn't need to remember and use these additional environment variables. This would greatly simplify the workflow and reduce cognitive overhead.

Show 261 quoted lines
> Thanks
>
> Phillip
>
> >      The implementation follows the same pattern as the --author option,
> >      accepting the format "Name " and properly validating the input.
> >
> > Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1997%2Fadlternative%2Fzh%2Fimplement-committer-option-v1
> > Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1997/adlternative/zh/implement-committer-option-v1
> > Pull-Request: https://github.com/gitgitgadget/git/pull/1997
> >
> >   Documentation/git-commit.adoc |  9 +++-
> >   builtin/commit.c              | 58 ++++++++++++++++++++++++-
> >   t/t7509-commit-authorship.sh  | 80 +++++++++++++++++++++++++++++++++++
> >   3 files changed, 144 insertions(+), 3 deletions(-)
> >
> > diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
> > index 54c207ad45..a015c8328e 100644
> > --- a/Documentation/git-commit.adoc
> > +++ b/Documentation/git-commit.adoc
> > @@ -12,7 +12,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
> >          [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]
> >          [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
> >          [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
> > -        [--date=<date>] [--cleanup=<mode>] [--[no-]status]
> > +        [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]
> >          [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
> >          [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
> >          [--] [<pathspec>...]
> > @@ -181,6 +181,13 @@ See linkgit:git-rebase[1] for details.
> >   `--date=<date>`::
> >       Override the author date used in the commit.
> >
> > +`--committer=<committer>`::
> > +     Override the committer for the commit. Specify an explicit committer using the
> > +     standard `A U Thor <committer@example.com>` format. Otherwise _<committer>_
> > +     is assumed to be a pattern and is used to search for an existing
> > +     commit by that author (i.e. `git rev-list --all -i --author=<committer>`);
> > +     the commit author is then copied from the first such commit found.
> > +
> >   `-m <msg>`::
> >   `--message=<msg>`::
> >       Use _<msg>_ as the commit message.
> > diff --git a/builtin/commit.c b/builtin/commit.c
> > index 0243f17d53..88e77cbaab 100644
> > --- a/builtin/commit.c
> > +++ b/builtin/commit.c
> > @@ -49,7 +49,7 @@ static const char * const builtin_commit_usage[] = {
> >          "           [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]\n"
> >          "           [-F <file> | -m <msg>] [--reset-author] [--allow-empty]\n"
> >          "           [--allow-empty-message] [--no-verify] [-e] [--author=<author>]\n"
> > -        "           [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
> > +        "           [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]\n"
> >          "           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
> >          "           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n"
> >          "           [--] [<pathspec>...]"),
> > @@ -112,6 +112,7 @@ static enum {
> >   } commit_style;
> >
> >   static const char *force_author;
> > +static const char *force_committer;
> >   static char *logfile;
> >   static char *template_file;
> >   /*
> > @@ -690,6 +691,48 @@ static void determine_author_info(struct strbuf *author_ident)
> >       free(date);
> >   }
> >
> > +static void determine_committer_info(struct strbuf *committer_ident)
> > +{
> > +     char *name, *email, *date;
> > +     struct ident_split committer;
> > +
> > +     name = xstrdup_or_null(getenv("GIT_COMMITTER_NAME"));
> > +     email = xstrdup_or_null(getenv("GIT_COMMITTER_EMAIL"));
> > +     date = xstrdup_or_null(getenv("GIT_COMMITTER_DATE"));
> > +
> > +     if (force_committer) {
> > +             struct ident_split ident;
> > +
> > +             if (split_ident_line(&ident, force_committer, strlen(force_committer)) < 0)
> > +                     die(_("malformed --committer parameter"));
> > +             set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
> > +             set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
> > +
> > +             if (ident.date_begin) {
> > +                     struct strbuf date_buf = STRBUF_INIT;
> > +                     strbuf_addch(&date_buf, '@');
> > +                     strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
> > +                     strbuf_addch(&date_buf, ' ');
> > +                     strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
> > +                     set_ident_var(&date, strbuf_detach(&date_buf, NULL));
> > +             }
> > +     }
> > +
> > +     if (force_date) {
> > +             struct strbuf date_buf = STRBUF_INIT;
> > +             if (parse_force_date(force_date, &date_buf))
> > +                     die(_("invalid date format: %s"), force_date);
> > +             set_ident_var(&date, strbuf_detach(&date_buf, NULL));
> > +     }
> > +
> > +     strbuf_addstr(committer_ident, fmt_ident(name, email, WANT_COMMITTER_IDENT, date,
> > +                             IDENT_STRICT));
> > +     assert_split_ident(&committer, committer_ident);
> > +     free(name);
> > +     free(email);
> > +     free(date);
> > +}
> > +
> >   static int author_date_is_interesting(void)
> >   {
> >       return author_message || force_date;
> > @@ -1321,6 +1364,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
> >       if (force_author && renew_authorship)
> >               die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author");
> >
> > +     if (force_committer && !strchr(force_committer, '>'))
> > +             force_committer = find_author_by_nickname(force_committer);
> > +
> >       if (logfile || have_option_m || use_message)
> >               use_editor = 0;
> >
> > @@ -1709,6 +1755,7 @@ int cmd_commit(int argc,
> >               OPT_FILENAME('F', "file", &logfile, N_("read message from file")),
> >               OPT_STRING(0, "author", &force_author, N_("author"), N_("override author for commit")),
> >               OPT_STRING(0, "date", &force_date, N_("date"), N_("override date for commit")),
> > +             OPT_STRING(0, "committer", &force_committer, N_("committer"), N_("override committer for commit")),
> >               OPT_CALLBACK('m', "message", &message, N_("message"), N_("commit message"), opt_parse_m),
> >               OPT_STRING('c', "reedit-message", &edit_message, N_("commit"), N_("reuse and edit message from specified commit")),
> >               OPT_STRING('C', "reuse-message", &use_message, N_("commit"), N_("reuse message from specified commit")),
> > @@ -1785,6 +1832,7 @@ int cmd_commit(int argc,
> >
> >       struct strbuf sb = STRBUF_INIT;
> >       struct strbuf author_ident = STRBUF_INIT;
> > +     struct strbuf committer_ident = STRBUF_INIT;
> >       const char *index_file, *reflog_msg;
> >       struct object_id oid;
> >       struct commit_list *parents = NULL;
> > @@ -1930,8 +1978,13 @@ int cmd_commit(int argc,
> >               append_merge_tag_headers(parents, &tail);
> >       }
> >
> > +     if (force_committer) {
> > +             determine_committer_info(&committer_ident);
> > +     }
> > +
> >       if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,
> > -                              parents, &oid, author_ident.buf, NULL,
> > +                              parents, &oid, author_ident.buf,
> > +                              force_committer ? committer_ident.buf : NULL,
> >                                sign_commit, extra)) {
> >               rollback_index_files();
> >               die(_("failed to write commit object"));
> > @@ -1980,6 +2033,7 @@ cleanup:
> >       free_commit_extra_headers(extra);
> >       free_commit_list(parents);
> >       strbuf_release(&author_ident);
> > +     strbuf_release(&committer_ident);
> >       strbuf_release(&err);
> >       strbuf_release(&sb);
> >       free(logfile);
> > diff --git a/t/t7509-commit-authorship.sh b/t/t7509-commit-authorship.sh
> > index 8e373b566b..45527f6a70 100755
> > --- a/t/t7509-commit-authorship.sh
> > +++ b/t/t7509-commit-authorship.sh
> > @@ -12,6 +12,11 @@ author_header () {
> >       sed -n -e '/^$/q' -e '/^author /p'
> >   }
> >
> > +committer_header () {
> > +     git cat-file commit "$1" |
> > +     sed -n -e '/^$/q' -e '/^committer /p'
> > +}
> > +
> >   message_body () {
> >       git cat-file commit "$1" |
> >       sed -e '1,/^$/d'
> > @@ -171,4 +176,79 @@ test_expect_success '--reset-author with CHERRY_PICK_HEAD' '
> >       test_cmp expect actual
> >   '
> >
> > +test_expect_success '--committer option overrides committer' '
> > +     git checkout Initial &&
> > +     echo "Test --committer" >>foo &&
> > +     test_tick &&
> > +     git commit -a -m "test committer" --committer="Custom Committer <custom@committer.example>" &&
> > +     committer_header HEAD >actual &&
> > +     grep "Custom Committer <custom@committer.example>" actual
> > +'
> > +
> > +test_expect_success '--committer with pattern search' '
> > +     echo "Test committer pattern" >>foo &&
> > +     test_tick &&
> > +     git commit -a -m "test committer pattern" --committer="Frigate" &&
> > +     committer_header HEAD >actual &&
> > +     grep "Frigate <flying@over.world>" actual
> > +'
> > +
> > +test_expect_success '--committer malformed parameter' '
> > +     echo "Test malformed" >>foo &&
> > +     test_tick &&
> > +     test_must_fail git commit -a -m "test malformed" --committer="malformed committer"
> > +'
> > +
> > +test_expect_success '--committer with --amend option' '
> > +     git checkout -f Initial &&
> > +     echo "Test committer with amend" >>foo &&
> > +     test_tick &&
> > +     git commit -a -m "initial commit for amend test" &&
> > +     echo "Modified for amend" >>foo &&
> > +     test_tick &&
> > +     git commit -a --amend --no-edit \
> > +             --author="Test Author <test@author.example>" \
> > +             --committer="Test Committer <test@committer.example>" &&
> > +     author_header HEAD >actual_author &&
> > +     grep "Test Author <test@author.example>" actual_author &&
> > +     committer_header HEAD >actual_committer &&
> > +     grep "Test Committer <test@committer.example>" actual_committer
> > +'
> > +
> > +test_expect_success 'GIT_COMMITTER_* environment variables' '
> > +     git checkout -f Initial &&
> > +     echo "Test env vars" >>foo &&
> > +     test_tick &&
> > +     GIT_COMMITTER_NAME="Env Committer" \
> > +     GIT_COMMITTER_EMAIL="env@test.example" \
> > +     git commit -a -m "test committer env vars" &&
> > +     committer_header HEAD >actual &&
> > +     grep "Env Committer <env@test.example>" actual
> > +'
> > +
> > +test_expect_success '--committer overrides GIT_COMMITTER_* environment variables' '
> > +     echo "Test override" >>foo &&
> > +     test_tick &&
> > +     GIT_COMMITTER_NAME="Env Committer" \
> > +     GIT_COMMITTER_EMAIL="env@test.example" \
> > +     git commit -a -m "test override" \
> > +             --committer="Override Committer <override@test.example>" &&
> > +     committer_header HEAD >actual &&
> > +     grep "Override Committer <override@test.example>" actual
> > +'
> > +
> > +test_expect_success '--date with --committer changes both author and committer dates' '
> > +     git checkout -f Initial &&
> > +     echo "Test date override" >>foo &&
> > +     test_tick &&
> > +     git commit -a -m "test date" \
> > +             --author="Date Author <date@author.example>" \
> > +             --committer="Date Committer <date@committer.example>" \
> > +             --date="2024-06-15 10:30:00 +0800" &&
> > +     git log -1 --format="%ai" >author_date &&
> > +     git log -1 --format="%ci" >committer_date &&
> > +     grep "2024-06-15 10:30:00 +0800" author_date &&
> > +     grep "2024-06-15 10:30:00 +0800" committer_date
> > +'
> > +
> >   test_done
> >
> > base-commit: 4badef0c3503dc29059d678abba7fac0f042bc84
>
Phillip Wood· Nov 11, 2025, 14:38 UTC · re: ZheNing Hu · lore

Re: [PATCH] commit: add --committer option

On 11/11/2025 13:01, ZheNing Hu wrote:
Show 31 quoted lines
> Phillip Wood <phillip.wood123@gmail.com> 于2025年11月11日周二 00:50写道:
>> On 09/11/2025 10:22, ZheNing Hu via GitGitGadget wrote:
>>> From: ZheNing Hu <adlternative@gmail.com>
>>>
>>>
>>>       This patch introduces the --committer option to git-commit, providing:
>>>
>>>        1. Consistency with the existing --author option
>>>        2. A more convenient alternative to environment variables
>>>        3. Better support for automated workflows and scripts
>>>        4. Improved user experience when managing multiple identities
>>
>> What's the use case for the same person committing under different
>> identities? We already have a config mechanism to set different
>> identities for different repositories but I'm struggling to see why
>> someone would want to create commits under multiple identities in a
>> single repository. For scripts it easy enough to set the relevant
>> environment variables if a tool wants to create commits under its own
>> identity.
>>
> 
> I frequently need to distinguish between different user.name and user.email
> configurations on our company's internal GitHub.
> 
> The current problems are:
> 
> When I misconfigure (which happens occasionally), git commit --author only fixes
> the author part, I still need to additionally set GIT_COMMITTER_NAME and
> GIT_COMMITTER_EMAIL environment variables to fix the committer information
> These environment variables are painful to use, requiring manual setup
> every time

I'm afraid I don't quite follow. If you are amending existing commits to fix them up after you have corrected your configuration then they will have the correct committer automatically when you run "git commit --amend --author=..." to correct the author. If you are committing before you have realized that user.{name,email} are misconfigured then I don't see how "--committer" helps because you have not yet realized anything is wrong.

Thanks
Phillip
Show 267 quoted lines
> If a --committer option could be provided to align with --author, users wouldn't
> need to remember and use these additional environment variables.
> This would greatly simplify the workflow and reduce cognitive overhead.
> 
> 
>> Thanks
>>
>> Phillip
>>
>>>       The implementation follows the same pattern as the --author option,
>>>       accepting the format "Name " and properly validating the input.
>>>
>>> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1997%2Fadlternative%2Fzh%2Fimplement-committer-option-v1
>>> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1997/adlternative/zh/implement-committer-option-v1
>>> Pull-Request: https://github.com/gitgitgadget/git/pull/1997
>>>
>>>    Documentation/git-commit.adoc |  9 +++-
>>>    builtin/commit.c              | 58 ++++++++++++++++++++++++-
>>>    t/t7509-commit-authorship.sh  | 80 +++++++++++++++++++++++++++++++++++
>>>    3 files changed, 144 insertions(+), 3 deletions(-)
>>>
>>> diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
>>> index 54c207ad45..a015c8328e 100644
>>> --- a/Documentation/git-commit.adoc
>>> +++ b/Documentation/git-commit.adoc
>>> @@ -12,7 +12,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
>>>           [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]
>>>           [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
>>>           [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
>>> -        [--date=<date>] [--cleanup=<mode>] [--[no-]status]
>>> +        [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]
>>>           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
>>>           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
>>>           [--] [<pathspec>...]
>>> @@ -181,6 +181,13 @@ See linkgit:git-rebase[1] for details.
>>>    `--date=<date>`::
>>>        Override the author date used in the commit.
>>>
>>> +`--committer=<committer>`::
>>> +     Override the committer for the commit. Specify an explicit committer using the
>>> +     standard `A U Thor <committer@example.com>` format. Otherwise _<committer>_
>>> +     is assumed to be a pattern and is used to search for an existing
>>> +     commit by that author (i.e. `git rev-list --all -i --author=<committer>`);
>>> +     the commit author is then copied from the first such commit found.
>>> +
>>>    `-m <msg>`::
>>>    `--message=<msg>`::
>>>        Use _<msg>_ as the commit message.
>>> diff --git a/builtin/commit.c b/builtin/commit.c
>>> index 0243f17d53..88e77cbaab 100644
>>> --- a/builtin/commit.c
>>> +++ b/builtin/commit.c
>>> @@ -49,7 +49,7 @@ static const char * const builtin_commit_usage[] = {
>>>           "           [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]\n"
>>>           "           [-F <file> | -m <msg>] [--reset-author] [--allow-empty]\n"
>>>           "           [--allow-empty-message] [--no-verify] [-e] [--author=<author>]\n"
>>> -        "           [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
>>> +        "           [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]\n"
>>>           "           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
>>>           "           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n"
>>>           "           [--] [<pathspec>...]"),
>>> @@ -112,6 +112,7 @@ static enum {
>>>    } commit_style;
>>>
>>>    static const char *force_author;
>>> +static const char *force_committer;
>>>    static char *logfile;
>>>    static char *template_file;
>>>    /*
>>> @@ -690,6 +691,48 @@ static void determine_author_info(struct strbuf *author_ident)
>>>        free(date);
>>>    }
>>>
>>> +static void determine_committer_info(struct strbuf *committer_ident)
>>> +{
>>> +     char *name, *email, *date;
>>> +     struct ident_split committer;
>>> +
>>> +     name = xstrdup_or_null(getenv("GIT_COMMITTER_NAME"));
>>> +     email = xstrdup_or_null(getenv("GIT_COMMITTER_EMAIL"));
>>> +     date = xstrdup_or_null(getenv("GIT_COMMITTER_DATE"));
>>> +
>>> +     if (force_committer) {
>>> +             struct ident_split ident;
>>> +
>>> +             if (split_ident_line(&ident, force_committer, strlen(force_committer)) < 0)
>>> +                     die(_("malformed --committer parameter"));
>>> +             set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
>>> +             set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
>>> +
>>> +             if (ident.date_begin) {
>>> +                     struct strbuf date_buf = STRBUF_INIT;
>>> +                     strbuf_addch(&date_buf, '@');
>>> +                     strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
>>> +                     strbuf_addch(&date_buf, ' ');
>>> +                     strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
>>> +                     set_ident_var(&date, strbuf_detach(&date_buf, NULL));
>>> +             }
>>> +     }
>>> +
>>> +     if (force_date) {
>>> +             struct strbuf date_buf = STRBUF_INIT;
>>> +             if (parse_force_date(force_date, &date_buf))
>>> +                     die(_("invalid date format: %s"), force_date);
>>> +             set_ident_var(&date, strbuf_detach(&date_buf, NULL));
>>> +     }
>>> +
>>> +     strbuf_addstr(committer_ident, fmt_ident(name, email, WANT_COMMITTER_IDENT, date,
>>> +                             IDENT_STRICT));
>>> +     assert_split_ident(&committer, committer_ident);
>>> +     free(name);
>>> +     free(email);
>>> +     free(date);
>>> +}
>>> +
>>>    static int author_date_is_interesting(void)
>>>    {
>>>        return author_message || force_date;
>>> @@ -1321,6 +1364,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
>>>        if (force_author && renew_authorship)
>>>                die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author");
>>>
>>> +     if (force_committer && !strchr(force_committer, '>'))
>>> +             force_committer = find_author_by_nickname(force_committer);
>>> +
>>>        if (logfile || have_option_m || use_message)
>>>                use_editor = 0;
>>>
>>> @@ -1709,6 +1755,7 @@ int cmd_commit(int argc,
>>>                OPT_FILENAME('F', "file", &logfile, N_("read message from file")),
>>>                OPT_STRING(0, "author", &force_author, N_("author"), N_("override author for commit")),
>>>                OPT_STRING(0, "date", &force_date, N_("date"), N_("override date for commit")),
>>> +             OPT_STRING(0, "committer", &force_committer, N_("committer"), N_("override committer for commit")),
>>>                OPT_CALLBACK('m', "message", &message, N_("message"), N_("commit message"), opt_parse_m),
>>>                OPT_STRING('c', "reedit-message", &edit_message, N_("commit"), N_("reuse and edit message from specified commit")),
>>>                OPT_STRING('C', "reuse-message", &use_message, N_("commit"), N_("reuse message from specified commit")),
>>> @@ -1785,6 +1832,7 @@ int cmd_commit(int argc,
>>>
>>>        struct strbuf sb = STRBUF_INIT;
>>>        struct strbuf author_ident = STRBUF_INIT;
>>> +     struct strbuf committer_ident = STRBUF_INIT;
>>>        const char *index_file, *reflog_msg;
>>>        struct object_id oid;
>>>        struct commit_list *parents = NULL;
>>> @@ -1930,8 +1978,13 @@ int cmd_commit(int argc,
>>>                append_merge_tag_headers(parents, &tail);
>>>        }
>>>
>>> +     if (force_committer) {
>>> +             determine_committer_info(&committer_ident);
>>> +     }
>>> +
>>>        if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,
>>> -                              parents, &oid, author_ident.buf, NULL,
>>> +                              parents, &oid, author_ident.buf,
>>> +                              force_committer ? committer_ident.buf : NULL,
>>>                                 sign_commit, extra)) {
>>>                rollback_index_files();
>>>                die(_("failed to write commit object"));
>>> @@ -1980,6 +2033,7 @@ cleanup:
>>>        free_commit_extra_headers(extra);
>>>        free_commit_list(parents);
>>>        strbuf_release(&author_ident);
>>> +     strbuf_release(&committer_ident);
>>>        strbuf_release(&err);
>>>        strbuf_release(&sb);
>>>        free(logfile);
>>> diff --git a/t/t7509-commit-authorship.sh b/t/t7509-commit-authorship.sh
>>> index 8e373b566b..45527f6a70 100755
>>> --- a/t/t7509-commit-authorship.sh
>>> +++ b/t/t7509-commit-authorship.sh
>>> @@ -12,6 +12,11 @@ author_header () {
>>>        sed -n -e '/^$/q' -e '/^author /p'
>>>    }
>>>
>>> +committer_header () {
>>> +     git cat-file commit "$1" |
>>> +     sed -n -e '/^$/q' -e '/^committer /p'
>>> +}
>>> +
>>>    message_body () {
>>>        git cat-file commit "$1" |
>>>        sed -e '1,/^$/d'
>>> @@ -171,4 +176,79 @@ test_expect_success '--reset-author with CHERRY_PICK_HEAD' '
>>>        test_cmp expect actual
>>>    '
>>>
>>> +test_expect_success '--committer option overrides committer' '
>>> +     git checkout Initial &&
>>> +     echo "Test --committer" >>foo &&
>>> +     test_tick &&
>>> +     git commit -a -m "test committer" --committer="Custom Committer <custom@committer.example>" &&
>>> +     committer_header HEAD >actual &&
>>> +     grep "Custom Committer <custom@committer.example>" actual
>>> +'
>>> +
>>> +test_expect_success '--committer with pattern search' '
>>> +     echo "Test committer pattern" >>foo &&
>>> +     test_tick &&
>>> +     git commit -a -m "test committer pattern" --committer="Frigate" &&
>>> +     committer_header HEAD >actual &&
>>> +     grep "Frigate <flying@over.world>" actual
>>> +'
>>> +
>>> +test_expect_success '--committer malformed parameter' '
>>> +     echo "Test malformed" >>foo &&
>>> +     test_tick &&
>>> +     test_must_fail git commit -a -m "test malformed" --committer="malformed committer"
>>> +'
>>> +
>>> +test_expect_success '--committer with --amend option' '
>>> +     git checkout -f Initial &&
>>> +     echo "Test committer with amend" >>foo &&
>>> +     test_tick &&
>>> +     git commit -a -m "initial commit for amend test" &&
>>> +     echo "Modified for amend" >>foo &&
>>> +     test_tick &&
>>> +     git commit -a --amend --no-edit \
>>> +             --author="Test Author <test@author.example>" \
>>> +             --committer="Test Committer <test@committer.example>" &&
>>> +     author_header HEAD >actual_author &&
>>> +     grep "Test Author <test@author.example>" actual_author &&
>>> +     committer_header HEAD >actual_committer &&
>>> +     grep "Test Committer <test@committer.example>" actual_committer
>>> +'
>>> +
>>> +test_expect_success 'GIT_COMMITTER_* environment variables' '
>>> +     git checkout -f Initial &&
>>> +     echo "Test env vars" >>foo &&
>>> +     test_tick &&
>>> +     GIT_COMMITTER_NAME="Env Committer" \
>>> +     GIT_COMMITTER_EMAIL="env@test.example" \
>>> +     git commit -a -m "test committer env vars" &&
>>> +     committer_header HEAD >actual &&
>>> +     grep "Env Committer <env@test.example>" actual
>>> +'
>>> +
>>> +test_expect_success '--committer overrides GIT_COMMITTER_* environment variables' '
>>> +     echo "Test override" >>foo &&
>>> +     test_tick &&
>>> +     GIT_COMMITTER_NAME="Env Committer" \
>>> +     GIT_COMMITTER_EMAIL="env@test.example" \
>>> +     git commit -a -m "test override" \
>>> +             --committer="Override Committer <override@test.example>" &&
>>> +     committer_header HEAD >actual &&
>>> +     grep "Override Committer <override@test.example>" actual
>>> +'
>>> +
>>> +test_expect_success '--date with --committer changes both author and committer dates' '
>>> +     git checkout -f Initial &&
>>> +     echo "Test date override" >>foo &&
>>> +     test_tick &&
>>> +     git commit -a -m "test date" \
>>> +             --author="Date Author <date@author.example>" \
>>> +             --committer="Date Committer <date@committer.example>" \
>>> +             --date="2024-06-15 10:30:00 +0800" &&
>>> +     git log -1 --format="%ai" >author_date &&
>>> +     git log -1 --format="%ci" >committer_date &&
>>> +     grep "2024-06-15 10:30:00 +0800" author_date &&
>>> +     grep "2024-06-15 10:30:00 +0800" committer_date
>>> +'
>>> +
>>>    test_done
>>>
>>> base-commit: 4badef0c3503dc29059d678abba7fac0f042bc84
>>
> 
ZheNing Hu· Nov 12, 2025, 15:58 UTC · re: Phillip Wood · lore

Re: [PATCH] commit: add --committer option

Phillip Wood <phillip.wood123@gmail.com> 于2025年11月11日周二 22:38写道:
Show 42 quoted lines
>
> On 11/11/2025 13:01, ZheNing Hu wrote:
> > Phillip Wood <phillip.wood123@gmail.com> 于2025年11月11日周二 00:50写道:
> >> On 09/11/2025 10:22, ZheNing Hu via GitGitGadget wrote:
> >>> From: ZheNing Hu <adlternative@gmail.com>
> >>>
> >>>
> >>>       This patch introduces the --committer option to git-commit, providing:
> >>>
> >>>        1. Consistency with the existing --author option
> >>>        2. A more convenient alternative to environment variables
> >>>        3. Better support for automated workflows and scripts
> >>>        4. Improved user experience when managing multiple identities
> >>
> >> What's the use case for the same person committing under different
> >> identities? We already have a config mechanism to set different
> >> identities for different repositories but I'm struggling to see why
> >> someone would want to create commits under multiple identities in a
> >> single repository. For scripts it easy enough to set the relevant
> >> environment variables if a tool wants to create commits under its own
> >> identity.
> >>
> >
> > I frequently need to distinguish between different user.name and user.email
> > configurations on our company's internal GitHub.
> >
> > The current problems are:
> >
> > When I misconfigure (which happens occasionally), git commit --author only fixes
> > the author part, I still need to additionally set GIT_COMMITTER_NAME and
> > GIT_COMMITTER_EMAIL environment variables to fix the committer information
> > These environment variables are painful to use, requiring manual setup
> > every time
>
> I'm afraid I don't quite follow. If you are amending existing commits to
> fix them up after you have corrected your configuration then they will
> have the correct committer automatically when you run "git commit
> --amend --author=..." to correct the author. If you are committing
> before you have realized that user.{name,email} are misconfigured then I
> don't see how "--committer" helps because you have not yet realized
> anything is wrong.
>
You're right that after realizing the misconfiguration and correcting the
repository's user.name and user.email, running `git commit --amend` will
 fix the committer information, but the author remains unchanged. Users
then need an additional `git commit --amend --author=...` to fix the author,
which does work but requires an extra step.

I see your point that this becomes more cumbersome when dealing with multiple commits. In such cases, users currently need to use something like:

``` GIT_AUTHOR_NAME="..." GIT_AUTHOR_EMAIL="..." \ GIT_COMMITTER_NAME="..." GIT_COMMITTER_EMAIL="..." \ git rebase -f <target> ```

This is indeed tedious and error-prone, especially when you want to quickly fix and push commits to the platform.

`git commit --amend --author --committer` or a new `git rebase --author --committer` would provide a more user-friendly workflow for correcting identity information after misconfiguration, eliminating the need to manually set multiple environment variables or run multiple commands.

> Thanks
>
> Phillip
>
Thanks
ZheNing Hu
Show 268 quoted lines
> > If a --committer option could be provided to align with --author, users wouldn't
> > need to remember and use these additional environment variables.
> > This would greatly simplify the workflow and reduce cognitive overhead.
> >
> >
> >> Thanks
> >>
> >> Phillip
> >>
> >>>       The implementation follows the same pattern as the --author option,
> >>>       accepting the format "Name " and properly validating the input.
> >>>
> >>> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1997%2Fadlternative%2Fzh%2Fimplement-committer-option-v1
> >>> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1997/adlternative/zh/implement-committer-option-v1
> >>> Pull-Request: https://github.com/gitgitgadget/git/pull/1997
> >>>
> >>>    Documentation/git-commit.adoc |  9 +++-
> >>>    builtin/commit.c              | 58 ++++++++++++++++++++++++-
> >>>    t/t7509-commit-authorship.sh  | 80 +++++++++++++++++++++++++++++++++++
> >>>    3 files changed, 144 insertions(+), 3 deletions(-)
> >>>
> >>> diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
> >>> index 54c207ad45..a015c8328e 100644
> >>> --- a/Documentation/git-commit.adoc
> >>> +++ b/Documentation/git-commit.adoc
> >>> @@ -12,7 +12,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
> >>>           [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]
> >>>           [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
> >>>           [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
> >>> -        [--date=<date>] [--cleanup=<mode>] [--[no-]status]
> >>> +        [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]
> >>>           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
> >>>           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
> >>>           [--] [<pathspec>...]
> >>> @@ -181,6 +181,13 @@ See linkgit:git-rebase[1] for details.
> >>>    `--date=<date>`::
> >>>        Override the author date used in the commit.
> >>>
> >>> +`--committer=<committer>`::
> >>> +     Override the committer for the commit. Specify an explicit committer using the
> >>> +     standard `A U Thor <committer@example.com>` format. Otherwise _<committer>_
> >>> +     is assumed to be a pattern and is used to search for an existing
> >>> +     commit by that author (i.e. `git rev-list --all -i --author=<committer>`);
> >>> +     the commit author is then copied from the first such commit found.
> >>> +
> >>>    `-m <msg>`::
> >>>    `--message=<msg>`::
> >>>        Use _<msg>_ as the commit message.
> >>> diff --git a/builtin/commit.c b/builtin/commit.c
> >>> index 0243f17d53..88e77cbaab 100644
> >>> --- a/builtin/commit.c
> >>> +++ b/builtin/commit.c
> >>> @@ -49,7 +49,7 @@ static const char * const builtin_commit_usage[] = {
> >>>           "           [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]\n"
> >>>           "           [-F <file> | -m <msg>] [--reset-author] [--allow-empty]\n"
> >>>           "           [--allow-empty-message] [--no-verify] [-e] [--author=<author>]\n"
> >>> -        "           [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
> >>> +        "           [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]\n"
> >>>           "           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
> >>>           "           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n"
> >>>           "           [--] [<pathspec>...]"),
> >>> @@ -112,6 +112,7 @@ static enum {
> >>>    } commit_style;
> >>>
> >>>    static const char *force_author;
> >>> +static const char *force_committer;
> >>>    static char *logfile;
> >>>    static char *template_file;
> >>>    /*
> >>> @@ -690,6 +691,48 @@ static void determine_author_info(struct strbuf *author_ident)
> >>>        free(date);
> >>>    }
> >>>
> >>> +static void determine_committer_info(struct strbuf *committer_ident)
> >>> +{
> >>> +     char *name, *email, *date;
> >>> +     struct ident_split committer;
> >>> +
> >>> +     name = xstrdup_or_null(getenv("GIT_COMMITTER_NAME"));
> >>> +     email = xstrdup_or_null(getenv("GIT_COMMITTER_EMAIL"));
> >>> +     date = xstrdup_or_null(getenv("GIT_COMMITTER_DATE"));
> >>> +
> >>> +     if (force_committer) {
> >>> +             struct ident_split ident;
> >>> +
> >>> +             if (split_ident_line(&ident, force_committer, strlen(force_committer)) < 0)
> >>> +                     die(_("malformed --committer parameter"));
> >>> +             set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
> >>> +             set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
> >>> +
> >>> +             if (ident.date_begin) {
> >>> +                     struct strbuf date_buf = STRBUF_INIT;
> >>> +                     strbuf_addch(&date_buf, '@');
> >>> +                     strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
> >>> +                     strbuf_addch(&date_buf, ' ');
> >>> +                     strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
> >>> +                     set_ident_var(&date, strbuf_detach(&date_buf, NULL));
> >>> +             }
> >>> +     }
> >>> +
> >>> +     if (force_date) {
> >>> +             struct strbuf date_buf = STRBUF_INIT;
> >>> +             if (parse_force_date(force_date, &date_buf))
> >>> +                     die(_("invalid date format: %s"), force_date);
> >>> +             set_ident_var(&date, strbuf_detach(&date_buf, NULL));
> >>> +     }
> >>> +
> >>> +     strbuf_addstr(committer_ident, fmt_ident(name, email, WANT_COMMITTER_IDENT, date,
> >>> +                             IDENT_STRICT));
> >>> +     assert_split_ident(&committer, committer_ident);
> >>> +     free(name);
> >>> +     free(email);
> >>> +     free(date);
> >>> +}
> >>> +
> >>>    static int author_date_is_interesting(void)
> >>>    {
> >>>        return author_message || force_date;
> >>> @@ -1321,6 +1364,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
> >>>        if (force_author && renew_authorship)
> >>>                die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author");
> >>>
> >>> +     if (force_committer && !strchr(force_committer, '>'))
> >>> +             force_committer = find_author_by_nickname(force_committer);
> >>> +
> >>>        if (logfile || have_option_m || use_message)
> >>>                use_editor = 0;
> >>>
> >>> @@ -1709,6 +1755,7 @@ int cmd_commit(int argc,
> >>>                OPT_FILENAME('F', "file", &logfile, N_("read message from file")),
> >>>                OPT_STRING(0, "author", &force_author, N_("author"), N_("override author for commit")),
> >>>                OPT_STRING(0, "date", &force_date, N_("date"), N_("override date for commit")),
> >>> +             OPT_STRING(0, "committer", &force_committer, N_("committer"), N_("override committer for commit")),
> >>>                OPT_CALLBACK('m', "message", &message, N_("message"), N_("commit message"), opt_parse_m),
> >>>                OPT_STRING('c', "reedit-message", &edit_message, N_("commit"), N_("reuse and edit message from specified commit")),
> >>>                OPT_STRING('C', "reuse-message", &use_message, N_("commit"), N_("reuse message from specified commit")),
> >>> @@ -1785,6 +1832,7 @@ int cmd_commit(int argc,
> >>>
> >>>        struct strbuf sb = STRBUF_INIT;
> >>>        struct strbuf author_ident = STRBUF_INIT;
> >>> +     struct strbuf committer_ident = STRBUF_INIT;
> >>>        const char *index_file, *reflog_msg;
> >>>        struct object_id oid;
> >>>        struct commit_list *parents = NULL;
> >>> @@ -1930,8 +1978,13 @@ int cmd_commit(int argc,
> >>>                append_merge_tag_headers(parents, &tail);
> >>>        }
> >>>
> >>> +     if (force_committer) {
> >>> +             determine_committer_info(&committer_ident);
> >>> +     }
> >>> +
> >>>        if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,
> >>> -                              parents, &oid, author_ident.buf, NULL,
> >>> +                              parents, &oid, author_ident.buf,
> >>> +                              force_committer ? committer_ident.buf : NULL,
> >>>                                 sign_commit, extra)) {
> >>>                rollback_index_files();
> >>>                die(_("failed to write commit object"));
> >>> @@ -1980,6 +2033,7 @@ cleanup:
> >>>        free_commit_extra_headers(extra);
> >>>        free_commit_list(parents);
> >>>        strbuf_release(&author_ident);
> >>> +     strbuf_release(&committer_ident);
> >>>        strbuf_release(&err);
> >>>        strbuf_release(&sb);
> >>>        free(logfile);
> >>> diff --git a/t/t7509-commit-authorship.sh b/t/t7509-commit-authorship.sh
> >>> index 8e373b566b..45527f6a70 100755
> >>> --- a/t/t7509-commit-authorship.sh
> >>> +++ b/t/t7509-commit-authorship.sh
> >>> @@ -12,6 +12,11 @@ author_header () {
> >>>        sed -n -e '/^$/q' -e '/^author /p'
> >>>    }
> >>>
> >>> +committer_header () {
> >>> +     git cat-file commit "$1" |
> >>> +     sed -n -e '/^$/q' -e '/^committer /p'
> >>> +}
> >>> +
> >>>    message_body () {
> >>>        git cat-file commit "$1" |
> >>>        sed -e '1,/^$/d'
> >>> @@ -171,4 +176,79 @@ test_expect_success '--reset-author with CHERRY_PICK_HEAD' '
> >>>        test_cmp expect actual
> >>>    '
> >>>
> >>> +test_expect_success '--committer option overrides committer' '
> >>> +     git checkout Initial &&
> >>> +     echo "Test --committer" >>foo &&
> >>> +     test_tick &&
> >>> +     git commit -a -m "test committer" --committer="Custom Committer <custom@committer.example>" &&
> >>> +     committer_header HEAD >actual &&
> >>> +     grep "Custom Committer <custom@committer.example>" actual
> >>> +'
> >>> +
> >>> +test_expect_success '--committer with pattern search' '
> >>> +     echo "Test committer pattern" >>foo &&
> >>> +     test_tick &&
> >>> +     git commit -a -m "test committer pattern" --committer="Frigate" &&
> >>> +     committer_header HEAD >actual &&
> >>> +     grep "Frigate <flying@over.world>" actual
> >>> +'
> >>> +
> >>> +test_expect_success '--committer malformed parameter' '
> >>> +     echo "Test malformed" >>foo &&
> >>> +     test_tick &&
> >>> +     test_must_fail git commit -a -m "test malformed" --committer="malformed committer"
> >>> +'
> >>> +
> >>> +test_expect_success '--committer with --amend option' '
> >>> +     git checkout -f Initial &&
> >>> +     echo "Test committer with amend" >>foo &&
> >>> +     test_tick &&
> >>> +     git commit -a -m "initial commit for amend test" &&
> >>> +     echo "Modified for amend" >>foo &&
> >>> +     test_tick &&
> >>> +     git commit -a --amend --no-edit \
> >>> +             --author="Test Author <test@author.example>" \
> >>> +             --committer="Test Committer <test@committer.example>" &&
> >>> +     author_header HEAD >actual_author &&
> >>> +     grep "Test Author <test@author.example>" actual_author &&
> >>> +     committer_header HEAD >actual_committer &&
> >>> +     grep "Test Committer <test@committer.example>" actual_committer
> >>> +'
> >>> +
> >>> +test_expect_success 'GIT_COMMITTER_* environment variables' '
> >>> +     git checkout -f Initial &&
> >>> +     echo "Test env vars" >>foo &&
> >>> +     test_tick &&
> >>> +     GIT_COMMITTER_NAME="Env Committer" \
> >>> +     GIT_COMMITTER_EMAIL="env@test.example" \
> >>> +     git commit -a -m "test committer env vars" &&
> >>> +     committer_header HEAD >actual &&
> >>> +     grep "Env Committer <env@test.example>" actual
> >>> +'
> >>> +
> >>> +test_expect_success '--committer overrides GIT_COMMITTER_* environment variables' '
> >>> +     echo "Test override" >>foo &&
> >>> +     test_tick &&
> >>> +     GIT_COMMITTER_NAME="Env Committer" \
> >>> +     GIT_COMMITTER_EMAIL="env@test.example" \
> >>> +     git commit -a -m "test override" \
> >>> +             --committer="Override Committer <override@test.example>" &&
> >>> +     committer_header HEAD >actual &&
> >>> +     grep "Override Committer <override@test.example>" actual
> >>> +'
> >>> +
> >>> +test_expect_success '--date with --committer changes both author and committer dates' '
> >>> +     git checkout -f Initial &&
> >>> +     echo "Test date override" >>foo &&
> >>> +     test_tick &&
> >>> +     git commit -a -m "test date" \
> >>> +             --author="Date Author <date@author.example>" \
> >>> +             --committer="Date Committer <date@committer.example>" \
> >>> +             --date="2024-06-15 10:30:00 +0800" &&
> >>> +     git log -1 --format="%ai" >author_date &&
> >>> +     git log -1 --format="%ci" >committer_date &&
> >>> +     grep "2024-06-15 10:30:00 +0800" author_date &&
> >>> +     grep "2024-06-15 10:30:00 +0800" committer_date
> >>> +'
> >>> +
> >>>    test_done
> >>>
> >>> base-commit: 4badef0c3503dc29059d678abba7fac0f042bc84
> >>
> >
>
Junio C Hamano· Nov 12, 2025, 17:24 UTC · re: ZheNing Hu · lore

Re: [PATCH] commit: add --committer option

ZheNing Hu <adlternative@gmail.com> writes:
Show 14 quoted lines
>> I'm afraid I don't quite follow. If you are amending existing commits to
>> fix them up after you have corrected your configuration then they will
>> have the correct committer automatically when you run "git commit
>> --amend --author=..." to correct the author. If you are committing
>> before you have realized that user.{name,email} are misconfigured then I
>> don't see how "--committer" helps because you have not yet realized
>> anything is wrong.
>>
>
> You're right that after realizing the misconfiguration and correcting the
> repository's user.name and user.email, running `git commit --amend` will
>  fix the committer information, but the author remains unchanged. Users
> then need an additional `git commit --amend --author=...` to fix the author,
> which does work but requires an extra step.

Isn't it more like "You need to run with --author to correct the authorship by amending the commit *anyway*, but while doing so, the committer information will automatically be corrected"? As I said earlier in a separate message, the author and the committer are not symmetric, so having "--author" does make sense in the above picture, while "--committer", as Phillip points out, much less.

ZheNing Hu· Nov 15, 2025, 05:29 UTC · re: Junio C Hamano · lore

Re: [PATCH] commit: add --committer option

Junio C Hamano <gitster@pobox.com> 于2025年11月13日周四 01:24写道:
Show 25 quoted lines
>
> ZheNing Hu <adlternative@gmail.com> writes:
>
> >> I'm afraid I don't quite follow. If you are amending existing commits to
> >> fix them up after you have corrected your configuration then they will
> >> have the correct committer automatically when you run "git commit
> >> --amend --author=..." to correct the author. If you are committing
> >> before you have realized that user.{name,email} are misconfigured then I
> >> don't see how "--committer" helps because you have not yet realized
> >> anything is wrong.
> >>
> >
> > You're right that after realizing the misconfiguration and correcting the
> > repository's user.name and user.email, running `git commit --amend` will
> >  fix the committer information, but the author remains unchanged. Users
> > then need an additional `git commit --amend --author=...` to fix the author,
> > which does work but requires an extra step.
>
> Isn't it more like "You need to run with --author to correct the
> authorship by amending the commit *anyway*, but while doing so, the
> committer information will automatically be corrected"?  As I said
> earlier in a separate message, the author and the committer are not
> symmetric, so having "--author" does make sense in the above picture,
> while "--committer", as Phillip points out, much less.
>

Well, I admit that perhaps the design philosophy of author/committer is inconsistent (which has caused too much trouble), but for users, a consistent parameter interface is easier to understand and use.

Junio C Hamano· Nov 16, 2025, 01:06 UTC · re: ZheNing Hu · lore

Re: [PATCH] commit: add --committer option

ZheNing Hu <adlternative@gmail.com> writes:
Show 11 quoted lines
>> Isn't it more like "You need to run with --author to correct the
>> authorship by amending the commit *anyway*, but while doing so, the
>> committer information will automatically be corrected"?  As I said
>> earlier in a separate message, the author and the committer are not
>> symmetric, so having "--author" does make sense in the above picture,
>> while "--committer", as Phillip points out, much less.
>>
>
> Well, I admit that perhaps the design philosophy of author/committer is
> inconsistent (which has caused too much trouble), but for users, a consistent
> parameter interface is easier to understand and use.
I do not think it is about design philosophy at all, though.

The distinction comes from the difference between what "author" and "committer" fields record. The committer records the identity of the person who was at the keyboard when the commit object was created. The author records the identity of the person who wrote the change that the committer is turning into a commit. There is no symmetry between them, hence there is no inconsistency here.

And it also comes from the actual human user behaviour.

Many authors can pass their patches to a smaller number of committers who make them into part of the official project history, so when a commit is made, there is much stronger need to tweak who the author is for the commit than to tweak who the committer is. On the other hand, it is rare (if ever done) for multiple committers to share a single shell terminal session and take turns to make commit, where you would need to be able to say "this invocations of 'git commit' command is done by person X, who is different from the one who made the previous commit in this same shell session".

ZheNing Hu· Nov 17, 2025, 15:06 UTC · re: Junio C Hamano · lore

Re: [PATCH] commit: add --committer option

Junio C Hamano <gitster@pobox.com> 于2025年11月16日周日 09:06写道:
Show 24 quoted lines
>
> ZheNing Hu <adlternative@gmail.com> writes:
>
> >> Isn't it more like "You need to run with --author to correct the
> >> authorship by amending the commit *anyway*, but while doing so, the
> >> committer information will automatically be corrected"?  As I said
> >> earlier in a separate message, the author and the committer are not
> >> symmetric, so having "--author" does make sense in the above picture,
> >> while "--committer", as Phillip points out, much less.
> >>
> >
> > Well, I admit that perhaps the design philosophy of author/committer is
> > inconsistent (which has caused too much trouble), but for users, a consistent
> > parameter interface is easier to understand and use.
>
> I do not think it is about design philosophy at all, though.
>
> The distinction comes from the difference between what "author" and
> "committer" fields record.  The committer records the identity of
> the person who was at the keyboard when the commit object was
> created.  The author records the identity of the person who wrote
> the change that the committer is turning into a commit.  There is no
> symmetry between them, hence there is no inconsistency here.
>

I understand the difference between committer and author, but some regular Git users don't really pay much attention to the distinction between the two. For these users, being able to quickly and easily correct user information is sufficient — it's just that some previous solutions were a bit complicated.

Show 12 quoted lines
> And it also comes from the actual human user behaviour.
>
> Many authors can pass their patches to a smaller number of
> committers who make them into part of the official project history,
> so when a commit is made, there is much stronger need to tweak who
> the author is for the commit than to tweak who the committer is.  On
> the other hand, it is rare (if ever done) for multiple committers to
> share a single shell terminal session and take turns to make commit,
> where you would need to be able to say "this invocations of 'git
> commit' command is done by person X, who is different from the one
> who made the previous commit in this same shell session".
>

Yes, I believe users in this case are very rare. As I mentioned before, I just want to help those users who want to fix things after misconfiguration, making the fix a bit simpler.

Matej Dujava· Nov 16, 2025, 22:12 UTC · re: ZheNing Hu · lore

Re: [PATCH] commit: add --committer option

On Wed, Nov 12, 2025 at 11:58:02PM +0800, ZheNing Hu wrote:
Show 44 quoted lines
>Phillip Wood <phillip.wood123@gmail.com> 于2025年11月11日周二 22:38写道:
>>
>> On 11/11/2025 13:01, ZheNing Hu wrote:
>> > Phillip Wood <phillip.wood123@gmail.com> 于2025年11月11日周二 00:50写道:
>> >> On 09/11/2025 10:22, ZheNing Hu via GitGitGadget wrote:
>> >>> From: ZheNing Hu <adlternative@gmail.com>
>> >>>
>> >>>
>> >>>       This patch introduces the --committer option to git-commit, providing:
>> >>>
>> >>>        1. Consistency with the existing --author option
>> >>>        2. A more convenient alternative to environment variables
>> >>>        3. Better support for automated workflows and scripts
>> >>>        4. Improved user experience when managing multiple identities
>> >>
>> >> What's the use case for the same person committing under different
>> >> identities? We already have a config mechanism to set different
>> >> identities for different repositories but I'm struggling to see why
>> >> someone would want to create commits under multiple identities in a
>> >> single repository. For scripts it easy enough to set the relevant
>> >> environment variables if a tool wants to create commits under its own
>> >> identity.
>> >>
>> >
>> > I frequently need to distinguish between different user.name and user.email
>> > configurations on our company's internal GitHub.
>> >
>> > The current problems are:
>> >
>> > When I misconfigure (which happens occasionally), git commit --author only fixes
>> > the author part, I still need to additionally set GIT_COMMITTER_NAME and
>> > GIT_COMMITTER_EMAIL environment variables to fix the committer information
>> > These environment variables are painful to use, requiring manual setup
>> > every time
>>
>> I'm afraid I don't quite follow. If you are amending existing commits to
>> fix them up after you have corrected your configuration then they will
>> have the correct committer automatically when you run "git commit
>> --amend --author=..." to correct the author. If you are committing
>> before you have realized that user.{name,email} are misconfigured then I
>> don't see how "--committer" helps because you have not yet realized
>> anything is wrong.
>>
>
Hi
I use includeIf pattern in a config to separate identities
~/.gitconfig:
```
  [includeIf "gitdir:~/.local/src/personal/"]
      path ~/.gitconfig-personal
  [includeIf "gitdir:~/.local/src/companyA/"]
      path ~/.gitconfig-companyA
  [includeIf "gitdir:~/.local/src/companyB/"]
      path ~/.gitconfig-companyB
```
then each
~/.gitconfig-IDENTITY:
```
  [user]
      name = ...
      email = ...
      signingkey = ...
```
Show 5 quoted lines
>You're right that after realizing the misconfiguration and correcting the
>repository's user.name and user.email, running `git commit --amend` will
> fix the committer information, but the author remains unchanged. Users
>then need an additional `git commit --amend --author=...` to fix the author,
>which does work but requires an extra step.

For just one commit, after you fix identity (update .git/config or move project so includeIf uses correct config) then `git commit --amend --reset-author` should get right identity for both commiter and author.

Show 9 quoted lines
>
>I see your point that this becomes more cumbersome when dealing with
>multiple commits. In such cases, users currently need to use something like:
>
>```
>GIT_AUTHOR_NAME="..." GIT_AUTHOR_EMAIL="..." \
>GIT_COMMITTER_NAME="..." GIT_COMMITTER_EMAIL="..." \
>git rebase -f <target>
>```

In my test ^ (using 2.51.2) did not set specified AUTHOR identity, but using:

   git rebase <target> -fx "git commit --amend --no-edit --reset-author"

is close to rewriting commits with new identity, but this will change both dates (committer, author).

If --reset-author is not used but either GIT_AUTHOR_* are exported or --author '...' is used in a -x arg, then author date is kept untouched.

Show 20 quoted lines
>
>This is indeed tedious and error-prone, especially when you want
>to quickly fix and push commits to the platform.
>
>`git commit --amend --author --committer` or a new `git rebase
>--author --committer`
>would provide a more user-friendly workflow for correcting identity
>information after misconfiguration, eliminating the need to manually
>set multiple
>environment variables or run multiple commands.
>
>> Thanks
>>
>> Phillip
>>
>
>Thanks
>
>ZheNing Hu
>

-- Thanks, Matej

Phillip Wood· Nov 17, 2025, 14:27 UTC · re: Matej Dujava · lore

Re: [PATCH] commit: add --committer option

On 16/11/2025 22:12, Matej Dujava wrote:
Show 23 quoted lines
> On Wed, Nov 12, 2025 at 11:58:02PM +0800, ZheNing Hu wrote:
>> Phillip Wood <phillip.wood123@gmail.com> 于2025年11月11日周二 22:38写道:
> 
> I use includeIf pattern in a config to separate identities
> 
> ~/.gitconfig:
> ```
>   [includeIf "gitdir:~/.local/src/personal/"]
>       path ~/.gitconfig-personal
>   [includeIf "gitdir:~/.local/src/companyA/"]
>       path ~/.gitconfig-companyA
>   [includeIf "gitdir:~/.local/src/companyB/"]
>       path ~/.gitconfig-companyB
> ```
> 
> then each
> ~/.gitconfig-IDENTITY:
> ```
>   [user]
>       name = ...
>       email = ...
>       signingkey = ...
> ```

I think that's a common pattern, so long as one can arrange the directory structure so that the repositories for each identity are under a different sub-directory it works well.

Show 10 quoted lines
>> You're right that after realizing the misconfiguration and correcting the
>> repository's user.name and user.email, running `git commit --amend` will
>> fix the committer information, but the author remains unchanged. Users
>> then need an additional `git commit --amend --author=...` to fix the 
>> author,
>> which does work but requires an extra step.
> 
> For just one commit, after you fix identity (update .git/config or move
> project so includeIf uses correct config) then `git commit --amend
> --reset-author` should get right identity for both commiter and author.

As you note below it also resets the author date which might to be desirable.

Show 11 quoted lines
>> I see your point that this becomes more cumbersome when dealing with
>> multiple commits. In such cases, users currently need to use something 
>> like:
>>
>> ```
>> GIT_AUTHOR_NAME="..." GIT_AUTHOR_EMAIL="..." \
>> GIT_COMMITTER_NAME="..." GIT_COMMITTER_EMAIL="..." \
>> git rebase -f <target>
>> ```
> 
> In my test ^ (using 2.51.2) did not set specified AUTHOR identity

Indeed, rebase sets GIT_AUTHOR_{NAME,EMAIL,DATE} when running "git commit" to preserve the authorship of the commit being picked so any value that is set in the environment when running "git rebase" is ignored.

Show 8 quoted lines
> but using:
> 
>    git rebase <target> -fx "git commit --amend --no-edit --reset-author"
> 
> is close to rewriting commits with new identity, but this will change
> both dates (committer, author).
> If --reset-author is not used but either GIT_AUTHOR_* are exported or
> --author '...' is used in a -x arg, then author date is kept untouched.

Yes "git rebase -x 'git commit --author=... --amend --no-edit'" is probably the easiest way to reset the author and committer.

Show 10 quoted lines
>>
>> This is indeed tedious and error-prone, especially when you want
>> to quickly fix and push commits to the platform.
>>
>> `git commit --amend --author --committer` or a new `git rebase
>> --author --committer`
>> would provide a more user-friendly workflow for correcting identity
>> information after misconfiguration, eliminating the need to manually
>> set multiple
>> environment variables or run multiple commands.

I'm still not sure why we need a "--committer" option when the committer identity is taken from the corrected config anyway.

Thanks
Phillip
ZheNing Hu· Nov 17, 2025, 15:18 UTC · re: Phillip Wood · lore

Re: [PATCH] commit: add --committer option

Phillip Wood <phillip.wood123@gmail.com> 于2025年11月17日周一 22:27写道:
Show 84 quoted lines
>
> On 16/11/2025 22:12, Matej Dujava wrote:
> > On Wed, Nov 12, 2025 at 11:58:02PM +0800, ZheNing Hu wrote:
> >> Phillip Wood <phillip.wood123@gmail.com> 于2025年11月11日周二 22:38写道:
> >
> > I use includeIf pattern in a config to separate identities
> >
> > ~/.gitconfig:
> > ```
> >   [includeIf "gitdir:~/.local/src/personal/"]
> >       path ~/.gitconfig-personal
> >   [includeIf "gitdir:~/.local/src/companyA/"]
> >       path ~/.gitconfig-companyA
> >   [includeIf "gitdir:~/.local/src/companyB/"]
> >       path ~/.gitconfig-companyB
> > ```
> >
> > then each
> > ~/.gitconfig-IDENTITY:
> > ```
> >   [user]
> >       name = ...
> >       email = ...
> >       signingkey = ...
> > ```
>
> I think that's a common pattern, so long as one can arrange the
> directory structure so that the repositories for each identity are under
> a different sub-directory it works well.
>
> >> You're right that after realizing the misconfiguration and correcting the
> >> repository's user.name and user.email, running `git commit --amend` will
> >> fix the committer information, but the author remains unchanged. Users
> >> then need an additional `git commit --amend --author=...` to fix the
> >> author,
> >> which does work but requires an extra step.
> >
> > For just one commit, after you fix identity (update .git/config or move
> > project so includeIf uses correct config) then `git commit --amend
> > --reset-author` should get right identity for both commiter and author.
>
> As you note below it also resets the author date which might to be
> desirable.
> >> I see your point that this becomes more cumbersome when dealing with
> >> multiple commits. In such cases, users currently need to use something
> >> like:
> >>
> >> ```
> >> GIT_AUTHOR_NAME="..." GIT_AUTHOR_EMAIL="..." \
> >> GIT_COMMITTER_NAME="..." GIT_COMMITTER_EMAIL="..." \
> >> git rebase -f <target>
> >> ```
> >
> > In my test ^ (using 2.51.2) did not set specified AUTHOR identity
>
> Indeed, rebase sets GIT_AUTHOR_{NAME,EMAIL,DATE} when running "git
> commit" to preserve the authorship of the commit being picked so any
> value that is set in the environment when running "git rebase" is ignored.
>
> > but using:
> >
> >    git rebase <target> -fx "git commit --amend --no-edit --reset-author"
> >
> > is close to rewriting commits with new identity, but this will change
> > both dates (committer, author).
> > If --reset-author is not used but either GIT_AUTHOR_* are exported or
> > --author '...' is used in a -x arg, then author date is kept untouched.
>
> Yes "git rebase -x 'git commit --author=... --amend --no-edit'" is
> probably the easiest way to reset the author and committer.
> >>
> >> This is indeed tedious and error-prone, especially when you want
> >> to quickly fix and push commits to the platform.
> >>
> >> `git commit --amend --author --committer` or a new `git rebase
> >> --author --committer`
> >> would provide a more user-friendly workflow for correcting identity
> >> information after misconfiguration, eliminating the need to manually
> >> set multiple
> >> environment variables or run multiple commands.
>
> I'm still not sure why we need a "--committer" option when the committer
> identity is taken from the corrected config anyway.
>

Well, if the `git rebase --fx` approach is simple enough, I can also give up on introducing `--committer`.

> Thanks
>
> Phillip
>
ZheNing Hu· Nov 17, 2025, 15:15 UTC · re: Matej Dujava · lore

Re: [PATCH] commit: add --committer option

Matej Dujava <mdujava@kocurkovo.cz> 于2025年11月17日周一 06:12写道:
Show 70 quoted lines
>
> On Wed, Nov 12, 2025 at 11:58:02PM +0800, ZheNing Hu wrote:
> >Phillip Wood <phillip.wood123@gmail.com> 于2025年11月11日周二 22:38写道:
> >>
> >> On 11/11/2025 13:01, ZheNing Hu wrote:
> >> > Phillip Wood <phillip.wood123@gmail.com> 于2025年11月11日周二 00:50写道:
> >> >> On 09/11/2025 10:22, ZheNing Hu via GitGitGadget wrote:
> >> >>> From: ZheNing Hu <adlternative@gmail.com>
> >> >>>
> >> >>>
> >> >>>       This patch introduces the --committer option to git-commit, providing:
> >> >>>
> >> >>>        1. Consistency with the existing --author option
> >> >>>        2. A more convenient alternative to environment variables
> >> >>>        3. Better support for automated workflows and scripts
> >> >>>        4. Improved user experience when managing multiple identities
> >> >>
> >> >> What's the use case for the same person committing under different
> >> >> identities? We already have a config mechanism to set different
> >> >> identities for different repositories but I'm struggling to see why
> >> >> someone would want to create commits under multiple identities in a
> >> >> single repository. For scripts it easy enough to set the relevant
> >> >> environment variables if a tool wants to create commits under its own
> >> >> identity.
> >> >>
> >> >
> >> > I frequently need to distinguish between different user.name and user.email
> >> > configurations on our company's internal GitHub.
> >> >
> >> > The current problems are:
> >> >
> >> > When I misconfigure (which happens occasionally), git commit --author only fixes
> >> > the author part, I still need to additionally set GIT_COMMITTER_NAME and
> >> > GIT_COMMITTER_EMAIL environment variables to fix the committer information
> >> > These environment variables are painful to use, requiring manual setup
> >> > every time
> >>
> >> I'm afraid I don't quite follow. If you are amending existing commits to
> >> fix them up after you have corrected your configuration then they will
> >> have the correct committer automatically when you run "git commit
> >> --amend --author=..." to correct the author. If you are committing
> >> before you have realized that user.{name,email} are misconfigured then I
> >> don't see how "--committer" helps because you have not yet realized
> >> anything is wrong.
> >>
> >
>
> Hi
>
> I use includeIf pattern in a config to separate identities
>
> ~/.gitconfig:
> ```
>   [includeIf "gitdir:~/.local/src/personal/"]
>       path ~/.gitconfig-personal
>   [includeIf "gitdir:~/.local/src/companyA/"]
>       path ~/.gitconfig-companyA
>   [includeIf "gitdir:~/.local/src/companyB/"]
>       path ~/.gitconfig-companyB
> ```
>
> then each
> ~/.gitconfig-IDENTITY:
> ```
>   [user]
>       name = ...
>       email = ...
>       signingkey = ...
> ```
>

This does indeed appear to be a very good standard, but unfortunately many users in the past and possibly even in the future may not be aware of such a good practice.

Show 10 quoted lines
> >You're right that after realizing the misconfiguration and correcting the
> >repository's user.name and user.email, running `git commit --amend` will
> > fix the committer information, but the author remains unchanged. Users
> >then need an additional `git commit --amend --author=...` to fix the author,
> >which does work but requires an extra step.
>
> For just one commit, after you fix identity (update .git/config or move
> project so includeIf uses correct config) then `git commit --amend
> --reset-author` should get right identity for both commiter and author.
>
Ok...
Show 21 quoted lines
> >
> >I see your point that this becomes more cumbersome when dealing with
> >multiple commits. In such cases, users currently need to use something like:
> >
> >```
> >GIT_AUTHOR_NAME="..." GIT_AUTHOR_EMAIL="..." \
> >GIT_COMMITTER_NAME="..." GIT_COMMITTER_EMAIL="..." \
> >git rebase -f <target>
> >```
>
> In my test ^ (using 2.51.2) did not set specified AUTHOR identity, but
> using:
>
>    git rebase <target> -fx "git commit --amend --no-edit --reset-author"
>
> is close to rewriting commits with new identity, but this will change
> both dates (committer, author).
>
> If --reset-author is not used but either GIT_AUTHOR_* are exported or
> --author '...' is used in a -x arg, then author date is kept untouched.
>

This does look like a good approach indeed, and it should pretty much meet the user's requirements.

Show 24 quoted lines
> >
> >This is indeed tedious and error-prone, especially when you want
> >to quickly fix and push commits to the platform.
> >
> >`git commit --amend --author --committer` or a new `git rebase
> >--author --committer`
> >would provide a more user-friendly workflow for correcting identity
> >information after misconfiguration, eliminating the need to manually
> >set multiple
> >environment variables or run multiple commands.
> >
> >> Thanks
> >>
> >> Phillip
> >>
> >
> >Thanks
> >
> >ZheNing Hu
> >
>
> --
> Thanks,
> Matej
ZheNing Hu via GitGitGadget· Nov 10, 2025, 16:56 UTC · re: ZheNing Hu via GitGitGadget · lore

[PATCH v2] commit: add --committer option

From: ZheNing Hu <adlternative@gmail.com>

Add --committer option to git-commit, allowing users to override the committer identity similar to how --author works. This provides a more convenient alternative to setting GIT_COMMITTER_* environment variables.

Like --author, the --committer option supports two formats:
- Explicit identity: --committer="Name <email@example.com>"
- Pattern search: --committer="pattern" searches commit history for a
  matching committer and reuses that identity
To share code with the existing --author option, this patch refactors:
1. find_author_by_nickname() into find_identity_by_nickname() which
   handles both author and committer searches through an is_author
   parameter.
2. determine_author_info() into determine_identity() which handles
   identity parsing and setting for both author and committer through
   an is_author parameter.
Signed-off-by: ZheNing Hu <adlternative@gmail.com>
Co-authored-by: Aone-Agent <aone-agent@alibaba-inc.com>
---
    commit: add --committer option
    
    Currently, when users need to override the committer identity in
    git-commit, they have to set GIT_COMMITTER_NAME and GIT_COMMITTER_EMAIL
    environment variables, which can be cumbersome in scripting scenarios or
    when frequently switching committer identities.
    
    While git-commit already provides the --author option to conveniently
    override the author identity, there's no equivalent --committer option
    for the committer identity. This asymmetry creates an inconsistent user
    experience.
    
    This patch introduces the --committer option to git-commit, providing:
    
     1. Consistency with the existing --author option
     2. A more convenient alternative to environment variables
     3. Better support for automated workflows and scripts
     4. Improved user experience when managing multiple identities
    
    The implementation follows the same pattern as the --author option,
    accepting the format "Name " and properly validating the input.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1997%2Fadlternative%2Fzh%2Fimplement-committer-option-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1997/adlternative/zh/implement-committer-option-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/1997
Range-diff vs v1:
 1:  05e97b439f ! 1:  58e9e5c9d7 commit: add --committer option
     @@ Commit message
          committer identity similar to how --author works. This provides a more
          convenient alternative to setting GIT_COMMITTER_* environment variables.
      
     -    Signed-off-by: ZheNing Hu <adlternative@gmail.com>
     +    Like --author, the --committer option supports two formats:
     +    - Explicit identity: --committer="Name <email@example.com>"
     +    - Pattern search: --committer="pattern" searches commit history for a
     +      matching committer and reuses that identity
     +
     +    To share code with the existing --author option, this patch refactors:
     +
     +    1. find_author_by_nickname() into find_identity_by_nickname() which
     +       handles both author and committer searches through an is_author
     +       parameter.
     +
     +    2. determine_author_info() into determine_identity() which handles
     +       identity parsing and setting for both author and committer through
     +       an is_author parameter.
      
     +    Signed-off-by: ZheNing Hu <adlternative@gmail.com>
          Co-authored-by: Aone-Agent <aone-agent@alibaba-inc.com>
      
       ## Documentation/git-commit.adoc ##
     @@ Documentation/git-commit.adoc: git commit [-a | --interactive | --patch] [-s] [-
       	   [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
       	   [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
      -	   [--date=<date>] [--cleanup=<mode>] [--[no-]status]
     -+	   [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]
     ++	   [--committer=<committer>] [--date=<date>] [--cleanup=<mode>] [--[no-]status]
       	   [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
       	   [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
       	   [--] [<pathspec>...]
      @@ Documentation/git-commit.adoc: See linkgit:git-rebase[1] for details.
     - `--date=<date>`::
     - 	Override the author date used in the commit.
     + 	commit by that author (i.e. `git rev-list --all -i --author=<author>`);
     + 	the commit author is then copied from the first such commit found.
       
      +`--committer=<committer>`::
      +	Override the committer for the commit. Specify an explicit committer using the
     -+	standard `A U Thor <committer@example.com>` format. Otherwise _<committer>_
     ++	standard `C O Mitter <committer@example.com>` format. Otherwise _<committer>_
      +	is assumed to be a pattern and is used to search for an existing
     -+	commit by that author (i.e. `git rev-list --all -i --author=<committer>`);
     -+	the commit author is then copied from the first such commit found.
     ++	commit by that committer (i.e. `git rev-list --all -i --committer=<committer>`);
     ++	the commit committer is then copied from the first such commit found.
      +
     - `-m <msg>`::
     - `--message=<msg>`::
     - 	Use _<msg>_ as the commit message.
     + `--date=<date>`::
     + 	Override the author date used in the commit.
     + 
      
       ## builtin/commit.c ##
      @@ builtin/commit.c: static const char * const builtin_commit_usage[] = {
     @@ builtin/commit.c: static const char * const builtin_commit_usage[] = {
       	   "           [-F <file> | -m <msg>] [--reset-author] [--allow-empty]\n"
       	   "           [--allow-empty-message] [--no-verify] [-e] [--author=<author>]\n"
      -	   "           [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
     -+	   "           [--date=<date>] [--committer=<committer>] [--cleanup=<mode>] [--[no-]status]\n"
     ++	   "           [--committer=<committer>] [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
       	   "           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
       	   "           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n"
       	   "           [--] [<pathspec>...]"),
     @@ builtin/commit.c: static enum {
       static char *logfile;
       static char *template_file;
       /*
     -@@ builtin/commit.c: static void determine_author_info(struct strbuf *author_ident)
     - 	free(date);
     +@@ builtin/commit.c: static void set_ident_var(char **buf, char *val)
     + 	*buf = val;
       }
       
     -+static void determine_committer_info(struct strbuf *committer_ident)
     -+{
     -+	char *name, *email, *date;
     -+	struct ident_split committer;
     -+
     -+	name = xstrdup_or_null(getenv("GIT_COMMITTER_NAME"));
     -+	email = xstrdup_or_null(getenv("GIT_COMMITTER_EMAIL"));
     -+	date = xstrdup_or_null(getenv("GIT_COMMITTER_DATE"));
     +-static void determine_author_info(struct strbuf *author_ident)
     ++static void determine_identity(struct strbuf *ident_str, int is_author)
     + {
     + 	char *name, *email, *date;
     +-	struct ident_split author;
     +-
     +-	name = xstrdup_or_null(getenv("GIT_AUTHOR_NAME"));
     +-	email = xstrdup_or_null(getenv("GIT_AUTHOR_EMAIL"));
     +-	date = xstrdup_or_null(getenv("GIT_AUTHOR_DATE"));
     +-
     +-	if (author_message) {
     +-		struct ident_split ident;
     ++	struct ident_split ident;
     ++	const char *env_name = is_author ? "GIT_AUTHOR_NAME" : "GIT_COMMITTER_NAME";
     ++	const char *env_email = is_author ? "GIT_AUTHOR_EMAIL" : "GIT_COMMITTER_EMAIL";
     ++	const char *env_date = is_author ? "GIT_AUTHOR_DATE" : "GIT_COMMITTER_DATE";
     ++	const char *force_ident = is_author ? force_author : force_committer;
     ++	const char *param_name = is_author ? "--author" : "--committer";
     ++	int ident_flag = is_author ? WANT_AUTHOR_IDENT : WANT_COMMITTER_IDENT;
      +
     -+	if (force_committer) {
     -+		struct ident_split ident;
     ++	name = xstrdup_or_null(getenv(env_name));
     ++	email = xstrdup_or_null(getenv(env_email));
     ++	date = xstrdup_or_null(getenv(env_date));
      +
     -+		if (split_ident_line(&ident, force_committer, strlen(force_committer)) < 0)
     -+			die(_("malformed --committer parameter"));
     -+		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
     -+		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
     ++	if (is_author && author_message) {
     ++		struct ident_split msg_ident;
     + 		size_t len;
     + 		const char *a;
     + 
     + 		a = find_commit_header(author_message_buffer, "author", &len);
     + 		if (!a)
     + 			die(_("commit '%s' lacks author header"), author_message);
     +-		if (split_ident_line(&ident, a, len) < 0)
     ++		if (split_ident_line(&msg_ident, a, len) < 0)
     + 			die(_("commit '%s' has malformed author line"), author_message);
     + 
     +-		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
     +-		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
     ++		set_ident_var(&name, xmemdupz(msg_ident.name_begin, msg_ident.name_end - msg_ident.name_begin));
     ++		set_ident_var(&email, xmemdupz(msg_ident.mail_begin, msg_ident.mail_end - msg_ident.mail_begin));
     + 
     +-		if (ident.date_begin) {
     ++		if (msg_ident.date_begin) {
     + 			struct strbuf date_buf = STRBUF_INIT;
     + 			strbuf_addch(&date_buf, '@');
     +-			strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
     ++			strbuf_add(&date_buf, msg_ident.date_begin, msg_ident.date_end - msg_ident.date_begin);
     + 			strbuf_addch(&date_buf, ' ');
     +-			strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
     ++			strbuf_add(&date_buf, msg_ident.tz_begin, msg_ident.tz_end - msg_ident.tz_begin);
     + 			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
     + 		}
     + 	}
     + 
     +-	if (force_author) {
     +-		struct ident_split ident;
     ++	if (force_ident) {
     ++		struct ident_split force_ident_split;
      +
     -+		if (ident.date_begin) {
     ++		if (split_ident_line(&force_ident_split, force_ident, strlen(force_ident)) < 0)
     ++			die(_("malformed %s parameter"), param_name);
     ++		set_ident_var(&name, xmemdupz(force_ident_split.name_begin, force_ident_split.name_end - force_ident_split.name_begin));
     ++		set_ident_var(&email, xmemdupz(force_ident_split.mail_begin, force_ident_split.mail_end - force_ident_split.mail_begin));
     + 
     +-		if (split_ident_line(&ident, force_author, strlen(force_author)) < 0)
     +-			die(_("malformed --author parameter"));
     +-		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
     +-		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
     ++		if (!is_author && force_ident_split.date_begin) {
      +			struct strbuf date_buf = STRBUF_INIT;
      +			strbuf_addch(&date_buf, '@');
     -+			strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
     ++			strbuf_add(&date_buf, force_ident_split.date_begin, force_ident_split.date_end - force_ident_split.date_begin);
      +			strbuf_addch(&date_buf, ' ');
     -+			strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
     ++			strbuf_add(&date_buf, force_ident_split.tz_begin, force_ident_split.tz_end - force_ident_split.tz_begin);
      +			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
      +		}
     -+	}
     + 	}
     + 
     + 	if (force_date) {
     +@@ builtin/commit.c: static void determine_author_info(struct strbuf *author_ident)
     + 		set_ident_var(&date, strbuf_detach(&date_buf, NULL));
     + 	}
     + 
     +-	strbuf_addstr(author_ident, fmt_ident(name, email, WANT_AUTHOR_IDENT, date,
     ++	strbuf_addstr(ident_str, fmt_ident(name, email, ident_flag, date,
     + 				IDENT_STRICT));
     +-	assert_split_ident(&author, author_ident);
     +-	export_one("GIT_AUTHOR_NAME", author.name_begin, author.name_end, 0);
     +-	export_one("GIT_AUTHOR_EMAIL", author.mail_begin, author.mail_end, 0);
     +-	export_one("GIT_AUTHOR_DATE", author.date_begin, author.tz_end, '@');
     ++	assert_split_ident(&ident, ident_str);
      +
     -+	if (force_date) {
     -+		struct strbuf date_buf = STRBUF_INIT;
     -+		if (parse_force_date(force_date, &date_buf))
     -+			die(_("invalid date format: %s"), force_date);
     -+		set_ident_var(&date, strbuf_detach(&date_buf, NULL));
     ++	if (is_author) {
     ++		export_one("GIT_AUTHOR_NAME", ident.name_begin, ident.name_end, 0);
     ++		export_one("GIT_AUTHOR_EMAIL", ident.mail_begin, ident.mail_end, 0);
     ++		export_one("GIT_AUTHOR_DATE", ident.date_begin, ident.tz_end, '@');
     ++	} else {
     ++		export_one("GIT_COMMITTER_NAME", ident.name_begin, ident.name_end, 0);
     ++		export_one("GIT_COMMITTER_EMAIL", ident.mail_begin, ident.mail_end, 0);
     ++		export_one("GIT_COMMITTER_DATE", ident.date_begin, ident.tz_end, '@');
      +	}
      +
     -+	strbuf_addstr(committer_ident, fmt_ident(name, email, WANT_COMMITTER_IDENT, date,
     -+				IDENT_STRICT));
     -+	assert_split_ident(&committer, committer_ident);
     -+	free(name);
     -+	free(email);
     -+	free(date);
     + 	free(name);
     + 	free(email);
     + 	free(date);
     + }
     + 
     ++static void determine_author_info(struct strbuf *author_ident)
     ++{
     ++	determine_identity(author_ident, 1);
     ++}
     ++
     ++static void determine_committer_info(struct strbuf *committer_ident)
     ++{
     ++	determine_identity(committer_ident, 0);
      +}
      +
       static int author_date_is_interesting(void)
       {
       	return author_message || force_date;
     +@@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const char *prefix,
     + 	return 1;
     + }
     + 
     +-static const char *find_author_by_nickname(const char *name)
     ++static const char *find_identity_by_nickname(const char *name, int is_author)
     + {
     + 	struct rev_info revs;
     + 	struct commit *commit;
     + 	struct strbuf buf = STRBUF_INIT;
     + 	const char *av[20];
     + 	int ac = 0;
     ++	const char *field = is_author ? "author" : "committer";
     ++	const char *format = is_author ? "%aN <%aE>" : "%cN <%cE>";
     + 
     + 	repo_init_revisions(the_repository, &revs, NULL);
     +-	strbuf_addf(&buf, "--author=%s", name);
     ++	strbuf_addf(&buf, "--%s=%s", field, name);
     + 	av[++ac] = "--all";
     + 	av[++ac] = "-i";
     + 	av[++ac] = buf.buf;
     +@@ builtin/commit.c: static const char *find_author_by_nickname(const char *name)
     + 		ctx.date_mode.type = DATE_NORMAL;
     + 		strbuf_release(&buf);
     + 		repo_format_commit_message(the_repository, commit,
     +-					   "%aN <%aE>", &buf, &ctx);
     ++					   format, &buf, &ctx);
     + 		release_revisions(&revs);
     + 		return strbuf_detach(&buf, NULL);
     + 	}
     +-	die(_("--author '%s' is not 'Name <email>' and matches no existing author"), name);
     ++	die(_("--%s '%s' is not 'Name <email>' and matches no existing %s"),
     ++	    field, name, field);
     ++}
     ++
     ++static const char *find_author_by_nickname(const char *name)
     ++{
     ++	return find_identity_by_nickname(name, 1);
     ++}
     ++
     ++static const char *find_committer_by_nickname(const char *name)
     ++{
     ++	return find_identity_by_nickname(name, 0);
     + }
     + 
     + static void handle_ignored_arg(struct wt_status *s)
      @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *argv[],
       	if (force_author && renew_authorship)
       		die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author");
       
      +	if (force_committer && !strchr(force_committer, '>'))
     -+		force_committer = find_author_by_nickname(force_committer);
     ++		force_committer = find_committer_by_nickname(force_committer);
      +
       	if (logfile || have_option_m || use_message)
       		use_editor = 0;
     @@ builtin/commit.c: int cmd_commit(int argc,
       		append_merge_tag_headers(parents, &tail);
       	}
       
     -+	if (force_committer) {
     ++	if (force_committer)
      +		determine_committer_info(&committer_ident);
     -+	}
      +
       	if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,
      -				 parents, &oid, author_ident.buf, NULL,
     @@ t/t7509-commit-authorship.sh: author_header () {
       message_body () {
       	git cat-file commit "$1" |
       	sed -e '1,/^$/d'
     + }
     + 
     + test_expect_success '-C option copies authorship and message' '
     +-	test_commit --author Frigate\ \<flying@over.world\> \
     ++	test_env GIT_COMMITTER_NAME="Frigate" \
     ++		GIT_COMMITTER_EMAIL="flying@over.world" \
     ++		test_commit --author Frigate\ \<flying@over.world\> \
     + 		"Initial Commit" foo Initial Initial &&
     + 	echo "Test 1" >>foo &&
     + 	test_tick &&
      @@ t/t7509-commit-authorship.sh: test_expect_success '--reset-author with CHERRY_PICK_HEAD' '
       	test_cmp expect actual
       '
     @@ t/t7509-commit-authorship.sh: test_expect_success '--reset-author with CHERRY_PI
      +	git checkout -f Initial &&
      +	echo "Test env vars" >>foo &&
      +	test_tick &&
     -+	GIT_COMMITTER_NAME="Env Committer" \
     -+	GIT_COMMITTER_EMAIL="env@test.example" \
     -+	git commit -a -m "test committer env vars" &&
     ++	test_env GIT_COMMITTER_NAME="Env Committer" \
     ++		GIT_COMMITTER_EMAIL="env@test.example" \
     ++		git commit -a -m "test committer env vars" &&
      +	committer_header HEAD >actual &&
      +	grep "Env Committer <env@test.example>" actual
      +'
     @@ t/t7509-commit-authorship.sh: test_expect_success '--reset-author with CHERRY_PI
      +test_expect_success '--committer overrides GIT_COMMITTER_* environment variables' '
      +	echo "Test override" >>foo &&
      +	test_tick &&
     -+	GIT_COMMITTER_NAME="Env Committer" \
     -+	GIT_COMMITTER_EMAIL="env@test.example" \
     -+	git commit -a -m "test override" \
     ++	test_env GIT_COMMITTER_NAME="Env Committer" \
     ++		GIT_COMMITTER_EMAIL="env@test.example" \
     ++		git commit -a -m "test override" \
      +		--committer="Override Committer <override@test.example>" &&
      +	committer_header HEAD >actual &&
      +	grep "Override Committer <override@test.example>" actual
 Documentation/git-commit.adoc |   9 ++-
 builtin/commit.c              | 121 +++++++++++++++++++++++++---------
 t/t7509-commit-authorship.sh  |  84 ++++++++++++++++++++++-
 3 files changed, 180 insertions(+), 34 deletions(-)
Show changes to 3 files +180 −34

Documentation/git-commit.adoc, builtin/commit.c, t/t7509-commit-authorship.sh

diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
index 54c207ad45..ed4c54ae81 100644
--- a/Documentation/git-commit.adoc
+++ b/Documentation/git-commit.adoc
@@ -12,7 +12,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
 	   [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]
 	   [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
 	   [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
-	   [--date=<date>] [--cleanup=<mode>] [--[no-]status]
+	   [--committer=<committer>] [--date=<date>] [--cleanup=<mode>] [--[no-]status]
 	   [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
 	   [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
 	   [--] [<pathspec>...]
@@ -178,6 +178,13 @@ See linkgit:git-rebase[1] for details.
 	commit by that author (i.e. `git rev-list --all -i --author=<author>`);
 	the commit author is then copied from the first such commit found.
 
+`--committer=<committer>`::
+	Override the committer for the commit. Specify an explicit committer using the
+	standard `C O Mitter <committer@example.com>` format. Otherwise _<committer>_
+	is assumed to be a pattern and is used to search for an existing
+	commit by that committer (i.e. `git rev-list --all -i --committer=<committer>`);
+	the commit committer is then copied from the first such commit found.
+
 `--date=<date>`::
 	Override the author date used in the commit.
 
diff --git a/builtin/commit.c b/builtin/commit.c
index 0243f17d53..3b249dd878 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -49,7 +49,7 @@ static const char * const builtin_commit_usage[] = {
 	   "           [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]\n"
 	   "           [-F <file> | -m <msg>] [--reset-author] [--allow-empty]\n"
 	   "           [--allow-empty-message] [--no-verify] [-e] [--author=<author>]\n"
-	   "           [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
+	   "           [--committer=<committer>] [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
 	   "           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
 	   "           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n"
 	   "           [--] [<pathspec>...]"),
@@ -112,6 +112,7 @@ static enum {
 } commit_style;
 
 static const char *force_author;
+static const char *force_committer;
 static char *logfile;
 static char *template_file;
 /*
@@ -630,46 +631,61 @@ static void set_ident_var(char **buf, char *val)
 	*buf = val;
 }
 
-static void determine_author_info(struct strbuf *author_ident)
+static void determine_identity(struct strbuf *ident_str, int is_author)
 {
 	char *name, *email, *date;
-	struct ident_split author;
-
-	name = xstrdup_or_null(getenv("GIT_AUTHOR_NAME"));
-	email = xstrdup_or_null(getenv("GIT_AUTHOR_EMAIL"));
-	date = xstrdup_or_null(getenv("GIT_AUTHOR_DATE"));
-
-	if (author_message) {
-		struct ident_split ident;
+	struct ident_split ident;
+	const char *env_name = is_author ? "GIT_AUTHOR_NAME" : "GIT_COMMITTER_NAME";
+	const char *env_email = is_author ? "GIT_AUTHOR_EMAIL" : "GIT_COMMITTER_EMAIL";
+	const char *env_date = is_author ? "GIT_AUTHOR_DATE" : "GIT_COMMITTER_DATE";
+	const char *force_ident = is_author ? force_author : force_committer;
+	const char *param_name = is_author ? "--author" : "--committer";
+	int ident_flag = is_author ? WANT_AUTHOR_IDENT : WANT_COMMITTER_IDENT;
+
+	name = xstrdup_or_null(getenv(env_name));
+	email = xstrdup_or_null(getenv(env_email));
+	date = xstrdup_or_null(getenv(env_date));
+
+	if (is_author && author_message) {
+		struct ident_split msg_ident;
 		size_t len;
 		const char *a;
 
 		a = find_commit_header(author_message_buffer, "author", &len);
 		if (!a)
 			die(_("commit '%s' lacks author header"), author_message);
-		if (split_ident_line(&ident, a, len) < 0)
+		if (split_ident_line(&msg_ident, a, len) < 0)
 			die(_("commit '%s' has malformed author line"), author_message);
 
-		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
-		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
+		set_ident_var(&name, xmemdupz(msg_ident.name_begin, msg_ident.name_end - msg_ident.name_begin));
+		set_ident_var(&email, xmemdupz(msg_ident.mail_begin, msg_ident.mail_end - msg_ident.mail_begin));
 
-		if (ident.date_begin) {
+		if (msg_ident.date_begin) {
 			struct strbuf date_buf = STRBUF_INIT;
 			strbuf_addch(&date_buf, '@');
-			strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
+			strbuf_add(&date_buf, msg_ident.date_begin, msg_ident.date_end - msg_ident.date_begin);
 			strbuf_addch(&date_buf, ' ');
-			strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
+			strbuf_add(&date_buf, msg_ident.tz_begin, msg_ident.tz_end - msg_ident.tz_begin);
 			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
 		}
 	}
 
-	if (force_author) {
-		struct ident_split ident;
+	if (force_ident) {
+		struct ident_split force_ident_split;
+
+		if (split_ident_line(&force_ident_split, force_ident, strlen(force_ident)) < 0)
+			die(_("malformed %s parameter"), param_name);
+		set_ident_var(&name, xmemdupz(force_ident_split.name_begin, force_ident_split.name_end - force_ident_split.name_begin));
+		set_ident_var(&email, xmemdupz(force_ident_split.mail_begin, force_ident_split.mail_end - force_ident_split.mail_begin));
 
-		if (split_ident_line(&ident, force_author, strlen(force_author)) < 0)
-			die(_("malformed --author parameter"));
-		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
-		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
+		if (!is_author && force_ident_split.date_begin) {
+			struct strbuf date_buf = STRBUF_INIT;
+			strbuf_addch(&date_buf, '@');
+			strbuf_add(&date_buf, force_ident_split.date_begin, force_ident_split.date_end - force_ident_split.date_begin);
+			strbuf_addch(&date_buf, ' ');
+			strbuf_add(&date_buf, force_ident_split.tz_begin, force_ident_split.tz_end - force_ident_split.tz_begin);
+			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
+		}
 	}
 
 	if (force_date) {
@@ -679,17 +695,35 @@ static void determine_author_info(struct strbuf *author_ident)
 		set_ident_var(&date, strbuf_detach(&date_buf, NULL));
 	}
 
-	strbuf_addstr(author_ident, fmt_ident(name, email, WANT_AUTHOR_IDENT, date,
+	strbuf_addstr(ident_str, fmt_ident(name, email, ident_flag, date,
 				IDENT_STRICT));
-	assert_split_ident(&author, author_ident);
-	export_one("GIT_AUTHOR_NAME", author.name_begin, author.name_end, 0);
-	export_one("GIT_AUTHOR_EMAIL", author.mail_begin, author.mail_end, 0);
-	export_one("GIT_AUTHOR_DATE", author.date_begin, author.tz_end, '@');
+	assert_split_ident(&ident, ident_str);
+
+	if (is_author) {
+		export_one("GIT_AUTHOR_NAME", ident.name_begin, ident.name_end, 0);
+		export_one("GIT_AUTHOR_EMAIL", ident.mail_begin, ident.mail_end, 0);
+		export_one("GIT_AUTHOR_DATE", ident.date_begin, ident.tz_end, '@');
+	} else {
+		export_one("GIT_COMMITTER_NAME", ident.name_begin, ident.name_end, 0);
+		export_one("GIT_COMMITTER_EMAIL", ident.mail_begin, ident.mail_end, 0);
+		export_one("GIT_COMMITTER_DATE", ident.date_begin, ident.tz_end, '@');
+	}
+
 	free(name);
 	free(email);
 	free(date);
 }
 
+static void determine_author_info(struct strbuf *author_ident)
+{
+	determine_identity(author_ident, 1);
+}
+
+static void determine_committer_info(struct strbuf *committer_ident)
+{
+	determine_identity(committer_ident, 0);
+}
+
 static int author_date_is_interesting(void)
 {
 	return author_message || force_date;
@@ -1137,16 +1171,18 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 	return 1;
 }
 
-static const char *find_author_by_nickname(const char *name)
+static const char *find_identity_by_nickname(const char *name, int is_author)
 {
 	struct rev_info revs;
 	struct commit *commit;
 	struct strbuf buf = STRBUF_INIT;
 	const char *av[20];
 	int ac = 0;
+	const char *field = is_author ? "author" : "committer";
+	const char *format = is_author ? "%aN <%aE>" : "%cN <%cE>";
 
 	repo_init_revisions(the_repository, &revs, NULL);
-	strbuf_addf(&buf, "--author=%s", name);
+	strbuf_addf(&buf, "--%s=%s", field, name);
 	av[++ac] = "--all";
 	av[++ac] = "-i";
 	av[++ac] = buf.buf;
@@ -1164,11 +1200,22 @@ static const char *find_author_by_nickname(const char *name)
 		ctx.date_mode.type = DATE_NORMAL;
 		strbuf_release(&buf);
 		repo_format_commit_message(the_repository, commit,
-					   "%aN <%aE>", &buf, &ctx);
+					   format, &buf, &ctx);
 		release_revisions(&revs);
 		return strbuf_detach(&buf, NULL);
 	}
-	die(_("--author '%s' is not 'Name <email>' and matches no existing author"), name);
+	die(_("--%s '%s' is not 'Name <email>' and matches no existing %s"),
+	    field, name, field);
+}
+
+static const char *find_author_by_nickname(const char *name)
+{
+	return find_identity_by_nickname(name, 1);
+}
+
+static const char *find_committer_by_nickname(const char *name)
+{
+	return find_identity_by_nickname(name, 0);
 }
 
 static void handle_ignored_arg(struct wt_status *s)
@@ -1321,6 +1368,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
 	if (force_author && renew_authorship)
 		die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author");
 
+	if (force_committer && !strchr(force_committer, '>'))
+		force_committer = find_committer_by_nickname(force_committer);
+
 	if (logfile || have_option_m || use_message)
 		use_editor = 0;
 
@@ -1709,6 +1759,7 @@ int cmd_commit(int argc,
 		OPT_FILENAME('F', "file", &logfile, N_("read message from file")),
 		OPT_STRING(0, "author", &force_author, N_("author"), N_("override author for commit")),
 		OPT_STRING(0, "date", &force_date, N_("date"), N_("override date for commit")),
+		OPT_STRING(0, "committer", &force_committer, N_("committer"), N_("override committer for commit")),
 		OPT_CALLBACK('m', "message", &message, N_("message"), N_("commit message"), opt_parse_m),
 		OPT_STRING('c', "reedit-message", &edit_message, N_("commit"), N_("reuse and edit message from specified commit")),
 		OPT_STRING('C', "reuse-message", &use_message, N_("commit"), N_("reuse message from specified commit")),
@@ -1785,6 +1836,7 @@ int cmd_commit(int argc,
 
 	struct strbuf sb = STRBUF_INIT;
 	struct strbuf author_ident = STRBUF_INIT;
+	struct strbuf committer_ident = STRBUF_INIT;
 	const char *index_file, *reflog_msg;
 	struct object_id oid;
 	struct commit_list *parents = NULL;
@@ -1930,8 +1982,12 @@ int cmd_commit(int argc,
 		append_merge_tag_headers(parents, &tail);
 	}
 
+	if (force_committer)
+		determine_committer_info(&committer_ident);
+
 	if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,
-				 parents, &oid, author_ident.buf, NULL,
+				 parents, &oid, author_ident.buf,
+				 force_committer ? committer_ident.buf : NULL,
 				 sign_commit, extra)) {
 		rollback_index_files();
 		die(_("failed to write commit object"));
@@ -1980,6 +2036,7 @@ cleanup:
 	free_commit_extra_headers(extra);
 	free_commit_list(parents);
 	strbuf_release(&author_ident);
+	strbuf_release(&committer_ident);
 	strbuf_release(&err);
 	strbuf_release(&sb);
 	free(logfile);
diff --git a/t/t7509-commit-authorship.sh b/t/t7509-commit-authorship.sh
index 8e373b566b..7e163e02d1 100755
--- a/t/t7509-commit-authorship.sh
+++ b/t/t7509-commit-authorship.sh
@@ -12,13 +12,20 @@ author_header () {
 	sed -n -e '/^$/q' -e '/^author /p'
 }
 
+committer_header () {
+	git cat-file commit "$1" |
+	sed -n -e '/^$/q' -e '/^committer /p'
+}
+
 message_body () {
 	git cat-file commit "$1" |
 	sed -e '1,/^$/d'
 }
 
 test_expect_success '-C option copies authorship and message' '
-	test_commit --author Frigate\ \<flying@over.world\> \
+	test_env GIT_COMMITTER_NAME="Frigate" \
+		GIT_COMMITTER_EMAIL="flying@over.world" \
+		test_commit --author Frigate\ \<flying@over.world\> \
 		"Initial Commit" foo Initial Initial &&
 	echo "Test 1" >>foo &&
 	test_tick &&
@@ -171,4 +178,79 @@ test_expect_success '--reset-author with CHERRY_PICK_HEAD' '
 	test_cmp expect actual
 '
 
+test_expect_success '--committer option overrides committer' '
+	git checkout Initial &&
+	echo "Test --committer" >>foo &&
+	test_tick &&
+	git commit -a -m "test committer" --committer="Custom Committer <custom@committer.example>" &&
+	committer_header HEAD >actual &&
+	grep "Custom Committer <custom@committer.example>" actual
+'
+
+test_expect_success '--committer with pattern search' '
+	echo "Test committer pattern" >>foo &&
+	test_tick &&
+	git commit -a -m "test committer pattern" --committer="Frigate" &&
+	committer_header HEAD >actual &&
+	grep "Frigate <flying@over.world>" actual
+'
+
+test_expect_success '--committer malformed parameter' '
+	echo "Test malformed" >>foo &&
+	test_tick &&
+	test_must_fail git commit -a -m "test malformed" --committer="malformed committer"
+'
+
+test_expect_success '--committer with --amend option' '
+	git checkout -f Initial &&
+	echo "Test committer with amend" >>foo &&
+	test_tick &&
+	git commit -a -m "initial commit for amend test" &&
+	echo "Modified for amend" >>foo &&
+	test_tick &&
+	git commit -a --amend --no-edit \
+		--author="Test Author <test@author.example>" \
+		--committer="Test Committer <test@committer.example>" &&
+	author_header HEAD >actual_author &&
+	grep "Test Author <test@author.example>" actual_author &&
+	committer_header HEAD >actual_committer &&
+	grep "Test Committer <test@committer.example>" actual_committer
+'
+
+test_expect_success 'GIT_COMMITTER_* environment variables' '
+	git checkout -f Initial &&
+	echo "Test env vars" >>foo &&
+	test_tick &&
+	test_env GIT_COMMITTER_NAME="Env Committer" \
+		GIT_COMMITTER_EMAIL="env@test.example" \
+		git commit -a -m "test committer env vars" &&
+	committer_header HEAD >actual &&
+	grep "Env Committer <env@test.example>" actual
+'
+
+test_expect_success '--committer overrides GIT_COMMITTER_* environment variables' '
+	echo "Test override" >>foo &&
+	test_tick &&
+	test_env GIT_COMMITTER_NAME="Env Committer" \
+		GIT_COMMITTER_EMAIL="env@test.example" \
+		git commit -a -m "test override" \
+		--committer="Override Committer <override@test.example>" &&
+	committer_header HEAD >actual &&
+	grep "Override Committer <override@test.example>" actual
+'
+
+test_expect_success '--date with --committer changes both author and committer dates' '
+	git checkout -f Initial &&
+	echo "Test date override" >>foo &&
+	test_tick &&
+	git commit -a -m "test date" \
+		--author="Date Author <date@author.example>" \
+		--committer="Date Committer <date@committer.example>" \
+		--date="2024-06-15 10:30:00 +0800" &&
+	git log -1 --format="%ai" >author_date &&
+	git log -1 --format="%ci" >committer_date &&
+	grep "2024-06-15 10:30:00 +0800" author_date &&
+	grep "2024-06-15 10:30:00 +0800" committer_date
+'
+
 test_done

base-commit: 4badef0c3503dc29059d678abba7fac0f042bc84
-- 
gitgitgadget
Junio C Hamano· Nov 10, 2025, 19:22 UTC · re: ZheNing Hu via GitGitGadget · lore

Re: [PATCH v2] commit: add --committer option

"ZheNing Hu via GitGitGadget" <gitgitgadget@gmail.com> writes:
> Signed-off-by: ZheNing Hu <adlternative@gmail.com>
> Co-authored-by: Aone-Agent <aone-agent@alibaba-inc.com>

What is this second author and how would its presence in the author list interact with your DCO obligation?

How did you make sure that whatever is in this patch were not copied by the "agent" from somewhere that we cannot copy the code from before deciding to send this patch? The "cannot copy from" may come in different shapes, from "their code is proprietary" to "their licensing terms are not compatible with GPLv2" to "they welcome us borrowing but we must give credit to them", any of which we should be careful to avoid.

Junio C Hamano· Nov 10, 2025, 19:29 UTC · re: Junio C Hamano · lore

Re: [PATCH v2] commit: add --committer option

Junio C Hamano <gitster@pobox.com> writes:
Show 15 quoted lines
> "ZheNing Hu via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
>> Signed-off-by: ZheNing Hu <adlternative@gmail.com>
>> Co-authored-by: Aone-Agent <aone-agent@alibaba-inc.com>
>
> What is this second author and how would its presence in the author
> list interact with your DCO obligation?
>
> How did you make sure that whatever is in this patch were not copied
> by the "agent" from somewhere that we cannot copy the code from
> before deciding to send this patch?  The "cannot copy from" may come
> in different shapes, from "their code is proprietary" to "their
> licensing terms are not compatible with GPLv2" to "they welcome us
> borrowing but we must give credit to them", any of which we should
> be careful to avoid.

Well, the last one is not something we should *avoid*. If their licensing terms are compatible with ours but they want to be credited, then we comply that request and credit them.

But I think readers got the idea.
Thanks.
ZheNing Hu· Nov 11, 2025, 13:36 UTC · re: Junio C Hamano · lore

Re: [PATCH v2] commit: add --committer option

Junio C Hamano <gitster@pobox.com> 于2025年11月11日周二 03:22写道:
Show 18 quoted lines
>
> "ZheNing Hu via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
> > Signed-off-by: ZheNing Hu <adlternative@gmail.com>
> > Co-authored-by: Aone-Agent <aone-agent@alibaba-inc.com>
>
> What is this second author and how would its presence in the author
> list interact with your DCO obligation?
>
> How did you make sure that whatever is in this patch were not copied
> by the "agent" from somewhere that we cannot copy the code from
> before deciding to send this patch?  The "cannot copy from" may come
> in different shapes, from "their code is proprietary" to "their
> licensing terms are not compatible with GPLv2" to "they welcome us
> borrowing but we must give credit to them", any of which we should
> be careful to avoid.
>
>

This was automatically added by some code assistance tools. I indeed forgot to consider its impact on the open source license. I'll remove it right away.

Junio C Hamano· Nov 11, 2025, 15:40 UTC · re: ZheNing Hu · lore

Re: [PATCH v2] commit: add --committer option

ZheNing Hu <adlternative@gmail.com> writes:
Show 23 quoted lines
> Junio C Hamano <gitster@pobox.com> 于2025年11月11日周二 03:22写道:
>>
>> "ZheNing Hu via GitGitGadget" <gitgitgadget@gmail.com> writes:
>>
>> > Signed-off-by: ZheNing Hu <adlternative@gmail.com>
>> > Co-authored-by: Aone-Agent <aone-agent@alibaba-inc.com>
>>
>> What is this second author and how would its presence in the author
>> list interact with your DCO obligation?
>>
>> How did you make sure that whatever is in this patch were not copied
>> by the "agent" from somewhere that we cannot copy the code from
>> before deciding to send this patch?  The "cannot copy from" may come
>> in different shapes, from "their code is proprietary" to "their
>> licensing terms are not compatible with GPLv2" to "they welcome us
>> borrowing but we must give credit to them", any of which we should
>> be careful to avoid.
>>
>>
>
> This was automatically added by some code assistance tools.
> I indeed forgot to consider its impact on the open source license.
> I'll remove it right away.

Please don't silently remove it without answering the question you were asked. "The tool adds it but I disabled the agentic features of the tool and everything readers see in the submitted patch was what I typed, with no agent input" would be a perfect answer. "I did not consider the ramifications of use of the agentic tool, and I do not know where the code the tool added for me came from, so I cannot be sure I can contribute this patch to the project" would be a sad but may be an honest answer. "I cannot tell the origin but I can remove the line and claim I wrote everything myself" is not an answer that we want to see. I am sure that your answer would fall within the spectrum, and am hoping it would be the earlier, perfect one, or one close to it, but we need to hear it.

Thanks.
ZheNing Hu· Nov 12, 2025, 16:23 UTC · re: Junio C Hamano · lore

Re: [PATCH v2] commit: add --committer option

Junio C Hamano <gitster@pobox.com> 于2025年11月11日周二 23:40写道:
Show 40 quoted lines
>
> ZheNing Hu <adlternative@gmail.com> writes:
>
> > Junio C Hamano <gitster@pobox.com> 于2025年11月11日周二 03:22写道:
> >>
> >> "ZheNing Hu via GitGitGadget" <gitgitgadget@gmail.com> writes:
> >>
> >> > Signed-off-by: ZheNing Hu <adlternative@gmail.com>
> >> > Co-authored-by: Aone-Agent <aone-agent@alibaba-inc.com>
> >>
> >> What is this second author and how would its presence in the author
> >> list interact with your DCO obligation?
> >>
> >> How did you make sure that whatever is in this patch were not copied
> >> by the "agent" from somewhere that we cannot copy the code from
> >> before deciding to send this patch?  The "cannot copy from" may come
> >> in different shapes, from "their code is proprietary" to "their
> >> licensing terms are not compatible with GPLv2" to "they welcome us
> >> borrowing but we must give credit to them", any of which we should
> >> be careful to avoid.
> >>
> >>
> >
> > This was automatically added by some code assistance tools.
> > I indeed forgot to consider its impact on the open source license.
> > I'll remove it right away.
>
> Please don't silently remove it without answering the question you
> were asked.  "The tool adds it but I disabled the agentic features
> of the tool and everything readers see in the submitted patch was
> what I typed, with no agent input" would be a perfect answer.  "I
> did not consider the ramifications of use of the agentic tool, and I
> do not know where the code the tool added for me came from, so I
> cannot be sure I can contribute this patch to the project" would be
> a sad but may be an honest answer.  "I cannot tell the origin but I
> can remove the line and claim I wrote everything myself" is not an
> answer that we want to see.  I am sure that your answer would fall
> within the spectrum, and am hoping it would be the earlier, perfect
> one, or one close to it, but we need to hear it.
>

Ha, I think my situation is closer to the second case, but I'm certain that identical code cannot be found on GitHub or any other platform. Moreover, this code is entirely modeled after Git's own code (e.g., determine_identity is based on determine_author_info). There's no possibility of copying code from other projects with licenses incompatible with Git. However, your reminder is valid, especially in this era where AI Agents are heavily infiltrating development. Any similar open-source contributions in the future will need to pay attention to this point.

> Thanks.
ZheNing Hu via GitGitGadget· Nov 12, 2025, 16:55 UTC · re: ZheNing Hu via GitGitGadget · lore

[PATCH v3] commit: add --committer option

From: ZheNing Hu <adlternative@gmail.com>

Add --committer option to git-commit, allowing users to override the committer identity similar to how --author works. This provides a more convenient alternative to setting GIT_COMMITTER_* environment variables.

Like --author, the --committer option supports two formats:
- Explicit identity: --committer="Name <email@example.com>"
- Pattern search: --committer="pattern" searches commit history for a
  matching committer and reuses that identity
To share code with the existing --author option, this patch refactors:
1. find_author_by_nickname() into find_identity_by_nickname() which
   handles both author and committer searches through an is_author
   parameter.
2. determine_author_info() into determine_identity() which handles
   identity parsing and setting for both author and committer through
   an is_author parameter.
Signed-off-by: ZheNing Hu <adlternative@gmail.com>
---
    commit: add --committer option
    
    Currently, when users need to override the committer identity in
    git-commit, they have to set GIT_COMMITTER_NAME and GIT_COMMITTER_EMAIL
    environment variables, which can be cumbersome in scripting scenarios or
    when frequently switching committer identities.
    
    While git-commit already provides the --author option to conveniently
    override the author identity, there's no equivalent --committer option
    for the committer identity. This asymmetry creates an inconsistent user
    experience.
    
    This patch introduces the --committer option to git-commit, providing:
    
     1. Consistency with the existing --author option
     2. A more convenient alternative to environment variables
     3. Better support for automated workflows and scripts
     4. Improved user experience when managing multiple identities
    
    The implementation follows the same pattern as the --author option,
    accepting the format "Name " and properly validating the input.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1997%2Fadlternative%2Fzh%2Fimplement-committer-option-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1997/adlternative/zh/implement-committer-option-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/1997
Range-diff vs v2:
 1:  58e9e5c9d7 ! 1:  acf724fad5 commit: add --committer option
     @@ Commit message
             an is_author parameter.
      
          Signed-off-by: ZheNing Hu <adlternative@gmail.com>
     -    Co-authored-by: Aone-Agent <aone-agent@alibaba-inc.com>
      
       ## Documentation/git-commit.adoc ##
      @@ Documentation/git-commit.adoc: git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
 Documentation/git-commit.adoc |   9 ++-
 builtin/commit.c              | 121 +++++++++++++++++++++++++---------
 t/t7509-commit-authorship.sh  |  84 ++++++++++++++++++++++-
 3 files changed, 180 insertions(+), 34 deletions(-)
Show changes to 3 files +180 −34

Documentation/git-commit.adoc, builtin/commit.c, t/t7509-commit-authorship.sh

diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
index 54c207ad45..ed4c54ae81 100644
--- a/Documentation/git-commit.adoc
+++ b/Documentation/git-commit.adoc
@@ -12,7 +12,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
 	   [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]
 	   [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
 	   [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
-	   [--date=<date>] [--cleanup=<mode>] [--[no-]status]
+	   [--committer=<committer>] [--date=<date>] [--cleanup=<mode>] [--[no-]status]
 	   [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
 	   [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
 	   [--] [<pathspec>...]
@@ -178,6 +178,13 @@ See linkgit:git-rebase[1] for details.
 	commit by that author (i.e. `git rev-list --all -i --author=<author>`);
 	the commit author is then copied from the first such commit found.
 
+`--committer=<committer>`::
+	Override the committer for the commit. Specify an explicit committer using the
+	standard `C O Mitter <committer@example.com>` format. Otherwise _<committer>_
+	is assumed to be a pattern and is used to search for an existing
+	commit by that committer (i.e. `git rev-list --all -i --committer=<committer>`);
+	the commit committer is then copied from the first such commit found.
+
 `--date=<date>`::
 	Override the author date used in the commit.
 
diff --git a/builtin/commit.c b/builtin/commit.c
index 0243f17d53..3b249dd878 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -49,7 +49,7 @@ static const char * const builtin_commit_usage[] = {
 	   "           [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]\n"
 	   "           [-F <file> | -m <msg>] [--reset-author] [--allow-empty]\n"
 	   "           [--allow-empty-message] [--no-verify] [-e] [--author=<author>]\n"
-	   "           [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
+	   "           [--committer=<committer>] [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
 	   "           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
 	   "           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n"
 	   "           [--] [<pathspec>...]"),
@@ -112,6 +112,7 @@ static enum {
 } commit_style;
 
 static const char *force_author;
+static const char *force_committer;
 static char *logfile;
 static char *template_file;
 /*
@@ -630,46 +631,61 @@ static void set_ident_var(char **buf, char *val)
 	*buf = val;
 }
 
-static void determine_author_info(struct strbuf *author_ident)
+static void determine_identity(struct strbuf *ident_str, int is_author)
 {
 	char *name, *email, *date;
-	struct ident_split author;
-
-	name = xstrdup_or_null(getenv("GIT_AUTHOR_NAME"));
-	email = xstrdup_or_null(getenv("GIT_AUTHOR_EMAIL"));
-	date = xstrdup_or_null(getenv("GIT_AUTHOR_DATE"));
-
-	if (author_message) {
-		struct ident_split ident;
+	struct ident_split ident;
+	const char *env_name = is_author ? "GIT_AUTHOR_NAME" : "GIT_COMMITTER_NAME";
+	const char *env_email = is_author ? "GIT_AUTHOR_EMAIL" : "GIT_COMMITTER_EMAIL";
+	const char *env_date = is_author ? "GIT_AUTHOR_DATE" : "GIT_COMMITTER_DATE";
+	const char *force_ident = is_author ? force_author : force_committer;
+	const char *param_name = is_author ? "--author" : "--committer";
+	int ident_flag = is_author ? WANT_AUTHOR_IDENT : WANT_COMMITTER_IDENT;
+
+	name = xstrdup_or_null(getenv(env_name));
+	email = xstrdup_or_null(getenv(env_email));
+	date = xstrdup_or_null(getenv(env_date));
+
+	if (is_author && author_message) {
+		struct ident_split msg_ident;
 		size_t len;
 		const char *a;
 
 		a = find_commit_header(author_message_buffer, "author", &len);
 		if (!a)
 			die(_("commit '%s' lacks author header"), author_message);
-		if (split_ident_line(&ident, a, len) < 0)
+		if (split_ident_line(&msg_ident, a, len) < 0)
 			die(_("commit '%s' has malformed author line"), author_message);
 
-		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
-		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
+		set_ident_var(&name, xmemdupz(msg_ident.name_begin, msg_ident.name_end - msg_ident.name_begin));
+		set_ident_var(&email, xmemdupz(msg_ident.mail_begin, msg_ident.mail_end - msg_ident.mail_begin));
 
-		if (ident.date_begin) {
+		if (msg_ident.date_begin) {
 			struct strbuf date_buf = STRBUF_INIT;
 			strbuf_addch(&date_buf, '@');
-			strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
+			strbuf_add(&date_buf, msg_ident.date_begin, msg_ident.date_end - msg_ident.date_begin);
 			strbuf_addch(&date_buf, ' ');
-			strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
+			strbuf_add(&date_buf, msg_ident.tz_begin, msg_ident.tz_end - msg_ident.tz_begin);
 			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
 		}
 	}
 
-	if (force_author) {
-		struct ident_split ident;
+	if (force_ident) {
+		struct ident_split force_ident_split;
+
+		if (split_ident_line(&force_ident_split, force_ident, strlen(force_ident)) < 0)
+			die(_("malformed %s parameter"), param_name);
+		set_ident_var(&name, xmemdupz(force_ident_split.name_begin, force_ident_split.name_end - force_ident_split.name_begin));
+		set_ident_var(&email, xmemdupz(force_ident_split.mail_begin, force_ident_split.mail_end - force_ident_split.mail_begin));
 
-		if (split_ident_line(&ident, force_author, strlen(force_author)) < 0)
-			die(_("malformed --author parameter"));
-		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
-		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
+		if (!is_author && force_ident_split.date_begin) {
+			struct strbuf date_buf = STRBUF_INIT;
+			strbuf_addch(&date_buf, '@');
+			strbuf_add(&date_buf, force_ident_split.date_begin, force_ident_split.date_end - force_ident_split.date_begin);
+			strbuf_addch(&date_buf, ' ');
+			strbuf_add(&date_buf, force_ident_split.tz_begin, force_ident_split.tz_end - force_ident_split.tz_begin);
+			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
+		}
 	}
 
 	if (force_date) {
@@ -679,17 +695,35 @@ static void determine_author_info(struct strbuf *author_ident)
 		set_ident_var(&date, strbuf_detach(&date_buf, NULL));
 	}
 
-	strbuf_addstr(author_ident, fmt_ident(name, email, WANT_AUTHOR_IDENT, date,
+	strbuf_addstr(ident_str, fmt_ident(name, email, ident_flag, date,
 				IDENT_STRICT));
-	assert_split_ident(&author, author_ident);
-	export_one("GIT_AUTHOR_NAME", author.name_begin, author.name_end, 0);
-	export_one("GIT_AUTHOR_EMAIL", author.mail_begin, author.mail_end, 0);
-	export_one("GIT_AUTHOR_DATE", author.date_begin, author.tz_end, '@');
+	assert_split_ident(&ident, ident_str);
+
+	if (is_author) {
+		export_one("GIT_AUTHOR_NAME", ident.name_begin, ident.name_end, 0);
+		export_one("GIT_AUTHOR_EMAIL", ident.mail_begin, ident.mail_end, 0);
+		export_one("GIT_AUTHOR_DATE", ident.date_begin, ident.tz_end, '@');
+	} else {
+		export_one("GIT_COMMITTER_NAME", ident.name_begin, ident.name_end, 0);
+		export_one("GIT_COMMITTER_EMAIL", ident.mail_begin, ident.mail_end, 0);
+		export_one("GIT_COMMITTER_DATE", ident.date_begin, ident.tz_end, '@');
+	}
+
 	free(name);
 	free(email);
 	free(date);
 }
 
+static void determine_author_info(struct strbuf *author_ident)
+{
+	determine_identity(author_ident, 1);
+}
+
+static void determine_committer_info(struct strbuf *committer_ident)
+{
+	determine_identity(committer_ident, 0);
+}
+
 static int author_date_is_interesting(void)
 {
 	return author_message || force_date;
@@ -1137,16 +1171,18 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 	return 1;
 }
 
-static const char *find_author_by_nickname(const char *name)
+static const char *find_identity_by_nickname(const char *name, int is_author)
 {
 	struct rev_info revs;
 	struct commit *commit;
 	struct strbuf buf = STRBUF_INIT;
 	const char *av[20];
 	int ac = 0;
+	const char *field = is_author ? "author" : "committer";
+	const char *format = is_author ? "%aN <%aE>" : "%cN <%cE>";
 
 	repo_init_revisions(the_repository, &revs, NULL);
-	strbuf_addf(&buf, "--author=%s", name);
+	strbuf_addf(&buf, "--%s=%s", field, name);
 	av[++ac] = "--all";
 	av[++ac] = "-i";
 	av[++ac] = buf.buf;
@@ -1164,11 +1200,22 @@ static const char *find_author_by_nickname(const char *name)
 		ctx.date_mode.type = DATE_NORMAL;
 		strbuf_release(&buf);
 		repo_format_commit_message(the_repository, commit,
-					   "%aN <%aE>", &buf, &ctx);
+					   format, &buf, &ctx);
 		release_revisions(&revs);
 		return strbuf_detach(&buf, NULL);
 	}
-	die(_("--author '%s' is not 'Name <email>' and matches no existing author"), name);
+	die(_("--%s '%s' is not 'Name <email>' and matches no existing %s"),
+	    field, name, field);
+}
+
+static const char *find_author_by_nickname(const char *name)
+{
+	return find_identity_by_nickname(name, 1);
+}
+
+static const char *find_committer_by_nickname(const char *name)
+{
+	return find_identity_by_nickname(name, 0);
 }
 
 static void handle_ignored_arg(struct wt_status *s)
@@ -1321,6 +1368,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
 	if (force_author && renew_authorship)
 		die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author");
 
+	if (force_committer && !strchr(force_committer, '>'))
+		force_committer = find_committer_by_nickname(force_committer);
+
 	if (logfile || have_option_m || use_message)
 		use_editor = 0;
 
@@ -1709,6 +1759,7 @@ int cmd_commit(int argc,
 		OPT_FILENAME('F', "file", &logfile, N_("read message from file")),
 		OPT_STRING(0, "author", &force_author, N_("author"), N_("override author for commit")),
 		OPT_STRING(0, "date", &force_date, N_("date"), N_("override date for commit")),
+		OPT_STRING(0, "committer", &force_committer, N_("committer"), N_("override committer for commit")),
 		OPT_CALLBACK('m', "message", &message, N_("message"), N_("commit message"), opt_parse_m),
 		OPT_STRING('c', "reedit-message", &edit_message, N_("commit"), N_("reuse and edit message from specified commit")),
 		OPT_STRING('C', "reuse-message", &use_message, N_("commit"), N_("reuse message from specified commit")),
@@ -1785,6 +1836,7 @@ int cmd_commit(int argc,
 
 	struct strbuf sb = STRBUF_INIT;
 	struct strbuf author_ident = STRBUF_INIT;
+	struct strbuf committer_ident = STRBUF_INIT;
 	const char *index_file, *reflog_msg;
 	struct object_id oid;
 	struct commit_list *parents = NULL;
@@ -1930,8 +1982,12 @@ int cmd_commit(int argc,
 		append_merge_tag_headers(parents, &tail);
 	}
 
+	if (force_committer)
+		determine_committer_info(&committer_ident);
+
 	if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,
-				 parents, &oid, author_ident.buf, NULL,
+				 parents, &oid, author_ident.buf,
+				 force_committer ? committer_ident.buf : NULL,
 				 sign_commit, extra)) {
 		rollback_index_files();
 		die(_("failed to write commit object"));
@@ -1980,6 +2036,7 @@ cleanup:
 	free_commit_extra_headers(extra);
 	free_commit_list(parents);
 	strbuf_release(&author_ident);
+	strbuf_release(&committer_ident);
 	strbuf_release(&err);
 	strbuf_release(&sb);
 	free(logfile);
diff --git a/t/t7509-commit-authorship.sh b/t/t7509-commit-authorship.sh
index 8e373b566b..7e163e02d1 100755
--- a/t/t7509-commit-authorship.sh
+++ b/t/t7509-commit-authorship.sh
@@ -12,13 +12,20 @@ author_header () {
 	sed -n -e '/^$/q' -e '/^author /p'
 }
 
+committer_header () {
+	git cat-file commit "$1" |
+	sed -n -e '/^$/q' -e '/^committer /p'
+}
+
 message_body () {
 	git cat-file commit "$1" |
 	sed -e '1,/^$/d'
 }
 
 test_expect_success '-C option copies authorship and message' '
-	test_commit --author Frigate\ \<flying@over.world\> \
+	test_env GIT_COMMITTER_NAME="Frigate" \
+		GIT_COMMITTER_EMAIL="flying@over.world" \
+		test_commit --author Frigate\ \<flying@over.world\> \
 		"Initial Commit" foo Initial Initial &&
 	echo "Test 1" >>foo &&
 	test_tick &&
@@ -171,4 +178,79 @@ test_expect_success '--reset-author with CHERRY_PICK_HEAD' '
 	test_cmp expect actual
 '
 
+test_expect_success '--committer option overrides committer' '
+	git checkout Initial &&
+	echo "Test --committer" >>foo &&
+	test_tick &&
+	git commit -a -m "test committer" --committer="Custom Committer <custom@committer.example>" &&
+	committer_header HEAD >actual &&
+	grep "Custom Committer <custom@committer.example>" actual
+'
+
+test_expect_success '--committer with pattern search' '
+	echo "Test committer pattern" >>foo &&
+	test_tick &&
+	git commit -a -m "test committer pattern" --committer="Frigate" &&
+	committer_header HEAD >actual &&
+	grep "Frigate <flying@over.world>" actual
+'
+
+test_expect_success '--committer malformed parameter' '
+	echo "Test malformed" >>foo &&
+	test_tick &&
+	test_must_fail git commit -a -m "test malformed" --committer="malformed committer"
+'
+
+test_expect_success '--committer with --amend option' '
+	git checkout -f Initial &&
+	echo "Test committer with amend" >>foo &&
+	test_tick &&
+	git commit -a -m "initial commit for amend test" &&
+	echo "Modified for amend" >>foo &&
+	test_tick &&
+	git commit -a --amend --no-edit \
+		--author="Test Author <test@author.example>" \
+		--committer="Test Committer <test@committer.example>" &&
+	author_header HEAD >actual_author &&
+	grep "Test Author <test@author.example>" actual_author &&
+	committer_header HEAD >actual_committer &&
+	grep "Test Committer <test@committer.example>" actual_committer
+'
+
+test_expect_success 'GIT_COMMITTER_* environment variables' '
+	git checkout -f Initial &&
+	echo "Test env vars" >>foo &&
+	test_tick &&
+	test_env GIT_COMMITTER_NAME="Env Committer" \
+		GIT_COMMITTER_EMAIL="env@test.example" \
+		git commit -a -m "test committer env vars" &&
+	committer_header HEAD >actual &&
+	grep "Env Committer <env@test.example>" actual
+'
+
+test_expect_success '--committer overrides GIT_COMMITTER_* environment variables' '
+	echo "Test override" >>foo &&
+	test_tick &&
+	test_env GIT_COMMITTER_NAME="Env Committer" \
+		GIT_COMMITTER_EMAIL="env@test.example" \
+		git commit -a -m "test override" \
+		--committer="Override Committer <override@test.example>" &&
+	committer_header HEAD >actual &&
+	grep "Override Committer <override@test.example>" actual
+'
+
+test_expect_success '--date with --committer changes both author and committer dates' '
+	git checkout -f Initial &&
+	echo "Test date override" >>foo &&
+	test_tick &&
+	git commit -a -m "test date" \
+		--author="Date Author <date@author.example>" \
+		--committer="Date Committer <date@committer.example>" \
+		--date="2024-06-15 10:30:00 +0800" &&
+	git log -1 --format="%ai" >author_date &&
+	git log -1 --format="%ci" >committer_date &&
+	grep "2024-06-15 10:30:00 +0800" author_date &&
+	grep "2024-06-15 10:30:00 +0800" committer_date
+'
+
 test_done

base-commit: 4badef0c3503dc29059d678abba7fac0f042bc84
-- 
gitgitgadget
Junio C Hamano· Nov 12, 2025, 18:56 UTC · re: ZheNing Hu via GitGitGadget · lore

Re: [PATCH v3] commit: add --committer option

"ZheNing Hu via GitGitGadget" <gitgitgadget@gmail.com> writes:
> +`--committer=<committer>`::
> +	Override the committer for the commit. Specify an explicit committer using the
Isn't "set" or "use" more appropirate verb to use here?

We already take the committer identity from multiple plases, like user.{name,email}, or GIT_COMMITTER_{NAME,EMAIL} configuration, and with the patch we also take it from a command line option, with the usual precedence order (i.e., command line trumps environment which trumps configuration).

> -static void determine_author_info(struct strbuf *author_ident)
> +static void determine_identity(struct strbuf *ident_str, int is_author)

"is_author" does not sound grammatical for this case; if you are giving an ident of an unknown kind to this function and supplying another parameter to let it know which kind, "is_author" may make sense, but not here.

As you will convert it into WANT_{AUTHOR,COMMITTER}_IDENT before using anyway, why not let the caller use the "enum want_ident" to tell this function what to do?

Show 51 quoted lines
>  {
>  	char *name, *email, *date;
> -	struct ident_split author;
> -
> -	name = xstrdup_or_null(getenv("GIT_AUTHOR_NAME"));
> -	email = xstrdup_or_null(getenv("GIT_AUTHOR_EMAIL"));
> -	date = xstrdup_or_null(getenv("GIT_AUTHOR_DATE"));
> -
> -	if (author_message) {
> -		struct ident_split ident;
> +	struct ident_split ident;
> +	const char *env_name = is_author ? "GIT_AUTHOR_NAME" : "GIT_COMMITTER_NAME";
> +	const char *env_email = is_author ? "GIT_AUTHOR_EMAIL" : "GIT_COMMITTER_EMAIL";
> +	const char *env_date = is_author ? "GIT_AUTHOR_DATE" : "GIT_COMMITTER_DATE";
> +	const char *force_ident = is_author ? force_author : force_committer;
> +	const char *param_name = is_author ? "--author" : "--committer";
> +	int ident_flag = is_author ? WANT_AUTHOR_IDENT : WANT_COMMITTER_IDENT;
> +
> +	name = xstrdup_or_null(getenv(env_name));
> +	email = xstrdup_or_null(getenv(env_email));
> +	date = xstrdup_or_null(getenv(env_date));
> +
> +	if (is_author && author_message) {
> +		struct ident_split msg_ident;
>  		size_t len;
>  		const char *a;
>  
>  		a = find_commit_header(author_message_buffer, "author", &len);
>  		if (!a)
>  			die(_("commit '%s' lacks author header"), author_message);
> -		if (split_ident_line(&ident, a, len) < 0)
> +		if (split_ident_line(&msg_ident, a, len) < 0)
>  			die(_("commit '%s' has malformed author line"), author_message);
>  
> -		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
> -		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
> +		set_ident_var(&name, xmemdupz(msg_ident.name_begin, msg_ident.name_end - msg_ident.name_begin));
> +		set_ident_var(&email, xmemdupz(msg_ident.mail_begin, msg_ident.mail_end - msg_ident.mail_begin));
>  
> -		if (ident.date_begin) {
> +		if (msg_ident.date_begin) {
>  			struct strbuf date_buf = STRBUF_INIT;
>  			strbuf_addch(&date_buf, '@');
> -			strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
> +			strbuf_add(&date_buf, msg_ident.date_begin, msg_ident.date_end - msg_ident.date_begin);
>  			strbuf_addch(&date_buf, ' ');
> -			strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
> +			strbuf_add(&date_buf, msg_ident.tz_begin, msg_ident.tz_end - msg_ident.tz_begin);
>  			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
>  		}
>  	}

The helper tries to be generic between both kinds of ident, but we still need conditional that says "this part of the function is only when we are looking for author", which is rather unsatisfactory.

Also why do we need this much patch noise, only because you renamed one variable? I wonder if it would make it cleaner to move the body of this if() {} statement into a separate helper function, leaving only

	if (whose_ident == WANT_AUTHOR_IDENT)
		set_author_from_message(&name, &email, &date);
or something simple here?
ZheNing Hu· Nov 15, 2025, 06:33 UTC · re: Junio C Hamano · lore

Re: [PATCH v3] commit: add --committer option

Junio C Hamano <gitster@pobox.com> 于2025年11月13日周四 02:56写道:
Show 14 quoted lines
>
> "ZheNing Hu via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
> > +`--committer=<committer>`::
> > +     Override the committer for the commit. Specify an explicit committer using the
>
> Isn't "set" or "use" more appropirate verb to use here?
>
> We already take the committer identity from multiple plases, like
> user.{name,email}, or GIT_COMMITTER_{NAME,EMAIL} configuration, and
> with the patch we also take it from a command line option, with the
> usual precedence order (i.e., command line trumps environment which
> trumps configuration).
>

Since both `--author=<author>` and `--date=<date>` use the "Override the *" phrasing, `--committer` uses "Override the" just to maintain consistency, though changing it to "Set the" would also be acceptable.

Show 12 quoted lines
> > -static void determine_author_info(struct strbuf *author_ident)
> > +static void determine_identity(struct strbuf *ident_str, int is_author)
>
> "is_author" does not sound grammatical for this case; if you are
> giving an ident of an unknown kind to this function and supplying
> another parameter to let it know which kind, "is_author" may make
> sense, but not here.
>
> As you will convert it into WANT_{AUTHOR,COMMITTER}_IDENT before
> using anyway, why not let the caller use the "enum want_ident" to
> tell this function what to do?
>
Ok, will change.
Show 56 quoted lines
> >  {
> >       char *name, *email, *date;
> > -     struct ident_split author;
> > -
> > -     name = xstrdup_or_null(getenv("GIT_AUTHOR_NAME"));
> > -     email = xstrdup_or_null(getenv("GIT_AUTHOR_EMAIL"));
> > -     date = xstrdup_or_null(getenv("GIT_AUTHOR_DATE"));
> > -
> > -     if (author_message) {
> > -             struct ident_split ident;
> > +     struct ident_split ident;
> > +     const char *env_name = is_author ? "GIT_AUTHOR_NAME" : "GIT_COMMITTER_NAME";
> > +     const char *env_email = is_author ? "GIT_AUTHOR_EMAIL" : "GIT_COMMITTER_EMAIL";
> > +     const char *env_date = is_author ? "GIT_AUTHOR_DATE" : "GIT_COMMITTER_DATE";
> > +     const char *force_ident = is_author ? force_author : force_committer;
> > +     const char *param_name = is_author ? "--author" : "--committer";
> > +     int ident_flag = is_author ? WANT_AUTHOR_IDENT : WANT_COMMITTER_IDENT;
> > +
> > +     name = xstrdup_or_null(getenv(env_name));
> > +     email = xstrdup_or_null(getenv(env_email));
> > +     date = xstrdup_or_null(getenv(env_date));
> > +
> > +     if (is_author && author_message) {
> > +             struct ident_split msg_ident;
> >               size_t len;
> >               const char *a;
> >
> >               a = find_commit_header(author_message_buffer, "author", &len);
> >               if (!a)
> >                       die(_("commit '%s' lacks author header"), author_message);
> > -             if (split_ident_line(&ident, a, len) < 0)
> > +             if (split_ident_line(&msg_ident, a, len) < 0)
> >                       die(_("commit '%s' has malformed author line"), author_message);
> >
> > -             set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
> > -             set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
> > +             set_ident_var(&name, xmemdupz(msg_ident.name_begin, msg_ident.name_end - msg_ident.name_begin));
> > +             set_ident_var(&email, xmemdupz(msg_ident.mail_begin, msg_ident.mail_end - msg_ident.mail_begin));
> >
> > -             if (ident.date_begin) {
> > +             if (msg_ident.date_begin) {
> >                       struct strbuf date_buf = STRBUF_INIT;
> >                       strbuf_addch(&date_buf, '@');
> > -                     strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
> > +                     strbuf_add(&date_buf, msg_ident.date_begin, msg_ident.date_end - msg_ident.date_begin);
> >                       strbuf_addch(&date_buf, ' ');
> > -                     strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
> > +                     strbuf_add(&date_buf, msg_ident.tz_begin, msg_ident.tz_end - msg_ident.tz_begin);
> >                       set_ident_var(&date, strbuf_detach(&date_buf, NULL));
> >               }
> >       }
>
> The helper tries to be generic between both kinds of ident, but we
> still need conditional that says "this part of the function is only
> when we are looking for author", which is rather unsatisfactory.
>

Indeed, some specific logic should be moved to determine_author_info()/determine_committer_info().

Show 8 quoted lines
> Also why do we need this much patch noise, only because you renamed
> one variable?  I wonder if it would make it cleaner to move the body
> of this if() {} statement into a separate helper function, leaving
> only
>
>         if (whose_ident == WANT_AUTHOR_IDENT)
>                 set_author_from_message(&name, &email, &date);
>
Ok.
> or something simple here?
ZheNing Hu via GitGitGadget· Nov 15, 2025, 15:43 UTC · re: ZheNing Hu via GitGitGadget · lore

[PATCH v4] commit: add --committer option

From: ZheNing Hu <adlternative@gmail.com>

Add --committer option to git-commit, allowing users to override the committer identity similar to how --author works. This provides a more convenient alternative to setting GIT_COMMITTER_* environment variables.

Like --author, the --committer option supports two formats:
- Explicit identity: --committer="Name <email@example.com>"
- Pattern search: --committer="pattern" searches commit history for a
  matching committer and reuses that identity
To share code with the existing --author option, this patch refactors:
1. find_author_by_nickname() into find_identity_by_nickname() which
   handles both author and committer searches through an is_author
   parameter.
2. determine_author_info() into determine_identity() which handles
   identity parsing and setting for both author and committer through
   an is_author parameter.
Signed-off-by: ZheNing Hu <adlternative@gmail.com>
---
    commit: add --committer option
    
    Currently, when users need to override the committer identity in
    git-commit, they have to set GIT_COMMITTER_NAME and GIT_COMMITTER_EMAIL
    environment variables, which can be cumbersome in scripting scenarios or
    when frequently switching committer identities.
    
    While git-commit already provides the --author option to conveniently
    override the author identity, there's no equivalent --committer option
    for the committer identity. This asymmetry creates an inconsistent user
    experience.
    
    This patch introduces the --committer option to git-commit, providing:
    
     1. Consistency with the existing --author option
     2. A more convenient alternative to environment variables
     3. Better support for automated workflows and scripts
     4. Improved user experience when managing multiple identities
    
    The implementation follows the same pattern as the --author option,
    accepting the format "Name " and properly validating the input.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1997%2Fadlternative%2Fzh%2Fimplement-committer-option-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1997/adlternative/zh/implement-committer-option-v4
Pull-Request: https://github.com/gitgitgadget/git/pull/1997
Range-diff vs v3:
 1:  acf724fad5 ! 1:  bb17f810ef commit: add --committer option
     @@ Documentation/git-commit.adoc: See linkgit:git-rebase[1] for details.
       	the commit author is then copied from the first such commit found.
       
      +`--committer=<committer>`::
     -+	Override the committer for the commit. Specify an explicit committer using the
     ++	Set the committer for the commit. Specify an explicit committer using the
      +	standard `C O Mitter <committer@example.com>` format. Otherwise _<committer>_
      +	is assumed to be a pattern and is used to search for an existing
      +	commit by that committer (i.e. `git rev-list --all -i --committer=<committer>`);
     @@ builtin/commit.c: static void set_ident_var(char **buf, char *val)
       }
       
      -static void determine_author_info(struct strbuf *author_ident)
     -+static void determine_identity(struct strbuf *ident_str, int is_author)
     - {
     - 	char *name, *email, *date;
     +-{
     +-	char *name, *email, *date;
      -	struct ident_split author;
     --
     ++static void set_author_from_message(char **name, char **email, char **date) {
     ++	struct ident_split ident;
     ++	size_t len;
     ++	const char *a;
     + 
      -	name = xstrdup_or_null(getenv("GIT_AUTHOR_NAME"));
      -	email = xstrdup_or_null(getenv("GIT_AUTHOR_EMAIL"));
      -	date = xstrdup_or_null(getenv("GIT_AUTHOR_DATE"));
     --
     ++	if (!author_message)
     ++		return;
     + 
      -	if (author_message) {
      -		struct ident_split ident;
     -+	struct ident_split ident;
     -+	const char *env_name = is_author ? "GIT_AUTHOR_NAME" : "GIT_COMMITTER_NAME";
     -+	const char *env_email = is_author ? "GIT_AUTHOR_EMAIL" : "GIT_COMMITTER_EMAIL";
     -+	const char *env_date = is_author ? "GIT_AUTHOR_DATE" : "GIT_COMMITTER_DATE";
     -+	const char *force_ident = is_author ? force_author : force_committer;
     -+	const char *param_name = is_author ? "--author" : "--committer";
     -+	int ident_flag = is_author ? WANT_AUTHOR_IDENT : WANT_COMMITTER_IDENT;
     -+
     -+	name = xstrdup_or_null(getenv(env_name));
     -+	email = xstrdup_or_null(getenv(env_email));
     -+	date = xstrdup_or_null(getenv(env_date));
     -+
     -+	if (is_author && author_message) {
     -+		struct ident_split msg_ident;
     - 		size_t len;
     - 		const char *a;
     - 
     - 		a = find_commit_header(author_message_buffer, "author", &len);
     - 		if (!a)
     - 			die(_("commit '%s' lacks author header"), author_message);
     +-		size_t len;
     +-		const char *a;
     +-
     +-		a = find_commit_header(author_message_buffer, "author", &len);
     +-		if (!a)
     +-			die(_("commit '%s' lacks author header"), author_message);
      -		if (split_ident_line(&ident, a, len) < 0)
     -+		if (split_ident_line(&msg_ident, a, len) < 0)
     - 			die(_("commit '%s' has malformed author line"), author_message);
     - 
     +-			die(_("commit '%s' has malformed author line"), author_message);
     +-
      -		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
      -		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
     -+		set_ident_var(&name, xmemdupz(msg_ident.name_begin, msg_ident.name_end - msg_ident.name_begin));
     -+		set_ident_var(&email, xmemdupz(msg_ident.mail_begin, msg_ident.mail_end - msg_ident.mail_begin));
     - 
     +-
      -		if (ident.date_begin) {
     -+		if (msg_ident.date_begin) {
     - 			struct strbuf date_buf = STRBUF_INIT;
     - 			strbuf_addch(&date_buf, '@');
     +-			struct strbuf date_buf = STRBUF_INIT;
     +-			strbuf_addch(&date_buf, '@');
      -			strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
     -+			strbuf_add(&date_buf, msg_ident.date_begin, msg_ident.date_end - msg_ident.date_begin);
     - 			strbuf_addch(&date_buf, ' ');
     +-			strbuf_addch(&date_buf, ' ');
      -			strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
     -+			strbuf_add(&date_buf, msg_ident.tz_begin, msg_ident.tz_end - msg_ident.tz_begin);
     - 			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
     - 		}
     +-			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
     +-		}
     ++	a = find_commit_header(author_message_buffer, "author", &len);
     ++	if (!a)
     ++		die(_("commit '%s' lacks author header"), author_message);
     ++	if (split_ident_line(&ident, a, len) < 0)
     ++		die(_("commit '%s' has malformed author line"), author_message);
     ++
     ++	set_ident_var(name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
     ++	set_ident_var(email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
     ++
     ++	if (ident.date_begin) {
     ++		struct strbuf date_buf = STRBUF_INIT;
     ++		strbuf_addch(&date_buf, '@');
     ++		strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
     ++		strbuf_addch(&date_buf, ' ');
     ++		strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
     ++		set_ident_var(date, strbuf_detach(&date_buf, NULL));
       	}
     ++}
       
      -	if (force_author) {
      -		struct ident_split ident;
     ++static void determine_identity(struct strbuf *ident_str, enum want_ident whose_ident,
     ++			       const char *env_name, const char *env_email, const char *env_date,
     ++			       const char *force_ident, const char *param_name,
     ++			       char *name, char *email, char *date)
     ++{
     ++	struct ident_split ident;
     ++
     ++
      +	if (force_ident) {
      +		struct ident_split force_ident_split;
     -+
     -+		if (split_ident_line(&force_ident_split, force_ident, strlen(force_ident)) < 0)
     -+			die(_("malformed %s parameter"), param_name);
     -+		set_ident_var(&name, xmemdupz(force_ident_split.name_begin, force_ident_split.name_end - force_ident_split.name_begin));
     -+		set_ident_var(&email, xmemdupz(force_ident_split.mail_begin, force_ident_split.mail_end - force_ident_split.mail_begin));
       
      -		if (split_ident_line(&ident, force_author, strlen(force_author)) < 0)
      -			die(_("malformed --author parameter"));
      -		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
      -		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
     -+		if (!is_author && force_ident_split.date_begin) {
     -+			struct strbuf date_buf = STRBUF_INIT;
     -+			strbuf_addch(&date_buf, '@');
     -+			strbuf_add(&date_buf, force_ident_split.date_begin, force_ident_split.date_end - force_ident_split.date_begin);
     -+			strbuf_addch(&date_buf, ' ');
     -+			strbuf_add(&date_buf, force_ident_split.tz_begin, force_ident_split.tz_end - force_ident_split.tz_begin);
     -+			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
     -+		}
     ++		if (split_ident_line(&force_ident_split, force_ident, strlen(force_ident)) < 0)
     ++			die(_("malformed %s parameter"), param_name);
     ++		set_ident_var(&name, xmemdupz(force_ident_split.name_begin, force_ident_split.name_end - force_ident_split.name_begin));
     ++		set_ident_var(&email, xmemdupz(force_ident_split.mail_begin, force_ident_split.mail_end - force_ident_split.mail_begin));
       	}
       
       	if (force_date) {
     @@ builtin/commit.c: static void determine_author_info(struct strbuf *author_ident)
       	}
       
      -	strbuf_addstr(author_ident, fmt_ident(name, email, WANT_AUTHOR_IDENT, date,
     -+	strbuf_addstr(ident_str, fmt_ident(name, email, ident_flag, date,
     ++	strbuf_addstr(ident_str, fmt_ident(name, email, whose_ident, date,
       				IDENT_STRICT));
      -	assert_split_ident(&author, author_ident);
      -	export_one("GIT_AUTHOR_NAME", author.name_begin, author.name_end, 0);
     @@ builtin/commit.c: static void determine_author_info(struct strbuf *author_ident)
      -	export_one("GIT_AUTHOR_DATE", author.date_begin, author.tz_end, '@');
      +	assert_split_ident(&ident, ident_str);
      +
     -+	if (is_author) {
     -+		export_one("GIT_AUTHOR_NAME", ident.name_begin, ident.name_end, 0);
     -+		export_one("GIT_AUTHOR_EMAIL", ident.mail_begin, ident.mail_end, 0);
     -+		export_one("GIT_AUTHOR_DATE", ident.date_begin, ident.tz_end, '@');
     -+	} else {
     -+		export_one("GIT_COMMITTER_NAME", ident.name_begin, ident.name_end, 0);
     -+		export_one("GIT_COMMITTER_EMAIL", ident.mail_begin, ident.mail_end, 0);
     -+		export_one("GIT_COMMITTER_DATE", ident.date_begin, ident.tz_end, '@');
     -+	}
     ++	export_one(env_name, ident.name_begin, ident.name_end, 0);
     ++	export_one(env_email, ident.mail_begin, ident.mail_end, 0);
     ++	export_one(env_date, ident.date_begin, ident.tz_end, '@');
      +
       	free(name);
       	free(email);
     @@ builtin/commit.c: static void determine_author_info(struct strbuf *author_ident)
       
      +static void determine_author_info(struct strbuf *author_ident)
      +{
     -+	determine_identity(author_ident, 1);
     ++	char *name, *email, *date;
     ++
     ++	name = xstrdup_or_null(getenv("GIT_AUTHOR_NAME"));
     ++	email = xstrdup_or_null(getenv("GIT_AUTHOR_EMAIL"));
     ++	date = xstrdup_or_null(getenv("GIT_AUTHOR_DATE"));
     ++
     ++	set_author_from_message(&name, &email, &date);
     ++
     ++	determine_identity(author_ident, WANT_AUTHOR_IDENT,
     ++			   "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_AUTHOR_DATE",
     ++			   force_author, "--author",
     ++			   name, email, date);
      +}
      +
      +static void determine_committer_info(struct strbuf *committer_ident)
      +{
     -+	determine_identity(committer_ident, 0);
     ++	char *name, *email, *date;
     ++
     ++	name = xstrdup_or_null(getenv("GIT_COMMITTER_NAME"));
     ++	email = xstrdup_or_null(getenv("GIT_COMMITTER_EMAIL"));
     ++	date = xstrdup_or_null(getenv("GIT_COMMITTER_DATE"));
     ++
     ++	determine_identity(committer_ident, WANT_COMMITTER_IDENT,
     ++			   "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL", "GIT_COMMITTER_DATE",
     ++			   force_committer, "--committer",
     ++			   name, email, date);
      +}
      +
       static int author_date_is_interesting(void)
     @@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const cha
       }
       
      -static const char *find_author_by_nickname(const char *name)
     -+static const char *find_identity_by_nickname(const char *name, int is_author)
     ++static const char *find_identity_by_nickname(const char *name, enum want_ident whose_ident)
       {
       	struct rev_info revs;
       	struct commit *commit;
       	struct strbuf buf = STRBUF_INIT;
       	const char *av[20];
       	int ac = 0;
     -+	const char *field = is_author ? "author" : "committer";
     -+	const char *format = is_author ? "%aN <%aE>" : "%cN <%cE>";
     ++	const char *field, *format;
     ++
     ++	if (whose_ident != WANT_AUTHOR_IDENT && whose_ident != WANT_COMMITTER_IDENT)
     ++		BUG("find_identity_by_nickname requires WANT_AUTHOR_IDENT or WANT_COMMITTER_IDENT");
     ++
     ++	field = whose_ident == WANT_AUTHOR_IDENT ? "author" : "committer";
     ++	format = whose_ident == WANT_AUTHOR_IDENT ? "%aN <%aE>" : "%cN <%cE>";
       
       	repo_init_revisions(the_repository, &revs, NULL);
      -	strbuf_addf(&buf, "--author=%s", name);
     @@ builtin/commit.c: static const char *find_author_by_nickname(const char *name)
      +
      +static const char *find_author_by_nickname(const char *name)
      +{
     -+	return find_identity_by_nickname(name, 1);
     ++	return find_identity_by_nickname(name, WANT_AUTHOR_IDENT);
      +}
      +
      +static const char *find_committer_by_nickname(const char *name)
      +{
     -+	return find_identity_by_nickname(name, 0);
     ++	return find_identity_by_nickname(name, WANT_COMMITTER_IDENT);
       }
       
       static void handle_ignored_arg(struct wt_status *s)
 Documentation/git-commit.adoc |   9 +-
 builtin/commit.c              | 155 ++++++++++++++++++++++++----------
 t/t7509-commit-authorship.sh  |  84 +++++++++++++++++-
 3 files changed, 200 insertions(+), 48 deletions(-)
Show changes to 3 files +200 −48

Documentation/git-commit.adoc, builtin/commit.c, t/t7509-commit-authorship.sh

diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
index 54c207ad45..a0c8a586de 100644
--- a/Documentation/git-commit.adoc
+++ b/Documentation/git-commit.adoc
@@ -12,7 +12,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
 	   [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]
 	   [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
 	   [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
-	   [--date=<date>] [--cleanup=<mode>] [--[no-]status]
+	   [--committer=<committer>] [--date=<date>] [--cleanup=<mode>] [--[no-]status]
 	   [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
 	   [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
 	   [--] [<pathspec>...]
@@ -178,6 +178,13 @@ See linkgit:git-rebase[1] for details.
 	commit by that author (i.e. `git rev-list --all -i --author=<author>`);
 	the commit author is then copied from the first such commit found.
 
+`--committer=<committer>`::
+	Set the committer for the commit. Specify an explicit committer using the
+	standard `C O Mitter <committer@example.com>` format. Otherwise _<committer>_
+	is assumed to be a pattern and is used to search for an existing
+	commit by that committer (i.e. `git rev-list --all -i --committer=<committer>`);
+	the commit committer is then copied from the first such commit found.
+
 `--date=<date>`::
 	Override the author date used in the commit.
 
diff --git a/builtin/commit.c b/builtin/commit.c
index 0243f17d53..13138a3868 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -49,7 +49,7 @@ static const char * const builtin_commit_usage[] = {
 	   "           [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]\n"
 	   "           [-F <file> | -m <msg>] [--reset-author] [--allow-empty]\n"
 	   "           [--allow-empty-message] [--no-verify] [-e] [--author=<author>]\n"
-	   "           [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
+	   "           [--committer=<committer>] [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
 	   "           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
 	   "           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n"
 	   "           [--] [<pathspec>...]"),
@@ -112,6 +112,7 @@ static enum {
 } commit_style;
 
 static const char *force_author;
+static const char *force_committer;
 static char *logfile;
 static char *template_file;
 /*
@@ -630,46 +631,48 @@ static void set_ident_var(char **buf, char *val)
 	*buf = val;
 }
 
-static void determine_author_info(struct strbuf *author_ident)
-{
-	char *name, *email, *date;
-	struct ident_split author;
+static void set_author_from_message(char **name, char **email, char **date) {
+	struct ident_split ident;
+	size_t len;
+	const char *a;
 
-	name = xstrdup_or_null(getenv("GIT_AUTHOR_NAME"));
-	email = xstrdup_or_null(getenv("GIT_AUTHOR_EMAIL"));
-	date = xstrdup_or_null(getenv("GIT_AUTHOR_DATE"));
+	if (!author_message)
+		return;
 
-	if (author_message) {
-		struct ident_split ident;
-		size_t len;
-		const char *a;
-
-		a = find_commit_header(author_message_buffer, "author", &len);
-		if (!a)
-			die(_("commit '%s' lacks author header"), author_message);
-		if (split_ident_line(&ident, a, len) < 0)
-			die(_("commit '%s' has malformed author line"), author_message);
-
-		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
-		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
-
-		if (ident.date_begin) {
-			struct strbuf date_buf = STRBUF_INIT;
-			strbuf_addch(&date_buf, '@');
-			strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
-			strbuf_addch(&date_buf, ' ');
-			strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
-			set_ident_var(&date, strbuf_detach(&date_buf, NULL));
-		}
+	a = find_commit_header(author_message_buffer, "author", &len);
+	if (!a)
+		die(_("commit '%s' lacks author header"), author_message);
+	if (split_ident_line(&ident, a, len) < 0)
+		die(_("commit '%s' has malformed author line"), author_message);
+
+	set_ident_var(name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
+	set_ident_var(email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
+
+	if (ident.date_begin) {
+		struct strbuf date_buf = STRBUF_INIT;
+		strbuf_addch(&date_buf, '@');
+		strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
+		strbuf_addch(&date_buf, ' ');
+		strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
+		set_ident_var(date, strbuf_detach(&date_buf, NULL));
 	}
+}
 
-	if (force_author) {
-		struct ident_split ident;
+static void determine_identity(struct strbuf *ident_str, enum want_ident whose_ident,
+			       const char *env_name, const char *env_email, const char *env_date,
+			       const char *force_ident, const char *param_name,
+			       char *name, char *email, char *date)
+{
+	struct ident_split ident;
+
+
+	if (force_ident) {
+		struct ident_split force_ident_split;
 
-		if (split_ident_line(&ident, force_author, strlen(force_author)) < 0)
-			die(_("malformed --author parameter"));
-		set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
-		set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
+		if (split_ident_line(&force_ident_split, force_ident, strlen(force_ident)) < 0)
+			die(_("malformed %s parameter"), param_name);
+		set_ident_var(&name, xmemdupz(force_ident_split.name_begin, force_ident_split.name_end - force_ident_split.name_begin));
+		set_ident_var(&email, xmemdupz(force_ident_split.mail_begin, force_ident_split.mail_end - force_ident_split.mail_begin));
 	}
 
 	if (force_date) {
@@ -679,17 +682,49 @@ static void determine_author_info(struct strbuf *author_ident)
 		set_ident_var(&date, strbuf_detach(&date_buf, NULL));
 	}
 
-	strbuf_addstr(author_ident, fmt_ident(name, email, WANT_AUTHOR_IDENT, date,
+	strbuf_addstr(ident_str, fmt_ident(name, email, whose_ident, date,
 				IDENT_STRICT));
-	assert_split_ident(&author, author_ident);
-	export_one("GIT_AUTHOR_NAME", author.name_begin, author.name_end, 0);
-	export_one("GIT_AUTHOR_EMAIL", author.mail_begin, author.mail_end, 0);
-	export_one("GIT_AUTHOR_DATE", author.date_begin, author.tz_end, '@');
+	assert_split_ident(&ident, ident_str);
+
+	export_one(env_name, ident.name_begin, ident.name_end, 0);
+	export_one(env_email, ident.mail_begin, ident.mail_end, 0);
+	export_one(env_date, ident.date_begin, ident.tz_end, '@');
+
 	free(name);
 	free(email);
 	free(date);
 }
 
+static void determine_author_info(struct strbuf *author_ident)
+{
+	char *name, *email, *date;
+
+	name = xstrdup_or_null(getenv("GIT_AUTHOR_NAME"));
+	email = xstrdup_or_null(getenv("GIT_AUTHOR_EMAIL"));
+	date = xstrdup_or_null(getenv("GIT_AUTHOR_DATE"));
+
+	set_author_from_message(&name, &email, &date);
+
+	determine_identity(author_ident, WANT_AUTHOR_IDENT,
+			   "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_AUTHOR_DATE",
+			   force_author, "--author",
+			   name, email, date);
+}
+
+static void determine_committer_info(struct strbuf *committer_ident)
+{
+	char *name, *email, *date;
+
+	name = xstrdup_or_null(getenv("GIT_COMMITTER_NAME"));
+	email = xstrdup_or_null(getenv("GIT_COMMITTER_EMAIL"));
+	date = xstrdup_or_null(getenv("GIT_COMMITTER_DATE"));
+
+	determine_identity(committer_ident, WANT_COMMITTER_IDENT,
+			   "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL", "GIT_COMMITTER_DATE",
+			   force_committer, "--committer",
+			   name, email, date);
+}
+
 static int author_date_is_interesting(void)
 {
 	return author_message || force_date;
@@ -1137,16 +1172,23 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 	return 1;
 }
 
-static const char *find_author_by_nickname(const char *name)
+static const char *find_identity_by_nickname(const char *name, enum want_ident whose_ident)
 {
 	struct rev_info revs;
 	struct commit *commit;
 	struct strbuf buf = STRBUF_INIT;
 	const char *av[20];
 	int ac = 0;
+	const char *field, *format;
+
+	if (whose_ident != WANT_AUTHOR_IDENT && whose_ident != WANT_COMMITTER_IDENT)
+		BUG("find_identity_by_nickname requires WANT_AUTHOR_IDENT or WANT_COMMITTER_IDENT");
+
+	field = whose_ident == WANT_AUTHOR_IDENT ? "author" : "committer";
+	format = whose_ident == WANT_AUTHOR_IDENT ? "%aN <%aE>" : "%cN <%cE>";
 
 	repo_init_revisions(the_repository, &revs, NULL);
-	strbuf_addf(&buf, "--author=%s", name);
+	strbuf_addf(&buf, "--%s=%s", field, name);
 	av[++ac] = "--all";
 	av[++ac] = "-i";
 	av[++ac] = buf.buf;
@@ -1164,11 +1206,22 @@ static const char *find_author_by_nickname(const char *name)
 		ctx.date_mode.type = DATE_NORMAL;
 		strbuf_release(&buf);
 		repo_format_commit_message(the_repository, commit,
-					   "%aN <%aE>", &buf, &ctx);
+					   format, &buf, &ctx);
 		release_revisions(&revs);
 		return strbuf_detach(&buf, NULL);
 	}
-	die(_("--author '%s' is not 'Name <email>' and matches no existing author"), name);
+	die(_("--%s '%s' is not 'Name <email>' and matches no existing %s"),
+	    field, name, field);
+}
+
+static const char *find_author_by_nickname(const char *name)
+{
+	return find_identity_by_nickname(name, WANT_AUTHOR_IDENT);
+}
+
+static const char *find_committer_by_nickname(const char *name)
+{
+	return find_identity_by_nickname(name, WANT_COMMITTER_IDENT);
 }
 
 static void handle_ignored_arg(struct wt_status *s)
@@ -1321,6 +1374,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
 	if (force_author && renew_authorship)
 		die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author");
 
+	if (force_committer && !strchr(force_committer, '>'))
+		force_committer = find_committer_by_nickname(force_committer);
+
 	if (logfile || have_option_m || use_message)
 		use_editor = 0;
 
@@ -1709,6 +1765,7 @@ int cmd_commit(int argc,
 		OPT_FILENAME('F', "file", &logfile, N_("read message from file")),
 		OPT_STRING(0, "author", &force_author, N_("author"), N_("override author for commit")),
 		OPT_STRING(0, "date", &force_date, N_("date"), N_("override date for commit")),
+		OPT_STRING(0, "committer", &force_committer, N_("committer"), N_("override committer for commit")),
 		OPT_CALLBACK('m', "message", &message, N_("message"), N_("commit message"), opt_parse_m),
 		OPT_STRING('c', "reedit-message", &edit_message, N_("commit"), N_("reuse and edit message from specified commit")),
 		OPT_STRING('C', "reuse-message", &use_message, N_("commit"), N_("reuse message from specified commit")),
@@ -1785,6 +1842,7 @@ int cmd_commit(int argc,
 
 	struct strbuf sb = STRBUF_INIT;
 	struct strbuf author_ident = STRBUF_INIT;
+	struct strbuf committer_ident = STRBUF_INIT;
 	const char *index_file, *reflog_msg;
 	struct object_id oid;
 	struct commit_list *parents = NULL;
@@ -1930,8 +1988,12 @@ int cmd_commit(int argc,
 		append_merge_tag_headers(parents, &tail);
 	}
 
+	if (force_committer)
+		determine_committer_info(&committer_ident);
+
 	if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,
-				 parents, &oid, author_ident.buf, NULL,
+				 parents, &oid, author_ident.buf,
+				 force_committer ? committer_ident.buf : NULL,
 				 sign_commit, extra)) {
 		rollback_index_files();
 		die(_("failed to write commit object"));
@@ -1980,6 +2042,7 @@ cleanup:
 	free_commit_extra_headers(extra);
 	free_commit_list(parents);
 	strbuf_release(&author_ident);
+	strbuf_release(&committer_ident);
 	strbuf_release(&err);
 	strbuf_release(&sb);
 	free(logfile);
diff --git a/t/t7509-commit-authorship.sh b/t/t7509-commit-authorship.sh
index 8e373b566b..7e163e02d1 100755
--- a/t/t7509-commit-authorship.sh
+++ b/t/t7509-commit-authorship.sh
@@ -12,13 +12,20 @@ author_header () {
 	sed -n -e '/^$/q' -e '/^author /p'
 }
 
+committer_header () {
+	git cat-file commit "$1" |
+	sed -n -e '/^$/q' -e '/^committer /p'
+}
+
 message_body () {
 	git cat-file commit "$1" |
 	sed -e '1,/^$/d'
 }
 
 test_expect_success '-C option copies authorship and message' '
-	test_commit --author Frigate\ \<flying@over.world\> \
+	test_env GIT_COMMITTER_NAME="Frigate" \
+		GIT_COMMITTER_EMAIL="flying@over.world" \
+		test_commit --author Frigate\ \<flying@over.world\> \
 		"Initial Commit" foo Initial Initial &&
 	echo "Test 1" >>foo &&
 	test_tick &&
@@ -171,4 +178,79 @@ test_expect_success '--reset-author with CHERRY_PICK_HEAD' '
 	test_cmp expect actual
 '
 
+test_expect_success '--committer option overrides committer' '
+	git checkout Initial &&
+	echo "Test --committer" >>foo &&
+	test_tick &&
+	git commit -a -m "test committer" --committer="Custom Committer <custom@committer.example>" &&
+	committer_header HEAD >actual &&
+	grep "Custom Committer <custom@committer.example>" actual
+'
+
+test_expect_success '--committer with pattern search' '
+	echo "Test committer pattern" >>foo &&
+	test_tick &&
+	git commit -a -m "test committer pattern" --committer="Frigate" &&
+	committer_header HEAD >actual &&
+	grep "Frigate <flying@over.world>" actual
+'
+
+test_expect_success '--committer malformed parameter' '
+	echo "Test malformed" >>foo &&
+	test_tick &&
+	test_must_fail git commit -a -m "test malformed" --committer="malformed committer"
+'
+
+test_expect_success '--committer with --amend option' '
+	git checkout -f Initial &&
+	echo "Test committer with amend" >>foo &&
+	test_tick &&
+	git commit -a -m "initial commit for amend test" &&
+	echo "Modified for amend" >>foo &&
+	test_tick &&
+	git commit -a --amend --no-edit \
+		--author="Test Author <test@author.example>" \
+		--committer="Test Committer <test@committer.example>" &&
+	author_header HEAD >actual_author &&
+	grep "Test Author <test@author.example>" actual_author &&
+	committer_header HEAD >actual_committer &&
+	grep "Test Committer <test@committer.example>" actual_committer
+'
+
+test_expect_success 'GIT_COMMITTER_* environment variables' '
+	git checkout -f Initial &&
+	echo "Test env vars" >>foo &&
+	test_tick &&
+	test_env GIT_COMMITTER_NAME="Env Committer" \
+		GIT_COMMITTER_EMAIL="env@test.example" \
+		git commit -a -m "test committer env vars" &&
+	committer_header HEAD >actual &&
+	grep "Env Committer <env@test.example>" actual
+'
+
+test_expect_success '--committer overrides GIT_COMMITTER_* environment variables' '
+	echo "Test override" >>foo &&
+	test_tick &&
+	test_env GIT_COMMITTER_NAME="Env Committer" \
+		GIT_COMMITTER_EMAIL="env@test.example" \
+		git commit -a -m "test override" \
+		--committer="Override Committer <override@test.example>" &&
+	committer_header HEAD >actual &&
+	grep "Override Committer <override@test.example>" actual
+'
+
+test_expect_success '--date with --committer changes both author and committer dates' '
+	git checkout -f Initial &&
+	echo "Test date override" >>foo &&
+	test_tick &&
+	git commit -a -m "test date" \
+		--author="Date Author <date@author.example>" \
+		--committer="Date Committer <date@committer.example>" \
+		--date="2024-06-15 10:30:00 +0800" &&
+	git log -1 --format="%ai" >author_date &&
+	git log -1 --format="%ci" >committer_date &&
+	grep "2024-06-15 10:30:00 +0800" author_date &&
+	grep "2024-06-15 10:30:00 +0800" committer_date
+'
+
 test_done

base-commit: 4badef0c3503dc29059d678abba7fac0f042bc84
-- 
gitgitgadget

← back to recent threads