Merge branch 'nd/archive-pathspec'
authorJunio C Hamano <gitster@pobox.com>
Wed, 8 Oct 2014 20:05:25 +0000 (13:05 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 8 Oct 2014 20:05:26 +0000 (13:05 -0700)
"git archive" learned to filter what gets archived with pathspec.

* nd/archive-pathspec:
archive: support filtering paths with glob

1  2 
archive.c
t/t5000-tar-tree.sh
diff --combined archive.c
index 952a659bcb59ea8b476d6677f2a04d717c6c7dd2,5bdab7b1480f122397264c405f751c2f3547a6af..94a9981adf72c437f8660c66b67506b50ed8d3a9
+++ b/archive.c
@@@ -5,6 -5,7 +5,7 @@@
  #include "archive.h"
  #include "parse-options.h"
  #include "unpack-trees.h"
+ #include "dir.h"
  
  static char const * const archive_usage[] = {
        N_("git archive [options] <tree-ish> [<path>...]"),
@@@ -17,7 -18,6 +18,7 @@@
  static const struct archiver **archivers;
  static int nr_archivers;
  static int alloc_archivers;
 +static int remote_allow_unreachable;
  
  void register_archiver(struct archiver *ar)
  {
@@@ -98,9 -98,19 +99,19 @@@ static void setup_archive_check(struct 
        check[1].attr = attr_export_subst;
  }
  
+ struct directory {
+       struct directory *up;
+       unsigned char sha1[20];
+       int baselen, len;
+       unsigned mode;
+       int stage;
+       char path[FLEX_ARRAY];
+ };
  struct archiver_context {
        struct archiver_args *args;
        write_archive_entry_fn_t write_entry;
+       struct directory *bottom;
  };
  
  static int write_archive_entry(const unsigned char *sha1, const char *base,
        return write_entry(args, sha1, path.buf, path.len, mode);
  }
  
+ static void queue_directory(const unsigned char *sha1,
+               const char *base, int baselen, const char *filename,
+               unsigned mode, int stage, struct archiver_context *c)
+ {
+       struct directory *d;
+       d = xmallocz(sizeof(*d) + baselen + 1 + strlen(filename));
+       d->up      = c->bottom;
+       d->baselen = baselen;
+       d->mode    = mode;
+       d->stage   = stage;
+       c->bottom  = d;
+       d->len = sprintf(d->path, "%.*s%s/", baselen, base, filename);
+       hashcpy(d->sha1, sha1);
+ }
+ static int write_directory(struct archiver_context *c)
+ {
+       struct directory *d = c->bottom;
+       int ret;
+       if (!d)
+               return 0;
+       c->bottom = d->up;
+       d->path[d->len - 1] = '\0'; /* no trailing slash */
+       ret =
+               write_directory(c) ||
+               write_archive_entry(d->sha1, d->path, d->baselen,
+                                   d->path + d->baselen, d->mode,
+                                   d->stage, c) != READ_TREE_RECURSIVE;
+       free(d);
+       return ret ? -1 : 0;
+ }
+ static int queue_or_write_archive_entry(const unsigned char *sha1,
+               const char *base, int baselen, const char *filename,
+               unsigned mode, int stage, void *context)
+ {
+       struct archiver_context *c = context;
+       while (c->bottom &&
+              !(baselen >= c->bottom->len &&
+                !strncmp(base, c->bottom->path, c->bottom->len))) {
+               struct directory *next = c->bottom->up;
+               free(c->bottom);
+               c->bottom = next;
+       }
+       if (S_ISDIR(mode)) {
+               queue_directory(sha1, base, baselen, filename,
+                               mode, stage, c);
+               return READ_TREE_RECURSIVE;
+       }
+       if (write_directory(c))
+               return -1;
+       return write_archive_entry(sha1, base, baselen, filename, mode,
+                                  stage, context);
+ }
  int write_archive_entries(struct archiver_args *args,
                write_archive_entry_fn_t write_entry)
  {
                        return err;
        }
  
+       memset(&context, 0, sizeof(context));
        context.args = args;
        context.write_entry = write_entry;
  
        }
  
        err = read_tree_recursive(args->tree, "", 0, 0, &args->pathspec,
-                                 write_archive_entry, &context);
+                                 args->pathspec.has_wildcard ?
+                                 queue_or_write_archive_entry :
+                                 write_archive_entry,
+                                 &context);
        if (err == READ_TREE_RECURSIVE)
                err = 0;
+       while (context.bottom) {
+               struct directory *next = context.bottom->up;
+               free(context.bottom);
+               context.bottom = next;
+       }
        return err;
  }
  
