Fix handling of negated and rooted entries in .gitignore files.

* src/exclist.c (struct vcs_ignore_file): New field: matchfn.
(struct exclist): New field: vcs.
(info_attach_exclist): Initialize vcs.
(excluded_name): Rewrite using excluded_file_name_status from gnulib.
(git_addfn): Special handling for entries starting with '!' and '/'.
(vcs_ignore_files): Initialize matchfn member in the ".gitignore"
entry.

* tests/exclude23.at: New test.
* tests/Makefile.am: Add new test.
* tests/testsuite.at: Include new test.
This commit is contained in:
Sergey Poznyakoff
2026-08-18 21:51:55 +03:00
parent c1ada134b2
commit f022ac4ee0
5 changed files with 128 additions and 19 deletions
+70 -19
View File
@@ -33,6 +33,7 @@ struct vcs_ignore_file
int flags;
add_fn addfn;
void *(*initfn) (void *);
int (*matchfn) (struct exclude *, char const *);
void *data;
};
@@ -69,6 +70,7 @@ struct exclist
char *prefix;
size_t prefix_len;
struct exclude *excluded;
struct vcs_ignore_file *vcs;
};
void
@@ -115,6 +117,7 @@ info_attach_exclist (struct tar_stat_info *dir)
ent->flags = file->flags;
ent->prefix_len = strlen (dir->orig_file_name);
ent->prefix = xstrdup (dir->orig_file_name);
ent->vcs = vcsfile;
ent->prev = tail;
ent->next = NULL;
@@ -152,7 +155,7 @@ excluded_name (char const *name, struct tar_stat_info *st)
{
struct exclist *ep;
char *bname = NULL;
bool result;
int result;
int nr = 0;
name += FILE_SYSTEM_PREFIX_LEN (name);
@@ -164,7 +167,7 @@ excluded_name (char const *name, struct tar_stat_info *st)
if (!st)
return false;
for (result = false; st && !result; st = st->parent, nr = EXCL_NON_RECURSIVE)
for (result = 0; st; st = st->parent, nr = EXCL_NON_RECURSIVE)
{
for (ep = st->exclude_list; ep; ep = ep->next)
{
@@ -173,27 +176,37 @@ excluded_name (char const *name, struct tar_stat_info *st)
if (ep->flags & nr)
continue;
if ((result = excluded_file_name (ep->excluded, name)))
break;
result = excluded_file_name_status (ep->excluded, name);
if (result & EXCLUDED_MATCHED)
goto end;
if (ep->prefix_len && strlen (name) > ep->prefix_len &&
memcmp (name, ep->prefix, ep->prefix_len) == 0)
rname = name + ep->prefix_len;
else
else
rname = name + dotslashlen (name);
if ((result = excluded_file_name (ep->excluded, rname)))
break;
result = excluded_file_name_status (ep->excluded, rname);
if (result & EXCLUDED_MATCHED)
goto end;
if (ep->vcs->matchfn)
{
result = ep->vcs->matchfn (ep->excluded, rname);
if (result & EXCLUDED_MATCHED)
goto end;
}
if (!bname)
bname = base_name (name);
if ((result = excluded_file_name (ep->excluded, bname)))
break;
result = excluded_file_name_status (ep->excluded, bname);
if (result & EXCLUDED_MATCHED)
goto end;
}
}
end:
free (bname);
return result;
return (result & EXCLUDED_MATCHED) ? (result & EXCLUDED_EXCLUDED) : 0;
}
static void
@@ -220,11 +233,32 @@ git_addfn (struct exclude *ex, char const *pattern, int options,
++pattern;
if (*pattern == 0 || *pattern == '#')
return;
if (*pattern == '\\' && pattern[1] == '#')
if (*pattern == '\\' && (pattern[1] == '#' || pattern[1] == '!'))
++pattern;
else if (*pattern == '!')
{
++pattern;
options |= EXCLUDE_INCLUDE;
}
else if (*pattern == '/')
{
options |= EXCLUDE_ANCHORED;
}
add_exclude (ex, pattern, options);
}
static int
git_matchfn (struct exclude *ex, char const *name)
{
int rc;
char *rname = xmalloc (strlen (name) + 2);
rname[0] = '/';
strcpy (rname + 1, name);
rc = excluded_file_name_status (ex, rname);
free (rname);
return rc;
}
static void
bzr_addfn (struct exclude *ex, char const *pattern, int options,
void *UNNAMED (data))
@@ -303,18 +337,35 @@ hg_addfn (struct exclude *ex, char const *pattern, int options, void *data)
}
static struct vcs_ignore_file vcs_ignore_files[] = {
{ ".cvsignore", EXCL_NON_RECURSIVE, cvs_addfn, NULL, NULL },
{ ".gitignore", 0, git_addfn, NULL, NULL },
{ ".bzrignore", 0, bzr_addfn, NULL, NULL },
{ ".hgignore", 0, hg_addfn, hg_initfn, NULL },
{ NULL, 0, git_addfn, NULL, NULL }
{
.filename = ".cvsignore",
.flags = EXCL_NON_RECURSIVE,
.addfn = cvs_addfn
},
{
.filename = ".gitignore",
.addfn = git_addfn,
.matchfn = git_matchfn
},
{
.filename = ".bzrignore",
.addfn = bzr_addfn
},
{
.filename = ".hgignore",
.addfn = hg_addfn,
.initfn = hg_initfn
},
{
.addfn = git_addfn
}
};
static struct vcs_ignore_file *
get_vcs_ignore_file (const char *name)
{
struct vcs_ignore_file *p;
struct vcs_ignore_file *p
;
for (p = vcs_ignore_files; p->filename; p++)
if (streq (p->filename, name))
break;
+1
View File
@@ -112,6 +112,7 @@ TESTSUITE_AT = \
exclude20.at\
exclude21.at\
exclude22.at\
exclude23.at\
extrac01.at\
extrac02.at\
extrac03.at\
+1
View File
@@ -21,6 +21,7 @@
AT_SETUP([--exclude-ignore-recursive with transforms])
AT_KEYWORDS([exclude exclude-ignore-recursive extract exclude22])
AT_TAR_CHECK([
AT_SORT_PREREQ
mkdir dir dir/subdir
touch dir/file1 dir/file.ext dir/subdir/file2.foo dir/subdir/file3 \
dir/subdir/file4.txt dir/subdir/file5.ext
+55
View File
@@ -0,0 +1,55 @@
# Process this file with autom4te to create testsuite. -*- Autotest -*-
# Test suite for GNU tar.
# Copyright 2026 Free Software Foundation, Inc.
# This file is part of GNU tar.
# GNU tar is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# GNU tar is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
AT_SETUP([gitignore excludes])
AT_KEYWORDS([exclude gitignore extract exclude23])
AT_TAR_CHECK([
AT_SORT_PREREQ
# Test whether negations (!) and rooted patterns (/) work in
# .gitignore files.
mkdir dir dir/subdir
touch dir/subdir/some.1 dir/subdir/any.1 dir/subdir/bar dir/subdir/FILE
cat > dir/subdir/.gitignore <<EOT
!any.1
EOT
touch dir/foo.1 dir/file.1 dir/file.2 dir/file.3 dir/file.4 dir/FILE
cat > dir/.gitignore <<EOT
*.1
*.2
!foo.1
*.3
/FILE
/subdir/b?r
EOT
set -e
tar --exclude-ignore-recursive=.gitignore -cf dir.tar dir
tar tf dir.tar | sort
],
[0],
[dir/
dir/.gitignore
dir/file.4
dir/foo.1
dir/subdir/
dir/subdir/.gitignore
dir/subdir/FILE
dir/subdir/any.1
])
AT_CLEANUP
+1
View File
@@ -319,6 +319,7 @@ m4_include([exclude19.at])
m4_include([exclude20.at])
m4_include([exclude21.at])
m4_include([exclude22.at])
m4_include([exclude23.at])
AT_BANNER([Deletions])
m4_include([delete01.at])