diff mbox series

[1/2] diff: use HEAD for attributes when using bare repository

Message ID 0fc704cf1c0724473a61086098d44c3a82938b03.1678758818.git.gitgitgadget@gmail.com (mailing list archive)
State New, archived
Headers show
Series diff: support bare repositories when reading gitattributes for diff algorithm | expand

Commit Message

John Cai March 14, 2023, 1:53 a.m. UTC
From: John Cai <johncai86@gmail.com>

(a4cf900e diff: teach diff to read algorithm from diff driver,
2022-02-20) does not support bare repositories. Since running diff
on bare repositories is often done on Git servers, it would be useful to
allow bare repositories to also take advantage of setting the algorithm
via the diff driver.

Teach diff to check against the attributes from HEAD if a bare
repository is being operated on. This change also allows custom diff
drivers to work with bare repositories.

Signed-off-by: John Cai <johncai86@gmail.com>
---
 Documentation/gitattributes.txt |  4 ++++
 diff.c                          | 33 +++++++++++++++++++++++++++++----
 t/lib-diff-alternative.sh       | 10 ++++++++++
 t/t4018-diff-funcname.sh        | 11 +++++++++++
 userdiff.c                      |  9 ++++++++-
 userdiff.h                      |  4 ++++
 6 files changed, 66 insertions(+), 5 deletions(-)

Comments

Junio C Hamano March 14, 2023, 4:54 p.m. UTC | #1
"John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: John Cai <johncai86@gmail.com>
>
> (a4cf900e diff: teach diff to read algorithm from diff driver,
> 2022-02-20) does not support bare repositories.

Wrong placement of opening parenthesis, i.e. "a4cf900e (diff: teach
diff to read algorithm from diff driver, 2023-02-20)".  The date is
also wrong.  Use "git show -s --format=reference a4cf900e".  Insert
its output directly to your editor instead of transcribing manually.

I do not think the commit is to blame in the first place for two
reasons.

 * Even in a bare repository, the attributes defined in the
   $GIT_DIR/info/attributes file are in effect, and the new feature
   added by a4cf900e should work just fine with it.

 * By definition, there is no working tree in a bare repository, and
   it always has been naturally expected we won't read attributes
   from working tree files.  This is in no way limited to the diff
   driver feature recently added.

So the above does not look like a good first sentence to explain
this change.

> Since running diff
> on bare repositories is often done on Git servers, it would be useful to
> allow bare repositories to also take advantage of setting the algorithm
> via the diff driver.

Since referring to in-tree attributes is often useful with any
command, not limited to "diff", I wonder if it is feasible to add
support for the --attr-source=<tree> option globally, instead of
implementing such an option piecemeal.  If your "git diff" in a bare
repository starts honoring attributes recorded in HEAD, don't your
users expect your "git log" and "git show" to also honor them?
Junio C Hamano March 14, 2023, 5:43 p.m. UTC | #2
Junio C Hamano <gitster@pobox.com> writes:

> Since referring to in-tree attributes is often useful with any
> command, not limited to "diff", I wonder if it is feasible to add
> support for the --attr-source=<tree> option globally, instead of
> implementing such an option piecemeal.  If your "git diff" in a bare
> repository starts honoring attributes recorded in HEAD, don't your
> users expect your "git log" and "git show" to also honor them?

Just for illustration, here is one way to do so.

The implementation goes in the opposite direction from the more
recent trend, which is why I am not making it an official patch, but
with this you can do things like:

  $ git --attr-source=e83c5163 check-attr whitespace cache.h
  cache.h: whitespace: unspecified
  $ git --attr-source=e2f6331a142^ check-attr whitespace cache.h
  cache.h: whitespace: set
  $ git --attr-source=HEAD check-attr whitespace cache.h
  cache.h: whitespace: indent,trail,space

where e83c5163 is the very first version of Git from 2005 where
.gitattributes did not exist, e2f6331a142^ is the last version
before we set whitespace to indent,trail,space, and HEAD is a more
recent version of our source tree.

In the following illustration patch, the attr-source tree object
name is kept as a string as long as possible and at the very last
minute when we call git_check_attr() for the first time we turn it
into an object id.  This is because at the very early stage when we
parse the global option we may not even know where our repository is
(hence do not have enough information to parse HEAD).  We also figure
out is_bare_repository() late in the process for the same reason.



 attr.c | 29 +++++++++++++++++++++++++++++
 attr.h |  6 ++++++
 git.c  |  3 +++
 3 files changed, 38 insertions(+)

diff --git c/attr.c w/attr.c
index 1053dfcd4b..2fd6e0eab2 100644
--- c/attr.c
+++ w/attr.c
@@ -1165,12 +1165,41 @@ static void collect_some_attrs(struct index_state *istate,
 	fill(path, pathlen, basename_offset, check->stack, check->all_attrs, rem);
 }
 