@@@ -211,7 -289,16 +290,16 @@@ static int reject_entry(const unsigned 
                        int baselen, const char *filename, unsigned mode,
                        int stage, void *context)
  {
-       return -1;
+       int ret = -1;
+       if (S_ISDIR(mode)) {
+               struct strbuf sb = STRBUF_INIT;
+               strbuf_addstr(&sb, base);
+               strbuf_addstr(&sb, filename);
+               if (!match_pathspec(context, sb.buf, sb.len, 0, NULL, 1))
+                       ret = READ_TREE_RECURSIVE;
+               strbuf_release(&sb);
+       }
+       return ret;
  }
  
  static int path_exists(struct tree *tree, const char *path)
        int ret;
  
        parse_pathspec(&pathspec, 0, 0, "", paths);
-       ret = read_tree_recursive(tree, "", 0, 0, &pathspec, reject_entry, NULL);
+       pathspec.recursive = 1;
+       ret = read_tree_recursive(tree, "", 0, 0, &pathspec,
+                                 reject_entry, &pathspec);
        free_pathspec(&pathspec);
        return ret != 0;
  }
@@@ -237,6 -326,7 +327,7 @@@ static void parse_pathspec_arg(const ch
        parse_pathspec(&ar_args->pathspec, 0,
                       PATHSPEC_PREFER_FULL,
                       "", pathspec);
+       ar_args->pathspec.recursive = 1;
        if (pathspec) {
                while (*pathspec) {
                        if (**pathspec && !path_exists(ar_args->tree, *pathspec))
@@@ -258,10 -348,10 +349,10 @@@ static void parse_treeish_arg(const cha
        unsigned char sha1[20];
  
        /* Remotes are only allowed to fetch actual refs */
 -      if (remote) {
 +      if (remote && !remote_allow_unreachable) {
                char *ref = NULL;
 -              const char *colon = strchr(name, ':');
 -              int refnamelen = colon ? colon - name : strlen(name);
 +              const char *colon = strchrnul(name, ':');
 +              int refnamelen = colon - name;
  
                if (!dwim_ref(name, refnamelen, sha1, &ref))
                        die("no such ref: %.*s", refnamelen, name);
@@@ -412,9 -502,7 +503,9 @@@ int write_archive(int argc, const char 
        if (setup_prefix && prefix == NULL)
                prefix = setup_git_directory_gently(&nongit);
  
 +      git_config_get_bool("uploadarchive.allowunreachable", &remote_allow_unreachable);
        git_config(git_default_config, NULL);
 +
        init_tar_archiver();
        init_zip_archiver();
  
diff --combined t/t5000-tar-tree.sh
index 7b8babd89b22dc94d3a3f839d72cce86c5aaea59,c9abab493ab67f14d6ae65e931e5fbf538aa48d4..d01bbdc968a0fcde9eb5410793f8a22108ee6c2d
@@@ -72,7 -72,7 +72,7 @@@ check_tar() 
                        for header in *.paxheader
                        do
                                data=${header%.paxheader}.data &&
 -                              if test -h $data -o -e $data
 +                              if test -h $data || test -e $data
                                then
                                        path=$(get_pax_header $header path) &&
                                        if test -n "$path"
@@@ -119,10 -119,14 +119,10 @@@ test_expect_success 
      'echo ignore me >a/ignored &&
       echo ignored export-ignore >.git/info/attributes'
  
 -test_expect_success \
 -    'add files to repository' \
 -    'find a -type f | xargs git update-index --add &&
 -     find a -type l | xargs git update-index --add &&
 -     treeid=`git write-tree` &&
 -     echo $treeid >treeid &&
 -     git update-ref HEAD $(TZ=GMT GIT_COMMITTER_DATE="2005-05-27 22:00:00" \
 -     git commit-tree $treeid </dev/null)'
 +test_expect_success 'add files to repository' '
 +      git add a &&
 +      GIT_COMMITTER_DATE="2005-05-27 22:00" git commit -m initial
 +'
  
  test_expect_success 'setup export-subst' '
        echo "substfile?" export-subst >>.git/info/attributes &&
@@@ -160,7 -164,7 +160,7 @@@ check_tar with_olde-prefix olde
  test_expect_success 'git archive on large files' '
      test_config core.bigfilethreshold 1 &&
      git archive HEAD >b3.tar &&
 -    test_cmp b.tar b3.tar
 +    test_cmp_bin b.tar b3.tar
  '
  
  test_expect_success \
  
  test_expect_success \
      'git archive vs. the same in a bare repo' \
 -    'test_cmp b.tar b3.tar'
 +    'test_cmp_bin b.tar b3.tar'
  
  test_expect_success 'git archive with --output' \
      'git archive --output=b4.tar HEAD &&
 -    test_cmp b.tar b4.tar'
 +    test_cmp_bin b.tar b4.tar'
  
  test_expect_success 'git archive --remote' \
      'git archive --remote=. HEAD >b5.tar &&
 -    test_cmp b.tar b5.tar'
 +    test_cmp_bin b.tar b5.tar'
  
  test_expect_success \
      'validate file modification time' \
@@@ -194,7 -198,7 +194,7 @@@ test_expect_success 
  
  test_expect_success 'git archive with --output, override inferred format' '
        git archive --format=tar --output=d4.zip HEAD &&
 -      test_cmp b.tar d4.zip
 +      test_cmp_bin b.tar d4.zip
  '
  
  test_expect_success \
  
  test_expect_success 'clients cannot access unreachable commits' '
        test_commit unreachable &&
 -      sha1=`git rev-parse HEAD` &&
 +      sha1=$(git rev-parse HEAD) &&
        git reset --hard HEAD^ &&
        git archive $sha1 >remote.tar &&
        test_must_fail git archive --remote=. $sha1 >remote.tar
  '
  
 +test_expect_success 'upload-archive can allow unreachable commits' '
 +      test_commit unreachable1 &&
 +      sha1=$(git rev-parse HEAD) &&
 +      git reset --hard HEAD^ &&
 +      git archive $sha1 >remote.tar &&
 +      test_config uploadarchive.allowUnreachable true &&
 +      git archive --remote=. $sha1 >remote.tar
 +'
 +
  test_expect_success 'setup tar filters' '
        git config tar.tar.foo.command "tr ab ba" &&
        git config tar.bar.command "tr ab ba" &&
@@@ -240,34 -235,34 +240,34 @@@ test_expect_success 'archive --list sho
  test_expect_success 'invoke tar filter by format' '
        git archive --format=tar.foo HEAD >config.tar.foo &&
        tr ab ba <config.tar.foo >config.tar &&
 -      test_cmp b.tar config.tar &&
 +      test_cmp_bin b.tar config.tar &&
        git archive --format=bar HEAD >config.bar &&
        tr ab ba <config.bar >config.tar &&
 -      test_cmp b.tar config.tar
 +      test_cmp_bin b.tar config.tar
  '
  
  test_expect_success 'invoke tar filter by extension' '
        git archive -o config-implicit.tar.foo HEAD &&
 -      test_cmp config.tar.foo config-implicit.tar.foo &&
 +      test_cmp_bin config.tar.foo config-implicit.tar.foo &&
        git archive -o config-implicit.bar HEAD &&
 -      test_cmp config.tar.foo config-implicit.bar
 +      test_cmp_bin config.tar.foo config-implicit.bar
  '
  
  test_expect_success 'default output format remains tar' '
        git archive -o config-implicit.baz HEAD &&
 -      test_cmp b.tar config-implicit.baz
 +      test_cmp_bin b.tar config-implicit.baz
  '
  
  test_expect_success 'extension matching requires dot' '
        git archive -o config-implicittar.foo HEAD &&
 -      test_cmp b.tar config-implicittar.foo
 +      test_cmp_bin b.tar config-implicittar.foo
  '
  
  test_expect_success 'only enabled filters are available remotely' '
        test_must_fail git archive --remote=. --format=tar.foo HEAD \
                >remote.tar.foo &&
        git archive --remote=. --format=bar >remote.bar HEAD &&
 -      test_cmp remote.bar config.bar
 +      test_cmp_bin remote.bar config.bar
  '
  
  test_expect_success GZIP 'git archive --format=tgz' '
  
  test_expect_success GZIP 'git archive --format=tar.gz' '
        git archive --format=tar.gz HEAD >j1.tar.gz &&
 -      test_cmp j.tgz j1.tar.gz
 +      test_cmp_bin j.tgz j1.tar.gz
  '
  
  test_expect_success GZIP 'infer tgz from .tgz filename' '
        git archive --output=j2.tgz HEAD &&
 -      test_cmp j.tgz j2.tgz
 +      test_cmp_bin j.tgz j2.tgz
  '
  
  test_expect_success GZIP 'infer tgz from .tar.gz filename' '
        git archive --output=j3.tar.gz HEAD &&
 -      test_cmp j.tgz j3.tar.gz
 +      test_cmp_bin j.tgz j3.tar.gz
  '
  
  test_expect_success GZIP 'extract tgz file' '
        gzip -d -c <j.tgz >j.tar &&
 -      test_cmp b.tar j.tar
 +      test_cmp_bin b.tar j.tar
  '
  
  test_expect_success GZIP 'remote tar.gz is allowed by default' '
        git archive --remote=. --format=tar.gz HEAD >remote.tar.gz &&
 -      test_cmp j.tgz remote.tar.gz
 +      test_cmp_bin j.tgz remote.tar.gz
  '
  
  test_expect_success GZIP 'remote tar.gz can be disabled' '
                >remote.tar.gz
  '
  
+ test_expect_success 'archive and :(glob)' '
+       git archive -v HEAD -- ":(glob)**/sh" >/dev/null 2>actual &&
+       cat >expect <<EOF &&
+ a/
+ a/bin/
+ a/bin/sh
+ EOF
+       test_cmp expect actual
+ '
+ test_expect_success 'catch non-matching pathspec' '
+       test_must_fail git archive -v HEAD -- "*.abc" >/dev/null
+ '
  test_done