attr: teach "--attr-source=<tree>" global option to "git"

Earlier, 47cfc9bd (attr: add flag `--source` to work with tree-ish, 2023-01-14) taught "git check-attr" the "--source=<tree>" option to allow it to read attribute files from a tree-ish, but did so only for the command. Just like "check-attr" users wanted a way to use attributes from a tree-ish and not from the working tree files, users of other commands (like "git diff") would benefit from the same. Undo most of the UI change the commit made, while keeping the internal logic to read attributes from a given tree-ish. Expose the internal logic via a new "--attr-source=<tree>" command line option given to "git", so that it can be used with any git command that runs as part of the main git process. Additionally, add an environment variable GIT_ATTR_SOURCE that is set when --attr-source is passed in, so that subprocesses use the same value for the attributes source tree. Signed-off-by: John Cai <johncai86@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

John Cai committed May 6, 2023 at 04:15 UTC 44451a2e5eec5360378be23e2cdbd9ecee49e14e
16 files changed +140 -29
Documentation/git.txt
+8
@@ -212,6 +212,11 @@ If you just want to run git as if it was started in `<path>` then use
212 nohelpers (exclude helper commands), alias and config
213 (retrieve command list from config variable completion.commands)
214
215 +--attr-source=<tree-ish>::
216 + Read gitattributes from <tree-ish> instead of the worktree. See
217 + linkgit:gitattributes[5]. This is equivalent to setting the
218 + `GIT_ATTR_SOURCE` environment variable.
219 +
220 GIT COMMANDS
221 ------------
222
@@ -686,6 +691,9 @@ for further details.
691 tells Git not to verify the SSL certificate when fetching or
692 pushing over HTTPS.
693
694 +`GIT_ATTR_SOURCE`::
695 + Sets the treeish that gitattributes will be read from.
696 +
697 `GIT_ASKPASS`::
698 If this environment variable is set, then Git commands which need to
699 acquire passwords or passphrases (e.g. for HTTP or IMAP authentication)
archive.c
+1 -1
@@ -128,7 +128,7 @@ static const struct attr_check *get_archive_attrs(struct index_state *istate,
128 static struct attr_check *check;
129 if (!check)
130 check = attr_check_initl("export-ignore", "export-subst", NULL);
131 - git_check_attr(istate, NULL, path, check);
131 + git_check_attr(istate, path, check);
132 return check;
133 }
134
attr.c
+35 -2
@@ -20,6 +20,7 @@
20 #include "object-store.h"
21 #include "setup.h"
22 #include "thread-utils.h"
23 +#include "object-name.h"
24
25 const char git_attr__true[] = "(builtin)true";
26 const char git_attr__false[] = "\0(builtin)false";
@@ -1169,11 +1170,42 @@ static void collect_some_attrs(struct index_state *istate,
1170 fill(path, pathlen, basename_offset, check->stack, check->all_attrs, rem);
1171 }
1172
1173 +static const char *default_attr_source_tree_object_name;
1174 +
1175 +void set_git_attr_source(const char *tree_object_name)
1176 +{
1177 + default_attr_source_tree_object_name = xstrdup(tree_object_name);
1178 +}
1179 +
1180 +static void compute_default_attr_source(struct object_id *attr_source)
1181 +{
1182 + if (!default_attr_source_tree_object_name)
1183 + default_attr_source_tree_object_name = getenv(GIT_ATTR_SOURCE_ENVIRONMENT);
1184 +
1185 + if (!default_attr_source_tree_object_name || !is_null_oid(attr_source))
1186 + return;
1187 +
1188 + if (repo_get_oid_treeish(the_repository, default_attr_source_tree_object_name, attr_source))
1189 + die(_("bad --attr-source or GIT_ATTR_SOURCE"));
1190 +}
1191 +
1192 +static struct object_id *default_attr_source(void)
1193 +{
1194 + static struct object_id attr_source;
1195 +
1196 + if (is_null_oid(&attr_source))
1197 + compute_default_attr_source(&attr_source);
1198 + if (is_null_oid(&attr_source))
1199 + return NULL;
1200 + return &attr_source;
1201 +}
1202 +
1203 void git_check_attr(struct index_state *istate,
1173 - const struct object_id *tree_oid, const char *path,
1204 + const char *path,
1205 struct attr_check *check)
1206 {
1207 int i;
1208 + const struct object_id *tree_oid = default_attr_source();
1209
1210 collect_some_attrs(istate, tree_oid, path, check);
1211
@@ -1186,10 +1218,11 @@ void git_check_attr(struct index_state *istate,
1218 }
1219 }
1220
1189 -void git_all_attrs(struct index_state *istate, const struct object_id *tree_oid,
1221 +void git_all_attrs(struct index_state *istate,
1222 const char *path, struct attr_check *check)
1223 {
1224 int i;
1225 + const struct object_id *tree_oid = default_attr_source();
1226
1227 attr_check_reset(check);
1228 collect_some_attrs(istate, tree_oid, path, check);
attr.h
+9 -4
@@ -45,7 +45,7 @@
45 * const char *path;
46 *
47 * setup_check();
48 - * git_check_attr(&the_index, tree_oid, path, check);
48 + * git_check_attr(&the_index, path, check);
49 * ------------
50 *
51 * - Act on `.value` member of the result, left in `check->items[]`:
@@ -120,7 +120,6 @@
120 #define ATTR_MAX_FILE_SIZE (100 * 1024 * 1024)
121
122 struct index_state;
123 -struct object_id;
123
124 /**
125 * An attribute is an opaque object that is identified by its name. Pass the
@@ -135,6 +134,12 @@ struct git_attr;
134 struct all_attrs_item;
135 struct attr_stack;
136
137 +/*
138 + * The textual object name for the tree-ish used by git_check_attr()
139 + * to read attributes from (instead of from the working tree).
140 + */
141 +void set_git_attr_source(const char *);
142 +
143 /*
144 * Given a string, return the gitattribute object that
145 * corresponds to it.
@@ -203,14 +208,14 @@ void attr_check_free(struct attr_check *check);
208 const char *git_attr_name(const struct git_attr *);
209
210 void git_check_attr(struct index_state *istate,
206 - const struct object_id *tree_oid, const char *path,
211 + const char *path,
212 struct attr_check *check);
213
214 /*
215 * Retrieve all attributes that apply to the specified path.
216 * check holds the attributes and their values.
217 */
213 -void git_all_attrs(struct index_state *istate, const struct object_id *tree_oid,
218 +void git_all_attrs(struct index_state *istate,
219 const char *path, struct attr_check *check);
220
221 enum git_attr_direction {
builtin/check-attr.c
+8 -9
@@ -63,7 +63,7 @@ static void output_attr(struct attr_check *check, const char *file)
63 }
64
65 static void check_attr(const char *prefix, struct attr_check *check,
66 - const struct object_id *tree_oid, int collect_all,
66 + int collect_all,
67 const char *file)
68
69 {
@@ -71,9 +71,9 @@ static void check_attr(const char *prefix, struct attr_check *check,
71 prefix_path(prefix, prefix ? strlen(prefix) : 0, file);
72
73 if (collect_all) {
74 - git_all_attrs(&the_index, tree_oid, full_path, check);
74 + git_all_attrs(&the_index, full_path, check);
75 } else {
76 - git_check_attr(&the_index, tree_oid, full_path, check);
76 + git_check_attr(&the_index, full_path, check);
77 }
78 output_attr(check, file);
79
@@ -81,7 +81,7 @@ static void check_attr(const char *prefix, struct attr_check *check,
81 }
82
83 static void check_attr_stdin_paths(const char *prefix, struct attr_check *check,
84 - const struct object_id *tree_oid, int collect_all)
84 + int collect_all)
85 {
86 struct strbuf buf = STRBUF_INIT;
87 struct strbuf unquoted = STRBUF_INIT;
@@ -95,7 +95,7 @@ static void check_attr_stdin_paths(const char *prefix, struct attr_check *check,
95 die("line is badly quoted");
96 strbuf_swap(&buf, &unquoted);
97 }
98 - check_attr(prefix, check, tree_oid, collect_all, buf.buf);
98 + check_attr(prefix, check, collect_all, buf.buf);
99 maybe_flush_or_die(stdout, "attribute to stdout");
100 }
101 strbuf_release(&buf);
@@ -111,7 +111,6 @@ static NORETURN void error_with_usage(const char *msg)
111 int cmd_check_attr(int argc, const char **argv, const char *prefix)
112 {
113 struct attr_check *check;
114 - struct object_id *tree_oid = NULL;
114 struct object_id initialized_oid;
115 int cnt, i, doubledash, filei;
116
@@ -187,14 +186,14 @@ int cmd_check_attr(int argc, const char **argv, const char *prefix)
186 if (source) {
187 if (repo_get_oid_tree(the_repository, source, &initialized_oid))
188 die("%s: not a valid tree-ish source", source);
190 - tree_oid = &initialized_oid;
189 + set_git_attr_source(source);
190 }
191
192 if (stdin_paths)
194 - check_attr_stdin_paths(prefix, check, tree_oid, all_attrs);
193 + check_attr_stdin_paths(prefix, check, all_attrs);
194 else {
195 for (i = filei; i < argc; i++)
197 - check_attr(prefix, check, tree_oid, all_attrs, argv[i]);
196 + check_attr(prefix, check, all_attrs, argv[i]);
197 maybe_flush_or_die(stdout, "attribute to stdout");
198 }
199
builtin/pack-objects.c
+1 -1
@@ -1331,7 +1331,7 @@ static int no_try_delta(const char *path)
1331
1332 if (!check)
1333 check = attr_check_initl("delta", NULL);
1334 - git_check_attr(the_repository->index, NULL, path, check);
1334 + git_check_attr(the_repository->index, path, check);
1335 if (ATTR_FALSE(check->items[0].value))
1336 return 1;
1337 return 0;
convert.c
+1 -1
@@ -1314,7 +1314,7 @@ void convert_attrs(struct index_state *istate,
1314 git_config(read_convert_config, NULL);
1315 }
1316
1317 - git_check_attr(istate, NULL, path, check);
1317 + git_check_attr(istate, path, check);
1318 ccheck = check->items;
1319 ca->crlf_action = git_path_check_crlf(ccheck + 4);
1320 if (ca->crlf_action == CRLF_UNDEFINED)
environment.h
+1
@@ -55,6 +55,7 @@ const char *getenv_safe(struct strvec *argv, const char *name);
55 #define GIT_QUARANTINE_ENVIRONMENT "GIT_QUARANTINE_PATH"
56 #define GIT_OPTIONAL_LOCKS_ENVIRONMENT "GIT_OPTIONAL_LOCKS"
57 #define GIT_TEXT_DOMAIN_DIR_ENVIRONMENT "GIT_TEXTDOMAINDIR"
58 +#define GIT_ATTR_SOURCE_ENVIRONMENT "GIT_ATTR_SOURCE"
59
60 /*
61 * Environment variable used in handshaking the wire protocol.
git.c
+16
@@ -9,6 +9,7 @@
9 #include "alias.h"
10 #include "replace-object.h"
11 #include "setup.h"
12 +#include "attr.h"
13 #include "shallow.h"
14 #include "trace.h"
15 #include "trace2.h"
@@ -314,6 +315,21 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
315 } else {
316 exit(list_cmds(cmd));
317 }
318 + } else if (!strcmp(cmd, "--attr-source")) {
319 + if (*argc < 2) {
320 + fprintf(stderr, _("no attribute source given for --attr-source\n" ));
321 + usage(git_usage_string);
322 + }
323 + setenv(GIT_ATTR_SOURCE_ENVIRONMENT, (*argv)[1], 1);
324 + if (envchanged)
325 + *envchanged = 1;
326 + (*argv)++;
327 + (*argc)--;
328 + } else if (skip_prefix(cmd, "--attr-source=", &cmd)) {
329 + set_git_attr_source(cmd);
330 + setenv(GIT_ATTR_SOURCE_ENVIRONMENT, cmd, 1);
331 + if (envchanged)
332 + *envchanged = 1;
333 } else {
334 fprintf(stderr, _("unknown option: %s\n"), cmd);
335 usage(git_usage_string);
ll-merge.c
+2 -2
@@ -393,7 +393,7 @@ enum ll_merge_result ll_merge(mmbuffer_t *result_buf,
393 normalize_file(theirs, path, istate);
394 }
395
396 - git_check_attr(istate, NULL, path, check);
396 + git_check_attr(istate, path, check);
397 ll_driver_name = check->items[0].value;
398 if (check->items[1].value) {
399 marker_size = atoi(check->items[1].value);
@@ -421,7 +421,7 @@ int ll_merge_marker_size(struct index_state *istate, const char *path)
421
422 if (!check)
423 check = attr_check_initl("conflict-marker-size", NULL);
424 - git_check_attr(istate, NULL, path, check);
424 + git_check_attr(istate, path, check);
425 if (check->items[0].value) {
426 marker_size = atoi(check->items[0].value);
427 if (marker_size <= 0)
pathspec.c
+1 -1
@@ -734,7 +734,7 @@ int match_pathspec_attrs(struct index_state *istate,
734 if (name[namelen])
735 name = to_free = xmemdupz(name, namelen);
736
737 - git_check_attr(istate, NULL, name, item->attr_check);
737 + git_check_attr(istate, name, item->attr_check);
738
739 free(to_free);
740
t/lib-diff-alternative.sh
+26 -5
@@ -112,15 +112,36 @@ EOF
112
113 STRATEGY=$1
114
115 - test_expect_success "$STRATEGY diff from attributes" '
115 + test_expect_success "setup attributes files for tests with $STRATEGY" '
116 + git checkout -b master &&
117 echo "file* diff=driver" >.gitattributes &&
117 - git config diff.driver.algorithm "$STRATEGY" &&
118 - test_must_fail git diff --no-index file1 file2 > output &&
119 - cat expect &&
120 - cat output &&
118 + git add file1 file2 .gitattributes &&
119 + git commit -m "adding files" &&
120 + git checkout -b branchA &&
121 + echo "file* diff=driverA" >.gitattributes &&
122 + git add .gitattributes &&
123 + git commit -m "adding driverA as diff driver" &&
124 + git checkout master &&
125 + git clone --bare --no-local . bare.git
126 + '
127 +
128 + test_expect_success "$STRATEGY diff from attributes" '
129 + test_must_fail git -c diff.driver.algorithm=$STRATEGY diff --no-index file1 file2 > output &&
130 test_cmp expect output
131 '
132
133 + test_expect_success "diff from attributes with bare repo with source" '
134 + git -C bare.git --attr-source=branchA -c diff.driver.algorithm=myers \
135 + -c diff.driverA.algorithm=$STRATEGY \
136 + diff HEAD:file1 HEAD:file2 >output &&
137 + test_cmp expect output
138 + '
139 +
140 + test_expect_success "diff from attributes with bare repo with invalid source" '
141 + test_must_fail git -C bare.git --attr-source=invalid-branch diff \
142 + HEAD:file1 HEAD:file2
143 + '
144 +
145 test_expect_success "$STRATEGY diff from attributes has valid diffstat" '
146 echo "file* diff=driver" >.gitattributes &&
147 git config diff.driver.algorithm "$STRATEGY" &&
t/t0003-attributes.sh
+10 -1
@@ -30,8 +30,17 @@ attr_check_quote () {
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 &&
33 echo "$path: test: $expect" >expect &&
34 +
35 + git $git_opts check-attr --source $source test -- "$path" >actual 2>err &&
36 + test_cmp expect actual &&
37 + test_must_be_empty err &&
38 +
39 + git $git_opts --attr-source="$source" check-attr test -- "$path" >actual 2>err &&
40 + test_cmp expect actual &&
41 + test_must_be_empty err
42 +
43 + GIT_ATTR_SOURCE="$source" git $git_opts check-attr test -- "$path" >actual 2>err &&
44 test_cmp expect actual &&
45 test_must_be_empty err
46 }
t/t4018-diff-funcname.sh
+19
@@ -63,6 +63,25 @@ do
63 test_i18ngrep ! fatal msg &&
64 test_i18ngrep ! error msg
65 '
66 +
67 + test_expect_success "builtin $p pattern compiles on bare repo with --attr-source" '
68 + test_when_finished "rm -rf bare.git" &&
69 + git checkout -B master &&
70 + git add . &&
71 + echo "*.java diff=notexist" >.gitattributes &&
72 + git add .gitattributes &&
73 + git commit -am "changing gitattributes" &&
74 + git checkout -B branchA &&
75 + echo "*.java diff=$p" >.gitattributes &&
76 + git add .gitattributes &&
77 + git commit -am "changing gitattributes" &&
78 + git clone --bare --no-local . bare.git &&
79 + git -C bare.git symbolic-ref HEAD refs/heads/master &&
80 + test_expect_code 1 git -C bare.git --attr-source=branchA \
81 + diff --exit-code HEAD:A.java HEAD:B.java 2>msg &&
82 + test_i18ngrep ! fatal msg &&
83 + test_i18ngrep ! error msg
84 + '
85 done
86
87 test_expect_success 'last regexp must not be negated' '
userdiff.c
+1 -1
@@ -444,7 +444,7 @@ struct userdiff_driver *userdiff_find_by_path(struct index_state *istate,
444 check = attr_check_initl("diff", NULL);
445 if (!path)
446 return NULL;
447 - git_check_attr(istate, NULL, path, check);
447 + git_check_attr(istate, path, check);
448
449 if (ATTR_TRUE(check->items[0].value))
450 return &driver_true;
ws.c
+1 -1
@@ -79,7 +79,7 @@ unsigned whitespace_rule(struct index_state *istate, const char *pathname)
79 if (!attr_whitespace_rule)
80 attr_whitespace_rule = attr_check_initl("whitespace", NULL);
81
82 - git_check_attr(istate, NULL, pathname, attr_whitespace_rule);
82 + git_check_attr(istate, pathname, attr_whitespace_rule);
83 value = attr_whitespace_rule->items[0].value;
84 if (ATTR_TRUE(value)) {
85 /* true (whitespace) */