Commit e1d27bcaafaadf4ef5eeae19c96835c6663c4289

Edward Thomson 2015-09-06T10:51:29

Merge pull request #3413 from libgit2/cmn/follow-symlink filebuf: follow symlinks when creating a lock file

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f28cbf..1f3c3ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@ v0.23 + 1
   example `filter=*`.  Consumers should examine the attributes parameter
   of the `check` function for details.
 
+* Symlinks are now followed when locking a file, which can be
+  necessary when multiple worktrees share a base repository.
+
 ### API additions
 
 * `git_config_lock()` has been added, which allow for
diff --git a/src/filebuf.c b/src/filebuf.c
index 838f4b4..2bbc210 100644
--- a/src/filebuf.c
+++ b/src/filebuf.c
@@ -191,6 +191,81 @@ static int write_deflate(git_filebuf *file, void *source, size_t len)
 	return 0;
 }
 
+#define MAX_SYMLINK_DEPTH 5
+
+static int resolve_symlink(git_buf *out, const char *path)
+{
+	int i, error, root;
+	ssize_t ret;
+	struct stat st;
+	git_buf curpath = GIT_BUF_INIT, target = GIT_BUF_INIT;
+
+	if ((error = git_buf_grow(&target, GIT_PATH_MAX + 1)) < 0 ||
+	    (error = git_buf_puts(&curpath, path)) < 0)
+		return error;
+
+	for (i = 0; i < MAX_SYMLINK_DEPTH; i++) {
+		error = p_lstat(curpath.ptr, &st);
+		if (error < 0 && errno == ENOENT) {
+			error = git_buf_puts(out, curpath.ptr);
+			goto cleanup;
+		}
+
+		if (error < 0) {
+			giterr_set(GITERR_OS, "failed to stat '%s'", curpath.ptr);
+			error = -1;
+			goto cleanup;
+		}
+
+		if (!S_ISLNK(st.st_mode)) {
+			error = git_buf_puts(out, curpath.ptr);
+			goto cleanup;
+		}
+
+		ret = p_readlink(curpath.ptr, target.ptr, GIT_PATH_MAX);
+		if (ret < 0) {
+			giterr_set(GITERR_OS, "failed to read symlink '%s'", curpath.ptr);
+			error = -1;
+			goto cleanup;
+		}
+
+		if (ret == GIT_PATH_MAX) {
+			giterr_set(GITERR_INVALID, "symlink target too long");
+			error = -1;
+			goto cleanup;
+		}
+
+		/* readlink(2) won't NUL-terminate for us */
+		target.ptr[ret] = '\0';
+		target.size = ret;
+
+		root = git_path_root(target.ptr);
+		if (root >= 0) {
+			if ((error = git_buf_puts(&curpath, target.ptr)) < 0)
+				goto cleanup;
+		} else {
+			git_buf dir = GIT_BUF_INIT;
+
+			if ((error = git_path_dirname_r(&dir, curpath.ptr)) < 0)
+				goto cleanup;
+
+			git_buf_swap(&curpath, &dir);
+			git_buf_free(&dir);
+
+			if ((error = git_path_apply_relative(&curpath, target.ptr)) < 0)
+				goto cleanup;
+		}
+	}
+
+	giterr_set(GITERR_INVALID, "maximum symlink depth reached");
+	error = -1;
+
+cleanup:
+	git_buf_free(&curpath);
+	git_buf_free(&target);
+	return error;
+}
+
 int git_filebuf_open(git_filebuf *file, const char *path, int flags, mode_t mode)
 {
 	int compression, error = -1;
@@ -265,11 +340,14 @@ int git_filebuf_open(git_filebuf *file, const char *path, int flags, mode_t mode
 		file->path_lock = git_buf_detach(&tmp_path);
 		GITERR_CHECK_ALLOC(file->path_lock);
 	} else {
-		path_len = strlen(path);
+		git_buf resolved_path = GIT_BUF_INIT;
+
+		if ((error = resolve_symlink(&resolved_path, path)) < 0)
+			goto cleanup;
 
 		/* Save the original path of the file */
-		file->path_original = git__strdup(path);
-		GITERR_CHECK_ALLOC(file->path_original);
+		path_len = resolved_path.size;
+		file->path_original = git_buf_detach(&resolved_path);
 
 		/* create the locking path by appending ".lock" to the original */
 		GITERR_CHECK_ALLOC_ADD(&alloc_len, path_len, GIT_FILELOCK_EXTLENGTH);
diff --git a/tests/core/filebuf.c b/tests/core/filebuf.c
index 3f7dc85..39d98ff 100644
--- a/tests/core/filebuf.c
+++ b/tests/core/filebuf.c
@@ -151,3 +151,56 @@ void test_core_filebuf__rename_error(void)
 
 	cl_assert_equal_i(false, git_path_exists(test_lock));
 }
+
+void test_core_filebuf__symlink_follow(void)
+{
+	git_filebuf file = GIT_FILEBUF_INIT;
+	const char *dir = "linkdir", *source = "linkdir/link";
+
+#ifdef GIT_WIN32
+	cl_skip();
+#endif
+
+	cl_git_pass(p_mkdir(dir, 0777));
+	cl_git_pass(p_symlink("target", source));
+
+	cl_git_pass(git_filebuf_open(&file, source, 0, 0666));
+	cl_git_pass(git_filebuf_printf(&file, "%s\n", "libgit2 rocks"));
+
+	cl_assert_equal_i(true, git_path_exists("linkdir/target.lock"));
+
+	cl_git_pass(git_filebuf_commit(&file));
+	cl_assert_equal_i(true, git_path_exists("linkdir/target"));
+
+	git_filebuf_cleanup(&file);
+
+	/* The second time around, the target file does exist */
+	cl_git_pass(git_filebuf_open(&file, source, 0, 0666));
+	cl_git_pass(git_filebuf_printf(&file, "%s\n", "libgit2 rocks"));
+
+	cl_assert_equal_i(true, git_path_exists("linkdir/target.lock"));
+
+	cl_git_pass(git_filebuf_commit(&file));
+	cl_assert_equal_i(true, git_path_exists("linkdir/target"));
+
+	git_filebuf_cleanup(&file);
+	cl_git_pass(git_futils_rmdir_r(dir, NULL, GIT_RMDIR_REMOVE_FILES));
+}
+
+void test_core_filebuf__symlink_depth(void)
+{
+	git_filebuf file = GIT_FILEBUF_INIT;
+	const char *dir = "linkdir", *source = "linkdir/link";
+
+#ifdef GIT_WIN32
+	cl_skip();
+#endif
+
+	cl_git_pass(p_mkdir(dir, 0777));
+	/* Endless loop */
+	cl_git_pass(p_symlink("link", source));
+
+	cl_git_fail(git_filebuf_open(&file, source, 0, 0666));
+
+	cl_git_pass(git_futils_rmdir_r(dir, NULL, GIT_RMDIR_REMOVE_FILES));
+}