attr: add flag `--source` to work with tree-ish

The contents of the .gitattributes files may evolve over time, but "git check-attr" always checks attributes against them in the working tree and/or in the index. It may be beneficial to optionally allow the users to check attributes taken from a commit other than HEAD against paths. Add a new flag `--source` which will allow users to check the attributes against a commit (actually any tree-ish would do). When the user uses this flag, we go through the stack of .gitattributes files but instead of checking the current working tree and/or in the index, we check the blobs from the provided tree-ish object. This allows the command to also be used in bare repositories. Since we use a tree-ish object, the user can pass "--source HEAD:subdirectory" and all the attributes will be looked up as if subdirectory was the root directory of the repository. We cannot simply use the `<rev>:<path>` syntax without the `--source` flag, similar to how it is used in `git show` because any non-flag parameter before `--` is treated as an attribute and any parameter after `--` is treated as a pathname. The change involves creating a new function `read_attr_from_blob`, which given the path reads the blob for the path against the provided source and parses the attributes line by line. This function is plugged into `read_attr()` function wherein we go through the stack of attributes files. Signed-off-by: Karthik Nayak <karthik.188@gmail.com> Signed-off-by: Toon Claes <toon@iotcl.com> Co-authored-by: toon@iotcl.com Signed-off-by: Junio C Hamano <gitster@pobox.com>

