From: Stefan Sperling Subject: allow merging tags To: gameoftrees@openbsd.org Date: Thu, 18 Jun 2026 14:28:44 +0200 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;