From f022ac4ee0ad80a411386f14ec7570856a7cc8d1 Mon Sep 17 00:00:00 2001 From: Sergey Poznyakoff Date: Tue, 18 Aug 2026 21:51:55 +0300 Subject: [PATCH] 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. --- src/exclist.c | 89 ++++++++++++++++++++++++++++++++++++---------- tests/Makefile.am | 1 + tests/exclude22.at | 1 + tests/exclude23.at | 55 ++++++++++++++++++++++++++++ tests/testsuite.at | 1 + 5 files changed, 128 insertions(+), 19 deletions(-) create mode 100644 tests/exclude23.at diff --git a/src/exclist.c b/src/exclist.c index 56f7e735..e668b2a4 100644 --- a/src/exclist.c +++ b/src/exclist.c @@ -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; diff --git a/tests/Makefile.am b/tests/Makefile.am index 869ebf9c..5e837857 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -112,6 +112,7 @@ TESTSUITE_AT = \ exclude20.at\ exclude21.at\ exclude22.at\ + exclude23.at\ extrac01.at\ extrac02.at\ extrac03.at\ diff --git a/tests/exclude22.at b/tests/exclude22.at index 4f0addde..d8d818a2 100644 --- a/tests/exclude22.at +++ b/tests/exclude22.at @@ -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 diff --git a/tests/exclude23.at b/tests/exclude23.at new file mode 100644 index 00000000..b1e52582 --- /dev/null +++ b/tests/exclude23.at @@ -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 . + +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 < dir/.gitignore <