Commit 0bd547a8bee02bf984ea5c7acdc8172044fcb3a4

Edward Thomson 2021-07-22T15:29:46

attr: introduce GIT_ATTR_CHECK_INCLUDE_COMMIT Introduce `GIT_ATTR_CHECK_INCLUDE_COMMIT`, which like 4fd5748 allows attribute information to be read from files in the repository. 4fd5748 always reads the information from HEAD, while `GIT_ATTR_CHECK_INCLUDE_COMMIT` allows users to provide the commit to read the attributes from.

diff --git a/include/git2/attr.h b/include/git2/attr.h
index 306893c..62c2ed6 100644
--- a/include/git2/attr.h
+++ b/include/git2/attr.h
@@ -130,9 +130,13 @@ GIT_EXTERN(git_attr_value_t) git_attr_value(const char *attr);
  *
  * Passing the `GIT_ATTR_CHECK_INCLUDE_HEAD` flag will use attributes
  * from a `.gitattributes` file in the repository at the HEAD revision.
+ *
+ * Passing the `GIT_ATTR_CHECK_INCLUDE_COMMIT` flag will use attributes
+ * from a `.gitattributes` file in a specific commit.
  */
 #define GIT_ATTR_CHECK_NO_SYSTEM        (1 << 2)
 #define GIT_ATTR_CHECK_INCLUDE_HEAD     (1 << 3)
+#define GIT_ATTR_CHECK_INCLUDE_COMMIT   (1 << 4)
 
 /**
 * An options structure for querying attributes.
@@ -142,6 +146,12 @@ typedef struct {
 
 	/** A combination of GIT_ATTR_CHECK flags */
 	unsigned int flags;
+
+	/**
+	 * The commit to load attributes from, when
+	 * `GIT_ATTR_CHECK_INCLUDE_COMMIT` is specified.
+	 */
+	git_oid *commit_id;
 } git_attr_options;
 
 #define GIT_ATTR_OPTIONS_VERSION 1
