Commit 853ba203208fcb4e7c3765f53a4d17a3db77f160

Edward Thomson 2021-09-27T08:39:39

Merge pull request #6073 from libgit2/ethomson/attr_lookups

diff --git a/include/git2/repository.h b/include/git2/repository.h
index e699016..8d1cffc 100644
--- a/include/git2/repository.h
+++ b/include/git2/repository.h
@@ -762,13 +762,15 @@ GIT_EXTERN(int) git_repository_mergehead_foreach(
  *
  * @param out Output value of calculated SHA
  * @param repo Repository pointer
- * @param path Path to file on disk whose contents should be hashed. If the
- *             repository is not NULL, this can be a relative path.
+ * @param path Path to file on disk whose contents should be hashed.  This
+ *             may be an absolute path or a relative path, in which case it
+ *             will be treated as a path within the working directory.
  * @param type The object type to hash as (e.g. GIT_OBJECT_BLOB)
  * @param as_path The path to use to look up filtering rules. If this is
- *             NULL, then the `path` parameter will be used instead. If
- *             this is passed as the empty string, then no filters will be
- *             applied when calculating the hash.
+ *             an empty string then no filters will be applied when
+ *             calculating the hash. If this is `NULL` and the `path`
+ *             parameter is a file within the repository's working
+ *             directory, then the `path` will be used.
  * @return 0 on success, or an error code
  */
 GIT_EXTERN(int) git_repository_hashfile(
diff --git a/src/attr.c b/src/attr.c
index 14eab5b..cd704a4 100644
--- a/src/attr.c
+++ b/src/attr.c
@@ -629,6 +629,8 @@ static int collect_attr_files(
 	const char *workdir = git_repository_workdir(repo);
 	attr_walk_up_info info = { NULL };
 
+	GIT_ASSERT(!git_path_is_absolute(path));
+
 	if ((error = attr_setup(repo, attr_session, opts)) < 0)
 		return error;
 
diff --git a/src/blob.c b/src/blob.c
index 06a4a00..09b5b5d 100644
--- a/src/blob.c
+++ b/src/blob.c
@@ -277,21 +277,20 @@ int git_blob_create_from_disk(
 {
 	int error;
 	git_buf full_path = GIT_BUF_INIT;
-	const char *workdir, *hintpath;
+	const char *workdir, *hintpath = NULL;
 
 	if ((error = git_path_prettify(&full_path, path, NULL)) < 0) {
 		git_buf_dispose(&full_path);
 		return error;
 	}
 
-	hintpath = git_buf_cstr(&full_path);
 	workdir  = git_repository_workdir(repo);
 
-	if (workdir && !git__prefixcmp(hintpath, workdir))
-		hintpath += strlen(workdir);
+	if (workdir && !git__prefixcmp(full_path.ptr, workdir))
+		hintpath = full_path.ptr + strlen(workdir);
 
 	error = git_blob__create_from_paths(
-		id, NULL, repo, git_buf_cstr(&full_path), hintpath, 0, true);
+		id, NULL, repo, git_buf_cstr(&full_path), hintpath, 0, !!hintpath);
 
 	git_buf_dispose(&full_path);
 	return error;
diff --git a/src/checkout.c b/src/checkout.c
index 7131591..3a17106 100644
--- a/src/checkout.c
+++ b/src/checkout.c
@@ -1520,8 +1520,7 @@ static int blob_content_to_file(
 	int fd;
 	int error = 0;
 
-	if (hint_path == NULL)
-		hint_path = path;
+	GIT_ASSERT(hint_path != NULL);
 
 	if ((error = mkpath2file(data, path, data->opts.dir_mode)) < 0)
 		return error;
@@ -1789,7 +1788,7 @@ static int checkout_blob(
 	}
 
 	error = checkout_write_content(
-		data, &file->id, fullpath->ptr, NULL, file->mode, &st);
+		data, &file->id, fullpath->ptr, file->path, file->mode, &st);
 
 	/* update the index unless prevented */
 	if (!error && (data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0)
@@ -1975,7 +1974,7 @@ static int checkout_write_entry(
 	checkout_conflictdata *conflict,
 	const git_index_entry *side)
 {
-	const char *hint_path = NULL, *suffix;
+	const char *hint_path, *suffix;
 	git_buf *fullpath;
 	struct stat st;
 	int error;
@@ -1998,10 +1997,10 @@ static int checkout_write_entry(
 
 		if (checkout_path_suffixed(fullpath, suffix) < 0)
 			return -1;
-
-		hint_path = side->path;
 	}
 
+	hint_path = side->path;
+
 	if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0 &&
 		(error = checkout_safe_for_update_only(data, fullpath->ptr, side->mode)) <= 0)
 		return error;
@@ -2118,7 +2117,7 @@ static int checkout_write_merge(
 		filter_session.temp_buf = &data->tmp;
 
 		if ((error = git_filter_list__load(
-				&fl, data->repo, NULL, git_buf_cstr(&path_workdir),
+				&fl, data->repo, NULL, result.path,
 				GIT_FILTER_TO_WORKTREE, &filter_session)) < 0 ||
 			(error = git_filter_list__convert_buf(&out_data, fl, &in_data)) < 0)
 			goto done;
diff --git a/src/repository.c b/src/repository.c
index 8f0f477..9b3e9c9 100644
--- a/src/repository.c
+++ b/src/repository.c
@@ -2840,34 +2840,36 @@ int git_repository_hashfile(
 	git_file fd = -1;
 	uint64_t len;
 	git_buf full_path = GIT_BUF_INIT;
+	const char *workdir = git_repository_workdir(repo);
 
 	 /* as_path can be NULL */
 	GIT_ASSERT_ARG(out);
 	GIT_ASSERT_ARG(path);
 	GIT_ASSERT_ARG(repo);
 
-	/* At some point, it would be nice if repo could be NULL to just
-	 * apply filter rules defined in system and global files, but for
-	 * now that is not possible because git_filters_load() needs it.
-	 */
-
-	if ((error = git_path_join_unrooted(
-		&full_path, path, git_repository_workdir(repo), NULL)) < 0 ||
+	if ((error = git_path_join_unrooted(&full_path, path, workdir, NULL)) < 0 ||
 	    (error = git_path_validate_workdir_buf(repo, &full_path)) < 0)
 		return error;
 
-	if (!as_path)
-		as_path = path;
+	/*
+	 * NULL as_path means that we should derive it from the
+	 * given path.
+	 */
+	if (!as_path) {
+		if (workdir && !git__prefixcmp(full_path.ptr, workdir))
+			as_path = full_path.ptr + strlen(workdir);
+		else
+			as_path = "";
+	}
 
 	/* passing empty string for "as_path" indicated --no-filters */
 	if (strlen(as_path) > 0) {
 		error = git_filter_list_load(
 			&fl, repo, NULL, as_path,
 			GIT_FILTER_TO_ODB, GIT_FILTER_DEFAULT);
+
 		if (error < 0)
 			return error;
-	} else {
-		error = 0;
 	}
 
 	/* at this point, error is a count of the number of loaded filters */
diff --git a/src/win32/posix_w32.c b/src/win32/posix_w32.c
index 66d12cf..8af07e6 100644
--- a/src/win32/posix_w32.c
+++ b/src/win32/posix_w32.c
@@ -648,6 +648,8 @@ int p_getcwd(char *buffer_out, size_t size)
 	if (!cwd)
 		return -1;
 
+	git_win32_path_remove_namespace(cwd, wcslen(cwd));
+
 	/* Convert the working directory back to UTF-8 */
 	if (git__utf16_to_8(buffer_out, size, cwd) < 0) {
 		DWORD code = GetLastError();
@@ -660,6 +662,7 @@ int p_getcwd(char *buffer_out, size_t size)
 		return -1;
 	}
 
+	git_path_mkposix(buffer_out);
 	return 0;
 }
 
diff --git a/tests/repo/hashfile.c b/tests/repo/hashfile.c
index 0fb4e67..bffb51b 100644
--- a/tests/repo/hashfile.c
+++ b/tests/repo/hashfile.c
@@ -10,6 +10,7 @@ void test_repo_hashfile__initialize(void)
 
 void test_repo_hashfile__cleanup(void)
 {
+	cl_fixture_cleanup("absolute");
 	cl_git_sandbox_cleanup();
 	_repo = NULL;
 }
@@ -38,10 +39,18 @@ void test_repo_hashfile__simple(void)
 	git_buf_dispose(&full);
 }
 
-void test_repo_hashfile__filtered(void)
+void test_repo_hashfile__filtered_in_workdir(void)
 {
+	git_buf root = GIT_BUF_INIT, txt = GIT_BUF_INIT, bin = GIT_BUF_INIT;
+	char cwd[GIT_PATH_MAX];
 	git_oid a, b;
 
+	cl_must_pass(p_getcwd(cwd, GIT_PATH_MAX));
+	cl_must_pass(p_mkdir("absolute", 0777));
+	cl_git_pass(git_buf_joinpath(&root, cwd, "status"));
+	cl_git_pass(git_buf_joinpath(&txt, root.ptr, "testfile.txt"));
+	cl_git_pass(git_buf_joinpath(&bin, root.ptr, "testfile.bin"));
+
 	cl_repo_set_bool(_repo, "core.autocrlf", true);
 
 	cl_git_append2file("status/.gitattributes", "*.txt text\n*.bin binary\n\n");
@@ -55,21 +64,41 @@ void test_repo_hashfile__filtered(void)
 	cl_git_pass(git_repository_hashfile(&b, _repo, "testfile.txt", GIT_OBJECT_BLOB, NULL));
 	cl_assert(git_oid_cmp(&a, &b));
 
+	/* not equal hashes because of filtering when specified by absolute path */
+	cl_git_pass(git_odb_hashfile(&a, "status/testfile.txt", GIT_OBJECT_BLOB));
+	cl_git_pass(git_repository_hashfile(&b, _repo, txt.ptr, GIT_OBJECT_BLOB, NULL));
+	cl_assert(git_oid_cmp(&a, &b));
+
 	/* equal hashes because filter is binary */
 	cl_git_pass(git_odb_hashfile(&a, "status/testfile.bin", GIT_OBJECT_BLOB));
 	cl_git_pass(git_repository_hashfile(&b, _repo, "testfile.bin", GIT_OBJECT_BLOB, NULL));
 	cl_assert_equal_oid(&a, &b);
 
+	/* equal hashes because filter is binary when specified by absolute path */
+	cl_git_pass(git_odb_hashfile(&a, "status/testfile.bin", GIT_OBJECT_BLOB));
+	cl_git_pass(git_repository_hashfile(&b, _repo, bin.ptr, GIT_OBJECT_BLOB, NULL));
+	cl_assert_equal_oid(&a, &b);
+
 	/* equal hashes when 'as_file' points to binary filtering */
 	cl_git_pass(git_odb_hashfile(&a, "status/testfile.txt", GIT_OBJECT_BLOB));
 	cl_git_pass(git_repository_hashfile(&b, _repo, "testfile.txt", GIT_OBJECT_BLOB, "foo.bin"));
 	cl_assert_equal_oid(&a, &b);
 
+	/* equal hashes when 'as_file' points to binary filtering (absolute path) */
+	cl_git_pass(git_odb_hashfile(&a, "status/testfile.txt", GIT_OBJECT_BLOB));
+	cl_git_pass(git_repository_hashfile(&b, _repo, txt.ptr, GIT_OBJECT_BLOB, "foo.bin"));
+	cl_assert_equal_oid(&a, &b);
+
 	/* not equal hashes when 'as_file' points to text filtering */
 	cl_git_pass(git_odb_hashfile(&a, "status/testfile.bin", GIT_OBJECT_BLOB));
 	cl_git_pass(git_repository_hashfile(&b, _repo, "testfile.bin", GIT_OBJECT_BLOB, "foo.txt"));
 	cl_assert(git_oid_cmp(&a, &b));
 
+	/* not equal hashes when 'as_file' points to text filtering */
+	cl_git_pass(git_odb_hashfile(&a, "status/testfile.bin", GIT_OBJECT_BLOB));
+	cl_git_pass(git_repository_hashfile(&b, _repo, bin.ptr, GIT_OBJECT_BLOB, "foo.txt"));
+	cl_assert(git_oid_cmp(&a, &b));
+
 	/* equal hashes when 'as_file' is empty and turns off filtering */
 	cl_git_pass(git_odb_hashfile(&a, "status/testfile.txt", GIT_OBJECT_BLOB));
 	cl_git_pass(git_repository_hashfile(&b, _repo, "testfile.txt", GIT_OBJECT_BLOB, ""));
@@ -79,7 +108,65 @@ void test_repo_hashfile__filtered(void)
 	cl_git_pass(git_repository_hashfile(&b, _repo, "testfile.bin", GIT_OBJECT_BLOB, ""));
 	cl_assert_equal_oid(&a, &b);
 
+	cl_git_pass(git_odb_hashfile(&a, "status/testfile.txt", GIT_OBJECT_BLOB));
+	cl_git_pass(git_repository_hashfile(&b, _repo, txt.ptr, GIT_OBJECT_BLOB, ""));
+	cl_assert_equal_oid(&a, &b);
+
+	cl_git_pass(git_odb_hashfile(&a, "status/testfile.bin", GIT_OBJECT_BLOB));
+	cl_git_pass(git_repository_hashfile(&b, _repo, bin.ptr, GIT_OBJECT_BLOB, ""));
+	cl_assert_equal_oid(&a, &b);
+
 	/* some hash type failures */
 	cl_git_fail(git_odb_hashfile(&a, "status/testfile.txt", 0));
 	cl_git_fail(git_repository_hashfile(&b, _repo, "testfile.txt", GIT_OBJECT_ANY, NULL));
+
+	git_buf_dispose(&txt);
+	git_buf_dispose(&bin);
+	git_buf_dispose(&root);
+}
+
+void test_repo_hashfile__filtered_outside_workdir(void)
+{
+	git_buf root = GIT_BUF_INIT, txt = GIT_BUF_INIT, bin = GIT_BUF_INIT;
+	char cwd[GIT_PATH_MAX];
+	git_oid a, b;
+
+	cl_must_pass(p_getcwd(cwd, GIT_PATH_MAX));
+	cl_must_pass(p_mkdir("absolute", 0777));
+	cl_git_pass(git_buf_joinpath(&root, cwd, "absolute"));
+	cl_git_pass(git_buf_joinpath(&txt, root.ptr, "testfile.txt"));
+	cl_git_pass(git_buf_joinpath(&bin, root.ptr, "testfile.bin"));
+
+	cl_repo_set_bool(_repo, "core.autocrlf", true);
+	cl_git_append2file("status/.gitattributes", "*.txt text\n*.bin binary\n\n");
+
+	/* create some sample content with CRLF in it */
+	cl_git_mkfile("absolute/testfile.txt", "content\r\n");
+	cl_git_mkfile("absolute/testfile.bin", "other\r\nstuff\r\n");
+
+	/* not equal hashes because of filtering */
+	cl_git_pass(git_odb_hashfile(&a, "absolute/testfile.txt", GIT_OBJECT_BLOB));
+	cl_git_pass(git_repository_hashfile(&b, _repo, txt.ptr, GIT_OBJECT_BLOB, "testfile.txt"));
+	cl_assert(git_oid_cmp(&a, &b));
+
+	/* equal hashes because filter is binary */
+	cl_git_pass(git_odb_hashfile(&a, "absolute/testfile.bin", GIT_OBJECT_BLOB));
+	cl_git_pass(git_repository_hashfile(&b, _repo, bin.ptr, GIT_OBJECT_BLOB, "testfile.bin"));
+	cl_assert_equal_oid(&a, &b);
+
+	/*
+	 * equal hashes because no filtering occurs for absolute paths outside the working
+	 * directory unless as_path is specified
+	 */
+	cl_git_pass(git_odb_hashfile(&a, "absolute/testfile.txt", GIT_OBJECT_BLOB));
+	cl_git_pass(git_repository_hashfile(&b, _repo, txt.ptr, GIT_OBJECT_BLOB, NULL));
+	cl_assert_equal_oid(&a, &b);
+
+	cl_git_pass(git_odb_hashfile(&a, "absolute/testfile.bin", GIT_OBJECT_BLOB));
+	cl_git_pass(git_repository_hashfile(&b, _repo, bin.ptr, GIT_OBJECT_BLOB, NULL));
+	cl_assert_equal_oid(&a, &b);
+
+	git_buf_dispose(&txt);
+	git_buf_dispose(&bin);
+	git_buf_dispose(&root);
 }