Commit 00bb5ea05eb54b4ec01ed195104765f7baf80169

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

make 'got add' work on symlinks and let 'got status' display them

diff --git a/lib/worktree.c b/lib/worktree.c
index da56ac7..9014f14 100644
--- a/lib/worktree.c
+++ b/lib/worktree.c
@@ -1333,7 +1333,7 @@ get_file_status(unsigned char *status, struct stat *sb,
 		}
 	}
 
-	if (!S_ISREG(sb->st_mode)) {
+	if (!S_ISREG(sb->st_mode) && !S_ISLNK(sb->st_mode)) {
 		*status = GOT_STATUS_OBSTRUCTED;
 		goto done;
 	}
@@ -2850,10 +2850,6 @@ status_new(void *arg, struct dirent *de, const char *parent_path, int dirfd)
 	if (a->cancel_cb && a->cancel_cb(a->cancel_arg))
 		return got_error(GOT_ERR_CANCELLED);
 
-	/* XXX ignore symlinks for now */
-	if (de->d_type == DT_LNK)
-		return NULL;
-
 	if (parent_path[0]) {
 		if (asprintf(&path, "%s/%s", parent_path, de->d_name) == -1)
 			return got_error_from_errno("asprintf");
@@ -2912,7 +2908,7 @@ void *status_arg, struct got_repository *repo, int report_unchanged)
 		return NULL;
 	}
 
-	if (S_ISREG(sb.st_mode))
+	if (S_ISREG(sb.st_mode) || S_ISLNK(sb.st_mode))
 		return (*status_cb)(status_arg, GOT_STATUS_UNVERSIONED,
 		    GOT_STATUS_NO_CHANGE, path, NULL, NULL, NULL, -1, NULL);
 