+static const char *default_attr_source_tree_object_name;
+
+void set_git_attr_source(const char *tree_object_name)
+{
+	default_attr_source_tree_object_name = xstrdup(tree_object_name);
+}
+
+
+static struct object_id *default_attr_source(void)
+{
+	static struct object_id attr_source;
+
+	if (is_null_oid(&attr_source)) {
+		if (!default_attr_source_tree_object_name &&
+		    is_bare_repository())
+			default_attr_source_tree_object_name = "HEAD";
+
+		if (default_attr_source_tree_object_name &&
+		    is_null_oid(&attr_source))
+			get_oid(default_attr_source_tree_object_name, &attr_source);
+	}
+	if (is_null_oid(&attr_source))
+		return NULL;
+	return &attr_source;
+}
+
 void git_check_attr(struct index_state *istate,
 		    const struct object_id *tree_oid, const char *path,
 		    struct attr_check *check)
 {
 	int i;
 
+	if (!tree_oid)
+		tree_oid = default_attr_source();
+
 	collect_some_attrs(istate, tree_oid, path, check);
 
 	for (i = 0; i < check->nr; i++) {
diff --git c/attr.h w/attr.h
index 9884ea2bc6..a77e3713b2 100644
--- c/attr.h
+++ w/attr.h
@@ -135,6 +135,12 @@ struct git_attr;
 struct all_attrs_item;
 struct attr_stack;
 
+/*
+ * The textual object name for the tree-ish used by git_check_attr()
+ * when NULL is given to tree_oid.
+ */
+void set_git_attr_source(const char *);
+
 /*
  * Given a string, return the gitattribute object that
  * corresponds to it.
diff --git c/git.c w/git.c
index 6171fd6769..21bddc5718 100644
--- c/git.c
+++ w/git.c
@@ -4,6 +4,7 @@
 #include "help.h"
 #include "run-command.h"
 #include "alias.h"
+#include "attr.h"
 #include "shallow.h"
 
 #define RUN_SETUP		(1<<0)
@@ -307,6 +308,8 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
 			} else {
 				exit(list_cmds(cmd));
 			}
+		} else if (skip_prefix(cmd, "--attr-source=", &cmd)) {
+			set_git_attr_source(cmd);
 		} else {
 			fprintf(stderr, _("unknown option: %s\n"), cmd);
 			usage(git_usage_string);
John Cai March 14, 2023, 7:38 p.m. UTC | #3
On 23/03/14 10:43AM, Junio C Hamano wrote:
> Junio C Hamano <gitster@pobox.com> writes:

Hi Junio,

> 
> > Since referring to in-tree attributes is often useful with any
> > command, not limited to "diff", I wonder if it is feasible to add
> > support for the --attr-source=<tree> option globally, instead of
> > implementing such an option piecemeal.  If your "git diff" in a bare
> > repository starts honoring attributes recorded in HEAD, don't your
> > users expect your "git log" and "git show" to also honor them?
> 
> Just for illustration, here is one way to do so.
> 
> The implementation goes in the opposite direction from the more
> recent trend, which is why I am not making it an official patch, but

Could you explain why this goes against the "more recent trend" for my
understanding?

> with this you can do things like:
> 
>   $ git --attr-source=e83c5163 check-attr whitespace cache.h
>   cache.h: whitespace: unspecified
>   $ git --attr-source=e2f6331a142^ check-attr whitespace cache.h
>   cache.h: whitespace: set
>   $ git --attr-source=HEAD check-attr whitespace cache.h
>   cache.h: whitespace: indent,trail,space

I like the idea of an option that is global. For git-check-attr however, we
already have a --source option. Not sure how big of a deal that is. If we wanted
to, we could put --source on a deprecation path by making it hidden for a
while, etc.

> 
> where e83c5163 is the very first version of Git from 2005 where
> .gitattributes did not exist, e2f6331a142^ is the last version
> before we set whitespace to indent,trail,space, and HEAD is a more
> recent version of our source tree.
> 
> In the following illustration patch, the attr-source tree object
> name is kept as a string as long as possible and at the very last
> minute when we call git_check_attr() for the first time we turn it
> into an object id.  This is because at the very early stage when we
> parse the global option we may not even know where our repository is
> (hence do not have enough information to parse HEAD).  We also figure
> out is_bare_repository() late in the process for the same reason.
> 
> 
> 
>  attr.c | 29 +++++++++++++++++++++++++++++
>  attr.h |  6 ++++++
>  git.c  |  3 +++
>  3 files changed, 38 insertions(+)
> 
> diff --git c/attr.c w/attr.c
> index 1053dfcd4b..2fd6e0eab2 100644
> --- c/attr.c
> +++ w/attr.c
> @@ -1165,12 +1165,41 @@ static void collect_some_attrs(struct index_state *istate,
>  	fill(path, pathlen, basename_offset, check->stack, check->all_attrs, rem);
>  }
>  
> +static const char *default_attr_source_tree_object_name;
> +
> +void set_git_attr_source(const char *tree_object_name)
> +{
> +	default_attr_source_tree_object_name = xstrdup(tree_object_name);
> +}
> +
> +
> +static struct object_id *default_attr_source(void)
> +{
> +	static struct object_id attr_source;
> +
> +	if (is_null_oid(&attr_source)) {
> +		if (!default_attr_source_tree_object_name &&
> +		    is_bare_repository())
> +			default_attr_source_tree_object_name = "HEAD";
> +
> +		if (default_attr_source_tree_object_name &&
> +		    is_null_oid(&attr_source))
> +			get_oid(default_attr_source_tree_object_name, &attr_source);
> +	}
> +	if (is_null_oid(&attr_source))
> +		return NULL;
> +	return &attr_source;
> +}
> +
>  void git_check_attr(struct index_state *istate,
>  		    const struct object_id *tree_oid, const char *path,
>  		    struct attr_check *check)
>  {
>  	int i;
>  
> +	if (!tree_oid)
> +		tree_oid = default_attr_source();
> +
>  	collect_some_attrs(istate, tree_oid, path, check);
>  
>  	for (i = 0; i < check->nr; i++) {
> diff --git c/attr.h w/attr.h
> index 9884ea2bc6..a77e3713b2 100644
> --- c/attr.h
> +++ w/attr.h
> @@ -135,6 +135,12 @@ struct git_attr;
>  struct all_attrs_item;
>  struct attr_stack;
>  
> +/*
> + * The textual object name for the tree-ish used by git_check_attr()
> + * when NULL is given to tree_oid.
> + */
> +void set_git_attr_source(const char *);
> +
>  /*
>   * Given a string, return the gitattribute object that
>   * corresponds to it.
> diff --git c/git.c w/git.c
> index 6171fd6769..21bddc5718 100644
> --- c/git.c
> +++ w/git.c
> @@ -4,6 +4,7 @@
>  #include "help.h"
>  #include "run-command.h"
>  #include "alias.h"
> +#include "attr.h"
>  #include "shallow.h"
>  
>  #define RUN_SETUP		(1<<0)
> @@ -307,6 +308,8 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
>  			} else {
>  				exit(list_cmds(cmd));
>  			}
> +		} else if (skip_prefix(cmd, "--attr-source=", &cmd)) {
> +			set_git_attr_source(cmd);
>  		} else {
>  			fprintf(stderr, _("unknown option: %s\n"), cmd);
>  			usage(git_usage_string);
Junio C Hamano March 14, 2023, 8:37 p.m. UTC | #4
John Cai <johncai86@gmail.com> writes:

>> Just for illustration, here is one way to do so.
>> 
>> The implementation goes in the opposite direction from the more
>> recent trend, which is why I am not making it an official patch, but
>
> Could you explain why this goes against the "more recent trend" for my
> understanding?

The illustration uses a global state.

The recent trend is to reduce reliance on global states and use the
repository object and others that hold such state through the
callchain.

But a new global variable that holds the fallback tree-ish object name
was a so convenient way to illustrate the core of the idea, without
having to change many callchains.

>> with this you can do things like:
>> 
>>   $ git --attr-source=e83c5163 check-attr whitespace cache.h
>>   cache.h: whitespace: unspecified
>>   $ git --attr-source=e2f6331a142^ check-attr whitespace cache.h
>>   cache.h: whitespace: set
>>   $ git --attr-source=HEAD check-attr whitespace cache.h
>>   cache.h: whitespace: indent,trail,space
>
> I like the idea of an option that is global. For git-check-attr however, we

I guess I shouldn't have used check-attr to avoid confusion.

The point is that the internal mechanisms introduced by 47cfc9bd
(attr: add flag `--source` to work with tree-ish, 2023-01-14), which
taught check-attr the --source option and is reused by this
illustration patch, was a good idea, but its UI was a mistake.  We
do not need per-command --source option the commit adds if we did
the global option from day one.  Yes, I think we can deprecate the
"--source" option from there, if we all prefer the global option
avenue.  I _think_ git_all_attrs() needs to be told about the
default attr-source trick (which I didn't touch in my illustration
patch) before it happens, though.

If you used the mechanism in the illustration patch I gave you, and
adjusted the test part of your patch to match (i.e. "diff" does not
learn "--attr-source" option, but "git --attr-source=... diff" is
how you make it read attributes from a tree-ish), would the result
work well, or do we need more work to make it usable?  How well do
other commands (e.g. "git show") work in the same test repository
you created in your version?

Thanks.
John Cai March 16, 2023, 4:46 p.m. UTC | #5
Hi Junio,

On 14 Mar 2023, at 16:37, Junio C Hamano wrote:

> John Cai <johncai86@gmail.com> writes:
>
>>> Just for illustration, here is one way to do so.
>>>
>>> The implementation goes in the opposite direction from the more
>>> recent trend, which is why I am not making it an official patch, but
>>
>> Could you explain why this goes against the "more recent trend" for my
>> understanding?
>
> The illustration uses a global state.
>
> The recent trend is to reduce reliance on global states and use the
> repository object and others that hold such state through the
> callchain.

Ah I see--in that case, what would be a good object to put this state into? Mabe
repo_settings?

>
> But a new global variable that holds the fallback tree-ish object name
> was a so convenient way to illustrate the core of the idea, without
> having to change many callchains.
>
>>> with this you can do things like:
>>>
>>>   $ git --attr-source=e83c5163 check-attr whitespace cache.h
>>>   cache.h: whitespace: unspecified
>>>   $ git --attr-source=e2f6331a142^ check-attr whitespace cache.h
>>>   cache.h: whitespace: set
>>>   $ git --attr-source=HEAD check-attr whitespace cache.h
>>>   cache.h: whitespace: indent,trail,space
>>
>> I like the idea of an option that is global. For git-check-attr however, we
>
> I guess I shouldn't have used check-attr to avoid confusion.
>
> The point is that the internal mechanisms introduced by 47cfc9bd
> (attr: add flag `--source` to work with tree-ish, 2023-01-14), which
> taught check-attr the --source option and is reused by this
> illustration patch, was a good idea, but its UI was a mistake.  We
> do not need per-command --source option the commit adds if we did
> the global option from day one.  Yes, I think we can deprecate the
> "--source" option from there, if we all prefer the global option
> avenue.  I _think_ git_all_attrs() needs to be told about the
> default attr-source trick (which I didn't touch in my illustration
> patch) before it happens, though.

Good point. I think to have a global flag would be a better user experience.

>
> If you used the mechanism in the illustration patch I gave you, and
> adjusted the test part of your patch to match (i.e. "diff" does not
> learn "--attr-source" option, but "git --attr-source=... diff" is
> how you make it read attributes from a tree-ish), would the result
> work well, or do we need more work to make it usable?  How well do
> other commands (e.g. "git show") work in the same test repository
> you created in your version?

I think it works pretty smoothly. I adjusted my tests to work with this git
flag, and I also tested git show and it works well.

In fact, your proposal would serve the needs of my patch independently of any of
the diff code, which is nice.

>
> Thanks.

Thanks,
John
Junio C Hamano March 16, 2023, 10:56 p.m. UTC | #6
John Cai <johncai86@gmail.com> writes:

> Ah I see--in that case, what would be a good object to put this state into? Mabe
> repo_settings?

I personally think a global is good enough for a case like this.  It
is not like the code in the attribute subsystem is structured to
work in multiple repositories at the same time in the first place, so
in repo_settings, the_repository, or elsewhere is just as good as
having it in a plain old file-scope static, and even after moving it
into the_repository or something, the access will initially be done
by referring to the_repository global (which may not even exist by
the time "git --attr-source=<tree>" gets parsed), and not by plumbing
a "struct repository *" parameter through the callchain.

Another thing to note is that there needs a mechanism to convey the
tree object used to read the attributes from down to subprocesses,
e.g. exporting the GIT_ATTR_SOURCE environment variable and make Git
behave as if it saw the "git --attr-source=<tree>" option when such
an environment variable exists, or something.  Otherwise, the custom
attribute source would only take effect inside built-in commands.

In any case, here is what I have now, stealing some tests from your
patch.  I do not think I'll be working on it further at least for
now, so feel free to run with it.  I am not adding the "in a bare
repository, always read from HEAD unless told otherwise", as I do
not see a good way to countermand it from a command line.

----- >8 ----- cut here ----- >8 ----- cut here ----- >8 -----
Subject: [PATCH] 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 internally.

The tests were stolen and adjusted from John Cai's effort where only
"git diff" learned the "--attr-source" option to read from a
tree-ish.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 archive.c                 |  2 +-
 attr.c                    | 34 ++++++++++++++++++++++++++++++++--
 attr.h                    | 13 +++++++++----
 builtin/check-attr.c      | 17 ++++++++---------
 builtin/pack-objects.c    |  2 +-
 convert.c                 |  2 +-
 git.c                     |  3 +++
 ll-merge.c                |  4 ++--
 pathspec.c                |  2 +-
 t/lib-diff-alternative.sh | 31 ++++++++++++++++++++++++++-----
 t/t0003-attributes.sh     |  7 ++++++-
 t/t4018-diff-funcname.sh  | 19 +++++++++++++++++++
 userdiff.c                |  2 +-
 ws.c                      |  2 +-
 14 files changed, 111 insertions(+), 29 deletions(-)

diff --git c/archive.c w/archive.c
index 9aeaf2bd87..385aa5f248 100644
--- c/archive.c
+++ w/archive.c
@@ -120,7 +120,7 @@ static const struct attr_check *get_archive_attrs(struct index_state *istate,
 	static struct attr_check *check;
 	if (!check)
 		check = attr_check_initl("export-ignore", "export-subst", NULL);
-	git_check_attr(istate, NULL, path, check);
+	git_check_attr(istate, path, check);
 	return check;
 }
 
diff --git c/attr.c w/attr.c
index 1053dfcd4b..d34c7e9d54 100644
--- c/attr.c
+++ w/attr.c
@@ -1165,11 +1165,40 @@ static void collect_some_attrs(struct index_state *istate,
 	fill(path, pathlen, basename_offset, check->stack, check->all_attrs, rem);
 }
 
+static const char *default_attr_source_tree_object_name;
+
+void set_git_attr_source(const char *tree_object_name)
+{
+	default_attr_source_tree_object_name = xstrdup(tree_object_name);
+}
+
+
+static void compute_default_attr_source(struct object_id *attr_source)
+{
+	if (!default_attr_source_tree_object_name || !is_null_oid(attr_source))
+		return;
+
+	if (get_oid_treeish(default_attr_source_tree_object_name, attr_source))
+		die(_("bad --attr-source object"));
+}
+
+static struct object_id *default_attr_source(void)
+{
+	static struct object_id attr_source;
+
+	if (is_null_oid(&attr_source))
+		compute_default_attr_source(&attr_source);
+	if (is_null_oid(&attr_source))
+		return NULL;
+	return &attr_source;
+}
+
 void git_check_attr(struct index_state *istate,
-		    const struct object_id *tree_oid, const char *path,
+		    const char *path,
 		    struct attr_check *check)
 {
 	int i;
+	const struct object_id *tree_oid = default_attr_source();
 
 	collect_some_attrs(istate, tree_oid, path, check);
 
@@ -1182,10 +1211,11 @@ void git_check_attr(struct index_state *istate,
 	}
 }
 
-void git_all_attrs(struct index_state *istate, const struct object_id *tree_oid,
+void git_all_attrs(struct index_state *istate,
 		   const char *path, struct attr_check *check)
 {
 	int i;
+	const struct object_id *tree_oid = default_attr_source();
 
 	attr_check_reset(check);
 	collect_some_attrs(istate, tree_oid, path, check);
diff --git c/attr.h w/attr.h
index 9884ea2bc6..676bd17ce2 100644
--- c/attr.h
+++ w/attr.h
@@ -45,7 +45,7 @@
  * const char *path;
  *
  * setup_check();
- * git_check_attr(&the_index, tree_oid, path, check);
+ * git_check_attr(&the_index, path, check);
  * ------------
  *
  * - Act on `.value` member of the result, left in `check->items[]`:
@@ -120,7 +120,6 @@
 #define ATTR_MAX_FILE_SIZE (100 * 1024 * 1024)
 
 struct index_state;
-struct object_id;
 
 /**
  * An attribute is an opaque object that is identified by its name. Pass the
@@ -135,6 +134,12 @@ struct git_attr;
 struct all_attrs_item;
 struct attr_stack;
 
+/*
+ * The textual object name for the tree-ish used by git_check_attr()
+ * to read attributes from (instead of from the working tree).
+ */
+void set_git_attr_source(const char *);
+
 /*
  * Given a string, return the gitattribute object that
  * corresponds to it.
@@ -203,14 +208,14 @@ void attr_check_free(struct attr_check *check);
 const char *git_attr_name(const struct git_attr *);
 
 void git_check_attr(struct index_state *istate,
-		    const struct object_id *tree_oid, const char *path,
+		    const char *path,
 		    struct attr_check *check);
 
 /*
  * Retrieve all attributes that apply to the specified path.
  * check holds the attributes and their values.
  */
-void git_all_attrs(struct index_state *istate, const struct object_id *tree_oid,
+void git_all_attrs(struct index_state *istate,
 		   const char *path, struct attr_check *check);
 
 enum git_attr_direction {
diff --git c/builtin/check-attr.c w/builtin/check-attr.c
index d7a40e674c..1a7929c980 100644
--- c/builtin/check-attr.c
+++ w/builtin/check-attr.c
@@ -58,7 +58,7 @@ static void output_attr(struct attr_check *check, const char *file)
 }
 
 static void check_attr(const char *prefix, struct attr_check *check,
-		       const struct object_id *tree_oid, int collect_all,
+		       int collect_all,
 		       const char *file)
 
 {
@@ -66,9 +66,9 @@ static void check_attr(const char *prefix, struct attr_check *check,
 		prefix_path(prefix, prefix ? strlen(prefix) : 0, file);
 
 	if (collect_all) {
-		git_all_attrs(&the_index, tree_oid, full_path, check);
+		git_all_attrs(&the_index, full_path, check);
 	} else {
-		git_check_attr(&the_index, tree_oid, full_path, check);
+		git_check_attr(&the_index, full_path, check);
 	}
 	output_attr(check, file);
 
@@ -76,7 +76,7 @@ static void check_attr(const char *prefix, struct attr_check *check,
 }
 
 static void check_attr_stdin_paths(const char *prefix, struct attr_check *check,
-				   const struct object_id *tree_oid, int collect_all)
+				   int collect_all)
 {
 	struct strbuf buf = STRBUF_INIT;
 	struct strbuf unquoted = STRBUF_INIT;
@@ -90,7 +90,7 @@ static void check_attr_stdin_paths(const char *prefix, struct attr_check *check,
 				die("line is badly quoted");
 			strbuf_swap(&buf, &unquoted);
 		}
-		check_attr(prefix, check, tree_oid, collect_all, buf.buf);
+		check_attr(prefix, check, collect_all, buf.buf);
 		maybe_flush_or_die(stdout, "attribute to stdout");
 	}
 	strbuf_release(&buf);
@@ -106,7 +106,6 @@ static NORETURN void error_with_usage(const char *msg)
 int cmd_check_attr(int argc, const char **argv, const char *prefix)
 {
 	struct attr_check *check;
-	struct object_id *tree_oid = NULL;
 	struct object_id initialized_oid;
 	int cnt, i, doubledash, filei;
 
@@ -182,14 +181,14 @@ int cmd_check_attr(int argc, const char **argv, const char *prefix)
 	if (source) {
 		if (repo_get_oid_tree(the_repository, source, &initialized_oid))
 			die("%s: not a valid tree-ish source", source);
-		tree_oid = &initialized_oid;
+		set_git_attr_source(source);
 	}
 
 	if (stdin_paths)
-		check_attr_stdin_paths(prefix, check, tree_oid, all_attrs);
+		check_attr_stdin_paths(prefix, check, all_attrs);
 	else {
 		for (i = filei; i < argc; i++)
-			check_attr(prefix, check, tree_oid, all_attrs, argv[i]);
+			check_attr(prefix, check, all_attrs, argv[i]);
 		maybe_flush_or_die(stdout, "attribute to stdout");
 	}
 
diff --git c/builtin/pack-objects.c w/builtin/pack-objects.c
index 74a167a180..d561541b8c 100644
--- c/builtin/pack-objects.c
+++ w/builtin/pack-objects.c
@@ -1320,7 +1320,7 @@ static int no_try_delta(const char *path)
 
 	if (!check)
 		check = attr_check_initl("delta", NULL);
-	git_check_attr(the_repository->index, NULL, path, check);
+	git_check_attr(the_repository->index, path, check);
 	if (ATTR_FALSE(check->items[0].value))
 		return 1;
 	return 0;
diff --git c/convert.c w/convert.c
index a54d1690c0..9b67649032 100644
--- c/convert.c
+++ w/convert.c
@@ -1308,7 +1308,7 @@ void convert_attrs(struct index_state *istate,
 		git_config(read_convert_config, NULL);
 	}
 
-	git_check_attr(istate, NULL, path, check);
+	git_check_attr(istate, path, check);
 	ccheck = check->items;
 	ca->crlf_action = git_path_check_crlf(ccheck + 4);
 	if (ca->crlf_action == CRLF_UNDEFINED)
diff --git c/git.c w/git.c
index 6171fd6769..21bddc5718 100644
--- c/git.c
+++ w/git.c
@@ -4,6 +4,7 @@
 #include "help.h"
 #include "run-command.h"
 #include "alias.h"
+#include "attr.h"
 #include "shallow.h"
 
 #define RUN_SETUP		(1<<0)
@@ -307,6 +308,8 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
 			} else {
 				exit(list_cmds(cmd));
 			}
+		} else if (skip_prefix(cmd, "--attr-source=", &cmd)) {
+			set_git_attr_source(cmd);
 		} else {
 			fprintf(stderr, _("unknown option: %s\n"), cmd);
 			usage(git_usage_string);
diff --git c/ll-merge.c w/ll-merge.c
index 130d26501c..22a603e8af 100644
--- c/ll-merge.c
+++ w/ll-merge.c
@@ -391,7 +391,7 @@ enum ll_merge_result ll_merge(mmbuffer_t *result_buf,
 		normalize_file(theirs, path, istate);
 	}
 
-	git_check_attr(istate, NULL, path, check);
+	git_check_attr(istate, path, check);
 	ll_driver_name = check->items[0].value;
 	if (check->items[1].value) {
 		marker_size = atoi(check->items[1].value);
@@ -419,7 +419,7 @@ int ll_merge_marker_size(struct index_state *istate, const char *path)
 
 	if (!check)
 		check = attr_check_initl("conflict-marker-size", NULL);
-	git_check_attr(istate, NULL, path, check);
+	git_check_attr(istate, path, check);
 	if (check->items[0].value) {
 		marker_size = atoi(check->items[0].value);
 		if (marker_size <= 0)
diff --git c/pathspec.c w/pathspec.c
index ab70fcbe61..74e02c75fc 100644
--- c/pathspec.c
+++ w/pathspec.c
@@ -730,7 +730,7 @@ int match_pathspec_attrs(struct index_state *istate,
 	if (name[namelen])
 		name = to_free = xmemdupz(name, namelen);
 
-	git_check_attr(istate, NULL, name, item->attr_check);
+	git_check_attr(istate, name, item->attr_check);
 
 	free(to_free);
 
diff --git c/t/lib-diff-alternative.sh w/t/lib-diff-alternative.sh
index a8f5d3274a..02381eb7f1 100644
--- c/t/lib-diff-alternative.sh
+++ w/t/lib-diff-alternative.sh
@@ -112,15 +112,36 @@ EOF
 
 	STRATEGY=$1
 
-	test_expect_success "$STRATEGY diff from attributes" '
+	test_expect_success "setup attributes files for tests with $STRATEGY" '
+		git checkout -b master &&
 		echo "file* diff=driver" >.gitattributes &&
-		git config diff.driver.algorithm "$STRATEGY" &&
-		test_must_fail git diff --no-index file1 file2 > output &&
-		cat expect &&
-		cat output &&
+		git add file1 file2 .gitattributes &&
+		git commit -m "adding files" &&
+		git checkout -b branchA &&
+		echo "file* diff=driverA" >.gitattributes &&
+		git add .gitattributes &&
+		git commit -m "adding driverA as diff driver" &&
+		git checkout master &&
+		git clone --bare --no-local . bare.git
+	'
+
+	test_expect_success "$STRATEGY diff from attributes" '
+		test_must_fail git -c diff.driver.algorithm=$STRATEGY diff --no-index file1 file2 > output &&
 		test_cmp expect output
 	'
 
+	test_expect_success "diff from attributes with bare repo when branch different than HEAD" '
+		git -C bare.git --attr-source=branchA -c diff.driver.algorithm=myers \
+			-c diff.driverA.algorithm=$STRATEGY \
+			diff HEAD:file1 HEAD:file2 >output &&
+		test_cmp expect output
+	'
+
+	test_expect_success "diff from attributes with bare repo with invalid source" '
+		test_must_fail git -C bare.git --attr-source=invalid-branch diff \
+			HEAD:file1 HEAD:file2
+	'
+
 	test_expect_success "$STRATEGY diff from attributes has valid diffstat" '
 		echo "file* diff=driver" >.gitattributes &&
 		git config diff.driver.algorithm "$STRATEGY" &&
diff --git c/t/t0003-attributes.sh w/t/t0003-attributes.sh
index 89b306cb11..73db37a7f3 100755
--- c/t/t0003-attributes.sh
+++ w/t/t0003-attributes.sh
@@ -30,8 +30,13 @@ attr_check_quote () {
 attr_check_source () {
 	path="$1" expect="$2" source="$3" git_opts="$4" &&
 
-	git $git_opts check-attr --source $source test -- "$path" >actual 2>err &&
 	echo "$path: test: $expect" >expect &&
+
+	git $git_opts check-attr --source $source test -- "$path" >actual 2>err &&
+	test_cmp expect actual &&
+	test_must_be_empty err &&
+
+	git $git_opts --attr-source="$source" check-attr test -- "$path" >actual 2>err &&
 	test_cmp expect actual &&
 	test_must_be_empty err
 }
diff --git c/t/t4018-diff-funcname.sh w/t/t4018-diff-funcname.sh
index 42a2b9a13b..c8d555771d 100755
--- c/t/t4018-diff-funcname.sh
+++ w/t/t4018-diff-funcname.sh
@@ -63,6 +63,25 @@ do
 		test_i18ngrep ! fatal msg &&
 		test_i18ngrep ! error msg
 	'
+
+	test_expect_success "builtin $p pattern compiles on bare repo with --attr-source" '
+		test_when_finished "rm -rf bare.git" &&
+		git checkout -B master &&
+		git add . &&
+		echo "*.java diff=notexist" >.gitattributes &&
+		git add .gitattributes &&
+		git commit -am "changing gitattributes" &&
+		git checkout -B branchA &&
+		echo "*.java diff=$p" >.gitattributes &&
+		git add .gitattributes &&
+		git commit -am "changing gitattributes" &&
+		git clone --bare --no-local . bare.git &&
+		git -C bare.git symbolic-ref HEAD refs/heads/master &&
+		test_expect_code 1 git -C bare.git --attr-source=branchA \
+			diff --exit-code HEAD:A.java HEAD:B.java 2>msg &&
+		test_i18ngrep ! fatal msg &&
+		test_i18ngrep ! error msg
+	'
 done
 
 test_expect_success 'last regexp must not be negated' '
diff --git c/userdiff.c w/userdiff.c
index 58a3d59ef8..156660eaca 100644
--- c/userdiff.c
+++ w/userdiff.c
@@ -415,7 +415,7 @@ struct userdiff_driver *userdiff_find_by_path(struct index_state *istate,
 		check = attr_check_initl("diff", NULL);
 	if (!path)
 		return NULL;
-	git_check_attr(istate, NULL, path, check);
+	git_check_attr(istate, path, check);
 
 	if (ATTR_TRUE(check->items[0].value))
 		return &driver_true;
diff --git c/ws.c w/ws.c
index da3d0e28cb..903bfcd53e 100644
--- c/ws.c
+++ w/ws.c
@@ -79,7 +79,7 @@ unsigned whitespace_rule(struct index_state *istate, const char *pathname)
 	if (!attr_whitespace_rule)
 		attr_whitespace_rule = attr_check_initl("whitespace", NULL);
 
-	git_check_attr(istate, NULL, pathname, attr_whitespace_rule);
+	git_check_attr(istate, pathname, attr_whitespace_rule);
 	value = attr_whitespace_rule->items[0].value;
 	if (ATTR_TRUE(value)) {
 		/* true (whitespace) */
John Cai March 17, 2023, 2:11 p.m. UTC | #7
Hi Junio,

On 16 Mar 2023, at 18:56, Junio C Hamano wrote:

> John Cai <johncai86@gmail.com> writes:
>
>> Ah I see--in that case, what would be a good object to put this state into? Mabe
>> repo_settings?
>
> I personally think a global is good enough for a case like this.  It
> is not like the code in the attribute subsystem is structured to
> work in multiple repositories at the same time in the first place, so
> in repo_settings, the_repository, or elsewhere is just as good as
> having it in a plain old file-scope static, and even after moving it
> into the_repository or something, the access will initially be done
> by referring to the_repository global (which may not even exist by
> the time "git --attr-source=<tree>" gets parsed), and not by plumbing
> a "struct repository *" parameter through the callchain.

Makes sense.

>
> Another thing to note is that there needs a mechanism to convey the
> tree object used to read the attributes from down to subprocesses,
> e.g. exporting the GIT_ATTR_SOURCE environment variable and make Git
> behave as if it saw the "git --attr-source=<tree>" option when such
> an environment variable exists, or something.  Otherwise, the custom
> attribute source would only take effect inside built-in commands.
>
> In any case, here is what I have now, stealing some tests from your
> patch.  I do not think I'll be working on it further at least for
> now, so feel free to run with it.  I am not adding the "in a bare
> repository, always read from HEAD unless told otherwise", as I do
> not see a good way to countermand it from a command line.

Thanks for the patch! Looks like you took it most of the way. I'll take a look
and make some additions if necessary.

thanks
John

>
> ----- >8 ----- cut here ----- >8 ----- cut here ----- >8 -----
> Subject: [PATCH] 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 internally.
>
> The tests were stolen and adjusted from John Cai's effort where only
> "git diff" learned the "--attr-source" option to read from a
> tree-ish.
>
> Signed-off-by: Junio C Hamano <gitster@pobox.com>
> ---
>  archive.c                 |  2 +-
>  attr.c                    | 34 ++++++++++++++++++++++++++++++++--
>  attr.h                    | 13 +++++++++----
>  builtin/check-attr.c      | 17 ++++++++---------
>  builtin/pack-objects.c    |  2 +-
>  convert.c                 |  2 +-
>  git.c                     |  3 +++
>  ll-merge.c                |  4 ++--
>  pathspec.c                |  2 +-
>  t/lib-diff-alternative.sh | 31 ++++++++++++++++++++++++++-----
>  t/t0003-attributes.sh     |  7 ++++++-
>  t/t4018-diff-funcname.sh  | 19 +++++++++++++++++++
>  userdiff.c                |  2 +-
>  ws.c                      |  2 +-
>  14 files changed, 111 insertions(+), 29 deletions(-)
>
> diff --git c/archive.c w/archive.c
> index 9aeaf2bd87..385aa5f248 100644
> --- c/archive.c
> +++ w/archive.c
> @@ -120,7 +120,7 @@ static const struct attr_check *get_archive_attrs(struct index_state *istate,
>  	static struct attr_check *check;
>  	if (!check)
>  		check = attr_check_initl("export-ignore", "export-subst", NULL);
> -	git_check_attr(istate, NULL, path, check);
> +	git_check_attr(istate, path, check);
>  	return check;
>  }
>
> diff --git c/attr.c w/attr.c
> index 1053dfcd4b..d34c7e9d54 100644
> --- c/attr.c
> +++ w/attr.c
> @@ -1165,11 +1165,40 @@ static void collect_some_attrs(struct index_state *istate,
>  	fill(path, pathlen, basename_offset, check->stack, check->all_attrs, rem);
>  }
>
> +static const char *default_attr_source_tree_object_name;
> +
> +void set_git_attr_source(const char *tree_object_name)
> +{
> +	default_attr_source_tree_object_name = xstrdup(tree_object_name);
> +}
> +
> +
> +static void compute_default_attr_source(struct object_id *attr_source)
> +{
> +	if (!default_attr_source_tree_object_name || !is_null_oid(attr_source))
> +		return;
> +
> +	if (get_oid_treeish(default_attr_source_tree_object_name, attr_source))
> +		die(_("bad --attr-source object"));
> +}
> +
> +static struct object_id *default_attr_source(void)
> +{
> +	static struct object_id attr_source;
> +
> +	if (is_null_oid(&attr_source))
> +		compute_default_attr_source(&attr_source);
> +	if (is_null_oid(&attr_source))
> +		return NULL;
> +	return &attr_source;
> +}
> +
>  void git_check_attr(struct index_state *istate,
> -		    const struct object_id *tree_oid, const char *path,
> +		    const char *path,
>  		    struct attr_check *check)
>  {
>  	int i;
> +	const struct object_id *tree_oid = default_attr_source();
>
>  	collect_some_attrs(istate, tree_oid, path, check);
>
> @@ -1182,10 +1211,11 @@ void git_check_attr(struct index_state *istate,
>  	}
>  }
>
> -void git_all_attrs(struct index_state *istate, const struct object_id *tree_oid,
> +void git_all_attrs(struct index_state *istate,
>  		   const char *path, struct attr_check *check)
>  {
>  	int i;
> +	const struct object_id *tree_oid = default_attr_source();
>
>  	attr_check_reset(check);
>  	collect_some_attrs(istate, tree_oid, path, check);
> diff --git c/attr.h w/attr.h
> index 9884ea2bc6..676bd17ce2 100644
> --- c/attr.h
> +++ w/attr.h
> @@ -45,7 +45,7 @@
>   * const char *path;
>   *
>   * setup_check();
> - * git_check_attr(&the_index, tree_oid, path, check);
> + * git_check_attr(&the_index, path, check);
>   * ------------
>   *
>   * - Act on `.value` member of the result, left in `check->items[]`:
> @@ -120,7 +120,6 @@
>  #define ATTR_MAX_FILE_SIZE (100 * 1024 * 1024)
>
>  struct index_state;
> -struct object_id;
>
>  /**
>   * An attribute is an opaque object that is identified by its name. Pass the
> @@ -135,6 +134,12 @@ struct git_attr;
>  struct all_attrs_item;
>  struct attr_stack;
>
> +/*
> + * The textual object name for the tree-ish used by git_check_attr()
> + * to read attributes from (instead of from the working tree).
> + */
> +void set_git_attr_source(const char *);
> +
>  /*
>   * Given a string, return the gitattribute object that
>   * corresponds to it.
> @@ -203,14 +208,14 @@ void attr_check_free(struct attr_check *check);
>  const char *git_attr_name(const struct git_attr *);
>
>  void git_check_attr(struct index_state *istate,
> -		    const struct object_id *tree_oid, const char *path,
> +		    const char *path,
>  		    struct attr_check *check);
>
>  /*
>   * Retrieve all attributes that apply to the specified path.
>   * check holds the attributes and their values.
>   */
> -void git_all_attrs(struct index_state *istate, const struct object_id *tree_oid,
> +void git_all_attrs(struct index_state *istate,
>  		   const char *path, struct attr_check *check);
>
>  enum git_attr_direction {
> diff --git c/builtin/check-attr.c w/builtin/check-attr.c
> index d7a40e674c..1a7929c980 100644
> --- c/builtin/check-attr.c
> +++ w/builtin/check-attr.c
> @@ -58,7 +58,7 @@ static void output_attr(struct attr_check *check, const char *file)
>  }
>
>  static void check_attr(const char *prefix, struct attr_check *check,
> -		       const struct object_id *tree_oid, int collect_all,
> +		       int collect_all,
>  		       const char *file)
>
>  {
> @@ -66,9 +66,9 @@ static void check_attr(const char *prefix, struct attr_check *check,
>  		prefix_path(prefix, prefix ? strlen(prefix) : 0, file);
>
>  	if (collect_all) {
> -		git_all_attrs(&the_index, tree_oid, full_path, check);
> +		git_all_attrs(&the_index, full_path, check);
>  	} else {
> -		git_check_attr(&the_index, tree_oid, full_path, check);
> +		git_check_attr(&the_index, full_path, check);
>  	}
>  	output_attr(check, file);
>
> @@ -76,7 +76,7 @@ static void check_attr(const char *prefix, struct attr_check *check,
>  }
>
>  static void check_attr_stdin_paths(const char *prefix, struct attr_check *check,
> -				   const struct object_id *tree_oid, int collect_all)
> +				   int collect_all)
>  {
>  	struct strbuf buf = STRBUF_INIT;
>  	struct strbuf unquoted = STRBUF_INIT;
> @@ -90,7 +90,7 @@ static void check_attr_stdin_paths(const char *prefix, struct attr_check *check,
>  				die("line is badly quoted");
>  			strbuf_swap(&buf, &unquoted);
>  		}
> -		check_attr(prefix, check, tree_oid, collect_all, buf.buf);
> +		check_attr(prefix, check, collect_all, buf.buf);
>  		maybe_flush_or_die(stdout, "attribute to stdout");
>  	}
>  	strbuf_release(&buf);
> @@ -106,7 +106,6 @@ static NORETURN void error_with_usage(const char *msg)
>  int cmd_check_attr(int argc, const char **argv, const char *prefix)
>  {
>  	struct attr_check *check;
> -	struct object_id *tree_oid = NULL;
>  	struct object_id initialized_oid;
>  	int cnt, i, doubledash, filei;
>
> @@ -182,14 +181,14 @@ int cmd_check_attr(int argc, const char **argv, const char *prefix)
>  	if (source) {
>  		if (repo_get_oid_tree(the_repository, source, &initialized_oid))
>  			die("%s: not a valid tree-ish source", source);
> -		tree_oid = &initialized_oid;
> +		set_git_attr_source(source);
>  	}
>
>  	if (stdin_paths)
> -		check_attr_stdin_paths(prefix, check, tree_oid, all_attrs);
> +		check_attr_stdin_paths(prefix, check, all_attrs);
>  	else {
>  		for (i = filei; i < argc; i++)
> -			check_attr(prefix, check, tree_oid, all_attrs, argv[i]);
> +			check_attr(prefix, check, all_attrs, argv[i]);
>  		maybe_flush_or_die(stdout, "attribute to stdout");
>  	}
>
> diff --git c/builtin/pack-objects.c w/builtin/pack-objects.c
> index 74a167a180..d561541b8c 100644
> --- c/builtin/pack-objects.c
> +++ w/builtin/pack-objects.c
> @@ -1320,7 +1320,7 @@ static int no_try_delta(const char *path)
>
>  	if (!check)
>  		check = attr_check_initl("delta", NULL);
> -	git_check_attr(the_repository->index, NULL, path, check);
> +	git_check_attr(the_repository->index, path, check);
>  	if (ATTR_FALSE(check->items[0].value))
>  		return 1;
>  	return 0;
> diff --git c/convert.c w/convert.c
> index a54d1690c0..9b67649032 100644
> --- c/convert.c
> +++ w/convert.c
> @@ -1308,7 +1308,7 @@ void convert_attrs(struct index_state *istate,
>  		git_config(read_convert_config, NULL);
>  	}
>
> -	git_check_attr(istate, NULL, path, check);
> +	git_check_attr(istate, path, check);
>  	ccheck = check->items;
>  	ca->crlf_action = git_path_check_crlf(ccheck + 4);
>  	if (ca->crlf_action == CRLF_UNDEFINED)
> diff --git c/git.c w/git.c
> index 6171fd6769..21bddc5718 100644
> --- c/git.c
> +++ w/git.c
> @@ -4,6 +4,7 @@
>  #include "help.h"
>  #include "run-command.h"
>  #include "alias.h"
> +#include "attr.h"
>  #include "shallow.h"
>
>  #define RUN_SETUP		(1<<0)
> @@ -307,6 +308,8 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
>  			} else {
>  				exit(list_cmds(cmd));
>  			}
> +		} else if (skip_prefix(cmd, "--attr-source=", &cmd)) {
> +			set_git_attr_source(cmd);
>  		} else {
>  			fprintf(stderr, _("unknown option: %s\n"), cmd);
>  			usage(git_usage_string);
> diff --git c/ll-merge.c w/ll-merge.c
> index 130d26501c..22a603e8af 100644
> --- c/ll-merge.c
> +++ w/ll-merge.c
> @@ -391,7 +391,7 @@ enum ll_merge_result ll_merge(mmbuffer_t *result_buf,
>  		normalize_file(theirs, path, istate);
>  	}
>
> -	git_check_attr(istate, NULL, path, check);
> +	git_check_attr(istate, path, check);
>  	ll_driver_name = check->items[0].value;
>  	if (check->items[1].value) {
>  		marker_size = atoi(check->items[1].value);
> @@ -419,7 +419,7 @@ int ll_merge_marker_size(struct index_state *istate, const char *path)
>
>  	if (!check)
>  		check = attr_check_initl("conflict-marker-size", NULL);
> -	git_check_attr(istate, NULL, path, check);
> +	git_check_attr(istate, path, check);
>  	if (check->items[0].value) {
>  		marker_size = atoi(check->items[0].value);
>  		if (marker_size <= 0)
> diff --git c/pathspec.c w/pathspec.c
> index ab70fcbe61..74e02c75fc 100644
> --- c/pathspec.c
> +++ w/pathspec.c
> @@ -730,7 +730,7 @@ int match_pathspec_attrs(struct index_state *istate,
>  	if (name[namelen])
>  		name = to_free = xmemdupz(name, namelen);
>
> -	git_check_attr(istate, NULL, name, item->attr_check);
> +	git_check_attr(istate, name, item->attr_check);
>
>  	free(to_free);
>
> diff --git c/t/lib-diff-alternative.sh w/t/lib-diff-alternative.sh
> index a8f5d3274a..02381eb7f1 100644
> --- c/t/lib-diff-alternative.sh
> +++ w/t/lib-diff-alternative.sh
> @@ -112,15 +112,36 @@ EOF
>
>  	STRATEGY=$1
>
> -	test_expect_success "$STRATEGY diff from attributes" '
> +	test_expect_success "setup attributes files for tests with $STRATEGY" '
> +		git checkout -b master &&
>  		echo "file* diff=driver" >.gitattributes &&
> -		git config diff.driver.algorithm "$STRATEGY" &&
> -		test_must_fail git diff --no-index file1 file2 > output &&
> -		cat expect &&
> -		cat output &&
> +		git add file1 file2 .gitattributes &&
> +		git commit -m "adding files" &&
> +		git checkout -b branchA &&
> +		echo "file* diff=driverA" >.gitattributes &&
> +		git add .gitattributes &&
> +		git commit -m "adding driverA as diff driver" &&
> +		git checkout master &&
> +		git clone --bare --no-local . bare.git
> +	'
> +
> +	test_expect_success "$STRATEGY diff from attributes" '
> +		test_must_fail git -c diff.driver.algorithm=$STRATEGY diff --no-index file1 file2 > output &&
>  		test_cmp expect output
>  	'
>
> +	test_expect_success "diff from attributes with bare repo when branch different than HEAD" '
> +		git -C bare.git --attr-source=branchA -c diff.driver.algorithm=myers \
> +			-c diff.driverA.algorithm=$STRATEGY \
> +			diff HEAD:file1 HEAD:file2 >output &&
> +		test_cmp expect output
> +	'
> +
> +	test_expect_success "diff from attributes with bare repo with invalid source" '
> +		test_must_fail git -C bare.git --attr-source=invalid-branch diff \
> +			HEAD:file1 HEAD:file2
> +	'
> +
>  	test_expect_success "$STRATEGY diff from attributes has valid diffstat" '
>  		echo "file* diff=driver" >.gitattributes &&
>  		git config diff.driver.algorithm "$STRATEGY" &&
> diff --git c/t/t0003-attributes.sh w/t/t0003-attributes.sh
> index 89b306cb11..73db37a7f3 100755
> --- c/t/t0003-attributes.sh
> +++ w/t/t0003-attributes.sh
> @@ -30,8 +30,13 @@ attr_check_quote () {
>  attr_check_source () {
>  	path="$1" expect="$2" source="$3" git_opts="$4" &&
>
> -	git $git_opts check-attr --source $source test -- "$path" >actual 2>err &&
>  	echo "$path: test: $expect" >expect &&
> +
> +	git $git_opts check-attr --source $source test -- "$path" >actual 2>err &&
> +	test_cmp expect actual &&
> +	test_must_be_empty err &&
> +
> +	git $git_opts --attr-source="$source" check-attr test -- "$path" >actual 2>err &&
>  	test_cmp expect actual &&
>  	test_must_be_empty err
>  }
> diff --git c/t/t4018-diff-funcname.sh w/t/t4018-diff-funcname.sh
> index 42a2b9a13b..c8d555771d 100755
> --- c/t/t4018-diff-funcname.sh
> +++ w/t/t4018-diff-funcname.sh
> @@ -63,6 +63,25 @@ do
>  		test_i18ngrep ! fatal msg &&
>  		test_i18ngrep ! error msg
>  	'
> +
> +	test_expect_success "builtin $p pattern compiles on bare repo with --attr-source" '
> +		test_when_finished "rm -rf bare.git" &&
> +		git checkout -B master &&
> +		git add . &&
> +		echo "*.java diff=notexist" >.gitattributes &&
> +		git add .gitattributes &&
> +		git commit -am "changing gitattributes" &&
> +		git checkout -B branchA &&
> +		echo "*.java diff=$p" >.gitattributes &&
> +		git add .gitattributes &&
> +		git commit -am "changing gitattributes" &&
> +		git clone --bare --no-local . bare.git &&
> +		git -C bare.git symbolic-ref HEAD refs/heads/master &&
> +		test_expect_code 1 git -C bare.git --attr-source=branchA \
> +			diff --exit-code HEAD:A.java HEAD:B.java 2>msg &&
> +		test_i18ngrep ! fatal msg &&
> +		test_i18ngrep ! error msg
> +	'
>  done
>
>  test_expect_success 'last regexp must not be negated' '
> diff --git c/userdiff.c w/userdiff.c
> index 58a3d59ef8..156660eaca 100644
> --- c/userdiff.c
> +++ w/userdiff.c
> @@ -415,7 +415,7 @@ struct userdiff_driver *userdiff_find_by_path(struct index_state *istate,
>  		check = attr_check_initl("diff", NULL);
>  	if (!path)
>  		return NULL;
> -	git_check_attr(istate, NULL, path, check);
> +	git_check_attr(istate, path, check);
>
>  	if (ATTR_TRUE(check->items[0].value))
>  		return &driver_true;
> diff --git c/ws.c w/ws.c
> index da3d0e28cb..903bfcd53e 100644
> --- c/ws.c
> +++ w/ws.c
> @@ -79,7 +79,7 @@ unsigned whitespace_rule(struct index_state *istate, const char *pathname)
>  	if (!attr_whitespace_rule)
>  		attr_whitespace_rule = attr_check_initl("whitespace", NULL);
>
> -	git_check_attr(istate, NULL, pathname, attr_whitespace_rule);
> +	git_check_attr(istate, pathname, attr_whitespace_rule);
>  	value = attr_whitespace_rule->items[0].value;
>  	if (ATTR_TRUE(value)) {
>  		/* true (whitespace) */
diff mbox series

Patch

diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt
index 39bfbca1ffe..15488bd92b2 100644
--- a/Documentation/gitattributes.txt
+++ b/Documentation/gitattributes.txt
@@ -758,6 +758,8 @@  with the above configuration, i.e. `j-c-diff`, with 7
 parameters, just like `GIT_EXTERNAL_DIFF` program is called.
 See linkgit:git[1] for details.
 
+When using a bare repository, the gitattributes from HEAD will be used.
+
 Setting the internal diff algorithm
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -785,6 +787,8 @@  This diff algorithm applies to user facing diff output like git-diff(1),
 git-show(1) and is used for the `--stat` output as well. The merge machinery
 will not use the diff algorithm set through this method.
 
+When using a bare repository, the gitattributes from HEAD will be used.
+
 NOTE: If `diff.<name>.command` is defined for path with the
 `diff=<name>` attribute, it is executed as an external diff driver
 (see above), and adding `diff.<name>.algorithm` has no effect, as the
diff --git a/diff.c b/diff.c
index 469e18aed20..51baf893bb0 100644
--- a/diff.c
+++ b/diff.c
@@ -29,6 +29,7 @@ 
 #include "promisor-remote.h"
 #include "dir.h"
 #include "strmap.h"
+#include "tree.h"
 
 #ifdef NO_FAST_WORKING_DIRECTORY
 #define FAST_WORKING_DIRECTORY 0
@@ -4443,6 +4444,27 @@  static void fill_metainfo(struct strbuf *msg,
 	}
 }
 
+static void get_userdiff(struct diff_options *o,
+			     struct userdiff_driver **drv,
+			     const char *attr_path)
+{
+	const char *commit = "HEAD";
+	struct object_id *tree_oid = NULL;
+
+	if (is_bare_repository() && o->repo->gitdir) {
+		struct object_id oid;
+
+		if (!get_oid(commit, &oid)) {
+			struct tree *t = parse_tree_indirect(&oid);
+
+			if (t)
+				tree_oid = &t->object.oid;
+		}
+	}
+
+	*drv = userdiff_find_by_tree_and_path(o->repo->index, tree_oid, attr_path);
+}
+
 static void run_diff_cmd(const char *pgm,
 			 const char *name,
 			 const char *other,
@@ -4458,8 +4480,10 @@  static void run_diff_cmd(const char *pgm,
 	int must_show_header = 0;
 	struct userdiff_driver *drv = NULL;
 
-	if (o->flags.allow_external || !o->ignore_driver_algorithm)
-		drv = userdiff_find_by_path(o->repo->index, attr_path);
+	if (o->flags.allow_external || !o->ignore_driver_algorithm) {
+
+		get_userdiff(o, &drv, attr_path);
+	}
 
 	if (o->flags.allow_external && drv && drv->external)
 		pgm = drv->external;
@@ -4586,8 +4610,9 @@  static void run_diffstat(struct diff_filepair *p, struct diff_options *o,
 	const char *other;
 
 	if (!o->ignore_driver_algorithm) {
-		struct userdiff_driver *drv = userdiff_find_by_path(o->repo->index,
-								    p->one->path);
+		struct userdiff_driver *drv = NULL;
+
+		get_userdiff(o, &drv, p->one->path);
 
 		if (drv && drv->algorithm)
 			set_diff_algorithm(o, drv->algorithm);
diff --git a/t/lib-diff-alternative.sh b/t/lib-diff-alternative.sh
index a8f5d3274a5..0d99af83dd2 100644
--- a/t/lib-diff-alternative.sh
+++ b/t/lib-diff-alternative.sh
@@ -121,6 +121,16 @@  EOF
 		test_cmp expect output
 	'
 
+	test_expect_success "$STRATEGY diff from attributes with bare repo" '
+		echo "file* diff=driver" >.gitattributes &&
+		git add file1 file2 .gitattributes &&
+		git commit -m "adding files" &&
+		git clone --bare --no-local . bare.git &&
+		git -C bare.git config diff.driver.algorithm "$STRATEGY" &&
+		git -C bare.git diff HEAD:file1 HEAD:file2 > output &&
+		test_cmp expect output
+	'
+
 	test_expect_success "$STRATEGY diff from attributes has valid diffstat" '
 		echo "file* diff=driver" >.gitattributes &&
 		git config diff.driver.algorithm "$STRATEGY" &&
diff --git a/t/t4018-diff-funcname.sh b/t/t4018-diff-funcname.sh
index 42a2b9a13b7..451af08c611 100755
--- a/t/t4018-diff-funcname.sh
+++ b/t/t4018-diff-funcname.sh
@@ -63,6 +63,17 @@  do
 		test_i18ngrep ! fatal msg &&
 		test_i18ngrep ! error msg
 	'
+	test_expect_success "builtin $p pattern compiles on bare repo" '
+		test_when_finished "rm -rf bare.git" &&
+		echo "*.java diff=$p" >.gitattributes &&
+		git add . &&
+		git commit -am "adding files" &&
+		git clone --bare --no-local . bare.git &&
+		test_expect_code 1 git -C bare.git diff --exit-code \
+			HEAD:A.java HEAD:B.java 2>msg &&
+		test_i18ngrep ! fatal msg &&
+		test_i18ngrep ! error msg
+	'
 done
 
 test_expect_success 'last regexp must not be negated' '
diff --git a/userdiff.c b/userdiff.c
index 58a3d59ef8f..2305d363244 100644
--- a/userdiff.c
+++ b/userdiff.c
@@ -408,6 +408,13 @@  struct userdiff_driver *userdiff_find_by_name(const char *name)
 
 struct userdiff_driver *userdiff_find_by_path(struct index_state *istate,
 					      const char *path)
+{
+	return userdiff_find_by_tree_and_path(istate, NULL, path);
+}
+
+struct userdiff_driver *userdiff_find_by_tree_and_path(struct index_state *istate,
+						       const struct object_id *tree_oid,
+						       const char *path)
 {
 	static struct attr_check *check;
 
@@ -415,7 +422,7 @@  struct userdiff_driver *userdiff_find_by_path(struct index_state *istate,
 		check = attr_check_initl("diff", NULL);
 	if (!path)
 		return NULL;
-	git_check_attr(istate, NULL, path, check);
+	git_check_attr(istate, tree_oid, path, check);
 
 	if (ATTR_TRUE(check->items[0].value))
 		return &driver_true;
diff --git a/userdiff.h b/userdiff.h
index 24419db6973..f9cd7c238de 100644
--- a/userdiff.h
+++ b/userdiff.h
@@ -33,6 +33,10 @@  int userdiff_config(const char *k, const char *v);
 struct userdiff_driver *userdiff_find_by_name(const char *name);
 struct userdiff_driver *userdiff_find_by_path(struct index_state *istate,
 					      const char *path);
+struct userdiff_driver *userdiff_find_by_tree_and_path(struct index_state *istate,
+						       const struct object_id *tree_oid,
+						       const char *path);
+
 
 /*
  * Initialize any textconv-related fields in the driver and return it, or NULL