From: Jeff King <hidden> Date: 2016-06-15 22:56:26
Prior to v1.8.1.1, if I did this:
git init
echo content >foo &&
mkdir subdir &&
echo content >subdir/bar &&
echo "subdir export-ignore" >.gitattributes
git add . &&
git commit -m one &&
git archive HEAD | tar tf -
my archive would contain only "foo" and ".gitattributes", not subdir. As
of v1.8.1.1, the attribute on subdir is ignored unless it is written
with a trailing slash, like:
subdir/ export-ignore
The issue bisects to 94bc671 (Add directory pattern matching to
attributes, 2012-12-08). That commit actually tests not only that
"subdir/" matches, but also that just "subdir" does not match.
The commit message there is vague about the reasoning, but my
understanding is that it was meant to harmonize gitignore and
gitattributes, the former of which can take "dir/". I don't have a
problem with offering "dir/" to match only directories, but what is the
point in disallowing just "dir" to match a directory?
It seems like a pointless regression to me, but I'm not clear whether it
was intentional or not (and if it was intentional, I think we would need
to handle it with a proper transition period, not in a maint release).
-Peff
From: Jeff King <hidden> Date: 2016-06-15 22:56:27
On Tue, Mar 19, 2013 at 01:57:56PM -0400, Jeff King wrote:
Prior to v1.8.1.1, if I did this:
git init
echo content >foo &&
mkdir subdir &&
echo content >subdir/bar &&
echo "subdir export-ignore" >.gitattributes
git add . &&
git commit -m one &&
git archive HEAD | tar tf -
my archive would contain only "foo" and ".gitattributes", not subdir. As
of v1.8.1.1, the attribute on subdir is ignored unless it is written
with a trailing slash, like:
subdir/ export-ignore
The issue bisects to 94bc671 (Add directory pattern matching to
attributes, 2012-12-08). That commit actually tests not only that
"subdir/" matches, but also that just "subdir" does not match.
Sorry, I mis-read the tests. They are not testing that "subdir" does not
work, only that "subdir/" will match only a directory, not a regular
file. Which does make sense.
So I think the regression is accidental. And we would want tests like
this on top (which currently fail):
From: Jeff King <hidden> Date: 2016-06-15 22:56:29
On Tue, Mar 19, 2013 at 02:10:42PM -0400, Jeff King wrote:
quoted
The issue bisects to 94bc671 (Add directory pattern matching to
attributes, 2012-12-08). That commit actually tests not only that
"subdir/" matches, but also that just "subdir" does not match.
[...]
So I think the regression is accidental. And we would want tests like
this on top (which currently fail):
[...]
I'm having trouble figuring out the right solution for this.
The problem is in path_matches, which used to receive just the unadorned
pathname, and now receives "path/" for directories. It now looks like
this:
static int path_matches(const char *pathname, int pathlen,
const char *basename,
const struct pattern *pat,
const char *base, int baselen)
{
const char *pattern = pat->pattern;
int prefix = pat->nowildcardlen;
if ((pat->flags & EXC_FLAG_MUSTBEDIR) &&
((!pathlen) || (pathname[pathlen-1] != '/')))
return 0;
This first stanza checks that a pattern like "foo/" must be matched by a
real directory. Which is fine; that's the point of adding the "/" to the
pattern.
But then here we'll end up feeding "foo/" to be compared with "foo",
which we don't want. For a pattern "foo", we want to match _either_
"foo/" or "foo". So you'd think something like:
if (pathlen && pathname[pathlen-1] == '/')
pathlen--;
would work. But it seems that match_basename, despite taking the length
of all of the strings we pass it, will happily use NUL-terminated
functions like strcmp or fnmatch. Converting the former to check lengths
should be pretty straightforward. But there is no version of fnmatch
that does what we want. I wonder if we using wildmatch can get around
this limitation.
-Peff
On Fri, Mar 22, 2013 at 06:24:39PM -0400, Jeff King wrote:
I'm having trouble figuring out the right solution for this.
Thanks for looking into this. It was on my todo list, but you beat me
to it :)
But then here we'll end up feeding "foo/" to be compared with "foo",
which we don't want. For a pattern "foo", we want to match _either_
"foo/" or "foo". So you'd think something like:
if (pathlen && pathname[pathlen-1] == '/')
pathlen--;
would work. But it seems that match_basename, despite taking the length
of all of the strings we pass it, will happily use NUL-terminated
functions like strcmp or fnmatch. Converting the former to check lengths
should be pretty straightforward. But there is no version of fnmatch
that does what we want. I wonder if we using wildmatch can get around
this limitation.
You can use nwildmatch() from this patch. I tested it lightly with
t3070-wildmatch.sh, feeding the strings with no terminating NUL. It
seems to work ok.
-- 8< --
Subject: [PATCH] wildmatch: do not require "text" to be NUL-terminated
This may be helpful when we just want to match a part of "text".
nwildmatch can be used for this purpose.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
wildmatch.c | 25 +++++++++++++------------
wildmatch.h | 11 +++++++++--
2 files changed, 22 insertions(+), 14 deletions(-)
@@ -52,7 +52,8 @@ typedef unsigned char uchar;#define ISXDIGIT(c) (ISASCII(c) && isxdigit(c))/* Match pattern "p" against "text" */-staticintdowild(constuchar*p,constuchar*text,unsignedintflags)+staticintdowild(constuchar*p,constuchar*text,+constuchar*textend,unsignedintflags){ucharp_ch;constuchar*pattern=p;
@@ -60,8 +61,9 @@ static int dowild(const uchar *p, const uchar *text, unsigned int flags)for(;(p_ch=*p)!='\0';text++,p++){intmatched,match_slash,negated;uchart_ch,prev_ch;-if((t_ch=*text)=='\0'&&p_ch!='*')+if(text>=textend&&p_ch!='*')returnWM_ABORT_ALL;+t_ch=*text;if((flags&WM_CASEFOLD)&&ISUPPER(t_ch))t_ch=tolower(t_ch);if((flags&WM_CASEFOLD)&&ISUPPER(p_ch))
@@ -101,7 +103,7 @@ static int dowild(const uchar *p, const uchar *text, unsigned int flags)*bothfoo/barandfoo/a/bar.*/if(p[0]=='/'&&-dowild(p+1,text,flags)==WM_MATCH)+dowild(p+1,text,textend,flags)==WM_MATCH)returnWM_MATCH;match_slash=1;}else
@@ -130,9 +132,7 @@ static int dowild(const uchar *p, const uchar *text, unsigned int flags)/* the slash is consumed by the top-level for loop */break;}-while(1){-if(t_ch=='\0')-break;+while(text<textend){/**Trytoadvancefasterwhenanasteriskis*followedbyaliteral.Weknowinthiscase
@@ -145,18 +145,18 @@ static int dowild(const uchar *p, const uchar *text, unsigned int flags)p_ch=*p;if((flags&WM_CASEFOLD)&&ISUPPER(p_ch))p_ch=tolower(p_ch);-while((t_ch=*text)!='\0'&&+while(text<textend&&(match_slash||t_ch!='/')){if((flags&WM_CASEFOLD)&&ISUPPER(t_ch))t_ch=tolower(t_ch);if(t_ch==p_ch)break;-text++;+t_ch=*++text;}if(t_ch!=p_ch)returnWM_NOMATCH;}-if((matched=dowild(p,text,flags))!=WM_NOMATCH){+if((matched=dowild(p,text,textend,flags))!=WM_NOMATCH){if(!match_slash||matched!=WM_ABORT_TO_STARSTAR)returnmatched;}elseif(!match_slash&&t_ch=='/')
@@ -261,12 +261,13 @@ static int dowild(const uchar *p, const uchar *text, unsigned int flags)}}-return*text?WM_NOMATCH:WM_MATCH;+returntext<textend?WM_NOMATCH:WM_MATCH;}/* Match the "pattern" against the "text" string. */-intwildmatch(constchar*pattern,constchar*text,+intnwildmatch(constchar*pattern,constchar*text,inttextlen,unsignedintflags,structwildopts*wo){-returndowild((constuchar*)pattern,(constuchar*)text,flags);+returndowild((constuchar*)pattern,(constuchar*)text,+(constuchar*)text+textlen,flags);}
@@ -12,7 +12,14 @@structwildopts;-intwildmatch(constchar*pattern,constchar*text,-unsignedintflags,+intnwildmatch(constchar*pattern,constchar*text,+intlen,unsignedintflags,structwildopts*wo);++/* Match the "pattern" against the "text" string. */+staticinlineintwildmatch(constchar*pattern,constchar*text,+unsignedintflags,structwildopts*wo)+{+returnnwildmatch(pattern,text,strlen(text),flags,wo);+}#endif
On Sat, Mar 23, 2013 at 11:18:24AM +0700, Duy Nguyen wrote:
You can use nwildmatch() from this patch. I tested it lightly with
t3070-wildmatch.sh, feeding the strings with no terminating NUL. It
seems to work ok.
And valgrind spotted my faults, especially for using strchr. You would
need this on top:
-- 8< --
@@ -125,10 +130,11 @@ static int dowild(const uchar *p, const uchar *text,*withWM_PATHNAMEmatchesthenext*directory*/-constchar*slash=strchr((char*)text,'/');-if(!slash)+for(;text<textend;text++)+if(*text=='/')+break;+if(text==textend)returnWM_NOMATCH;-text=(constuchar*)slash;/* the slash is consumed by the top-level for loop */break;}
I think the fix is something like this. There is still one thing I'd
like to do: make this code not rely on NUL for terminating the
patterns. That should remove the ugly "p[len] = '\0'" in
prepare_attr_stack() 4/4 and and the reallocation in add_exclude() (in
current code). But let's deal with the regression first.
Nguyễn Thái Ngọc Duy (4):
wildmatch: do not require "text" to be NUL-terminated
attr.c: fix pattern{,len} inconsistency in struct match_attr
dir.c: make match_{base,path}name respect {basename,path}len
attr.c: fix matching "subdir" without the trailing slash
attr.c | 11 ++++++++++-
dir.c | 13 ++++++++-----
dir.h | 2 +-
t/t5002-archive-attr-pattern.sh | 6 ++++++
wildmatch.c | 43 ++++++++++++++++++++++++-----------------
wildmatch.h | 11 +++++++++--
6 files changed, 59 insertions(+), 27 deletions(-)
--
1.8.2.82.gc24b958
This may be helpful when we just want to match a part of "text".
nwildmatch can be used for this purpose.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
wildmatch.c | 43 +++++++++++++++++++++++++------------------
wildmatch.h | 11 +++++++++--
2 files changed, 34 insertions(+), 20 deletions(-)
@@ -123,16 +130,15 @@ static int dowild(const uchar *p, const uchar *text, unsigned int flags)*withWM_PATHNAMEmatchesthenext*directory*/-constchar*slash=strchr((char*)text,'/');-if(!slash)+for(;text<textend;text++)+if(*text=='/')+break;+if(text==textend)returnWM_NOMATCH;-text=(constuchar*)slash;/* the slash is consumed by the top-level for loop */break;}-while(1){-if(t_ch=='\0')-break;+while(text<textend){/**Trytoadvancefasterwhenanasteriskis*followedbyaliteral.Weknowinthiscase
@@ -145,18 +151,18 @@ static int dowild(const uchar *p, const uchar *text, unsigned int flags)p_ch=*p;if((flags&WM_CASEFOLD)&&ISUPPER(p_ch))p_ch=tolower(p_ch);-while((t_ch=*text)!='\0'&&+while(text<textend&&(match_slash||t_ch!='/')){if((flags&WM_CASEFOLD)&&ISUPPER(t_ch))t_ch=tolower(t_ch);if(t_ch==p_ch)break;-text++;+t_ch=++text<textend?*text:'\0';}if(t_ch!=p_ch)returnWM_NOMATCH;}-if((matched=dowild(p,text,flags))!=WM_NOMATCH){+if((matched=dowild(p,text,textend,flags))!=WM_NOMATCH){if(!match_slash||matched!=WM_ABORT_TO_STARSTAR)returnmatched;}elseif(!match_slash&&t_ch=='/')
@@ -261,12 +267,13 @@ static int dowild(const uchar *p, const uchar *text, unsigned int flags)}}-return*text?WM_NOMATCH:WM_MATCH;+returntext<textend?WM_NOMATCH:WM_MATCH;}/* Match the "pattern" against the "text" string. */-intwildmatch(constchar*pattern,constchar*text,+intnwildmatch(constchar*pattern,constchar*text,inttextlen,unsignedintflags,structwildopts*wo){-returndowild((constuchar*)pattern,(constuchar*)text,flags);+returndowild((constuchar*)pattern,(constuchar*)text,+(constuchar*)text+textlen,flags);}
@@ -12,7 +12,14 @@structwildopts;-intwildmatch(constchar*pattern,constchar*text,-unsignedintflags,+intnwildmatch(constchar*pattern,constchar*text,+intlen,unsignedintflags,structwildopts*wo);++/* Match the "pattern" against the "text" string. */+staticinlineintwildmatch(constchar*pattern,constchar*text,+unsignedintflags,structwildopts*wo)+{+returnnwildmatch(pattern,text,strlen(text),flags,wo);+}#endif
When parse_exclude_pattern detects EXC_FLAG_MUSTBEDIR, it sets
patternlen _not_ to include the trailing slash and expects the caller
to trim it. Of the two callers, add_exclude() does, parse_attr_line()
does not.
Because of that, after parse_attr_line() returns, we may have pattern
"foo/" but its length is reported 3. Some functions do not care about
patternlen and will see the pattern as "foo/" while others may see it
as "foo". This patch makes patternlen reflect the true length of
pattern.
This is a bandage patch that's required for the next patch to pass the
test suite as that patch will rely on patternlen's correctness. The
true fix comes in the patch after the next one.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
attr.c | 2 ++
dir.h | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
When match_basename was split out of excluded_from_list in 593cb88
(exclude: split basename matching code into a separate function -
2012-10-15), it took basenamelen only as a hint. basename was required
to be NUL-terminated at the given length.
This was fine until match_basename had a new caller (from attr.c) and
therefore was no longer excluded_from_list's internal business. Make
match_basename stop relying on the NUL assumption above.
Do the same for match_pathname. From now on, only pattern is required
to be NUL-terminated at the specified patternlen for both
match_{base,path}name.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
dir.c | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
@@ -626,15 +626,18 @@ int match_basename(const char *basename, int basenamelen,intflags){if(prefix==patternlen){-if(!strcmp_icase(pattern,basename))+if(patternlen==basenamelen&&+!strncmp_icase(pattern,basename,basenamelen))return1;}elseif(flags&EXC_FLAG_ENDSWITH){if(patternlen-1<=basenamelen&&-!strcmp_icase(pattern+1,-basename+basenamelen-patternlen+1))+!strncmp_icase(pattern+1,+basename+basenamelen-patternlen+1,+patternlen-1))return1;}else{-if(fnmatch_icase(pattern,basename,0)==0)+if(nwildmatch(pattern,basename,basenamelen,+ignore_case?WM_CASEFOLD:0,NULL)==0)return1;}return0;
@@ -684,7 +687,7 @@ int match_pathname(const char *pathname, int pathlen,namelen-=prefix;}-returnwildmatch(pattern,name,+returnnwildmatch(pattern,name,namelen,WM_PATHNAME|(ignore_case?WM_CASEFOLD:0),NULL)==0;}
The story goes back to 94bc671 (Add directory pattern matching to
attributes - 2012-12-08). Before this commit, directories are passed
to path_matches without the trailing slash. This is fine for matching
pattern "subdir" with "foo/subdir".
Patterns like "subdir/" (i.e. match _directory_ subdir) won't work
though. So paths are now passed to path_matches with the trailing
slash (i.e. "subdir/"). The trailing slash is used as the directory
indicator (similar to dtype in exclude case). This makes pattern
"subdir/" match directory "subdir/". Pattern "subdir" no longer match
subdir, which is now "subdir/".
As the trailing slash in pathname is the directory indicator, we do
not need to keep it in the pathname for matching. The trailing slash
should be turned to dtype "DT_DIR" and stripped out of pathname. This
keeps the code pattern similar to exclude.
The same applies for the pattern "subdir/". The trailing slash is
converted to flag EXC_FLAG_MUSTBEDIR and should not remain in the
pattern, as noted in parse_exclude_pattern(). prepare_attr_stack()
breaks this and keeps the trailing slash anyway.
To sum up, both patterns and pathnames should never have the trailing
slash when it comes to match_basename.
Reported-and-analyzed-by: Jeff King [off-list ref]
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
attr.c | 11 +++++++++--
t/t5002-archive-attr-pattern.sh | 6 ++++++
2 files changed, 15 insertions(+), 2 deletions(-)
On Mon, Mar 25, 2013 at 1:05 PM, Nguyễn Thái Ngọc Duy [off-list ref] wrote:
The story goes back to 94bc671 (Add directory pattern matching to
attributes - 2012-12-08). Before this commit, directories are passed
to path_matches without the trailing slash. This is fine for matching
pattern "subdir" with "foo/subdir".
Patterns like "subdir/" (i.e. match _directory_ subdir) won't work
though. So paths are now passed to path_matches with the trailing
slash (i.e. "subdir/"). The trailing slash is used as the directory
indicator (similar to dtype in exclude case). This makes pattern
"subdir/" match directory "subdir/". Pattern "subdir" no longer match
subdir, which is now "subdir/".
As the trailing slash in pathname is the directory indicator, we do
not need to keep it in the pathname for matching. The trailing slash
should be turned to dtype "DT_DIR" and stripped out of pathname. This
keeps the code pattern similar to exclude.
On second thought, maybe we should not pass path "subdir/" at all.
Instead we create a fake dtype based on the trailing slash and pass it
down to attr.c:fill() -> path_matches(), just like how
last_exclude_matching_from_list() is called.
--
Duy
On Mon, Mar 25, 2013 at 02:20:31PM +0700, Duy Nguyen wrote:
On second thought, maybe we should not pass path "subdir/" at all.
Instead we create a fake dtype based on the trailing slash and pass it
down to attr.c:fill() -> path_matches(), just like how
last_exclude_matching_from_list() is called.
I was hoping to make a small patch, but as it turns out,
collect_all_attrs() takes a const path that contains the trailing
slash, we still need to ignore it in match_{base,path}name so the
whole series is still required. The only difference is in the final
patch, which is a bit longer:
-- 8< --