@@ -2988,7 +2984,8 @@ worktree_status(struct got_worktree *worktree, const char *path,
 
 	fd = open(ondisk_path, O_RDONLY | O_NOFOLLOW | O_DIRECTORY);
 	if (fd == -1) {
-		if (errno != ENOTDIR && errno != ENOENT && errno != EACCES)
+		if (errno != ENOTDIR && errno != ENOENT && errno != EACCES &&
+		    errno != ELOOP)
 			err = got_error_from_errno2("open", ondisk_path);
 		else
 			err = report_single_file_status(path, ondisk_path,
@@ -3058,21 +3055,58 @@ got_worktree_resolve_path(char **wt_path, struct got_worktree *worktree,
     const char *arg)
 {
 	const struct got_error *err = NULL;
-	char *resolved, *cwd = NULL, *path = NULL;
+	char *resolved = NULL, *cwd = NULL, *path = NULL;
 	size_t len;
+	struct stat sb;
 
 	*wt_path = NULL;
 
-	resolved = realpath(arg, NULL);
-	if (resolved == NULL) {
-		if (errno != ENOENT)
-			return got_error_from_errno2("realpath", arg);
-		cwd = getcwd(NULL, 0);
-		if (cwd == NULL)
-			return got_error_from_errno("getcwd");
-		if (asprintf(&resolved, "%s/%s", cwd, arg) == -1) {
-			err = got_error_from_errno("asprintf");
+	cwd = getcwd(NULL, 0);
+	if (cwd == NULL)
+		return got_error_from_errno("getcwd");
+
+	if (lstat(arg, &sb) == -1) {
+		if (errno != ENOENT) {
+			err = got_error_from_errno2("lstat", arg);
+			goto done;
+		}
+	}
+	if (S_ISLNK(sb.st_mode)) {
+		/*
+		 * We cannot use realpath(3) with symlinks since we want to
+		 * operate on the symlink itself.
+		 * But we can make the path absolute, assuming it is relative
+		 * to the current working directory, and then canonicalize it.
+		 */
+		char *abspath = NULL;
+		char canonpath[PATH_MAX];
+		if (!got_path_is_absolute(arg)) {
+			if (asprintf(&abspath, "%s/%s", cwd, arg) == -1) {
+				err = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+		}
+		err = got_canonpath(abspath ? abspath : arg, canonpath,
+		    sizeof(canonpath));
+		if (err)
 			goto done;
+		resolved = strdup(canonpath);
+		if (resolved == NULL) {
+			err = got_error_from_errno("strdup");
+			goto done;
+		}
+	} else {
+		resolved = realpath(arg, NULL);
+		if (resolved == NULL) {
+			if (errno != ENOENT) {
+				err = got_error_from_errno2("realpath", arg);
+				goto done;
+			}
+			if (asprintf(&resolved, "%s/%s", cwd, arg) == -1) {
+				err = got_error_from_errno("asprintf");
+				goto done;
+			}
 		}
 	}
 
diff --git a/regress/cmdline/add.sh b/regress/cmdline/add.sh
index 030ece4..789676b 100755
--- a/regress/cmdline/add.sh
+++ b/regress/cmdline/add.sh
@@ -296,6 +296,71 @@ function test_add_clashes_with_submodule {
 	test_done "$testroot" "$ret"
 }
 
+function test_add_symlink {
+	local testroot=`test_init add_symlink`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && ln -s alpha alpha.link)
+	(cd $testroot/wt && ln -s epsilon epsilon.link)
+	(cd $testroot/wt && ln -s /etc/passwd passwd.link)
+	(cd $testroot/wt && ln -s ../beta epsilon/beta.link)
+	(cd $testroot/wt && ln -s nonexistent nonexistent.link)
+
+	echo "A  alpha.link" > $testroot/stdout.expected
+	(cd $testroot/wt && got add alpha.link > $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "A  epsilon.link" > $testroot/stdout.expected
+	(cd $testroot/wt && got add epsilon.link > $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
+
+	echo "A  passwd.link" > $testroot/stdout.expected
+	(cd $testroot/wt && got add passwd.link > $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
+
+	echo "A  epsilon/beta.link" > $testroot/stdout.expected
+	(cd $testroot/wt && got add epsilon/beta.link > $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
+
+	echo "A  nonexistent.link" > $testroot/stdout.expected
+	(cd $testroot/wt && got add nonexistent.link > $testroot/stdout)
+	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_add_basic
 run_test test_double_add
 run_test test_add_multiple
@@ -303,3 +368,4 @@ run_test test_add_file_in_new_subdir
 run_test test_add_deleted
 run_test test_add_directory
 run_test test_add_clashes_with_submodule
+run_test test_add_symlink
diff --git a/regress/cmdline/status.sh b/regress/cmdline/status.sh
index bf27874..580563c 100755
--- a/regress/cmdline/status.sh
+++ b/regress/cmdline/status.sh
@@ -258,9 +258,8 @@ function test_status_unversioned_subdirs {
 	test_done "$testroot" "$ret"
 }
 
-# 'got status' ignores symlinks at present; this might change eventually
-function test_status_ignores_symlink {
-	local testroot=`test_init status_ignores_symlink 1`
+function test_status_symlink {
+	local testroot=`test_init status_symlink`
 
 	mkdir $testroot/repo/ramdisk/
 	touch $testroot/repo/ramdisk/Makefile
@@ -276,7 +275,32 @@ function test_status_ignores_symlink {
 
 	ln -s /usr/obj/distrib/i386/ramdisk $testroot/wt/ramdisk/obj
 
-	echo -n > $testroot/stdout.expected
+	echo "?  ramdisk/obj" > $testroot/stdout.expected
+
+	(cd $testroot/wt && got status > $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
+
+	(cd $testroot/wt && ln -s alpha alpha.link)
+	(cd $testroot/wt && ln -s epsilon epsilon.link)
+	(cd $testroot/wt && ln -s /etc/passwd passwd.link)
+	(cd $testroot/wt && ln -s ../beta epsilon/beta.link)
+	(cd $testroot/wt && ln -s nonexistent nonexistent.link)
+	(cd $testroot/wt && got add alpha.link epsilon.link \
+		passwd.link epsilon/beta.link nonexistent.link > /dev/null)
+
+	echo 'A  alpha.link' > $testroot/stdout.expected
+	echo 'A  epsilon/beta.link' >> $testroot/stdout.expected
+	echo 'A  epsilon.link' >> $testroot/stdout.expected
+	echo 'A  nonexistent.link' >> $testroot/stdout.expected
+	echo 'A  passwd.link' >> $testroot/stdout.expected
+	echo "?  ramdisk/obj" >> $testroot/stdout.expected
 
 	(cd $testroot/wt && got status > $testroot/stdout)
 
@@ -612,7 +636,7 @@ run_test test_status_subdir_no_mods2
 run_test test_status_obstructed
 run_test test_status_shows_local_mods_after_update
 run_test test_status_unversioned_subdirs
-run_test test_status_ignores_symlink
+run_test test_status_symlink
 run_test test_status_shows_no_mods_after_complete_merge
 run_test test_status_shows_conflict
 run_test test_status_empty_dir