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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
Re: rebase suffers '?' conflicts
To:
Christian Weisgerber <naddy@mips.inka.de>
Cc:
gameoftrees@openbsd.org
Date:
Wed, 12 Apr 2023 14:26:40 +0200

Download raw body.

Thread
On Sat, Apr 01, 2023 at 04:13:54PM +0200, Christian Weisgerber wrote:
> Christian Weisgerber:
> 
> > git clone --bare https://github.com/openbsd/src.git
> > cd src.git
> > git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
> > git remote add jca https://github.com/jcourreges/openbsd-src.git
> > git fetch --all
> > cd ..
> > got co src.git
> > cd src
> > got br tmp
> > got br -c jca/llvm15-12 llvm15
> > got up -b tmp
> > got rb llvm15
> > got rb -a
> 
> The problem is here, after the aborted rebase.  Despite "got status"
> and "got diff" showing nothing, the worktree is in reality full of
> changes.
 
Yes, this is where the bug is. Essentially this is a bug in the
'got update' machinery. Let me try to explain:

The work tree does indeed have no local changes. However, the
base commit ID of some files is left at a wrong value.

What happens is that the first commit is rebased ok, i.e. changes
are merged into the work tree and then a new commit is created on
a temporary branch which accumulates rebased commits.
When this commit is created the files which are changed by this
commit get a new base commit ID assigned; the ID of the new tip
commit of the temporary branch.

Then another commit is rebased, and this time there are merge
conflicts, and the rebase operation stops. At this point the work
tree's base commit ID is still pointing at the temporary branch tip
and all state is consistent.

When we abort the rebase operation the work tree's global base commit
ID gets reset to that of the pre-rebase branch (but keep in mind that
each file has a separate base commit ID in the file index).
Any local changes are reverted, which doesn't affect the files which
received a new base commit ID because these files appear unmodified
in the work tree (still relative to the first rebased commit).

Then all files are checked out to ensure that files in the work tree
return to the state from before the rebase operation.
The bug is that base commit IDs of affeted files are left as-is.
This base commit ID is of course the wrong one because the files are
now based on the pre-rebase branch again.

The fix is trivial and I've added test coverage for this issue.

When the rebase_abort test is run without the fix in place then
it now fails like this:

$ ./rebase.sh test_rebase_abort
test_rebase_abort --- /tmp/got-test-rebase_abort-p8vJzncg/stdout.expected       Wed Apr 12 13:25:15 2023
+++ /tmp/got-test-rebase_abort-p8vJzncg/stdout  Wed Apr 12 13:25:15 2023
@@ -1 +1,2 @@
 based on commit: 10162cdd439d90b5c3bcd725cae498d10dfd3dd3
+based on commit: ac3716263a661f70593e8e6ddd192e231612961e
test failed; leaving test data in /tmp/got-test-rebase_abort-p8vJzncg
$

And if we make the test skip this base ID commit check by commenting
the relevant lines then the update which the test now runs at the end
is not a no-op. Instead, it updates the file beta. Which matches what
you saw on your llvm branch.

ok?

-----------------------------------------------
 fix rebase -a not resetting base commit IDs of files changed during rebase
 
 Update base commit IDs of unmodified files while checking out files.
 This avoids spurious merge conflicts when the work tree is later used
 for another rebase operation. It also makes 'got update' right after
 'rebase -a' a no-op, as it should be.
 
 Problem found by naddy@ while rebasing jca's llvm15 branch
 
