Commit Diff


commit - 56d815a9328f873fd6818ac625999d06a8752a45
commit + 993e2a1b1ac61a9d56877df5325d519f7b737375
blob - f6e9f099eeee7a69e9bd4089987f1a6883cee164
blob + 7468ff6fc6fe2ee999848cdbe8cb3db5c139cf01
--- lib/worktree.c
+++ lib/worktree.c
@@ -846,8 +846,14 @@ update_symlink(const char *ondisk_path, const char *ta
 /*
  * Overwrite a symlink (or a regular file in case there was a "bad" symlink)
  * in the work tree with a file that contains conflict markers and the
- * conflicting target paths of the original version and two derived versions
- * of a symlink.
+ * conflicting target paths of the original version, a "derived version"
+ * of a symlink from an incoming change, and a local version of the symlink.
+ *
+ * The original versions's target path can be NULL if it is not available,
+ * such as if both derived versions added a new symlink at the same path.
+ *
+ * The incoming derived symlink target is NULL in case the incoming change
+ * has deleted this symlink.
  */
 static const struct got_error *
 install_symlink_conflict(const char *deriv_target,
@@ -878,7 +884,8 @@ install_symlink_conflict(const char *deriv_target,
 	    "contents may be filled in.\nThe following conflicting symlink "
 	    "target paths were found:\n"
 	    "%s %s\n%s\n%s%s%s%s%s\n%s\n%s\n", getprogname(),
-	    GOT_DIFF_CONFLICT_MARKER_BEGIN, label_deriv, deriv_target,
+	    GOT_DIFF_CONFLICT_MARKER_BEGIN, label_deriv,
+	    deriv_target ? deriv_target : "(symlink was deleted)",
 	    orig_target ? label_orig : "",
 	    orig_target ? "\n" : "",
 	    orig_target ? orig_target : "",
@@ -1820,10 +1827,17 @@ update_blob(struct got_worktree *worktree,
 				goto done;
 			}
 		}
-		err = merge_blob(&update_timestamps, worktree, blob2,
-		    ondisk_path, path, sb.st_mode, label_orig, blob,
-		    worktree->base_commit_id, repo,
-		    progress_cb, progress_arg);
+		if (S_ISLNK(te->mode)) {
+			err = merge_symlink(worktree, blob2,
+			    ondisk_path, path, sb.st_mode, label_orig,
+			    blob, worktree->base_commit_id, repo,
+			    progress_cb, progress_arg);
+		} else {
+			err = merge_blob(&update_timestamps, worktree, blob2,
+			    ondisk_path, path, sb.st_mode, label_orig, blob,
+			    worktree->base_commit_id, repo,
+			    progress_cb, progress_arg);
+		}
 		free(label_orig);
 		if (blob2)
 			got_object_blob_close(blob2);
@@ -1920,6 +1934,25 @@ delete_blob(struct got_worktree *worktree, struct got_
 	if (err)
 		goto done;
 
+	if (S_ISLNK(sb.st_mode) && status != GOT_STATUS_NO_CHANGE) {
+		char ondisk_target[PATH_MAX];
+		ssize_t ondisk_len = readlink(ondisk_path, ondisk_target,
+		    sizeof(ondisk_target));
+		if (ondisk_len == -1) {
+			err = got_error_from_errno2("readlink", ondisk_path);
+			goto done;
+		}
+		ondisk_target[ondisk_len] = '\0';
+		err = install_symlink_conflict(NULL, worktree->base_commit_id,
+		    NULL, NULL, /* XXX pass common ancestor info? */
+		    ondisk_target, ondisk_path);
+		if (err)
+			goto done;
+		err = (*progress_cb)(progress_arg, GOT_STATUS_CONFLICT,
+		    ie->path);
+		goto done;
+	}
+
 	if (status == GOT_STATUS_MODIFY || status == GOT_STATUS_CONFLICT ||
 	    status == GOT_STATUS_ADD) {
 		err = (*progress_cb)(progress_arg, GOT_STATUS_MERGE, ie->path);
blob - b61d2a00448a591d108d2209b91d0e762aea07f2
blob + 2ee2f8f53e19a50823b280fff77f85ce353b0389
--- regress/cmdline/update.sh
+++ regress/cmdline/update.sh
@@ -1931,6 +1931,322 @@ function test_update_adds_symlink {
 		diff -u $testroot/stdout.expected $testroot/stdout
 	fi
 	test_done "$testroot" "$ret"
+}
+
+function test_update_deletes_symlink {
+	local testroot=`test_init update_deletes_symlink`
+
+	(cd $testroot/repo && ln -s alpha alpha.link)
+	(cd $testroot/repo && git add .)
+	git_commit $testroot/repo -m "add symlink"
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/repo && git rm -q alpha.link)
+	git_commit $testroot/repo -m "delete symlink"
+
+	echo "D  alpha.link" > $testroot/stdout.expected
+	echo -n "Updated to commit " >> $testroot/stdout.expected
+	git_show_head $testroot/repo >> $testroot/stdout.expected
+	echo >> $testroot/stdout.expected
+
+	(cd $testroot/wt && got update > $testroot/stdout)
+
+	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/alpha.link ]; then
+		echo "alpha.link still exists on disk"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	test_done "$testroot" "0"
+}
+
+function test_update_symlink_conflicts {
+	local testroot=`test_init update_symlink_conflicts`
+
+	(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 && 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
+		echo "checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/repo && ln -sf beta alpha.link)
+	(cd $testroot/repo && ln -sfh gamma epsilon.link)
+	(cd $testroot/repo && ln -sf ../gamma/delta epsilon/beta.link)
+	echo 'this is regular file foo' > $testroot/repo/dotgotfoo.link
+	(cd $testroot/repo && ln -sf .got/bar dotgotbar.link)
+	(cd $testroot/repo && git rm -q nonexistent.link)
+	(cd $testroot/repo && ln -sf gamma/delta zeta.link)
+	(cd $testroot/repo && ln -sf alpha new.link)
+	(cd $testroot/repo && git add .)
+	git_commit $testroot/repo -m "change symlinks"
+	local commit_id2=`git_show_head $testroot/repo`
+
+	# modified symlink to file A vs modified symlink to file B
+	(cd $testroot/wt && ln -sf gamma/delta alpha.link)
+	# modified symlink to dir A vs modified symlink to file B
+	(cd $testroot/wt && ln -sfh beta epsilon.link)
+	# modeified symlink to file A vs modified symlink to dir B
+	(cd $testroot/wt && ln -sfh ../gamma epsilon/beta.link)
+	# added regular file A vs added bad symlink to file A
+	(cd $testroot/wt && ln -sf .got/bar dotgotfoo.link)
+	# added bad symlink to file A vs added regular file A
+	echo 'this is regular file bar' > $testroot/wt/dotgotbar.link
+	# removed symlink to non-existent file A vs modified symlink
+	# to nonexistent file B
+	(cd $testroot/wt && ln -sf nonexistent2 nonexistent.link)
+	# modified symlink to file A vs removed symlink to file A
+	(cd $testroot/wt && got rm zeta.link > /dev/null)
+	# added symlink to file A vs added symlink to file B
+	(cd $testroot/wt && ln -sf beta new.link)
+	(cd $testroot/wt && got add new.link > /dev/null)
+
+	(cd $testroot/wt && got update > $testroot/stdout)
+
+	echo "C  alpha.link" >> $testroot/stdout.expected
+	echo "U  dotgotbar.link" >> $testroot/stdout.expected
+	echo "U  dotgotfoo.link" >> $testroot/stdout.expected
+	echo "C  epsilon/beta.link" >> $testroot/stdout.expected
+	echo "C  epsilon.link" >> $testroot/stdout.expected
+	echo "C  new.link" >> $testroot/stdout.expected
+	echo "C  nonexistent.link" >> $testroot/stdout.expected
+	echo "G  zeta.link" >> $testroot/stdout.expected
+	echo -n "Updated to commit " >> $testroot/stdout.expected
+	git_show_head $testroot/repo >> $testroot/stdout.expected
+	echo >> $testroot/stdout.expected
+	echo "Files with new merge conflicts: 5" >> $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/alpha.link ]; then
+		echo "alpha.link is a symlink"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cat > $testroot/symlink-conflict-header <<EOF
+got: Could not install symbolic link because of merge conflict.
+ln(1) may be used to fix the situation. If this is intended to be a
+regular file instead then its expected contents may be filled in.
+The following conflicting symlink target paths were found:
+EOF
+	cp $testroot/symlink-conflict-header $testroot/content.expected
+	echo "<<<<<<< merged change: commit $commit_id2" \
+		>> $testroot/content.expected
+	echo "beta" >> $testroot/content.expected
+	echo "3-way merge base: commit $commit_id1" \
+		>> $testroot/content.expected
+	echo "alpha" >> $testroot/content.expected
+	echo "=======" >> $testroot/content.expected
+	echo "gamma/delta" >> $testroot/content.expected
+	echo '>>>>>>>' >> $testroot/content.expected
+	echo -n "" >> $testroot/content.expected
+
+	cp $testroot/wt/alpha.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
+
+	if [ -h $testroot/wt/epsilon.link ]; then
+		echo "epsilon.link is a symlink"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cp $testroot/symlink-conflict-header $testroot/content.expected
+	echo "<<<<<<< merged change: commit $commit_id2" \
+		>> $testroot/content.expected
+	echo "gamma" >> $testroot/content.expected
+	echo "3-way merge base: commit $commit_id1" \
+		>> $testroot/content.expected
+	echo "epsilon" >> $testroot/content.expected
+	echo "=======" >> $testroot/content.expected
+	echo "beta" >> $testroot/content.expected
+	echo '>>>>>>>' >> $testroot/content.expected
+	echo -n "" >> $testroot/content.expected
+
+	cp $testroot/wt/epsilon.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
+
+	if [ -h $testroot/wt/passwd.link ]; then
+		echo -n "passwd.link symlink points outside of work tree: " >&2
+		readlink $testroot/wt/passwd.link >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo -n "/etc/passwd" > $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
+
+	if [ -h $testroot/wt/epsilon/beta.link ]; then
+		echo "epsilon/beta.link is a symlink"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cp $testroot/symlink-conflict-header $testroot/content.expected
+	echo "<<<<<<< merged change: commit $commit_id2" \
+		>> $testroot/content.expected
+	echo "../gamma/delta" >> $testroot/content.expected
+	echo "3-way merge base: commit $commit_id1" \
+		>> $testroot/content.expected
+	echo "../beta" >> $testroot/content.expected
+	echo "=======" >> $testroot/content.expected
+	echo "../gamma" >> $testroot/content.expected
+	echo '>>>>>>>' >> $testroot/content.expected
+	echo -n "" >> $testroot/content.expected
+
+	cp $testroot/wt/epsilon/beta.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
+
+	if [ -h $testroot/wt/nonexistent.link ]; then
+		echo -n "nonexistent.link still exists on disk: " >&2
+		readlink $testroot/wt/nonexistent.link >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cp $testroot/symlink-conflict-header $testroot/content.expected
+	echo "<<<<<<< merged change: commit $commit_id2" \
+		>> $testroot/content.expected
+	echo "(symlink was deleted)" >> $testroot/content.expected
+	echo "=======" >> $testroot/content.expected
+	echo "nonexistent2" >> $testroot/content.expected
+	echo '>>>>>>>' >> $testroot/content.expected
+	echo -n "" >> $testroot/content.expected
+
+	cp $testroot/wt/nonexistent.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
+
+	if [ -h $testroot/wt/dotgotfoo.link ]; then
+		echo "dotgotfoo.link is a symlink"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "this is regular file foo" > $testroot/content.expected
+	cp $testroot/wt/dotgotfoo.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
+
+	if [ -h $testroot/wt/dotgotbar.link ]; then
+		echo "dotgotbar.link is a symlink"
+		test_done "$testroot" "1"
+		return 1
+	fi
+	echo -n ".got/bar" > $testroot/content.expected
+	cp $testroot/wt/dotgotbar.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
+
+	if [ -h $testroot/wt/new.link ]; then
+		echo "new.link is a symlink"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cp $testroot/symlink-conflict-header $testroot/content.expected
+	echo "<<<<<<< merged change: commit $commit_id2" \
+		>> $testroot/content.expected
+	echo "alpha" >> $testroot/content.expected
+	echo "=======" >> $testroot/content.expected
+	echo "beta" >> $testroot/content.expected
+	echo '>>>>>>>' >> $testroot/content.expected
+	echo -n "" >> $testroot/content.expected
+
+	cp $testroot/wt/new.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
+
+	echo "A  dotgotfoo.link" > $testroot/stdout.expected
+	echo "M  new.link" >> $testroot/stdout.expected
+	echo "D  nonexistent.link" >> $testroot/stdout.expected
+	(cd $testroot/wt && got status > $testroot/stdout)
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "0"
+
 }
 
 run_test test_update_basic
@@ -1969,3 +2285,5 @@ run_test test_update_modified_submodules
 run_test test_update_adds_submodule
 run_test test_update_conflict_wt_file_vs_repo_submodule
 run_test test_update_adds_symlink
+run_test test_update_deletes_symlink
+run_test test_update_symlink_conflicts