Commit 3d9a4ec407702ad2b932c522001f1b88a36571de

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

add symlink support to 'got commit'

diff --git a/lib/object_create.c b/lib/object_create.c
index fa3eec2..cf8a728 100644
--- a/lib/object_create.c
+++ b/lib/object_create.c
@@ -128,10 +128,15 @@ got_object_blob_create(struct got_object_id **id, const char *ondisk_path,
 	SHA1Init(&sha1_ctx);
 
 	fd = open(ondisk_path, O_RDONLY | O_NOFOLLOW);
-	if (fd == -1)
-		return got_error_from_errno2("open", ondisk_path);
+	if (fd == -1) {
+		if (errno != ELOOP)
+			return got_error_from_errno2("open", ondisk_path);
 
-	if (fstat(fd, &sb) == -1) {
+		if (lstat(ondisk_path, &sb) == -1) {
+			err = got_error_from_errno2("lstat", ondisk_path);
+			goto done;
+		}
+	} else if (fstat(fd, &sb) == -1) {
 		err = got_error_from_errno2("fstat", ondisk_path);
 		goto done;
 	}
@@ -156,13 +161,21 @@ got_object_blob_create(struct got_object_id **id, const char *ondisk_path,
 		goto done;
 	}
 	for (;;) {
-		char buf[8192];
+		char buf[PATH_MAX];
 		ssize_t inlen;
 
-		inlen = read(fd, buf, sizeof(buf));
-		if (inlen == -1) {
-			err = got_error_from_errno("read");
-			goto done;
+		if (S_ISLNK(sb.st_mode)) {
+			inlen = readlink(ondisk_path, buf, sizeof(buf));
+			if (inlen == -1) {
+				err = got_error_from_errno("readlink");
+				goto done;
+			}
+		} else {
+			inlen = read(fd, buf, sizeof(buf));
+			if (inlen == -1) {
+				err = got_error_from_errno("read");
+				goto done;
+			}
 		}
 		if (inlen == 0)
 			break; /* EOF */
@@ -172,6 +185,8 @@ got_object_blob_create(struct got_object_id **id, const char *ondisk_path,
 			err = got_ferror(blobfile, GOT_ERR_IO);
 			goto done;
 		}
+		if (S_ISLNK(sb.st_mode))
+			break;
 	}
 
 	*id = malloc(sizeof(**id));
@@ -218,6 +233,8 @@ te_mode2str(char *buf, size_t len, struct got_tree_entry *te)
 			mode |= S_IXUSR | S_IXGRP | S_IXOTH;
 	} else if (got_object_tree_entry_is_submodule(te))
 		mode = S_IFDIR | S_IFLNK;
+	else if (S_ISLNK(te->mode))
+		mode = S_IFLNK; /* Git leaves all the other bits unset. */
 	else if (S_ISDIR(te->mode))
 		mode = S_IFDIR; /* Git leaves all the other bits unset. */
 	else
diff --git a/lib/worktree.c b/lib/worktree.c
index 18b7915..4f2a832 100644
--- a/lib/worktree.c
+++ b/lib/worktree.c
@@ -4343,6 +4343,9 @@ match_ct_parent_path(int *match, struct got_commitable *ct, const char *path)
 static mode_t
 get_ct_file_mode(struct got_commitable *ct)
 {
+	if (S_ISLNK(ct->mode))
+		return S_IFLNK;
+
 	return S_IFREG | (ct->mode & ((S_IRWXU | S_IRWXG | S_IRWXO)));
 }
 
diff --git a/regress/cmdline/commit.sh b/regress/cmdline/commit.sh
index 190d9e7..9bf3085 100755
--- a/regress/cmdline/commit.sh
+++ b/regress/cmdline/commit.sh
@@ -902,6 +902,119 @@ function test_commit_with_unrelated_submodule {
 	test_done "$testroot" "$ret"
 }
 
+function test_commit_symlink {
+	local testroot=`test_init commit_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)
+	(cd $testroot/wt && got add alpha.link epsilon.link passwd.link \
+		epsilon/beta.link nonexistent.link > /dev/null)
+
+	(cd $testroot/wt && got commit -m 'test commit_symlink' > $testroot/stdout)
+
+	local head_rev=`git_show_head $testroot/repo`
+	echo "A  alpha.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 "A  epsilon/beta.link" >> $testroot/stdout.expected
+	echo "Created commit $head_rev" >> $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
+
+	got checkout $testroot/repo $testroot/wt2 > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	if ! [ -h $testroot/wt2/alpha.link ]; then
+		echo "alpha.link is not a symlink"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	readlink $testroot/wt2/alpha.link > $testroot/stdout
+	echo "alpha" > $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/wt2/epsilon.link ]; then
+		echo "epsilon.link is not a symlink"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	readlink $testroot/wt2/epsilon.link > $testroot/stdout
+	echo "epsilon" > $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/wt2/passwd.link ]; then
+		echo -n "passwd.link symlink points outside of work tree: " >&2
+		readlink $testroot/wt2/passwd.link >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo -n "/etc/passwd" > $testroot/content.expected
+	cp $testroot/wt2/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/wt2/epsilon/beta.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
+
+	readlink $testroot/wt2/nonexistent.link > $testroot/stdout
+	echo "nonexistent" > $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_commit_basic
 run_test test_commit_new_subdir
 run_test test_commit_subdir
@@ -922,3 +1035,4 @@ run_test test_commit_gitconfig_author
 run_test test_commit_xbit_change
 run_test test_commit_normalizes_filemodes
 run_test test_commit_with_unrelated_submodule
+run_test test_commit_symlink