cat-file: add --follow-symlinks to --batch

This wires the in-repo-symlink following code through to the cat-file builtin. In the event of an out-of-repo link, cat-file will print the link in a new format. Signed-off-by: David Turner <dturner@twopensource.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

David Turner committed May 20, 2015 at 13:03 UTC 122d53464b29d3ac20891c5ee2f75ac5ecbb7b20
3 files changed +348 -7
Documentation/git-cat-file.txt
+98 -1
@@ -10,7 +10,7 @@ SYNOPSIS
10 --------
11 [verse]
12 'git cat-file' (-t | -s | -e | -p | <type> | --textconv ) <object>
13 -'git cat-file' (--batch | --batch-check) < <list-of-objects>
13 +'git cat-file' (--batch | --batch-check) [--follow-symlinks] < <list-of-objects>
14
15 DESCRIPTION
16 -----------
@@ -69,6 +69,62 @@ OPTIONS
69 not be combined with any other options or arguments. See the
70 section `BATCH OUTPUT` below for details.
71
72 +--follow-symlinks::
73 + With --batch or --batch-check, follow symlinks inside the
74 + repository when requesting objects with extended SHA-1
75 + expressions of the form tree-ish:path-in-tree. Instead of
76 + providing output about the link itself, provide output about
77 + the linked-to object. If a symlink points outside the
78 + tree-ish (e.g. a link to /foo or a root-level link to ../foo),
79 + the portion of the link which is outside the tree will be
80 + printed.
81 ++
82 +This option does not (currently) work correctly when an object in the
83 +index is specified (e.g. `:link` instead of `HEAD:link`) rather than
84 +one in the tree.
85 ++
86 +This option cannot (currently) be used unless `--batch` or
87 +`--batch-check` is used.
88 ++
89 +For example, consider a git repository containing:
90 ++
91 +--
92 + f: a file containing "hello\n"
93 + link: a symlink to f
94 + dir/link: a symlink to ../f
95 + plink: a symlink to ../f
96 + alink: a symlink to /etc/passwd
97 +--
98 ++
99 +For a regular file `f`, `echo HEAD:f | git cat-file --batch` would print
100 ++
101 +--
102 + ce013625030ba8dba906f756967f9e9ca394464a blob 6
103 +--
104 ++
105 +And `echo HEAD:link | git cat-file --batch --follow-symlinks` would
106 +print the same thing, as would `HEAD:dir/link`, as they both point at
107 +`HEAD:f`.
108 ++
109 +Without `--follow-symlinks`, these would print data about the symlink
110 +itself. In the case of `HEAD:link`, you would see
111 ++
112 +--
113 + 4d1ae35ba2c8ec712fa2a379db44ad639ca277bd blob 1
114 +--
115 ++
116 +Both `plink` and `alink` point outside the tree, so they would
117 +respectively print:
118 ++
119 +--
120 + symlink 4
121 + ../f
122 +
123 + symlink 11
124 + /etc/passwd
125 +--
126 +
127 +
128 OUTPUT
129 ------
130 If '-t' is specified, one of the <type>.
@@ -148,6 +204,47 @@ the repository, then `cat-file` will ignore any custom format and print:
204 <object> SP missing LF
205 ------------
206
207 +If --follow-symlinks is used, and a symlink in the repository points
208 +outside the repository, then `cat-file` will ignore any custom format
209 +and print:
210 +
211 +------------
212 +symlink SP <size> LF
213 +<symlink> LF
214 +------------
215 +
216 +The symlink will either be absolute (beginning with a /), or relative
217 +to the tree root. For instance, if dir/link points to ../../foo, then
218 +<symlink> will be ../foo. <size> is the size of the symlink in bytes.
219 +
220 +If --follow-symlinks is used, the following error messages will be
221 +displayed:
222 +
223 +------------
224 +<object> SP missing LF
225 +------------
226 +is printed when the initial symlink requested does not exist.
227 +
228 +------------
229 +dangling SP <size> LF
230 +<object> LF
231 +------------
232 +is printed when the initial symlink exists, but something that
233 +it (transitive-of) points to does not.
234 +
235 +------------
236 +loop SP <size> LF
237 +<object> LF
238 +------------
239 +is printed for symlink loops (or any symlinks that
240 +require more than 40 link resolutions to resolve).
241 +
242 +------------
243 +notdir SP <size> LF
244 +<object> LF
245 +------------
246 +is printed when, during symlink resolution, a file is used as a
247 +directory name.
248
249 CAVEATS
250 -------
builtin/cat-file.c
+45 -6
@@ -8,6 +8,7 @@
8 #include "parse-options.h"
9 #include "userdiff.h"
10 #include "streaming.h"
11 +#include "tree-walk.h"
12
13 static int cat_one_file(int opt, const char *exp_type, const char *obj_name)
14 {
@@ -224,6 +225,7 @@ static void print_object_or_die(int fd, struct expand_data *data)
225
226 struct batch_options {
227 int enabled;
228 + int follow_symlinks;
229 int print_contents;
230 const char *format;
231 };
@@ -232,12 +234,44 @@ static int batch_one_object(const char *obj_name, struct batch_options *opt,
234 struct expand_data *data)
235 {
236 struct strbuf buf = STRBUF_INIT;
237 + struct object_context ctx;
238 + int flags = opt->follow_symlinks ? GET_SHA1_FOLLOW_SYMLINKS : 0;
239 + enum follow_symlinks_result result;
240
241 if (!obj_name)
242 return 1;
243
239 - if (get_sha1(obj_name, data->sha1)) {
240 - printf("%s missing\n", obj_name);
244 + result = get_sha1_with_context(obj_name, flags, data->sha1, &ctx);
245 + if (result != FOUND) {
246 + switch (result) {
247 + case MISSING_OBJECT:
248 + printf("%s missing\n", obj_name);
249 + break;
250 + case DANGLING_SYMLINK:
251 + printf("dangling %"PRIuMAX"\n%s\n",
252 + (uintmax_t)strlen(obj_name), obj_name);
253 + break;
254 + case SYMLINK_LOOP:
255 + printf("loop %"PRIuMAX"\n%s\n",
256 + (uintmax_t)strlen(obj_name), obj_name);
257 + break;
258 + case NOT_DIR:
259 + printf("notdir %"PRIuMAX"\n%s\n",
260 + (uintmax_t)strlen(obj_name), obj_name);
261 + break;
262 + default:
263 + die("BUG: unknown get_sha1_with_context result %d\n",
264 + result);
265 + break;
266 + }
267 + fflush(stdout);
268 + return 0;
269 + }
270 +
271 + if (ctx.mode == 0) {
272 + printf("symlink %"PRIuMAX"\n%s\n",
273 + (uintmax_t)ctx.symlink_path.len,
274 + ctx.symlink_path.buf);
275 fflush(stdout);
276 return 0;
277 }
@@ -324,7 +358,7 @@ static int batch_objects(struct batch_options *opt)
358
359 static const char * const cat_file_usage[] = {
360 N_("git cat-file (-t | -s | -e | -p | <type> | --textconv) <object>"),
327 - N_("git cat-file (--batch | --batch-check) < <list-of-objects>"),
361 + N_("git cat-file (--batch | --batch-check) [--follow-symlinks] < <list-of-objects>"),
362 NULL
363 };
364
@@ -342,9 +376,8 @@ static int batch_option_callback(const struct option *opt,
376 {
377 struct batch_options *bo = opt->value;
378
345 - if (unset) {
346 - memset(bo, 0, sizeof(*bo));
347 - return 0;
379 + if (bo->enabled) {
380 + return 1;
381 }
382
383 bo->enabled = 1;
@@ -375,6 +408,8 @@ int cmd_cat_file(int argc, const char **argv, const char *prefix)
408 { OPTION_CALLBACK, 0, "batch-check", &batch, "format",
409 N_("show info about objects fed from the standard input"),
410 PARSE_OPT_OPTARG, batch_option_callback },
411 + OPT_BOOL(0, "follow-symlinks", &batch.follow_symlinks,
412 + N_("follow in-tree symlinks (used with --batch or --batch-check)")),
413 OPT_END()
414 };
415
@@ -402,6 +437,10 @@ int cmd_cat_file(int argc, const char **argv, const char *prefix)
437 usage_with_options(cat_file_usage, options);
438 }
439
440 + if (batch.follow_symlinks && !batch.enabled) {
441 + usage_with_options(cat_file_usage, options);
442 + }
443 +
444 if (batch.enabled)
445 return batch_objects(&batch);
446
t/t1006-cat-file.sh
+205
@@ -189,6 +189,13 @@ do
189 '
190 done
191
192 +for opt in t s e p
193 +do
194 + test_expect_success "Passing -$opt with --follow-symlinks fails" '
195 + test_must_fail git cat-file --follow-symlinks -$opt $hello_sha1
196 + '
197 +done
198 +
199 test_expect_success "--batch-check for a non-existent named object" '
200 test "foobar42 missing
201 foobar84 missing" = \
@@ -296,4 +303,202 @@ test_expect_success '%(deltabase) reports packed delta bases' '
303 }
304 '
305
306 +# Tests for git cat-file --follow-symlinks
307 +test_expect_success 'prep for symlink tests' '
308 + echo_without_newline "$hello_content" >morx &&
309 + test_ln_s_add morx same-dir-link &&
310 + test_ln_s_add dir link-to-dir &&
311 + test_ln_s_add ../fleem out-of-repo-link &&
312 + test_ln_s_add .. out-of-repo-link-dir &&
313 + test_ln_s_add same-dir-link link-to-link &&
314 + test_ln_s_add nope broken-same-dir-link &&
315 + mkdir dir &&
316 + test_ln_s_add ../morx dir/parent-dir-link &&
317 + test_ln_s_add .. dir/link-dir &&
318 + test_ln_s_add ../../escape dir/out-of-repo-link &&
319 + test_ln_s_add ../.. dir/out-of-repo-link-dir &&
320 + test_ln_s_add nope dir/broken-link-in-dir &&
321 + mkdir dir/subdir &&
322 + test_ln_s_add ../../morx dir/subdir/grandparent-dir-link &&
323 + test_ln_s_add ../../../great-escape dir/subdir/out-of-repo-link &&
324 + test_ln_s_add ../../.. dir/subdir/out-of-repo-link-dir &&
325 + test_ln_s_add ../../../ dir/subdir/out-of-repo-link-dir-trailing &&
326 + test_ln_s_add ../parent-dir-link dir/subdir/parent-dir-link-to-link &&
327 + echo_without_newline "$hello_content" >dir/subdir/ind2 &&
328 + echo_without_newline "$hello_content" >dir/ind1 &&
329 + test_ln_s_add dir dirlink &&
330 + test_ln_s_add dir/subdir subdirlink &&
331 + test_ln_s_add subdir/ind2 dir/link-to-child &&
332 + test_ln_s_add dir/link-to-child link-to-down-link &&
333 + test_ln_s_add dir/.. up-down &&
334 + test_ln_s_add dir/../ up-down-trailing &&
335 + test_ln_s_add dir/../morx up-down-file &&
336 + test_ln_s_add dir/../../morx up-up-down-file &&
337 + test_ln_s_add subdirlink/../../morx up-two-down-file &&
338 + test_ln_s_add loop1 loop2 &&
339 + test_ln_s_add loop2 loop1 &&
340 + git add morx dir/subdir/ind2 dir/ind1 &&
341 + git commit -am "test" &&
342 + echo $hello_sha1 blob $hello_size >found
343 +'
344 +
345 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for non-links' '
346 + echo HEAD:morx | git cat-file --batch-check --follow-symlinks >actual &&
347 + test_cmp found actual &&
348 + echo HEAD:nope missing >expect &&
349 + echo HEAD:nope | git cat-file --batch-check --follow-symlinks >actual &&
350 + test_cmp expect actual
351 +'
352 +
353 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for in-repo, same-dir links' '
354 + echo HEAD:same-dir-link | git cat-file --batch-check --follow-symlinks >actual &&
355 + test_cmp found actual
356 +'
357 +
358 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for in-repo, links to dirs' '
359 + echo HEAD:link-to-dir/ind1 | git cat-file --batch-check --follow-symlinks >actual &&
360 + test_cmp found actual
361 +'
362 +
363 +
364 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for broken in-repo, same-dir links' '
365 + echo dangling 25 >expect &&
366 + echo HEAD:broken-same-dir-link >>expect &&
367 + echo HEAD:broken-same-dir-link | git cat-file --batch-check --follow-symlinks >actual &&
368 + test_cmp expect actual
369 +'
370 +
371 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for same-dir links-to-links' '
372 + echo HEAD:link-to-link | git cat-file --batch-check --follow-symlinks >actual &&
373 + test_cmp found actual
374 +'
375 +
376 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for parent-dir links' '
377 + echo HEAD:dir/parent-dir-link | git cat-file --batch-check --follow-symlinks >actual &&
378 + test_cmp found actual &&
379 + echo notdir 29 >expect &&
380 + echo HEAD:dir/parent-dir-link/nope >>expect &&
381 + echo HEAD:dir/parent-dir-link/nope | git cat-file --batch-check --follow-symlinks >actual &&
382 + test_cmp expect actual
383 +'
384 +
385 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for .. links' '
386 + echo dangling 22 >expect &&
387 + echo HEAD:dir/link-dir/nope >>expect &&
388 + echo HEAD:dir/link-dir/nope | git cat-file --batch-check --follow-symlinks >actual &&
389 + test_cmp expect actual &&
390 + echo HEAD:dir/link-dir/morx | git cat-file --batch-check --follow-symlinks >actual &&
391 + test_cmp found actual &&
392 + echo dangling 27 >expect &&
393 + echo HEAD:dir/broken-link-in-dir >>expect &&
394 + echo HEAD:dir/broken-link-in-dir | git cat-file --batch-check --follow-symlinks >actual &&
395 + test_cmp expect actual
396 +'
397 +
398 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for ../.. links' '
399 + echo notdir 41 >expect &&
400 + echo HEAD:dir/subdir/grandparent-dir-link/nope >>expect &&
401 + echo HEAD:dir/subdir/grandparent-dir-link/nope | git cat-file --batch-check --follow-symlinks >actual &&
402 + test_cmp expect actual &&
403 + echo HEAD:dir/subdir/grandparent-dir-link | git cat-file --batch-check --follow-symlinks >actual &&
404 + test_cmp found actual &&
405 + echo HEAD:dir/subdir/parent-dir-link-to-link | git cat-file --batch-check --follow-symlinks >actual &&
406 + test_cmp found actual
407 +'
408 +
409 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for dir/ links' '
410 + echo dangling 17 >expect &&
411 + echo HEAD:dirlink/morx >>expect &&
412 + echo HEAD:dirlink/morx | git cat-file --batch-check --follow-symlinks >actual &&
413 + test_cmp expect actual &&
414 + echo $hello_sha1 blob $hello_size >expect &&
415 + echo HEAD:dirlink/ind1 | git cat-file --batch-check --follow-symlinks >actual &&
416 + test_cmp expect actual
417 +'
418 +
419 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for dir/subdir links' '
420 + echo dangling 20 >expect &&
421 + echo HEAD:subdirlink/morx >>expect &&
422 + echo HEAD:subdirlink/morx | git cat-file --batch-check --follow-symlinks >actual &&
423 + test_cmp expect actual &&
424 + echo HEAD:subdirlink/ind2 | git cat-file --batch-check --follow-symlinks >actual &&
425 + test_cmp found actual
426 +'
427 +
428 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for dir ->subdir links' '
429 + echo notdir 27 >expect &&
430 + echo HEAD:dir/link-to-child/morx >>expect &&
431 + echo HEAD:dir/link-to-child/morx | git cat-file --batch-check --follow-symlinks >actual &&
432 + test_cmp expect actual &&
433 + echo HEAD:dir/link-to-child | git cat-file --batch-check --follow-symlinks >actual &&
434 + test_cmp found actual &&
435 + echo HEAD:link-to-down-link | git cat-file --batch-check --follow-symlinks >actual &&
436 + test_cmp found actual
437 +'
438 +
439 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-of-repo symlinks' '
440 + echo symlink 8 >expect &&
441 + echo ../fleem >>expect &&
442 + echo HEAD:out-of-repo-link | git cat-file --batch-check --follow-symlinks >actual &&
443 + test_cmp expect actual &&
444 + echo symlink 2 >expect &&
445 + echo .. >>expect &&
446 + echo HEAD:out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual &&
447 + test_cmp expect actual
448 +'
449 +
450 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-of-repo symlinks in dirs' '
451 + echo symlink 9 >expect &&
452 + echo ../escape >>expect &&
453 + echo HEAD:dir/out-of-repo-link | git cat-file --batch-check --follow-symlinks >actual &&
454 + test_cmp expect actual &&
455 + echo symlink 2 >expect &&
456 + echo .. >>expect &&
457 + echo HEAD:dir/out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual &&
458 + test_cmp expect actual
459 +'
460 +
461 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-of-repo symlinks in subdirs' '
462 + echo symlink 15 >expect &&
463 + echo ../great-escape >>expect &&
464 + echo HEAD:dir/subdir/out-of-repo-link | git cat-file --batch-check --follow-symlinks >actual &&
465 + test_cmp expect actual &&
466 + echo symlink 2 >expect &&
467 + echo .. >>expect &&
468 + echo HEAD:dir/subdir/out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual &&
469 + test_cmp expect actual &&
470 + echo symlink 3 >expect &&
471 + echo ../ >>expect &&
472 + echo HEAD:dir/subdir/out-of-repo-link-dir-trailing | git cat-file --batch-check --follow-symlinks >actual &&
473 + test_cmp expect actual
474 +'
475 +
476 +test_expect_success 'git cat-file --batch-check --follow-symlinks works for symlinks with internal ..' '
477 + echo HEAD: | git cat-file --batch-check >expect &&
478 + echo HEAD:up-down | git cat-file --batch-check --follow-symlinks >actual &&
479 + test_cmp expect actual &&
480 + echo HEAD:up-down-trailing | git cat-file --batch-check --follow-symlinks >actual &&
481 + test_cmp expect actual &&
482 + echo HEAD:up-down-file | git cat-file --batch-check --follow-symlinks >actual &&
483 + test_cmp found actual &&
484 + echo symlink 7 >expect &&
485 + echo ../morx >>expect &&
486 + echo HEAD:up-up-down-file | git cat-file --batch-check --follow-symlinks >actual &&
487 + test_cmp expect actual &&
488 + echo HEAD:up-two-down-file | git cat-file --batch-check --follow-symlinks >actual &&
489 + test_cmp found actual
490 +'
491 +
492 +test_expect_success 'git cat-file --batch-check --follow-symlink breaks loops' '
493 + echo loop 10 >expect &&
494 + echo HEAD:loop1 >>expect &&
495 + echo HEAD:loop1 | git cat-file --batch-check --follow-symlinks >actual &&
496 + test_cmp expect actual
497 +'
498 +
499 +test_expect_success 'git cat-file --batch --follow-symlink returns correct sha and mode' '
500 + echo HEAD:morx | git cat-file --batch >expect &&
501 + echo HEAD:morx | git cat-file --batch --follow-symlinks >actual &&
502 + test_cmp expect actual
503 +'
504 test_done