{"thread":{"id":"39341","subject":"[PATCH 2/3] sha1_name: get_sha1_with_context learns to follow symlinks","startedAt":"2015-05-14T20:38:05Z","lastAt":"2015-05-14T20:38:08Z","messageCount":4,"participants":["David Turner"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"261282","messageId":"1431635888-11538-1-git-send-email-dturner@twitter.com","threadId":"39341","inReplyTo":null,"subject":"[PATCH v11 0/3] cat-file --follow-symlinks","fromName":"David Turner","fromEmail":"dturner@twopensource.com","sentAt":"2015-05-14T20:38:05Z","receivedAt":"2015-05-14T20:38:05Z","isPatch":true,"sender":{"key":"novalis@novalis.org","avatar":"https://avatars.githubusercontent.com/u/77003?v=4"},"body":"Squashes Junio's formatting and text to cat-file.c on top of my doco\nfixes from v10.\n"},{"id":"261284","messageId":"1431635888-11538-2-git-send-email-dturner@twitter.com","threadId":"39341","inReplyTo":"1431635888-11538-1-git-send-email-dturner@twitter.com","subject":"[PATCH 1/3] tree-walk: learn get_tree_entry_follow_symlinks","fromName":"David Turner","fromEmail":"dturner@twopensource.com","sentAt":"2015-05-14T20:38:06Z","receivedAt":"2015-05-14T20:38:06Z","isPatch":true,"sender":{"key":"novalis@novalis.org","avatar":"https://avatars.githubusercontent.com/u/77003?v=4"},"body":"Add a new function, get_tree_entry_follow_symlinks, to tree-walk.[ch].\nThe function is not yet used.  It will be used to implement git\ncat-file --batch --follow-symlinks.\n\nThe function locates an object by path, following symlinks in the\nrepository.  If the symlinks lead outside the repository, the function\nreports this to the caller.\n\nSigned-off-by: David Turner <dturner@twitter.com>\n---\n tree-walk.c | 207 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tree-walk.h |  18 ++++++\n 2 files changed, 225 insertions(+)\n\ndiff --git a/tree-walk.c b/tree-walk.c\nindex 5dd9a71..8031f3a 100644\n--- a/tree-walk.c\n+++ b/tree-walk.c\n@@ -415,6 +415,12 @@ int traverse_trees(int n, struct tree_desc *t, struct traverse_info *info)\n \treturn error;\n }\n \n+struct dir_state {\n+\tvoid *tree;\n+\tunsigned long size;\n+\tunsigned char sha1[20];\n+};\n+\n static int find_tree_entry(struct tree_desc *t, const char *name, unsigned char *result, unsigned *mode)\n {\n \tint namelen = strlen(name);\n@@ -478,6 +484,207 @@ int get_tree_entry(const unsigned char *tree_sha1, const char *name, unsigned ch\n \treturn retval;\n }\n \n+/*\n+ * This is Linux's built-in max for the number of symlinks to follow.\n+ * That limit, of course, does not affect git, but it's a reasonable\n+ * choice.\n+ */\n+#define GET_TREE_ENTRY_FOLLOW_SYMLINKS_MAX_LINKS 40\n+\n+/**\n+ * Find a tree entry by following symlinks in tree_sha (which is\n+ * assumed to be the root of the repository).  In the event that a\n+ * symlink points outside the repository (e.g. a link to /foo or a\n+ * root-level link to ../foo), the portion of the link which is\n+ * outside the repository will be returned in result_path, and *mode\n+ * will be set to 0.  It is assumed that result_path is uninitialized.\n+ * If there are no symlinks, or the end result of the symlink chain\n+ * points to an object inside the repository, result will be filled in\n+ * with the sha1 of the found object, and *mode will hold the mode of\n+ * the object.\n+ *\n+ * See the code for enum follow_symlink_result for a description of\n+ * the return values.\n+ */\n+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)\n+{\n+\tint retval = MISSING_OBJECT;\n+\tstruct dir_state *parents = NULL;\n+\tsize_t parents_alloc = 0;\n+\tssize_t parents_nr = 0;\n+\tunsigned char current_tree_sha1[20];\n+\tstruct strbuf namebuf = STRBUF_INIT;\n+\tstruct tree_desc t = {0};\n+\tint follows_remaining = GET_TREE_ENTRY_FOLLOW_SYMLINKS_MAX_LINKS;\n+\tint i;\n+\n+\tresult_path->buf = 0;\n+\tresult_path->alloc = 0;\n+\tresult_path->len = 0;\n+\tstrbuf_addstr(&namebuf, name);\n+\thashcpy(current_tree_sha1, tree_sha1);\n+\n+\twhile (1) {\n+\t\tint find_result;\n+\t\tchar *first_slash;\n+\t\tchar *remainder = NULL;\n+\n+\t\tif (!t.buffer) {\n+\t\t\tvoid *tree;\n+\t\t\tunsigned char root[20];\n+\t\t\tunsigned long size;\n+\t\t\ttree = read_object_with_reference(current_tree_sha1,\n+\t\t\t\t\t\t\t  tree_type, &size,\n+\t\t\t\t\t\t\t  root);\n+\t\t\tif (!tree)\n+\t\t\t\tgoto done;\n+\n+\t\t\tALLOC_GROW(parents, parents_nr + 1, parents_alloc);\n+\t\t\tparents[parents_nr].tree = tree;\n+\t\t\tparents[parents_nr].size = size;\n+\t\t\thashcpy(parents[parents_nr].sha1, root);\n+\t\t\tparents_nr++;\n+\n+\t\t\tif (namebuf.buf[0] == '\\0') {\n+\t\t\t\thashcpy(result, root);\n+\t\t\t\tretval = FOUND;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tif (!size)\n+\t\t\t\tgoto done;\n+\n+\t\t\t/* descend */\n+\t\t\tinit_tree_desc(&t, tree, size);\n+\t\t}\n+\n+\t\t/* Handle symlinks to e.g. a//b by removing leading slashes */\n+\t\twhile (namebuf.buf[0] == '/') {\n+\t\t\tstrbuf_remove(&namebuf, 0, 1);\n+\t\t}\n+\n+\t\t/* Split namebuf into a first component and a remainder */\n+\t\tif ((first_slash = strchr(namebuf.buf, '/'))) {\n+\t\t\t*first_slash = 0;\n+\t\t\tremainder = first_slash + 1;\n+\t\t}\n+\n+\t\tif (!strcmp(namebuf.buf, \"..\")) {\n+\t\t\tstruct dir_state *parent;\n+\t\t\t/*\n+\t\t\t * We could end up with .. in the namebuf if it\n+\t\t\t * appears in a symlink.\n+\t\t\t */\n+\n+\t\t\tif (parents_nr == 1) {\n+\t\t\t\tif (remainder)\n+\t\t\t\t\t*first_slash = '/';\n+\t\t\t\tstrbuf_add(result_path, namebuf.buf,\n+\t\t\t\t\t   namebuf.len);\n+\t\t\t\t*mode = 0;\n+\t\t\t\tretval = FOUND;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tparent = &parents[parents_nr - 1];\n+\t\t\tfree(parent->tree);\n+\t\t\tparents_nr--;\n+\t\t\tparent = &parents[parents_nr - 1];\n+\t\t\tinit_tree_desc(&t, parent->tree, parent->size);\n+\t\t\tstrbuf_remove(&namebuf, 0, remainder ? 3 : 2);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\t/* We could end up here via a symlink to dir/.. */\n+\t\tif (namebuf.buf[0] == '\\0') {\n+\t\t\thashcpy(result, parents[parents_nr - 1].sha1);\n+\t\t\tretval = FOUND;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* Look up the first (or only) path component in the tree. */\n+\t\tfind_result = find_tree_entry(&t, namebuf.buf,\n+\t\t\t\t\t      current_tree_sha1, mode);\n+\t\tif (find_result) {\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (S_ISDIR(*mode)) {\n+\t\t\tif (!remainder) {\n+\t\t\t\thashcpy(result, current_tree_sha1);\n+\t\t\t\tretval = FOUND;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\t/* Descend the tree */\n+\t\t\tt.buffer = NULL;\n+\t\t\tstrbuf_remove(&namebuf, 0,\n+\t\t\t\t      1 + first_slash - namebuf.buf);\n+\t\t} else if (S_ISREG(*mode)) {\n+\t\t\tif (!remainder) {\n+\t\t\t\thashcpy(result, current_tree_sha1);\n+\t\t\t\tretval = FOUND;\n+\t\t\t} else {\n+\t\t\t\tretval = NOT_DIR;\n+\t\t\t}\n+\t\t\tgoto done;\n+\t\t} else if (S_ISLNK(*mode)) {\n+\t\t\t/* Follow a symlink */\n+\t\t\tunsigned long link_len;\n+\t\t\tsize_t len;\n+\t\t\tchar *contents, *contents_start;\n+\t\t\tstruct dir_state *parent;\n+\t\t\tenum object_type type;\n+\n+\t\t\tif (follows_remaining-- == 0) {\n+\t\t\t\t/* Too many symlinks followed */\n+\t\t\t\tretval = SYMLINK_LOOP;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\t/*\n+\t\t\t * At this point, we have followed at a least\n+\t\t\t * one symlink, so on error we need to report this.\n+\t\t\t */\n+\t\t\tretval = DANGLING_SYMLINK;\n+\n+\t\t\tcontents = read_sha1_file(current_tree_sha1, &type,\n+\t\t\t\t\t\t  &link_len);\n+\n+\t\t\tif (!contents)\n+\t\t\t\tgoto done;\n+\n+\t\t\tif (contents[0] == '/') {\n+\t\t\t\tstrbuf_addstr(result_path, contents);\n+\t\t\t\tfree(contents);\n+\t\t\t\t*mode = 0;\n+\t\t\t\tretval = FOUND;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tif (remainder)\n+\t\t\t\tlen = first_slash - namebuf.buf;\n+\t\t\telse\n+\t\t\t\tlen = namebuf.len;\n+\n+\t\t\tcontents_start = contents;\n+\n+\t\t\tparent = &parents[parents_nr - 1];\n+\t\t\tinit_tree_desc(&t, parent->tree, parent->size);\n+\t\t\tstrbuf_splice(&namebuf, 0, len,\n+\t\t\t\t      contents_start, link_len);\n+\t\t\tif (remainder)\n+\t\t\t\tnamebuf.buf[link_len] = '/';\n+\t\t\tfree(contents);\n+\t\t}\n+\t}\n+done:\n+\tfor (i = 0; i < parents_nr; i++)\n+\t\tfree(parents[i].tree);\n+\tfree(parents);\n+\n+\tstrbuf_release(&namebuf);\n+\treturn retval;\n+}\n+\n static int match_entry(const struct pathspec_item *item,\n \t\t       const struct name_entry *entry, int pathlen,\n \t\t       const char *match, int matchlen,\ndiff --git a/tree-walk.h b/tree-walk.h\nindex ae7fb3a..3b2f7bf 100644\n--- a/tree-walk.h\n+++ b/tree-walk.h\n@@ -40,6 +40,24 @@ struct traverse_info;\n typedef int (*traverse_callback_t)(int n, unsigned long mask, unsigned long dirmask, struct name_entry *entry, struct traverse_info *);\n int traverse_trees(int n, struct tree_desc *t, struct traverse_info *info);\n \n+enum follow_symlinks_result {\n+\tFOUND = 0, /* This includes out-of-tree links */\n+\tMISSING_OBJECT = -1, /* The initial symlink is missing */\n+\tDANGLING_SYMLINK = -2, /*\n+\t\t\t\t* The initial symlink is there, but\n+\t\t\t\t* (transitively) points to a missing\n+\t\t\t\t* in-tree file\n+\t\t\t\t*/\n+\tSYMLINK_LOOP = -3,\n+\tNOT_DIR = -4, /*\n+\t\t       * Somewhere along the symlink chain, a path is\n+\t\t       * requested which contains a file as a\n+\t\t       * non-final element.\n+\t\t       */\n+};\n+\n+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);\n+\n struct traverse_info {\n \tstruct traverse_info *prev;\n \tstruct name_entry name;\n-- \n2.0.4.315.gad8727a-twtrsrc\n"},{"id":"261281","messageId":"1431635888-11538-3-git-send-email-dturner@twitter.com","threadId":"39341","inReplyTo":"1431635888-11538-1-git-send-email-dturner@twitter.com","subject":"[PATCH 2/3] sha1_name: get_sha1_with_context learns to follow symlinks","fromName":"David Turner","fromEmail":"dturner@twopensource.com","sentAt":"2015-05-14T20:38:07Z","receivedAt":"2015-05-14T20:38:07Z","isPatch":true,"sender":{"key":"novalis@novalis.org","avatar":"https://avatars.githubusercontent.com/u/77003?v=4"},"body":"Wire up get_sha1_with_context to call get_tree_entry_follow_symlinks\nwhen GET_SHA1_FOLLOW_SYMLINKS is passed in flags. G_S_FOLLOW_SYMLINKS\nis incompatible with G_S_ONLY_TO_DIE because the diagnosis\nthat ONLY_TO_DIE triggers does not at present consider symlinks, and\nit would be a significant amount of additional code to allow it to\ndo so.\n\nSigned-off-by: David Turner <dturner@twitter.com>\n---\n cache.h     | 20 +++++++++++++-------\n sha1_name.c | 20 +++++++++++++++-----\n 2 files changed, 28 insertions(+), 12 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex 3d3244b..65505d1 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -922,15 +922,21 @@ struct object_context {\n \tunsigned char tree[20];\n \tchar path[PATH_MAX];\n \tunsigned mode;\n+\t/*\n+\t * symlink_path is only used by get_tree_entry_follow_symlinks,\n+\t * and only for symlinks that point outside the repository.\n+\t */\n+\tstruct strbuf symlink_path;\n };\n \n-#define GET_SHA1_QUIETLY        01\n-#define GET_SHA1_COMMIT         02\n-#define GET_SHA1_COMMITTISH     04\n-#define GET_SHA1_TREE          010\n-#define GET_SHA1_TREEISH       020\n-#define GET_SHA1_BLOB\t       040\n-#define GET_SHA1_ONLY_TO_DIE 04000\n+#define GET_SHA1_QUIETLY           01\n+#define GET_SHA1_COMMIT            02\n+#define GET_SHA1_COMMITTISH        04\n+#define GET_SHA1_TREE             010\n+#define GET_SHA1_TREEISH          020\n+#define GET_SHA1_BLOB             040\n+#define GET_SHA1_FOLLOW_SYMLINKS 0100\n+#define GET_SHA1_ONLY_TO_DIE    04000\n \n extern int get_sha1(const char *str, unsigned char *sha1);\n extern int get_sha1_commit(const char *str, unsigned char *sha1);\ndiff --git a/sha1_name.c b/sha1_name.c\nindex 6d10f05..0c26515 100644\n--- a/sha1_name.c\n+++ b/sha1_name.c\n@@ -1434,11 +1434,19 @@ static int get_sha1_with_context_1(const char *name,\n \t\t\tnew_filename = resolve_relative_path(filename);\n \t\t\tif (new_filename)\n \t\t\t\tfilename = new_filename;\n-\t\t\tret = get_tree_entry(tree_sha1, filename, sha1, &oc->mode);\n-\t\t\tif (ret && only_to_die) {\n-\t\t\t\tdiagnose_invalid_sha1_path(prefix, filename,\n-\t\t\t\t\t\t\t   tree_sha1,\n-\t\t\t\t\t\t\t   name, len);\n+\t\t\tif (flags & GET_SHA1_FOLLOW_SYMLINKS) {\n+\t\t\t\tret = get_tree_entry_follow_symlinks(tree_sha1,\n+\t\t\t\t\tfilename, sha1, &oc->symlink_path,\n+\t\t\t\t\t&oc->mode);\n+\t\t\t} else {\n+\t\t\t\tret = get_tree_entry(tree_sha1, filename,\n+\t\t\t\t\t\t     sha1, &oc->mode);\n+\t\t\t\tif (ret && only_to_die) {\n+\t\t\t\t\tdiagnose_invalid_sha1_path(prefix,\n+\t\t\t\t\t\t\t\t   filename,\n+\t\t\t\t\t\t\t\t   tree_sha1,\n+\t\t\t\t\t\t\t\t   name, len);\n+\t\t\t\t}\n \t\t\t}\n \t\t\thashcpy(oc->tree, tree_sha1);\n \t\t\tstrlcpy(oc->path, filename, sizeof(oc->path));\n@@ -1469,5 +1477,7 @@ void maybe_die_on_misspelt_object_name(const char *name, const char *prefix)\n \n int get_sha1_with_context(const char *str, unsigned flags, unsigned char *sha1, struct object_context *orc)\n {\n+\tif (flags & GET_SHA1_FOLLOW_SYMLINKS && flags & GET_SHA1_ONLY_TO_DIE)\n+\t\tdie(\"BUG: incompatible flags for get_sha1_with_context\");\n \treturn get_sha1_with_context_1(str, flags, NULL, sha1, orc);\n }\n-- \n2.0.4.315.gad8727a-twtrsrc\n"},{"id":"261283","messageId":"1431635888-11538-4-git-send-email-dturner@twitter.com","threadId":"39341","inReplyTo":"1431635888-11538-1-git-send-email-dturner@twitter.com","subject":"[PATCH 3/3] cat-file: add --follow-symlinks to --batch","fromName":"David Turner","fromEmail":"dturner@twopensource.com","sentAt":"2015-05-14T20:38:08Z","receivedAt":"2015-05-14T20:38:08Z","isPatch":true,"sender":{"key":"novalis@novalis.org","avatar":"https://avatars.githubusercontent.com/u/77003?v=4"},"body":"This wires the in-repo-symlink following code through to the cat-file\nbuiltin.  In the event of an out-of-repo link, cat-file will print\nthe link in a new format.\n\nSigned-off-by: David Turner <dturner@twitter.com>\n---\n Documentation/git-cat-file.txt | 100 +++++++++++++++++++-\n builtin/cat-file.c             |  51 ++++++++--\n t/t1006-cat-file.sh            | 205 +++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 349 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-cat-file.txt b/Documentation/git-cat-file.txt\nindex f6a16f4..050448c 100644\n--- a/Documentation/git-cat-file.txt\n+++ b/Documentation/git-cat-file.txt\n@@ -10,7 +10,7 @@ SYNOPSIS\n --------\n [verse]\n 'git cat-file' (-t | -s | -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>\n \n DESCRIPTION\n -----------\n@@ -69,6 +69,63 @@ OPTIONS\n \tnot be combined with any other options or arguments.  See the\n \tsection `BATCH OUTPUT` below for details.\n \n+--follow-symlinks::\n+\tWith --batch or --batch-check, follow symlinks inside the\n+\trepository when requesting objects with extended SHA-1\n+\texpressions of the form tree-ish:path-in-tree.  Instead of\n+\tproviding output about the link itself, provide output about\n+\tthe linked-to object.  If a symlink points outside the\n+\ttree-ish (e.g. a link to /foo or a root-level link to ../foo),\n+\tthe portion of the link which is outside the tree will be\n+\tprinted.\n+[normal]\n+\tThis option does not (currently) work correctly when an\n+\tobject in the index is specified (e.g. `:link` instead of\n+\t`HEAD:link`) rather than one in the tree.\n+[normal]\n+\tThis option cannot (currently) be used unless `--batch` or\n+\t`--batch-check` is used.\n+[normal]\n+\tFor example, consider a git repository containing:\n++\n+--\n+\tf: a file containing \"hello\\n\"\n+\tlink: a symlink to f\n+\tdir/link: a symlink to ../f\n+\tplink: a symlink to ../f\n+\talink: a symlink to /etc/passwd\n+--\n+[normal]\n+\tFor a regular file `f`, `echo HEAD:f | git cat-file --batch` would\n+\tprint\n++\n+--\n+\tce013625030ba8dba906f756967f9e9ca394464a blob 6\n+--\n+[normal]\n+\tAnd `echo HEAD:link | git cat-file --batch --follow-symlinks`\n+\twould print the same thing, as would `HEAD:dir/link`, as they\n+\tboth point at `HEAD:f`.\n+[normal]\n+\tWithout `--follow-symlinks`, these would print data about the\n+\tsymlink itself.  In the case of `HEAD:link`, you would see\n++\n+--\n+\t4d1ae35ba2c8ec712fa2a379db44ad639ca277bd blob 1\n+--\n+[normal]\n+\tBoth `plink` and `alink` point outside the tree, so they would\n+\trespectively print:\n++\n+--\n+\tsymlink 4\n+\t../f\n+\n+\tsymlink 11\n+\t/etc/passwd\n+--\n+\n+\n OUTPUT\n ------\n If '-t' is specified, one of the <type>.\n@@ -148,6 +205,47 @@ the repository, then `cat-file` will ignore any custom format and print:\n <object> SP missing LF\n ------------\n \n+If --follow-symlinks is used, and a symlink in the repository points\n+outside the repository, then `cat-file` will ignore any custom format\n+and print:\n+\n+------------\n+symlink SP <size> LF\n+<symlink> LF\n+------------\n+\n+The symlink will either be absolute (beginning with a /), or relative\n+to the tree root.  For instance, if dir/link points to ../../foo, then\n+<symlink> will be ../foo.  <size> is the size of the symlink in bytes.\n+\n+If --follow-symlinks is used, the following error messages will be\n+displayed:\n+\n+------------\n+<object> SP missing LF\n+------------\n+is printed when the initial symlink requested does not exist.\n+\n+------------\n+dangling SP <size> LF\n+<object> LF\n+------------\n+is printed when the initial symlink exists, but something that\n+it (transitive-of) points to does not.\n+\n+------------\n+loop SP <size> LF\n+<object> LF\n+------------\n+is printed for symlink loops (or any symlinks that\n+require more than 40 link resolutions to resolve).\n+\n+------------\n+notdir SP <size> LF\n+<object> LF\n+------------\n+is printed when, during symlink resolution, a file is used as a\n+directory name.\n \n CAVEATS\n -------\ndiff --git a/builtin/cat-file.c b/builtin/cat-file.c\nindex df99df4..43338bb 100644\n--- a/builtin/cat-file.c\n+++ b/builtin/cat-file.c\n@@ -8,6 +8,7 @@\n #include \"parse-options.h\"\n #include \"userdiff.h\"\n #include \"streaming.h\"\n+#include \"tree-walk.h\"\n \n static int cat_one_file(int opt, const char *exp_type, const char *obj_name)\n {\n@@ -224,6 +225,7 @@ static void print_object_or_die(int fd, struct expand_data *data)\n \n struct batch_options {\n \tint enabled;\n+\tint follow_symlinks;\n \tint print_contents;\n \tconst char *format;\n };\n@@ -232,12 +234,44 @@ static int batch_one_object(const char *obj_name, struct batch_options *opt,\n \t\t\t    struct expand_data *data)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n+\tstruct object_context ctx;\n+\tint flags = opt->follow_symlinks ? GET_SHA1_FOLLOW_SYMLINKS : 0;\n+\tenum follow_symlinks_result result;\n \n \tif (!obj_name)\n \t   return 1;\n \n-\tif (get_sha1(obj_name, data->sha1)) {\n-\t\tprintf(\"%s missing\\n\", obj_name);\n+\tresult = get_sha1_with_context(obj_name, flags, data->sha1, &ctx);\n+\tif (result != FOUND) {\n+\t\tswitch (result) {\n+\t\tcase MISSING_OBJECT:\n+\t\t\tprintf(\"%s missing\\n\", obj_name);\n+\t\t\tbreak;\n+\t\tcase DANGLING_SYMLINK:\n+\t\t\tprintf(\"dangling %\"PRIuMAX\"\\n%s\\n\",\n+\t\t\t       (uintmax_t)strlen(obj_name), obj_name);\n+\t\t\tbreak;\n+\t\tcase SYMLINK_LOOP:\n+\t\t\tprintf(\"loop %\"PRIuMAX\"\\n%s\\n\",\n+\t\t\t       (uintmax_t)strlen(obj_name), obj_name);\n+\t\t\tbreak;\n+\t\tcase NOT_DIR:\n+\t\t\tprintf(\"notdir %\"PRIuMAX\"\\n%s\\n\",\n+\t\t\t       (uintmax_t)strlen(obj_name), obj_name);\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tdie(\"BUG: unknown get_sha1_with_context result %d\\n\",\n+\t\t\t       result);\n+\t\t\tbreak;\n+\t\t}\n+\t\tfflush(stdout);\n+\t\treturn 0;\n+\t}\n+\n+\tif (ctx.mode == 0) {\n+\t\tprintf(\"symlink %\"PRIuMAX\"\\n%s\\n\",\n+\t\t       (uintmax_t)ctx.symlink_path.len,\n+\t\t       ctx.symlink_path.buf);\n \t\tfflush(stdout);\n \t\treturn 0;\n \t}\n@@ -324,7 +358,7 @@ static int batch_objects(struct batch_options *opt)\n \n static const char * const cat_file_usage[] = {\n \tN_(\"git cat-file (-t | -s | -e | -p | <type> | --textconv) <object>\"),\n-\tN_(\"git cat-file (--batch | --batch-check) < <list-of-objects>\"),\n+\tN_(\"git cat-file (--batch | --batch-check) [--follow-symlinks] < <list-of-objects>\"),\n \tNULL\n };\n \n@@ -342,9 +376,8 @@ static int batch_option_callback(const struct option *opt,\n {\n \tstruct batch_options *bo = opt->value;\n \n-\tif (unset) {\n-\t\tmemset(bo, 0, sizeof(*bo));\n-\t\treturn 0;\n+\tif (bo->enabled) {\n+\t\treturn 1;\n \t}\n \n \tbo->enabled = 1;\n@@ -375,6 +408,8 @@ int cmd_cat_file(int argc, const char **argv, const char *prefix)\n \t\t{ OPTION_CALLBACK, 0, \"batch-check\", &batch, \"format\",\n \t\t\tN_(\"show info about objects fed from the standard input\"),\n \t\t\tPARSE_OPT_OPTARG, batch_option_callback },\n+\t\tOPT_BOOL(0, \"follow-symlinks\", &batch.follow_symlinks,\n+\t\t\t N_(\"follow in-tree symlinks (used with --batch or --batch-check)\")),\n \t\tOPT_END()\n \t};\n \n@@ -402,6 +437,10 @@ int cmd_cat_file(int argc, const char **argv, const char *prefix)\n \t\tusage_with_options(cat_file_usage, options);\n \t}\n \n+\tif (batch.follow_symlinks && !batch.enabled) {\n+\t\tusage_with_options(cat_file_usage, options);\n+\t}\n+\n \tif (batch.enabled)\n \t\treturn batch_objects(&batch);\n \ndiff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh\nindex ab36b1e..a494013 100755\n--- a/t/t1006-cat-file.sh\n+++ b/t/t1006-cat-file.sh\n@@ -189,6 +189,13 @@ do\n     '\n done\n \n+for opt in t s e p\n+do\n+    test_expect_success \"Passing -$opt with --follow-symlinks fails\" '\n+\t    test_must_fail git cat-file --follow-symlinks -$opt $hello_sha1\n+\t'\n+done\n+\n test_expect_success \"--batch-check for a non-existent named object\" '\n     test \"foobar42 missing\n foobar84 missing\" = \\\n@@ -296,4 +303,202 @@ test_expect_success '%(deltabase) reports packed delta bases' '\n \t}\n '\n \n+# Tests for git cat-file --follow-symlinks\n+test_expect_success 'prep for symlink tests' '\n+\techo_without_newline \"$hello_content\" >morx &&\n+\ttest_ln_s_add morx same-dir-link &&\n+\ttest_ln_s_add dir link-to-dir &&\n+\ttest_ln_s_add ../fleem out-of-repo-link &&\n+\ttest_ln_s_add .. out-of-repo-link-dir &&\n+\ttest_ln_s_add same-dir-link link-to-link &&\n+\ttest_ln_s_add nope broken-same-dir-link &&\n+\tmkdir dir &&\n+\ttest_ln_s_add ../morx dir/parent-dir-link &&\n+\ttest_ln_s_add .. dir/link-dir &&\n+\ttest_ln_s_add ../../escape dir/out-of-repo-link &&\n+\ttest_ln_s_add ../.. dir/out-of-repo-link-dir &&\n+\ttest_ln_s_add nope dir/broken-link-in-dir &&\n+\tmkdir dir/subdir &&\n+\ttest_ln_s_add ../../morx dir/subdir/grandparent-dir-link &&\n+\ttest_ln_s_add ../../../great-escape dir/subdir/out-of-repo-link &&\n+\ttest_ln_s_add ../../.. dir/subdir/out-of-repo-link-dir &&\n+\ttest_ln_s_add ../../../ dir/subdir/out-of-repo-link-dir-trailing &&\n+\ttest_ln_s_add ../parent-dir-link dir/subdir/parent-dir-link-to-link &&\n+\techo_without_newline \"$hello_content\" >dir/subdir/ind2 &&\n+\techo_without_newline \"$hello_content\" >dir/ind1 &&\n+\ttest_ln_s_add dir dirlink &&\n+\ttest_ln_s_add dir/subdir subdirlink &&\n+\ttest_ln_s_add subdir/ind2 dir/link-to-child &&\n+\ttest_ln_s_add dir/link-to-child link-to-down-link &&\n+\ttest_ln_s_add dir/.. up-down &&\n+\ttest_ln_s_add dir/../ up-down-trailing &&\n+\ttest_ln_s_add dir/../morx up-down-file &&\n+\ttest_ln_s_add dir/../../morx up-up-down-file &&\n+\ttest_ln_s_add subdirlink/../../morx up-two-down-file &&\n+\ttest_ln_s_add loop1 loop2 &&\n+\ttest_ln_s_add loop2 loop1 &&\n+\tgit add morx dir/subdir/ind2 dir/ind1 &&\n+\tgit commit -am \"test\" &&\n+\techo $hello_sha1 blob $hello_size >found\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for non-links' '\n+\techo HEAD:morx | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual &&\n+\techo HEAD:nope missing >expect &&\n+\techo HEAD:nope | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for in-repo, same-dir links' '\n+\techo HEAD:same-dir-link | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for in-repo, links to dirs' '\n+\techo HEAD:link-to-dir/ind1 | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual\n+'\n+\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for broken in-repo, same-dir links' '\n+\techo dangling 25 >expect &&\n+\techo HEAD:broken-same-dir-link >>expect &&\n+\techo HEAD:broken-same-dir-link | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for same-dir links-to-links' '\n+\techo HEAD:link-to-link | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for parent-dir links' '\n+\techo HEAD:dir/parent-dir-link | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual &&\n+\techo notdir 29 >expect &&\n+\techo HEAD:dir/parent-dir-link/nope >>expect &&\n+\techo HEAD:dir/parent-dir-link/nope | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for .. links' '\n+\techo dangling 22 >expect &&\n+\techo HEAD:dir/link-dir/nope >>expect &&\n+\techo HEAD:dir/link-dir/nope | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual &&\n+\techo HEAD:dir/link-dir/morx | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual &&\n+\techo dangling 27 >expect &&\n+\techo HEAD:dir/broken-link-in-dir >>expect &&\n+\techo HEAD:dir/broken-link-in-dir | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for ../.. links' '\n+\techo notdir 41 >expect &&\n+\techo HEAD:dir/subdir/grandparent-dir-link/nope >>expect &&\n+\techo HEAD:dir/subdir/grandparent-dir-link/nope | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual &&\n+\techo HEAD:dir/subdir/grandparent-dir-link | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual &&\n+\techo HEAD:dir/subdir/parent-dir-link-to-link | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for dir/ links' '\n+\techo dangling 17 >expect &&\n+\techo HEAD:dirlink/morx >>expect &&\n+\techo HEAD:dirlink/morx | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual &&\n+\techo $hello_sha1 blob $hello_size >expect &&\n+\techo HEAD:dirlink/ind1 | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for dir/subdir links' '\n+\techo dangling 20 >expect &&\n+\techo HEAD:subdirlink/morx >>expect &&\n+\techo HEAD:subdirlink/morx | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual &&\n+\techo HEAD:subdirlink/ind2 | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for dir ->subdir links' '\n+\techo notdir 27 >expect &&\n+\techo HEAD:dir/link-to-child/morx >>expect &&\n+\techo HEAD:dir/link-to-child/morx | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual &&\n+\techo HEAD:dir/link-to-child | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual &&\n+\techo HEAD:link-to-down-link | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-of-repo symlinks' '\n+\techo symlink 8 >expect &&\n+\techo ../fleem >>expect &&\n+\techo HEAD:out-of-repo-link | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual &&\n+\techo symlink 2 >expect &&\n+\techo .. >>expect &&\n+\techo HEAD:out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-of-repo symlinks in dirs' '\n+\techo symlink 9 >expect &&\n+\techo ../escape >>expect &&\n+\techo HEAD:dir/out-of-repo-link | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual &&\n+\techo symlink 2 >expect &&\n+\techo .. >>expect &&\n+\techo HEAD:dir/out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-of-repo symlinks in subdirs' '\n+\techo symlink 15 >expect &&\n+\techo ../great-escape >>expect &&\n+\techo HEAD:dir/subdir/out-of-repo-link | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual &&\n+\techo symlink 2 >expect &&\n+\techo .. >>expect &&\n+\techo HEAD:dir/subdir/out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual &&\n+\techo symlink 3 >expect &&\n+\techo ../ >>expect &&\n+\techo HEAD:dir/subdir/out-of-repo-link-dir-trailing | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlinks works for symlinks with internal ..' '\n+\techo HEAD: | git cat-file --batch-check >expect &&\n+\techo HEAD:up-down | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual &&\n+\techo HEAD:up-down-trailing | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual &&\n+\techo HEAD:up-down-file | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual &&\n+\techo symlink 7 >expect &&\n+\techo ../morx >>expect &&\n+\techo HEAD:up-up-down-file | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual &&\n+\techo HEAD:up-two-down-file | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp found actual\n+'\n+\n+test_expect_success 'git cat-file --batch-check --follow-symlink breaks loops' '\n+\techo loop 10 >expect &&\n+\techo HEAD:loop1 >>expect &&\n+\techo HEAD:loop1 | git cat-file --batch-check --follow-symlinks >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git cat-file --batch --follow-symlink returns correct sha and mode' '\n+\techo HEAD:morx | git cat-file --batch >expect &&\n+\techo HEAD:morx | git cat-file --batch --follow-symlinks >actual &&\n+\ttest_cmp expect actual\n+'\n test_done\n-- \n2.0.4.315.gad8727a-twtrsrc\n"}]}