diff mbox series

[v3,08/10] trailer: move arg handling to interpret-trailers.c

Message ID 465dc51cdcba28d235241021bc52369f6082d329.1706664145.git.gitgitgadget@gmail.com (mailing list archive)
State Superseded
Headers show
Series Enrich Trailer API | expand

Commit Message

Linus Arver Jan. 31, 2024, 1:22 a.m. UTC
From: Linus Arver <linusa@google.com>

We don't move the "arg_item" struct to interpret-trailers.c, because it
is now a struct that contains information about trailers that could be
added into the input text's own trailers. This is a generic concept that
extends beyond trailers defined as CLI arguments (it applies to trailers
defined in configuration as well). We will rename "arg_item" to
"trailer_template" in a follow-up patch to keep the diff here small.

Signed-off-by: Linus Arver <linusa@google.com>
---
 builtin/interpret-trailers.c | 88 ++++++++++++++++++++++--------------
 trailer.c                    | 62 ++++++++++++++++++-------
 trailer.h                    | 12 +++++
 3 files changed, 112 insertions(+), 50 deletions(-)

Comments

Junio C Hamano Feb. 1, 2024, 10:23 p.m. UTC | #1
"Linus Arver via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: Linus Arver <linusa@google.com>
>
> We don't move the "arg_item" struct to interpret-trailers.c, because it
> is now a struct that contains information about trailers that could be
> added into the input text's own trailers. This is a generic concept that
> extends beyond trailers defined as CLI arguments (it applies to trailers
> defined in configuration as well). We will rename "arg_item" to
> "trailer_template" in a follow-up patch to keep the diff here small.
>
> Signed-off-by: Linus Arver <linusa@google.com>
> ---
>  builtin/interpret-trailers.c | 88 ++++++++++++++++++++++--------------
>  trailer.c                    | 62 ++++++++++++++++++-------
>  trailer.h                    | 12 +++++
>  3 files changed, 112 insertions(+), 50 deletions(-)
>
> diff --git a/builtin/interpret-trailers.c b/builtin/interpret-trailers.c
> index 9f0ba39b317..9a902012912 100644
> --- a/builtin/interpret-trailers.c
> +++ b/builtin/interpret-trailers.c
> @@ -45,23 +45,17 @@ static int option_parse_if_missing(const struct option *opt,
>  	return trailer_set_if_missing(opt->value, arg);
>  }
>  
> -static void free_new_trailers(struct list_head *trailers)
> -{
> -	struct list_head *pos, *tmp;
> -	struct new_trailer_item *item;
> -
> -	list_for_each_safe(pos, tmp, trailers) {
> -		item = list_entry(pos, struct new_trailer_item, list);
> -		list_del(pos);
> -		free(item);
> -	}
> -}
> +static char *cl_separators;

It somehow feels ugly and goes backward to depend on a new global,
especially when you are aiming to libify more things.  If we are
doing something like ...

