commit 179f9db092046395efaa1dc9e767a1ec109e1174 from: James Cook date: Tue Jun 20 13:49:35 2023 UTC Implement fast-forward merges. Split part of got_worktree_merge_prepare into a new function, got_worktree_merge_write_refs, since that part doesn't make sense in the fast-forward case. ok stsp@ commit - 58e5e037d6e3e5d94bdf674e074fcdf411e80bfc commit + 179f9db092046395efaa1dc9e767a1ec109e1174 blob - 51fec6b6e692d45a3c6ed44d2cd5a4a5649253e6 blob + 1f7297bf2a0f12c2fbcce5fbb38a533d88f94d4e --- TODO +++ TODO @@ -8,15 +8,9 @@ got: instead of the root directory checked out at /usr/src. The next LLVM release 12.1 would later be committed onto the llvm-12 branch and then merged into main at /usr/src/gnu/llvm in the same way. -- Teach 'got merge' to forward a branch reference if possible, instead of - creating a merge commit. Forwarding should only be done if linear - history exists from the tip of the branch being merged to the tip of - the work tree's branch, and if the tip of the work tree's branch is - itself not a merge commit (this makes "stacked" merges possible - by default, and prevents a 'main' branch reference from being forwarded - to a vendor branch in case no new commits were added to 'main' since - the previous vendor merge). Provide an option (-M) which forces creation - of a merge commit, for cases where users deem forwarding undesirable. +- Provide an option (-M) to 'got merge' which forces creation of a merge commit + even when forwarding is possible, for cases where users deem forwarding + undesirable. - When a clone fails the HEAD symref will always point to "refs/heads/main" (ie. the internal default HEAD symref of Got). Resuming a failed clone with 'got fetch' is supposed to work. To make this easier, if the HEAD symref blob - cf38fa07face9d2a811a09f32c2b6e6bc5cff414 blob + ee5ecec15b1c9954ab672ace7b9165717723b7d2 --- got/got.1 +++ got/got.1 @@ -2796,9 +2796,18 @@ or reverted with .Op Ar branch .Xc .Dl Pq alias: Cm mg -Create a merge commit based on the current branch of the work tree and -the specified -.Ar branch . +Merge the specified +.Ar branch +into the current branch of the work tree. +If the branches have diverged, creates a merge commit. +Otherwise, if +.Ar branch +already includes all commits from the work tree's branch, updates the work +tree's branch to be the same as +.Ar branch +without creating a commit, and updates the work tree to the most recent commit +on the branch. +.Pp If a linear project history is desired, then use of .Cm got rebase should be preferred over @@ -2854,14 +2863,6 @@ the work tree's current branch unmodified. .Pp .Cm got merge will refuse to run if certain preconditions are not met. -If history of the -.Ar branch -is based on the work tree's current branch, then no merge commit can -be created and -.Cm got integrate -may be used to integrate the -.Ar branch -instead. If the work tree is not yet fully updated to the tip commit of its branch, then the work tree must first be updated with .Cm got update . blob - a14b71c8099e53f924c43a9d3f6a90a70ffe1449 blob + ea8a08baed68affba73cafa577c3125e9756466a --- got/got.c +++ got/got.c @@ -13311,21 +13311,54 @@ cmd_merge(int argc, char *argv[]) error = check_path_prefix(wt_branch_tip, branch_tip, got_worktree_get_path_prefix(worktree), GOT_ERR_MERGE_PATH, repo); + if (error) + goto done; + error = got_worktree_merge_prepare(&fileindex, worktree, repo); if (error) goto done; if (yca_id && got_object_id_cmp(wt_branch_tip, yca_id) == 0) { - 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); + struct got_pathlist_head paths; + if (interrupt_merge) { + error = got_error_msg(GOT_ERR_SAME_BRANCH, + "merge is a fast-forward; this is " + "incompatible with got merge -n"); + goto done; + } + printf("Forwarding %s to %s\n", + got_ref_get_name(wt_branch), branch_name); + error = got_ref_change_ref(wt_branch, branch_tip); + if (error) + goto done; + error = got_ref_write(wt_branch, repo); + if (error) + goto done; + error = got_worktree_set_base_commit_id(worktree, repo, + branch_tip); + if (error) + goto done; + TAILQ_INIT(&paths); + error = got_pathlist_append(&paths, "", NULL); + if (error) + goto done; + error = got_worktree_checkout_files(worktree, + &paths, repo, update_progress, &upa, + check_cancelled, NULL); + got_pathlist_free(&paths, GOT_PATHLIST_FREE_NONE); + if (error) + goto done; + if (upa.did_something) { + char *id_str; + error = got_object_id_str(&id_str, branch_tip); + if (error) + goto done; + printf("Updated to commit %s\n", id_str); + free(id_str); + } else + printf("Already up-to-date\n"); + print_update_progress_stats(&upa); goto done; } - error = got_worktree_merge_prepare(&fileindex, worktree, - branch, repo); + error = got_worktree_merge_write_refs(worktree, branch, repo); if (error) goto done; blob - f7de090bfb2bd10312bdd0c389b5eabe41a33fdd blob + 5b0b7259e6ff5fc0edf8711ac6780dd49315fe54 --- include/got_worktree.h +++ include/got_worktree.h @@ -485,16 +485,22 @@ const struct got_error *got_worktree_merge_in_progress struct got_worktree *, struct got_repository *); /* - * Prepare for merging a branch into the work tree's current branch. + * Prepare for merging a branch into the work tree's current branch: lock the + * worktree and check that preconditions are satisfied. The function also + * returns a pointer to a fileindex which must be passed back to other + * merge-related functions. + */ +const struct got_error *got_worktree_merge_prepare(struct got_fileindex **, + struct got_worktree *, struct got_repository *); + +/* * This function creates a reference to the branch being merged, and to * this branch's current tip commit, in the "got/worktree/merge/" namespace. * These references are used to keep track of merge operation state and are * used as input and/or output arguments with other merge-related functions. - * The function also returns a pointer to a fileindex which must be - * passed back to other merge-related functions. */ -const struct got_error *got_worktree_merge_prepare(struct got_fileindex **, - struct got_worktree *, struct got_reference *, struct got_repository *); +const struct got_error *got_worktree_merge_write_refs(struct got_worktree *, + struct got_reference *, struct got_repository *); /* * Continue an interrupted merge operation. blob - e97f10e73cd0df0521ce4e36b40897121837e8d5 blob + 95e2e6bfa46ac12fd077b083c122ade77e54106f --- lib/worktree.c +++ lib/worktree.c @@ -8486,14 +8486,12 @@ got_worktree_merge_in_progress(int *in_progress, struc const struct got_error *got_worktree_merge_prepare( struct got_fileindex **fileindex, struct got_worktree *worktree, - struct got_reference *branch, struct got_repository *repo) + struct got_repository *repo) { const struct got_error *err = NULL; char *fileindex_path = NULL; - char *branch_refname = NULL, *commit_refname = NULL; - struct got_reference *wt_branch = NULL, *branch_ref = NULL; - struct got_reference *commit_ref = NULL; - struct got_object_id *branch_tip = NULL, *wt_branch_tip = NULL; + struct got_reference *wt_branch = NULL; + struct got_object_id *wt_branch_tip = NULL; struct check_rebase_ok_arg ok_arg; *fileindex = NULL; @@ -8513,14 +8511,6 @@ const struct got_error *got_worktree_merge_prepare( &ok_arg); if (err) goto done; - - err = get_merge_branch_ref_name(&branch_refname, worktree); - if (err) - return err; - - err = get_merge_commit_ref_name(&commit_refname, worktree); - if (err) - return err; err = got_ref_open(&wt_branch, repo, worktree->head_ref_name, 0); @@ -8535,7 +8525,39 @@ const struct got_error *got_worktree_merge_prepare( err = got_error(GOT_ERR_MERGE_OUT_OF_DATE); goto done; } + +done: + free(fileindex_path); + if (wt_branch) + got_ref_close(wt_branch); + free(wt_branch_tip); + if (err) { + if (*fileindex) { + got_fileindex_free(*fileindex); + *fileindex = NULL; + } + lock_worktree(worktree, LOCK_SH); + } + return err; +} +const struct got_error *got_worktree_merge_write_refs( + struct got_worktree *worktree, struct got_reference *branch, + struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *branch_refname = NULL, *commit_refname = NULL; + struct got_reference *branch_ref = NULL, *commit_ref = NULL; + struct got_object_id *branch_tip = NULL; + + err = get_merge_branch_ref_name(&branch_refname, worktree); + if (err) + return err; + + err = get_merge_commit_ref_name(&commit_refname, worktree); + if (err) + return err; + err = got_ref_resolve(&branch_tip, repo, branch); if (err) goto done; @@ -8557,21 +8579,11 @@ const struct got_error *got_worktree_merge_prepare( done: free(branch_refname); free(commit_refname); - free(fileindex_path); if (branch_ref) got_ref_close(branch_ref); if (commit_ref) got_ref_close(commit_ref); - if (wt_branch) - got_ref_close(wt_branch); - free(wt_branch_tip); - if (err) { - if (*fileindex) { - got_fileindex_free(*fileindex); - *fileindex = NULL; - } - lock_worktree(worktree, LOCK_SH); - } + free(branch_tip); return err; } blob - 71e5e49a5b7f5ebee4e83f1e29b7b2352977c697 blob + 92b590aa2ef2958738fb2d4e8419cb879fc9bf5c --- regress/cmdline/merge.sh +++ regress/cmdline/merge.sh @@ -52,31 +52,7 @@ test_merge_basic() { return 1 fi - # need a divergant commit on the main branch for 'got merge' - (cd $testroot/wt && got merge newbranch \ - > $testroot/stdout 2> $testroot/stderr) - ret=$? - if [ $ret -eq 0 ]; then - echo "got merge succeeded unexpectedly" >&2 - test_done "$testroot" "1" - return 1 - fi - echo -n "got: cannot create a merge commit because " \ - > $testroot/stderr.expected - echo -n "refs/heads/newbranch is based on refs/heads/master; " \ - >> $testroot/stderr.expected - echo -n "refs/heads/newbranch can be integrated with " \ - >> $testroot/stderr.expected - echo "'got integrate' instead" >> $testroot/stderr.expected - cmp -s $testroot/stderr.expected $testroot/stderr - ret=$? - if [ $ret -ne 0 ]; then - diff -u $testroot/stderr.expected $testroot/stderr - test_done "$testroot" "$ret" - return 1 - fi - - # create the required dirvergant commit + # create a divergent commit (cd $testroot/repo && git checkout -q master) echo "modified zeta on master" > $testroot/repo/epsilon/zeta git_commit $testroot/repo -m "committing to zeta on master" @@ -363,11 +339,163 @@ gamma/ gamma/delta symlink@ -> alpha EOF + 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_merge_forward() { + local testroot=`test_init merge_forward` + local commit0=`git_show_head $testroot/repo` + + # Create a commit before branching, which will be used to help test + # preconditions for "got merge". + echo "modified alpha" > $testroot/repo/alpha + git_commit $testroot/repo -m "common commit" + local commit1=`git_show_head $testroot/repo` + + (cd $testroot/repo && git checkout -q -b newbranch) + echo "modified beta on branch" > $testroot/repo/beta + git_commit $testroot/repo -m "committing to beta on newbranch" + local commit2=`git_show_head $testroot/repo` + + 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 + + # must not use a mixed-commit work tree with 'got merge' + (cd $testroot/wt && got update -c $commit0 alpha > /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 newbranch \ + > $testroot/stdout 2> $testroot/stderr) + ret=$? + if [ $ret -eq 0 ]; then + echo "got merge succeeded unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + echo -n "got: work tree contains files from multiple base commits; " \ + > $testroot/stderr.expected + echo "the entire work tree must be updated first" \ + >> $testroot/stderr.expected + cmp -s $testroot/stderr.expected $testroot/stderr + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done "$testroot" "$ret" + return 1 + fi + + (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 + + # 'got merge -n' refuses to fast-forward + (cd $testroot/wt && got merge -n newbranch \ + > $testroot/stdout 2> $testroot/stderr) + ret=$? + if [ $ret -eq 0 ]; then + echo "got merge succeeded unexpectedly" >&2 + test_done "$testroot" "1" + return 1 + fi + echo -n "got: merge is a fast-forward; " > $testroot/stderr.expected + echo "this is incompatible with got merge -n" \ + >> $testroot/stderr.expected + cmp -s $testroot/stderr.expected $testroot/stderr + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done "$testroot" "$ret" + return 1 + fi + + (cd $testroot/wt && got merge newbranch \ + > $testroot/stdout 2> $testroot/stderr) + ret=$? + if [ $ret -ne 0 ]; then + echo "got merge failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "Forwarding refs/heads/master to refs/heads/newbranch" \ + > $testroot/stdout.expected + echo "U beta" >> $testroot/stdout.expected + echo "Updated to commit $commit2" \ + >> $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 | grep ^commit > $testroot/stdout) + echo -n "commit $commit2 " > $testroot/stdout.expected + echo "(master, newbranch)" >> $testroot/stdout.expected + echo "commit $commit1" >> $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 + test_done "$testroot" "$ret" +} + +test_merge_backward() { + local testroot=`test_init merge_backward` + local commit0=`git_show_head $testroot/repo` + + (cd $testroot/repo && git checkout -q -b newbranch) + (cd $testroot/repo && git checkout -q master) + echo "modified alpha on master" > $testroot/repo/alpha + git_commit $testroot/repo -m "committing to alpha on master" + + 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 + + (cd $testroot/wt && got merge newbranch \ + > $testroot/stdout 2> $testroot/stderr) + ret=$? + if [ $ret -ne 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 -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi test_done "$testroot" "$ret" } @@ -1566,6 +1694,8 @@ test_merge_gitconfig_author() { test_parseargs "$@" run_test test_merge_basic +run_test test_merge_forward +run_test test_merge_backward run_test test_merge_continue run_test test_merge_continue_new_commit run_test test_merge_abort