Commit 39449a05d564e098f638d1be356d1efbba3ab38c

Stefan Sperling 2020-07-23T14:21:29

make 'got diff' show changes to symlinks in a work tree

diff --git a/got/got.c b/got/got.c
index d2353a4..8d5e78e 100644
--- a/got/got.c
+++ b/got/got.c
@@ -3632,6 +3632,52 @@ struct print_diff_arg {
 	int ignore_whitespace;
 };
 
+/*
+ * Create a file which contains the target path of a symlink so we can feed
+ * it as content to the diff engine.
+ */
+static const struct got_error *
+get_symlink_target_file(int *fd, int dirfd, const char *de_name,
+    const char *abspath)
+{
+	const struct got_error *err = NULL;
+	char target_path[PATH_MAX];
+	ssize_t target_len, outlen;
+
+	*fd = -1;
+
+	if (dirfd != -1) {
+		target_len = readlinkat(dirfd, de_name, target_path, PATH_MAX);
+		if (target_len == -1)
+			return got_error_from_errno2("readlinkat", abspath);
+	} else {
+		target_len = readlink(abspath, target_path, PATH_MAX);
+		if (target_len == -1)
+			return got_error_from_errno2("readlink", abspath);
+	}
+
+	*fd = got_opentempfd();
+	if (*fd == -1)
+		return got_error_from_errno("got_opentempfd");
+
+	outlen = write(*fd, target_path, target_len);
+	if (outlen == -1) {
+		err = got_error_from_errno("got_opentempfd");
+		goto done;
+	}
+
+	if (lseek(*fd, 0, SEEK_SET) == -1) {
+		err = got_error_from_errno2("lseek", abspath);
+		goto done;
+	}
+done:
+	if (err) {
+		close(*fd);
+		*fd = -1;
+	}
+	return err;
+}
+
 static const struct got_error *
 print_diff(void *arg, unsigned char status, unsigned char staged_status,
     const char *path, struct got_object_id *blob_id,
@@ -3723,14 +3769,28 @@ print_diff(void *arg, unsigned char status, unsigned char staged_status,
 		if (dirfd != -1) {
 			fd = openat(dirfd, de_name, O_RDONLY | O_NOFOLLOW);
 			if (fd == -1) {
-				err = got_error_from_errno2("openat", abspath);
-				goto done;
+				if (errno != ELOOP) {
+					err = got_error_from_errno2("openat",
+					    abspath);
+					goto done;
+				}
+				err = get_symlink_target_file(&fd, dirfd,
+				    de_name, abspath);
+				if (err)
+					goto done;
 			}
 		} else {
 			fd = open(abspath, O_RDONLY | O_NOFOLLOW);
 			if (fd == -1) {
-				err = got_error_from_errno2("open", abspath);
-				goto done;
+				if (errno != ELOOP) {
+					err = got_error_from_errno2("open",
+					    abspath);
+					goto done;
+				}
+				err = get_symlink_target_file(&fd, dirfd,
+				    de_name, abspath);
+				if (err)
+					goto done;
 			}
 		}
 		if (fstat(fd, &sb) == -1) {
diff --git a/regress/cmdline/diff.sh b/regress/cmdline/diff.sh
index a3f99c8..b29b40b 100755
--- a/regress/cmdline/diff.sh
+++ b/regress/cmdline/diff.sh
@@ -362,9 +362,102 @@ function test_diff_submodule_of_same_repo {
 	test_done "$testroot" "$ret"
 }
 
+function test_diff_symlinks_in_work_tree {
+	local testroot=`test_init diff_symlinks_in_work_tree`
+
+	(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 -s .got/foo dotgotfoo.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
+
+	(cd $testroot/wt && ln -sf beta alpha.link)
+	(cd $testroot/wt && ln -sf gamma epsilon.link)
+	(cd $testroot/wt && ln -sf ../gamma/delta epsilon/beta.link)
+	echo -n '.got/bar' > $testroot/wt/dotgotfoo.link
+	(cd $testroot/wt && got rm nonexistent.link > /dev/null)
+	(cd $testroot/wt && ln -sf epsilon/zeta zeta.link)
+	(cd $testroot/wt && got add zeta.link > /dev/null)
+	(cd $testroot/wt && got diff > $testroot/stdout)
+
+	echo "diff $commit_id1 $testroot/wt" > $testroot/stdout.expected
+	echo -n 'blob - ' >> $testroot/stdout.expected
+	got tree -r $testroot/repo -c $commit_id1 -i | \
+		grep 'alpha.link@ -> alpha$' | \
+		cut -d' ' -f 1 >> $testroot/stdout.expected
+	echo 'file + alpha.link' >> $testroot/stdout.expected
+	echo '--- alpha.link' >> $testroot/stdout.expected
+	echo '+++ alpha.link' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-alpha' >> $testroot/stdout.expected
+	echo '\ No newline at end of file' >> $testroot/stdout.expected
+	echo '+beta' >> $testroot/stdout.expected
+	echo '\ No newline at end of file' >> $testroot/stdout.expected
+	echo -n 'blob - ' >> $testroot/stdout.expected
+	got tree -r $testroot/repo -c $commit_id1 -i | \
+		grep 'dotgotfoo.link@ -> .got/foo$' | \
+		cut -d' ' -f 1 >> $testroot/stdout.expected
+	echo 'file + dotgotfoo.link' >> $testroot/stdout.expected
+	echo '--- dotgotfoo.link' >> $testroot/stdout.expected
+	echo '+++ dotgotfoo.link' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-.got/foo' >> $testroot/stdout.expected
+	echo '\ No newline at end of file' >> $testroot/stdout.expected
+	echo '+.got/bar' >> $testroot/stdout.expected
+	echo '\ No newline at end of file' >> $testroot/stdout.expected
+	echo -n 'blob - ' >> $testroot/stdout.expected
+	got tree -r $testroot/repo -c $commit_id1 -i epsilon | \
+		grep 'beta.link@ -> ../beta$' | \
+		cut -d' ' -f 1 >> $testroot/stdout.expected
+	echo 'file + epsilon/beta.link' >> $testroot/stdout.expected
+	echo '--- epsilon/beta.link' >> $testroot/stdout.expected
+	echo '+++ epsilon/beta.link' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-../beta' >> $testroot/stdout.expected
+	echo '\ No newline at end of file' >> $testroot/stdout.expected
+	echo '+../gamma/delta' >> $testroot/stdout.expected
+	echo '\ No newline at end of file' >> $testroot/stdout.expected
+	echo -n 'blob - ' >> $testroot/stdout.expected
+	got tree -r $testroot/repo -c $commit_id1 -i | \
+		grep 'nonexistent.link@ -> nonexistent$' | \
+		cut -d' ' -f 1 >> $testroot/stdout.expected
+	echo 'file + /dev/null' >> $testroot/stdout.expected
+	echo '--- nonexistent.link' >> $testroot/stdout.expected
+	echo '+++ nonexistent.link' >> $testroot/stdout.expected
+	echo '@@ -1 +0,0 @@' >> $testroot/stdout.expected
+	echo '-nonexistent' >> $testroot/stdout.expected
+	echo '\ No newline at end of file' >> $testroot/stdout.expected
+	echo 'blob - /dev/null' >> $testroot/stdout.expected
+	echo 'file + zeta.link' >> $testroot/stdout.expected
+	echo '--- zeta.link' >> $testroot/stdout.expected
+	echo '+++ zeta.link' >> $testroot/stdout.expected
+	echo '@@ -0,0 +1 @@' >> $testroot/stdout.expected
+	echo '+epsilon/zeta' >> $testroot/stdout.expected
+	echo '\ No newline at end of file' >> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
 run_test test_diff_basic
 run_test test_diff_shows_conflict
 run_test test_diff_tag
 run_test test_diff_lightweight_tag
 run_test test_diff_ignore_whitespace
 run_test test_diff_submodule_of_same_repo
+run_test test_diff_symlinks_in_work_tree