Commit db6d8ad82e16cafbf76d8b787390a449a357a068

Stefan Sperling 2020-03-21T19:40:36

prevent existing tags from being overwritten by 'got fetch' by default

diff --git a/got/got.1 b/got/got.1
index a9c53ae..e6c4598 100644
--- a/got/got.1
+++ b/got/got.1
@@ -275,7 +275,7 @@ The maximum is 3.
 .It Cm cl
 Short alias for
 .Cm clone .
-.It Cm fetch Oo Fl a Oc Oo Fl b Ar branch Oc Oo Fl d Oc Oo Fl l Oc Oo Fl r Ar repository-path Oc Oo Fl q Oc Oo Fl v Oc Op Ar remote-repository
+.It Cm fetch Oo Fl a Oc Oo Fl b Ar branch Oc Oo Fl d Oc Oo Fl l Oc Oo Fl r Ar repository-path Oc Oo Fl t Oc Oo Fl q Oc Oo Fl v Oc Op Ar remote-repository
 Fetch new changes from a remote repository.
 If no
 .Ar remote-repository
@@ -309,10 +309,10 @@ If those branches contained local commits, these commits will no longer be
 reachable via a reference and will therefore be at risk of being discarded
 by Git's garbage collector.
 .Pp
-In any case, existing references in the
+In any case, references in the
 .Dq refs/tags/
-namespace will always be changed to match tags contained in the remote
-repository.
+namespace will always be fetched and mapped directly to local references
+in the same namespace.
 .Pp
 The options for
 .Cm got fetch
@@ -350,6 +350,11 @@ Cannot be used together with any of the other options except
 .Fl v
 and
 .Fl r .
+.It Fl t
+Allow existing references in the
+.Dq refs/tags
+namespace to be updated if they have changed on the server.
+If not specified, only new tag references will be created.
 .It Fl r Ar repository-path
 Use the repository at the specified path.
 If not specified, assume the repository is located at or above the current
