Commit 1fa4907213af37d50029ee33b989867f41f0f124

Stefan Sperling 2021-09-28T21:30:53

interrupt 'got rebase' upon missing/unversioned/not-deleted files

diff --git a/got/got.1 b/got/got.1
index d988c12..266da97 100644
--- a/got/got.1
+++ b/got/got.1
@@ -1750,6 +1750,12 @@ using the following status codes:
 .Pp
 If merge conflicts occur the rebase operation is interrupted and may
 be continued once conflicts have been resolved.
+If any files with destined changes are found to be missing or unversioned,
+or if files could not be deleted due to differences in deleted content,
+the rebase operation will be interrupted to prevent potentially incomplete
+changes from being committed to the repository without user intervention.
+The work tree may be modified as desired and the rebase operation can be
+continued once the changes present in the work tree are considered complete.
 Alternatively, the rebase operation may be aborted which will leave
 .Ar branch
 unmodified and the work tree switched back to its original branch.
diff --git a/got/got.c b/got/got.c
index 5aa3eb4..cc893a7 100644
--- a/got/got.c
+++ b/got/got.c
@@ -8808,14 +8808,15 @@ cmd_rebase(int argc, char *argv[])
 	int ch, rebase_in_progress = 0, abort_rebase = 0, continue_rebase = 0;
 	int histedit_in_progress = 0, merge_in_progress = 0;
 	int create_backup = 1, list_backups = 0, delete_backups = 0;
-	unsigned char rebase_status = GOT_STATUS_NO_CHANGE;
 	struct got_object_id_queue commits;
 	struct got_pathlist_head merged_paths;
 	const struct got_object_id_queue *parent_ids;
 	struct got_object_qid *qid, *pid;
+	struct got_update_progress_arg upa;
 
 	STAILQ_INIT(&commits);
 	TAILQ_INIT(&merged_paths);
