filebuf: follow symlinks when creating a lock file We create a lockfile to update files under GIT_DIR. Sometimes these files are actually located elsewhere and a symlink takes their place. In that case we should lock and update the file at its final location rather than overwrite the symlink.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
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));
+}