Commit 64c6d99023a672f220d1de2662dfe52300f51ea2

Stefan Sperling 2019-07-11T16:37:36

prevent rebase in a work tree with an incompatible path prefix

diff --git a/got/got.1 b/got/got.1
index 1780abc..f5918cd 100644
--- a/got/got.1
+++ b/got/got.1
@@ -581,6 +581,10 @@ to a single base commit with
 .Cm got update .
 If the work tree contains local changes, these changes must be committed
 or reverted first.
+If the
+.Ar branch
+contains changes to files outside of the work tree's path prefix,
+the work tree cannot be used to rebase this branch.
 .Pp
 The
 .Cm got update
diff --git a/got/got.c b/got/got.c
index 178e89a..26e1d49 100644
--- a/got/got.c
+++ b/got/got.c
@@ -3311,6 +3311,73 @@ done:
 	return error;
 }
 
+struct check_path_prefix_arg {
+	const char *path_prefix;
+	size_t len;
+};
+
+static const struct got_error *
+check_path_prefix(void *arg, struct got_blob_object *blob1,
+    struct got_blob_object *blob2, struct got_object_id *id1,
+    struct got_object_id *id2, const char *path1, const char *path2,
+    struct got_repository *repo)
+{
+	struct check_path_prefix_arg *a = arg;
+
+	if ((path1 && !got_path_is_child(path1, a->path_prefix, a->len)) ||
+	    (path2 && !got_path_is_child(path2, a->path_prefix, a->len)))
+		return got_error(GOT_ERR_REBASE_PATH);
+
+	return NULL;
+}
+
+static const struct got_error *
+rebase_check_path_prefix(struct got_object_id *parent_id,
+    struct got_object_id *commit_id, const char *path_prefix,
+    struct got_repository *repo)
+{
+	const struct got_error *err;
+	struct got_tree_object *tree1 = NULL, *tree2 = NULL;
+	struct got_commit_object *commit = NULL, *parent_commit = NULL;
+	struct check_path_prefix_arg cpp_arg;
+
+	if (got_path_is_root_dir(path_prefix))
+		return NULL;
+
+	err = got_object_open_as_commit(&commit, repo, commit_id);
+	if (err)
+		goto done;
+
+	err = got_object_open_as_commit(&parent_commit, repo, parent_id);
+	if (err)
+		goto done;
+
+	err = got_object_open_as_tree(&tree1, repo,
+	    got_object_commit_get_tree_id(parent_commit));
+	if (err)
+		goto done;
+
+	err = got_object_open_as_tree(&tree2, repo,
+	    got_object_commit_get_tree_id(commit));
+	if (err)
+		goto done;
+
+	cpp_arg.path_prefix = path_prefix;
+	cpp_arg.len = strlen(path_prefix);
+	err = got_diff_tree(tree1, tree2, "", "", repo, check_path_prefix,
+	    &cpp_arg);
+done:
+	if (tree1)
+		got_object_tree_close(tree1);
+	if (tree2)
+		got_object_tree_close(tree2);
+	if (commit)
+		got_object_commit_close(commit);
+	if (parent_commit)
+		got_object_commit_close(parent_commit);
+	return err;
+}
+
 static const struct got_error *
 cmd_rebase(int argc, char *argv[])
 {
@@ -3487,6 +3554,11 @@ cmd_rebase(int argc, char *argv[])
 			if (error)
 				goto done;
 		} else {
+			error = rebase_check_path_prefix(parent_id, commit_id,
+			    got_worktree_get_path_prefix(worktree), repo);
+			if (error)
+				goto done;
+
 			error = got_object_qid_alloc(&qid, commit_id);
 			if (error)
 				goto done;
diff --git a/include/got_error.h b/include/got_error.h
index 4a4ef0b..48ede3e 100644
--- a/include/got_error.h
+++ b/include/got_error.h
@@ -102,6 +102,7 @@
 #define GOT_ERR_EMPTY_REBASE	86
 #define GOT_ERR_REBASE_COMMITID	87
 #define GOT_ERR_REBASING	88
+#define GOT_ERR_REBASE_PATH	89
 
 static const struct got_error {
 	int code;
@@ -201,6 +202,8 @@ static const struct got_error {
 	{ GOT_ERR_REBASE_COMMITID,"rebase commit ID mismatch" },
 	{ GOT_ERR_REBASING,	"a rebase operation is in progress in this "
 	    "work tree and must be continued or aborted first" },
+	{ GOT_ERR_REBASE_PATH,	"cannot rebase branch which contains "
+	    "changes outside of this work tree's path prefix" },
 };
 
 /*
diff --git a/regress/cmdline/rebase.sh b/regress/cmdline/rebase.sh
index 91b9ba8..7ea64d7 100755
--- a/regress/cmdline/rebase.sh
+++ b/regress/cmdline/rebase.sh
@@ -589,9 +589,55 @@ function test_rebase_in_progress {
 	test_done "$testroot" "$ret"
 }
 
+function test_rebase_path_prefix {
+	local testroot=`test_init rebase_path_prefix`
+
+	(cd $testroot/repo && git checkout -q -b newbranch)
+	echo "modified delta on branch" > $testroot/repo/gamma/delta
+	git_commit $testroot/repo -m "committing to delta on newbranch"
+
+	local orig_commit1=`git_show_parent_commit $testroot/repo`
+	local orig_commit2=`git_show_head $testroot/repo`
+
+	(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"
+	local master_commit=`git_show_head $testroot/repo`
+
+	got checkout -p epsilon $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got rebase newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+
+	echo -n > $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: cannot rebase branch which contains changes outside " \
+		> $testroot/stderr.expected
+	echo "of this work tree's path prefix" >> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+	fi
+	test_done "$testroot" "$ret"
+}
+
 run_test test_rebase_basic
 run_test test_rebase_ancestry_check
 run_test test_rebase_continue
 run_test test_rebase_abort
 run_test test_rebase_no_op_change
 run_test test_rebase_in_progress
+run_test test_rebase_path_prefix