diff --git a/src/attr.c b/src/attr.c
index a2d78e6..03b720c 100644
--- a/src/attr.c
+++ b/src/attr.c
@@ -381,8 +381,9 @@ static int attr_setup(
 	git_attr_options *opts)
 {
 	git_buf system = GIT_BUF_INIT, info = GIT_BUF_INIT;
-	git_attr_file_source index_source = { GIT_ATTR_FILE_SOURCE_INDEX, NULL, GIT_ATTR_FILE };
-	git_attr_file_source head_source = { GIT_ATTR_FILE_SOURCE_COMMIT, NULL, GIT_ATTR_FILE };
+	git_attr_file_source index_source = { GIT_ATTR_FILE_SOURCE_INDEX, NULL, GIT_ATTR_FILE, NULL };
+	git_attr_file_source head_source = { GIT_ATTR_FILE_SOURCE_COMMIT, NULL, GIT_ATTR_FILE, NULL };
+	git_attr_file_source commit_source = { GIT_ATTR_FILE_SOURCE_COMMIT, NULL, GIT_ATTR_FILE, NULL };
 	git_index *idx = NULL;
 	const char *workdir;
 	int error = 0;
@@ -430,6 +431,13 @@ static int attr_setup(
 	    (error = preload_attr_source(repo, attr_session, &head_source)) < 0)
 		goto out;
 
+	if ((opts && (opts->flags & GIT_ATTR_CHECK_INCLUDE_COMMIT) != 0)) {
+		commit_source.commit_id = opts->commit_id;
+
+		if ((error = preload_attr_source(repo, attr_session, &commit_source)) < 0)
+			goto out;
+	}
+
 	if (attr_session)
 		attr_session->init_setup = 1;
 
@@ -480,7 +488,7 @@ int git_attr_add_macro(
 typedef struct {
 	git_repository *repo;
 	git_attr_session *attr_session;
-	uint32_t flags;
+	git_attr_options *opts;
 	const char *workdir;
 	git_index *index;
 	git_vector *files;
@@ -513,7 +521,8 @@ static int attr_decide_sources(
 		break;
 	}
 
-	if ((flags & GIT_ATTR_CHECK_INCLUDE_HEAD) != 0)
+	if ((flags & GIT_ATTR_CHECK_INCLUDE_HEAD) != 0 ||
+	    (flags & GIT_ATTR_CHECK_INCLUDE_COMMIT) != 0)
 		srcs[count++] = GIT_ATTR_FILE_SOURCE_COMMIT;
 
 	return count;
@@ -563,13 +572,19 @@ static int push_one_attr(void *ref, const char *path)
 	int error = 0, n_src, i;
 	bool allow_macros;
 
-	n_src = attr_decide_sources(
-		info->flags, info->workdir != NULL, info->index != NULL, src);
+	n_src = attr_decide_sources(info->opts ? info->opts->flags : 0,
+	                            info->workdir != NULL,
+	                            info->index != NULL,
+	                            src);
+
 	allow_macros = info->workdir ? !strcmp(info->workdir, path) : false;
 
 	for (i = 0; !error && i < n_src; ++i) {
 		git_attr_file_source source = { src[i], path, GIT_ATTR_FILE };
 
+		if (src[i] == GIT_ATTR_FILE_SOURCE_COMMIT && info->opts)
+			source.commit_id = info->opts->commit_id;
+
 		error = push_attr_source(info->repo, info->attr_session, info->files,
 		                       &source, allow_macros);
 	}
@@ -631,7 +646,7 @@ static int collect_attr_files(
 
 	info.repo = repo;
 	info.attr_session = attr_session;
-	info.flags = opts ? opts->flags : 0;
+	info.opts = opts;
 	info.workdir = workdir;
 	if (git_repository_index__weakptr(&info.index, repo) < 0)
 		git_error_clear(); /* no error even if there is no index */
diff --git a/src/attr_file.c b/src/attr_file.c
index 3b8965a..f862738 100644
--- a/src/attr_file.c
+++ b/src/attr_file.c
@@ -113,6 +113,7 @@ int git_attr_file__load(
 	bool allow_macros)
 {
 	int error = 0;
+	git_commit *commit = NULL;
 	git_tree *tree = NULL;
 	git_tree_entry *tree_entry = NULL;
 	git_blob *blob = NULL;
@@ -163,8 +164,14 @@ int git_attr_file__load(
 		break;
 	}
 	case GIT_ATTR_FILE_SOURCE_COMMIT: {
-		if ((error = git_repository_head_tree(&tree, repo)) < 0)
-			goto cleanup;
+		if (source->commit_id) {
+			if ((error = git_commit_lookup(&commit, repo, source->commit_id)) < 0 ||
+			    (error = git_commit_tree(&tree, commit)) < 0)
+				goto cleanup;
+		} else {
+			if ((error = git_repository_head_tree(&tree, repo)) < 0)
+				goto cleanup;
+		}
 
 		if ((error = git_tree_entry_bypath(&tree_entry, tree, entry->path)) < 0) {
 			/*
@@ -239,6 +246,7 @@ cleanup:
 	git_blob_free(blob);
 	git_tree_entry_free(tree_entry);
 	git_tree_free(tree);
+	git_commit_free(commit);
 	git_buf_dispose(&content);
 
 	return error;
@@ -247,7 +255,8 @@ cleanup:
 int git_attr_file__out_of_date(
 	git_repository *repo,
 	git_attr_session *attr_session,
-	git_attr_file *file)
+	git_attr_file *file,
+	git_attr_file_source *source)
 {
 	if (!file)
 		return 1;
@@ -280,13 +289,26 @@ int git_attr_file__out_of_date(
 	}
 
 	case GIT_ATTR_FILE_SOURCE_COMMIT: {
-		git_tree *tree;
+		git_tree *tree = NULL;
 		int error;
 
-		if ((error = git_repository_head_tree(&tree, repo)) < 0)
+		if (source->commit_id) {
+			git_commit *commit = NULL;
+
+			if ((error = git_commit_lookup(&commit, repo, source->commit_id)) < 0)
+				return error;
+
+			error = git_commit_tree(&tree, commit);
+
+			git_commit_free(commit);
+		} else {
+			error = git_repository_head_tree(&tree, repo);
+		}
+
+		if (error < 0)
 			return error;
 
-		error = git_oid__cmp(&file->cache_data.oid, git_tree_id(tree));
+		error = (git_oid__cmp(&file->cache_data.oid, git_tree_id(tree)) != 0);
 
 		git_tree_free(tree);
 		return error;
diff --git a/src/attr_file.h b/src/attr_file.h
index 5c8a412..16e33ca 100644
--- a/src/attr_file.h
+++ b/src/attr_file.h
@@ -55,6 +55,12 @@ typedef struct {
 	 */
 	const char *base;
 	const char *filename;
+
+	/*
+	 * The commit ID when the given source type is a commit (or NULL
+	 * for the repository's HEAD commit.)
+	 */
+	git_oid *commit_id;
 } git_attr_file_source;
 
 extern const char *git_attr__true;
@@ -171,7 +177,7 @@ int git_attr_file__load_standalone(
 	git_attr_file **out, const char *path);
 
 int git_attr_file__out_of_date(
-	git_repository *repo, git_attr_session *session, git_attr_file *file);
+	git_repository *repo, git_attr_session *session, git_attr_file *file, git_attr_file_source *source);
 
 int git_attr_file__parse_buffer(
 	git_repository *repo, git_attr_file *attrs, const char *data, bool allow_macros);
diff --git a/src/attrcache.c b/src/attrcache.c
index f077127..7fe2bfb 100644
--- a/src/attrcache.c
+++ b/src/attrcache.c
@@ -224,16 +224,17 @@ int git_attr_cache__get(
 		return error;
 
 	/* load file if we don't have one or if existing one is out of date */
-	if (!file || (error = git_attr_file__out_of_date(repo, attr_session, file)) > 0)
+	if (!file ||
+	    (error = git_attr_file__out_of_date(repo, attr_session, file, source)) > 0)
 		error = git_attr_file__load(&updated, repo, attr_session,
 		                            entry, source, parser,
 		                            allow_macros);
 
 	/* if we loaded the file, insert into and/or update cache */
 	if (updated) {
-		if ((error = attr_cache_upsert(cache, updated)) < 0)
+		if ((error = attr_cache_upsert(cache, updated)) < 0) {
 			git_attr_file__free(updated);
-		else {
+		} else {
 			git_attr_file__free(file); /* offset incref from lookup */
 			file = updated;
 		}