-GNU tar NEWS - User visible changes. 2014-02-14
+GNU tar NEWS - User visible changes. 2014-02-21
Please send GNU tar bug reports to <bug-tar@gnu.org>
\f
of disk seeks made when creating the archive and thus can considerably
speed up archivation.
+* New exclusion options
+
+ --exclude-ignore=FILE Before dumping a directory check if it
+ contains FILE, and if so read exclude
+ patterns for this directory from FILE.
+ --exclude-ignore-recursive=FILE
+ Same as above, but the exclusion patterns
+ read from FILE remain in effect for any
+ subdirectory, recursively.
+ --exclude-vcs-ignores Read exclude tags from VCS ignore files,
+ where such files exist. Supported VCS's
+ are: CVS, Git, Bazaar, Mercurial.
+
+
* Manpages
This release includes official tar(1) and rmt(8) manpages.
.\"
.\" You should have received a copy of the GNU General Public License
.\" along with this program. If not, see <http://www.gnu.org/licenses/>.
-.TH TAR 1 "February 14, 2014" "TAR" "GNU TAR Manual"
+.TH TAR 1 "February 22, 2014" "TAR" "GNU TAR Manual"
.SH NAME
tar \- an archiving utility
.SH SYNOPSIS
\fB\-\-exclude\-caches\-under\fR
Exclude everything under directories containing \fBCACHEDIR.TAG\fR
.TP
+\fB\-\-exclude\-ignore=\fIFILE\fR
+Before dumping a directory, see if it contains \fIFILE\fR.
+If so, read exclusion patterns from this file. The patterns affect
+only the directory itself.
+.TP
+\fB\-\-exclude\-ignore\-recursive=\fIFILE\fR
+Same as \fB\-\-exclude\-ignore\fR, except that patterns from
+\fIFILE\fR affect both the directory and all its subdirectories.
+.TP
\fB\-\-exclude\-tag\fR=\fIFILE\fR
Exclude contents of directories containing \fIFILE\fR, except for
\fIFILE\fR itself.
\fB\-\-exclude\-vcs\fR
Exclude version control system directories.
.TP
+\fB\-\-exclude\-vcs\-ignores\fR
+Exclude files that match patterns read from VCS-specific ignore
+files. Supported files are:
+.BR .cvsignore ,
+.BR .gitignore ,
+.BR .bzrignore ", and"
+.BR .hgignore .
+.TP
\fB\-h\fR, \fB\-\-dereference\fR
Follow symlinks; archive and dump the files they point to.
.TP
Exclude from dump any directory containing a valid cache directory
tag file. @xref{exclude}.
+@opsummary{exclude-ignore}
+@item --exclude-ignore=@var{file}
+Before dumping a directory, @command{tar} checks if it contains
+@var{file}. If so, exclusion patterns are read from this file.
+The patterns affect only the directory itself. @xref{exclude}.
+
+@opsummary{exclude-ignore-recursive}
+@item --exclude-ignore-recursive=@var{file}
+Before dumping a directory, @command{tar} checks if it contains
+@var{file}. If so, exclusion patterns are read from this file.
+The patterns affect the directory and all itssubdirectories.
+@xref{exclude}.
+
@opsummary{exclude-tag}
@item --exclude-tag=@var{file}
Exclude from dump directories and files, that are internal for some
widely used version control systems.
-@xref{exclude,,exclude-vcs}.
+@xref{exclude-vcs}.
+
+@opsummary{exclude-vcs-ignores}
+@item --exclude-vcs-ignores
+Exclude files that match patterns read from VCS-specific ignore
+files. Supported files are: @file{.cvsignore}, @file{.gitignore},
+@file{.bzrignore}, and @file{.hgignore}. The semantics of each file
+is the same as for the corresponding VCS, e.g. patterns read from
+@file{.gitignore} affect the directory and all its subdirectories.
+@xref{exclude-vcs-ignores}.
@opsummary{file}
@item --file=@var{archive}
However, empty lines are OK.
+@cindex VCS, excluding patterns from ignore files
+@cindex VCS, ignore files
+@cindex CVS, ignore files
+@cindex Git, ignore files
+@cindex Bazaar, ignore files
+@cindex Mercurial, ignore files
+When archiving directories that are under some version control system (VCS),
+it is often convenient to read exclusion patterns from this VCS'
+ignore files (e.g. @file{.cvsignore}, @file{.gitignore}, etc.) The
+following options provide such possibilty:
+
+@table @option
+@anchor{exclude-vcs-ignores}
+@opindex exclude-vcs-ignores
+@item --exclude-vcs-ignores
+Before archiving a directory, see if it contains any of the following
+files: @file{cvsignore}, @file{.gitignore}, @file{.bzrignore}, or
+@file{.hgignore}. If so, read ignore patterns from these files.
+
+The patterns are treated much as the corresponding VCS would treat
+them, i.e.:
+
+@table @file
+@findex .cvsignore
+@item .cvsignore
+Contains shell-style globbing patterns that apply only to the
+directory where this file resides. No comments are allowed in the
+file. Empty lines are ignored.
+
+@findex .gitignore
+@item .gitignore
+Contains shell-style globbing patterns. Applies to the directory
+where @file{.gitfile} is located and all its subdirectories.
+
+Any line beginning with a @samp{#} is a comment. Backslash escapes
+the comment character.
+
+@findex .bzrignore
+@item .bzrignore
+Contains shell globbing-patterns and regular expressions (if prefixed
+with @samp{RE:}@footnote{According to the Bazaar docs,
+globbing-patterns are Korn-shell style and regular expressions are
+perl-style. As of @GNUTAR{} version @value{VERSION}, these are
+treated as shell-style globs and posix extended regexps. This will be
+fixed in future releases.}. Patterns affect the directory and all its
+subdirectories.
+
+Any line beginning with a @samp{#} is a comment.
+
+@findex .hgignore
+@item .hgignore
+Contains posix regular expressions@footnote{Support for perl-style
+regexps will appear in future releases.}. The line @samp{syntax:
+glob} switches to shell globbing patterns. The line @samp{syntax:
+regexp} switches back. Comments begin with a @samp{#}. Patterns
+affect the directory and all its subdirectories.
+@end table
+
+@opindex exclude-ignore
+@item --exclude-ignore=@var{file}
+Before dumping a directory, @command{tar} checks if it contains
+@var{file}. If so, exclusion patterns are read from this file.
+The patterns affect only the directory itself.
+
+@opindex exclude-ignore-recursive
+@item --exclude-ignore-recursive=@var{file}
+Same as @option{--exclude-ignore}, except that the patterns read
+affect both the directory where @var{file} resides and all its
+subdirectories.
+@end table
+
@table @option
@cindex version control system, excluding files
@cindex VCS, excluding files
@cindex Arch, excluding files
@cindex Mercurial, excluding files
@cindex Darcs, excluding files
+@anchor{exclude-vcs}
@opindex exclude-vcs
@item --exclude-vcs
Exclude files and directories used by following version control
create.c\
delete.c\
exit.c\
+ exclist.c\
extract.c\
xheader.c\
incremen.c\
size_t stripped_prefix_len (char const *file_name, size_t num);
bool all_names_found (struct tar_stat_info *st);
-bool excluded_name (char const *name);
-
void add_avoided_name (char const *name);
bool is_avoided_name (char const *name);
/* Module exit.c */
extern void (*fatal_exit_hook) (void);
+/* Module exclist.c */
+#define EXCL_DEFAULT 0x00
+#define EXCL_RECURSIVE 0x01
+#define EXCL_NON_RECURSIVE 0x02
+
+void excfile_add (const char *name, int flags);
+void info_attach_exclist (struct tar_stat_info *dir);
+void info_cleanup_exclist (struct tar_stat_info *dir);
+void info_free_exclist (struct tar_stat_info *dir);
+bool excluded_name (char const *name, struct tar_stat_info *st);
+void exclude_vcs_ignores (void);
+
_GL_INLINE_HEADER_END
if (!blk)
return;
+ info_attach_exclist (st);
+
if (incremental_option && archive_format != POSIX_FORMAT)
blk->header.typeflag = GNUTYPE_DUMPDIR;
else /* if (standard_option) */
char const *entry;
size_t entry_len;
size_t name_len;
-
+
name_buf = xstrdup (st->orig_file_name);
name_size = name_len = strlen (name_buf);
name_buf = xrealloc (name_buf, name_size + 1);
}
strcpy (name_buf + name_len, entry);
- if (!excluded_name (name_buf))
+ if (!excluded_name (name_buf, st))
dump_file (st, entry, name_buf);
}
collect_and_sort_names ();
while ((p = name_from_list ()) != NULL)
- if (!excluded_name (p->name))
+ if (!excluded_name (p->name, NULL))
dump_file (0, p->name, p->name);
blank_name_list ();
while ((p = name_from_list ()) != NULL)
- if (!excluded_name (p->name))
+ if (!excluded_name (p->name, NULL))
{
struct tar_stat_info st;
size_t plen = strlen (p->name);
if (! ISSLASH (buffer[plen - 1]))
buffer[plen++] = DIRECTORY_SEPARATOR;
tar_stat_init (&st);
- q = directory_contents (gnu_list_name->directory);
+ q = directory_contents (p->directory);
if (q)
while (*q)
{
{
const char *name;
while ((name = name_next (1)) != NULL)
- if (!excluded_name (name))
+ if (!excluded_name (name, NULL))
dump_file (0, name, name);
}
--- /dev/null
+/* Per-directory exclusion files for tar.
+
+ Copyright 2014 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/>.
+*/
+#include <system.h>
+#include <quotearg.h>
+#include <fnmatch.h>
+#include <wordsplit.h>
+#include "common.h"
+
+typedef void (*add_fn) (struct exclude *, char const *, int, void *);
+
+struct vcs_ignore_file
+{
+ char const *filename;
+ int flags;
+ add_fn addfn;
+ void *(*initfn) (void *);
+ void *data;
+};
+
+static struct vcs_ignore_file *get_vcs_ignore_file (const char *name);
+\f
+struct excfile
+{
+ struct excfile *next;
+ int flags;
+ char name[1];
+};
+
+struct excfile *excfile_head, *excfile_tail;
+
+void
+excfile_add (const char *name, int flags)
+{
+ struct excfile *p = xmalloc (sizeof (*p) + strlen (name));
+ p->next = NULL;
+ p->flags = flags;
+ strcpy (p->name, name);
+ if (excfile_tail)
+ excfile_tail->next = p;
+ else
+ excfile_head = p;
+ excfile_tail = p;
+}
+
+struct exclist
+{
+ struct exclist *next, *prev;
+ int flags;
+ struct exclude *excluded;
+};
+
+void
+info_attach_exclist (struct tar_stat_info *dir)
+{
+ struct excfile *file;
+ struct exclist *head = NULL, *tail = NULL, *ent;
+ struct vcs_ignore_file *vcsfile;
+
+ if (dir->exclude_list)
+ return;
+ for (file = excfile_head; file; file = file->next)
+ {
+ if (faccessat (dir ? dir->fd : chdir_fd, file->name, F_OK, 0) == 0)
+ {
+ FILE *fp;
+ struct exclude *ex = NULL;
+ int fd = subfile_open (dir, file->name, O_RDONLY);
+ if (fd == -1)
+ {
+ open_error (file->name);
+ continue;
+ }
+ fp = fdopen (fd, "r");
+ if (!fp)
+ {
+ ERROR ((0, errno, _("%s: fdopen failed"), file->name));
+ close (fd);
+ continue;
+ }
+
+ if (!ex)
+ ex = new_exclude ();
+
+ vcsfile = get_vcs_ignore_file (file->name);
+
+ if (vcsfile->initfn)
+ vcsfile->data = vcsfile->initfn (vcsfile->data);
+
+ if (add_exclude_fp (vcsfile->addfn, ex, fp,
+ EXCLUDE_WILDCARDS|EXCLUDE_ANCHORED, '\n',
+ vcsfile->data))
+ {
+ int e = errno;
+ FATAL_ERROR ((0, e, "%s", quotearg_colon (file->name)));
+ }
+ fclose (fp);
+
+ ent = xmalloc (sizeof (*ent));
+ ent->excluded = ex;
+ ent->flags = file->flags == EXCL_DEFAULT
+ ? file->flags : vcsfile->flags;
+ ent->prev = tail;
+ ent->next = NULL;
+
+ if (tail)
+ tail->next = ent;
+ else
+ head = ent;
+ tail = ent;
+ }
+ }
+ dir->exclude_list = head;
+}
+
+void
+info_cleanup_exclist (struct tar_stat_info *dir)
+{
+ struct exclist *ep = dir->exclude_list;
+
+ while (ep)
+ {
+ struct exclist *next = ep->next;
+
+ if (ep->flags & EXCL_NON_RECURSIVE)
+ {
+
+ /* Remove the entry */
+ if (ep->prev)
+ ep->prev->next = ep->next;
+ else
+ dir->exclude_list = ep->next;
+
+ if (ep->next)
+ ep->next->prev = ep->prev;
+
+ free_exclude (ep->excluded);
+ free (ep);
+ }
+ ep = next;
+ }
+}
+
+void
+info_free_exclist (struct tar_stat_info *dir)
+{
+ struct exclist *ep = dir->exclude_list;
+
+ while (ep)
+ {
+ struct exclist *next = ep->next;
+ free_exclude (ep->excluded);
+ free (ep);
+ ep = next;
+ }
+
+ dir->exclude_list = NULL;
+}
+
+
+/* Return nonzero if file NAME is excluded. */
+bool
+excluded_name (char const *name, struct tar_stat_info *st)
+{
+ struct exclist *ep;
+ const char *rname = NULL;
+ char *bname = NULL;
+ bool result;
+ int nr = 0;
+
+ name += FILE_SYSTEM_PREFIX_LEN (name);
+
+ /* Try global exclusion list first */
+ if (excluded_file_name (excluded, name))
+ return true;
+
+ if (!st)
+ return false;
+
+ for (result = false; st && !result; st = st->parent, nr = EXCL_NON_RECURSIVE)
+ {
+ for (ep = st->exclude_list; ep; ep = ep->next)
+ {
+ if (ep->flags & nr)
+ continue;
+ if ((result = excluded_file_name (ep->excluded, name)))
+ break;
+
+ if (!rname)
+ {
+ rname = name;
+ /* Skip leading ./ */
+ while (*rname == '.' && ISSLASH (rname[1]))
+ rname += 2;
+ }
+ if ((result = excluded_file_name (ep->excluded, rname)))
+ break;
+
+ if (!bname)
+ bname = base_name (name);
+ if ((result = excluded_file_name (ep->excluded, bname)))
+ break;
+ }
+ }
+
+ free (bname);
+
+ return result;
+}
+\f
+static void
+cvs_addfn (struct exclude *ex, char const *pattern, int options, void *data)
+{
+ struct wordsplit ws;
+ size_t i;
+
+ if (wordsplit (pattern, &ws,
+ WRDSF_NOVAR | WRDSF_NOCMD | WRDSF_SQUEEZE_DELIMS))
+ return;
+ for (i = 0; i < ws.ws_wordc; i++)
+ add_exclude (ex, ws.ws_wordv[i], options);
+ wordsplit_free (&ws);
+}
+
+static void
+git_addfn (struct exclude *ex, char const *pattern, int options, void *data)
+{
+ while (isspace (*pattern))
+ ++pattern;
+ if (*pattern == 0 || *pattern == '#')
+ return;
+ if (*pattern == '\\' && pattern[1] == '#')
+ ++pattern;
+ add_exclude (ex, pattern, options);
+}
+
+static void
+bzr_addfn (struct exclude *ex, char const *pattern, int options, void *data)
+{
+ while (isspace (*pattern))
+ ++pattern;
+ if (*pattern == 0 || *pattern == '#')
+ return;
+ if (*pattern == '!')
+ {
+ if (*++pattern == '!')
+ ++pattern;
+ else
+ options |= EXCLUDE_INCLUDE;
+ }
+ /* FIXME: According to the docs, globbing patterns are rsync-style,
+ and regexps are perl-style. */
+ if (strncmp (pattern, "RE:", 3) == 0)
+ {
+ pattern += 3;
+ options &= ~EXCLUDE_WILDCARDS;
+ options |= EXCLUDE_REGEX;
+ }
+ add_exclude (ex, pattern, options);
+}
+
+static void *
+hg_initfn (void *data)
+{
+ int *hgopt;
+ static int hg_options;
+
+ if (!data)
+ hgopt = &hg_options;
+
+ *hgopt = EXCLUDE_REGEX;
+ return hgopt;
+}
+
+static void
+hg_addfn (struct exclude *ex, char const *pattern, int options, void *data)
+{
+ int *hgopt = data;
+ size_t len;
+
+ while (isspace (*pattern))
+ ++pattern;
+ if (*pattern == 0 || *pattern == '#')
+ return;
+ if (strncmp (pattern, "syntax:", 7) == 0)
+ {
+ for (pattern += 7; isspace (*pattern); ++pattern)
+ ;
+ if (strcmp (pattern, "regexp") == 0)
+ /* FIXME: Regexps must be perl-style */
+ *hgopt = EXCLUDE_REGEX;
+ else if (strcmp (pattern, "glob") == 0)
+ *hgopt = EXCLUDE_WILDCARDS;
+ /* Ignore unknown syntax */
+ return;
+ }
+
+ len = strlen(pattern);
+ if (pattern[len-1] == '/')
+ {
+ char *p;
+
+ --len;
+ p = xmalloc (len+1);
+ memcpy (p, pattern, len);
+ p[len] = 0;
+ pattern = p;
+ exclude_add_pattern_buffer (ex, p);
+ options |= FNM_LEADING_DIR|EXCLUDE_ALLOC;
+ }
+
+ add_exclude (ex, pattern,
+ ((*hgopt == EXCLUDE_REGEX)
+ ? (options & ~EXCLUDE_WILDCARDS)
+ : (options & ~EXCLUDE_REGEX)) | *hgopt);
+}
+\f
+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 }
+};
+
+static struct vcs_ignore_file *
+get_vcs_ignore_file (const char *name)
+{
+ struct vcs_ignore_file *p;
+
+ for (p = vcs_ignore_files; p->filename; p++)
+ if (strcmp (p->filename, name) == 0)
+ break;
+
+ return p;
+}
+\f
+void
+exclude_vcs_ignores (void)
+{
+ struct vcs_ignore_file *p;
+
+ for (p = vcs_ignore_files; p->filename; p++)
+ excfile_add (p->filename, EXCL_DEFAULT);
+}
if (! dirp)
savedir_error (dir);
+ info_attach_exclist (st);
+
tmp = xstrdup (dir);
zap_slashes (tmp);
if (*entry == 'I') /* Ignored entry */
*entry = 'N';
- else if (excluded_name (full_name))
+ else if (excluded_name (full_name, st))
*entry = 'N';
else
{
mtime.tv_nsec = 0,
current_stat_info.mtime = mtime,
OLDER_TAR_STAT_TIME (current_stat_info, m)))
- || excluded_name (current_stat_info.file_name))
+ || excluded_name (current_stat_info.file_name,
+ current_stat_info.parent))
{
switch (current_header->header.typeflag)
{
return buffer;
}
-/* Return nonzero if file NAME is excluded. */
-bool
-excluded_name (char const *name)
-{
- return excluded_file_name (excluded, name + FILE_SYSTEM_PREFIX_LEN (name));
-}
\f
/* Return the size of the prefix of FILE_NAME that is removed after
EXCLUDE_CACHES_UNDER_OPTION,
EXCLUDE_CACHES_ALL_OPTION,
EXCLUDE_OPTION,
+ EXCLUDE_IGNORE_OPTION,
+ EXCLUDE_IGNORE_RECURSIVE_OPTION,
EXCLUDE_TAG_OPTION,
EXCLUDE_TAG_UNDER_OPTION,
EXCLUDE_TAG_ALL_OPTION,
EXCLUDE_VCS_OPTION,
+ EXCLUDE_VCS_IGNORES_OPTION,
FORCE_LOCAL_OPTION,
FULL_TIME_OPTION,
GROUP_OPTION,
{"exclude-tag", EXCLUDE_TAG_OPTION, N_("FILE"), 0,
N_("exclude contents of directories containing FILE, except"
" for FILE itself"), GRID+1 },
+ {"exclude-ignore", EXCLUDE_IGNORE_OPTION, N_("FILE"), 0,
+ N_("read exclude patterns for each directory from FILE, if it exists"),
+ GRID+1 },
+ {"exclude-ignore-recursive", EXCLUDE_IGNORE_RECURSIVE_OPTION, N_("FILE"), 0,
+ N_("read exclude patterns for each directory and its subdirectories "
+ "from FILE, if it exists"), GRID+1 },
{"exclude-tag-under", EXCLUDE_TAG_UNDER_OPTION, N_("FILE"), 0,
N_("exclude everything under directories containing FILE"), GRID+1 },
{"exclude-tag-all", EXCLUDE_TAG_ALL_OPTION, N_("FILE"), 0,
N_("exclude directories containing FILE"), GRID+1 },
{"exclude-vcs", EXCLUDE_VCS_OPTION, NULL, 0,
N_("exclude version control system directories"), GRID+1 },
+ {"exclude-vcs-ignores", EXCLUDE_VCS_IGNORES_OPTION, NULL, 0,
+ N_("read exclude patterns from the VCS ignore files"), GRID+1 },
{"exclude-backups", EXCLUDE_BACKUPS_OPTION, NULL, 0,
N_("exclude backup and lock files"), GRID+1 },
{"no-recursion", NO_RECURSION_OPTION, 0, 0,
cachedir_file_p);
break;
+ case EXCLUDE_IGNORE_OPTION:
+ excfile_add (arg, EXCL_NON_RECURSIVE);
+ break;
+
+ case EXCLUDE_IGNORE_RECURSIVE_OPTION:
+ excfile_add (arg, EXCL_RECURSIVE);
+ break;
+
case EXCLUDE_TAG_OPTION:
add_exclusion_tag (arg, exclusion_tag_contents, NULL);
break;
add_exclude_array (vcs_file_table, 0);
break;
+ case EXCLUDE_VCS_IGNORES_OPTION:
+ exclude_vcs_ignores ();
+ break;
+
case FORCE_LOCAL_OPTION:
force_local_option = true;
break;
blocking_factor = DEFAULT_BLOCKING;
record_size = DEFAULT_BLOCKING * BLOCKSIZE;
excluded = new_exclude ();
+
newer_mtime_option.tv_sec = TYPE_MINIMUM (time_t);
newer_mtime_option.tv_nsec = -1;
recursion_option = FNM_LEADING_DIR;
free (st->sparse_map);
free (st->dumpdir);
xheader_destroy (&st->xhdr);
+ info_free_exclist (st);
memset (st, 0, sizeof (*st));
}
It is negative if it could not be reopened after it was closed.
Negate it to find out what errno was when the reopen failed. */
int fd;
+
+ /* Exclusion list */
+ struct exclist *exclude_list;
};
union block
while ((p = name_from_list ()) != NULL)
{
char *file_name = p->name;
- if (excluded_name (file_name))
+ if (excluded_name (file_name, NULL))
continue;
if (interactive_option && !confirm ("add", file_name))
continue;