Commit 9862ef8ef8ffd95a74be8082acab9fea0de85edb

Russell Belfer 2014-05-02T09:42:07

Merge pull request #2310 from libgit2/cmn/commit-create-safe commit: safer commit creation with reference update

diff --git a/include/git2/commit.h b/include/git2/commit.h
index 834330b..fb53a70 100644
--- a/include/git2/commit.h
+++ b/include/git2/commit.h
@@ -254,7 +254,8 @@ GIT_EXTERN(int) git_commit_nth_gen_ancestor(
  *	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.
+ *	exist yet, it will be created. If it does exist, the first
+ *	parent must be the tip of this branch.
  *
  * @param author Signature with author and author time of commit
  *
@@ -329,7 +330,7 @@ GIT_EXTERN(int) git_commit_create_v(
  *
  * The `update_ref` value works as in the regular `git_commit_create()`,
  * updating the ref to point to the newly rewritten commit.  If you want
- * to amend a commit that is not currently the HEAD of the branch and then
+ * to amend a commit that is not currently the tip of the branch and then
  * rewrite the following commits to reach a ref, pass this as NULL and
  * update the rest of the commit chain and ref separately.
  *
diff --git a/src/commit.c b/src/commit.c
index 255debe..227d5c4 100644
--- a/src/commit.c
+++ b/src/commit.c
@@ -34,6 +34,35 @@ void git_commit__free(void *_commit)
 	git__free(commit);
 }
 
+static int update_ref_for_commit(git_repository *repo, git_reference *ref, const char *update_ref, const git_oid *id, const git_signature *committer)
+{
+	git_reference *ref2 = NULL;
+	int error;
+	git_commit *c;
+	const char *shortmsg;
+	git_buf reflog_msg = GIT_BUF_INIT;
+
+	if ((error = git_commit_lookup(&c, repo, id)) < 0) {
+		return error;
+	}
+
+	shortmsg = git_commit_summary(c);
+	git_buf_printf(&reflog_msg, "commit%s: %s",
+		       git_commit_parentcount(c) == 0 ? " (initial)" : "",
+		       shortmsg);
+	git_commit_free(c);
+
+	if (ref) {
+		error = git_reference_set_target(&ref2, ref, id, committer, git_buf_cstr(&reflog_msg));
+		git_reference_free(ref2);
+	} else {
+		error = git_reference__update_terminal(repo, update_ref, id, committer, git_buf_cstr(&reflog_msg));
+	}
+
+	git_buf_free(&reflog_msg);
+	return error;
+}
+
 int git_commit_create_from_callback(
 	git_oid *id,
 	git_repository *repo,
@@ -46,6 +75,9 @@ int git_commit_create_from_callback(
 	git_commit_parent_callback parent_cb,
 	void *parent_payload)
 {
+	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;
 	git_odb *odb;
@@ -53,10 +85,31 @@ int git_commit_create_from_callback(
 
 	assert(id && repo && tree && parent_cb);
 
+	if (update_ref) {
+		error = git_reference_lookup_resolved(&ref, repo, update_ref, 10);
+		if (error < 0 && error != GIT_ENOTFOUND)
+			return error;
+	}
+	giterr_clear();
+
+	if (ref)
+		current_id = git_reference_target(ref);
+
 	git_oid__writebuf(&commit, "tree ", tree);
 
-	while ((parent = parent_cb(i++, parent_payload)) != NULL)
+	while ((parent = parent_cb(i, parent_payload)) != NULL) {
 		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);
@@ -78,24 +131,8 @@ int git_commit_create_from_callback(
 	git_buf_free(&commit);
 
 	if (update_ref != NULL) {
-		int error;
-		git_commit *c;
-		const char *shortmsg;
-		git_buf reflog_msg = GIT_BUF_INIT;
-
-		if (git_commit_lookup(&c, repo, id) < 0)
-			goto on_error;
-
-		shortmsg = git_commit_summary(c);
-		git_buf_printf(&reflog_msg, "commit%s: %s",
-				git_commit_parentcount(c) == 0 ? " (initial)" : "",
-				shortmsg);
-		git_commit_free(c);
-
-		error = git_reference__update_terminal(repo, update_ref, id,
-				committer, git_buf_cstr(&reflog_msg));
-
-		git_buf_free(&reflog_msg);
+		error = update_ref_for_commit(repo, ref, update_ref, id, committer);
+		git_reference_free(ref);
 		return error;
 	}
 
@@ -242,6 +279,8 @@ int git_commit_amend(
 {
 	git_repository *repo;
 	git_oid tree_id;
+	git_reference *ref;
+	int error;
 
 	assert(id && commit_to_amend);
 
@@ -266,9 +305,27 @@ int git_commit_amend(
 		git_oid_cpy(&tree_id, git_tree_id(tree));
 	}
 
-	return git_commit_create_from_callback(
-		id, repo, update_ref, author, committer, message_encoding, message,
+	if (update_ref) {
+		if ((error = git_reference_lookup_resolved(&ref, repo, update_ref, 5)) < 0)
+			return error;
+
+		if (git_oid_cmp(git_commit_id(commit_to_amend), git_reference_target(ref))) {
+			git_reference_free(ref);
+			giterr_set(GITERR_REFERENCE, "commit to amend is not the tip of the given branch");
+			return -1;
+		}
+	}
+
+	error = git_commit_create_from_callback(
+		id, repo, NULL, author, committer, message_encoding, message,
 		&tree_id, commit_parent_for_amend, (void *)commit_to_amend);
+
+	if (!error && update_ref) {
+		error = update_ref_for_commit(repo, ref, NULL, id, committer);
+		git_reference_free(ref);
+	}
+
+	return error;
 }
 
 int git_commit__parse(void *_commit, git_odb_object *odb_obj)
diff --git a/tests/commit/commit.c b/tests/commit/commit.c
index 38397d2..fa181b7 100644
--- a/tests/commit/commit.c
+++ b/tests/commit/commit.c
@@ -38,6 +38,10 @@ void test_commit_commit__create_unexisting_update_ref(void)
 	cl_git_pass(git_commit_create(&oid, _repo, "refs/heads/foo/bar", s, s,
 				      NULL, "some msg", tree, 1, (const git_commit **) &commit));
 
+	/* fail because the parent isn't the tip of the branch anymore */
+	cl_git_fail(git_commit_create(&oid, _repo, "refs/heads/foo/bar", s, s,
+				      NULL, "some msg", tree, 1, (const git_commit **) &commit));
+
 	cl_git_pass(git_reference_lookup(&ref, _repo, "refs/heads/foo/bar"));
 	cl_assert(!git_oid_cmp(&oid, git_reference_target(ref)));
 
diff --git a/tests/object/commit/commitstagedfile.c b/tests/object/commit/commitstagedfile.c
index 3e7b3c0..9758ea9 100644
--- a/tests/object/commit/commitstagedfile.c
+++ b/tests/object/commit/commitstagedfile.c
@@ -175,6 +175,10 @@ void test_object_commit_commitstagedfile__amend_commit(void)
 	cl_git_pass(git_commit_amend(
 		&new_oid, old_commit, "HEAD", NULL, NULL, NULL, "Initial commit", NULL));
 
+	/* fail because the commit isn't the tip of the branch anymore */
+	cl_git_fail(git_commit_amend(
+		&new_oid, old_commit, "HEAD", NULL, NULL, NULL, "Initial commit", NULL));
+
 	cl_git_pass(git_commit_lookup(&new_commit, repo, &new_oid));
 
 	cl_assert_equal_i(0, git_commit_parentcount(new_commit));
@@ -182,6 +186,7 @@ void test_object_commit_commitstagedfile__amend_commit(void)
 	assert_commit_is_head(new_commit);
 
 	git_commit_free(old_commit);
+
 	old_commit = new_commit;
 
 	/* let's amend the tree of that last commit */
@@ -192,6 +197,10 @@ void test_object_commit_commitstagedfile__amend_commit(void)
 	cl_git_pass(git_tree_lookup(&tree, repo, &tree_oid));
 	cl_assert_equal_i(2, git_tree_entrycount(tree));
 
+	/* fail to amend on a ref which does not exist */
+	cl_git_fail_with(GIT_ENOTFOUND, git_commit_amend(
+		&new_oid, old_commit, "refs/heads/nope", NULL, NULL, NULL, "Initial commit", tree));
+
 	cl_git_pass(git_commit_amend(
 		&new_oid, old_commit, "HEAD", NULL, NULL, NULL, "Initial commit", tree));
 	git_tree_free(tree);