Commit Diff


commit - aa0926921193099319156437044e75e4b7b702e1
commit + 36bf999ca5297d07cc17f79a154b0875c1574148
blob - 22c99171fca9a1866a27a828b37e35a9114f2a8f
blob + d7c970469aa82014707191e80167f76aa30fb17e
--- lib/worktree.c
+++ lib/worktree.c
@@ -972,20 +972,19 @@ merge_blob(int *, struct got_worktree *, struct got_bl
 
 /*
  * Merge a symlink into the work tree, where blob_orig acts as the common
- * ancestor, blob_deriv acts as the first derived version, and the symlink
- * on disk acts as the second derived version.
+ * ancestor, deriv_target is the link target of the first derived version,
+ * and the symlink on disk acts as the second derived version.
  * Assume that contents of both blobs represent symlinks.
  */
 static const struct got_error *
 merge_symlink(struct got_worktree *worktree,
     struct got_blob_object *blob_orig, const char *ondisk_path,
-    const char *path, const char *label_orig,
-    struct got_blob_object *blob_deriv,
+    const char *path, const char *label_orig, const char *deriv_target,
     struct got_object_id *deriv_base_commit_id, struct got_repository *repo,
     got_worktree_checkout_cb progress_cb, void *progress_arg)
 {
 	const struct got_error *err = NULL;
-	char *ancestor_target = NULL, *deriv_target = NULL;
+	char *ancestor_target = NULL;
 	struct stat sb;
 	ssize_t ondisk_len, deriv_len;
 	char ondisk_target[PATH_MAX];
@@ -1009,10 +1008,6 @@ merge_symlink(struct got_worktree *worktree,
 		if (err)
 			goto done;
 	}
-
-	err = got_object_blob_read_to_str(&deriv_target, blob_deriv);
-	if (err)
-		goto done;
 
 	if (ancestor_target == NULL ||
 	    (ondisk_len != strlen(ancestor_target) ||
@@ -1071,7 +1066,6 @@ merge_symlink(struct got_worktree *worktree,
 
 done:
 	free(ancestor_target);
-	free(deriv_target);
 	return err;
 }
 
@@ -1913,10 +1907,14 @@ update_blob(struct got_worktree *worktree,
 			}
 		}
 		if (S_ISLNK(te->mode) && S_ISLNK(sb.st_mode)) {
-			err = merge_symlink(worktree, blob2,
-			    ondisk_path, path, label_orig, blob,
-			    worktree->base_commit_id, repo,
-			    progress_cb, progress_arg);
+			char *link_target;
+			err = got_object_blob_read_to_str(&link_target, blob);
+			if (err)
+				goto done;
+			err = merge_symlink(worktree, blob2, ondisk_path, path,
+			    label_orig, link_target, worktree->base_commit_id,
+			    repo, progress_cb, progress_arg);
+			free(link_target);
 		} else {
 			err = merge_blob(&update_timestamps, worktree, blob2,
 			    ondisk_path, path, sb.st_mode, label_orig, blob,
@@ -2718,9 +2716,14 @@ merge_file_cb(void *arg, struct got_blob_object *blob1
 		}
 
 		if (S_ISLNK(mode1) && S_ISLNK(mode2)) {
+			char *link_target2;
+			err = got_object_blob_read_to_str(&link_target2, blob2);
+			if (err)
+				goto done;
 			err = merge_symlink(a->worktree, blob1, ondisk_path,
-			    path2, a->label_orig, blob2, a->commit_id2, repo,
-			    a->progress_cb, a->progress_arg);
+			    path2, a->label_orig, link_target2, a->commit_id2,
+			    repo, a->progress_cb, a->progress_arg);
+			free(link_target2);
 		} else {
 			err = merge_blob(&local_changes_subsumed, a->worktree,
 			    blob1, ondisk_path, path2, sb.st_mode,
@@ -2800,10 +2803,16 @@ merge_file_cb(void *arg, struct got_blob_object *blob1
 				goto done;
 			}
 			if (S_ISLNK(mode2) && S_ISLNK(sb.st_mode)) {
+				char *link_target2;
+				err = got_object_blob_read_to_str(&link_target2,
+				    blob2);
+				if (err)
+					goto done;
 				err = merge_symlink(a->worktree, NULL,
 				    ondisk_path, path2, a->label_orig,
-				    blob2, a->commit_id2, repo,
+				    link_target2, a->commit_id2, repo,
 				    a->progress_cb, a->progress_arg);
+				free(link_target2);
 			} else if (S_ISREG(sb.st_mode)) {
 				err = merge_blob(&local_changes_subsumed,
 				    a->worktree, NULL, ondisk_path, path2,
@@ -7457,12 +7466,51 @@ unstage_path(void *arg, unsigned char status,
 					    staged_blob_id->sha1,
 					    SHA1_DIGEST_LENGTH);
 				}
-				err = merge_file(&local_changes_subsumed,
-				    a->worktree, blob_base, ondisk_path,
-				    relpath, got_fileindex_perms_to_st(ie),
-				    path_unstaged_content, label_orig,
-				    "unstaged", a->repo, a->progress_cb,
-				    a->progress_arg);
+				if (got_fileindex_entry_staged_filetype_get(ie)
+				    == GOT_FILEIDX_MODE_SYMLINK) {
+					char unstaged_target[PATH_MAX];
+					FILE *f;
+					size_t r;
+					f = fopen(path_unstaged_content, "r");
+					if (f == NULL) {
+						err = got_error_from_errno2(
+						    "fopen",
+						    path_unstaged_content);
+						goto done;
+					}
+					r = fread(unstaged_target, 1,
+					    sizeof(unstaged_target), f);
+					if (r == 0 && ferror(f)) {
+						err = got_error_from_errno(
+						    "fread");
+						fclose(f);
+						break;
+					}
+					if (fclose(f) == EOF) {
+						err = got_error_from_errno2(
+						    "fclose",
+						    path_unstaged_content);
+					}
+					if (r >= sizeof(unstaged_target)) {
+						err = got_error(
+						    GOT_ERR_NO_SPACE);
+						goto done;
+					}
+					unstaged_target[r] = '\0';
+					err = merge_symlink(a->worktree,
+					    blob_base, ondisk_path, relpath,
+					    label_orig, unstaged_target,
+					    a->worktree->base_commit_id,
+					    a->repo, a->progress_cb,
+					    a->progress_arg);
+				} else {
+					err = merge_file(&local_changes_subsumed,
+					    a->worktree, blob_base, ondisk_path,
+					    relpath, got_fileindex_perms_to_st(ie),
+					    path_unstaged_content, label_orig,
+					    "unstaged", a->repo, a->progress_cb,
+					    a->progress_arg);
+				}
 				if (err == NULL &&
 				    path_new_staged_content == NULL)
 					got_fileindex_entry_stage_set(ie,
@@ -7486,11 +7534,17 @@ unstage_path(void *arg, unsigned char status,
 			break;
 		case GOT_FILEIDX_MODE_SYMLINK:
 			if (S_ISLNK(got_fileindex_perms_to_st(ie))) {
+				char *staged_target;
+				err = got_object_blob_read_to_str(
+				    &staged_target, blob_staged);
+				if (err)
+					goto done;
 				err = merge_symlink(a->worktree, blob_base,
 				    ondisk_path, relpath, label_orig,
-				    blob_staged, commit_id ? commit_id :
+				    staged_target, commit_id ? commit_id :
 				    a->worktree->base_commit_id,
 				    a->repo, a->progress_cb, a->progress_arg);
+				free(staged_target);
 			} else {
 				err = merge_blob(&local_changes_subsumed,
 				    a->worktree, blob_base, ondisk_path,
blob - d01d1ed083c9dd96adb1abead9c4bc6d42fa5c87
blob + 3cd20d37557eceb704929d98cb9c43eab91e88eb
--- regress/cmdline/unstage.sh
+++ regress/cmdline/unstage.sh
@@ -1134,7 +1134,289 @@ EOF
 
 	test_done "$testroot" "0"
 }
+
+function test_unstage_patch_symlink {
+	local testroot=`test_init unstage_patch_symlink`
+
+	(cd $testroot/repo && ln -s alpha alpha.link)
+	(cd $testroot/repo && ln -s epsilon epsilon.link)
+	(cd $testroot/repo && ln -s /etc/passwd passwd.link)
+	(cd $testroot/repo && ln -s ../beta epsilon/beta.link)
+	(cd $testroot/repo && ln -s nonexistent nonexistent.link)
+	(cd $testroot/repo && ln -sf epsilon/zeta zeta.link)
+	(cd $testroot/repo && ln -sf epsilon/zeta zeta2.link)
+	(cd $testroot/repo && git add .)
+	git_commit $testroot/repo -m "add symlinks"
+	local commit_id1=`git_show_head $testroot/repo`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# symlink to file A now points to file B
+	(cd $testroot/wt && ln -sf gamma/delta alpha.link)
+	# symlink to a directory A now points to file B
+	(cd $testroot/wt && ln -sfh beta epsilon.link)
+	# "bad" symlink now contains a different target path
+	echo "foo" > $testroot/wt/passwd.link
+	# relative symlink to directory A now points to relative directory B
+	(cd $testroot/wt && ln -sfh ../gamma epsilon/beta.link)
+	# an unversioned symlink
+	(cd $testroot/wt && ln -sf .got/foo dotgotfoo.link)
+	# symlink to file A now points to non-existent file B
+	(cd $testroot/wt && ln -sf nonexistent2 nonexistent.link)
+	# removed symlink
+	(cd $testroot/wt && got rm zeta.link > /dev/null)
+	(cd $testroot/wt && got rm zeta2.link > /dev/null)
+	# added symlink
+	(cd $testroot/wt && ln -sf beta new.link)
+	(cd $testroot/wt && got add new.link > /dev/null)
+	(cd $testroot/wt && ln -sf beta zeta3.link)
+	(cd $testroot/wt && got add zeta3.link > /dev/null)
 
+	(cd $testroot/wt && got stage -S > /dev/null)
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+	cat > $testroot/stdout.expected <<EOF
+ M alpha.link
+?  dotgotfoo.link
+ M epsilon/beta.link
+ M epsilon.link
+ A new.link
+ M nonexistent.link
+ M passwd.link
+ D zeta.link
+ D zeta2.link
+ A zeta3.link
+EOF
+	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
+
+	printf "y\nn\ny\nn\ny\ny\nn\ny\ny\n" > $testroot/patchscript
+	(cd $testroot/wt && got unstage -F $testroot/patchscript -p \
+		> $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got unstage command failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cat > $testroot/stdout.expected <<EOF
+-----------------------------------------------
+@@ -1 +1 @@
+-alpha
+\ No newline at end of file
++gamma/delta
+\ No newline at end of file
+-----------------------------------------------
+M  alpha.link (change 1 of 1)
+unstage this change? [y/n/q] y
+G  alpha.link
+-----------------------------------------------
+@@ -1 +1 @@
+-../beta
+\ No newline at end of file
++../gamma
+\ No newline at end of file
+-----------------------------------------------
+M  epsilon/beta.link (change 1 of 1)
+unstage this change? [y/n/q] n
+-----------------------------------------------
+@@ -1 +1 @@
+-epsilon
+\ No newline at end of file
++beta
+\ No newline at end of file
+-----------------------------------------------
+M  epsilon.link (change 1 of 1)
+unstage this change? [y/n/q] y
+G  epsilon.link
+A  new.link
+unstage this addition? [y/n] n
+-----------------------------------------------
+@@ -1 +1 @@
+-nonexistent
+\ No newline at end of file
++nonexistent2
+\ No newline at end of file
+-----------------------------------------------
+M  nonexistent.link (change 1 of 1)
+unstage this change? [y/n/q] y
+G  nonexistent.link
+-----------------------------------------------
+@@ -1 +1 @@
+-/etc/passwd
+\ No newline at end of file
++foo
+-----------------------------------------------
+M  passwd.link (change 1 of 1)
+unstage this change? [y/n/q] y
+G  passwd.link
+D  zeta.link
+unstage this deletion? [y/n] n
+D  zeta2.link
+unstage this deletion? [y/n] y
+D  zeta2.link
+A  zeta3.link
+unstage this addition? [y/n] y
+G  zeta3.link
+EOF
+	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
+
+	if ! [ -h $testroot/wt/alpha.link ]; then
+		echo "alpha.link is not a symlink"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	readlink $testroot/wt/alpha.link > $testroot/stdout
+	echo "gamma/delta" > $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
+
+	if ! [ -h $testroot/wt/epsilon.link ]; then
+		echo "epsilon.link is not a symlink"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	readlink $testroot/wt/epsilon.link > $testroot/stdout
+	echo "beta" > $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
+
+	if [ -h $testroot/wt/passwd.link ]; then
+		echo "passwd.link should not be a symlink" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "foo" > $testroot/content.expected
+	cp $testroot/wt/passwd.link $testroot/content
+
+	cmp -s $testroot/content.expected $testroot/content
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	readlink $testroot/wt/epsilon/beta.link > $testroot/stdout
+	echo "../gamma" > $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
+
+	readlink $testroot/wt/nonexistent.link > $testroot/stdout
+	echo "nonexistent2" > $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
+
+	if [ ! -h $testroot/wt/dotgotfoo.link ]; then
+		echo "dotgotfoo.link is not a symlink " >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+	readlink $testroot/wt/dotgotfoo.link > $testroot/stdout
+	echo ".got/foo" > $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
+
+
+	if [ -e $testroot/wt/zeta.link ]; then
+		echo -n "zeta.link should not exist on disk" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	if [ -e $testroot/wt/zeta2.link ]; then
+		echo -n "zeta2.link exists on disk" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	if [ ! -h $testroot/wt/zeta3.link ]; then
+		echo -n "zeta3.link is not a symlink" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	readlink $testroot/wt/zeta3.link > $testroot/stdout
+	echo "beta" > $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
+
+	if [ ! -h $testroot/wt/new.link ]; then
+		echo -n "new.link is not a symlink" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+	echo "M  alpha.link" > $testroot/stdout.expected
+	echo "?  dotgotfoo.link" >> $testroot/stdout.expected
+	echo " M epsilon/beta.link" >> $testroot/stdout.expected
+	echo "M  epsilon.link" >> $testroot/stdout.expected
+	echo " A new.link" >> $testroot/stdout.expected
+	echo "M  nonexistent.link" >> $testroot/stdout.expected
+	echo "M  passwd.link" >> $testroot/stdout.expected
+	echo " D zeta.link" >> $testroot/stdout.expected
+	echo "D  zeta2.link" >> $testroot/stdout.expected
+	echo "A  zeta3.link" >> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		return 1
+	fi
+	test_done "$testroot" "$ret"
+}
+
 run_test test_unstage_basic
 run_test test_unstage_unversioned
 run_test test_unstage_nonexistent
@@ -1143,3 +1425,4 @@ run_test test_unstage_patch_added
 run_test test_unstage_patch_removed
 run_test test_unstage_patch_quit
 run_test test_unstage_symlink
+run_test test_unstage_patch_symlink