diff cc88020e952af813c1e01b91ab6516969562e972 6a06538c34c30a7ad5174e4f2bf1d669224391ef
commit - cc88020e952af813c1e01b91ab6516969562e972
commit + 6a06538c34c30a7ad5174e4f2bf1d669224391ef
blob - 9e87414b038cb6aa0ef965f162999f1108c5d82a
blob + 9ac685c7ccc7e64878f575b0e825c1fce2c67f19
--- lib/worktree.c
+++ lib/worktree.c
@@ -1996,7 +1996,13 @@ update_blob(struct got_worktree *worktree,
 		if (got_fileindex_entry_has_blob(ie) &&
 		    memcmp(ie->blob_sha1, te->id.sha1,
 		    SHA1_DIGEST_LENGTH) == 0) {
-			/* Different commit but the same blob. */
+			/*
+			 * Different commit but the same blob.
+			 * Update the base commit ID of this file.
+			 */
+			memcpy(ie->commit_sha1,
+			    worktree->base_commit_id->sha1,
+			    sizeof(ie->commit_sha1));
 			err = sync_timestamps(worktree->root_fd,
 			    path, status, ie, &sb);
 			if (err)
blob - 6a4b676aff1b3de6b3384590c038eb5b5045981b
blob + d664d34f3511e6a4a2023e233230f89f2bcec896
--- regress/cmdline/rebase.sh
+++ regress/cmdline/rebase.sh
@@ -413,11 +413,16 @@ test_rebase_abort() {
 	local init_commit=`git_show_head $testroot/repo`
 
 	(cd $testroot/repo && git checkout -q -b newbranch)
-	echo "modified alpha on branch" > $testroot/repo/alpha
-	git_commit $testroot/repo -m "committing to alpha on newbranch"
+	echo "modified beta on branch" > $testroot/repo/beta
+	git_commit $testroot/repo -m "committing to beta on newbranch"
 	local orig_commit1=`git_show_head $testroot/repo`
 	local short_orig_commit1=`trim_obj_id 28 $orig_commit1`
 
+	echo "modified alpha on branch" > $testroot/repo/alpha
+	git_commit $testroot/repo -m "committing to alpha on newbranch"
+	local orig_commit2=`git_show_head $testroot/repo`
+	local short_orig_commit2=`trim_obj_id 28 $orig_commit2`
+
 	(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"
@@ -436,9 +441,17 @@ test_rebase_abort() {
 	(cd $testroot/wt && got rebase newbranch > $testroot/stdout \
 		2> $testroot/stderr)
 
-	echo "C  alpha" > $testroot/stdout.expected
+	new_commit1=$(cd $testroot/wt && got info beta | \
+		grep '^based on commit:' | cut -d' ' -f4)
+	local short_new_commit1=`trim_obj_id 28 $new_commit1`
+	
+	echo "G  beta" > $testroot/stdout.expected
+	echo -n "$short_orig_commit1 -> $short_new_commit1" \
+		>> $testroot/stdout.expected
+	echo ": committing to beta on newbranch" >> $testroot/stdout.expected
+	echo "C  alpha" >> $testroot/stdout.expected
 	echo "Files with new merge conflicts: 1" >> $testroot/stdout.expected
-	echo -n "$short_orig_commit1 -> merge conflict" \
+	echo -n "$short_orig_commit2 -> merge conflict" \
 		>> $testroot/stdout.expected
 	echo ": committing to alpha on newbranch" >> $testroot/stdout.expected
 	cmp -s $testroot/stdout.expected $testroot/stdout
@@ -461,12 +474,12 @@ test_rebase_abort() {
 
 	echo '<<<<<<<' > $testroot/content.expected
 	echo "modified alpha on master" >> $testroot/content.expected
-	echo "||||||| 3-way merge base: commit $init_commit" \
+	echo "||||||| 3-way merge base: commit $orig_commit1" \
 		>> $testroot/content.expected
 	echo "alpha" >> $testroot/content.expected
 	echo "=======" >> $testroot/content.expected
 	echo "modified alpha on branch" >> $testroot/content.expected
-	echo ">>>>>>> merged change: commit $orig_commit1" \
+	echo ">>>>>>> merged change: commit $orig_commit2" \
 		>> $testroot/content.expected
 	cat $testroot/wt/alpha > $testroot/content
 	cmp -s $testroot/content.expected $testroot/content
@@ -519,13 +532,36 @@ test_rebase_abort() {
 
 	(cd $testroot/wt && got log -l3 -c newbranch \
 		| grep ^commit > $testroot/stdout)
-	echo "commit $orig_commit1 (newbranch)" > $testroot/stdout.expected
+	echo "commit $orig_commit2 (newbranch)" > $testroot/stdout.expected
+	echo "commit $orig_commit1" >> $testroot/stdout.expected
 	echo "commit $init_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
+
+	(cd $testroot/wt && got info .| \
+		grep '^based on commit:' | sort | uniq > $testroot/stdout)
+	echo "based on commit: $master_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
+
+	# A subsequent update should be a no-op.
+	(cd $testroot/wt && got update > $testroot/stdout)
+	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
+	fi
 	test_done "$testroot" "$ret"
 }
 
blob - 822844175d6069cb0ad59c5bd870b63d886b9990
blob + c08237c6cd1b14453b8e4f8e39789a3c6c017f1c
--- regress/cmdline/update.sh
+++ regress/cmdline/update.sh
@@ -1899,12 +1899,7 @@ test_update_modified_submodules() {
 	(cd $testroot/repo && git add repo2)
 	git_commit $testroot/repo -m "modified submodule link"
 
-	# This update only records the new base commit. Otherwise it is a
-	# no-op change because Got's file index does not track submodules.
-	echo -n "Updated to refs/heads/master: " > $testroot/stdout.expected
-	git_show_head $testroot/repo >> $testroot/stdout.expected
-	echo >> $testroot/stdout.expected
-
+	echo "Already up-to-date" > $testroot/stdout.expected
 	(cd $testroot/wt && got update > $testroot/stdout)
 
 	cmp -s $testroot/stdout.expected $testroot/stdout