Merge branch 'sp/safecrlf'
authorJunio C Hamano <gitster@pobox.com>
Sun, 17 Feb 2008 01:59:20 +0000 (17:59 -0800)
committerJunio C Hamano <gitster@pobox.com>
Sun, 17 Feb 2008 01:59:20 +0000 (17:59 -0800)
* sp/safecrlf:
safecrlf: Add mechanism to warn about irreversible crlf conversions

1  2 
Documentation/config.txt
builtin-apply.c
builtin-blame.c
cache.h
config.c
convert.c
diff.c
environment.c
diff --combined Documentation/config.txt
index f9bdb164e054ec2d633baab3da69ddd47e14931f,7b8cae1541ea6987699472b1b44d593fbddd5dac..f2f6a774e044e03e78d83e4e77aedcff122f5688
@@@ -139,6 -139,51 +139,51 @@@ core.autocrlf:
        "text" (i.e. be subjected to the autocrlf mechanism) is
        decided purely based on the contents.
  
+ core.safecrlf::
+       If true, makes git check if converting `CRLF` as controlled by
+       `core.autocrlf` is reversible.  Git will verify if a command
+       modifies a file in the work tree either directly or indirectly.
+       For example, committing a file followed by checking out the
+       same file should yield the original file in the work tree.  If
+       this is not the case for the current setting of
+       `core.autocrlf`, git will reject the file.  The variable can
+       be set to "warn", in which case git will only warn about an
+       irreversible conversion but continue the operation.
+ +
+ CRLF conversion bears a slight chance of corrupting data.
+ autocrlf=true will convert CRLF to LF during commit and LF to
+ CRLF during checkout.  A file that contains a mixture of LF and
+ CRLF before the commit cannot be recreated by git.  For text
+ files this is the right thing to do: it corrects line endings
+ such that we have only LF line endings in the repository.
+ But for binary files that are accidentally classified as text the
+ conversion can corrupt data.
+ +
+ If you recognize such corruption early you can easily fix it by
+ setting the conversion type explicitly in .gitattributes.  Right
+ after committing you still have the original file in your work
+ tree and this file is not yet corrupted.  You can explicitly tell
+ git that this file is binary and git will handle the file
+ appropriately.
+ +
+ Unfortunately, the desired effect of cleaning up text files with
+ mixed line endings and the undesired effect of corrupting binary
+ files cannot be distinguished.  In both cases CRLFs are removed
+ in an irreversible way.  For text files this is the right thing
+ to do because CRLFs are line endings, while for binary files
+ converting CRLFs corrupts data.
+ +
+ Note, this safety check does not mean that a checkout will generate a
+ file identical to the original file for a different setting of
+ `core.autocrlf`, but only for the current one.  For example, a text
+ file with `LF` would be accepted with `core.autocrlf=input` and could
+ later be checked out with `core.autocrlf=true`, in which case the
+ resulting file would contain `CRLF`, although the original file
+ contained `LF`.  However, in both work trees the line endings would be
+ consistent, that is either all `LF` or all `CRLF`, but never mixed.  A
+ file with mixed line endings would be reported by the `core.safecrlf`
+ mechanism.
  core.symlinks::
        If false, symbolic links are checked out as small plain files that
        contain the link text. linkgit:git-update-index[1] and
@@@ -333,7 -378,7 +378,7 @@@ branch.autosetupmerge:
        so that linkgit:git-pull[1] will appropriately merge from that
        remote branch.  Note that even if this option is not set,
        this behavior can be chosen per-branch using the `--track`
 -      and `--no-track` options.  This option defaults to false.
 +      and `--no-track` options.  This option defaults to true.
  
  branch.<name>.remote::
        When in branch <name>, it tells `git fetch` which remote to fetch.
@@@ -766,12 -811,6 +811,12 @@@ pack.indexVersion:
        whenever the corresponding pack is larger than 2 GB.  Otherwise
        the default is 1.
  
 +pack.packSizeLimit:
 +      The default maximum size of a pack.  This setting only affects
 +      packing to a file, i.e. the git:// protocol is unaffected.  It
 +      can be overridden by the `\--max-pack-size` option of
 +      linkgit:git-repack[1].
 +
  pull.octopus::
        The default merge strategy to use when pulling multiple branches
        at once.
