"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
allow vendor branch merges with 'got merge'
To:
gameoftrees@openbsd.org
Date:
Sat, 25 Sep 2021 18:33:10 +0200

Download raw body.

Thread
This makes it possible to merge vendor branches with 'got merge'.
The idea is that when 'got import' is used to create a vendor branch,
as an entirely separate line of history with its own root commit, then
we should be able to use 'got merge' in order to merge vendor code
changes into the 'main' branch.

The initial 'got merge' implementation required that twe two branches
share a common ancestry. This makes it impossible to merge branches
which do not share the same root commit.

With this patch, this requirement is lifted. When merging multiple times,
we detect the last common merge commit between the branches and use it
to drive the tree diff such that only new changes will be merged.
To achieve this, we make use of the commit graph's existing ability to
traverse history of multiple branches in parallel to identify a common
ancestor which is the most recent merge commit between the two otherwise
unrelated branches.

While here, also fix the behaviour of 'got merge' if no changes are
available for merging. We required the user to abort such a merge
manually with 'got merge -a'. This now happens automatically and
the user will simply see the message 'Already up-to-date'.
 
ok?

(this patch sits on top of my "bad-symlinks-merge" patch which has
not been committed for lack of OKs)

diff b380729c04bdb5ee1ee63cf542b892a03486b82a 3900e2cd23b57a17655f6b84cdf02e7eaa6753ba
blob - 06422c812cb6a86575f656e0a5293c72ed775769
blob + c408a296cf7c404394c1001c724ca9a0355c7e6a
--- got/got.1
+++ got/got.1
@@ -2141,8 +2141,9 @@ If a linear project history is desired, then use of
 should be preferred over
 .Cm got merge .
 However, even strictly linear projects may require merge commits in order
-to merge in new versions of code imported from third-party projects on
-vendor branches.
+to merge in new versions of third-party code stored on vendor branches
+created with
+.Cm got import .
 .Pp
 Merge commits are commits based on multiple parent commits.
 The tip commit of the work tree's current branch, which must be set with
@@ -2154,14 +2155,14 @@ The tip commit of the specified
 .Ar branch
 will be used as the second parent.
 .Pp
+No ancestral relationship between the two branches is required.
+If the two branches have already been merged previously, only new changes
+will be merged.
+.Pp
 It is not possible to create merge commits with more than two parents.
 If more than one branch needs to be merged, then multiple merge commits
 with two parents each can be created in sequence.
 .Pp
-The
-.Ar branch
-must share common ancestry with the work tree's current branch.
-.Pp
 While merging changes found on the
 .Ar branch
 into the work tree, show the status of each affected file,
blob - 66b864fee620072787f5c6149732f3c409f4f0cf
blob + 39c5172e4d8e1b72ff03f0dbd81fc4ae8935b1b9
--- got/got.c
+++ got/got.c
@@ -2701,7 +2701,7 @@ check_linear_ancestry(struct got_object_id *commit_id,
 	struct got_object_id *yca_id;
 
 	err = got_commit_graph_find_youngest_common_ancestor(&yca_id,
-	    commit_id, base_commit_id, repo, check_cancelled, NULL);
+	    commit_id, base_commit_id, 1, repo, check_cancelled, NULL);
 	if (err)
 		return err;
 
@@ -8597,7 +8597,7 @@ print_backup_ref(const char *branch_name, const char *
 		goto done;
 
 	err = got_commit_graph_find_youngest_common_ancestor(&yca_id,
-	    old_commit_id, new_commit_id, repo, check_cancelled, NULL);
+	    old_commit_id, new_commit_id, 1, repo, check_cancelled, NULL);
 	if (err)
 		goto done;
 
@@ -8954,7 +8954,7 @@ cmd_rebase(int argc, char *argv[])
 
 		base_commit_id = got_worktree_get_base_commit_id(worktree);
 		error = got_commit_graph_find_youngest_common_ancestor(&yca_id,
-		    base_commit_id, branch_head_commit_id, repo,
+		    base_commit_id, branch_head_commit_id, 1, repo,
 		    check_cancelled, NULL);
 		if (error)
 			goto done;
