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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
allow merging tags
To:
gameoftrees@openbsd.org
Date:
Thu, 18 Jun 2026 14:28:44 +0200

Download raw body.

Thread
  • Stefan Sperling:

    allow merging tags

Make 'got merge' accept tags as merge source argument.
 
Reuse a tag-resolution function from tog.c, moving it to the lib directory
such that the got command can make use of it as well.
 
Add test coverage.

ok?

I need this for github-specific workflows used in the Plakar project.
 
M  got/got.c                 |    2+   1-
M  include/got_reference.h   |   10+   0-
M  lib/reference.c           |   63+   0-
M  regress/cmdline/merge.sh  |  107+   0-
M  tog/tog.c                 |    2+  64-

5 files changed, 184 insertions(+), 65 deletions(-)

commit - 69ac886cd64b82483fbb0e3114eb447f1d2ff9e0
commit + b15b671e96f233b655a2ea7de6d1bcc617c738b7
blob - 7b2602c1b70e154f1f39d3222ca03af18120b5ff
blob + f57ee6d3b40565d80708f683462754d670794ab1
--- got/got.c
+++ got/got.c
@@ -13928,7 +13928,8 @@ cmd_merge(int argc, char *argv[])
 			error = got_error_from_errno("strdup");
 			goto done;
 		}
-		error = got_ref_resolve(&branch_tip, repo, branch);
+		error = got_ref_resolve_commit_or_tag(&branch_tip, repo,
+		    branch);
 		if (error)
 			goto done;
 	}
blob - 55e4df44b40d37a2a722f120946119046d3edc41
blob + 2a94c23cc790b37daf1e5b78619811eb1faeb399
--- include/got_reference.h
+++ include/got_reference.h
@@ -91,6 +91,16 @@ const struct got_error *got_ref_resolve(struct got_obj
     struct got_repository *, struct got_reference *);
 
 /*
+ * Attempt to resolve a reference (symbolic or not) to an object ID,
+ * assuming the reference points at a commit or tag object. If the reference
+ * points at a tag then return the commit ID which the tag resolves to.
+ * Object types other than commit and tag result in GOT_ERR_OBJ_TYPE.
+ * 
+ */
+const struct got_error *got_ref_resolve_commit_or_tag(struct got_object_id **,
+    struct got_repository *, struct got_reference *);
+
+/*
  * Return a string representation of a reference.
  * The caller must dispose of it with free(3).
  */
blob - 792d13a093587be73459dbc1856937bce61a1b6c
blob + 40573c4679ab45fe6119a377d17ee557b87498f2
--- lib/reference.c
+++ lib/reference.c
@@ -613,6 +613,69 @@ got_ref_resolve(struct got_object_id **id, struct got_
 	return ref_resolve(id, repo, ref, GOT_REF_RECURSE_MAX);
 }
 
