add: introduce "--renormalize"

Make it safer to normalize the line endings in a repository. Files that had been commited with CRLF will be commited with LF. The old way to normalize a repo was like this: # Make sure that there are not untracked files $ echo "* text=auto" >.gitattributes $ git read-tree --empty $ git add . $ git commit -m "Introduce end-of-line normalization" The user must make sure that there are no untracked files, otherwise they would have been added and tracked from now on. The new "add --renormalize" does not add untracked files: $ echo "* text=auto" >.gitattributes $ git add --renormalize . $ git commit -m "Introduce end-of-line normalization" Note that "git add --renormalize <pathspec>" is the short form for "git add -u --renormalize <pathspec>". While at it, document that the same renormalization may be needed, whenever a clean filter is added or changed. Helped-By: Junio C Hamano <gitster@pobox.com> Signed-off-by: Torsten Bögershausen <tboegi@web.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Torsten Bögershausen committed Nov 16, 2017 at 17:38 UTC 9472935d81eaf9faed771878c9df0216ae0d9045
7 files changed +102 -18
Documentation/git-add.txt
+8 -1
@@ -10,7 +10,7 @@ SYNOPSIS
10 [verse]
11 'git add' [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p]
12 [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]]
13 - [--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing]
13 + [--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize]
14 [--chmod=(+|-)x] [--] [<pathspec>...]
15
16 DESCRIPTION
@@ -175,6 +175,13 @@ for "git add --no-all <pathspec>...", i.e. ignored removed files.
175 warning (e.g., if you are manually performing operations on
176 submodules).
177
178 +--renormalize::
179 + Apply the "clean" process freshly to all tracked files to
180 + forcibly add them again to the index. This is useful after
181 + changing `core.autocrlf` configuration or the `text` attribute
182 + in order to correct files added with wrong CRLF/LF line endings.
183 + This option implies `-u`.
184 +
185 --chmod=(+|-)x::
186 Override the executable bit of the added files. The executable
187 bit is only changed in the index, the files on disk are left
Documentation/gitattributes.txt
+4 -2
@@ -232,8 +232,7 @@ From a clean working directory:
232
233 -------------------------------------------------
234 $ echo "* text=auto" >.gitattributes
235 -$ git read-tree --empty # Clean index, force re-scan of working directory
236 -$ git add .
235 +$ git add --renormalize .
236 $ git status # Show files that will be normalized
237 $ git commit -m "Introduce end-of-line normalization"
238 -------------------------------------------------
@@ -328,6 +327,9 @@ You can declare that a filter turns a content that by itself is unusable
327 into a usable content by setting the filter.<driver>.required configuration
328 variable to `true`.
329
330 +Note: Whenever the clean filter is changed, the repo should be renormalized:
331 +$ git add --renormalize .
332 +
333 For example, in .gitattributes, you would assign the `filter`
334 attribute for paths.
335
builtin/add.c
+26 -2
@@ -26,6 +26,7 @@ static const char * const builtin_add_usage[] = {
26 };
27 static int patch_interactive, add_interactive, edit_interactive;
28 static int take_worktree_changes;
29 +static int add_renormalize;
30
31 struct update_callback_data {
32 int flags;
@@ -123,6 +124,25 @@ int add_files_to_cache(const char *prefix,
124 return !!data.add_errors;
125 }
126
127 +static int renormalize_tracked_files(const struct pathspec *pathspec, int flags)
128 +{
129 + int i, retval = 0;
130 +
131 + for (i = 0; i < active_nr; i++) {
132 + struct cache_entry *ce = active_cache[i];
133 +
134 + if (ce_stage(ce))
135 + continue; /* do not touch unmerged paths */
136 + if (!S_ISREG(ce->ce_mode) && !S_ISLNK(ce->ce_mode))
137 + continue; /* do not touch non blobs */
138 + if (pathspec && !ce_path_match(ce, pathspec, NULL))
139 + continue;
140 + retval |= add_file_to_cache(ce->name, flags | HASH_RENORMALIZE);
141 + }
142 +
143 + return retval;
144 +}
145 +
146 static char *prune_directory(struct dir_struct *dir, struct pathspec *pathspec, int prefix)
147 {
148 char *seen;
@@ -276,6 +296,7 @@ static struct option builtin_add_options[] = {
296 OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")),
297 OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files")),
298 OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")),
299 + OPT_BOOL(0, "renormalize", &add_renormalize, N_("renormalize EOL of tracked files (implies -u)")),
300 OPT_BOOL('N', "intent-to-add", &intent_to_add, N_("record only the fact that the path will be added later")),
301 OPT_BOOL('A', "all", &addremove_explicit, N_("add changes from all tracked and untracked files")),
302 { OPTION_CALLBACK, 0, "ignore-removal", &addremove_explicit,
@@ -406,7 +427,7 @@ int cmd_add(int argc, const char **argv, const char *prefix)
427 chmod_arg[1] != 'x' || chmod_arg[2]))
428 die(_("--chmod param '%s' must be either -x or +x"), chmod_arg);
429
409 - add_new_files = !take_worktree_changes && !refresh_only;
430 + add_new_files = !take_worktree_changes && !refresh_only && !add_renormalize;
431 require_pathspec = !(take_worktree_changes || (0 < addremove_explicit));
432
433 hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR);
@@ -500,7 +521,10 @@ int cmd_add(int argc, const char **argv, const char *prefix)
521
522 plug_bulk_checkin();
523
503 - exit_status |= add_files_to_cache(prefix, &pathspec, flags);
524 + if (add_renormalize)
525 + exit_status |= renormalize_tracked_files(&pathspec, flags);
526 + else
527 + exit_status |= add_files_to_cache(prefix, &pathspec, flags);
528
529 if (add_new_files)
530 exit_status |= add_files(&dir, flags);
cache.h
+1
@@ -686,6 +686,7 @@ extern int ie_modified(const struct index_state *, const struct cache_entry *, s
686
687 #define HASH_WRITE_OBJECT 1
688 #define HASH_FORMAT_CHECK 2
689 +#define HASH_RENORMALIZE 4
690 extern int index_fd(struct object_id *oid, int fd, struct stat *st, enum object_type type, const char *path, unsigned flags);
691 extern int index_path(struct object_id *oid, const char *path, struct stat *st, unsigned flags);
692
read-cache.c
+19 -11
@@ -631,13 +631,17 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st,
631 {
632 int size, namelen, was_same;
633 mode_t st_mode = st->st_mode;
634 - struct cache_entry *ce, *alias;
634 + struct cache_entry *ce, *alias = NULL;
635 unsigned ce_option = CE_MATCH_IGNORE_VALID|CE_MATCH_IGNORE_SKIP_WORKTREE|CE_MATCH_RACY_IS_DIRTY;
636 int verbose = flags & (ADD_CACHE_VERBOSE | ADD_CACHE_PRETEND);
637 int pretend = flags & ADD_CACHE_PRETEND;
638 int intent_only = flags & ADD_CACHE_INTENT;
639 int add_option = (ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE|
640 (intent_only ? ADD_CACHE_NEW_ONLY : 0));
641 + int newflags = HASH_WRITE_OBJECT;
642 +
643 + if (flags & HASH_RENORMALIZE)
644 + newflags |= HASH_RENORMALIZE;
645
646 if (!S_ISREG(st_mode) && !S_ISLNK(st_mode) && !S_ISDIR(st_mode))
647 return error("%s: can only add regular files, symbolic links or git-directories", path);
@@ -678,19 +682,23 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st,
682 if (ignore_case) {
683 adjust_dirname_case(istate, ce->name);
684 }
685 + if (!(flags & HASH_RENORMALIZE)) {
686 + alias = index_file_exists(istate, ce->name,
687 + ce_namelen(ce), ignore_case);
688 + if (alias &&
689 + !ce_stage(alias) &&
690 + !ie_match_stat(istate, alias, st, ce_option)) {
691 + /* Nothing changed, really */
692 + if (!S_ISGITLINK(alias->ce_mode))
693 + ce_mark_uptodate(alias);
694 + alias->ce_flags |= CE_ADDED;
695
682 - alias = index_file_exists(istate, ce->name, ce_namelen(ce), ignore_case);
683 - if (alias && !ce_stage(alias) && !ie_match_stat(istate, alias, st, ce_option)) {
684 - /* Nothing changed, really */
685 - if (!S_ISGITLINK(alias->ce_mode))
686 - ce_mark_uptodate(alias);
687 - alias->ce_flags |= CE_ADDED;
688 -
689 - free(ce);
690 - return 0;
696 + free(ce);
697 + return 0;
698 + }
699 }
700 if (!intent_only) {
693 - if (index_path(&ce->oid, path, st, HASH_WRITE_OBJECT)) {
701 + if (index_path(&ce->oid, path, st, newflags)) {
702 free(ce);
703 return error("unable to index file %s", path);
704 }
sha1_file.c
+14 -2
@@ -74,6 +74,18 @@ static struct cached_object *find_cached_object(const unsigned char *sha1)
74 return NULL;
75 }
76
77 +
78 +static enum safe_crlf get_safe_crlf(unsigned flags)
79 +{
80 + if (flags & HASH_RENORMALIZE)
81 + return SAFE_CRLF_RENORMALIZE;
82 + else if (flags & HASH_WRITE_OBJECT)
83 + return safe_crlf;
84 + else
85 + return SAFE_CRLF_FALSE;
86 +}
87 +
88 +
89 int mkdir_in_gitdir(const char *path)
90 {
91 if (mkdir(path, 0777)) {
@@ -1680,7 +1692,7 @@ static int index_mem(unsigned char *sha1, void *buf, size_t size,
1692 if ((type == OBJ_BLOB) && path) {
1693 struct strbuf nbuf = STRBUF_INIT;
1694 if (convert_to_git(&the_index, path, buf, size, &nbuf,
1683 - write_object ? safe_crlf : SAFE_CRLF_FALSE)) {
1695 + get_safe_crlf(flags))) {
1696 buf = strbuf_detach(&nbuf, &size);
1697 re_allocated = 1;
1698 }
@@ -1714,7 +1726,7 @@ static int index_stream_convert_blob(unsigned char *sha1, int fd,
1726 assert(would_convert_to_git_filter_fd(path));
1727
1728 convert_to_git_filter_fd(&the_index, path, fd, &sbuf,
1717 - write_object ? safe_crlf : SAFE_CRLF_FALSE);
1729 + get_safe_crlf(flags));
1730
1731 if (write_object)
1732 ret = write_sha1_file(sbuf.buf, sbuf.len, typename(OBJ_BLOB),
t/t0025-crlf-renormalize.sh new
+30
@@ -0,0 +1,30 @@
1 +#!/bin/sh
2 +
3 +test_description='CRLF renormalization'
4 +
5 +. ./test-lib.sh
6 +
7 +test_expect_success setup '
8 + git config core.autocrlf false &&
9 + printf "LINEONE\nLINETWO\nLINETHREE\n" >LF.txt &&
10 + printf "LINEONE\r\nLINETWO\r\nLINETHREE\r\n" >CRLF.txt &&
11 + printf "LINEONE\r\nLINETWO\nLINETHREE\n" >CRLF_mix_LF.txt &&
12 + git add . &&
13 + git commit -m initial
14 +'
15 +
16 +test_expect_success 'renormalize CRLF in repo' '
17 + echo "*.txt text=auto" >.gitattributes &&
18 + git add --renormalize "*.txt" &&
19 + cat >expect <<-\EOF &&
20 + i/lf w/crlf attr/text=auto CRLF.txt
21 + i/lf w/lf attr/text=auto LF.txt
22 + i/lf w/mixed attr/text=auto CRLF_mix_LF.txt
23 + EOF
24 + git ls-files --eol |
25 + sed -e "s/ / /g" -e "s/ */ /g" |
26 + sort >actual &&
27 + test_cmp expect actual
28 +'
29 +
30 +test_done