ls-files: add eol diagnostics

When working in a cross-platform environment, a user may want to check if text files are stored normalized in the repository and if .gitattributes are set appropriately. Make it possible to let Git show the line endings in the index and in the working tree and the effective text/eol attributes. The end of line ("eolinfo") are shown like this: "-text" binary (or with bare CR) file "none" text file without any EOL "lf" text file with LF "crlf" text file with CRLF "mixed" text file with mixed line endings. The effective text/eol attribute is one of these: "", "-text", "text", "text=auto", "text eol=lf", "text eol=crlf" git ls-files --eol gives an output like this: i/none w/none attr/text=auto t/t5100/empty i/-text w/-text attr/-text t/test-binary-2.png i/lf w/lf attr/text eol=lf t/t5100/rfc2047-info-0007 i/lf w/crlf attr/text eol=crlf doit.bat i/mixed w/mixed attr/ locale/XX.po to show what eol convention is used in the data in the index ('i'), and in the working tree ('w'), and what attribute is in effect, for each path that is shown. Add test cases in t0027. Helped-By: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Torsten Bögershausen <tboegi@web.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Torsten Bögershausen committed Jan 16, 2016 at 07:50 UTC a7630bd4274a0dff7cff8b92de3d3f064e321359
5 files changed +237 -49
Documentation/git-ls-files.txt
+22
@@ -12,6 +12,7 @@ SYNOPSIS
12 'git ls-files' [-z] [-t] [-v]
13 (--[cached|deleted|others|ignored|stage|unmerged|killed|modified])*
14 (-[c|d|o|i|s|u|k|m])*
15 + [--eol]
16 [-x <pattern>|--exclude=<pattern>]
17 [-X <file>|--exclude-from=<file>]
18 [--exclude-per-directory=<file>]
@@ -147,6 +148,24 @@ a space) at the start of each line:
148 possible for manual inspection; the exact format may change at
149 any time.
150
151 +--eol::
152 + Show <eolinfo> and <eolattr> of files.
153 + <eolinfo> is the file content identification used by Git when
154 + the "text" attribute is "auto" (or not set and core.autocrlf is not false).
155 + <eolinfo> is either "-text", "none", "lf", "crlf", "mixed" or "".
156 ++
157 +"" means the file is not a regular file, it is not in the index or
158 +not accessable in the working tree.
159 ++
160 +<eolattr> is the attribute that is used when checking out or committing,
161 +it is either "", "-text", "text", "text=auto", "text eol=lf", "text eol=crlf".
162 +Note: Currently Git does not support "text=auto eol=lf" or "text=auto eol=crlf",
163 +that may change in the future.
164 ++
165 +Both the <eolinfo> in the index ("i/<eolinfo>")
166 +and in the working tree ("w/<eolinfo>") are shown for regular files,
167 +followed by the ("attr/<eolattr>").
168 +
169 \--::
170 Do not interpret any more arguments as options.
171
@@ -161,6 +180,9 @@ which case it outputs:
180
181 [<tag> ]<mode> <object> <stage> <file>
182
183 +'git ls-files --eol' will show
184 + i/<eolinfo><SPACES>w/<eolinfo><SPACES>attr/<eolattr><SPACE*><TAB><file>
185 +
186 'git ls-files --unmerged' and 'git ls-files --stage' can be used to examine
187 detailed information on unmerged paths.
188
builtin/ls-files.c
+21
@@ -27,6 +27,7 @@ static int show_killed;
27 static int show_valid_bit;
28 static int line_terminator = '\n';
29 static int debug_mode;
30 +static int show_eol;
31
32 static const char *prefix;
33 static int max_prefix_len;
@@ -47,6 +48,23 @@ static const char *tag_modified = "";
48 static const char *tag_skip_worktree = "";
49 static const char *tag_resolve_undo = "";
50
51 +static void write_eolinfo(const struct cache_entry *ce, const char *path)
52 +{
53 + if (!show_eol)
54 + return;
55 + else {
56 + struct stat st;
57 + const char *i_txt = "";
58 + const char *w_txt = "";
59 + const char *a_txt = get_convert_attr_ascii(path);
60 + if (ce && S_ISREG(ce->ce_mode))
61 + i_txt = get_cached_convert_stats_ascii(ce->name);
62 + if (!lstat(path, &st) && S_ISREG(st.st_mode))
63 + w_txt = get_wt_convert_stats_ascii(path);
64 + printf("i/%-5s w/%-5s attr/%-17s\t", i_txt, w_txt, a_txt);
65 + }
66 +}
67 +
68 static void write_name(const char *name)
69 {
70 /*
@@ -68,6 +86,7 @@ static void show_dir_entry(const char *tag, struct dir_entry *ent)
86 return;
87
88 fputs(tag, stdout);
89 + write_eolinfo(NULL, ent->name);
90 write_name(ent->name);
91 }
92
@@ -170,6 +189,7 @@ static void show_ce_entry(const char *tag, const struct cache_entry *ce)
189 find_unique_abbrev(ce->sha1,abbrev),
190 ce_stage(ce));
191 }
192 + write_eolinfo(ce, ce->name);
193 write_name(ce->name);
194 if (debug_mode) {
195 const struct stat_data *sd = &ce->ce_stat_data;
@@ -433,6 +453,7 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
453 OPT_BIT(0, "directory", &dir.flags,
454 N_("show 'other' directories' names only"),
455 DIR_SHOW_OTHER_DIRECTORIES),
456 + OPT_BOOL(0, "eol", &show_eol, N_("show line endings of files")),
457 OPT_NEGBIT(0, "empty-directory", &dir.flags,
458 N_("don't show empty directories"),
459 DIR_HIDE_EMPTY_DIRECTORIES),
convert.c
+91 -28
@@ -13,6 +13,11 @@
13 * translation when the "text" attribute or "auto_crlf" option is set.
14 */
15
16 +/* Stat bits: When BIN is set, the txt bits are unset */
17 +#define CONVERT_STAT_BITS_TXT_LF 0x1
18 +#define CONVERT_STAT_BITS_TXT_CRLF 0x2
19 +#define CONVERT_STAT_BITS_BIN 0x4
20 +
21 enum crlf_action {
22 CRLF_GUESS = -1,
23 CRLF_BINARY = 0,
@@ -75,26 +80,75 @@ static void gather_stats(const char *buf, unsigned long size, struct text_stat *
80
81 /*
82 * The same heuristics as diff.c::mmfile_is_binary()
83 + * We treat files with bare CR as binary
84 */
79 -static int is_binary(unsigned long size, struct text_stat *stats)
85 +static int convert_is_binary(unsigned long size, const struct text_stat *stats)
86 {
81 -
87 + if (stats->cr != stats->crlf)
88 + return 1;
89 if (stats->nul)
90 return 1;
91 if ((stats->printable >> 7) < stats->nonprintable)
92 return 1;
86 - /*
87 - * Other heuristics? Average line length might be relevant,
88 - * as might LF vs CR vs CRLF counts..
89 - *
90 - * NOTE! It might be normal to have a low ratio of CRLF to LF
91 - * (somebody starts with a LF-only file and edits it with an editor
92 - * that adds CRLF only to lines that are added..). But do we
93 - * want to support CR-only? Probably not.
94 - */
93 return 0;
94 }
95
96 +static unsigned int gather_convert_stats(const char *data, unsigned long size)
97 +{
98 + struct text_stat stats;
99 + if (!data || !size)
100 + return 0;
101 + gather_stats(data, size, &stats);
102 + if (convert_is_binary(size, &stats))
103 + return CONVERT_STAT_BITS_BIN;
104 + else if (stats.crlf && stats.crlf == stats.lf)
105 + return CONVERT_STAT_BITS_TXT_CRLF;
106 + else if (stats.crlf && stats.lf)
107 + return CONVERT_STAT_BITS_TXT_CRLF | CONVERT_STAT_BITS_TXT_LF;
108 + else if (stats.lf)
109 + return CONVERT_STAT_BITS_TXT_LF;
110 + else
111 + return 0;
112 +}
113 +
114 +static const char *gather_convert_stats_ascii(const char *data, unsigned long size)
115 +{
116 + unsigned int convert_stats = gather_convert_stats(data, size);
117 +
118 + if (convert_stats & CONVERT_STAT_BITS_BIN)
119 + return "-text";
120 + switch (convert_stats) {
121 + case CONVERT_STAT_BITS_TXT_LF:
122 + return "lf";
123 + case CONVERT_STAT_BITS_TXT_CRLF:
124 + return "crlf";
125 + case CONVERT_STAT_BITS_TXT_LF | CONVERT_STAT_BITS_TXT_CRLF:
126 + return "mixed";
127 + default:
128 + return "none";
129 + }
130 +}
131 +
132 +const char *get_cached_convert_stats_ascii(const char *path)
133 +{
134 + const char *ret;
135 + unsigned long sz;
136 + void *data = read_blob_data_from_cache(path, &sz);
137 + ret = gather_convert_stats_ascii(data, sz);
138 + free(data);
139 + return ret;
140 +}
141 +
142 +const char *get_wt_convert_stats_ascii(const char *path)
143 +{
144 + const char *ret = "";
145 + struct strbuf sb = STRBUF_INIT;
146 + if (strbuf_read_file(&sb, path, 0) >= 0)
147 + ret = gather_convert_stats_ascii(sb.buf, sb.len);
148 + strbuf_release(&sb);
149 + return ret;
150 +}
151 +
152 static enum eol output_eol(enum crlf_action crlf_action)
153 {
154 switch (crlf_action) {
@@ -187,18 +241,7 @@ static int crlf_to_git(const char *path, const char *src, size_t len,
241 gather_stats(src, len, &stats);
242
243 if (crlf_action == CRLF_AUTO || crlf_action == CRLF_GUESS) {
190 - /*
191 - * We're currently not going to even try to convert stuff
192 - * that has bare CR characters. Does anybody do that crazy
193 - * stuff?
194 - */
195 - if (stats.cr != stats.crlf)
196 - return 0;
197 -
198 - /*
199 - * And add some heuristics for binary vs text, of course...
200 - */
201 - if (is_binary(len, &stats))
244 + if (convert_is_binary(len, &stats))
245 return 0;
246
247 if (crlf_action == CRLF_GUESS) {
@@ -277,11 +320,7 @@ static int crlf_to_worktree(const char *path, const char *src, size_t len,
320 return 0;
321 }
322
280 - /* If we have any bare CR characters, we're not going to touch it */
281 - if (stats.cr != stats.crlf)
282 - return 0;
283 -
284 - if (is_binary(len, &stats))
323 + if (convert_is_binary(len, &stats))
324 return 0;
325 }
326
@@ -777,6 +816,30 @@ int would_convert_to_git_filter_fd(const char *path)
816 return apply_filter(path, NULL, 0, -1, NULL, ca.drv->clean);
817 }
818
819 +const char *get_convert_attr_ascii(const char *path)
820 +{
821 + struct conv_attrs ca;
822 + enum crlf_action crlf_action;
823 +
824 + convert_attrs(&ca, path);
825 + crlf_action = input_crlf_action(ca.crlf_action, ca.eol_attr);
826 + switch (crlf_action) {
827 + case CRLF_GUESS:
828 + return "";
829 + case CRLF_BINARY:
830 + return "-text";
831 + case CRLF_TEXT:
832 + return "text";
833 + case CRLF_INPUT:
834 + return "text eol=lf";
835 + case CRLF_CRLF:
836 + return "text=auto eol=crlf";
837 + case CRLF_AUTO:
838 + return "text=auto";
839 + }
840 + return "";
841 +}
842 +
843 int convert_to_git(const char *path, const char *src, size_t len,
844 struct strbuf *dst, enum safe_crlf checksafe)
845 {
convert.h
+3
@@ -32,6 +32,9 @@ enum eol {
32 };
33
34 extern enum eol core_eol;
35 +extern const char *get_cached_convert_stats_ascii(const char *path);
36 +extern const char *get_wt_convert_stats_ascii(const char *path);
37 +extern const char *get_convert_attr_ascii(const char *path);
38
39 /* returns 1 if *dst was used */
40 extern int convert_to_git(const char *path, const char *src, size_t len,
t/t0027-auto-crlf.sh
+100 -21
@@ -56,21 +56,16 @@ create_gitattributes () {
56 }
57
58 create_NNO_files () {
59 - lfname=$1
60 - crlfname=$2
61 - lfmixcrlf=$3
62 - lfmixcr=$4
63 - crlfnul=$5
59 for crlf in false true input
60 do
61 for attr in "" auto text -text lf crlf
62 do
63 pfx=NNO_${crlf}_attr_${attr} &&
69 - cp $lfname ${pfx}_LF.txt &&
70 - cp $crlfname ${pfx}_CRLF.txt &&
71 - cp $lfmixcrlf ${pfx}_CRLF_mix_LF.txt &&
72 - cp $lfmixcr ${pfx}_LF_mix_CR.txt &&
73 - cp $crlfnul ${pfx}_CRLF_nul.txt
64 + cp CRLF_mix_LF ${pfx}_LF.txt &&
65 + cp CRLF_mix_LF ${pfx}_CRLF.txt &&
66 + cp CRLF_mix_LF ${pfx}_CRLF_mix_LF.txt &&
67 + cp CRLF_mix_LF ${pfx}_LF_mix_CR.txt &&
68 + cp CRLF_mix_LF ${pfx}_CRLF_nul.txt
69 done
70 done
71 }
@@ -96,7 +91,7 @@ commit_check_warn () {
91 crlfnul=$7
92 pfx=crlf_${crlf}_attr_${attr}
93 create_gitattributes "$attr" &&
99 - for f in LF CRLF repoMIX LF_mix_CR CRLF_mix_LF LF_nul CRLF_nul
94 + for f in LF CRLF LF_mix_CR CRLF_mix_LF LF_nul CRLF_nul
95 do
96 fname=${pfx}_$f.txt &&
97 cp $f $fname &&
@@ -149,6 +144,27 @@ commit_chk_wrnNNO () {
144 '
145 }
146
147 +stats_ascii () {
148 + case "$1" in
149 + LF)
150 + echo lf
151 + ;;
152 + CRLF)
153 + echo crlf
154 + ;;
155 + CRLF_mix_LF)
156 + echo mixed
157 + ;;
158 + LF_mix_CR|CRLF_nul|LF_nul|CRLF_mix_CR)
159 + echo "-text"
160 + ;;
161 + *)
162 + echo error_invalid $1
163 + ;;
164 + esac
165 +
166 +}
167 +
168 check_files_in_repo () {
169 crlf=$1
170 attr=$2
@@ -203,35 +219,83 @@ checkout_files () {
219 create_gitattributes $attr &&
220 git config core.autocrlf $crlf &&
221 pfx=eol_${eol}_crlf_${crlf}_attr_${attr}_ &&
206 - src=crlf_false_attr__ &&
222 for f in LF CRLF LF_mix_CR CRLF_mix_LF LF_nul
223 do
209 - rm $src$f.txt &&
224 + rm crlf_false_attr__$f.txt &&
225 if test -z "$eol"; then
211 - git checkout $src$f.txt
226 + git checkout crlf_false_attr__$f.txt
227 else
213 - git -c core.eol=$eol checkout $src$f.txt
228 + git -c core.eol=$eol checkout crlf_false_attr__$f.txt
229 fi
230 done
231
232 + test_expect_success "ls-files --eol $lfname ${pfx}LF.txt" '
233 + test_when_finished "rm expect actual" &&
234 + sort <<-EOF >expect &&
235 + i/crlf w/$(stats_ascii $crlfname) crlf_false_attr__CRLF.txt
236 + i/mixed w/$(stats_ascii $lfmixcrlf) crlf_false_attr__CRLF_mix_LF.txt
237 + i/lf w/$(stats_ascii $lfname) crlf_false_attr__LF.txt
238 + i/-text w/$(stats_ascii $lfmixcr) crlf_false_attr__LF_mix_CR.txt
239 + i/-text w/$(stats_ascii $crlfnul) crlf_false_attr__CRLF_nul.txt
240 + i/-text w/$(stats_ascii $crlfnul) crlf_false_attr__LF_nul.txt
241 + EOF
242 + git ls-files --eol crlf_false_attr__* |
243 + sed -e "s!attr/[^ ]*!!g" -e "s/ / /g" -e "s/ */ /g" |
244 + sort >actual &&
245 + test_cmp expect actual
246 + '
247 test_expect_success "checkout core.eol=$eol core.autocrlf=$crlf gitattributes=$attr file=LF" "
218 - compare_ws_file $pfx $lfname ${src}LF.txt
248 + compare_ws_file $pfx $lfname crlf_false_attr__LF.txt
249 "
250 test_expect_success "checkout core.eol=$eol core.autocrlf=$crlf gitattributes=$attr file=CRLF" "
221 - compare_ws_file $pfx $crlfname ${src}CRLF.txt
251 + compare_ws_file $pfx $crlfname crlf_false_attr__CRLF.txt
252 "
253 test_expect_success "checkout core.eol=$eol core.autocrlf=$crlf gitattributes=$attr file=CRLF_mix_LF" "
224 - compare_ws_file $pfx $lfmixcrlf ${src}CRLF_mix_LF.txt
254 + compare_ws_file $pfx $lfmixcrlf crlf_false_attr__CRLF_mix_LF.txt
255 "
256 test_expect_success "checkout core.eol=$eol core.autocrlf=$crlf gitattributes=$attr file=LF_mix_CR" "
227 - compare_ws_file $pfx $lfmixcr ${src}LF_mix_CR.txt
257 + compare_ws_file $pfx $lfmixcr crlf_false_attr__LF_mix_CR.txt
258 "
259 test_expect_success "checkout core.eol=$eol core.autocrlf=$crlf gitattributes=$attr file=LF_nul" "
230 - compare_ws_file $pfx $crlfnul ${src}LF_nul.txt
260 + compare_ws_file $pfx $crlfnul crlf_false_attr__LF_nul.txt
261 "
262 }
263
234 -#######
264 +# Test control characters
265 +# NUL SOH CR EOF==^Z
266 +test_expect_success 'ls-files --eol -o Text/Binary' '
267 + test_when_finished "rm expect actual TeBi_*" &&
268 + STRT=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA &&
269 + STR=$STRT$STRT$STRT$STRT &&
270 + printf "${STR}BBB\001" >TeBi_127_S &&
271 + printf "${STR}BBBB\001">TeBi_128_S &&
272 + printf "${STR}BBB\032" >TeBi_127_E &&
273 + printf "\032${STR}BBB" >TeBi_E_127 &&
274 + printf "${STR}BBBB\000">TeBi_128_N &&
275 + printf "${STR}BBB\012">TeBi_128_L &&
276 + printf "${STR}BBB\015">TeBi_127_C &&
277 + printf "${STR}BB\015\012" >TeBi_126_CL &&
278 + printf "${STR}BB\015\012\015" >TeBi_126_CLC &&
279 + sort <<-\EOF >expect &&
280 + i/ w/-text TeBi_127_S
281 + i/ w/none TeBi_128_S
282 + i/ w/none TeBi_127_E
283 + i/ w/-text TeBi_E_127
284 + i/ w/-text TeBi_128_N
285 + i/ w/lf TeBi_128_L
286 + i/ w/-text TeBi_127_C
287 + i/ w/crlf TeBi_126_CL
288 + i/ w/-text TeBi_126_CLC
289 + EOF
290 + git ls-files --eol -o |
291 + sed -n -e "/TeBi_/{s!attr/[ ]*!!g
292 + s! ! !g
293 + s! *! !g
294 + p
295 + }" | sort >actual &&
296 + test_cmp expect actual
297 +'
298 +
299 test_expect_success 'setup master' '
300 echo >.gitattributes &&
301 git checkout -b master &&
@@ -480,4 +544,19 @@ checkout_files native true "lf" LF CRLF CRLF_mix_LF LF_mix_CR
544 checkout_files native false "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul
545 checkout_files native true "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul
546
547 +# Should be the last test case: remove some files from the worktree
548 +test_expect_success 'ls-files --eol -d -z' '
549 + rm crlf_false_attr__CRLF.txt crlf_false_attr__CRLF_mix_LF.txt crlf_false_attr__LF.txt .gitattributes &&
550 + cat >expect <<-\EOF &&
551 + i/crlf w/ crlf_false_attr__CRLF.txt
552 + i/lf w/ .gitattributes
553 + i/lf w/ crlf_false_attr__LF.txt
554 + i/mixed w/ crlf_false_attr__CRLF_mix_LF.txt
555 + EOF
556 + git ls-files --eol -d |
557 + sed -e "s!attr/[^ ]*!!g" -e "s/ / /g" -e "s/ */ /g" |
558 + sort >actual &&
559 + test_cmp expect actual
560 +'
561 +
562 test_done