archive: implement configurable tar filters

It's common to pipe the tar output produce by "git archive" through gzip or some other compressor. Locally, this can easily be done by using a shell pipe. When requesting a remote archive, though, it cannot be done through the upload-archive interface. This patch allows configurable tar filters, so that one could define a "tar.gz" format that automatically pipes tar output through gzip. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff King committed Jun 21, 2011 at 21:26 UTC 767cf4579f0e34a3cfc6704d5c313842321dfafa
3 files changed +165 -1
Documentation/git-archive.txt
+16
@@ -101,6 +101,16 @@ tar.umask::
101 details. If `--remote` is used then only the configuration of
102 the remote repository takes effect.
103
104 +tar.<format>.command::
105 + This variable specifies a shell command through which the tar
106 + output generated by `git archive` should be piped. The command
107 + is executed using the shell with the generated tar file on its
108 + standard input, and should produce the final output on its
109 + standard output. Any compression-level options will be passed
110 + to the command (e.g., "-9"). An output file with the same
111 + extension as `<format>` will be use this format if no other
112 + format is given.
113 +
114 ATTRIBUTES
115 ----------
116
@@ -149,6 +159,12 @@ git archive -o latest.zip HEAD::
159 commit on the current branch. Note that the output format is
160 inferred by the extension of the output file.
161
162 +git config tar.tar.xz.command "xz -c"::
163 +
164 + Configure a "tar.xz" format for making LZMA-compressed tarfiles.
165 + You can use it specifying `--format=tar.xz`, or by creating an
166 + output file like `-o foo.tar.xz`.
167 +
168
169 SEE ALSO
170 --------
archive-tar.c
+106 -1
@@ -4,6 +4,7 @@
4 #include "cache.h"
5 #include "tar.h"
6 #include "archive.h"
7 +#include "run-command.h"
8
9 #define RECORDSIZE (512)
10 #define BLOCKSIZE (RECORDSIZE * 20)
@@ -13,6 +14,9 @@ static unsigned long offset;
14
15 static int tar_umask = 002;
16
17 +static int write_tar_filter_archive(const struct archiver *ar,
18 + struct archiver_args *args);
19 +
20 /* writes out the whole block, but only if it is full */
21 static void write_if_needed(void)
22 {
@@ -220,6 +224,60 @@ static int write_global_extended_header(struct archiver_args *args)
224 return err;
225 }
226
227 +static struct archiver **tar_filters;
228 +static int nr_tar_filters;
229 +static int alloc_tar_filters;
230 +
231 +static struct archiver *find_tar_filter(const char *name, int len)
232 +{
233 + int i;
234 + for (i = 0; i < nr_tar_filters; i++) {
235 + struct archiver *ar = tar_filters[i];
236 + if (!strncmp(ar->name, name, len) && !ar->name[len])
237 + return ar;
238 + }
239 + return NULL;
240 +}
241 +
242 +static int tar_filter_config(const char *var, const char *value, void *data)
243 +{
244 + struct archiver *ar;
245 + const char *dot;
246 + const char *name;
247 + const char *type;
248 + int namelen;
249 +
250 + if (prefixcmp(var, "tar."))
251 + return 0;
252 + dot = strrchr(var, '.');
253 + if (dot == var + 9)
254 + return 0;
255 +
256 + name = var + 4;
257 + namelen = dot - name;
258 + type = dot + 1;
259 +
260 + ar = find_tar_filter(name, namelen);
261 + if (!ar) {
262 + ar = xcalloc(1, sizeof(*ar));
263 + ar->name = xmemdupz(name, namelen);
264 + ar->write_archive = write_tar_filter_archive;
265 + ar->flags = ARCHIVER_WANT_COMPRESSION_LEVELS;
266 + ALLOC_GROW(tar_filters, nr_tar_filters + 1, alloc_tar_filters);
267 + tar_filters[nr_tar_filters++] = ar;
268 + }
269 +
270 + if (!strcmp(type, "command")) {
271 + if (!value)
272 + return config_error_nonbool(var);
273 + free(ar->data);
274 + ar->data = xstrdup(value);
275 + return 0;
276 + }
277 +
278 + return 0;
279 +}
280 +
281 static int git_tar_config(const char *var, const char *value, void *cb)
282 {
283 if (!strcmp(var, "tar.umask")) {
@@ -231,7 +289,8 @@ static int git_tar_config(const char *var, const char *value, void *cb)
289 }
290 return 0;
291 }
234 - return 0;
292 +
293 + return tar_filter_config(var, value, cb);
294 }
295
296 static int write_tar_archive(const struct archiver *ar,
@@ -248,6 +307,45 @@ static int write_tar_archive(const struct archiver *ar,
307 return err;
308 }
309
310 +static int write_tar_filter_archive(const struct archiver *ar,
311 + struct archiver_args *args)
312 +{
313 + struct strbuf cmd = STRBUF_INIT;
314 + struct child_process filter;
315 + const char *argv[2];
316 + int r;
317 +
318 + if (!ar->data)
319 + die("BUG: tar-filter archiver called with no filter defined");
320 +
321 + strbuf_addstr(&cmd, ar->data);
322 + if (args->compression_level >= 0)
323 + strbuf_addf(&cmd, " -%d", args->compression_level);
324 +
325 + memset(&filter, 0, sizeof(filter));
326 + argv[0] = cmd.buf;
327 + argv[1] = NULL;
328 + filter.argv = argv;
329 + filter.use_shell = 1;
330 + filter.in = -1;
331 +
332 + if (start_command(&filter) < 0)
333 + die_errno("unable to start '%s' filter", argv[0]);
334 + close(1);
335 + if (dup2(filter.in, 1) < 0)
336 + die_errno("unable to redirect descriptor");
337 + close(filter.in);
338 +
339 + r = write_tar_archive(ar, args);
340 +
341 + close(1);
342 + if (finish_command(&filter) != 0)
343 + die("'%s' filter reported error", argv[0]);
344 +
345 + strbuf_release(&cmd);
346 + return r;
347 +}
348 +
349 static struct archiver tar_archiver = {
350 "tar",
351 write_tar_archive,
@@ -256,6 +354,13 @@ static struct archiver tar_archiver = {
354
355 void init_tar_archiver(void)
356 {
357 + int i;
358 register_archiver(&tar_archiver);
359 +
360 git_config(git_tar_config, NULL);
361 + for (i = 0; i < nr_tar_filters; i++) {
362 + /* omit any filters that never had a command configured */
363 + if (tar_filters[i]->data)
364 + register_archiver(tar_filters[i]);
365 + }
366 }
t/t5000-tar-tree.sh
+43
@@ -252,4 +252,47 @@ test_expect_success 'git-archive --prefix=olde-' '
252 test -f h/olde-a/bin/sh
253 '
254
255 +test_expect_success 'setup tar filters' '
256 + git config tar.tar.foo.command "tr ab ba" &&
257 + git config tar.bar.command "tr ab ba"
258 +'
259 +
260 +test_expect_success 'archive --list mentions user filter' '
261 + git archive --list >output &&
262 + grep "^tar\.foo\$" output &&
263 + grep "^bar\$" output
264 +'
265 +
266 +test_expect_success 'archive --list shows remote user filters' '
267 + git archive --list --remote=. >output &&
268 + grep "^tar\.foo\$" output &&
269 + grep "^bar\$" output
270 +'
271 +
272 +test_expect_success 'invoke tar filter by format' '
273 + git archive --format=tar.foo HEAD >config.tar.foo &&
274 + tr ab ba <config.tar.foo >config.tar &&
275 + test_cmp b.tar config.tar &&
276 + git archive --format=bar HEAD >config.bar &&
277 + tr ab ba <config.bar >config.tar &&
278 + test_cmp b.tar config.tar
279 +'
280 +
281 +test_expect_success 'invoke tar filter by extension' '
282 + git archive -o config-implicit.tar.foo HEAD &&
283 + test_cmp config.tar.foo config-implicit.tar.foo &&
284 + git archive -o config-implicit.bar HEAD &&
285 + test_cmp config.tar.foo config-implicit.bar
286 +'
287 +
288 +test_expect_success 'default output format remains tar' '
289 + git archive -o config-implicit.baz HEAD &&
290 + test_cmp b.tar config-implicit.baz
291 +'
292 +
293 +test_expect_success 'extension matching requires dot' '
294 + git archive -o config-implicittar.foo HEAD &&
295 + test_cmp b.tar config-implicittar.foo
296 +'
297 +
298 test_done