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

1  2 
Documentation/git-cat-file.txt
builtin/cat-file.c
cache.h
sha1_name.c
t/t1006-cat-file.sh
index 499ae7b98a4d9bf48aa2c5a235608dfcffcc1c10,3568aff0de6bd2afdda7fe8b1ca1e4380e3e956c..319ab4cb086874da5e2f153d69d8f9af1f18461e
@@@ -9,8 -9,8 +9,8 @@@ git-cat-file - Provide content or type 
  SYNOPSIS
  --------
  [verse]
 -'git cat-file' (-t | -s | -e | -p | <type> | --textconv ) <object>
 +'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
  -----------
@@@ -69,9 -69,62 +69,65 @@@ OPTION
        not be combined with any other options or arguments.  See the
        section `BATCH OUTPUT` below for details.
  
 +--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 -204,47 +207,47 @@@ the repository, then `cat-file` will ig
  <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
  -------
diff --combined builtin/cat-file.c
index ecb488822f903c56b5d14bdec3130c71817e1b65,43338bb7de46e8e2d4babe9454f7fd32e8e54c37..049a95f1f113289a1a01f74a2485d1e9b282209f
@@@ -8,21 -8,15 +8,22 @@@
  #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)
 +static int cat_one_file(int opt, const char *exp_type, const char *obj_name,
 +                      int unknown_type)
  {
        unsigned char sha1[20];
        enum object_type type;
        char *buf;
        unsigned long size;
        struct object_context obj_context;
 +      struct object_info oi = {NULL};
 +      struct strbuf sb = STRBUF_INIT;
 +      unsigned flags = LOOKUP_REPLACE_OBJECT;
 +
 +      if (unknown_type)
 +              flags |= LOOKUP_UNKNOWN_OBJECT;
  
        if (get_sha1_with_context(obj_name, 0, sha1, &obj_context))
                die("Not a valid object name %s", obj_name);
        buf = NULL;
        switch (opt) {
        case 't':
 -              type = sha1_object_info(sha1, NULL);
 -              if (type > 0) {
 -                      printf("%s\n", typename(type));
 +              oi.typename = &sb;
 +              if (sha1_object_info_extended(sha1, &oi, flags) < 0)
 +                      die("git cat-file: could not get object info");
 +              if (sb.len) {
 +                      printf("%s\n", sb.buf);
 +                      strbuf_release(&sb);
                        return 0;
                }
                break;
  
        case 's':
 -              type = sha1_object_info(sha1, &size);
 -              if (type > 0) {
 -                      printf("%lu\n", size);
 -                      return 0;
 -              }
 -              break;
 +              oi.sizep = &size;
 +              if (sha1_object_info_extended(sha1, &oi, flags) < 0)
 +                      die("git cat-file: could not get object info");
 +              printf("%lu\n", size);
 +              return 0;
  
        case 'e':
                return !has_sha1_file(sha1);
@@@ -233,6 -225,7 +234,7 @@@ static void print_object_or_die(int fd
  
  struct batch_options {
        int enabled;
+       int follow_symlinks;
        int print_contents;
        const char *format;
  };
@@@ -241,12 -234,44 +243,44 @@@ static int batch_one_object(const char 
                            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;
        }
@@@ -332,8 -357,8 +366,8 @@@ static int batch_objects(struct batch_o
  }
  
  static const char * const cat_file_usage[] = {
 -      N_("git cat-file (-t | -s | -e | -p | <type> | --textconv) <object>"),
 +      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 -376,8 +385,8 @@@ static int batch_option_callback(const 
  {
        struct batch_options *bo = opt->value;
  
-       if (unset) {
-               memset(bo, 0, sizeof(*bo));
-               return 0;
+       if (bo->enabled) {
+               return 1;
        }
  
        bo->enabled = 1;
@@@ -368,30 -392,32 +401,32 @@@ int cmd_cat_file(int argc, const char *
        int opt = 0;
        const char *exp_type = NULL, *obj_name = NULL;
        struct batch_options batch = {0};
 +      int unknown_type = 0;
  
        const struct option options[] = {
                OPT_GROUP(N_("<type> can be one of: blob, tree, commit, tag")),
 -              OPT_SET_INT('t', NULL, &opt, N_("show object type"), 't'),
 -              OPT_SET_INT('s', NULL, &opt, N_("show object size"), 's'),
 -              OPT_SET_INT('e', NULL, &opt,
 +              OPT_CMDMODE('t', NULL, &opt, N_("show object type"), 't'),
 +              OPT_CMDMODE('s', NULL, &opt, N_("show object size"), 's'),
 +              OPT_CMDMODE('e', NULL, &opt,
                            N_("exit with zero when there's no error"), 'e'),
 -              OPT_SET_INT('p', NULL, &opt, N_("pretty-print object's content"), 'p'),
 -              OPT_SET_INT(0, "textconv", &opt,
 +              OPT_CMDMODE('p', NULL, &opt, N_("pretty-print object's content"), 'p'),
 +              OPT_CMDMODE(0, "textconv", &opt,
                            N_("for blob objects, run textconv on object's content"), 'c'),
 +              OPT_BOOL( 0, "allow-unknown-type", &unknown_type,
 +                        N_("allow -s and -t to work with broken/corrupt objects")),
                { OPTION_CALLBACK, 0, "batch", &batch, "format",
                        N_("show info and content of objects fed from the standard input"),
                        PARSE_OPT_OPTARG, batch_option_callback },
                { 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()
        };
  
        git_config(git_cat_file_config, NULL);
  
 -      if (argc != 3 && argc != 2)
 -              usage_with_options(cat_file_usage, options);
 -
        argc = parse_options(argc, argv, prefix, options, cat_file_usage, 0);
  
        if (opt) {
                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);
  
 -      return cat_one_file(opt, exp_type, obj_name);
 +      if (unknown_type && opt != 't' && opt != 's')
 +              die("git cat-file --allow-unknown-type: use with -s or -t");
 +      return cat_one_file(opt, exp_type, obj_name, unknown_type);
  }
diff --combined cache.h
index 9da9784824dc3a93db2c753613bb56866a356070,ec8d142d9f01a935a6fb700abb5c8832bf3b3b54..571c98f5e2406afad1352fcaddd1efff1af65cc1
+++ b/cache.h
@@@ -297,11 -297,8 +297,11 @@@ static inline unsigned int canon_mode(u
  #define RESOLVE_UNDO_CHANGED  (1 << 4)
  #define CACHE_TREE_CHANGED    (1 << 5)
  #define SPLIT_INDEX_ORDERED   (1 << 6)
 +#define UNTRACKED_CHANGED     (1 << 7)
  
  struct split_index;
 +struct untracked_cache;
 +
  struct index_state {
        struct cache_entry **cache;
        unsigned int version;
        struct hashmap name_hash;
        struct hashmap dir_hash;
        unsigned char sha1[20];
 +      struct untracked_cache *untracked;
  };
  
  extern struct index_state the_index;
@@@ -382,7 -378,6 +382,7 @@@ static inline enum object_type object_t
  
  /* Double-check local_repo_env below if you add to this list. */
  #define GIT_DIR_ENVIRONMENT "GIT_DIR"
 +#define GIT_COMMON_DIR_ENVIRONMENT "GIT_COMMON_DIR"
  #define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE"
  #define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE"
  #define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX"
@@@ -436,13 -431,11 +436,13 @@@ extern int is_inside_git_dir(void)
  extern char *git_work_tree_cfg;
  extern int is_inside_work_tree(void);
  extern const char *get_git_dir(void);
 +extern const char *get_git_common_dir(void);
  extern int is_git_directory(const char *path);
  extern char *get_object_directory(void);
  extern char *get_index_file(void);
  extern char *get_graft_file(void);
  extern int set_git_dir(const char *path);
 +extern int get_common_dir(struct strbuf *sb, const char *gitdir);
  extern const char *get_git_namespace(void);
  extern const char *strip_namespace(const char *namespaced_ref);
  extern const char *get_git_work_tree(void);
@@@ -567,8 -560,6 +567,8 @@@ extern void fill_stat_data(struct stat_
   * INODE_CHANGED, and DATA_CHANGED.
   */
  extern int match_stat_data(const struct stat_data *sd, struct stat *st);
 +extern int match_stat_data_racy(const struct index_state *istate,
 +                              const struct stat_data *sd, struct stat *st);
  
  extern void fill_stat_cache_info(struct cache_entry *ce, struct stat *st);
  
@@@ -629,7 -620,6 +629,7 @@@ extern int core_apply_sparse_checkout
  extern int precomposed_unicode;
  extern int protect_hfs;
  extern int protect_ntfs;
 +extern int git_db_env, git_index_env, git_graft_env, git_common_dir_env;
  
  /*
   * Include broken refs in all ref iterations, which will
@@@ -700,19 -690,18 +700,19 @@@ extern int check_repository_format(void
  
  extern char *mksnpath(char *buf, size_t n, const char *fmt, ...)
        __attribute__((format (printf, 3, 4)));
 -extern char *git_snpath(char *buf, size_t n, const char *fmt, ...)
 -      __attribute__((format (printf, 3, 4)));
 +extern void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
 +      __attribute__((format (printf, 2, 3)));
  extern char *git_pathdup(const char *fmt, ...)
        __attribute__((format (printf, 1, 2)));
  extern char *mkpathdup(const char *fmt, ...)
        __attribute__((format (printf, 1, 2)));
  
  /* Return a statically allocated filename matching the sha1 signature */
 -extern char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
 -extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
 -extern char *git_path_submodule(const char *path, const char *fmt, ...)
 +extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
 +extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
 +extern const char *git_path_submodule(const char *path, const char *fmt, ...)
        __attribute__((format (printf, 2, 3)));
 +extern void report_linked_checkout_garbage(void);
  
  /*
   * Return the name of the file in the local object database that would
@@@ -857,6 -846,7 +857,6 @@@ enum scld_error safe_create_leading_dir
  enum scld_error safe_create_leading_directories_const(const char *path);
  
  int mkdir_in_gitdir(const char *path);
 -extern void home_config_paths(char **global, char **xdg, char *file);
  extern char *expand_user_path(const char *path);
  const char *enter_repo(const char *path, int strict);
  static inline int is_absolute_path(const char *path)
@@@ -876,16 -866,8 +876,16 @@@ char *strip_path_suffix(const char *pat
  int daemon_avoid_alias(const char *path);
  extern int is_ntfs_dotgit(const char *name);
  
 +/**
 + * Return a newly allocated string with the evaluation of
 + * "$XDG_CONFIG_HOME/git/$filename" if $XDG_CONFIG_HOME is non-empty, otherwise
 + * "$HOME/.config/git/$filename". Return NULL upon error.
 + */
 +extern char *xdg_config_home(const char *filename);
 +
  /* object replacement */
  #define LOOKUP_REPLACE_OBJECT 1
 +#define LOOKUP_UNKNOWN_OBJECT 2
  extern void *read_sha1_file_extended(const unsigned char *sha1, enum object_type *type, unsigned long *size, unsigned flag);
  static inline void *read_sha1_file(const unsigned char *sha1, enum object_type *type, unsigned long *size)
  {
@@@ -922,7 -904,6 +922,7 @@@ static inline const unsigned char *look
  extern int sha1_object_info(const unsigned char *, unsigned long *);
  extern int hash_sha1_file(const void *buf, unsigned long len, const char *type, unsigned char *sha1);
  extern int write_sha1_file(const void *buf, unsigned long len, const char *type, unsigned char *return_sha1);
 +extern int hash_sha1_file_literally(const void *buf, unsigned long len, const char *type, unsigned char *sha1, unsigned flags);
  extern int pretend_sha1_file(void *, unsigned long, enum object_type, unsigned char *);
  extern int force_object_loose(const unsigned char *sha1, time_t mtime);
  extern int git_open_noatime(const char *name);
@@@ -971,15 -952,21 +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);
@@@ -1358,7 -1345,6 +1364,7 @@@ struct object_info 
        unsigned long *sizep;
        unsigned long *disk_sizep;
        unsigned char *delta_base_sha1;
 +      struct strbuf *typename;
  
        /* Response */
        enum {
@@@ -1547,13 -1533,9 +1553,13 @@@ extern const char *git_mailmap_blob
  extern void maybe_flush_or_die(FILE *, const char *);
  __attribute__((format (printf, 2, 3)))
  extern void fprintf_or_die(FILE *, const char *fmt, ...);
 +
 +#define COPY_READ_ERROR (-2)
 +#define COPY_WRITE_ERROR (-3)
  extern int copy_fd(int ifd, int ofd);
  extern int copy_file(const char *dst, const char *src, int mode);
  extern int copy_file_with_time(const char *dst, const char *src, int mode);
 +
  extern void write_or_die(int fd, const void *buf, size_t count);
  extern int write_or_whine(int fd, const void *buf, size_t count, const char *msg);
  extern int write_or_whine_pipe(int fd, const void *buf, size_t count, const char *msg);
@@@ -1567,8 -1549,6 +1573,8 @@@ static inline ssize_t write_str_in_full
  {
        return write_in_full(fd, str, strlen(str));
  }
 +__attribute__((format (printf, 3, 4)))
 +extern int write_file(const char *path, int fatal, const char *fmt, ...);
  
  /* pager.c */
  extern void setup_pager(void);
diff --combined sha1_name.c
index 6de8c87c8a120186eef41eb63c724a6e1a80e0c6,0c265150103426042a0c7aee120a0a4bf9eace9e..46218ba02c6c1fd87cf51fa189843da3b7950dbe
@@@ -6,7 -6,6 +6,7 @@@
  #include "tree-walk.h"
  #include "refs.h"
  #include "remote.h"
 +#include "dir.h"
  
  static int get_sha1_oneline(const char *, unsigned char *, struct commit_list *);
  
@@@ -1238,13 -1237,14 +1238,13 @@@ static void diagnose_invalid_sha1_path(
                                       const char *object_name,
                                       int object_name_len)
  {
 -      struct stat st;
        unsigned char sha1[20];
        unsigned mode;
  
        if (!prefix)
                prefix = "";
  
 -      if (!lstat(filename, &st))
 +      if (file_exists(filename))
                die("Path '%s' exists on disk, but not in '%.*s'.",
                    filename, object_name_len, object_name);
        if (errno == ENOENT || errno == ENOTDIR) {
@@@ -1271,6 -1271,7 +1271,6 @@@ static void diagnose_invalid_index_path
                                        const char *prefix,
                                        const char *filename)
  {
 -      struct stat st;
        const struct cache_entry *ce;
        int pos;
        unsigned namelen = strlen(filename);
                            ce_stage(ce), filename);
        }
  
 -      if (!lstat(filename, &st))
 +      if (file_exists(filename))
                die("Path '%s' exists on disk, but not in the index.", filename);
        if (errno == ENOENT || errno == ENOTDIR)
                die("Path '%s' does not exist (neither on disk nor in the index).",
@@@ -1433,11 -1434,19 +1433,19 @@@ static int get_sha1_with_context_1(cons
                        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 -1477,7 +1476,7 @@@ void maybe_die_on_misspelt_object_name(
  
  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);
  }
diff --combined t/t1006-cat-file.sh
index 4f225db9a72729e99f5eaab0b81ec579691b34c7,a494013ed32d82ffa0d02d04ecf8a0836c33044b..93a4794930bfebc99504a245392f32a3c92b3dde
@@@ -47,18 -47,6 +47,18 @@@ $content
        test_cmp expect actual
      '
  
 +    test_expect_success "Type of $type is correct using --allow-unknown-type" '
 +      echo $type >expect &&
 +      git cat-file -t --allow-unknown-type $sha1 >actual &&
 +      test_cmp expect actual
 +    '
 +
 +    test_expect_success "Size of $type is correct using --allow-unknown-type" '
 +      echo $size >expect &&
 +      git cat-file -s --allow-unknown-type $sha1 >actual &&
 +      test_cmp expect actual
 +    '
 +
      test -z "$content" ||
      test_expect_success "Content of $type is correct" '
        maybe_remove_timestamp "$content" $no_ts >expect &&
      '
  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" = \
@@@ -308,37 -303,202 +315,236 @@@ test_expect_success '%(deltabase) repor
        }
  '
  
 +bogus_type="bogus"
 +bogus_content="bogus"
 +bogus_size=$(strlen "$bogus_content")
 +bogus_sha1=$(echo_without_newline "$bogus_content" | git hash-object -t $bogus_type --literally -w --stdin)
 +
 +test_expect_success "Type of broken object is correct" '
 +      echo $bogus_type >expect &&
 +      git cat-file -t --allow-unknown-type $bogus_sha1 >actual &&
 +      test_cmp expect actual
 +'
 +
 +test_expect_success "Size of broken object is correct" '
 +      echo $bogus_size >expect &&
 +      git cat-file -s --allow-unknown-type $bogus_sha1 >actual &&
 +      test_cmp expect actual
 +'
 +bogus_type="abcdefghijklmnopqrstuvwxyz1234679"
 +bogus_content="bogus"
 +bogus_size=$(strlen "$bogus_content")
 +bogus_sha1=$(echo_without_newline "$bogus_content" | git hash-object -t $bogus_type --literally -w --stdin)
 +
 +test_expect_success "Type of broken object is correct when type is large" '
 +      echo $bogus_type >expect &&
 +      git cat-file -t --allow-unknown-type $bogus_sha1 >actual &&
 +      test_cmp expect actual
 +'
 +
 +test_expect_success "Size of large broken object is correct when type is large" '
 +      echo $bogus_size >expect &&
 +      git cat-file -s --allow-unknown-type $bogus_sha1 >actual &&
 +      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