Commit 7f47418fd49bc98fe4570c139767c057cd066409

Stefan Sperling 2019-12-20T15:54:59

make 'got checkout' and 'got update' work with read-only repositories but warn users about the garbage collection problem

diff --git a/got/got.c b/got/got.c
index 98571cf..e658e62 100644
--- a/got/got.c
+++ b/got/got.c
@@ -804,19 +804,39 @@ usage_checkout(void)
 	exit(1);
 }
 
+static void
+show_worktree_base_ref_warning(void)
+{
+	fprintf(stderr, "%s: warning: could not create a reference "
+	    "to the work tree's base commit; the commit could be "
+	    "garbage-collected by Git; making the repository "
+	    "writable and running 'got update' will prevent this\n",
+	    getprogname());
+}
+
+struct got_checkout_progress_arg {
+	const char *worktree_path;
+	int had_base_commit_ref_error;
+};
+
 static const struct got_error *
 checkout_progress(void *arg, unsigned char status, const char *path)
 {
-	char *worktree_path = arg;
+	struct got_checkout_progress_arg *a = arg;
 
 	/* Base commit bump happens silently. */
 	if (status == GOT_STATUS_BUMP_BASE)
 		return NULL;
 
+	if (status == GOT_STATUS_BASE_REF_ERR) {
+		a->had_base_commit_ref_error = 1;
+		return NULL;
+	}
+
 	while (path[0] == '/')
 		path++;
 
-	printf("%c  %s/%s\n", status, worktree_path, path);
+	printf("%c  %s/%s\n", status, a->worktree_path, path);
 	return NULL;
 }
 
@@ -985,6 +1005,7 @@ cmd_checkout(int argc, char *argv[])
 	char *commit_id_str = NULL;
 	int ch, same_path_prefix;
 	struct got_pathlist_head paths;
+	struct got_checkout_progress_arg cpa;
 
 	TAILQ_INIT(&paths);
 
@@ -1140,12 +1161,16 @@ cmd_checkout(int argc, char *argv[])
 	error = got_pathlist_append(&paths, "", NULL);
 	if (error)
 		goto done;
+	cpa.worktree_path = worktree_path;
+	cpa.had_base_commit_ref_error = 0;
 	error = got_worktree_checkout_files(worktree, &paths, repo,
-	    checkout_progress, worktree_path, check_cancelled, NULL);
+	    checkout_progress, &cpa, check_cancelled, NULL);
 	if (error != NULL)
 		goto done;
 
 	printf("Now shut up and hack\n");
+	if (cpa.had_base_commit_ref_error)
+		show_worktree_base_ref_warning();
 
 done:
 	got_pathlist_free(&paths);
@@ -1168,7 +1193,8 @@ update_progress(void *arg, unsigned char status, const char *path)
 {
 	int *did_something = arg;
 
-	if (status == GOT_STATUS_EXISTS)
+	if (status == GOT_STATUS_EXISTS ||
+	    status == GOT_STATUS_BASE_REF_ERR)
 		return NULL;
 
 	*did_something = 1;
diff --git a/include/got_worktree.h b/include/got_worktree.h
index 09b7625..7ee3eae 100644
--- a/include/got_worktree.h
+++ b/include/got_worktree.h
@@ -36,6 +36,7 @@ struct got_fileindex;
 #define GOT_STATUS_REVERT	'R'
 #define GOT_STATUS_CANNOT_DELETE 'd'
 #define GOT_STATUS_BUMP_BASE	'b'
+#define GOT_STATUS_BASE_REF_ERR	'B'
 
 /*
  * Attempt to initialize a new work tree on disk.
diff --git a/lib/worktree.c b/lib/worktree.c
index a8f6827..ba58428 100644
--- a/lib/worktree.c
+++ b/lib/worktree.c
@@ -1881,8 +1881,14 @@ checkout_files(struct got_worktree *worktree, struct got_fileindex *fileindex,
 	struct diff_cb_arg arg;
 
 	err = ref_base_commit(worktree, repo);
-	if (err)
-		goto done;
+	if (err) {
+		if (!(err->code == GOT_ERR_ERRNO && errno == EACCES))
+			goto done;
+		err = (*progress_cb)(progress_arg,
+		    GOT_STATUS_BASE_REF_ERR, worktree->root_path);
+		if (err)
+			return err;
+	}
 
 	err = got_object_open_as_commit(&commit, repo,
 	   worktree->base_commit_id);
diff --git a/regress/cmdline/checkout.sh b/regress/cmdline/checkout.sh
index 76ad000..34747be 100755
--- a/regress/cmdline/checkout.sh
+++ b/regress/cmdline/checkout.sh
@@ -296,6 +296,66 @@ function test_checkout_ignores_submodules {
 	test_done "$testroot" "$ret"
 }
 
+function test_checkout_read_only {
+	local testroot=`test_init checkout_read_only`
+
+	# Make the repostiory read-only
+	chmod -R a-w $testroot/repo
+
+	echo "A  $testroot/wt/alpha" > $testroot/stdout.expected
+	echo "A  $testroot/wt/beta" >> $testroot/stdout.expected
+	echo "A  $testroot/wt/epsilon/zeta" >> $testroot/stdout.expected
+	echo "A  $testroot/wt/gamma/delta" >> $testroot/stdout.expected
+	echo "Now shut up and hack" >> $testroot/stdout.expected
+
+	got checkout $testroot/repo $testroot/wt \
+		> $testroot/stdout 2> $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	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: warning: could not create a reference " \
+		> $testroot/stderr.expected
+	echo -n "to the work tree's base commit; the commit could " \
+		>> $testroot/stderr.expected
+	echo -n "be garbage-collected by Git; making the repository " \
+		>> $testroot/stderr.expected
+	echo "writable and running 'got update' will prevent this" \
+		>> $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
+
+	echo "alpha" > $testroot/content.expected
+	echo "beta" >> $testroot/content.expected
+	echo "zeta" >> $testroot/content.expected
+	echo "delta" >> $testroot/content.expected
+	cat $testroot/wt/alpha $testroot/wt/beta $testroot/wt/epsilon/zeta \
+	    $testroot/wt/gamma/delta > $testroot/content
+
+	cmp -s $testroot/content.expected $testroot/content
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/content.expected $testroot/content
+	fi
+	chmod -R u+w $testroot/repo # make repo cleanup work
+	test_done "$testroot" "$ret"
+}
+
 run_test test_checkout_basic
 run_test test_checkout_dir_exists
 run_test test_checkout_dir_not_empty
@@ -303,3 +363,4 @@ run_test test_checkout_sets_xbit
 run_test test_checkout_commit_from_wrong_branch
 run_test test_checkout_tag
 run_test test_checkout_ignores_submodules
+run_test test_checkout_read_only