+const struct got_error *
+got_ref_resolve_commit_or_tag(struct got_object_id **commit_id,
+    struct got_repository *repo, struct got_reference *ref)
+{
+	const struct got_error *err = NULL;
+	struct got_object_id *obj_id;
+	struct got_tag_object *tag = NULL;
+	int obj_type;
+
+	*commit_id = NULL;
+
+	err = got_ref_resolve(&obj_id, repo, ref);
+	if (err)
+		return err;
+
+	err = got_object_get_type(&obj_type, repo, obj_id);
+	if (err)
+		goto done;
+
+	switch (obj_type) {
+	case GOT_OBJ_TYPE_COMMIT:
+		break;
+	case GOT_OBJ_TYPE_TAG:
+		/*
+		 * Git allows nested tags that point to tags; keep peeling
+		 * till we reach the bottom, which is always a non-tag ref.
+		 */
+		do {
+			if (tag != NULL)
+				got_object_tag_close(tag);
+			err = got_object_open_as_tag(&tag, repo, obj_id);
+			if (err)
+				goto done;
+			free(obj_id);
+			obj_id = got_object_id_dup(
+			    got_object_tag_get_object_id(tag));
+			if (obj_id == NULL) {
+				err = got_error_from_errno("got_object_id_dup");
+				goto done;
+			}
+			err = got_object_get_type(&obj_type, repo, obj_id);
+			if (err)
+				goto done;
+		} while (obj_type == GOT_OBJ_TYPE_TAG);
+		if (obj_type != GOT_OBJ_TYPE_COMMIT)
+			err = got_error(GOT_ERR_OBJ_TYPE);
+		break;
+	default:
+		err = got_error(GOT_ERR_OBJ_TYPE);
+		break;
+	}
+
+done:
+	if (tag)
+		got_object_tag_close(tag);
+	if (err == NULL)
+		*commit_id = obj_id;
+	else
+		free(obj_id);
+	return err;
+}
+
+
 char *
 got_ref_to_str(struct got_reference *ref)
 {
blob - cd6a8a4afe865e1846927b4aa4b6ef571c40bee8
blob + e367646e800cc49403b273328b99c40f5eda42cd
--- regress/cmdline/merge.sh
+++ regress/cmdline/merge.sh
@@ -2012,6 +2012,112 @@ test_merge_fetched_branch_remote() {
 	test_done "$testroot" "$ret"
 }
 
+test_merge_tag() {
+	local testroot=`test_init merge_tag`
+	local commit0=`git_show_head $testroot/repo`
+	local commit0_author_time=`git_show_author_time $testroot/repo`
+
+	git -C $testroot/repo checkout -q -b newbranch
+	echo "modified delta on branch" > $testroot/repo/gamma/delta
+	git_commit $testroot/repo -m "committing to delta on newbranch"
+	local branch_commit=`git_show_branch_head $testroot/repo newbranch`
+
+	got checkout -b master $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# create a divergent commit
+	git -C $testroot/repo checkout -q master
+	echo "modified zeta on master" > $testroot/repo/epsilon/zeta
+	git_commit $testroot/repo -m "committing to zeta on master"
+	local master_commit=`git_show_head $testroot/repo`
+
+
+	# tag the branch
+	got tag -r $testroot/repo -c newbranch -m "1.0" 1.0 > /dev/null
+
+	# need an up-to-date work tree for 'got merge'
+	(cd $testroot/wt && got update > /dev/null)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got update failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got merge 1.0 > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got merge failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	local merge_commit=`git_show_head $testroot/repo`
+
+	echo "G  gamma/delta" >> $testroot/stdout.expected
+	echo -n "Merged refs/tags/1.0 into refs/heads/master: " \
+		>> $testroot/stdout.expected
+	echo $merge_commit >> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "modified delta on branch" > $testroot/content.expected
+	cat $testroot/wt/gamma/delta > $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	echo -n > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout)
+	echo "commit $merge_commit (master)" > $testroot/stdout.expected
+	echo "commit $master_commit" >> $testroot/stdout.expected
+	echo "commit $commit0" >> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# We should have created a merge commit with two parents.
+	(cd $testroot/wt && got log -l1 | grep ^parent > $testroot/stdout)
+	echo "parent 1: $master_commit" > $testroot/stdout.expected
+	echo "parent 2: $branch_commit" >> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 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_forward
@@ -2031,3 +2137,4 @@ run_test test_merge_umask
 run_test test_merge_gitconfig_author
 run_test test_merge_fetched_branch
 run_test test_merge_fetched_branch_remote
+run_test test_merge_tag
blob - 26d4f62fa1f5983edc7f7f2c380fbdc94facf7d5
blob + f4120d83d6a20bb1906c735a5037e405bdfb9790
--- tog/tog.c
+++ tog/tog.c
@@ -10072,68 +10072,6 @@ close_ref_view(struct tog_view *view)
 }
 
 static const struct got_error *
-resolve_reflist_entry(struct got_object_id **commit_id,
-    struct tog_reflist_entry *re, struct got_repository *repo)
-{
-	const struct got_error *err = NULL;
-	struct got_object_id *obj_id;
-	struct got_tag_object *tag = NULL;
-	int obj_type;
-
-	*commit_id = NULL;
-
-	err = got_ref_resolve(&obj_id, repo, re->ref);
-	if (err)
-		return err;
-
-	err = got_object_get_type(&obj_type, repo, obj_id);
-	if (err)
-		goto done;
-
-	switch (obj_type) {
-	case GOT_OBJ_TYPE_COMMIT:
-		break;
-	case GOT_OBJ_TYPE_TAG:
-		/*
-		 * Git allows nested tags that point to tags; keep peeling
-		 * till we reach the bottom, which is always a non-tag ref.
-		 */
-		do {
-			if (tag != NULL)
-				got_object_tag_close(tag);
-			err = got_object_open_as_tag(&tag, repo, obj_id);
-			if (err)
-				goto done;
-			free(obj_id);
-			obj_id = got_object_id_dup(
-			    got_object_tag_get_object_id(tag));
-			if (obj_id == NULL) {
-				err = got_error_from_errno("got_object_id_dup");
-				goto done;
-			}
-			err = got_object_get_type(&obj_type, repo, obj_id);
-			if (err)
-				goto done;
-		} while (obj_type == GOT_OBJ_TYPE_TAG);
-		if (obj_type != GOT_OBJ_TYPE_COMMIT)
-			err = got_error(GOT_ERR_OBJ_TYPE);
-		break;
-	default:
-		err = got_error(GOT_ERR_OBJ_TYPE);
-		break;
-	}
-
-done:
-	if (tag)
-		got_object_tag_close(tag);
-	if (err == NULL)
-		*commit_id = obj_id;
-	else
-		free(obj_id);
-	return err;
-}
-
-static const struct got_error *
 log_ref_entry(struct tog_view **new_view, int begin_y, int begin_x,
     struct tog_reflist_entry *re, struct got_repository *repo)
 {
@@ -10143,7 +10081,7 @@ log_ref_entry(struct tog_view **new_view, int begin_y,
 
 	*new_view = NULL;
 
-	err = resolve_reflist_entry(&commit_id, re, repo);
+	err = got_ref_resolve_commit_or_tag(&commit_id, repo, re->ref);
 	if (err)
 		return err;
 
@@ -10466,7 +10404,7 @@ browse_ref_tree(struct tog_view **new_view, int begin_
 
 	*new_view = NULL;
 
-	err = resolve_reflist_entry(&commit_id, re, repo);
+	err = got_ref_resolve_commit_or_tag(&commit_id, repo, re->ref);
 	if (err)
 		return err;