archive: improve support for running in subdirectory

When git archive is started in a subdirectory, it archives its corresponding tree and its child objects, only. That is intended. It does that by effectively cd'ing into that tree and setting "prefix" to the empty string. This has unfortunate consequences, though: Attributes are anchored at the root of the repository and git archive still applies them to subtrees, causing mismatches. And when checking pathspecs it cannot tell the difference between one that doesn't match anthing or one that matches some actual blob outside of the subdirectory, leading to a confusing error message. Fix that by keeping the "prefix" value and passing it to pathspec and attribute functions, and shortening it using relative_path() for paths written to the archive and (if --verbose is given) to stdout. Still reject attempts to archive files outside the current directory, but print a more specific error in that case. Recognizing it requires a full traversal of the subtree for each pathspec, however. Allowing them would be easier, but archive entry paths starting with "../" can be problematic to extract -- e.g. bsdtar skips them by default. Reported-by: Cristian Le <cristian.le@mpsd.mpg.de> Reported-by: Matthias Görgens <matthias.goergens@gmail.com> Signed-off-by: René Scharfe <l.s.r@web.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

René Scharfe committed Mar 24, 2023 at 23:27 UTC 92b1dd1b9e6e272c5115d6e37e19a892610a2686
3 files changed +83 -21
archive.c
+54 -21
@@ -166,6 +166,29 @@ static int write_archive_entry(const struct object_id *oid, const char *base,
166 args->convert = check_attr_export_subst(check);
167 }
168
169 + if (args->prefix) {
170 + static struct strbuf new_path = STRBUF_INIT;
171 + static struct strbuf buf = STRBUF_INIT;
172 + const char *rel;
173 +
174 + rel = relative_path(path_without_prefix, args->prefix, &buf);
175 +
176 + /*
177 + * We don't add an entry for the current working
178 + * directory when we are at the root; skip it also when
179 + * we're in a subdirectory or submodule. Skip entries
180 + * higher up as well.
181 + */
182 + if (!strcmp(rel, "./") || starts_with(rel, "../"))
183 + return S_ISDIR(mode) ? READ_TREE_RECURSIVE : 0;
184 +
185 + /* rel can refer to path, so don't edit it in place */
186 + strbuf_reset(&new_path);
187 + strbuf_add(&new_path, args->base, args->baselen);
188 + strbuf_addstr(&new_path, rel);
189 + strbuf_swap(&path, &new_path);
190 + }
191 +
192 if (args->verbose)
193 fprintf(stderr, "%.*s\n", (int)path.len, path.buf);
194
@@ -401,6 +424,27 @@ static int reject_entry(const struct object_id *oid UNUSED,
424 return ret;
425 }
426
427 +static int reject_outside(const struct object_id *oid UNUSED,
428 + struct strbuf *base, const char *filename,
429 + unsigned mode, void *context)
430 +{
431 + struct archiver_args *args = context;
432 + struct strbuf buf = STRBUF_INIT;
433 + struct strbuf path = STRBUF_INIT;
434 + int ret = 0;
435 +
436 + if (S_ISDIR(mode))
437 + return READ_TREE_RECURSIVE;
438 +
439 + strbuf_addbuf(&path, base);
440 + strbuf_addstr(&path, filename);
441 + if (starts_with(relative_path(path.buf, args->prefix, &buf), "../"))
442 + ret = -1;
443 + strbuf_release(&buf);
444 + strbuf_release(&path);
445 + return ret;
446 +}
447 +
448 static int path_exists(struct archiver_args *args, const char *path)
449 {
450 const char *paths[] = { path, NULL };
@@ -408,8 +452,13 @@ static int path_exists(struct archiver_args *args, const char *path)
452 int ret;
453
454 ctx.args = args;
411 - parse_pathspec(&ctx.pathspec, 0, 0, "", paths);
455 + parse_pathspec(&ctx.pathspec, 0, PATHSPEC_PREFER_CWD,
456 + args->prefix, paths);
457 ctx.pathspec.recursive = 1;
458 + if (args->prefix && read_tree(args->repo, args->tree, &ctx.pathspec,
459 + reject_outside, args))
460 + die(_("pathspec '%s' matches files outside the "
461 + "current directory"), path);
462 ret = read_tree(args->repo, args->tree,
463 &ctx.pathspec,
464 reject_entry, &ctx);
@@ -425,9 +474,8 @@ static void parse_pathspec_arg(const char **pathspec,
474 * Also if pathspec patterns are dependent, we're in big
475 * trouble as we test each one separately
476 */
428 - parse_pathspec(&ar_args->pathspec, 0,
429 - PATHSPEC_PREFER_FULL,
430 - "", pathspec);
477 + parse_pathspec(&ar_args->pathspec, 0, PATHSPEC_PREFER_CWD,
478 + ar_args->prefix, pathspec);
479 ar_args->pathspec.recursive = 1;
480 if (pathspec) {
481 while (*pathspec) {
@@ -439,8 +487,7 @@ static void parse_pathspec_arg(const char **pathspec,
487 }
488
489 static void parse_treeish_arg(const char **argv,
442 - struct archiver_args *ar_args, const char *prefix,
443 - int remote)
490 + struct archiver_args *ar_args, int remote)
491 {
492 const char *name = argv[0];
493 const struct object_id *commit_oid;
@@ -479,20 +526,6 @@ static void parse_treeish_arg(const char **argv,
526 if (!tree)
527 die(_("not a tree object: %s"), oid_to_hex(&oid));
528
482 - if (prefix) {
483 - struct object_id tree_oid;
484 - unsigned short mode;
485 - int err;
486 -
487 - err = get_tree_entry(ar_args->repo,
488 - &tree->object.oid,
489 - prefix, &tree_oid,
490 - &mode);
491 - if (err || !S_ISDIR(mode))
492 - die(_("current working directory is untracked"));
493 -
494 - tree = parse_tree_indirect(&tree_oid);
495 - }
529 ar_args->refname = ref;
530 ar_args->tree = tree;
531 ar_args->commit_oid = commit_oid;
@@ -710,7 +743,7 @@ int write_archive(int argc, const char **argv, const char *prefix,
743 setup_git_directory();
744 }
745
713 - parse_treeish_arg(argv, &args, prefix, remote);
746 + parse_treeish_arg(argv, &args, remote);
747 parse_pathspec_arg(argv + 1, &args);
748
749 rc = ar->write_archive(ar, &args);
t/t5000-tar-tree.sh
+13
@@ -433,6 +433,19 @@ test_expect_success 'catch non-matching pathspec' '
433 test_must_fail git archive -v HEAD -- "*.abc" >/dev/null
434 '
435
436 +test_expect_success 'reject paths outside the current directory' '
437 + test_must_fail git -C a/bin archive HEAD .. >/dev/null 2>err &&
438 + grep "outside the current directory" err
439 +'
440 +
441 +test_expect_success 'allow pathspecs that resolve to the current directory' '
442 + git -C a/bin archive -v HEAD ../bin >/dev/null 2>actual &&
443 + cat >expect <<-\EOF &&
444 + sh
445 + EOF
446 + test_cmp expect actual
447 +'
448 +
449 # Pull the size and date of each entry in a tarfile using the system tar.
450 #
451 # We'll pull out only the year from the date; that avoids any question of
t/t5001-archive-attr.sh
+16
@@ -33,6 +33,13 @@ test_expect_success 'setup' '
33 echo ignored-by-tree.d export-ignore >>.gitattributes &&
34 git add ignored-by-tree ignored-by-tree.d .gitattributes &&
35
36 + mkdir subdir &&
37 + >subdir/included &&
38 + >subdir/ignored-by-subtree &&
39 + >subdir/ignored-by-tree &&
40 + echo ignored-by-subtree export-ignore >subdir/.gitattributes &&
41 + git add subdir &&
42 +
43 echo ignored by worktree >ignored-by-worktree &&
44 echo ignored-by-worktree export-ignore >.gitattributes &&
45 git add ignored-by-worktree &&
@@ -93,6 +100,15 @@ test_expect_exists archive-pathspec-wildcard/ignored-by-worktree
100 test_expect_missing archive-pathspec-wildcard/excluded-by-pathspec.d
101 test_expect_missing archive-pathspec-wildcard/excluded-by-pathspec.d/file
102
103 +test_expect_success 'git -C subdir archive' '
104 + git -C subdir archive HEAD >archive-subdir.tar &&
105 + extract_tar_to_dir archive-subdir
106 +'
107 +
108 +test_expect_exists archive-subdir/included
109 +test_expect_missing archive-subdir/ignored-by-subtree
110 +test_expect_missing archive-subdir/ignored-by-tree
111 +
112 test_expect_success 'git archive with worktree attributes' '
113 git archive --worktree-attributes HEAD >worktree.tar &&
114 (mkdir worktree && cd worktree && "$TAR" xf -) <worktree.tar