+	memset(&upa, 0, sizeof(upa));
 
 	while ((ch = getopt(argc, argv, "aclX")) != -1) {
 		switch (ch) {
@@ -8931,7 +8932,6 @@ cmd_rebase(int argc, char *argv[])
 		goto done;
 
 	if (abort_rebase) {
-		struct got_update_progress_arg upa;
 		if (!rebase_in_progress) {
 			error = got_error(GOT_ERR_NOT_REBASING);
 			goto done;
@@ -8943,7 +8943,6 @@ cmd_rebase(int argc, char *argv[])
 			goto done;
 		printf("Switching work tree to %s\n",
 		    got_ref_get_symref_target(new_base_branch));
-		memset(&upa, 0, sizeof(upa));
 		error = got_worktree_rebase_abort(worktree, fileindex, repo,
 		    new_base_branch, update_progress, &upa);
 		if (error)
@@ -9029,8 +9028,6 @@ cmd_rebase(int argc, char *argv[])
 	pid = STAILQ_FIRST(parent_ids);
 	if (pid == NULL) {
 		if (!continue_rebase) {
-			struct got_update_progress_arg upa;
-			memset(&upa, 0, sizeof(upa));
 			error = got_worktree_rebase_abort(worktree, fileindex,
 			    repo, new_base_branch, update_progress, &upa);
 			if (error)
@@ -9080,7 +9077,6 @@ cmd_rebase(int argc, char *argv[])
 
 	pid = NULL;
 	STAILQ_FOREACH(qid, &commits, entry) {
-		struct got_update_progress_arg upa;
 
 		commit_id = qid->id;
 		parent_id = pid ? pid->id : yca_id;
@@ -9094,13 +9090,14 @@ cmd_rebase(int argc, char *argv[])
 			goto done;
 
 		print_merge_progress_stats(&upa);
-		if (upa.conflicts > 0)
-			rebase_status = GOT_STATUS_CONFLICT;
-
-		if (rebase_status == GOT_STATUS_CONFLICT) {
-			error = show_rebase_merge_conflict(qid->id, repo);
-			if (error)
-				goto done;
+		if (upa.conflicts > 0 || upa.missing > 0 ||
+		    upa.not_deleted > 0 || upa.unversioned > 0) {
+			if (upa.conflicts > 0) {
+				error = show_rebase_merge_conflict(qid->id,
+				    repo);
+				if (error)
+					goto done;
+			}
 			got_worktree_rebase_pathlist_free(&merged_paths);
 			break;
 		}
@@ -9112,12 +9109,30 @@ cmd_rebase(int argc, char *argv[])
 			goto done;
 	}
 
-	if (rebase_status == GOT_STATUS_CONFLICT) {
+	if (upa.conflicts > 0 || upa.missing > 0 ||
+	    upa.not_deleted > 0 || upa.unversioned > 0) {
 		error = got_worktree_rebase_postpone(worktree, fileindex);
 		if (error)
 			goto done;
-		error = got_error_msg(GOT_ERR_CONFLICTS,
-		    "conflicts must be resolved before rebasing can continue");
+		if (upa.conflicts > 0 && upa.missing == 0 &&
+		    upa.not_deleted == 0 && upa.unversioned == 0) {
+			error = got_error_msg(GOT_ERR_CONFLICTS,
+			    "conflicts must be resolved before rebasing "
+			    "can continue");
+		} else if (upa.conflicts > 0) {
+			error = got_error_msg(GOT_ERR_CONFLICTS,
+			    "conflicts must be resolved before rebasing "
+			    "can continue; changes destined for some "
+			    "files were not yet merged and should be "
+			    "merged manually if required before the "
+			    "rebase operation is continued");
+		} else {
+			error = got_error_msg(GOT_ERR_CONFLICTS,
+			    "changes destined for some files were not "
+			    "yet merged and should be merged manually "
+			    "if required before the rebase operation "
+			    "is continued");
+		}
 	} else
 		error = rebase_complete(worktree, fileindex, branch,
 		    new_base_branch, tmp_branch, repo, create_backup);
diff --git a/regress/cmdline/rebase.sh b/regress/cmdline/rebase.sh
index 3343303..8ca3370 100755
--- a/regress/cmdline/rebase.sh
+++ b/regress/cmdline/rebase.sh
@@ -1188,6 +1188,9 @@ test_rebase_delete_missing_file() {
 	local orig_commit1=`git_show_parent_commit $testroot/repo`
 	local orig_commit2=`git_show_head $testroot/repo`
 
+	local short_orig_commit1=`trim_obj_id 28 $orig_commit1`
+	local short_orig_commit2=`trim_obj_id 28 $orig_commit2`
+
 	(cd $testroot/wt && got update -b master > /dev/null)
 	(cd $testroot/wt && got rm beta d/f/g/new > /dev/null)
 	(cd $testroot/wt && got commit \
@@ -1197,12 +1200,18 @@ test_rebase_delete_missing_file() {
 	local master_commit=`git_show_head $testroot/repo`
 
 	(cd $testroot/wt && got update -b master > /dev/null)
-	(cd $testroot/wt && got rebase newbranch > $testroot/stdout)
+	(cd $testroot/wt && got rebase newbranch > $testroot/stdout \
+		2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" = "0" ]; then
+		echo "rebase succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
 
-	(cd $testroot/repo && git checkout -q newbranch)
-	local new_commit1=`git_show_head $testroot/repo`
+	local new_commit1=$(cd $testroot/wt && got info | \
+		grep '^work tree base commit: ' | cut -d: -f2 | tr -d ' ')
 
-	local short_orig_commit1=`trim_obj_id 28 $orig_commit1`
 	local short_orig_commit2=`trim_obj_id 28 $orig_commit2`
 	local short_new_commit1=`trim_obj_id 28 $new_commit1`
 
@@ -1215,8 +1224,37 @@ test_rebase_delete_missing_file() {
 	echo -n "Files which had incoming changes but could not be found " \
 		>> $testroot/stdout.expected
 	echo "in the work tree: 2" >> $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
+
+	echo -n "got: changes destined for some files were not yet merged " \
+		> $testroot/stderr.expected
+	echo -n "and should be merged manually if required before the " \
+		>> $testroot/stderr.expected
+	echo "rebase operation is continued" >> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# ignore the missing changes and continue
+	(cd $testroot/wt && got rebase -c > $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "rebase failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
 	echo -n "$short_orig_commit2 -> no-op change" \
-		>> $testroot/stdout.expected
+		> $testroot/stdout.expected
 	echo ": removing beta and d/f/g/new on newbranch" \
 		>> $testroot/stdout.expected
 	echo "Switching work tree to refs/heads/newbranch" \
@@ -1257,6 +1295,10 @@ test_rebase_delete_missing_file() {
 		return 1
 	fi
 
+	(cd $testroot/repo && git checkout -q newbranch)
+	local new_commit1=`git_show_head $testroot/repo`
+	local short_new_commit1=`trim_obj_id 28 $new_commit1`
+
 	(cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout)
 	echo "commit $new_commit1 (newbranch)" > $testroot/stdout.expected
 	echo "commit $master_commit (master)" >> $testroot/stdout.expected