diff --git a/got/got.c b/got/got.c
index b62a8f0..989af2e 100644
--- a/got/got.c
+++ b/got/got.c
@@ -1322,7 +1322,7 @@ done:
 
 static const struct got_error *
 update_ref(struct got_reference *ref, struct got_object_id *new_id,
-    int verbosity, struct got_repository *repo)
+    int replace_tags, int verbosity, struct got_repository *repo)
 {
 	const struct got_error *err = NULL;
 	char *new_id_str = NULL;
@@ -1332,6 +1332,15 @@ update_ref(struct got_reference *ref, struct got_object_id *new_id,
 	if (err)
 		goto done;
 
+	if (!replace_tags &&
+	    strncmp(got_ref_get_name(ref), "refs/tags/", 10) == 0) {
+		if (verbosity >= 0) {
+			printf("Rejecting update of existing tag %s: %s\n",
+			    got_ref_get_name(ref), new_id_str);
+		}
+		goto done;
+	}
+
 	if (got_ref_is_symbolic(ref)) {
 		struct got_reference *new_ref;
 		err = got_ref_alloc(&new_ref, got_ref_get_name(ref), new_id);
@@ -1376,7 +1385,7 @@ __dead static void
 usage_fetch(void)
 {
 	fprintf(stderr, "usage: %s fetch [-a] [-b branch] [-d] [-l] "
-	    "[-r repository-path] [-q] [-v] [remote-repository-name]\n",
+	    "[-r repository-path] [-t] [-q] [-v] [remote-repository-name]\n",
 	    getprogname());
 	exit(1);
 }
@@ -1453,13 +1462,13 @@ cmd_fetch(int argc, char *argv[])
 	pid_t fetchpid = -1;
 	struct got_fetch_progress_arg fpa;
 	int verbosity = 0, fetch_all_branches = 0, list_refs_only = 0;
-	int delete_refs = 0;
+	int delete_refs = 0, replace_tags = 0;
 
 	TAILQ_INIT(&refs);
 	TAILQ_INIT(&symrefs);
 	TAILQ_INIT(&wanted_branches);
 
-	while ((ch = getopt(argc, argv, "ab:dlr:vq")) != -1) {
+	while ((ch = getopt(argc, argv, "ab:dlr:tvq")) != -1) {
 		switch (ch) {
 		case 'a':
 			fetch_all_branches = 1;
@@ -1483,6 +1492,9 @@ cmd_fetch(int argc, char *argv[])
 				    optarg);
 			got_path_strip_trailing_slashes(repo_path);
 			break;
+		case 't':
+			replace_tags = 1;
+			break;
 		case 'v':
 			if (verbosity < 0)
 				verbosity = 0;
@@ -1660,7 +1672,8 @@ cmd_fetch(int argc, char *argv[])
 				if (error)
 					goto done;
 			} else {
-				error = update_ref(ref, id, verbosity, repo);
+				error = update_ref(ref, id, replace_tags,
+				    verbosity, repo);
 				got_ref_close(ref);
 				if (error)
 					goto done;
@@ -1681,7 +1694,8 @@ cmd_fetch(int argc, char *argv[])
 				if (error)
 					goto done;
 			} else {
-				error = update_ref(ref, id, verbosity, repo);
+				error = update_ref(ref, id, replace_tags,
+				    verbosity, repo);
 				got_ref_close(ref);
 				if (error)
 					goto done;
diff --git a/regress/cmdline/fetch.sh b/regress/cmdline/fetch.sh
index bfb411b..374586a 100755
--- a/regress/cmdline/fetch.sh
+++ b/regress/cmdline/fetch.sh
@@ -490,9 +490,162 @@ function test_fetch_delete_branch {
 
 }
 
+function test_fetch_update_tag {
+	local testroot=`test_init fetch_update_tag`
+	local testurl=ssh://127.0.0.1/$testroot
+	local commit_id=`git_show_head $testroot/repo`
+
+
+	got branch -r $testroot/repo -c $commit_id foo
+	got ref -r $testroot/repo refs/hoo/boo/zoo $commit_id
+	got tag -r $testroot/repo -c $commit_id -m tag "1.0" >/dev/null
+	local tag_id=`got ref -r $testroot/repo -l \
+		| grep "^refs/tags/$tag" | tr -d ' ' | cut -d: -f2`
+
+	got clone -a -q $testurl/repo $testroot/repo-clone
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got clone command failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "modified alpha on master" > $testroot/repo/alpha
+	git_commit $testroot/repo -m "modified alpha"
+	local commit_id2=`git_show_head $testroot/repo`
+
+	got ref -r $testroot/repo -d "refs/tags/1.0"  >/dev/null
+	got tag -r $testroot/repo -c $commit_id2 -m tag "1.0" >/dev/null
+	local tag_id2=`got ref -r $testroot/repo -l \
+		| grep "^refs/tags/$tag" | tr -d ' ' | cut -d: -f2`
+
+	got ref -l -r $testroot/repo-clone > $testroot/stdout
+
+	echo "HEAD: refs/heads/master" > $testroot/stdout.expected
+	echo "refs/heads/foo: $commit_id" >> $testroot/stdout.expected
+	echo "refs/heads/master: $commit_id" >> $testroot/stdout.expected
+	echo "refs/remotes/origin/foo: $commit_id" \
+		>> $testroot/stdout.expected
+	echo "refs/remotes/origin/master: $commit_id" \
+		>> $testroot/stdout.expected
+	# refs/hoo/boo/zoo is missing because it is outside of refs/heads
+	echo "refs/tags/1.0: $tag_id" >> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	got fetch -q -r $testroot/repo-clone
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got fetch command failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	got ref -l -r $testroot/repo-clone > $testroot/stdout
+
+	echo "HEAD: refs/heads/master" > $testroot/stdout.expected
+	echo "refs/heads/foo: $commit_id" >> $testroot/stdout.expected
+	echo "refs/heads/master: $commit_id" >> $testroot/stdout.expected
+	echo "refs/remotes/origin/foo: $commit_id" >> $testroot/stdout.expected
+	echo "refs/remotes/origin/master: $commit_id2" \
+		>> $testroot/stdout.expected
+	# refs/hoo/boo/zoo is missing because it is outside of refs/heads
+	echo "refs/tags/1.0: $tag_id" >> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	got fetch -r $testroot/repo-clone 2> $testroot/stderr | \
+		tail -n 1 > $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got fetch command failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "Rejecting update of existing tag refs/tags/1.0: $tag_id2" \
+		> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	got ref -l -r $testroot/repo-clone > $testroot/stdout
+
+	echo "HEAD: refs/heads/master" > $testroot/stdout.expected
+	echo "refs/heads/foo: $commit_id" >> $testroot/stdout.expected
+	echo "refs/heads/master: $commit_id" >> $testroot/stdout.expected
+	echo "refs/remotes/origin/foo: $commit_id" >> $testroot/stdout.expected
+	echo "refs/remotes/origin/master: $commit_id2" \
+		>> $testroot/stdout.expected
+	# refs/hoo/boo/zoo is missing because it is outside of refs/heads
+	echo "refs/tags/1.0: $tag_id" >> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	got fetch -q -t -r $testroot/repo-clone > $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got fetch command failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo -n > $testroot/stdout.expected
+
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	got ref -l -r $testroot/repo-clone > $testroot/stdout
+
+	echo "HEAD: refs/heads/master" > $testroot/stdout.expected
+	echo "refs/heads/foo: $commit_id" >> $testroot/stdout.expected
+	echo "refs/heads/master: $commit_id" >> $testroot/stdout.expected
+	echo "refs/remotes/origin/foo: $commit_id" >> $testroot/stdout.expected
+	echo "refs/remotes/origin/master: $commit_id2" \
+		>> $testroot/stdout.expected
+	# refs/hoo/boo/zoo is missing because it is outside of refs/heads
+	echo "refs/tags/1.0: $tag_id2" >> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
 run_test test_fetch_basic
 run_test test_fetch_list
 run_test test_fetch_branch
 run_test test_fetch_all
 run_test test_fetch_empty_packfile
 run_test test_fetch_delete_branch
+run_test test_fetch_update_tag