commit: split creating the commit and writing it out

Sometimes you want to create a commit but not write it out to the
objectdb immediately. For these cases, provide a new function to
retrieve the buffer instead of having to go through the db.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43476b9..21f972d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@
 
 ### API additions
 
+* `git_commit_create_buffer()` creates a commit and writes it into a
+  user-provided buffer instead of writing it into the object db.
+
 ### API removals
 
 ### Breaking API changes
diff --git a/include/git2/commit.h b/include/git2/commit.h
index 3488c74..44ea888 100644
--- a/include/git2/commit.h
+++ b/include/git2/commit.h
@@ -394,6 +394,52 @@
 	const char *message,
 	const git_tree *tree);
 
+/**
+ * Create a commit and write it into a buffer
+ *
+ * Create a commit as with `git_commit_create()` but instead of
+ * writing it to the objectdb, write the contents of the object into a
+ * buffer.
+ *
+ * @param out the buffer into which to write the commit object content
+ *
+ * @param repo Repository where the referenced tree and parents live
+ *
+ * @param author Signature with author and author time of commit
+ *
+ * @param committer Signature with committer and * commit time of commit
+ *
+ * @param message_encoding The encoding for the message in the
+ *  commit, represented with a standard encoding name.
+ *  E.g. "UTF-8". If NULL, no encoding header is written and
+ *  UTF-8 is assumed.
+ *
+ * @param message Full message for this commit
+ *
+ * @param tree An instance of a `git_tree` object that will
+ *  be used as the tree for the commit. This tree object must
+ *  also be owned by the given `repo`.
+ *
+ * @param parent_count Number of parents for this commit
+ *
+ * @param parents Array of `parent_count` pointers to `git_commit`
+ *  objects that will be used as the parents for this commit. This
+ *  array may be NULL if `parent_count` is 0 (root commit). All the
+ *  given commits must be owned by the `repo`.
+ *
+ * @return 0 or an error code
+ */
+GIT_EXTERN(int) git_commit_create_buffer(
+	git_buf *out,
+	git_repository *repo,
+	const git_signature *author,
+	const git_signature *committer,
+	const char *message_encoding,
+	const char *message,
+	const git_tree *tree,
+	size_t parent_count,
+	const git_commit *parents[]);
+
 /** @} */
 GIT_END_DECL
 #endif
diff --git a/src/commit.c b/src/commit.c
index 685c642..9d675ac 100644
--- a/src/commit.c
+++ b/src/commit.c
@@ -18,6 +18,7 @@
 #include "message.h"
 #include "refs.h"
 #include "object.h"
+#include "oidarray.h"
 
 void git_commit__free(void *_commit)
 {
@@ -37,6 +38,85 @@
 	git__free(commit);
 }
 
+static int git_commit__create_buffer_internal(
+	git_buf *out,
+	git_repository *repo,
+	const git_signature *author,
+	const git_signature *committer,
+	const char *message_encoding,
+	const char *message,
+	const git_oid *tree,
+	git_array_oid_t *parents)
+{
+	size_t i = 0;
+	const git_oid *parent;
+
+	assert(out && repo && tree);
+
+	git_oid__writebuf(out, "tree ", tree);
+
+	for (i = 0; i < git_array_size(*parents); i++) {
+		parent = git_array_get(*parents, i);
+		git_oid__writebuf(out, "parent ", parent);
+	}
+
+	git_signature__writebuf(out, "author ", author);
+	git_signature__writebuf(out, "committer ", committer);
+
+	if (message_encoding != NULL)
+		git_buf_printf(out, "encoding %s\n", message_encoding);
+
+	git_buf_putc(out, '\n');
+
+	if (git_buf_puts(out, message) < 0)
+		goto on_error;
+
+	return 0;
+
+on_error:
+	git_buf_free(out);
+	return -1;
+}
+
+static int validate_tree_and_parents(git_array_oid_t *parents, git_repository *repo, const git_oid *tree,
+				     git_commit_parent_callback parent_cb, void *parent_payload,
+				     const git_oid *current_id, bool validate)
+{
+	size_t i;
+	int error;
+	git_oid *parent_cpy;
+	const git_oid *parent;
+
+	if (validate && !git_object__is_valid(repo, tree, GIT_OBJ_TREE))
+		return -1;
+
+	i = 0;
+	while ((parent = parent_cb(i, parent_payload)) != NULL) {
+		if (validate && !git_object__is_valid(repo, parent, GIT_OBJ_COMMIT)) {
+			error = -1;
+			goto on_error;
+		}
+
+		parent_cpy = git_array_alloc(*parents);
+		GITERR_CHECK_ALLOC(parent_cpy);
+
+		git_oid_cpy(parent_cpy, parent);
+		i++;
+	}
+
+	if (current_id && git_oid_cmp(current_id, git_array_get(*parents, 0))) {
+		giterr_set(GITERR_OBJECT, "failed to create commit: current tip is not the first parent");
+		error = GIT_EMODIFIED;
+		goto on_error;
+	}
+
+	return 0;
+
+on_error:
+	git_array_clear(*parents);
+	return error;
+}
+
 static int git_commit__create_internal(
 	git_oid *id,
 	git_repository *repo,
@@ -50,18 +130,12 @@
 	void *parent_payload,
 	bool validate)
 {
-	git_reference *ref = NULL;
-	int error = 0, matched_parent = 0;
-	const git_oid *current_id = NULL;
-	git_buf commit = GIT_BUF_INIT;
-	size_t i = 0;
+	int error;
 	git_odb *odb;
-	const git_oid *parent;
-
-	assert(id && repo && tree && parent_cb);
-
-	if (validate && !git_object__is_valid(repo, tree, GIT_OBJ_TREE))
-		return -1;
+	git_reference *ref = NULL;
+	git_buf buf = GIT_BUF_INIT;
+	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);
@@ -73,58 +147,34 @@
 	if (ref)
 		current_id = git_reference_target(ref);
 