diff --combined builtin-apply.c
index 46dad5b2a1ce6d70d540df8a40da94702dde68c7,3b5618d43487bf819682dbc40678b7ae5ba0983e..6a88ff018df8890af8ef2e17340d5969dc1e396b
@@@ -1430,7 -1430,7 +1430,7 @@@ static int read_old_data(struct stat *s
        case S_IFREG:
                if (strbuf_read_file(buf, path, st->st_size) != st->st_size)
                        return error("unable to open or read %s", path);
-               convert_to_git(path, buf->buf, buf->len, buf);
+               convert_to_git(path, buf->buf, buf->len, buf, 0);
                return 0;
        default:
                return -1;
@@@ -1946,7 -1946,7 +1946,7 @@@ static int read_file_or_gitlink(struct 
        if (!ce)
                return 0;
  
 -      if (S_ISGITLINK(ntohl(ce->ce_mode))) {
 +      if (S_ISGITLINK(ce->ce_mode)) {
                strbuf_grow(buf, 100);
                strbuf_addf(buf, "Subproject commit %s\n", sha1_to_hex(ce->sha1));
        } else {
@@@ -2023,7 -2023,7 +2023,7 @@@ static int check_to_create_blob(const c
  
  static int verify_index_match(struct cache_entry *ce, struct stat *st)
  {
 -      if (S_ISGITLINK(ntohl(ce->ce_mode))) {
 +      if (S_ISGITLINK(ce->ce_mode)) {
                if (!S_ISDIR(st->st_mode))
                        return -1;
                return 0;
@@@ -2082,12 -2082,12 +2082,12 @@@ static int check_patch(struct patch *pa
                                return error("%s: does not match index",
                                             old_name);
                        if (cached)
 -                              st_mode = ntohl(ce->ce_mode);
 +                              st_mode = ce->ce_mode;
                } else if (stat_ret < 0)
                        return error("%s: %s", old_name, strerror(errno));
  
                if (!cached)
 -                      st_mode = ntohl(ce_mode_from_stat(ce, st.st_mode));
 +                      st_mode = ce_mode_from_stat(ce, st.st_mode);
  
                if (patch->is_new < 0)
                        patch->is_new = 0;
@@@ -2388,7 -2388,7 +2388,7 @@@ static void add_index_file(const char *
        ce = xcalloc(1, ce_size);
        memcpy(ce->name, path, namelen);
        ce->ce_mode = create_ce_mode(mode);
 -      ce->ce_flags = htons(namelen);
 +      ce->ce_flags = namelen;
        if (S_ISGITLINK(mode)) {
                const char *s = buf;
  
@@@ -2746,8 -2746,6 +2746,8 @@@ static int apply_patch(int fd, const ch
  static int git_apply_config(const char *var, const char *value)
  {
        if (!strcmp(var, "apply.whitespace")) {
 +              if (!value)
 +                      return config_error_nonbool(var);
                apply_default_whitespace = xstrdup(value);
                return 0;
        }
diff --combined builtin-blame.c
index c7e68874e7f02ac150343ab6d7a0aa95601f2ba3,c361ee194e0bbbfafdf97d92cf8d68a8eb638dfd..1cf254dcca7f814694741ce5ea1ec2b88a26706f
@@@ -2073,7 -2073,7 +2073,7 @@@ static struct commit *fake_working_tree
                if (strbuf_read(&buf, 0, 0) < 0)
                        die("read error %s from stdin", strerror(errno));
        }
-       convert_to_git(path, buf.buf, buf.len, &buf);
+       convert_to_git(path, buf.buf, buf.len, &buf, 0);
        origin->file.ptr = buf.buf;
        origin->file.size = buf.len;
        pretend_sha1_file(buf.buf, buf.len, OBJ_BLOB, origin->blob_sha1);
        if (!mode) {
                int pos = cache_name_pos(path, len);
                if (0 <= pos)
 -                      mode = ntohl(active_cache[pos]->ce_mode);
 +                      mode = active_cache[pos]->ce_mode;
                else
                        /* Let's not bother reading from HEAD tree */
                        mode = S_IFREG | 0644;
diff --combined cache.h
index 18fe8447f3bdff2a772716c9293e44e357178a4c,6003c83b2d5c004db3a59ccd18af59994baffe10..e1000bccb2470ad66038aabbf57b0af96e506f11
+++ b/cache.h
@@@ -3,7 -3,6 +3,7 @@@
  
  #include "git-compat-util.h"
  #include "strbuf.h"
 +#include "hash.h"
  
  #include SHA1_HEADER
  #include <zlib.h>
@@@ -95,116 -94,66 +95,116 @@@ struct cache_time 
   * We save the fields in big-endian order to allow using the
   * index file over NFS transparently.
   */
 +struct ondisk_cache_entry {
 +      struct cache_time ctime;
 +      struct cache_time mtime;
 +      unsigned int dev;
 +      unsigned int ino;
 +      unsigned int mode;
 +      unsigned int uid;
 +      unsigned int gid;
 +      unsigned int size;
 +      unsigned char sha1[20];
 +      unsigned short flags;
 +      char name[FLEX_ARRAY]; /* more */
 +};
 +
  struct cache_entry {
 -      struct cache_time ce_ctime;
 -      struct cache_time ce_mtime;
 +      struct cache_entry *next;
 +      unsigned int ce_ctime;
 +      unsigned int ce_mtime;
        unsigned int ce_dev;
        unsigned int ce_ino;
        unsigned int ce_mode;
        unsigned int ce_uid;
        unsigned int ce_gid;
        unsigned int ce_size;
 +      unsigned int ce_flags;
        unsigned char sha1[20];
 -      unsigned short ce_flags;
        char name[FLEX_ARRAY]; /* more */
  };
  
  #define CE_NAMEMASK  (0x0fff)
  #define CE_STAGEMASK (0x3000)
 -#define CE_UPDATE    (0x4000)
  #define CE_VALID     (0x8000)
  #define CE_STAGESHIFT 12
  
 -#define create_ce_flags(len, stage) htons((len) | ((stage) << CE_STAGESHIFT))
 -#define ce_namelen(ce) (CE_NAMEMASK & ntohs((ce)->ce_flags))
 +/* In-memory only */
 +#define CE_UPDATE    (0x10000)
 +#define CE_REMOVE    (0x20000)
 +#define CE_UPTODATE  (0x40000)
 +#define CE_UNHASHED  (0x80000)
 +
 +static inline unsigned create_ce_flags(size_t len, unsigned stage)
 +{
 +      if (len >= CE_NAMEMASK)
 +              len = CE_NAMEMASK;
 +      return (len | (stage << CE_STAGESHIFT));
 +}
 +
 +static inline size_t ce_namelen(const struct cache_entry *ce)
 +{
 +      size_t len = ce->ce_flags & CE_NAMEMASK;
 +      if (len < CE_NAMEMASK)
 +              return len;
 +      return strlen(ce->name + CE_NAMEMASK) + CE_NAMEMASK;
 +}
 +
  #define ce_size(ce) cache_entry_size(ce_namelen(ce))
 -#define ce_stage(ce) ((CE_STAGEMASK & ntohs((ce)->ce_flags)) >> CE_STAGESHIFT)
 +#define ondisk_ce_size(ce) ondisk_cache_entry_size(ce_namelen(ce))
 +#define ce_stage(ce) ((CE_STAGEMASK & (ce)->ce_flags) >> CE_STAGESHIFT)
 +#define ce_uptodate(ce) ((ce)->ce_flags & CE_UPTODATE)
 +#define ce_mark_uptodate(ce) ((ce)->ce_flags |= CE_UPTODATE)
  
  #define ce_permissions(mode) (((mode) & 0100) ? 0755 : 0644)
  static inline unsigned int create_ce_mode(unsigned int mode)
  {
        if (S_ISLNK(mode))
 -              return htonl(S_IFLNK);
 +              return S_IFLNK;
        if (S_ISDIR(mode) || S_ISGITLINK(mode))
 -              return htonl(S_IFGITLINK);
 -      return htonl(S_IFREG | ce_permissions(mode));
 +              return S_IFGITLINK;
 +      return S_IFREG | ce_permissions(mode);
  }
  static inline unsigned int ce_mode_from_stat(struct cache_entry *ce, unsigned int mode)
  {
        extern int trust_executable_bit, has_symlinks;
        if (!has_symlinks && S_ISREG(mode) &&
 -          ce && S_ISLNK(ntohl(ce->ce_mode)))
 +          ce && S_ISLNK(ce->ce_mode))
                return ce->ce_mode;
        if (!trust_executable_bit && S_ISREG(mode)) {
 -              if (ce && S_ISREG(ntohl(ce->ce_mode)))
 +              if (ce && S_ISREG(ce->ce_mode))
                        return ce->ce_mode;
                return create_ce_mode(0666);
        }
        return create_ce_mode(mode);
  }
 +static inline int ce_to_dtype(const struct cache_entry *ce)
 +{
 +      unsigned ce_mode = ntohl(ce->ce_mode);
 +      if (S_ISREG(ce_mode))
 +              return DT_REG;
 +      else if (S_ISDIR(ce_mode) || S_ISGITLINK(ce_mode))
 +              return DT_DIR;
 +      else if (S_ISLNK(ce_mode))
 +              return DT_LNK;
 +      else
 +              return DT_UNKNOWN;
 +}
  #define canon_mode(mode) \
        (S_ISREG(mode) ? (S_IFREG | ce_permissions(mode)) : \
        S_ISLNK(mode) ? S_IFLNK : S_ISDIR(mode) ? S_IFDIR : S_IFGITLINK)
  
  #define cache_entry_size(len) ((offsetof(struct cache_entry,name) + (len) + 8) & ~7)
 +#define ondisk_cache_entry_size(len) ((offsetof(struct ondisk_cache_entry,name) + (len) + 8) & ~7)
  
  struct index_state {
        struct cache_entry **cache;
        unsigned int cache_nr, cache_alloc, cache_changed;
        struct cache_tree *cache_tree;
        time_t timestamp;
 -      void *mmap;
 -      size_t mmap_size;
 +      void *alloc;
 +      unsigned name_hash_initialized : 1;
 +      struct hash_table name_hash;
  };
  
  extern struct index_state the_index;
  #define refresh_cache(flags) refresh_index(&the_index, (flags), NULL, NULL)
  #define ce_match_stat(ce, st, options) ie_match_stat(&the_index, (ce), (st), (options))
  #define ce_modified(ce, st, options) ie_modified(&the_index, (ce), (st), (options))
 +#define cache_name_exists(name, namelen) index_name_exists(&the_index, (name), (namelen))
  #endif
  
  enum object_type {
@@@ -315,7 -263,6 +315,7 @@@ extern int read_index_from(struct index
  extern int write_index(struct index_state *, int newfd);
  extern int discard_index(struct index_state *);
  extern int verify_path(const char *path);
 +extern int index_name_exists(struct index_state *istate, const char *name, int namelen);
  extern int index_name_pos(struct index_state *, const char *name, int namelen);
  #define ADD_CACHE_OK_TO_ADD 1         /* Ok to add */
  #define ADD_CACHE_OK_TO_REPLACE 2     /* Ok to replace file/directory */
@@@ -383,6 -330,14 +383,14 @@@ extern size_t packed_git_limit
  extern size_t delta_base_cache_limit;
  extern int auto_crlf;
  
+ enum safe_crlf {
+       SAFE_CRLF_FALSE = 0,
+       SAFE_CRLF_FAIL = 1,
+       SAFE_CRLF_WARN = 2,
+ };
+ extern enum safe_crlf safe_crlf;
  #define GIT_REPO_VERSION 0
  extern int repository_format_version;
  extern int check_repository_format(void);
@@@ -637,16 -592,11 +645,16 @@@ extern int git_parse_ulong(const char *
  extern int git_config_int(const char *, const char *);
  extern unsigned long git_config_ulong(const char *, const char *);
  extern int git_config_bool(const char *, const char *);
 +extern int git_config_string(const char **, const char *, const char *);
  extern int git_config_set(const char *, const char *);
  extern int git_config_set_multivar(const char *, const char *, const char *, int);
  extern int git_config_rename_section(const char *, const char *);
  extern const char *git_etc_gitconfig(void);
  extern int check_repository_format_version(const char *var, const char *value);
 +extern int git_env_bool(const char *, int);
 +extern int git_config_system(void);
 +extern int git_config_global(void);
 +extern int config_error_nonbool(const char *);
  
  #define MAX_GITNAME (1000)
  extern char git_default_email[MAX_GITNAME];
@@@ -666,12 -616,12 +674,12 @@@ extern int write_or_whine_pipe(int fd, 
  
  /* pager.c */
  extern void setup_pager(void);
 -extern char *pager_program;
 +extern const char *pager_program;
  extern int pager_in_use(void);
  extern int pager_use_color;
  
 -extern char *editor_program;
 -extern char *excludes_file;
 +extern const char *editor_program;
 +extern const char *excludes_file;
  
  /* base85 */
  int decode_85(char *dst, const char *line, int linelen);
@@@ -691,7 -641,8 +699,8 @@@ extern void trace_argv_printf(const cha
  
  /* convert.c */
  /* returns 1 if *dst was used */
- extern int convert_to_git(const char *path, const char *src, size_t len, struct strbuf *dst);
+ extern int convert_to_git(const char *path, const char *src, size_t len,
+                           struct strbuf *dst, enum safe_crlf checksafe);
  extern int convert_to_working_tree(const char *path, const char *src, size_t len, struct strbuf *dst);
  
  /* add */
diff --combined config.c
index 7881bbc7782b7937e450ad99c4bb6bda67c1f2e3,3256c99553564eedfcfc17e4b289d85a72bb145d..8064cae18290d66ee69f1a56e501391ecb317b3d
+++ b/config.c
@@@ -309,14 -309,6 +309,14 @@@ int git_config_bool(const char *name, c
        return git_config_int(name, value) != 0;
  }
  
 +int git_config_string(const char **dest, const char *var, const char *value)
 +{
 +      if (!value)
 +              return config_error_nonbool(var);
 +      *dest = xstrdup(value);
 +      return 0;
 +}
 +
  int git_default_config(const char *var, const char *value)
  {
        /* This needs a better name */
                return 0;
        }
  
+       if (!strcmp(var, "core.safecrlf")) {
+               if (value && !strcasecmp(value, "warn")) {
+                       safe_crlf = SAFE_CRLF_WARN;
+                       return 0;
+               }
+               safe_crlf = git_config_bool(var, value);
+               return 0;
+       }
        if (!strcmp(var, "user.name")) {
 +              if (!value)
 +                      return config_error_nonbool(var);
                strlcpy(git_default_name, value, sizeof(git_default_name));
                return 0;
        }
  
        if (!strcmp(var, "user.email")) {
 +              if (!value)
 +                      return config_error_nonbool(var);
                strlcpy(git_default_email, value, sizeof(git_default_email));
                return 0;
        }
  
 -      if (!strcmp(var, "i18n.commitencoding")) {
 -              git_commit_encoding = xstrdup(value);
 -              return 0;
 -      }
 -
 -      if (!strcmp(var, "i18n.logoutputencoding")) {
 -              git_log_output_encoding = xstrdup(value);
 -              return 0;
 -      }
 +      if (!strcmp(var, "i18n.commitencoding"))
 +              return git_config_string(&git_commit_encoding, var, value);
  
 +      if (!strcmp(var, "i18n.logoutputencoding"))
 +              return git_config_string(&git_log_output_encoding, var, value);
  
        if (!strcmp(var, "pager.color") || !strcmp(var, "color.pager")) {
                pager_use_color = git_config_bool(var,value);
                return 0;
        }
  
 -      if (!strcmp(var, "core.pager")) {
 -              pager_program = xstrdup(value);
 -              return 0;
 -      }
 +      if (!strcmp(var, "core.pager"))
 +              return git_config_string(&pager_program, var, value);
  
 -      if (!strcmp(var, "core.editor")) {
 -              editor_program = xstrdup(value);
 -              return 0;
 -      }
 +      if (!strcmp(var, "core.editor"))
 +              return git_config_string(&editor_program, var, value);
  
 -      if (!strcmp(var, "core.excludesfile")) {
 -              if (!value)
 -                      die("core.excludesfile without value");
 -              excludes_file = xstrdup(value);
 -              return 0;
 -      }
 +      if (!strcmp(var, "core.excludesfile"))
 +              return git_config_string(&excludes_file, var, value);
  
        if (!strcmp(var, "core.whitespace")) {
 +              if (!value)
 +                      return config_error_nonbool(var);
                whitespace_rule_cfg = parse_whitespace_rule(value);
                return 0;
        }
@@@ -493,22 -501,6 +502,22 @@@ const char *git_etc_gitconfig(void
        return system_wide;
  }
  
 +int git_env_bool(const char *k, int def)
 +{
 +      const char *v = getenv(k);
 +      return v ? git_config_bool(k, v) : def;
 +}
 +
 +int git_config_system(void)
 +{
 +      return !git_env_bool("GIT_CONFIG_NOSYSTEM", 0);
 +}
 +
 +int git_config_global(void)
 +{
 +      return !git_env_bool("GIT_CONFIG_NOGLOBAL", 0);
 +}
 +
  int git_config(config_fn_t fn)
  {
        int ret = 0;
         * config file otherwise. */
        filename = getenv(CONFIG_ENVIRONMENT);
        if (!filename) {
 -              if (!access(git_etc_gitconfig(), R_OK))
 +              if (git_config_system() && !access(git_etc_gitconfig(), R_OK))
                        ret += git_config_from_file(fn, git_etc_gitconfig());
                home = getenv("HOME");
                filename = getenv(CONFIG_LOCAL_ENVIRONMENT);
                        filename = repo_config = xstrdup(git_path("config"));
        }
  
 -      if (home) {
 +      if (git_config_global() && home) {
                char *user_config = xstrdup(mkpath("%s/.gitconfig", home));
                if (!access(user_config, R_OK))
                        ret = git_config_from_file(fn, user_config);
@@@ -718,17 -710,12 +727,17 @@@ static ssize_t find_beginning_of_line(c
        size_t equal_offset = size, bracket_offset = size;
        ssize_t offset;
  
 +contline:
        for (offset = offset_-2; offset > 0
                        && contents[offset] != '\n'; offset--)
                switch (contents[offset]) {
                        case '=': equal_offset = offset; break;
                        case ']': bracket_offset = offset; break;
                }
 +      if (offset > 0 && contents[offset-1] == '\\') {
 +              offset_ = offset;
 +              goto contline;
 +      }
        if (bracket_offset < equal_offset) {
                *found_bracket = 1;
                offset = bracket_offset+1;
@@@ -1096,12 -1083,3 +1105,12 @@@ int git_config_rename_section(const cha
        free(config_filename);
        return ret;
  }
 +
 +/*
 + * Call this to report error for your variable that should not
 + * get a boolean value (i.e. "[my] var" means "true").
 + */
 +int config_error_nonbool(const char *var)
 +{
 +      return error("Missing value for '%s'", var);
 +}
diff --combined convert.c
index 552707e8e65997ebfc2120887783c4fc5698e19f,3bbf0c51c24239ba47353a200527268bb17e250a..d8c94cb3edeceff7a79b2f5e6ba7cad250e6b301
+++ b/convert.c
@@@ -85,8 -85,39 +85,39 @@@ static int is_binary(unsigned long size
        return 0;
  }
  
+ static void check_safe_crlf(const char *path, int action,
+                             struct text_stat *stats, enum safe_crlf checksafe)
+ {
+       if (!checksafe)
+               return;
+       if (action == CRLF_INPUT || auto_crlf <= 0) {
+               /*
+                * CRLFs would not be restored by checkout:
+                * check if we'd remove CRLFs
+                */
+               if (stats->crlf) {
+                       if (checksafe == SAFE_CRLF_WARN)
+                               warning("CRLF will be replaced by LF in %s.", path);
+                       else /* i.e. SAFE_CRLF_FAIL */
+                               die("CRLF would be replaced by LF in %s.", path);
+               }
+       } else if (auto_crlf > 0) {
+               /*
+                * CRLFs would be added by checkout:
+                * check if we have "naked" LFs
+                */
+               if (stats->lf != stats->crlf) {
+                       if (checksafe == SAFE_CRLF_WARN)
+                               warning("LF will be replaced by CRLF in %s", path);
+                       else /* i.e. SAFE_CRLF_FAIL */
+                               die("LF would be replaced by CRLF in %s", path);
+               }
+       }
+ }
  static int crlf_to_git(const char *path, const char *src, size_t len,
-                        struct strbuf *buf, int action)
+                        struct strbuf *buf, int action, enum safe_crlf checksafe)
  {
        struct text_stat stats;
        char *dst;
                return 0;
  
        gather_stats(src, len, &stats);
-       /* No CR? Nothing to convert, regardless. */
-       if (!stats.cr)
-               return 0;
  
        if (action == CRLF_GUESS) {
                /*
                        return 0;
        }
  
+       check_safe_crlf(path, action, &stats, checksafe);
+       /* Optimization: No CR? Nothing to convert, regardless. */
+       if (!stats.cr)
+               return 0;
        /* only grow if not in place */
        if (strbuf_avail(buf) + buf->len < len)
                strbuf_grow(buf, len - buf->len);
@@@ -326,14 -360,14 +360,14 @@@ static int read_convert_config(const ch
  
        if (!strcmp("smudge", ep)) {
                if (!value)
 -                      return error("%s: lacks value", var);
 +                      return config_error_nonbool(var);
                drv->smudge = strdup(value);
                return 0;
        }
  
        if (!strcmp("clean", ep)) {
                if (!value)
 -                      return error("%s: lacks value", var);
 +                      return config_error_nonbool(var);
                drv->clean = strdup(value);
                return 0;
        }
@@@ -536,7 -570,8 +570,8 @@@ static int git_path_check_ident(const c
        return !!ATTR_TRUE(value);
  }
  
- int convert_to_git(const char *path, const char *src, size_t len, struct strbuf *dst)
+ int convert_to_git(const char *path, const char *src, size_t len,
+                    struct strbuf *dst, enum safe_crlf checksafe)
  {
        struct git_attr_check check[3];
        int crlf = CRLF_GUESS;
                src = dst->buf;
                len = dst->len;
        }
-       ret |= crlf_to_git(path, src, len, dst, crlf);
+       ret |= crlf_to_git(path, src, len, dst, crlf, checksafe);
        if (ret) {
                src = dst->buf;
                len = dst->len;
diff --combined diff.c
index 41ec2ced78d7f0d8fe86eea0285b76f70843a3a8,562c20ed40827070d15e241b342636a24f7cd107..58fe7750f9ca7d63192164a3ac8da27553489d11
--- 1/diff.c
--- 2/diff.c
+++ b/diff.c
@@@ -57,7 -57,7 +57,7 @@@ static int parse_diff_color_slot(const 
  static struct ll_diff_driver {
        const char *name;
        struct ll_diff_driver *next;
 -      char *cmd;
 +      const char *cmd;
  } *user_diff, **user_diff_tail;
  
  /*
@@@ -86,7 -86,10 +86,7 @@@ static int parse_lldiff_command(const c
                user_diff_tail = &(drv->next);
        }
  
 -      if (!value)
 -              return error("%s: lacks value", var);
 -      drv->cmd = strdup(value);
 -      return 0;
 +      return git_config_string(&(drv->cmd), var, value);
  }
  
  /*
@@@ -155,16 -158,16 +155,16 @@@ int git_diff_ui_config(const char *var
                return 0;
        }
        if (!strcmp(var, "diff.external")) {
 +              if (!value)
 +                      return config_error_nonbool(var);
                external_diff_cmd_cfg = xstrdup(value);
                return 0;
        }
        if (!prefixcmp(var, "diff.")) {
                const char *ep = strrchr(var, '.');
  
 -              if (ep != var + 4) {
 -                      if (!strcmp(ep, ".command"))
 -                              return parse_lldiff_command(var, ep, value);
 -              }
 +              if (ep != var + 4 && !strcmp(ep, ".command"))
 +                      return parse_lldiff_command(var, ep, value);
        }
  
        return git_diff_basic_config(var, value);
@@@ -174,8 -177,6 +174,8 @@@ int git_diff_basic_config(const char *v
  {
        if (!prefixcmp(var, "diff.color.") || !prefixcmp(var, "color.diff.")) {
                int slot = parse_diff_color_slot(var, 11);
 +              if (!value)
 +                      return config_error_nonbool(var);
                color_parse(value, var, diff_colors[slot]);
                return 0;
        }
        if (!prefixcmp(var, "diff.")) {
                const char *ep = strrchr(var, '.');
                if (ep != var + 4) {
 -                      if (!strcmp(ep, ".funcname"))
 +                      if (!strcmp(ep, ".funcname")) {
 +                              if (!value)
 +                                      return config_error_nonbool(var);
                                return parse_funcname_pattern(var, ep, value);
 +                      }
                }
        }
  
@@@ -1013,7 -1011,6 +1013,7 @@@ static void checkdiff_consume(void *pri
        char *err;
  
        if (line[0] == '+') {
 +              data->lineno++;
                data->status = check_and_emit_line(line + 1, len - 1,
                    data->ws_rule, NULL, NULL, NULL, NULL);
                if (!data->status)
                emit_line(set, reset, line, 1);
                (void)check_and_emit_line(line + 1, len - 1, data->ws_rule,
                    stdout, set, reset, ws);
 -              data->lineno++;
        } else if (line[0] == ' ')
                data->lineno++;
        else if (line[0] == '@') {
                char *plus = strchr(line, '+');
                if (plus)
 -                      data->lineno = strtol(plus, NULL, 10);
 +                      data->lineno = strtol(plus, NULL, 10) - 1;
                else
                        die("invalid diff");
        }
@@@ -1512,22 -1510,17 +1512,22 @@@ static int reuse_worktree_file(const ch
        if (pos < 0)
                return 0;
        ce = active_cache[pos];
 -      if ((lstat(name, &st) < 0) ||
 -          !S_ISREG(st.st_mode) || /* careful! */
 -          ce_match_stat(ce, &st, 0) ||
 -          hashcmp(sha1, ce->sha1))
 +
 +      /*
 +       * This is not the sha1 we are looking for, or
 +       * unreusable because it is not a regular file.
 +       */
 +      if (hashcmp(sha1, ce->sha1) || !S_ISREG(ce->ce_mode))
                return 0;
 -      /* we return 1 only when we can stat, it is a regular file,
 -       * stat information matches, and sha1 recorded in the cache
 -       * matches.  I.e. we know the file in the work tree really is
 -       * the same as the <name, sha1> pair.
 +
 +      /*
 +       * If ce matches the file in the work tree, we can reuse it.
         */
 -      return 1;
 +      if (ce_uptodate(ce) ||
 +          (!lstat(name, &st) && !ce_match_stat(ce, &st, 0)))
 +              return 1;
 +
 +      return 0;
  }
  
  static int populate_from_stdin(struct diff_filespec *s)
@@@ -1631,7 -1624,7 +1631,7 @@@ int diff_populate_filespec(struct diff_
                 * Convert from working tree format to canonical git format
                 */
                strbuf_init(&buf, 0);
-               if (convert_to_git(s->path, s->data, s->size, &buf)) {
+               if (convert_to_git(s->path, s->data, s->size, &buf, safe_crlf)) {
                        size_t size = 0;
                        munmap(s->data, s->size);
                        s->should_munmap = 0;
diff --combined environment.c
index fa3633372b8249899ff1e57a2f1431de029b211d,e351e99ed76827c2b341343a63aecca426867214..3527f1663f3f948085ab56a81609ed9764b4e6e9
@@@ -30,11 -30,12 +30,12 @@@ int core_compression_seen
  size_t packed_git_window_size = DEFAULT_PACKED_GIT_WINDOW_SIZE;
  size_t packed_git_limit = DEFAULT_PACKED_GIT_LIMIT;
  size_t delta_base_cache_limit = 16 * 1024 * 1024;
 -char *pager_program;
 +const char *pager_program;
  int pager_use_color = 1;
 -char *editor_program;
 -char *excludes_file;
 +const char *editor_program;
 +const char *excludes_file;
  int auto_crlf = 0;    /* 1: both ways, -1: only when adding git objects */
+ enum safe_crlf safe_crlf = SAFE_CRLF_WARN;
  unsigned whitespace_rule_cfg = WS_DEFAULT_RULE;
  
  /* This is set by setup_git_dir_gently() and/or git_default_config() */