>  static int option_parse_trailer(const struct option *opt,
>  				   const char *arg, int unset)
>  {
>  	struct list_head *trailers = opt->value;
> -	struct new_trailer_item *item;
> +	struct strbuf tok = STRBUF_INIT;
> +	struct strbuf val = STRBUF_INIT;
> +	const struct trailer_conf *conf;
> +	struct trailer_conf *conf_current = new_trailer_conf();
> +	ssize_t separator_pos;
>  
>  	if (unset) {
>  		free_new_trailers(trailers);
> @@ -71,12 +65,31 @@ static int option_parse_trailer(const struct option *opt,
>  	if (!arg)
>  		return -1;
>  
> -	item = xmalloc(sizeof(*item));
> -	item->text = arg;
> -	item->where = where;
> -	item->if_exists = if_exists;
> -	item->if_missing = if_missing;
> -	list_add_tail(&item->list, trailers);
> +	separator_pos = find_separator(arg, cl_separators);
> +	if (separator_pos) {
> +		parse_trailer(arg, separator_pos, &tok, &val, &conf);
> +		duplicate_trailer_conf(conf_current, conf);
> +
> +		/*
> +		 * Override conf_current with settings specified via CLI flags.
> +		 */

... this, which apparently needs to know that we are dealing with
input from CLI, shouldn't we be able to do the "CLI allows '=' as
well as whatever is the default" here, instead of mucking with the
global variable in the caller?

Even if you plan to later make this function callable from outside
the context of parse_options() callback, and if you do not want "CLI
allows '=' as well" for such new callers, we should be able to have
a shared helper function that is the bulk of this function but takes
additional parameter to tweak its behaviour slightly depending on
the needs of the callers?

> +		trailer_conf_set(where, if_exists, if_missing, conf_current);

I am getting an impression that the use of and the introduction of
the new helper, mixed with movement of the responsibility, makes
reviewing the change unnecessarily more cumbersome.  An equivalent
series with a few more steps, each of them focusing on a small
change (like, "updating the conf_current members is done with
assignments that appear as a pattern very often---introduce a helper
to reduce boilerplate") would have been more enticing to reviewers.

> +		trailer_add_arg_item(strbuf_detach(&tok, NULL),
> +				     strbuf_detach(&val, NULL),
> +				     conf_current,
> +				     trailers);
> +	} else {
> +		struct strbuf sb = STRBUF_INIT;
> +		strbuf_addstr(&sb, arg);
> +		strbuf_trim(&sb);
> +		error(_("empty trailer token in trailer '%.*s'"),
> +			(int) sb.len, sb.buf);
> +		strbuf_release(&sb);
> +	}
> +
> +	free(conf_current);
> +
>  	return 0;
>  }
>  
> @@ -135,7 +148,7 @@ static void read_input_file(struct strbuf *sb, const char *file)
>  }
>  
>  static void interpret_trailers(const struct process_trailer_options *opts,
> -			       struct list_head *new_trailer_head,
> +			       struct list_head *arg_trailers,
>  			       const char *file)
>  {
>  	LIST_HEAD(head);
> @@ -144,8 +157,6 @@ static void interpret_trailers(const struct process_trailer_options *opts,
>  	struct trailer_block *trailer_block;
>  	FILE *outfile = stdout;
>  
> -	trailer_config_init();
> -
>  	read_input_file(&sb, file);
>  
>  	if (opts->in_place)
> @@ -162,12 +173,7 @@ static void interpret_trailers(const struct process_trailer_options *opts,
>  
>  
>  	if (!opts->only_input) {
> -		LIST_HEAD(config_head);
> -		LIST_HEAD(arg_head);
> -		parse_trailers_from_config(&config_head);
> -		parse_trailers_from_command_line_args(&arg_head, new_trailer_head);
> -		list_splice(&config_head, &arg_head);
> -		process_trailers_lists(&head, &arg_head);
> +		process_trailers_lists(&head, arg_trailers);
>  	}

So, the bulk of the parsing is no longer responsibility of this
function.  Instead, the caller (e.g., cmd_interpret_trailers()) is
expected to do that before they call us.

> ...
>  	git_config(git_default_config, NULL);
> +	trailer_config_init();
> +
> +	if (!opts.only_input) {
> +		parse_trailers_from_config(&configured_trailers);
> +	}

By the way, until we add more statements in this block, lose the
unnecessary {} around it.

> +	/*
> +	* In command-line arguments, '=' is accepted (in addition to the
> +	* separators that are defined).
> +	*/
> +	cl_separators = xstrfmt("=%s", trailer_default_separators());
>  
>  	argc = parse_options(argc, argv, prefix, options,
>  			     git_interpret_trailers_usage, 0);
>  
> -	if (opts.only_input && !list_empty(&trailers))
> +	free(cl_separators);
> +
> +	if (opts.only_input && !list_empty(&arg_trailers))
>  		usage_msg_opt(
>  			_("--trailer with --only-input does not make sense"),
>  			git_interpret_trailers_usage,
>  			options);
>  
> +	list_splice(&configured_trailers, &arg_trailers);

This corresponds to what the old code did in interpret_trailers(),
OK.

The move of the responsibility may make sense.

>  	if (argc) {
>  		int i;
>  		for (i = 0; i < argc; i++)
> -			interpret_trailers(&opts, &trailers, argv[i]);
> +			interpret_trailers(&opts, &arg_trailers, argv[i]);
>  	} else {
>  		if (opts.in_place)
>  			die(_("no input file given for in-place editing"));
> -		interpret_trailers(&opts, &trailers, NULL);
> +		interpret_trailers(&opts, &arg_trailers, NULL);
>  	}
>  
> -	free_new_trailers(&trailers);
> +	free_new_trailers(&arg_trailers);
>  
>  	return 0;
>  }
> diff --git a/trailer.c b/trailer.c
> index c16f552b463..19637ff295d 100644
> --- a/trailer.c
> +++ b/trailer.c
> @@ -66,6 +66,11 @@ static LIST_HEAD(conf_head);
>  
>  static char *separators = ":";
>  
> +const char *trailer_default_separators(void)
> +{
> +	return separators;
> +}
> +
>  static int configured;
>  
>  #define TRAILER_ARG_STRING "$ARG"
> @@ -424,6 +429,25 @@ int trailer_set_if_missing(enum trailer_if_missing *item, const char *value)
>  	return 0;
>  }
>  
> +void trailer_conf_set(enum trailer_where where,
> +		      enum trailer_if_exists if_exists,
> +		      enum trailer_if_missing if_missing,
> +		      struct trailer_conf *conf)
> +{
> +	if (where != WHERE_DEFAULT)
> +		conf->where = where;
> +	if (if_exists != EXISTS_DEFAULT)
> +		conf->if_exists = if_exists;
> +	if (if_missing != MISSING_DEFAULT)
> +		conf->if_missing = if_missing;
> +}

So, this is such a helper function. It is curious that it
deliberately lacks the ability to reset what has already been
configured back to the default.

For example, we earlier saw this original code ...

> -	item = xmalloc(sizeof(*item));
> -	item->text = arg;
> -	item->where = where;
> -	item->if_exists = if_exists;
> -	item->if_missing = if_missing;

... gets replaced with this call.

> +	struct trailer_conf *conf_current = new_trailer_conf();
> ...
> +		trailer_conf_set(where, if_exists, if_missing, conf_current);

For this conversion to be correct, we require that the value of the
*_DEFAULT macro to be 0 and/or these three values getting assigned
are not *_DEFAULT values.  Maybe that may happen to be the case in
today's code, but such an unwritten assumption makes me feel nervous.

I am not sure if it is worth the confusion factor to make a function
whose name is $anything_set() to be making a conditional assignment.
If such a conditional assignment *also* happens often and deserves
to have its own helper, it is fine to have such a helper for
conditional assignment, but calling it $anything_set() is probably a
mistake.

Other than that, the main thrust of this step seems sensible.

Thanks.
Linus Arver Feb. 3, 2024, 1:48 a.m. UTC | #2
Junio C Hamano <gitster@pobox.com> writes:

> "Linus Arver via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
> [...]
>
> Other than that, the main thrust of this step seems sensible.
>
> Thanks.

Quick update: I've broken down patches 3 and 4 into smaller pieces, and
am now ready to break this one down, too.

Hopefully sometime over the weekend I'll have some answers ready for the
many good questions you've raised. Thanks.
Linus Arver Feb. 6, 2024, 1:01 a.m. UTC | #3
Junio C Hamano <gitster@pobox.com> writes:

> "Linus Arver via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
>> diff --git a/builtin/interpret-trailers.c b/builtin/interpret-trailers.c
>> index 9f0ba39b317..9a902012912 100644
>> --- a/builtin/interpret-trailers.c
>> +++ b/builtin/interpret-trailers.c
>> @@ -45,23 +45,17 @@ static int option_parse_if_missing(const struct option *opt,
>>  	return trailer_set_if_missing(opt->value, arg);
>>  }
>>  
>> -static void free_new_trailers(struct list_head *trailers)
>> -{
>> -	struct list_head *pos, *tmp;
>> -	struct new_trailer_item *item;
>> -
>> -	list_for_each_safe(pos, tmp, trailers) {
>> -		item = list_entry(pos, struct new_trailer_item, list);
>> -		list_del(pos);
>> -		free(item);
>> -	}
>> -}
>> +static char *cl_separators;
>
> It somehow feels ugly and goes backward to depend on a new global,
> especially when you are aiming to libify more things.

Removed in the next reroll (we can just use a regular stack variable,
not a global).

> Even if you plan to later make this function callable from outside
> the context of parse_options() callback, and if you do not want "CLI
> allows '=' as well" for such new callers, we should be able to have
> a shared helper function that is the bulk of this function but takes
> additional parameter to tweak its behaviour slightly depending on
> the needs of the callers?

I'll refrain from exploring this possibility in this series (and just
make do with the non-global variable as described above) because of the
additional pending changes around trailer configuration handling in the
larger series. FWIW even in the larger series I don't think there's a
need to have such a helper function.

>> +		trailer_conf_set(where, if_exists, if_missing, conf_current);
>
> I am getting an impression that the use of and the introduction of
> the new helper, mixed with movement of the responsibility, makes
> reviewing the change unnecessarily more cumbersome.  An equivalent
> series with a few more steps, each of them focusing on a small
> change (like, "updating the conf_current members is done with
> assignments that appear as a pattern very often---introduce a helper
> to reduce boilerplate") would have been more enticing to reviewers.

I've broken this patch down into smaller steps (along with the other
notable patches in this series).

>> +		trailer_add_arg_item(strbuf_detach(&tok, NULL),
>> +				     strbuf_detach(&val, NULL),
>> +				     conf_current,
>> +				     trailers);
>> +	} else {
>> +		struct strbuf sb = STRBUF_INIT;
>> +		strbuf_addstr(&sb, arg);
>> +		strbuf_trim(&sb);
>> +		error(_("empty trailer token in trailer '%.*s'"),
>> +			(int) sb.len, sb.buf);
>> +		strbuf_release(&sb);
>> +	}
>> +
>> +	free(conf_current);
>> +
>>  	return 0;
>>  }
>>  
>> @@ -135,7 +148,7 @@ static void read_input_file(struct strbuf *sb, const char *file)
>>  }
>>  
>>  static void interpret_trailers(const struct process_trailer_options *opts,
>> -			       struct list_head *new_trailer_head,
>> +			       struct list_head *arg_trailers,
>>  			       const char *file)
>>  {
>>  	LIST_HEAD(head);
>> @@ -144,8 +157,6 @@ static void interpret_trailers(const struct process_trailer_options *opts,
>>  	struct trailer_block *trailer_block;
>>  	FILE *outfile = stdout;
>>  
>> -	trailer_config_init();
>> -
>>  	read_input_file(&sb, file);
>>  
>>  	if (opts->in_place)
>> @@ -162,12 +173,7 @@ static void interpret_trailers(const struct process_trailer_options *opts,
>>  
>>  
>>  	if (!opts->only_input) {
>> -		LIST_HEAD(config_head);
>> -		LIST_HEAD(arg_head);
>> -		parse_trailers_from_config(&config_head);
>> -		parse_trailers_from_command_line_args(&arg_head, new_trailer_head);
>> -		list_splice(&config_head, &arg_head);
>> -		process_trailers_lists(&head, &arg_head);
>> +		process_trailers_lists(&head, arg_trailers);
>>  	}
>
> So, the bulk of the parsing is no longer responsibility of this
> function.  Instead, the caller (e.g., cmd_interpret_trailers()) is
> expected to do that before they call us.

Yup. This movement of "parsing responsibility" logic should be easy to
see in the next reroll (as it will be broken out separately).

>> ...
>>  	git_config(git_default_config, NULL);
>> +	trailer_config_init();
>> +
>> +	if (!opts.only_input) {
>> +		parse_trailers_from_config(&configured_trailers);
>> +	}
>
> By the way, until we add more statements in this block, lose the
> unnecessary {} around it.

Oops. Done.

>> @@ -424,6 +429,25 @@ int trailer_set_if_missing(enum trailer_if_missing *item, const char *value)
>>  	return 0;
>>  }
>>  
>> +void trailer_conf_set(enum trailer_where where,
>> +		      enum trailer_if_exists if_exists,
>> +		      enum trailer_if_missing if_missing,
>> +		      struct trailer_conf *conf)
>> +{
>> +	if (where != WHERE_DEFAULT)
>> +		conf->where = where;
>> +	if (if_exists != EXISTS_DEFAULT)
>> +		conf->if_exists = if_exists;
>> +	if (if_missing != MISSING_DEFAULT)
>> +		conf->if_missing = if_missing;
>> +}
>
> So, this is such a helper function. It is curious that it
> deliberately lacks the ability to reset what has already been
> configured back to the default.
>
> For example, we earlier saw this original code ...
>
>> -	item = xmalloc(sizeof(*item));
>> -	item->text = arg;
>> -	item->where = where;
>> -	item->if_exists = if_exists;
>> -	item->if_missing = if_missing;
>
> ... gets replaced with this call.
>
>> +	struct trailer_conf *conf_current = new_trailer_conf();
>> ...
>> +		trailer_conf_set(where, if_exists, if_missing, conf_current);
>
> For this conversion to be correct, we require that the value of the
> *_DEFAULT macro to be 0 and/or these three values getting assigned
> are not *_DEFAULT values.  Maybe that may happen to be the case in
> today's code, but such an unwritten assumption makes me feel nervous.
>
> I am not sure if it is worth the confusion factor to make a function
> whose name is $anything_set() to be making a conditional assignment.
> If such a conditional assignment *also* happens often and deserves
> to have its own helper, it is fine to have such a helper for
> conditional assignment, but calling it $anything_set() is probably a
> mistake.

Agreed. I've split this out into separate helpers that do not have
conditionals built into them.

> Other than that, the main thrust of this step seems sensible.

Ack.

Overall, for the next reroll I have the commits broken down and it is
mostly the same. I just need to wait for CI to pass and update the cover
letter, before sending it (as v4). Should be ready later tonight.
Thanks.
diff mbox series

Patch

diff --git a/builtin/interpret-trailers.c b/builtin/interpret-trailers.c
index 9f0ba39b317..9a902012912 100644
--- a/builtin/interpret-trailers.c
+++ b/builtin/interpret-trailers.c
@@ -45,23 +45,17 @@  static int option_parse_if_missing(const struct option *opt,
 	return trailer_set_if_missing(opt->value, arg);
 }
 
-static void free_new_trailers(struct list_head *trailers)
-{
-	struct list_head *pos, *tmp;
-	struct new_trailer_item *item;
-
-	list_for_each_safe(pos, tmp, trailers) {
-		item = list_entry(pos, struct new_trailer_item, list);
-		list_del(pos);
-		free(item);
-	}
-}
+static char *cl_separators;
 
 static int option_parse_trailer(const struct option *opt,
 				   const char *arg, int unset)
 {
 	struct list_head *trailers = opt->value;
-	struct new_trailer_item *item;
+	struct strbuf tok = STRBUF_INIT;
+	struct strbuf val = STRBUF_INIT;
+	const struct trailer_conf *conf;
+	struct trailer_conf *conf_current = new_trailer_conf();
+	ssize_t separator_pos;
 
 	if (unset) {
 		free_new_trailers(trailers);
@@ -71,12 +65,31 @@  static int option_parse_trailer(const struct option *opt,
 	if (!arg)
 		return -1;
 
-	item = xmalloc(sizeof(*item));
-	item->text = arg;
-	item->where = where;
-	item->if_exists = if_exists;
-	item->if_missing = if_missing;
-	list_add_tail(&item->list, trailers);
+	separator_pos = find_separator(arg, cl_separators);
+	if (separator_pos) {
+		parse_trailer(arg, separator_pos, &tok, &val, &conf);
+		duplicate_trailer_conf(conf_current, conf);
+
+		/*
+		 * Override conf_current with settings specified via CLI flags.
+		 */
+		trailer_conf_set(where, if_exists, if_missing, conf_current);
+
+		trailer_add_arg_item(strbuf_detach(&tok, NULL),
+				     strbuf_detach(&val, NULL),
+				     conf_current,
+				     trailers);
+	} else {
+		struct strbuf sb = STRBUF_INIT;
+		strbuf_addstr(&sb, arg);
+		strbuf_trim(&sb);
+		error(_("empty trailer token in trailer '%.*s'"),
+			(int) sb.len, sb.buf);
+		strbuf_release(&sb);
+	}
+
+	free(conf_current);
+
 	return 0;
 }
 
@@ -135,7 +148,7 @@  static void read_input_file(struct strbuf *sb, const char *file)
 }
 
 static void interpret_trailers(const struct process_trailer_options *opts,
-			       struct list_head *new_trailer_head,
+			       struct list_head *arg_trailers,
 			       const char *file)
 {
 	LIST_HEAD(head);
@@ -144,8 +157,6 @@  static void interpret_trailers(const struct process_trailer_options *opts,
 	struct trailer_block *trailer_block;
 	FILE *outfile = stdout;
 
-	trailer_config_init();
-
 	read_input_file(&sb, file);
 
 	if (opts->in_place)
@@ -162,12 +173,7 @@  static void interpret_trailers(const struct process_trailer_options *opts,
 
 
 	if (!opts->only_input) {
-		LIST_HEAD(config_head);
-		LIST_HEAD(arg_head);
-		parse_trailers_from_config(&config_head);
-		parse_trailers_from_command_line_args(&arg_head, new_trailer_head);
-		list_splice(&config_head, &arg_head);
-		process_trailers_lists(&head, &arg_head);
+		process_trailers_lists(&head, arg_trailers);
 	}
 
 	/* Print trailer block. */
@@ -193,7 +199,8 @@  static void interpret_trailers(const struct process_trailer_options *opts,
 int cmd_interpret_trailers(int argc, const char **argv, const char *prefix)
 {
 	struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT;
-	LIST_HEAD(trailers);
+	LIST_HEAD(configured_trailers);
+	LIST_HEAD(arg_trailers);
 
 	struct option options[] = {
 		OPT_BOOL(0, "in-place", &opts.in_place, N_("edit files in place")),
@@ -212,33 +219,48 @@  int cmd_interpret_trailers(int argc, const char **argv, const char *prefix)
 		OPT_CALLBACK_F(0, "parse", &opts, NULL, N_("alias for --only-trailers --only-input --unfold"),
 			PARSE_OPT_NOARG | PARSE_OPT_NONEG, parse_opt_parse),
 		OPT_BOOL(0, "no-divider", &opts.no_divider, N_("do not treat \"---\" as the end of input")),
-		OPT_CALLBACK(0, "trailer", &trailers, N_("trailer"),
+		OPT_CALLBACK(0, "trailer", &arg_trailers, N_("trailer"),
 				N_("trailer(s) to add"), option_parse_trailer),
 		OPT_END()
 	};
 
 	git_config(git_default_config, NULL);
+	trailer_config_init();
+
+	if (!opts.only_input) {
+		parse_trailers_from_config(&configured_trailers);
+	}
+
+	/*
+	* In command-line arguments, '=' is accepted (in addition to the
+	* separators that are defined).
+	*/
+	cl_separators = xstrfmt("=%s", trailer_default_separators());
 
 	argc = parse_options(argc, argv, prefix, options,
 			     git_interpret_trailers_usage, 0);
 
-	if (opts.only_input && !list_empty(&trailers))
+	free(cl_separators);
+
+	if (opts.only_input && !list_empty(&arg_trailers))
 		usage_msg_opt(
 			_("--trailer with --only-input does not make sense"),
 			git_interpret_trailers_usage,
 			options);
 
+	list_splice(&configured_trailers, &arg_trailers);
+
 	if (argc) {
 		int i;
 		for (i = 0; i < argc; i++)
-			interpret_trailers(&opts, &trailers, argv[i]);
+			interpret_trailers(&opts, &arg_trailers, argv[i]);
 	} else {
 		if (opts.in_place)
 			die(_("no input file given for in-place editing"));
-		interpret_trailers(&opts, &trailers, NULL);
+		interpret_trailers(&opts, &arg_trailers, NULL);
 	}
 
-	free_new_trailers(&trailers);
+	free_new_trailers(&arg_trailers);
 
 	return 0;
 }
diff --git a/trailer.c b/trailer.c
index c16f552b463..19637ff295d 100644
--- a/trailer.c
+++ b/trailer.c
@@ -66,6 +66,11 @@  static LIST_HEAD(conf_head);
 
 static char *separators = ":";
 
+const char *trailer_default_separators(void)
+{
+	return separators;
+}
+
 static int configured;
 
 #define TRAILER_ARG_STRING "$ARG"
@@ -424,6 +429,25 @@  int trailer_set_if_missing(enum trailer_if_missing *item, const char *value)
 	return 0;
 }
 
+void trailer_conf_set(enum trailer_where where,
+		      enum trailer_if_exists if_exists,
+		      enum trailer_if_missing if_missing,
+		      struct trailer_conf *conf)
+{
+	if (where != WHERE_DEFAULT)
+		conf->where = where;
+	if (if_exists != EXISTS_DEFAULT)
+		conf->if_exists = if_exists;
+	if (if_missing != MISSING_DEFAULT)
+		conf->if_missing = if_missing;
+}
+
+struct trailer_conf *new_trailer_conf(void)
+{
+	struct trailer_conf *new = xcalloc(1, sizeof(*new));
+	return new;
+}
+
 void duplicate_trailer_conf(struct trailer_conf *dst,
 			    const struct trailer_conf *src)
 {
@@ -642,6 +666,9 @@  ssize_t find_separator(const char *line, const char *separators)
 /*
  * Obtain the token, value, and conf from the given trailer.
  *
+ * The conf needs special handling. We first read hardcoded defaults, and
+ * override them if we find a matching trailer configuration.
+ *
  * separator_pos must not be 0, since the token cannot be an empty string.
  *
  * If separator_pos is -1, interpret the whole trailer as a token.
@@ -691,22 +718,13 @@  static struct trailer_item *add_trailer_item(struct list_head *head, char *tok,
 	return new_item;
 }
 
-static void trailer_add_arg_item(struct list_head *arg_head, char *tok, char *val,
-				 const struct trailer_conf *conf,
-				 const struct new_trailer_item *new_trailer_item)
+void trailer_add_arg_item(char *tok, char *val, const struct trailer_conf *conf,
+			  struct list_head *arg_head)
 {
 	struct arg_item *new_item = xcalloc(1, sizeof(*new_item));
 	new_item->token = tok;
 	new_item->value = val;
 	duplicate_trailer_conf(&new_item->conf, conf);
-	if (new_trailer_item) {
-		if (new_trailer_item->where != WHERE_DEFAULT)
-			new_item->conf.where = new_trailer_item->where;
-		if (new_trailer_item->if_exists != EXISTS_DEFAULT)
-			new_item->conf.if_exists = new_trailer_item->if_exists;
-		if (new_trailer_item->if_missing != MISSING_DEFAULT)
-			new_item->conf.if_missing = new_trailer_item->if_missing;
-	}
 	list_add_tail(&new_item->list, arg_head);
 }
 
@@ -719,10 +737,10 @@  void parse_trailers_from_config(struct list_head *config_head)
 	list_for_each(pos, &conf_head) {
 		item = list_entry(pos, struct arg_item, list);
 		if (item->conf.command)
-			trailer_add_arg_item(config_head,
-					     xstrdup(token_from_item(item, NULL)),
+			trailer_add_arg_item(xstrdup(token_from_item(item, NULL)),
 					     xstrdup(""),
-					     &item->conf, NULL);
+					     &item->conf,
+					     config_head);
 	}
 }
 
@@ -755,10 +773,10 @@  void parse_trailers_from_command_line_args(struct list_head *arg_head,
 			strbuf_release(&sb);
 		} else {
 			parse_trailer(tr->text, separator_pos, &tok, &val, &conf);
-			trailer_add_arg_item(arg_head,
-					     strbuf_detach(&tok, NULL),
+			trailer_add_arg_item(strbuf_detach(&tok, NULL),
 					     strbuf_detach(&val, NULL),
-					     conf, tr);
+					     conf,
+					     arg_head);
 		}
 	}
 
@@ -1148,6 +1166,16 @@  void free_trailers(struct list_head *trailers)
 	}
 }
 
+void free_new_trailers(struct list_head *trailers)
+{
+	struct list_head *pos, *p;
+
+	list_for_each_safe(pos, p, trailers) {
+		list_del(pos);
+		free_arg_item(list_entry(pos, struct arg_item, list));
+	}
+}
+
 size_t trailer_block_start(struct trailer_block *trailer_block)
 {
 	return trailer_block->start;
diff --git a/trailer.h b/trailer.h
index d724263e4f6..8fcf1969a3c 100644
--- a/trailer.h
+++ b/trailer.h
@@ -46,9 +46,20 @@  struct new_trailer_item {
 	enum trailer_if_missing if_missing;
 };
 
+void trailer_conf_set(enum trailer_where where,
+		      enum trailer_if_exists if_exists,
+		      enum trailer_if_missing if_missing,
+		      struct trailer_conf *conf);
+
+struct trailer_conf *new_trailer_conf(void);
 void duplicate_trailer_conf(struct trailer_conf *dst,
 			    const struct trailer_conf *src);
 
+const char *trailer_default_separators(void);
+
+void trailer_add_arg_item(char *tok, char *val, const struct trailer_conf *conf,
+			  struct list_head *arg_head);
+
 struct process_trailer_options {
 	int in_place;
 	int trim_empty;
@@ -95,6 +106,7 @@  void format_trailers(const struct process_trailer_options *opts,
 		     struct list_head *trailers,
 		     struct strbuf *out);
 void free_trailers(struct list_head *);
+void free_new_trailers(struct list_head *);
 
 /*
  * Convenience function to format the trailers from the commit msg "msg" into