Karthik Nayak committed Jan 14, 2023 at 09:30 UTC 47cfc9bd7d0add617cf6d928e96b7d207be614f1
12 files changed +151 -53
Documentation/git-check-attr.txt
+7 -2
@@ -9,8 +9,8 @@ git-check-attr - Display gitattributes information
9 SYNOPSIS
10 --------
11 [verse]
12 -'git check-attr' [-a | --all | <attr>...] [--] <pathname>...
13 -'git check-attr' --stdin [-z] [-a | --all | <attr>...]
12 +'git check-attr' [--source <tree-ish>] [-a | --all | <attr>...] [--] <pathname>...
13 +'git check-attr' --stdin [-z] [--source <tree-ish>] [-a | --all | <attr>...]
14
15 DESCRIPTION
16 -----------
@@ -36,6 +36,11 @@ OPTIONS
36 If `--stdin` is also given, input paths are separated
37 with a NUL character instead of a linefeed character.
38
39 +--source=<tree-ish>::
40 + Check attributes against the specified tree-ish. It is common to
41 + specify the source tree by naming a commit, branch or tag associated
42 + with it.
43 +
44 \--::
45 Interpret all preceding arguments as attributes and all following
46 arguments as path names.
archive.c
+1 -1
@@ -120,7 +120,7 @@ static const struct attr_check *get_archive_attrs(struct index_state *istate,
120 static struct attr_check *check;
121 if (!check)
122 check = attr_check_initl("export-ignore", "export-subst", NULL);
123 - git_check_attr(istate, path, check);
123 + git_check_attr(istate, NULL, path, check);
124 return check;
125 }
126
attr.c
+70 -27
@@ -13,6 +13,8 @@
13 #include "dir.h"
14 #include "utf8.h"
15 #include "quote.h"
16 +#include "revision.h"
17 +#include "object-store.h"
18 #include "thread-utils.h"
19
20 const char git_attr__true[] = "(builtin)true";
@@ -729,14 +731,62 @@ static struct attr_stack *read_attr_from_file(const char *path, unsigned flags)
731 return res;
732 }
733
732 -static struct attr_stack *read_attr_from_index(struct index_state *istate,
733 - const char *path,
734 - unsigned flags)
734 +static struct attr_stack *read_attr_from_buf(char *buf, const char *path,
735 + unsigned flags)
736 {
737 struct attr_stack *res;
737 - char *buf, *sp;
738 + char *sp;
739 int lineno = 0;
740
741 + if (!buf)
742 + return NULL;
743 +
744 + CALLOC_ARRAY(res, 1);
745 + for (sp = buf; *sp;) {
746 + char *ep;
747 + int more;
748 +
749 + ep = strchrnul(sp, '\n');
750 + more = (*ep == '\n');
751 + *ep = '\0';
752 + handle_attr_line(res, sp, path, ++lineno, flags);
753 + sp = ep + more;
754 + }
755 + free(buf);
756 +
757 + return res;
758 +}
759 +
760 +static struct attr_stack *read_attr_from_blob(struct index_state *istate,
761 + const struct object_id *tree_oid,
762 + const char *path, unsigned flags)
763 +{
764 + struct object_id oid;
765 + unsigned long sz;
766 + enum object_type type;
767 + void *buf;
768 + unsigned short mode;
769 +
770 + if (!tree_oid)
771 + return NULL;
772 +
773 + if (get_tree_entry(istate->repo, tree_oid, path, &oid, &mode))
774 + return NULL;
775 +
776 + buf = repo_read_object_file(istate->repo, &oid, &type, &sz);
777 + if (!buf || type != OBJ_BLOB) {
778 + free(buf);
779 + return NULL;
780 + }
781 +
782 + return read_attr_from_buf(buf, path, flags);
783 +}
784 +
785 +static struct attr_stack *read_attr_from_index(struct index_state *istate,
786 + const char *path, unsigned flags)
787 +{
788 + char *buf;
789 +
790 if (!istate)
791 return NULL;
792
@@ -758,28 +808,19 @@ static struct attr_stack *read_attr_from_index(struct index_state *istate,
808 if (!buf)
809 return NULL;
810
761 - CALLOC_ARRAY(res, 1);
762 - for (sp = buf; *sp; ) {
763 - char *ep;
764 - int more;
765 -
766 - ep = strchrnul(sp, '\n');
767 - more = (*ep == '\n');
768 - *ep = '\0';
769 - handle_attr_line(res, sp, path, ++lineno, flags);
770 - sp = ep + more;
771 - }
772 - free(buf);
773 - return res;
811 + return read_attr_from_buf(buf, path, flags);
812 }
813
814 static struct attr_stack *read_attr(struct index_state *istate,
815 + const struct object_id *tree_oid,
816 const char *path, unsigned flags)
817 {
818 struct attr_stack *res = NULL;
819
820 if (direction == GIT_ATTR_INDEX) {
821 res = read_attr_from_index(istate, path, flags);
822 + } else if (tree_oid) {
823 + res = read_attr_from_blob(istate, tree_oid, path, flags);
824 } else if (!is_bare_repository()) {
825 if (direction == GIT_ATTR_CHECKOUT) {
826 res = read_attr_from_index(istate, path, flags);
@@ -839,6 +880,7 @@ static void push_stack(struct attr_stack **attr_stack_p,
880 }
881
882 static void bootstrap_attr_stack(struct index_state *istate,
883 + const struct object_id *tree_oid,
884 struct attr_stack **stack)
885 {
886 struct attr_stack *e;
@@ -864,7 +906,7 @@ static void bootstrap_attr_stack(struct index_state *istate,
906 }
907
908 /* root directory */
867 - e = read_attr(istate, GITATTRIBUTES_FILE, flags | READ_ATTR_NOFOLLOW);
909 + e = read_attr(istate, tree_oid, GITATTRIBUTES_FILE, flags | READ_ATTR_NOFOLLOW);
910 push_stack(stack, e, xstrdup(""), 0);
911
912 /* info frame */
@@ -878,6 +920,7 @@ static void bootstrap_attr_stack(struct index_state *istate,
920 }
921
922 static void prepare_attr_stack(struct index_state *istate,
923 + const struct object_id *tree_oid,
924 const char *path, int dirlen,
925 struct attr_stack **stack)
926 {
@@ -899,7 +942,7 @@ static void prepare_attr_stack(struct index_state *istate,
942 * .gitattributes in deeper directories to shallower ones,
943 * and finally use the built-in set as the default.
944 */
902 - bootstrap_attr_stack(istate, stack);
945 + bootstrap_attr_stack(istate, tree_oid, stack);
946
947 /*
948 * Pop the "info" one that is always at the top of the stack.
@@ -954,7 +997,7 @@ static void prepare_attr_stack(struct index_state *istate,
997 strbuf_add(&pathbuf, path + pathbuf.len, (len - pathbuf.len));
998 strbuf_addf(&pathbuf, "/%s", GITATTRIBUTES_FILE);
999
957 - next = read_attr(istate, pathbuf.buf, READ_ATTR_NOFOLLOW);
1000 + next = read_attr(istate, tree_oid, pathbuf.buf, READ_ATTR_NOFOLLOW);
1001
1002 /* reset the pathbuf to not include "/.gitattributes" */
1003 strbuf_setlen(&pathbuf, len);
@@ -1074,8 +1117,8 @@ static void determine_macros(struct all_attrs_item *all_attrs,
1117 * Otherwise all attributes are collected.
1118 */
1119 static void collect_some_attrs(struct index_state *istate,
1077 - const char *path,
1078 - struct attr_check *check)
1120 + const struct object_id *tree_oid,
1121 + const char *path, struct attr_check *check)
1122 {
1123 int pathlen, rem, dirlen;
1124 const char *cp, *last_slash = NULL;
@@ -1094,7 +1137,7 @@ static void collect_some_attrs(struct index_state *istate,
1137 dirlen = 0;
1138 }
1139
1097 - prepare_attr_stack(istate, path, dirlen, &check->stack);
1140 + prepare_attr_stack(istate, tree_oid, path, dirlen, &check->stack);
1141 all_attrs_init(&g_attr_hashmap, check);
1142 determine_macros(check->all_attrs, check->stack);
1143
@@ -1103,12 +1146,12 @@ static void collect_some_attrs(struct index_state *istate,
1146 }
1147
1148 void git_check_attr(struct index_state *istate,
1106 - const char *path,
1149 + const struct object_id *tree_oid, const char *path,
1150 struct attr_check *check)
1151 {
1152 int i;
1153
1111 - collect_some_attrs(istate, path, check);
1154 + collect_some_attrs(istate, tree_oid, path, check);
1155
1156 for (i = 0; i < check->nr; i++) {
1157 size_t n = check->items[i].attr->attr_nr;
@@ -1119,13 +1162,13 @@ void git_check_attr(struct index_state *istate,
1162 }
1163 }
1164
1122 -void git_all_attrs(struct index_state *istate,
1165 +void git_all_attrs(struct index_state *istate, const struct object_id *tree_oid,
1166 const char *path, struct attr_check *check)
1167 {
1168 int i;
1169
1170 attr_check_reset(check);
1128 - collect_some_attrs(istate, path, check);
1171 + collect_some_attrs(istate, tree_oid, path, check);
1172
1173 for (i = 0; i < check->all_attrs_nr; i++) {
1174 const char *name = check->all_attrs[i].attr->name;
attr.h
+4 -2
@@ -108,6 +108,7 @@
108 */
109
110 struct index_state;
111 +struct object_id;
112
113 /**
114 * An attribute is an opaque object that is identified by its name. Pass the
@@ -190,13 +191,14 @@ void attr_check_free(struct attr_check *check);
191 const char *git_attr_name(const struct git_attr *);
192
193 void git_check_attr(struct index_state *istate,
193 - const char *path, struct attr_check *check);
194 + const struct object_id *tree_oid, const char *path,
195 + struct attr_check *check);
196
197 /*
198 * Retrieve all attributes that apply to the specified path.
199 * check holds the attributes and their values.
200 */
199 -void git_all_attrs(struct index_state *istate,
201 +void git_all_attrs(struct index_state *istate, const struct object_id *tree_oid,
202 const char *path, struct attr_check *check);
203
204 enum git_attr_direction {
builtin/check-attr.c
+22 -13
@@ -9,9 +9,10 @@
9 static int all_attrs;
10 static int cached_attrs;
11 static int stdin_paths;
12 +static char *source;
13 static const char * const check_attr_usage[] = {
13 -N_("git check-attr [-a | --all | <attr>...] [--] <pathname>..."),
14 -N_("git check-attr --stdin [-z] [-a | --all | <attr>...]"),
14 +N_("git check-attr [--source <tree-ish>] [-a | --all | <attr>...] [--] <pathname>..."),
15 +N_("git check-attr --stdin [-z] [--source <tree-ish>] [-a | --all | <attr>...]"),
16 NULL
17 };
18
@@ -23,6 +24,7 @@ static const struct option check_attr_options[] = {
24 OPT_BOOL(0 , "stdin", &stdin_paths, N_("read file names from stdin")),
25 OPT_BOOL('z', NULL, &nul_term_line,
26 N_("terminate input and output records by a NUL character")),
27 + OPT_STRING(0, "source", &source, N_("<tree-ish>"), N_("which tree-ish to check attributes at")),
28 OPT_END()
29 };
30
@@ -55,27 +57,26 @@ static void output_attr(struct attr_check *check, const char *file)
57 }
58 }
59
58 -static void check_attr(const char *prefix,
59 - struct attr_check *check,
60 - int collect_all,
60 +static void check_attr(const char *prefix, struct attr_check *check,
61 + const struct object_id *tree_oid, int collect_all,
62 const char *file)
63 +
64 {
65 char *full_path =
66 prefix_path(prefix, prefix ? strlen(prefix) : 0, file);
67
68 if (collect_all) {
67 - git_all_attrs(&the_index, full_path, check);
69 + git_all_attrs(&the_index, tree_oid, full_path, check);
70 } else {
69 - git_check_attr(&the_index, full_path, check);
71 + git_check_attr(&the_index, tree_oid, full_path, check);
72 }
73 output_attr(check, file);
74
75 free(full_path);
76 }
77
76 -static void check_attr_stdin_paths(const char *prefix,
77 - struct attr_check *check,
78 - int collect_all)
78 +static void check_attr_stdin_paths(const char *prefix, struct attr_check *check,
79 + const struct object_id *tree_oid, int collect_all)
80 {
81 struct strbuf buf = STRBUF_INIT;
82 struct strbuf unquoted = STRBUF_INIT;
@@ -89,7 +90,7 @@ static void check_attr_stdin_paths(const char *prefix,
90 die("line is badly quoted");
91 strbuf_swap(&buf, &unquoted);
92 }
92 - check_attr(prefix, check, collect_all, buf.buf);
93 + check_attr(prefix, check, tree_oid, collect_all, buf.buf);
94 maybe_flush_or_die(stdout, "attribute to stdout");
95 }
96 strbuf_release(&buf);
@@ -105,6 +106,8 @@ static NORETURN void error_with_usage(const char *msg)
106 int cmd_check_attr(int argc, const char **argv, const char *prefix)
107 {
108 struct attr_check *check;
109 + struct object_id *tree_oid = NULL;
110 + struct object_id initialized_oid;
111 int cnt, i, doubledash, filei;
112
113 if (!is_bare_repository())
@@ -176,11 +179,17 @@ int cmd_check_attr(int argc, const char **argv, const char *prefix)
179 }
180 }
181
182 + if (source) {
183 + if (repo_get_oid_tree(the_repository, source, &initialized_oid))
184 + die("%s: not a valid tree-ish source", source);
185 + tree_oid = &initialized_oid;
186 + }
187 +
188 if (stdin_paths)
180 - check_attr_stdin_paths(prefix, check, all_attrs);
189 + check_attr_stdin_paths(prefix, check, tree_oid, all_attrs);
190 else {
191 for (i = filei; i < argc; i++)
183 - check_attr(prefix, check, all_attrs, argv[i]);
192 + check_attr(prefix, check, tree_oid, all_attrs, argv[i]);
193 maybe_flush_or_die(stdout, "attribute to stdout");
194 }
195
builtin/pack-objects.c
+1 -1
@@ -1318,7 +1318,7 @@ static int no_try_delta(const char *path)
1318
1319 if (!check)
1320 check = attr_check_initl("delta", NULL);
1321 - git_check_attr(the_repository->index, path, check);
1321 + git_check_attr(the_repository->index, NULL, path, check);
1322 if (ATTR_FALSE(check->items[0].value))
1323 return 1;
1324 return 0;
convert.c
+1 -1
@@ -1308,7 +1308,7 @@ void convert_attrs(struct index_state *istate,
1308 git_config(read_convert_config, NULL);
1309 }
1310
1311 - git_check_attr(istate, path, check);
1311 + git_check_attr(istate, NULL, path, check);
1312 ccheck = check->items;
1313 ca->crlf_action = git_path_check_crlf(ccheck + 4);
1314 if (ca->crlf_action == CRLF_UNDEFINED)
ll-merge.c
+2 -2
@@ -391,7 +391,7 @@ enum ll_merge_result ll_merge(mmbuffer_t *result_buf,
391 normalize_file(theirs, path, istate);
392 }
393
394 - git_check_attr(istate, path, check);
394 + git_check_attr(istate, NULL, path, check);
395 ll_driver_name = check->items[0].value;
396 if (check->items[1].value) {
397 marker_size = atoi(check->items[1].value);
@@ -419,7 +419,7 @@ int ll_merge_marker_size(struct index_state *istate, const char *path)
419
420 if (!check)
421 check = attr_check_initl("conflict-marker-size", NULL);
422 - git_check_attr(istate, path, check);
422 + git_check_attr(istate, NULL, path, check);
423 if (check->items[0].value) {
424 marker_size = atoi(check->items[0].value);
425 if (marker_size <= 0)
pathspec.c
+1 -1
@@ -732,7 +732,7 @@ int match_pathspec_attrs(struct index_state *istate,
732 if (name[namelen])
733 name = to_free = xmemdupz(name, namelen);
734
735 - git_check_attr(istate, name, item->attr_check);
735 + git_check_attr(istate, NULL, name, item->attr_check);
736
737 free(to_free);
738
t/t0003-attributes.sh
+40 -1
@@ -25,7 +25,15 @@ attr_check_quote () {
25 git check-attr test -- "$path" >actual &&
26 echo "\"$quoted_path\": test: $expect" >expect &&
27 test_cmp expect actual
28 +}
29 +
30 +attr_check_source () {
31 + path="$1" expect="$2" source="$3" git_opts="$4" &&
32
33 + git $git_opts check-attr --source $source test -- "$path" >actual 2>err &&
34 + echo "$path: test: $expect" >expect &&
35 + test_cmp expect actual &&
36 + test_must_be_empty err
37 }
38
39 test_expect_success 'open-quoted pathname' '
@@ -33,7 +41,6 @@ test_expect_success 'open-quoted pathname' '
41 attr_check a unspecified
42 '
43
36 -
44 test_expect_success 'setup' '
45 mkdir -p a/b/d a/c b &&
46 (
@@ -80,12 +87,23 @@ test_expect_success 'setup' '
87 EOF
88 '
89
90 +test_expect_success 'setup branches' '
91 + mkdir -p foo/bar &&
92 + test_commit --printf "add .gitattributes" foo/bar/.gitattributes \
93 + "f test=f\na/i test=n\n" tag-1 &&
94 + test_commit --printf "add .gitattributes" foo/bar/.gitattributes \
95 + "g test=g\na/i test=m\n" tag-2 &&
96 + rm foo/bar/.gitattributes
97 +'
98 +
99 test_expect_success 'command line checks' '
100 test_must_fail git check-attr &&
101 test_must_fail git check-attr -- &&
102 test_must_fail git check-attr test &&
103 test_must_fail git check-attr test -- &&
104 test_must_fail git check-attr -- f &&
105 + test_must_fail git check-attr --source &&
106 + test_must_fail git check-attr --source not-a-valid-ref &&
107 echo "f" | test_must_fail git check-attr --stdin &&
108 echo "f" | test_must_fail git check-attr --stdin -- f &&
109 echo "f" | test_must_fail git check-attr --stdin test -- f &&
@@ -287,6 +305,15 @@ test_expect_success 'using --git-dir and --work-tree' '
305 )
306 '
307
308 +test_expect_success 'using --source' '
309 + attr_check_source foo/bar/f f tag-1 &&
310 + attr_check_source foo/bar/a/i n tag-1 &&
311 + attr_check_source foo/bar/f unspecified tag-2 &&
312 + attr_check_source foo/bar/a/i m tag-2 &&
313 + attr_check_source foo/bar/g g tag-2 &&
314 + attr_check_source foo/bar/g unspecified tag-1
315 +'
316 +
317 test_expect_success 'setup bare' '
318 git clone --template= --bare . bare.git
319 '
@@ -306,6 +333,18 @@ test_expect_success 'bare repository: check that .gitattribute is ignored' '
333 )
334 '
335
336 +test_expect_success 'bare repository: with --source' '
337 + (
338 + cd bare.git &&
339 + attr_check_source foo/bar/f f tag-1 &&
340 + attr_check_source foo/bar/a/i n tag-1 &&
341 + attr_check_source foo/bar/f unspecified tag-2 &&
342 + attr_check_source foo/bar/a/i m tag-2 &&
343 + attr_check_source foo/bar/g g tag-2 &&
344 + attr_check_source foo/bar/g unspecified tag-1
345 + )
346 +'
347 +
348 test_expect_success 'bare repository: check that --cached honors index' '
349 (
350 cd bare.git &&
userdiff.c
+1 -1
@@ -413,7 +413,7 @@ struct userdiff_driver *userdiff_find_by_path(struct index_state *istate,
413 check = attr_check_initl("diff", NULL);
414 if (!path)
415 return NULL;
416 - git_check_attr(istate, path, check);
416 + git_check_attr(istate, NULL, path, check);
417
418 if (ATTR_TRUE(check->items[0].value))
419 return &driver_true;
ws.c
+1 -1
@@ -78,7 +78,7 @@ unsigned whitespace_rule(struct index_state *istate, const char *pathname)
78 if (!attr_whitespace_rule)
79 attr_whitespace_rule = attr_check_initl("whitespace", NULL);
80
81 - git_check_attr(istate, pathname, attr_whitespace_rule);
81 + git_check_attr(istate, NULL, pathname, attr_whitespace_rule);
82 value = attr_whitespace_rule->items[0].value;
83 if (ATTR_TRUE(value)) {
84 /* true (whitespace) */