git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH v7] ls-files: Add eol diagnostics

From
Philip Oakley <philipoakley@iee.org>
Date
Dec 7, 2015, 19:57 UTC
Message-ID
<E2D76919B3F548E9864D12425A4C0EF6@PhilipOakley>
In-Reply-To
<5665BD39.1040403@web.de>
From: "Torsten Bögershausen" <tboegi@web.de>
> When working in a cross-platform environment, a user wants to
> check if text files are stored normalized in the repository and if
> .gitattributes are set appropriately.
>

The need for this came up again on the Git Users list (https://groups.google.com/d/msg/git-users/jC-mngwVYo4/Sr4JXgpGJVYJ). It will definitely be useful in mixed environments wher users can get rather confused.

I've added a couple of bikeshed comments on some of the wider issues this bumps into.

Show 9 quoted lines
> 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:
> "binary"       binary file
> "text-no-eol"  text file without any EOL
> "text-lf"      text file with LF
> "text-crlf"    text file with CRLF
> "text-crlf-lf" text file with mixed line endings.

<bikeshed> This "text-crlf-lf" can easily occur for eol=crlf files which have merge conflict markers, which are always eol=lf. It may be that the conflict markers should use the eol setting that is in place for the file being merged. e.g. http://stackoverflow.com/questions/17832616/make-git-use-crlf-on-its-head-merge-lines (which links to https://github.com/git/git/blob/master/xdiff/xmerge.c#L173, and 3 other places in fill_conflict_hunk). The issue of how to mark EOL conflicts in-text is tricky as it's a non-obvious white space issue - it doesn't fit well with the normal >>> === <<< conflict markers.

>
> The effective text/eol attribute is one of these:
> "", "-text", "text", "text=auto", "eol=lf", "eol=crlf"

<bikeshed> the "-text" attribute isn't explained in the gitattributes(5) page, except by implication from one of the examples.

Show 62 quoted lines
>
> git ls-files --eol gives an output like this:
>
> i/text-no-eol   w/text-no-eol   attr/text=auto t/t5100/empty
> i/binary        w/binary        attr/-text     t/test-binary-2.png
> i/text-lf       w/text-lf       attr/eol=lf    t/t5100/rfc2047-info-0007
> i/text-lf       w/text-crlf     attr/eol=crlf  doit.bat
> i/text-crlf-lf  w/text-crlf-lf  attr/          locale/XX.po
>
> Note that the output is meant to be human-readable and may change.
>
> Helped-By: Eric Sunshine <sunshine@sunshineco.com>
> Signed-off-by: Torsten Bögershausen <tboegi@web.de>
> ---
> Changes since v6:
> - Fixed potential memory leak in convert.c, when strbuf_read_file()
>  fails.
> - t0027:
>  Cleanups (empty lines, egrep, un-needed quoting)
>  test_when_finished 'rm e expect actual'
>  There doesn't seem to be 100% consistency when and how to remove files.
>  (I think if we create files, we should be able to remove them:
>  use "rm" rather than "rm -f")
>  Add comment about the "last test case", which removes file to run
>  'git ls-files -d"
>
> Documentation/git-ls-files.txt |  22 +++++++++
> builtin/ls-files.c             |  19 ++++++++
> convert.c                      |  85 ++++++++++++++++++++++++++++++++
> convert.h                      |   3 ++
> t/t0027-auto-crlf.sh           | 108 
> ++++++++++++++++++++++++++++++++++++-----
> 5 files changed, 226 insertions(+), 11 deletions(-)
>
> diff --git a/Documentation/git-ls-files.txt 
> b/Documentation/git-ls-files.txt
> index e26f01f..8f29c99 100644
> --- a/Documentation/git-ls-files.txt
> +++ b/Documentation/git-ls-files.txt
> @@ -12,6 +12,7 @@ SYNOPSIS
> 'git ls-files' [-z] [-t] [-v]
>  (--[cached|deleted|others|ignored|stage|unmerged|killed|modified])*
>  (-[c|d|o|i|s|u|k|m])*
> + [--eol]
>  [-x <pattern>|--exclude=<pattern>]
>  [-X <file>|--exclude-from=<file>]
>  [--exclude-per-directory=<file>]
> @@ -147,6 +148,18 @@ a space) at the start of each line:
>  possible for manual inspection; the exact format may change at
>  any time.
>
> +--eol::
> + Show line endings ("eolinfo") and the text/eol attributes 
> ("texteolattr") of files.
> + "eolinfo" is the file content identification used by Git when
> + the "text" attribute is "auto", or core.autocrlf != false.
> +
> + "eolinfo" is either "" (when the the info is not available"), or one of 
> "binary",
> + "text-no-eol", "text-lf", "text-crlf" or "text-crlf-lf".
> + The "texteolattr" can be "", "-text", "text", "text=auto", "eol=lf", 
> "eol=crlf".

perhaps add 'see also linkgit:gitattributes[5] .' though it may be too tangential.

Show 382 quoted lines
> +
> + Both the content in the index ("i/") and the content in the working tree 
> ("w/")
> + are shown for regular files, followed by the "texteolattr ("attr/").
> +
> \--::
>  Do not interpret any more arguments as options.
>
> @@ -161,6 +174,15 @@ which case it outputs:
>
>         [<tag> ]<mode> <object> <stage> <file>
>
> +'git ls-files --eol' will show
> + i/<eolinfo> w/<eolinfo> attr/<eolattr> <file>
> +
> +'git ls-files --eol -o' will show
> + i/          w/<eolinfo> attr/<eolattr> <file>
> +
> +'git ls-files --eol -s' will show
> +[<tag> ]<mode> <object> <stage> i/<eolinfo> w/<eolinfo> attr/<eolattr> 
> <file>
> +
> 'git ls-files --unmerged' and 'git ls-files --stage' can be used to 
> examine
> detailed information on unmerged paths.
>
> diff --git a/builtin/ls-files.c b/builtin/ls-files.c
> index b6a7cb0..ef892bc 100644
> --- a/builtin/ls-files.c
> +++ b/builtin/ls-files.c
> @@ -27,6 +27,7 @@ static int show_killed;
> static int show_valid_bit;
> static int line_terminator = '\n';
> static int debug_mode;
> +static int show_eol;
>
> static const char *prefix;
> static int max_prefix_len;
> @@ -47,6 +48,21 @@ static const char *tag_modified = "";
> static const char *tag_skip_worktree = "";
> static const char *tag_resolve_undo = "";
>
> +static void write_eolinfo(const struct cache_entry *ce, const char *path)
> +{
> + struct stat st;
> + const char *i_txt = "";
> + const char *w_txt = "";
> + if (!show_eol)
> + return;
> + if (ce && S_ISREG(ce->ce_mode))
> + i_txt = get_cached_convert_stats_ascii(ce->name);
> + if (!lstat(path, &st) && (S_ISREG(st.st_mode)))
> + w_txt = get_wt_convert_stats_ascii(path);
> + printf("i/%-13s w/%-13s attr/%-9s ", i_txt, w_txt,
> + get_convert_attr_ascii(path));
> +}
> +
> static void write_name(const char *name)
> {
>  /*
> @@ -68,6 +84,7 @@ static void show_dir_entry(const char *tag, struct 
> dir_entry *ent)
>  return;
>
>  fputs(tag, stdout);
> + write_eolinfo(NULL, ent->name);
>  write_name(ent->name);
> }
>
> @@ -170,6 +187,7 @@ static void show_ce_entry(const char *tag, const 
> struct cache_entry *ce)
>         find_unique_abbrev(ce->sha1,abbrev),
>         ce_stage(ce));
>  }
> + write_eolinfo(ce, ce->name);
>  write_name(ce->name);
>  if (debug_mode) {
>  const struct stat_data *sd = &ce->ce_stat_data;
> @@ -433,6 +451,7 @@ int cmd_ls_files(int argc, const char **argv, const 
> char *cmd_prefix)
>  OPT_BIT(0, "directory", &dir.flags,
>  N_("show 'other' directories' names only"),
>  DIR_SHOW_OTHER_DIRECTORIES),
> + OPT_BOOL(0, "eol", &show_eol, N_("show line endings of files")),
>  OPT_NEGBIT(0, "empty-directory", &dir.flags,
>  N_("don't show empty directories"),
>  DIR_HIDE_EMPTY_DIRECTORIES),
> diff --git a/convert.c b/convert.c
> index 814e814..61d6757 100644
> --- a/convert.c
> +++ b/convert.c
> @@ -13,6 +13,11 @@
>  * translation when the "text" attribute or "auto_crlf" option is set.
>  */
>
> +/* Stat bits: When BIN is set, the txt bits are unset */
> +#define CONVERT_STAT_BITS_TXT_LF   (1)
> +#define CONVERT_STAT_BITS_TXT_CRLF (2)
> +#define CONVERT_STAT_BITS_BIN      (4)
> +
> enum crlf_action {
>  CRLF_GUESS = -1,
>  CRLF_BINARY = 0,
> @@ -95,6 +100,62 @@ static int is_binary(unsigned long size, struct 
> text_stat *stats)
>  return 0;
> }
>
> +static unsigned int gather_convert_stats(const char *data, unsigned long 
> size)
> +{
> + struct text_stat stats;
> + if (!data || !size)
> + return 0;
> + gather_stats(data, size, &stats);
> + if (is_binary(size, &stats) || stats.cr != stats.crlf)
> + return CONVERT_STAT_BITS_BIN;
> + else if (stats.crlf && (stats.crlf == stats.lf))
> + return CONVERT_STAT_BITS_TXT_CRLF;
> + else if (stats.crlf && stats.lf)
> + return CONVERT_STAT_BITS_TXT_CRLF | CONVERT_STAT_BITS_TXT_LF;
> + else if (stats.lf)
> + return CONVERT_STAT_BITS_TXT_LF;
> + else
> + return 0;
> +}
> +
> +static const char *gather_convert_stats_ascii(const char *data, unsigned 
> long size)
> +{
> + unsigned int convert_stats = gather_convert_stats(data, size);
> +
> + if (convert_stats & CONVERT_STAT_BITS_BIN)
> + return "binary";
> + switch (convert_stats) {
> + case CONVERT_STAT_BITS_TXT_LF:
> + return("text-lf");
> + case CONVERT_STAT_BITS_TXT_CRLF:
> + return("text-crlf");
> + case CONVERT_STAT_BITS_TXT_LF | CONVERT_STAT_BITS_TXT_CRLF:
> + return("text-crlf-lf");
> + default:
> + return ("text-no-eol");
> + }
> +}
> +
> +const char *get_cached_convert_stats_ascii(const char *path)
> +{
> + const char *ret;
> + unsigned long sz;
> + void *data = read_blob_data_from_cache(path, &sz);
> + ret = gather_convert_stats_ascii(data, sz);
> + free(data);
> + return ret;
> +}
> +
> +const char *get_wt_convert_stats_ascii(const char *path)
> +{
> + const char *ret = "";
> + struct strbuf sb = STRBUF_INIT;
> + if (strbuf_read_file(&sb, path, 0) >= 0)
> + ret = gather_convert_stats_ascii(sb.buf, sb.len);
> + strbuf_release(&sb);
> + return ret;
> +}
> +
> static enum eol output_eol(enum crlf_action crlf_action)
> {
>  switch (crlf_action) {
> @@ -777,6 +838,30 @@ int would_convert_to_git_filter_fd(const char *path)
>  return apply_filter(path, NULL, 0, -1, NULL, ca.drv->clean);
> }
>
> +const char *get_convert_attr_ascii(const char *path)
> +{
> + struct conv_attrs ca;
> + enum crlf_action crlf_action;
> +
> + convert_attrs(&ca, path);
> + crlf_action = input_crlf_action(ca.crlf_action, ca.eol_attr);
> + switch (crlf_action) {
> + case CRLF_GUESS:
> + return "";
> + case CRLF_BINARY:
> + return "-text";
> + case CRLF_TEXT:
> + return "text";
> + case CRLF_INPUT:
> + return "eol=lf";
> + case CRLF_CRLF:
> + return "eol=crlf";
> + case CRLF_AUTO:
> + return "text=auto";
> + }
> + return "";
> +}
> +
> int convert_to_git(const char *path, const char *src, size_t len,
>                    struct strbuf *dst, enum safe_crlf checksafe)
> {
> diff --git a/convert.h b/convert.h
> index d9d853c..ccf436b 100644
> --- a/convert.h
> +++ b/convert.h
> @@ -32,6 +32,9 @@ enum eol {
> };
>
> extern enum eol core_eol;
> +extern const char *get_cached_convert_stats_ascii(const char *path);
> +extern const char *get_wt_convert_stats_ascii(const char *path);
> +extern const char *get_convert_attr_ascii(const char *path);
>
> /* returns 1 if *dst was used */
> extern int convert_to_git(const char *path, const char *src, size_t len,
> diff --git a/t/t0027-auto-crlf.sh b/t/t0027-auto-crlf.sh
> index b343651..a89f2ba 100755
> --- a/t/t0027-auto-crlf.sh
> +++ b/t/t0027-auto-crlf.sh
> @@ -56,21 +56,16 @@ create_gitattributes () {
> }
>
> create_NNO_files () {
> - lfname=$1
> - crlfname=$2
> - lfmixcrlf=$3
> - lfmixcr=$4
> - crlfnul=$5
>  for crlf in false true input
>  do
>  for attr in "" auto text -text lf crlf
>  do
>  pfx=NNO_${crlf}_attr_${attr} &&
> - cp $lfname    ${pfx}_LF.txt &&
> - cp $crlfname  ${pfx}_CRLF.txt &&
> - cp $lfmixcrlf ${pfx}_CRLF_mix_LF.txt &&
> - cp $lfmixcr   ${pfx}_LF_mix_CR.txt &&
> - cp $crlfnul   ${pfx}_CRLF_nul.txt
> + cp CRLF_mix_LF ${pfx}_LF.txt &&
> + cp CRLF_mix_LF ${pfx}_CRLF.txt &&
> + cp CRLF_mix_LF ${pfx}_CRLF_mix_LF.txt &&
> + cp CRLF_mix_LF ${pfx}_LF_mix_CR.txt &&
> + cp CRLF_mix_LF ${pfx}_CRLF_nul.txt
>  done
>  done
> }
> @@ -96,7 +91,7 @@ commit_check_warn () {
>  crlfnul=$7
>  pfx=crlf_${crlf}_attr_${attr}
>  create_gitattributes "$attr" &&
> - for f in LF CRLF repoMIX LF_mix_CR CRLF_mix_LF LF_nul CRLF_nul
> + for f in LF CRLF LF_mix_CR CRLF_mix_LF LF_nul CRLF_nul
>  do
>  fname=${pfx}_$f.txt &&
>  cp $f $fname &&
> @@ -149,6 +144,36 @@ commit_chk_wrnNNO () {
>  '
> }
>
> +stats_ascii () {
> + case "$1" in
> + LF)
> + echo text-lf
> + ;;
> + CRLF)
> + echo text-crlf
> + ;;
> + CRLF_mix_LF)
> + echo text-crlf-lf
> + ;;
> + LF_mix_CR)
> + echo binary
> + ;;
> + CRLF_nul)
> + echo binary
> + ;;
> + LF_nul)
> + echo binary
> + ;;
> + CRLF_mix_CR)
> + echo binary
> + ;;
> + *)
> + echo error_invalid $1
> + ;;
> + esac
> +
> +}
> +
> check_files_in_repo () {
>  crlf=$1
>  attr=$2
> @@ -214,6 +239,20 @@ checkout_files () {
>  fi
>  done
>
> + test_expect_success "ls-files --eol $lfname ${pfx}LF.txt" "
> +   test_when_finished 'rm e expect actual' &&
> + cat >e <<-EOF &&
> + i/text-crlf w/$(stats_ascii $crlfname) ${src}CRLF.txt
> + i/text-crlf-lf w/$(stats_ascii $lfmixcrlf) ${src}CRLF_mix_LF.txt
> + i/text-lf w/$(stats_ascii $lfname) ${src}LF.txt
> + i/binary w/$(stats_ascii $lfmixcr) ${src}LF_mix_CR.txt
> + i/binary w/$(stats_ascii $crlfnul) ${src}CRLF_nul.txt
> + i/binary w/$(stats_ascii $crlfnul) ${src}LF_nul.txt
> + EOF
> + sort <e >expect &&
> + git ls-files --eol $src* | sed -e 's!attr/[=a-z-]*!!g' -e 's/  */ /g' | 
> sort >actual &&
> + test_cmp expect actual
> + "
>  test_expect_success "checkout core.eol=$eol core.autocrlf=$crlf 
> gitattributes=$attr file=LF" "
>  compare_ws_file $pfx $lfname    ${src}LF.txt
>  "
> @@ -231,6 +270,37 @@ checkout_files () {
>  "
> }
>
> +# Test control characters
> +# NUL SOH CR EOF==^Z
> +test_expect_success 'ls-files --eol -o Text/Binary' '
> + test_when_finished "rm e expect actual TeBi_*" &&
> + STRT=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA &&
> + STR=$STRT$STRT$STRT$STRT &&
> + printf "${STR}BBB\001" >TeBi_127_S &&
> + printf "${STR}BBBB\001">TeBi_128_S &&
> + printf "${STR}BBB\032" >TeBi_127_E &&
> + printf "\032${STR}BBB" >TeBi_E_127 &&
> + printf "${STR}BBBB\000">TeBi_128_N &&
> + printf "${STR}BBB\012">TeBi_128_L &&
> + printf "${STR}BBB\015">TeBi_127_C &&
> + printf "${STR}BB\015\012" >TeBi_126_CL &&
> + printf "${STR}BB\015\012\015" >TeBi_126_CLC &&
> + cat >e <<-\EOF &&
> + i/ w/binary TeBi_127_S
> + i/ w/text-no-eol TeBi_128_S
> + i/ w/text-no-eol TeBi_127_E
> + i/ w/binary TeBi_E_127
> + i/ w/binary TeBi_128_N
> + i/ w/text-lf TeBi_128_L
> + i/ w/binary TeBi_127_C
> + i/ w/text-crlf TeBi_126_CL
> + i/ w/binary TeBi_126_CLC
> + EOF
> + sort <e >expect &&
> + git ls-files --eol -o | grep TeBi_ | sed -e 's!attr/[=a-z-]*!!g' -e "s/ 
> */ /g" | sort >actual &&
> + test_cmp expect actual
> +'
> +
> #######
> test_expect_success 'setup master' '
>  echo >.gitattributes &&
> @@ -480,4 +550,20 @@ checkout_files    native  true  "lf"      LF    CRLF 
> CRLF_mix_LF  LF_mix_CR
> checkout_files    native  false "crlf"    CRLF  CRLF  CRLF 
> CRLF_mix_CR  CRLF_nul
> checkout_files    native  true  "crlf"    CRLF  CRLF  CRLF 
> CRLF_mix_CR  CRLF_nul
>
> +
> +# Should be the last test case: remove some files from the worktree
> +# run 'git ls-files -d'
> +test_expect_success 'ls-files --eol -d' "
> + rm  crlf_false_attr__CRLF.txt crlf_false_attr__CRLF_mix_LF.txt 
> crlf_false_attr__LF.txt .gitattributes &&
> + cat >expect <<-\EOF &&
> + i/text-crlf w/ crlf_false_attr__CRLF.txt
> + i/text-crlf-lf w/ crlf_false_attr__CRLF_mix_LF.txt
> + i/text-lf w/ .gitattributes
> + i/text-lf w/ crlf_false_attr__LF.txt
> + EOF
> + git ls-files --eol -d | sed -e 's!attr/[=a-z-]*!!g' -e 's/  */ /g' | 
> sort >actual &&
> + test_cmp expect actual &&
> + rm expect actual
> +"
> +
> test_done
> -- 
> 2.6.2.403.gd7a84e3
>

-- Philip

Previous: Torsten BögershausenNext: Eric Sunshine
Message 2 of 3 in “ls-files: Add eol diagnostics”
  1. ls-files: Add eol diagnosticsTorsten Bögershausen, Dec 7, 2015
  2. Philip OakleyDec 7, 2015
  3. Eric SunshineDec 7, 2015

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.