-	git_oid__writebuf(&commit, "tree ", tree);
+	if ((error = validate_tree_and_parents(&parents, repo, tree, parent_cb, parent_payload, current_id, validate)) < 0)
+		goto cleanup;
 
-	while ((parent = parent_cb(i, parent_payload)) != NULL) {
-		if (validate && !git_object__is_valid(repo, parent, GIT_OBJ_COMMIT)) {
-			error = -1;
-			goto on_error;
-		}
+	error = git_commit__create_buffer_internal(&buf, repo, author, committer,
+						   message_encoding, message, tree,
+						   &parents);
 
-		git_oid__writebuf(&commit, "parent ", parent);
-		if (i == 0 && current_id && git_oid_equal(current_id, parent))
-			matched_parent = 1;
-		i++;
-	}
-
-	if (ref && !matched_parent) {
-		git_reference_free(ref);
-		git_buf_free(&commit);
-		giterr_set(GITERR_OBJECT, "failed to create commit: current tip is not the first parent");
-		return GIT_EMODIFIED;
-	}
-
-	git_signature__writebuf(&commit, "author ", author);
-	git_signature__writebuf(&commit, "committer ", committer);
-
-	if (message_encoding != NULL)
-		git_buf_printf(&commit, "encoding %s\n", message_encoding);
-
-	git_buf_putc(&commit, '\n');
-
-	if (git_buf_puts(&commit, message) < 0)
-		goto on_error;
+	if (error < 0)
+		goto cleanup;
 
 	if (git_repository_odb__weakptr(&odb, repo) < 0)
-		goto on_error;
+		goto cleanup;
 
-	if (git_odb_write(id, odb, commit.ptr, commit.size, GIT_OBJ_COMMIT) < 0)
-		goto on_error;
+	if (git_odb_write(id, odb, buf.ptr, buf.size, GIT_OBJ_COMMIT) < 0)
+		goto cleanup;
 
-	git_buf_free(&commit);
 
 	if (update_ref != NULL) {
 		error = git_reference__update_for_commit(
 			repo, ref, update_ref, id, "commit");
-		git_reference_free(ref);
-		return error;
+		goto cleanup;
 	}
 
-	return 0;
-
-on_error:
-	git_buf_free(&commit);
-	return -1;
+cleanup:
+	git_array_clear(parents);
+	git_reference_free(ref);
+	git_buf_free(&buf);
+	return error;
 }
 
 int git_commit_create_from_callback(
@@ -739,3 +789,34 @@
 	git_buf_clear(signed_data);
 	return error;
 }
+
+int git_commit_create_buffer(git_buf *out,
+	git_repository *repo,
+	const git_signature *author,
+	const git_signature *committer,
+	const char *message_encoding,
+	const char *message,
+	const git_tree *tree,
+	size_t parent_count,
+	const git_commit *parents[])
+{
+	int error;
+	commit_parent_data data = { parent_count, parents, repo };
+	git_array_oid_t parents_arr = GIT_ARRAY_INIT;
+	const git_oid *tree_id;
+
+	assert(tree && git_tree_owner(tree) == repo);
+
+	tree_id = git_tree_id(tree);
+
+	if ((error = validate_tree_and_parents(&parents_arr, repo, tree_id, commit_parent_from_array, &data, NULL, true)) < 0)
+		return error;
+
+	error = git_commit__create_buffer_internal(
+		out, repo, author, committer,
+		message_encoding, message, tree_id,
+		&parents_arr);
+
+	git_array_clear(parents_arr);
+	return error;
+}
diff --git a/tests/commit/write.c b/tests/commit/write.c
index 96b7cc3..9d1ae78 100644
--- a/tests/commit/write.c
+++ b/tests/commit/write.c
@@ -98,6 +98,45 @@
    cl_assert_equal_s(commit_message, git_commit_message(commit));
 }
 
+void test_commit_write__into_buf(void)
+{
+	git_oid tree_id;
+	git_signature *author, *committer;
+	git_tree *tree;
+	git_commit *parent;
+	git_oid parent_id;
+	git_buf commit = GIT_BUF_INIT;
+
+	git_oid_fromstr(&tree_id, tree_id_str);
+	cl_git_pass(git_tree_lookup(&tree, g_repo, &tree_id));
+
+	/* create signatures */
+	cl_git_pass(git_signature_new(&committer, committer_name, committer_email, 123456789, 60));
+	cl_git_pass(git_signature_new(&author, committer_name, committer_email, 987654321, 90));
+
+	git_oid_fromstr(&parent_id, parent_id_str);
+	cl_git_pass(git_commit_lookup(&parent, g_repo, &parent_id));
+
+	cl_git_pass(git_commit_create_buffer(&commit, g_repo, author, committer,
+					     NULL, root_commit_message, tree, 1, (const git_commit **) &parent));
+
+	cl_assert_equal_s(commit.ptr,
+			  "tree 1810dff58d8a660512d4832e740f692884338ccd\n\
+parent 8496071c1b46c854b31185ea97743be6a8774479\n\
+author Vicent Marti <vicent@github.com> 987654321 +0130\n\
+committer Vicent Marti <vicent@github.com> 123456789 +0100\n\
+\n\
+This is a root commit\n\
+   This is a root commit and should be the only one in this branch\n\
+");
+
+	git_buf_free(&commit);
+	git_tree_free(tree);
+	git_commit_free(parent);
+	git_signature_free(author);
+	git_signature_free(committer);
+}
+
 // create a root commit
 void test_commit_write__root(void)
 {