Merge branch 'dt/cat-file-follow-symlinks'
authorJunio C Hamano <gitster@pobox.com>
Mon, 1 Jun 2015 19:45:16 +0000 (12:45 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 1 Jun 2015 19:45:16 +0000 (12:45 -0700)
"git cat-file --batch(-check)" learned the "--follow-symlinks"
option that follows an in-tree symbolic link when asked about an
object via extended SHA-1 syntax, e.g. HEAD:RelNotes that points at
Documentation/RelNotes/2.5.0.txt. With the new option, the command
behaves as if HEAD:Documentation/RelNotes/2.5.0.txt was given as
input instead.

* dt/cat-file-follow-symlinks:
cat-file: add --follow-symlinks to --batch
sha1_name: get_sha1_with_context learns to follow symlinks
tree-walk: learn get_tree_entry_follow_symlinks

Documentation/git-cat-file.txt
builtin/cat-file.c
cache.h
sha1_name.c
t/t1006-cat-file.sh
tree-walk.c
tree-walk.h
index 499ae7b98a4d9bf48aa2c5a235608dfcffcc1c10..319ab4cb086874da5e2f153d69d8f9af1f18461e 100644 (file)
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [verse]
 'git cat-file' (-t [--allow-unknown-type]| -s [--allow-unknown-type]| -e | -p | <type> | --textconv ) <object>
-'git cat-file' (--batch | --batch-check) < <list-of-objects>
+'git cat-file' (--batch | --batch-check) [--follow-symlinks] < <list-of-objects>
 
 DESCRIPTION
 -----------
@@ -72,6 +72,62 @@ OPTIONS
 --allow-unknown-type::
        Allow -s or -t to query broken/corrupt objects of unknown type.
 
+--follow-symlinks::
+       With --batch or --batch-check, follow symlinks inside the
+       repository when requesting objects with extended SHA-1
+       expressions of the form tree-ish:path-in-tree.  Instead of
+       providing output about the link itself, provide output about
+       the linked-to object.  If a symlink points outside the
+       tree-ish (e.g. a link to /foo or a root-level link to ../foo),
+       the portion of the link which is outside the tree will be
+       printed.
++
+This option does not (currently) work correctly when an object in the
+index is specified (e.g. `:link` instead of `HEAD:link`) rather than
+one in the tree.
++
+This option cannot (currently) be used unless `--batch` or
+`--batch-check` is used.
++
+For example, consider a git repository containing:
++
+--
+       f: a file containing "hello\n"
+       link: a symlink to f
+       dir/link: a symlink to ../f
+       plink: a symlink to ../f
+       alink: a symlink to /etc/passwd
+--
++
+For a regular file `f`, `echo HEAD:f | git cat-file --batch` would print
++
+--
+       ce013625030ba8dba906f756967f9e9ca394464a blob 6
+--
++
+And `echo HEAD:link | git cat-file --batch --follow-symlinks` would
+print the same thing, as would `HEAD:dir/link`, as they both point at
+`HEAD:f`.
++
+Without `--follow-symlinks`, these would print data about the symlink
+itself.  In the case of `HEAD:link`, you would see
++
+--
+       4d1ae35ba2c8ec712fa2a379db44ad639ca277bd blob 1
+--
++
+Both `plink` and `alink` point outside the tree, so they would
+respectively print:
++
+--
+       symlink 4
+       ../f
+
+       symlink 11
+       /etc/passwd
+--
+
+
 OUTPUT
 ------
 If '-t' is specified, one of the <type>.
@@ -151,6 +207,47 @@ the repository, then `cat-file` will ignore any custom format and print:
 <object> SP missing LF
 ------------
 
+If --follow-symlinks is used, and a symlink in the repository points
+outside the repository, then `cat-file` will ignore any custom format
+and print:
+
+------------
+symlink SP <size> LF
+<symlink> LF
+------------
+
+The symlink will either be absolute (beginning with a /), or relative
+to the tree root.  For instance, if dir/link points to ../../foo, then
+<symlink> will be ../foo.  <size> is the size of the symlink in bytes.
+
+If --follow-symlinks is used, the following error messages will be
+displayed:
+
+------------
+<object> SP missing LF
+------------
+is printed when the initial symlink requested does not exist.
+
+------------
+dangling SP <size> LF
+<object> LF
+------------
+is printed when the initial symlink exists, but something that
+it (transitive-of) points to does not.
+
+------------
+loop SP <size> LF
+<object> LF
+------------
+is printed for symlink loops (or any symlinks that
+require more than 40 link resolutions to resolve).
+
+------------
+notdir SP <size> LF
+<object> LF
+------------
+is printed when, during symlink resolution, a file is used as a
+directory name.
 
 CAVEATS
 -------
index ecb488822f903c56b5d14bdec3130c71817e1b65..049a95f1f113289a1a01f74a2485d1e9b282209f 100644 (file)
@@ -8,6 +8,7 @@
 #include "parse-options.h"
 #include "userdiff.h"
 #include "streaming.h"
+#include "tree-walk.h"
 
 static int cat_one_file(int opt, const char *exp_type, const char *obj_name,
                        int unknown_type)
@@ -233,6 +234,7 @@ static void print_object_or_die(int fd, struct expand_data *data)
 
 struct batch_options {
        int enabled;
+       int follow_symlinks;
        int print_contents;
        const char *format;
 };
@@ -241,12 +243,44 @@ static int batch_one_object(const char *obj_name, struct batch_options *opt,
                            struct expand_data *data)
 {
        struct strbuf buf = STRBUF_INIT;
+       struct object_context ctx;
+       int flags = opt->follow_symlinks ? GET_SHA1_FOLLOW_SYMLINKS : 0;
+       enum follow_symlinks_result result;
 
        if (!obj_name)
           return 1;
 
-       if (get_sha1(obj_name, data->sha1)) {
-               printf("%s missing\n", obj_name);
+       result = get_sha1_with_context(obj_name, flags, data->sha1, &ctx);
+       if (result != FOUND) {
+               switch (result) {
+               case MISSING_OBJECT:
+                       printf("%s missing\n", obj_name);
+                       break;
+               case DANGLING_SYMLINK:
+                       printf("dangling %"PRIuMAX"\n%s\n",
+                              (uintmax_t)strlen(obj_name), obj_name);
+                       break;
+               case SYMLINK_LOOP:
+                       printf("loop %"PRIuMAX"\n%s\n",
+                              (uintmax_t)strlen(obj_name), obj_name);
+                       break;
+               case NOT_DIR:
+                       printf("notdir %"PRIuMAX"\n%s\n",
+                              (uintmax_t)strlen(obj_name), obj_name);
+                       break;
+               default:
+                       die("BUG: unknown get_sha1_with_context result %d\n",
+                              result);
+                       break;
+               }
+               fflush(stdout);
+               return 0;
+       }
+
+       if (ctx.mode == 0) {
+               printf("symlink %"PRIuMAX"\n%s\n",
+                      (uintmax_t)ctx.symlink_path.len,
+                      ctx.symlink_path.buf);
                fflush(stdout);
                return 0;
        }
@@ -333,7 +367,7 @@ static int batch_objects(struct batch_options *opt)
 
 static const char * const cat_file_usage[] = {
        N_("git cat-file (-t [--allow-unknown-type]|-s [--allow-unknown-type]|-e|-p|<type>|--textconv) <object>"),
-       N_("git cat-file (--batch | --batch-check) < <list-of-objects>"),
+       N_("git cat-file (--batch | --batch-check) [--follow-symlinks] < <list-of-objects>"),
        NULL
 };
 
@@ -351,9 +385,8 @@ static int batch_option_callback(const struct option *opt,
 {
        struct batch_options *bo = opt->value;
 
-       if (unset) {
-               memset(bo, 0, sizeof(*bo));
-               return 0;
+       if (bo->enabled) {
+               return 1;
        }
 
        bo->enabled = 1;
@@ -387,6 +420,8 @@ int cmd_cat_file(int argc, const char **argv, const char *prefix)
                { OPTION_CALLBACK, 0, "batch-check", &batch, "format",
                        N_("show info about objects fed from the standard input"),
                        PARSE_OPT_OPTARG, batch_option_callback },
+               OPT_BOOL(0, "follow-symlinks", &batch.follow_symlinks,
+                        N_("follow in-tree symlinks (used with --batch or --batch-check)")),
                OPT_END()
        };
 
@@ -411,6 +446,10 @@ int cmd_cat_file(int argc, const char **argv, const char *prefix)
                usage_with_options(cat_file_usage, options);
        }
 
+       if (batch.follow_symlinks && !batch.enabled) {
+               usage_with_options(cat_file_usage, options);
+       }
+
        if (batch.enabled)
                return batch_objects(&batch);
 
diff --git a/cache.h b/cache.h
index 9da9784824dc3a93db2c753613bb56866a356070..571c98f5e2406afad1352fcaddd1efff1af65cc1 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -971,15 +971,21 @@ struct object_context {
        unsigned char tree[20];
        char path[PATH_MAX];
        unsigned mode;
+       /*
+        * symlink_path is only used by get_tree_entry_follow_symlinks,
+        * and only for symlinks that point outside the repository.
+        */
+       struct strbuf symlink_path;
 };
 
-#define GET_SHA1_QUIETLY        01
-#define GET_SHA1_COMMIT         02
-#define GET_SHA1_COMMITTISH     04
-#define GET_SHA1_TREE          010
-#define GET_SHA1_TREEISH       020
-#define GET_SHA1_BLOB         040
-#define GET_SHA1_ONLY_TO_DIE 04000
+#define GET_SHA1_QUIETLY           01
+#define GET_SHA1_COMMIT            02
+#define GET_SHA1_COMMITTISH        04
+#define GET_SHA1_TREE             010
+#define GET_SHA1_TREEISH          020
+#define GET_SHA1_BLOB             040
+#define GET_SHA1_FOLLOW_SYMLINKS 0100
+#define GET_SHA1_ONLY_TO_DIE    04000
 
 extern int get_sha1(const char *str, unsigned char *sha1);
 extern int get_sha1_commit(const char *str, unsigned char *sha1);
index 6de8c87c8a120186eef41eb63c724a6e1a80e0c6..46218ba02c6c1fd87cf51fa189843da3b7950dbe 100644 (file)
@@ -1433,11 +1433,19 @@ static int get_sha1_with_context_1(const char *name,
                        new_filename = resolve_relative_path(filename);
                        if (new_filename)
                                filename = new_filename;
-                       ret = get_tree_entry(tree_sha1, filename, sha1, &oc->mode);
-                       if (ret && only_to_die) {
-                               diagnose_invalid_sha1_path(prefix, filename,
-                                                          tree_sha1,
-                                                          name, len);
+                       if (flags & GET_SHA1_FOLLOW_SYMLINKS) {
+                               ret = get_tree_entry_follow_symlinks(tree_sha1,
+                                       filename, sha1, &oc->symlink_path,
+                                       &oc->mode);
+                       } else {
+                               ret = get_tree_entry(tree_sha1, filename,
+                                                    sha1, &oc->mode);
+                               if (ret && only_to_die) {
+                                       diagnose_invalid_sha1_path(prefix,
+                                                                  filename,
+                                                                  tree_sha1,
+                                                                  name, len);
+                               }
                        }
                        hashcpy(oc->tree, tree_sha1);
                        strlcpy(oc->path, filename, sizeof(oc->path));
@@ -1468,5 +1476,7 @@ void maybe_die_on_misspelt_object_name(const char *name, const char *prefix)
 
 int get_sha1_with_context(const char *str, unsigned flags, unsigned char *sha1, struct object_context *orc)
 {
+       if (flags & GET_SHA1_FOLLOW_SYMLINKS && flags & GET_SHA1_ONLY_TO_DIE)
+               die("BUG: incompatible flags for get_sha1_with_context");
        return get_sha1_with_context_1(str, flags, NULL, sha1, orc);
 }
index 4f225db9a72729e99f5eaab0b81ec579691b34c7..93a4794930bfebc99504a245392f32a3c92b3dde 100755 (executable)
@@ -201,6 +201,13 @@ do
     '
 done
 
+for opt in t s e p
+do
+    test_expect_success "Passing -$opt with --follow-symlinks fails" '
+           test_must_fail git cat-file --follow-symlinks -$opt $hello_sha1
+       '
+done
+
 test_expect_success "--batch-check for a non-existent named object" '
     test "foobar42 missing
 foobar84 missing" = \
@@ -341,4 +348,203 @@ test_expect_success "Size of large broken object is correct when type is large"
        test_cmp expect actual
 '
 
+# Tests for git cat-file --follow-symlinks
+test_expect_success 'prep for symlink tests' '
+       echo_without_newline "$hello_content" >morx &&
+       test_ln_s_add morx same-dir-link &&
+       test_ln_s_add dir link-to-dir &&
+       test_ln_s_add ../fleem out-of-repo-link &&
+       test_ln_s_add .. out-of-repo-link-dir &&
+       test_ln_s_add same-dir-link link-to-link &&
+       test_ln_s_add nope broken-same-dir-link &&
+       mkdir dir &&
+       test_ln_s_add ../morx dir/parent-dir-link &&
+       test_ln_s_add .. dir/link-dir &&
+       test_ln_s_add ../../escape dir/out-of-repo-link &&
+       test_ln_s_add ../.. dir/out-of-repo-link-dir &&
+       test_ln_s_add nope dir/broken-link-in-dir &&
+       mkdir dir/subdir &&
+       test_ln_s_add ../../morx dir/subdir/grandparent-dir-link &&
+       test_ln_s_add ../../../great-escape dir/subdir/out-of-repo-link &&
+       test_ln_s_add ../../.. dir/subdir/out-of-repo-link-dir &&
+       test_ln_s_add ../../../ dir/subdir/out-of-repo-link-dir-trailing &&
+       test_ln_s_add ../parent-dir-link dir/subdir/parent-dir-link-to-link &&
+       echo_without_newline "$hello_content" >dir/subdir/ind2 &&
+       echo_without_newline "$hello_content" >dir/ind1 &&
+       test_ln_s_add dir dirlink &&
+       test_ln_s_add dir/subdir subdirlink &&
+       test_ln_s_add subdir/ind2 dir/link-to-child &&
+       test_ln_s_add dir/link-to-child link-to-down-link &&
+       test_ln_s_add dir/.. up-down &&
+       test_ln_s_add dir/../ up-down-trailing &&
+       test_ln_s_add dir/../morx up-down-file &&
+       test_ln_s_add dir/../../morx up-up-down-file &&
+       test_ln_s_add subdirlink/../../morx up-two-down-file &&
+       test_ln_s_add loop1 loop2 &&
+       test_ln_s_add loop2 loop1 &&
+       git add morx dir/subdir/ind2 dir/ind1 &&
+       git commit -am "test" &&
+       echo $hello_sha1 blob $hello_size >found
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for non-links' '
+       echo HEAD:morx | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual &&
+       echo HEAD:nope missing >expect &&
+       echo HEAD:nope | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for in-repo, same-dir links' '
+       echo HEAD:same-dir-link | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for in-repo, links to dirs' '
+       echo HEAD:link-to-dir/ind1 | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual
+'
+
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for broken in-repo, same-dir links' '
+       echo dangling 25 >expect &&
+       echo HEAD:broken-same-dir-link >>expect &&
+       echo HEAD:broken-same-dir-link | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for same-dir links-to-links' '
+       echo HEAD:link-to-link | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for parent-dir links' '
+       echo HEAD:dir/parent-dir-link | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual &&
+       echo notdir 29 >expect &&
+       echo HEAD:dir/parent-dir-link/nope >>expect &&
+       echo HEAD:dir/parent-dir-link/nope | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for .. links' '
+       echo dangling 22 >expect &&
+       echo HEAD:dir/link-dir/nope >>expect &&
+       echo HEAD:dir/link-dir/nope | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual &&
+       echo HEAD:dir/link-dir/morx | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual &&
+       echo dangling 27 >expect &&
+       echo HEAD:dir/broken-link-in-dir >>expect &&
+       echo HEAD:dir/broken-link-in-dir | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for ../.. links' '
+       echo notdir 41 >expect &&
+       echo HEAD:dir/subdir/grandparent-dir-link/nope >>expect &&
+       echo HEAD:dir/subdir/grandparent-dir-link/nope | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual &&
+       echo HEAD:dir/subdir/grandparent-dir-link | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual &&
+       echo HEAD:dir/subdir/parent-dir-link-to-link | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for dir/ links' '
+       echo dangling 17 >expect &&
+       echo HEAD:dirlink/morx >>expect &&
+       echo HEAD:dirlink/morx | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual &&
+       echo $hello_sha1 blob $hello_size >expect &&
+       echo HEAD:dirlink/ind1 | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for dir/subdir links' '
+       echo dangling 20 >expect &&
+       echo HEAD:subdirlink/morx >>expect &&
+       echo HEAD:subdirlink/morx | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual &&
+       echo HEAD:subdirlink/ind2 | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for dir ->subdir links' '
+       echo notdir 27 >expect &&
+       echo HEAD:dir/link-to-child/morx >>expect &&
+       echo HEAD:dir/link-to-child/morx | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual &&
+       echo HEAD:dir/link-to-child | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual &&
+       echo HEAD:link-to-down-link | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-of-repo symlinks' '
+       echo symlink 8 >expect &&
+       echo ../fleem >>expect &&
+       echo HEAD:out-of-repo-link | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual &&
+       echo symlink 2 >expect &&
+       echo .. >>expect &&
+       echo HEAD:out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-of-repo symlinks in dirs' '
+       echo symlink 9 >expect &&
+       echo ../escape >>expect &&
+       echo HEAD:dir/out-of-repo-link | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual &&
+       echo symlink 2 >expect &&
+       echo .. >>expect &&
+       echo HEAD:dir/out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-of-repo symlinks in subdirs' '
+       echo symlink 15 >expect &&
+       echo ../great-escape >>expect &&
+       echo HEAD:dir/subdir/out-of-repo-link | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual &&
+       echo symlink 2 >expect &&
+       echo .. >>expect &&
+       echo HEAD:dir/subdir/out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual &&
+       echo symlink 3 >expect &&
+       echo ../ >>expect &&
+       echo HEAD:dir/subdir/out-of-repo-link-dir-trailing | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlinks works for symlinks with internal ..' '
+       echo HEAD: | git cat-file --batch-check >expect &&
+       echo HEAD:up-down | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual &&
+       echo HEAD:up-down-trailing | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual &&
+       echo HEAD:up-down-file | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual &&
+       echo symlink 7 >expect &&
+       echo ../morx >>expect &&
+       echo HEAD:up-up-down-file | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual &&
+       echo HEAD:up-two-down-file | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp found actual
+'
+
+test_expect_success 'git cat-file --batch-check --follow-symlink breaks loops' '
+       echo loop 10 >expect &&
+       echo HEAD:loop1 >>expect &&
+       echo HEAD:loop1 | git cat-file --batch-check --follow-symlinks >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'git cat-file --batch --follow-symlink returns correct sha and mode' '
+       echo HEAD:morx | git cat-file --batch >expect &&
+       echo HEAD:morx | git cat-file --batch --follow-symlinks >actual &&
+       test_cmp expect actual
+'
+
 test_done
index 5dd9a718047bc14e9ae840a46d1f71cb435fd6d7..6dccd2d5dd78e0ee71c1ebc771a99010a56db157 100644 (file)
@@ -415,6 +415,12 @@ int traverse_trees(int n, struct tree_desc *t, struct traverse_info *info)
        return error;
 }
 
+struct dir_state {
+       void *tree;
+       unsigned long size;
+       unsigned char sha1[20];
+};
+
 static int find_tree_entry(struct tree_desc *t, const char *name, unsigned char *result, unsigned *mode)
 {
        int namelen = strlen(name);
@@ -478,6 +484,206 @@ int get_tree_entry(const unsigned char *tree_sha1, const char *name, unsigned ch
        return retval;
 }
 
+/*
+ * This is Linux's built-in max for the number of symlinks to follow.
+ * That limit, of course, does not affect git, but it's a reasonable
+ * choice.
+ */
+#define GET_TREE_ENTRY_FOLLOW_SYMLINKS_MAX_LINKS 40
+
+/**
+ * Find a tree entry by following symlinks in tree_sha (which is
+ * assumed to be the root of the repository).  In the event that a
+ * symlink points outside the repository (e.g. a link to /foo or a
+ * root-level link to ../foo), the portion of the link which is
+ * outside the repository will be returned in result_path, and *mode
+ * will be set to 0.  It is assumed that result_path is uninitialized.
+ * If there are no symlinks, or the end result of the symlink chain
+ * points to an object inside the repository, result will be filled in
+ * with the sha1 of the found object, and *mode will hold the mode of
+ * the object.
+ *
+ * See the code for enum follow_symlink_result for a description of
+ * the return values.
+ */
+enum follow_symlinks_result get_tree_entry_follow_symlinks(unsigned char *tree_sha1, const char *name, unsigned char *result, struct strbuf *result_path, unsigned *mode)
+{
+       int retval = MISSING_OBJECT;
+       struct dir_state *parents = NULL;
+       size_t parents_alloc = 0;
+       ssize_t parents_nr = 0;
+       unsigned char current_tree_sha1[20];
+       struct strbuf namebuf = STRBUF_INIT;
+       struct tree_desc t;
+       int follows_remaining = GET_TREE_ENTRY_FOLLOW_SYMLINKS_MAX_LINKS;
+       int i;
+
+       init_tree_desc(&t, NULL, 0UL);
+       strbuf_init(result_path, 0);
+       strbuf_addstr(&namebuf, name);
+       hashcpy(current_tree_sha1, tree_sha1);
+
+       while (1) {
+               int find_result;
+               char *first_slash;
+               char *remainder = NULL;
+
+               if (!t.buffer) {
+                       void *tree;
+                       unsigned char root[20];
+                       unsigned long size;
+                       tree = read_object_with_reference(current_tree_sha1,
+                                                         tree_type, &size,
+                                                         root);
+                       if (!tree)
+                               goto done;
+
+                       ALLOC_GROW(parents, parents_nr + 1, parents_alloc);
+                       parents[parents_nr].tree = tree;
+                       parents[parents_nr].size = size;
+                       hashcpy(parents[parents_nr].sha1, root);
+                       parents_nr++;
+
+                       if (namebuf.buf[0] == '\0') {
+                               hashcpy(result, root);
+                               retval = FOUND;
+                               goto done;
+                       }
+
+                       if (!size)
+                               goto done;
+
+                       /* descend */
+                       init_tree_desc(&t, tree, size);
+               }
+
+               /* Handle symlinks to e.g. a//b by removing leading slashes */
+               while (namebuf.buf[0] == '/') {
+                       strbuf_remove(&namebuf, 0, 1);
+               }
+
+               /* Split namebuf into a first component and a remainder */
+               if ((first_slash = strchr(namebuf.buf, '/'))) {
+                       *first_slash = 0;
+                       remainder = first_slash + 1;
+               }
+
+               if (!strcmp(namebuf.buf, "..")) {
+                       struct dir_state *parent;
+                       /*
+                        * We could end up with .. in the namebuf if it
+                        * appears in a symlink.
+                        */
+
+                       if (parents_nr == 1) {
+                               if (remainder)
+                                       *first_slash = '/';
+                               strbuf_add(result_path, namebuf.buf,
+                                          namebuf.len);
+                               *mode = 0;
+                               retval = FOUND;
+                               goto done;
+                       }
+                       parent = &parents[parents_nr - 1];
+                       free(parent->tree);
+                       parents_nr--;
+                       parent = &parents[parents_nr - 1];
+                       init_tree_desc(&t, parent->tree, parent->size);
+                       strbuf_remove(&namebuf, 0, remainder ? 3 : 2);
+                       continue;
+               }
+
+               /* We could end up here via a symlink to dir/.. */
+               if (namebuf.buf[0] == '\0') {
+                       hashcpy(result, parents[parents_nr - 1].sha1);
+                       retval = FOUND;
+                       goto done;
+               }
+
+               /* Look up the first (or only) path component in the tree. */
+               find_result = find_tree_entry(&t, namebuf.buf,
+                                             current_tree_sha1, mode);
+               if (find_result) {
+                       goto done;
+               }
+
+               if (S_ISDIR(*mode)) {
+                       if (!remainder) {
+                               hashcpy(result, current_tree_sha1);
+                               retval = FOUND;
+                               goto done;
+                       }
+                       /* Descend the tree */
+                       t.buffer = NULL;
+                       strbuf_remove(&namebuf, 0,
+                                     1 + first_slash - namebuf.buf);
+               } else if (S_ISREG(*mode)) {
+                       if (!remainder) {
+                               hashcpy(result, current_tree_sha1);
+                               retval = FOUND;
+                       } else {
+                               retval = NOT_DIR;
+                       }
+                       goto done;
+               } else if (S_ISLNK(*mode)) {
+                       /* Follow a symlink */
+                       unsigned long link_len;
+                       size_t len;
+                       char *contents, *contents_start;
+                       struct dir_state *parent;
+                       enum object_type type;
+
+                       if (follows_remaining-- == 0) {
+                               /* Too many symlinks followed */
+                               retval = SYMLINK_LOOP;
+                               goto done;
+                       }
+
+                       /*
+                        * At this point, we have followed at a least
+                        * one symlink, so on error we need to report this.
+                        */
+                       retval = DANGLING_SYMLINK;
+
+                       contents = read_sha1_file(current_tree_sha1, &type,
+                                                 &link_len);
+
+                       if (!contents)
+                               goto done;
+
+                       if (contents[0] == '/') {
+                               strbuf_addstr(result_path, contents);
+                               free(contents);
+                               *mode = 0;
+                               retval = FOUND;
+                               goto done;
+                       }
+
+                       if (remainder)
+                               len = first_slash - namebuf.buf;
+                       else
+                               len = namebuf.len;
+
+                       contents_start = contents;
+
+                       parent = &parents[parents_nr - 1];
+                       init_tree_desc(&t, parent->tree, parent->size);
+                       strbuf_splice(&namebuf, 0, len,
+                                     contents_start, link_len);
+                       if (remainder)
+                               namebuf.buf[link_len] = '/';
+                       free(contents);
+               }
+       }
+done:
+       for (i = 0; i < parents_nr; i++)
+               free(parents[i].tree);
+       free(parents);
+
+       strbuf_release(&namebuf);
+       return retval;
+}
+
 static int match_entry(const struct pathspec_item *item,
                       const struct name_entry *entry, int pathlen,
                       const char *match, int matchlen,
index ae7fb3a824a960c6c03efae799683cd7e18d402c..3b2f7bf17d37de5b475c415b41c6abb4281d5469 100644 (file)
@@ -40,6 +40,24 @@ struct traverse_info;
 typedef int (*traverse_callback_t)(int n, unsigned long mask, unsigned long dirmask, struct name_entry *entry, struct traverse_info *);
 int traverse_trees(int n, struct tree_desc *t, struct traverse_info *info);
 
+enum follow_symlinks_result {
+       FOUND = 0, /* This includes out-of-tree links */
+       MISSING_OBJECT = -1, /* The initial symlink is missing */
+       DANGLING_SYMLINK = -2, /*
+                               * The initial symlink is there, but
+                               * (transitively) points to a missing
+                               * in-tree file
+                               */
+       SYMLINK_LOOP = -3,
+       NOT_DIR = -4, /*
+                      * Somewhere along the symlink chain, a path is
+                      * requested which contains a file as a
+                      * non-final element.
+                      */
+};
+
+enum follow_symlinks_result get_tree_entry_follow_symlinks(unsigned char *tree_sha1, const char *name, unsigned char *result, struct strbuf *result_path, unsigned *mode);
+
 struct traverse_info {
        struct traverse_info *prev;
        struct name_entry name;