Gives all paths inherent 'mode' attribute values based on the paths' modes (one of 100644, 100755, 120000, 040000, 160000). Users may use this feature to filter by file types. For example a pathspec such as ':(attr:mode=160000)' could filter for submodules without needing `mode=160000` to actually be specified for each subdmoule path in .gitattributes. The native mode values are also reflected in `git check-attr` results. If there is any existing mode attribute for a path (e.g. there is !mode, -mode, mode, mode=<value> in .gitattributes) that setting will take precedence over the native mode value. --- I went with 'mode' because that is the word used in documentation (e.g. https://git-scm.com/book/sv/v2/Git-Internals-Git-Objects) and in the code (e.g. `ce_mode`). Please let me know what you think of this UX. The implementation happens within git_check_attr() method which is the common mathod called for getting a pathspec attribute value. The previous discussed idea did not work with `git check-attr`. (https://lore.kernel.org/all/CAMmZTi8swsSMcLUcW+YwUDg8GcrY_ks2+i35-nsHE3o9MNpsUQ@xxxxxxxxxxxxxx/). There are no tests for excluding based on pathspec attrs in subdirectories due to an existing bug. Since it is not specific to native mode, I thought it would be better to fix separately. https://lore.kernel.org/all/CAMmZTi89Un+bsLXdEdYs44oT8eLNp8y=Pm8ywaurcQ7ccRKGdQ@xxxxxxxxxxxxxx/ Documentation/gitattributes.txt | 10 +++++++ attr.c | 42 ++++++++++++++++++++++++-- t/t0003-attributes.sh | 40 +++++++++++++++++++++++++ t/t6135-pathspec-with-attrs.sh | 53 +++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 2 deletions(-) diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt index 8c1793c148..bb3c11f151 100644 --- a/Documentation/gitattributes.txt +++ b/Documentation/gitattributes.txt @@ -156,6 +156,16 @@ Unspecified:: Any other value causes Git to act as if `text` has been left unspecified. +`mode` +^^^^^ + +This attribute is for filtering files by their file bit modes +(40000, 120000, 160000, 100755, 100644) and has native support in git, +meaning values for this attribute are natively set (e.g. mode=100644) by +git without having to specify them in .gitattributes. However, if +'mode' is set in .gitattributest for some path, that value takes precendence, +whether it is 'set', 'unset', 'unspecified', or some other value. + `eol` ^^^^^ diff --git a/attr.c b/attr.c index e62876dfd3..95dc1cf695 100644 --- a/attr.c +++ b/attr.c @@ -1240,20 +1240,58 @@ static struct object_id *default_attr_source(void) return &attr_source; } + +/* + * This implements native file mode attr support. + * We always return the current mode of the path, not the mode stored + * in the index, unless the path is a directory where we check the index + * to see if it is a GITLINK. It is ok to rely on the index for GITLINK + * modes because a path cannot become a GITLINK (which is a git concept only) + * without having it indexed with a GITLINK mode in git. + */ +static unsigned int native_mode_attr(struct index_state *istate, const char *path) +{ + struct stat st; + unsigned int mode; + if (lstat(path, &st)) + die_errno(_("unable to stat '%s'"), path); + mode = canon_mode(st.st_mode); + if (S_ISDIR(mode)) { + int pos = index_name_pos(istate, path, strlen(path)); + if (pos >= 0) + if (S_ISGITLINK(istate->cache[pos]->ce_mode)) + return istate->cache[pos]->ce_mode; + } + return mode; +} + + void git_check_attr(struct index_state *istate, const char *path, struct attr_check *check) { int i; const struct object_id *tree_oid = default_attr_source(); + struct strbuf sb = STRBUF_INIT; collect_some_attrs(istate, tree_oid, path, check); for (i = 0; i < check->nr; i++) { unsigned int n = check->items[i].attr->attr_nr; const char *value = check->all_attrs[n].value; - if (value == ATTR__UNKNOWN) - value = ATTR__UNSET; + if (value == ATTR__UNKNOWN){ + if (strcmp(check->all_attrs[n].attr->name, "mode") == 0) { + /* + * If value is ATTR_UNKNOWN then it has not been set + * anywhere with -mode (ATTR_FALSE), !mode (ATTR_UNSET), + * mode (ATTR_TRUE), or an explicit value. We fill + * value with the native mode value. + */ + strbuf_addf(&sb, "%06o", native_mode_attr(istate, path)); + value = sb.buf; + } else + value = ATTR__UNSET; + } check->items[i].value = value; } } diff --git a/t/t0003-attributes.sh b/t/t0003-attributes.sh index aee2298f01..9c2603d8e2 100755 --- a/t/t0003-attributes.sh +++ b/t/t0003-attributes.sh @@ -19,6 +19,15 @@ attr_check () { test_must_be_empty err } +attr_check_mode () { + path="$1" expect="$2" git_opts="$3" && + + git $git_opts check-attr mode -- "$path" >actual 2>err && + echo "$path: mode: $expect" >expect && + test_cmp expect actual && + test_must_be_empty err +} + attr_check_quote () { path="$1" quoted_path="$2" expect="$3" && @@ -558,4 +567,35 @@ test_expect_success EXPENSIVE 'large attributes file ignored in index' ' test_cmp expect err ' +test_expect_success 'submodule with .git file' ' + mkdir sub && + (cd sub && + git init && + mv .git .real && + echo "gitdir: .real" >.git && + test_commit first) && + git update-index --add -- sub +' + +test_expect_success 'native mode attributes work' ' + >exec && chmod +x exec && attr_check_mode exec 100755 && + >normal && attr_check_mode normal 100644 && + mkdir dir && attr_check_mode dir 040000 && + ln -s normal normal_sym && attr_check_mode normal_sym 120000 && + attr_check_mode sub 160000 +' + +test_expect_success '.gitattributes mode values take precedence' ' + ( + echo "mode_value* mode=myownmode" && + echo "mode_set* mode" && + echo "mode_unset* -mode" && + echo "mode_unspecified* !mode" + ) >.gitattributes && + >mode_value && attr_check_mode mode_value myownmode && + >mode_unset && attr_check_mode mode_unset unset && + >mode_unspecified && attr_check_mode mode_unspecified unspecified && + >mode_set && attr_check_mode mode_set set +' + test_done diff --git a/t/t6135-pathspec-with-attrs.sh b/t/t6135-pathspec-with-attrs.sh index a9c1e4e0ec..fd9569d39b 100755 --- a/t/t6135-pathspec-with-attrs.sh +++ b/t/t6135-pathspec-with-attrs.sh @@ -64,6 +64,9 @@ test_expect_success 'setup .gitattributes' ' fileSetLabel label fileValue label=foo fileWrongLabel label☺ + mode_set* mode=1234 + mode_unset* -mode + mode_unspecified* !mode EOF echo fileSetLabel label1 >sub/.gitattributes && git add .gitattributes sub/.gitattributes && @@ -295,4 +298,54 @@ test_expect_success 'reading from .gitattributes in a subdirectory (3)' ' test_cmp expect actual ' +test_expect_success 'mode attr is handled correctly for overriden values' ' + >mode_set_1 && + >mode_unset_1 && + >mode_unspecified_1 && + >mode_regular_file_1 && + + git status -s ":(attr:mode=1234)mode*" >actual && + cat <<-\EOF >expect && + ?? mode_set_1 + EOF + test_cmp expect actual && + + git status -s ":(attr:-mode)mode*" >actual && + echo ?? mode_unset_1 >expect && + test_cmp expect actual && + + git status -s ":(attr:!mode)mode*" >actual && + echo ?? mode_unspecified_1 >expect && + test_cmp expect actual && + + git status -s ":(attr:mode=100644)mode*" >actual && + echo ?? mode_regular_file_1 >expect && + test_cmp expect actual +' + +test_expect_success 'mode attr values are current file modes, not indexed modes' ' + >mode_exec_file_1 && + + git status -s ":(attr:mode=100644)mode_exec_*" >actual && + echo ?? mode_exec_file_1 >expect && + test_cmp expect actual && + + git add mode_exec_file_1 && chmod +x mode_exec_file_1 && + git status -s ":(attr:mode=100755)mode_exec_*" >actual && + echo AM mode_exec_file_1 >expect && + test_cmp expect actual +' + +test_expect_success 'mode attr can be excluded' ' + >mode_1_regular && + >mode_1_exec && chmod +x mode_1_exec && + git status -s ":(exclude,attr:mode=100644)" "mode_1_*" >actual && + echo ?? mode_1_exec >expect && + test_cmp expect actual && + + git status -s ":(exclude,attr:mode=100755)" "mode_1_*" >actual && + echo ?? mode_1_regular >expect && + test_cmp expect actual +' + test_done -- 2.42.0.869.gea05f2083d-goog