Skip to content

Commit bb17f81

Browse files
committed
commit: add --committer option
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 protected]>" - 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 <[email protected]>
1 parent 4badef0 commit bb17f81

File tree

3 files changed

+200
-48
lines changed

3 files changed

+200
-48
lines changed

Documentation/git-commit.adoc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
1212
[--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]
1313
[-F <file> | -m <msg>] [--reset-author] [--allow-empty]
1414
[--allow-empty-message] [--no-verify] [-e] [--author=<author>]
15-
[--date=<date>] [--cleanup=<mode>] [--[no-]status]
15+
[--committer=<committer>] [--date=<date>] [--cleanup=<mode>] [--[no-]status]
1616
[-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
1717
[(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
1818
[--] [<pathspec>...]
@@ -178,6 +178,13 @@ See linkgit:git-rebase[1] for details.
178178
commit by that author (i.e. `git rev-list --all -i --author=<author>`);
179179
the commit author is then copied from the first such commit found.
180180

181+
`--committer=<committer>`::
182+
Set the committer for the commit. Specify an explicit committer using the
183+
standard `C O Mitter <[email protected]>` format. Otherwise _<committer>_
184+
is assumed to be a pattern and is used to search for an existing
185+
commit by that committer (i.e. `git rev-list --all -i --committer=<committer>`);
186+
the commit committer is then copied from the first such commit found.
187+
181188
`--date=<date>`::
182189
Override the author date used in the commit.
183190

builtin/commit.c

Lines changed: 109 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ static const char * const builtin_commit_usage[] = {
4949
" [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>]\n"
5050
" [-F <file> | -m <msg>] [--reset-author] [--allow-empty]\n"
5151
" [--allow-empty-message] [--no-verify] [-e] [--author=<author>]\n"
52-
" [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
52+
" [--committer=<committer>] [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
5353
" [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
5454
" [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n"
5555
" [--] [<pathspec>...]"),
@@ -112,6 +112,7 @@ static enum {
112112
} commit_style;
113113

114114
static const char *force_author;
115+
static const char *force_committer;
115116
static char *logfile;
116117
static char *template_file;
117118
/*
@@ -630,46 +631,48 @@ static void set_ident_var(char **buf, char *val)
630631
*buf = val;
631632
}
632633

633-
static void determine_author_info(struct strbuf *author_ident)
634-
{
635-
char *name, *email, *date;
636-
struct ident_split author;
634+
static void set_author_from_message(char **name, char **email, char **date) {
635+
struct ident_split ident;
636+
size_t len;
637+
const char *a;
637638

638-
name = xstrdup_or_null(getenv("GIT_AUTHOR_NAME"));
639-
email = xstrdup_or_null(getenv("GIT_AUTHOR_EMAIL"));
640-
date = xstrdup_or_null(getenv("GIT_AUTHOR_DATE"));
639+
if (!author_message)
640+
return;
641641

642-
if (author_message) {
643-
struct ident_split ident;
644-
size_t len;
645-
const char *a;
646-
647-
a = find_commit_header(author_message_buffer, "author", &len);
648-
if (!a)
649-
die(_("commit '%s' lacks author header"), author_message);
650-
if (split_ident_line(&ident, a, len) < 0)
651-
die(_("commit '%s' has malformed author line"), author_message);
652-
653-
set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
654-
set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
655-
656-
if (ident.date_begin) {
657-
struct strbuf date_buf = STRBUF_INIT;
658-
strbuf_addch(&date_buf, '@');
659-
strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
660-
strbuf_addch(&date_buf, ' ');
661-
strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
662-
set_ident_var(&date, strbuf_detach(&date_buf, NULL));
663-
}
642+
a = find_commit_header(author_message_buffer, "author", &len);
643+
if (!a)
644+
die(_("commit '%s' lacks author header"), author_message);
645+
if (split_ident_line(&ident, a, len) < 0)
646+
die(_("commit '%s' has malformed author line"), author_message);
647+
648+
set_ident_var(name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
649+
set_ident_var(email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
650+
651+
if (ident.date_begin) {
652+
struct strbuf date_buf = STRBUF_INIT;
653+
strbuf_addch(&date_buf, '@');
654+
strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin);
655+
strbuf_addch(&date_buf, ' ');
656+
strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin);
657+
set_ident_var(date, strbuf_detach(&date_buf, NULL));
664658
}
659+
}
665660

666-
if (force_author) {
667-
struct ident_split ident;
661+
static void determine_identity(struct strbuf *ident_str, enum want_ident whose_ident,
662+
const char *env_name, const char *env_email, const char *env_date,
663+
const char *force_ident, const char *param_name,
664+
char *name, char *email, char *date)
665+
{
666+
struct ident_split ident;
667+
668+
669+
if (force_ident) {
670+
struct ident_split force_ident_split;
668671

669-
if (split_ident_line(&ident, force_author, strlen(force_author)) < 0)
670-
die(_("malformed --author parameter"));
671-
set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin));
672-
set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin));
672+
if (split_ident_line(&force_ident_split, force_ident, strlen(force_ident)) < 0)
673+
die(_("malformed %s parameter"), param_name);
674+
set_ident_var(&name, xmemdupz(force_ident_split.name_begin, force_ident_split.name_end - force_ident_split.name_begin));
675+
set_ident_var(&email, xmemdupz(force_ident_split.mail_begin, force_ident_split.mail_end - force_ident_split.mail_begin));
673676
}
674677

675678
if (force_date) {
@@ -679,17 +682,49 @@ static void determine_author_info(struct strbuf *author_ident)
679682
set_ident_var(&date, strbuf_detach(&date_buf, NULL));
680683
}
681684

682-
strbuf_addstr(author_ident, fmt_ident(name, email, WANT_AUTHOR_IDENT, date,
685+
strbuf_addstr(ident_str, fmt_ident(name, email, whose_ident, date,
683686
IDENT_STRICT));
684-
assert_split_ident(&author, author_ident);
685-
export_one("GIT_AUTHOR_NAME", author.name_begin, author.name_end, 0);
686-
export_one("GIT_AUTHOR_EMAIL", author.mail_begin, author.mail_end, 0);
687-
export_one("GIT_AUTHOR_DATE", author.date_begin, author.tz_end, '@');
687+
assert_split_ident(&ident, ident_str);
688+
689+
export_one(env_name, ident.name_begin, ident.name_end, 0);
690+
export_one(env_email, ident.mail_begin, ident.mail_end, 0);
691+
export_one(env_date, ident.date_begin, ident.tz_end, '@');
692+
688693
free(name);
689694
free(email);
690695
free(date);
691696
}
692697

698+
static void determine_author_info(struct strbuf *author_ident)
699+
{
700+
char *name, *email, *date;
701+
702+
name = xstrdup_or_null(getenv("GIT_AUTHOR_NAME"));
703+
email = xstrdup_or_null(getenv("GIT_AUTHOR_EMAIL"));
704+
date = xstrdup_or_null(getenv("GIT_AUTHOR_DATE"));
705+
706+
set_author_from_message(&name, &email, &date);
707+
708+
determine_identity(author_ident, WANT_AUTHOR_IDENT,
709+
"GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_AUTHOR_DATE",
710+
force_author, "--author",
711+
name, email, date);
712+
}
713+
714+
static void determine_committer_info(struct strbuf *committer_ident)
715+
{
716+
char *name, *email, *date;
717+
718+
name = xstrdup_or_null(getenv("GIT_COMMITTER_NAME"));
719+
email = xstrdup_or_null(getenv("GIT_COMMITTER_EMAIL"));
720+
date = xstrdup_or_null(getenv("GIT_COMMITTER_DATE"));
721+
722+
determine_identity(committer_ident, WANT_COMMITTER_IDENT,
723+
"GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL", "GIT_COMMITTER_DATE",
724+
force_committer, "--committer",
725+
name, email, date);
726+
}
727+
693728
static int author_date_is_interesting(void)
694729
{
695730
return author_message || force_date;
@@ -1137,16 +1172,23 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
11371172
return 1;
11381173
}
11391174

1140-
static const char *find_author_by_nickname(const char *name)
1175+
static const char *find_identity_by_nickname(const char *name, enum want_ident whose_ident)
11411176
{
11421177
struct rev_info revs;
11431178
struct commit *commit;
11441179
struct strbuf buf = STRBUF_INIT;
11451180
const char *av[20];
11461181
int ac = 0;
1182+
const char *field, *format;
1183+
1184+
if (whose_ident != WANT_AUTHOR_IDENT && whose_ident != WANT_COMMITTER_IDENT)
1185+
BUG("find_identity_by_nickname requires WANT_AUTHOR_IDENT or WANT_COMMITTER_IDENT");
1186+
1187+
field = whose_ident == WANT_AUTHOR_IDENT ? "author" : "committer";
1188+
format = whose_ident == WANT_AUTHOR_IDENT ? "%aN <%aE>" : "%cN <%cE>";
11471189

11481190
repo_init_revisions(the_repository, &revs, NULL);
1149-
strbuf_addf(&buf, "--author=%s", name);
1191+
strbuf_addf(&buf, "--%s=%s", field, name);
11501192
av[++ac] = "--all";
11511193
av[++ac] = "-i";
11521194
av[++ac] = buf.buf;
@@ -1164,11 +1206,22 @@ static const char *find_author_by_nickname(const char *name)
11641206
ctx.date_mode.type = DATE_NORMAL;
11651207
strbuf_release(&buf);
11661208
repo_format_commit_message(the_repository, commit,
1167-
"%aN <%aE>", &buf, &ctx);
1209+
format, &buf, &ctx);
11681210
release_revisions(&revs);
11691211
return strbuf_detach(&buf, NULL);
11701212
}
1171-
die(_("--author '%s' is not 'Name <email>' and matches no existing author"), name);
1213+
die(_("--%s '%s' is not 'Name <email>' and matches no existing %s"),
1214+
field, name, field);
1215+
}
1216+
1217+
static const char *find_author_by_nickname(const char *name)
1218+
{
1219+
return find_identity_by_nickname(name, WANT_AUTHOR_IDENT);
1220+
}
1221+
1222+
static const char *find_committer_by_nickname(const char *name)
1223+
{
1224+
return find_identity_by_nickname(name, WANT_COMMITTER_IDENT);
11721225
}
11731226

11741227
static void handle_ignored_arg(struct wt_status *s)
@@ -1321,6 +1374,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
13211374
if (force_author && renew_authorship)
13221375
die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author");
13231376

1377+
if (force_committer && !strchr(force_committer, '>'))
1378+
force_committer = find_committer_by_nickname(force_committer);
1379+
13241380
if (logfile || have_option_m || use_message)
13251381
use_editor = 0;
13261382

@@ -1709,6 +1765,7 @@ int cmd_commit(int argc,
17091765
OPT_FILENAME('F', "file", &logfile, N_("read message from file")),
17101766
OPT_STRING(0, "author", &force_author, N_("author"), N_("override author for commit")),
17111767
OPT_STRING(0, "date", &force_date, N_("date"), N_("override date for commit")),
1768+
OPT_STRING(0, "committer", &force_committer, N_("committer"), N_("override committer for commit")),
17121769
OPT_CALLBACK('m', "message", &message, N_("message"), N_("commit message"), opt_parse_m),
17131770
OPT_STRING('c', "reedit-message", &edit_message, N_("commit"), N_("reuse and edit message from specified commit")),
17141771
OPT_STRING('C', "reuse-message", &use_message, N_("commit"), N_("reuse message from specified commit")),
@@ -1785,6 +1842,7 @@ int cmd_commit(int argc,
17851842

17861843
struct strbuf sb = STRBUF_INIT;
17871844
struct strbuf author_ident = STRBUF_INIT;
1845+
struct strbuf committer_ident = STRBUF_INIT;
17881846
const char *index_file, *reflog_msg;
17891847
struct object_id oid;
17901848
struct commit_list *parents = NULL;
@@ -1930,8 +1988,12 @@ int cmd_commit(int argc,
19301988
append_merge_tag_headers(parents, &tail);
19311989
}
19321990

1991+
if (force_committer)
1992+
determine_committer_info(&committer_ident);
1993+
19331994
if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,
1934-
parents, &oid, author_ident.buf, NULL,
1995+
parents, &oid, author_ident.buf,
1996+
force_committer ? committer_ident.buf : NULL,
19351997
sign_commit, extra)) {
19361998
rollback_index_files();
19371999
die(_("failed to write commit object"));
@@ -1980,6 +2042,7 @@ int cmd_commit(int argc,
19802042
free_commit_extra_headers(extra);
19812043
free_commit_list(parents);
19822044
strbuf_release(&author_ident);
2045+
strbuf_release(&committer_ident);
19832046
strbuf_release(&err);
19842047
strbuf_release(&sb);
19852048
free(logfile);

t/t7509-commit-authorship.sh

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,20 @@ author_header () {
1212
sed -n -e '/^$/q' -e '/^author /p'
1313
}
1414

15+
committer_header () {
16+
git cat-file commit "$1" |
17+
sed -n -e '/^$/q' -e '/^committer /p'
18+
}
19+
1520
message_body () {
1621
git cat-file commit "$1" |
1722
sed -e '1,/^$/d'
1823
}
1924

2025
test_expect_success '-C option copies authorship and message' '
21-
test_commit --author Frigate\ \<[email protected]\> \
26+
test_env GIT_COMMITTER_NAME="Frigate" \
27+
GIT_COMMITTER_EMAIL="[email protected]" \
28+
test_commit --author Frigate\ \<[email protected]\> \
2229
"Initial Commit" foo Initial Initial &&
2330
echo "Test 1" >>foo &&
2431
test_tick &&
@@ -171,4 +178,79 @@ test_expect_success '--reset-author with CHERRY_PICK_HEAD' '
171178
test_cmp expect actual
172179
'
173180

181+
test_expect_success '--committer option overrides committer' '
182+
git checkout Initial &&
183+
echo "Test --committer" >>foo &&
184+
test_tick &&
185+
git commit -a -m "test committer" --committer="Custom Committer <[email protected]>" &&
186+
committer_header HEAD >actual &&
187+
grep "Custom Committer <[email protected]>" actual
188+
'
189+
190+
test_expect_success '--committer with pattern search' '
191+
echo "Test committer pattern" >>foo &&
192+
test_tick &&
193+
git commit -a -m "test committer pattern" --committer="Frigate" &&
194+
committer_header HEAD >actual &&
195+
grep "Frigate <[email protected]>" actual
196+
'
197+
198+
test_expect_success '--committer malformed parameter' '
199+
echo "Test malformed" >>foo &&
200+
test_tick &&
201+
test_must_fail git commit -a -m "test malformed" --committer="malformed committer"
202+
'
203+
204+
test_expect_success '--committer with --amend option' '
205+
git checkout -f Initial &&
206+
echo "Test committer with amend" >>foo &&
207+
test_tick &&
208+
git commit -a -m "initial commit for amend test" &&
209+
echo "Modified for amend" >>foo &&
210+
test_tick &&
211+
git commit -a --amend --no-edit \
212+
--author="Test Author <[email protected]>" \
213+
--committer="Test Committer <[email protected]>" &&
214+
author_header HEAD >actual_author &&
215+
grep "Test Author <[email protected]>" actual_author &&
216+
committer_header HEAD >actual_committer &&
217+
grep "Test Committer <[email protected]>" actual_committer
218+
'
219+
220+
test_expect_success 'GIT_COMMITTER_* environment variables' '
221+
git checkout -f Initial &&
222+
echo "Test env vars" >>foo &&
223+
test_tick &&
224+
test_env GIT_COMMITTER_NAME="Env Committer" \
225+
GIT_COMMITTER_EMAIL="[email protected]" \
226+
git commit -a -m "test committer env vars" &&
227+
committer_header HEAD >actual &&
228+
grep "Env Committer <[email protected]>" actual
229+
'
230+
231+
test_expect_success '--committer overrides GIT_COMMITTER_* environment variables' '
232+
echo "Test override" >>foo &&
233+
test_tick &&
234+
test_env GIT_COMMITTER_NAME="Env Committer" \
235+
GIT_COMMITTER_EMAIL="[email protected]" \
236+
git commit -a -m "test override" \
237+
--committer="Override Committer <[email protected]>" &&
238+
committer_header HEAD >actual &&
239+
grep "Override Committer <[email protected]>" actual
240+
'
241+
242+
test_expect_success '--date with --committer changes both author and committer dates' '
243+
git checkout -f Initial &&
244+
echo "Test date override" >>foo &&
245+
test_tick &&
246+
git commit -a -m "test date" \
247+
--author="Date Author <[email protected]>" \
248+
--committer="Date Committer <[email protected]>" \
249+
--date="2024-06-15 10:30:00 +0800" &&
250+
git log -1 --format="%ai" >author_date &&
251+
git log -1 --format="%ci" >committer_date &&
252+
grep "2024-06-15 10:30:00 +0800" author_date &&
253+
grep "2024-06-15 10:30:00 +0800" committer_date
254+
'
255+
174256
test_done

0 commit comments

Comments
 (0)