@@ -10729,16 +10729,10 @@ cmd_merge(int argc, char *argv[])
 	if (error)
 		goto done;
 	error = got_commit_graph_find_youngest_common_ancestor(&yca_id,
-	    wt_branch_tip, branch_tip, repo,
+	    wt_branch_tip, branch_tip, 0, repo,
 	    check_cancelled, NULL);
-	if (error)
+	if (error && error->code != GOT_ERR_ANCESTRY)
 		goto done;
-	if (yca_id == NULL) {
-		error = got_error_msg(GOT_ERR_ANCESTRY,
-		    "specified branch shares no common ancestry "
-		    "with work tree's branch");
-		goto done;
-	}
 
 	if (!continue_merge) {
 		error = check_path_prefix(wt_branch_tip, branch_tip,
@@ -10746,22 +10740,24 @@ cmd_merge(int argc, char *argv[])
 		    GOT_ERR_MERGE_PATH, repo);
 		if (error)
 			goto done;
-		error = check_same_branch(wt_branch_tip, branch,
-		    yca_id, repo);
-		if (error) {
-			if (error->code != GOT_ERR_ANCESTRY)
+		if (yca_id) {
+			error = check_same_branch(wt_branch_tip, branch,
+			    yca_id, repo);
+			if (error) {
+				if (error->code != GOT_ERR_ANCESTRY)
+					goto done;
+				error = NULL;
+			} else {
+				static char msg[512];
+				snprintf(msg, sizeof(msg),
+				    "cannot create a merge commit because "
+				    "%s is based on %s; %s can be integrated "
+				    "with 'got integrate' instead", branch_name,
+				    got_worktree_get_head_ref_name(worktree),
+				    branch_name);
+				error = got_error_msg(GOT_ERR_SAME_BRANCH, msg);
 				goto done;
-			error = NULL;
-		} else {
-			static char msg[512];
-			snprintf(msg, sizeof(msg),
-			    "cannot create a merge commit because "
-			    "%s is based on %s; %s can be integrated "
-			    "with 'got integrate' instead", branch_name,
-			    got_worktree_get_head_ref_name(worktree),
-			    branch_name);
-			error = got_error_msg(GOT_ERR_SAME_BRANCH, msg);
-			goto done;
+			}
 		}
 		error = got_worktree_merge_prepare(&fileindex, worktree,
 		    branch, repo);
@@ -10774,6 +10770,14 @@ cmd_merge(int argc, char *argv[])
 		if (error)
 			goto done;
 		print_update_progress_stats(&upa);
+		if (!upa.did_something) {
+			error = got_worktree_merge_abort(worktree, fileindex,
+			    repo, update_progress, &upa);
+			if (error)
+				goto done;
+			printf("Already up-to-date\n");
+			goto done;
+		}
 	}
 
 	if (upa.conflicts > 0 || upa.missing > 0) {
blob - 1538d549835d34b1e9f1400e8da7166397acd4c0
blob + 617697ddc049b408054512990ba787719cfad80a
--- include/got_commit_graph.h
+++ include/got_commit_graph.h
@@ -32,4 +32,4 @@ const struct got_error *got_commit_graph_intersect(str
 /* Find the youngest common ancestor of two commits. */
 const struct got_error *got_commit_graph_find_youngest_common_ancestor(
     struct got_object_id **, struct got_object_id *, struct got_object_id *,
-    struct got_repository *, got_cancel_cb, void *);
+    int, struct got_repository *, got_cancel_cb, void *);
blob - 02b48fae94b68b2e73ce447027d7d94d3be3b6c5
blob + 55b4da29492815a14efed2a3680b55d68dddbfd0
--- lib/commit_graph.c
+++ lib/commit_graph.c
@@ -600,6 +600,7 @@ got_commit_graph_iter_next(struct got_object_id **id,
 const struct got_error *
 got_commit_graph_find_youngest_common_ancestor(struct got_object_id **yca_id,
     struct got_object_id *commit_id, struct got_object_id *commit_id2,
+    int first_parent_traversal,
     struct got_repository *repo, got_cancel_cb cancel_cb, void *cancel_arg)
 {
 	const struct got_error *err = NULL;
@@ -613,11 +614,11 @@ got_commit_graph_find_youngest_common_ancestor(struct 
 	if (commit_ids == NULL)
 		return got_error_from_errno("got_object_idset_alloc");
 
-	err = got_commit_graph_open(&graph, "/", 1);
+	err = got_commit_graph_open(&graph, "/", first_parent_traversal);
 	if (err)
 		goto done;
 
-	err = got_commit_graph_open(&graph2, "/", 1);
+	err = got_commit_graph_open(&graph2, "/", first_parent_traversal);
 	if (err)
 		goto done;
 
blob - 439105b666bbf6eb9e40bf4c2cfa6b3c25e52eeb
blob + 6a928efef76409f746a25050dabef6f6c6001c95
--- lib/send.c
+++ lib/send.c
@@ -167,7 +167,7 @@ check_linear_ancestry(const char *refname, struct got_
 		    "bad object type on server for %s", refname);
 
 	err = got_commit_graph_find_youngest_common_ancestor(&yca_id,
-	    my_id, their_id, repo, cancel_cb, cancel_arg);
+	    my_id, their_id, 1, repo, cancel_cb, cancel_arg);
 	if (err)
 		return err;
 	if (yca_id == NULL)
blob - 1c7ea8430ba3f5a85df6f00f7a3587641ae683ff
blob + 1d534c5958a2e7da9413b01d8fcff42599c6f25b
--- regress/cmdline/merge.sh
+++ regress/cmdline/merge.sh
@@ -1131,6 +1131,111 @@ test_merge_no_op() {
 	test_done "$testroot" "$ret"
 }
 
+test_merge_imported_branch() {
+	local testroot=`test_init merge_import`
+	local commit0=`git_show_head $testroot/repo`
+	local commit0_author_time=`git_show_author_time $testroot/repo`
+
+	# import a new sub-tree to the 'files' branch such that
+	# none of the files added here collide with existing ones
+	mkdir -p $testroot/tree/there
+	mkdir -p $testroot/tree/be/lots
+	mkdir -p $testroot/tree/files
+	echo "there should" > $testroot/tree/there/should
+	echo "be lots of" > $testroot/tree/be/lots/of
+	echo "files here" > $testroot/tree/files/here
+	got import -r $testroot/repo -b files -m 'import files' \
+		$testroot/tree > /dev/null
+
+	got checkout -b master $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got merge files > $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got merge failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	local merge_commit0=`git_show_head $testroot/repo`
+	cat > $testroot/stdout.expected <<EOF
+A  be/lots/of
+A  files/here
+A  there/should
+Merged refs/heads/files into refs/heads/master: $merge_commit0
+EOF
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# try to merge again while no new changes are available
+	(cd $testroot/wt && got merge files > $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got merge failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	echo "Already up-to-date" > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# update the 'files' branch
+	(cd $testroot/repo && git reset -q --hard master)
+	(cd $testroot/repo && git checkout -q files)
+	echo "indeed" > $testroot/repo/indeed
+	(cd $testroot/repo && git add indeed)
+	git_commit $testroot/repo -m "adding another file indeed"
+	echo "be lots and lots of" > $testroot/repo/be/lots/of
+	git_commit $testroot/repo -m "lots of changes"
+
+	(cd $testroot/wt && got update > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got update failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# we should now be able to merge more changes from files branch
+	(cd $testroot/wt && got merge files > $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got merge failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	local merge_commit1=`git_show_branch_head $testroot/repo master`
+	cat > $testroot/stdout.expected <<EOF
+G  be/lots/of
+A  indeed
+Merged refs/heads/files into refs/heads/master: $merge_commit1
+EOF
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_merge_basic
 run_test test_merge_continue
@@ -1139,3 +1244,4 @@ run_test test_merge_in_progress
 run_test test_merge_path_prefix
 run_test test_merge_missing_file
 run_test test_merge_no_op
+run_test test_merge_imported_branch