commit: introduce commit creation options

Introduce `git_commit_create_ext` and an options structure for
extensibility. Non-default arguments (message encoding, reference
updates) are in the options structure.

This simplifies the complex self-service commit creation APIs and allows
for future sustainable improvements.
This commit is contained in:
Edward Thomson
2026-04-27 14:11:37 +01:00
parent a7e1eb2638
commit cf02e92b54
2 changed files with 183 additions and 79 deletions

View File

@@ -316,6 +316,115 @@ GIT_EXTERN(int) git_commit_header_field(git_buf *out, const git_commit *commit,
*/
GIT_EXTERN(int) git_commit_extract_signature(git_buf *signature, git_buf *signed_data, git_repository *repo, git_oid *commit_id, const char *field);
typedef struct {
unsigned int version;
/**
* Flags for creating the commit.
*
* If `allow_empty_commit` is specified, a commit with no changes
* from the prior commit (an "empty" commit) is allowed. Otherwise,
* commit creation will be stopped.
*/
unsigned int allow_empty_commit : 1;
/** The commit author, or NULL for the default. */
const git_signature *author;
/** The committer, or NULL for the default. */
const git_signature *committer;
/** Encoding for the commit message; leave NULL for default. */
const char *message_encoding;
} git_commit_create_options;
/** Current version for the `git_commit_create_options` structure */
#define GIT_COMMIT_CREATE_OPTIONS_VERSION 1
/** Static constructor for `git_commit_create_options` */
#define GIT_COMMIT_CREATE_OPTIONS_INIT { GIT_COMMIT_CREATE_OPTIONS_VERSION }
/**
* Commits the staged changes in the repository; this is a near analog to
* `git commit -m message`.
*
* By default, empty commits are not allowed.
*
* @param id pointer to store the new commit's object id
* @param repo repository to commit changes in
* @param message the commit message
* @param opts options for creating the commit
* @return 0 on success, GIT_EUNCHANGED if there were no changes to commit, or an error code
*/
GIT_EXTERN(int) git_commit_create_from_stage(
git_oid *id,
git_repository *repo,
const char *message,
const git_commit_create_options *opts);
typedef struct {
unsigned int version;
/**
* If not NULL, the name of the reference that will be updated
* to point to this commit. If the reference is not direct, it
* will be resolved to a direct reference. Use `HEAD` to update
* the `HEAD` of the current branch and make it point to this
* commit. If the reference doesn't exist yet, it will be created.
* If it does exist, the first parent must be the tip of this
* branch.
*/
const char *update_ref;
/** Encoding for the commit message; leave NULL for default. */
const char *message_encoding;
} git_commit_create_ext_options;
/** Current version for the `git_commit_create_ext_options` structure */
#define GIT_COMMIT_CREATE_EXT_OPTIONS_VERSION 1
/** Static constructor for `git_commit_create_ext_options` */
#define GIT_COMMIT_CREATE_EXT_OPTIONS_INIT { GIT_COMMIT_CREATE_EXT_OPTIONS_VERSION }
/**
* Create a new commit object and write it to the object database.
*
* Note that the message will _not_ be cleaned up automatically. You
* may wish to use the `git_message_prettify` API to clean it up.
*
* This API provides the most control for generating commit objects;
* this is useful if (for example) you have a tree object already.
* If you have staged changes, and just want to emulate `git commit`
* on the command line, then `git_commit_create_from_stage` may be a
* simpler option.
*
* @param id_out pointer to store the new commit's object id
* @param repo repository to store the commit in
* @param author The name of the author and time of authorship
* @param committer The name of the committer and time of commit
* @param message Full message for this commit
* @param tree The `git_tree` that will be used as the tree for this commit
* @param parent_count Number of parents for this commit
* @param parents Array pointers to `git_commit` objects that will be used
* as parents for this commit.
* @param opts Additional options for creating this commit,
* or `NULL` for defaults
* @return 0 on success or an error code
*/
GIT_EXTERN(int) git_commit_create_ext(
git_oid *id_out,
git_repository *repo,
const git_signature *author,
const git_signature *committer,
const char *message,
const git_tree *tree,
size_t parent_count,
git_commit * const parents[],
const git_commit_create_ext_options *opts);
/**
* Create new commit in the repository from a list of `git_object` pointers
*
@@ -429,52 +538,6 @@ GIT_EXTERN(int) git_commit_create_v(
size_t parent_count,
...);
typedef struct {
unsigned int version;
/**
* Flags for creating the commit.
*
* If `allow_empty_commit` is specified, a commit with no changes
* from the prior commit (and "empty" commit) is allowed. Otherwise,
* commit creation will be stopped.
*/
unsigned int allow_empty_commit : 1;
/** The commit author, or NULL for the default. */
const git_signature *author;
/** The committer, or NULL for the default. */
const git_signature *committer;
/** Encoding for the commit message; leave NULL for default. */
const char *message_encoding;
} git_commit_create_options;
/** Current version for the `git_commit_create_options` structure */
#define GIT_COMMIT_CREATE_OPTIONS_VERSION 1
/** Static constructor for `git_commit_create_options` */
#define GIT_COMMIT_CREATE_OPTIONS_INIT { GIT_COMMIT_CREATE_OPTIONS_VERSION }
/**
* Commits the staged changes in the repository; this is a near analog to
* `git commit -m message`.
*
* By default, empty commits are not allowed.
*
* @param id pointer to store the new commit's object id
* @param repo repository to commit changes in
* @param message the commit message
* @param opts options for creating the commit
* @return 0 on success, GIT_EUNCHANGED if there were no changes to commit, or an error code
*/
GIT_EXTERN(int) git_commit_create_from_stage(
git_oid *id,
git_repository *repo,
const char *message,
const git_commit_create_options *opts);
/**
* Amend an existing commit by replacing only non-NULL values.
*

View File

@@ -46,10 +46,10 @@ static int git_commit__create_buffer_internal(
git_str *out,
const git_signature *author,
const git_signature *committer,
const char *message_encoding,
const char *message,
const git_oid *tree,
git_array_oid_t *parents)
git_array_oid_t *parents,
const git_commit_create_ext_options *opts)
{
size_t i = 0;
const git_oid *parent;
@@ -69,8 +69,8 @@ static int git_commit__create_buffer_internal(
git_signature__writebuf(out, "author ", author);
git_signature__writebuf(out, "committer ", committer);
if (message_encoding != NULL)
git_str_printf(out, "encoding %s\n", message_encoding);
if (opts && opts->message_encoding != NULL)
git_str_printf(out, "encoding %s\n", opts->message_encoding);
git_str_putc(out, '\n');
@@ -126,14 +126,13 @@ on_error:
static int git_commit__create_internal(
git_oid *id,
git_repository *repo,
const char *update_ref,
const git_signature *author,
const git_signature *committer,
const char *message_encoding,
const char *message,
const git_oid *tree,
git_commit_parent_callback parent_cb,
void *parent_payload,
const git_commit_create_ext_options *opts,
bool validate)
{
int error;
@@ -143,8 +142,10 @@ static int git_commit__create_internal(
const git_oid *current_id = NULL;
git_array_oid_t parents = GIT_ARRAY_INIT;
if (update_ref) {
error = git_reference_lookup_resolved(&ref, repo, update_ref, 10);
if (opts && opts->update_ref) {
error = git_reference_lookup_resolved(&ref,
repo, opts->update_ref, 10);
if (error < 0 && error != GIT_ENOTFOUND)
return error;
}
@@ -156,9 +157,8 @@ static int git_commit__create_internal(
if ((error = validate_tree_and_parents(&parents, repo, tree, parent_cb, parent_payload, current_id, validate)) < 0)
goto cleanup;
error = git_commit__create_buffer_internal(&buf, author, committer,
message_encoding, message, tree,
&parents);
error = git_commit__create_buffer_internal(&buf,
author, committer, message, tree, &parents, opts);
if (error < 0)
goto cleanup;
@@ -173,9 +173,9 @@ static int git_commit__create_internal(
goto cleanup;
if (update_ref != NULL) {
if (opts && opts->update_ref != NULL) {
error = git_reference__update_for_commit(
repo, ref, update_ref, id, "commit");
repo, ref, opts->update_ref, id, "commit");
goto cleanup;
}
@@ -198,9 +198,14 @@ int git_commit_create_from_callback(
git_commit_parent_callback parent_cb,
void *parent_payload)
{
git_commit_create_ext_options opts = GIT_COMMIT_CREATE_EXT_OPTIONS_INIT;
opts.update_ref = update_ref;
opts.message_encoding = message_encoding;
return git_commit__create_internal(
id, repo, update_ref, author, committer, message_encoding, message,
tree, parent_cb, parent_payload, true);
id, repo, author, committer, message,
tree, parent_cb, parent_payload, &opts, true);
}
typedef struct {
@@ -230,8 +235,9 @@ int git_commit_create_v(
size_t parent_count,
...)
{
int error = 0;
git_commit_create_ext_options opts = GIT_COMMIT_CREATE_EXT_OPTIONS_INIT;
commit_parent_varargs data;
int error = 0;
GIT_ASSERT_ARG(tree);
GIT_ASSERT_ARG(git_tree_owner(tree) == repo);
@@ -239,10 +245,12 @@ int git_commit_create_v(
data.total = parent_count;
va_start(data.args, parent_count);
opts.update_ref = update_ref;
opts.message_encoding = message_encoding;
error = git_commit__create_internal(
id, repo, update_ref, author, committer,
message_encoding, message, git_tree_id(tree),
commit_parent_from_varargs, &data, false);
id, repo, author, committer, message, git_tree_id(tree),
commit_parent_from_varargs, &data, &opts, false);
va_end(data.args);
return error;
@@ -271,17 +279,20 @@ int git_commit_create_from_ids(
size_t parent_count,
const git_oid *parents[])
{
git_commit_create_ext_options opts = GIT_COMMIT_CREATE_EXT_OPTIONS_INIT;
commit_parent_oids data = { parent_count, parents };
opts.update_ref = update_ref;
opts.message_encoding = message_encoding;
return git_commit__create_internal(
id, repo, update_ref, author, committer,
message_encoding, message, tree,
commit_parent_from_ids, &data, true);
id, repo, author, committer, message, tree,
commit_parent_from_ids, &data, &opts, true);
}
typedef struct {
size_t total;
const git_commit **parents;
git_commit * const *parents;
git_repository *repo;
} commit_parent_data;
@@ -297,6 +308,27 @@ static const git_oid *commit_parent_from_array(size_t curr, void *payload)
return git_commit_id(commit);
}
int git_commit_create_ext(
git_oid *id,
git_repository *repo,
const git_signature *author,
const git_signature *committer,
const char *message,
const git_tree *tree,
size_t parent_count,
git_commit * const parents[],
const git_commit_create_ext_options *opts)
{
commit_parent_data data = { parent_count, parents, repo };
GIT_ASSERT_ARG(tree);
GIT_ASSERT_ARG(git_tree_owner(tree) == repo);
return git_commit__create_internal(
id, repo, author, committer, message, git_tree_id(tree),
commit_parent_from_array, &data, opts, false);
}
int git_commit_create(
git_oid *id,
git_repository *repo,
@@ -309,15 +341,18 @@ int git_commit_create(
size_t parent_count,
const git_commit *parents[])
{
commit_parent_data data = { parent_count, parents, repo };
git_commit_create_ext_options opts = GIT_COMMIT_CREATE_EXT_OPTIONS_INIT;
commit_parent_data data = { parent_count, (git_commit * const *)parents, repo };
GIT_ASSERT_ARG(tree);
GIT_ASSERT_ARG(git_tree_owner(tree) == repo);
opts.update_ref = update_ref;
opts.message_encoding = message_encoding;
return git_commit__create_internal(
id, repo, update_ref, author, committer,
message_encoding, message, git_tree_id(tree),
commit_parent_from_array, &data, false);
id, repo, author, committer, message, git_tree_id(tree),
commit_parent_from_array, &data, &opts, false);
}
static const git_oid *commit_parent_for_amend(size_t curr, void *payload)
@@ -341,6 +376,7 @@ int git_commit_amend(
git_repository *repo;
git_oid tree_id;
git_reference *ref = NULL;
git_commit_create_ext_options opts = GIT_COMMIT_CREATE_EXT_OPTIONS_INIT;
int error;
GIT_ASSERT_ARG(id);
@@ -378,9 +414,12 @@ int git_commit_amend(
}
}
opts.message_encoding = message_encoding;
error = git_commit__create_internal(
id, repo, NULL, author, committer, message_encoding, message,
&tree_id, commit_parent_for_amend, (void *)commit_to_amend, false);
id, repo, author, committer, message, &tree_id,
commit_parent_for_amend, (void *)commit_to_amend,
&opts, false);
if (!error && update_ref) {
error = git_reference__update_for_commit(
@@ -964,9 +1003,10 @@ int git_commit__create_buffer(
const git_commit *parents[])
{
int error;
commit_parent_data data = { parent_count, parents, repo };
commit_parent_data data = { parent_count, (git_commit * const *)parents, repo };
git_array_oid_t parents_arr = GIT_ARRAY_INIT;
const git_oid *tree_id;
git_commit_create_ext_options opts = GIT_COMMIT_CREATE_EXT_OPTIONS_INIT;
GIT_ASSERT_ARG(tree);
GIT_ASSERT_ARG(git_tree_owner(tree) == repo);
@@ -976,10 +1016,11 @@ int git_commit__create_buffer(
if ((error = validate_tree_and_parents(&parents_arr, repo, tree_id, commit_parent_from_array, &data, NULL, true)) < 0)
return error;
opts.message_encoding = message_encoding;
error = git_commit__create_buffer_internal(
out, author, committer,
message_encoding, message, tree_id,
&parents_arr);
out, author, committer, message, tree_id,
&parents_arr, &opts);
git_array_clear(parents_arr);
return error;