commit: support custom user-specified headers

Some clients (eg GitButler) are storing additional information in custom
user-specified commit headers. We should make this a first-class
concept.
This commit is contained in:
Edward Thomson
2026-04-28 12:55:37 +01:00
parent cf02e92b54
commit a7786fcd7e
3 changed files with 109 additions and 29 deletions

View File

@@ -363,6 +363,11 @@ GIT_EXTERN(int) git_commit_create_from_stage(
const char *message,
const git_commit_create_options *opts);
/** The field name and value for a custom commit header entry. */
typedef struct {
const char *field;
const char *value;
} git_commit_header;
typedef struct {
unsigned int version;
@@ -380,6 +385,13 @@ typedef struct {
/** Encoding for the commit message; leave NULL for default. */
const char *message_encoding;
/**
* Extra headers can be specified as an array of field name and
* value pairs.
*/
const git_commit_header *extra_headers;
size_t extra_headers_len; /**< Number of extra headers */
} git_commit_create_ext_options;
/** Current version for the `git_commit_create_ext_options` structure */

View File

@@ -42,6 +42,32 @@ void git_commit__free(void *_commit)
git__free(commit);
}
/**
* Append to 'out' properly marking continuations when there's a newline in 'content'
*/
static int format_header_field(git_str *out, const char *field, const char *content)
{
const char *lf;
GIT_ASSERT_ARG(out);
GIT_ASSERT_ARG(field);
GIT_ASSERT_ARG(content);
git_str_puts(out, field);
git_str_putc(out, ' ');
while ((lf = strchr(content, '\n')) != NULL) {
git_str_put(out, content, lf - content);
git_str_puts(out, "\n ");
content = lf + 1;
}
git_str_puts(out, content);
git_str_putc(out, '\n');
return git_str_oom(out) ? -1 : 0;
}
static int git_commit__create_buffer_internal(
git_str *out,
const git_signature *author,
@@ -69,8 +95,20 @@ static int git_commit__create_buffer_internal(
git_signature__writebuf(out, "author ", author);
git_signature__writebuf(out, "committer ", committer);
if (opts && opts->message_encoding != NULL)
git_str_printf(out, "encoding %s\n", opts->message_encoding);
if (opts && opts->message_encoding != NULL) {
if (format_header_field(out, "encoding",
opts->message_encoding) < 0)
goto on_error;
}
if (opts && opts->extra_headers_len) {
for (i = 0; i < opts->extra_headers_len; i++) {
if (format_header_field(out,
opts->extra_headers[i].field,
opts->extra_headers[i].value) < 0)
goto on_error;
}
}
git_str_putc(out, '\n');
@@ -1026,38 +1064,11 @@ int git_commit__create_buffer(
return error;
}
/**
* Append to 'out' properly marking continuations when there's a newline in 'content'
*/
static int format_header_field(git_str *out, const char *field, const char *content)
{
const char *lf;
GIT_ASSERT_ARG(out);
GIT_ASSERT_ARG(field);
GIT_ASSERT_ARG(content);
git_str_puts(out, field);
git_str_putc(out, ' ');
while ((lf = strchr(content, '\n')) != NULL) {
git_str_put(out, content, lf - content);
git_str_puts(out, "\n ");
content = lf + 1;
}
git_str_puts(out, content);
git_str_putc(out, '\n');
return git_str_oom(out) ? -1 : 0;
}
static const git_oid *commit_parent_from_commit(size_t n, void *payload)
{
const git_commit *commit = (const git_commit *) payload;
return git_array_get(commit->parent_ids, n);
}
int git_commit_create_with_signature(

View File

@@ -422,3 +422,60 @@ a simple commit which works\n";
git_odb_object_free(obj);
git_odb_free(odb);
}
void test_commit_write__can_add_extra_headers(void)
{
git_odb *odb;
git_signature *author, *committer;
git_oid tree_id, parent_id, commit_id;
git_commit *parent;
git_tree *tree;
git_odb_object *obj;
git_commit_create_ext_options opts = GIT_COMMIT_CREATE_EXT_OPTIONS_INIT;
const git_commit_header extra_headers[] = {
{ "line_one", "First extra header" },
{ "line_two", "Second extra header" }
};
const char *expected = "tree 1810dff58d8a660512d4832e740f692884338ccd\n\
parent 8496071c1b46c854b31185ea97743be6a8774479\n\
author Vicent Marti <vicent@github.com> 987654321 +0130\n\
committer Vicent Marti <vicent@github.com> 123456789 +0100\n\
line_one First extra header\n\
line_two Second extra header\n\
\n\
This is a fun new commit.";
cl_git_pass(git_signature_new(
&author, committer_name, committer_email, 987654321, 90));
cl_git_pass(git_signature_new(
&committer, committer_name, committer_email, 123456789, 60));
/* this is a valid tree and parent */
git_oid_from_string(&tree_id, tree_id_str, GIT_OID_SHA1);
cl_git_pass(git_tree_lookup(&tree, g_repo, &tree_id));
git_oid_from_string(&parent_id, parent_id_str, GIT_OID_SHA1);
cl_git_pass(git_commit_lookup(&parent, g_repo, &parent_id));
opts.extra_headers = extra_headers;
opts.extra_headers_len = 2;
cl_git_pass(git_commit_create_ext(
&commit_id, g_repo, author, committer,
"This is a fun new commit.",
tree, 1, &parent, &opts));
cl_git_pass(git_repository_odb(&odb, g_repo));
cl_git_pass(git_odb_read(&obj, odb, &commit_id));
cl_assert_equal_s(expected, git_odb_object_data(obj));
git_odb_object_free(obj);
git_tree_free(tree);
git_commit_free(parent);
git_commit_free(commit);
git_signature_free(committer);
git_signature_free(author);
git_odb_free(odb);
}