{"thread":{"id":"1614","subject":"git-ls-files: generalized pathspecs","startedAt":"2005-08-22T00:27:50Z","lastAt":"2005-08-22T00:27:50Z","messageCount":1,"participants":["Linus Torvalds"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"7631","messageId":"Pine.LNX.4.58.0508211622570.3317@g5.osdl.org","threadId":"1614","inReplyTo":null,"subject":"git-ls-files: generalized pathspecs","fromName":"Linus Torvalds","fromEmail":"torvalds@osdl.org","sentAt":"2005-08-22T00:27:50Z","receivedAt":"2005-08-22T00:27:50Z","isPatch":false,"sender":{"key":"torvalds@linux-foundation.org","avatar":"https://avatars.githubusercontent.com/u/1024025?v=4"},"body":"\nThis generalizes the git \"glob\" string to be a lot more like the \ngit-diff-* pathspecs (but there are still differences: the diff family \ndoesn't do any globbing, and because the diff family always generates the \nfull native pathname, it doesn't have the issue with \"..\").\n\nIt does three things: \n\n - it allows multiple matching strings, ie you can do things like\n\n\tgit-ls-files arch/i386/ include/asm-i386/ | xargs grep pattern\n\n - the \"matching\" criteria is a combination of \"exact path component \n   match\" (the same as the git-diff-* family), and \"fnmatch()\". However, \n   you should be careful with the confusion between the git-ls-files\n   internal globbing and the standard shell globbing, ie\n\n\tgit-ls-files fs/*.c\n\n   does globbing in the shell, and does something totally different from \n\n\tgit-ls-files 'fs/*.c'\n\n   which does the globbing inside git-ls-files.\n\n   The latter has _one_ pathspec with a wildcard, and will match any .c \n   file anywhere under the fs/ directory, while the former has been \n   expanded by the shell into having _lots_ of pathspec entries, all of \n   which are just in the top-level fs/ subdirectory. They will happily\n   be matched exactly, but we will thus miss all the subdirectories under \n   fs/.\n\n   As a result, the first one will (on the current kernel) match 55 files,\n   while the second one will match 664 files!\n\n - it uses the generic path prefixing, so that \"..\" and friends at the \n   beginning of the path spec work automatically\n\n   NOTE! When generating relative pathname output (the default), a \n   pathspec that causes the base to be outside the current working \n   directory will be rejected with an error message like:\n\n\tfatal: git-ls-files: cannot generate relative filenames containing '..'\n\n   because we do not actually generate \"..\" in the output. However, the \n   \"..\" format works fine for the --full-name case:\n\n\tcd arch/i386/kernel\n\tgit-ls-files --full-name ../mm/\n\n   results in\n\n\tarch/i386/mm/Makefile\n\tarch/i386/mm/boot_ioremap.c\n\tarch/i386/mm/discontig.c\n\tarch/i386/mm/extable.c\n\tarch/i386/mm/fault.c\n\tarch/i386/mm/highmem.c\n\tarch/i386/mm/hugetlbpage.c\n\tarch/i386/mm/init.c\n\tarch/i386/mm/ioremap.c\n\tarch/i386/mm/mmap.c\n\tarch/i386/mm/pageattr.c\n\tarch/i386/mm/pgtable.c\n\n   Perhaps more commonly, the generic path prefixing means that \".\" and \n   \"./\" automatically get simplified and work properly.\n\nSigned-off-by: Linus Torvalds <torvalds@osdl.org>\n----\ndiff --git a/ls-files.c b/ls-files.c\n--- a/ls-files.c\n+++ b/ls-files.c\n@@ -21,7 +21,7 @@ static int line_terminator = '\\n';\n \n static int prefix_len = 0, prefix_offset = 0;\n static const char *prefix = NULL;\n-static const char *glob = NULL;\n+static const char **pathspec = NULL;\n \n static const char *tag_cached = \"\";\n static const char *tag_unmerged = \"\";\n@@ -302,6 +302,33 @@ static int cmp_name(const void *p1, cons\n \t\t\t\t  e2->name, e2->len);\n }\n \n+/*\n+ * Match a pathspec against a filename. The first \"len\" characters\n+ * are the common prefix\n+ */\n+static int match(const char **spec, const char *filename, int len)\n+{\n+\tconst char *m;\n+\n+\twhile ((m = *spec++) != NULL) {\n+\t\tint matchlen = strlen(m + len);\n+\n+\t\tif (!matchlen)\n+\t\t\treturn 1;\n+\t\tif (!strncmp(m + len, filename + len, matchlen)) {\n+\t\t\tif (m[len + matchlen - 1] == '/')\n+\t\t\t\treturn 1;\n+\t\t\tswitch (filename[len + matchlen]) {\n+\t\t\tcase '/': case '\\0':\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\t\t}\n+\t\tif (!fnmatch(m + len, filename + len, 0))\n+\t\t\treturn 1;\n+\t}\n+\treturn 0;\n+}\n+\n static void show_dir_entry(const char *tag, struct nond_on_fs *ent)\n {\n \tint len = prefix_len;\n@@ -310,7 +337,7 @@ static void show_dir_entry(const char *t\n \tif (len >= ent->len)\n \t\tdie(\"git-ls-files: internal error - directory entry not superset of prefix\");\n \n-\tif (glob && fnmatch(glob, ent->name + len, 0))\n+\tif (pathspec && !match(pathspec, ent->name, len))\n \t\treturn;\n \n \tprintf(\"%s%s%c\", tag, ent->name + offset, line_terminator);\n@@ -373,7 +400,7 @@ static void show_ce_entry(const char *ta\n \tif (len >= ce_namelen(ce))\n \t\tdie(\"git-ls-files: internal error - cache entry not superset of prefix\");\n \n-\tif (glob && fnmatch(glob, ce->name + len, 0))\n+\tif (pathspec && !match(pathspec, ce->name, len))\n \t\treturn;\n \n \tif (!show_stage)\n@@ -455,36 +482,44 @@ static void prune_cache(void)\n \tactive_nr = last;\n }\n \n-/*\n- * If the glob starts with a subdirectory, append it to\n- * the prefix instead, for more efficient operation.\n- *\n- * But we do not update the \"prefix_offset\", which tells\n- * how much of the name to ignore at printout.\n- */\n-static void extend_prefix(void)\n+static void verify_pathspec(void)\n {\n-\tconst char *p, *slash;\n-\tchar c;\n+\tconst char **p, *n, *prev;\n+\tchar *real_prefix;\n+\tunsigned long max;\n+\n+\tprev = NULL;\n+\tmax = PATH_MAX;\n+\tfor (p = pathspec; (n = *p) != NULL; p++) {\n+\t\tint i, len = 0;\n+\t\tfor (i = 0; i < max; i++) {\n+\t\t\tchar c = n[i];\n+\t\t\tif (prev && prev[i] != c)\n+\t\t\t\tbreak;\n+\t\t\tif (c == '*' || c == '?')\n+\t\t\t\tbreak;\n+\t\t\tif (c == '/')\n+\t\t\t\tlen = i+1;\n+\t\t}\n+\t\tprev = n;\n+\t\tif (len < max) {\n+\t\t\tmax = len;\n+\t\t\tif (!max)\n+\t\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tif (prefix_offset > max || memcmp(prev, prefix, prefix_offset))\n+\t\tdie(\"git-ls-files: cannot generate relative filenames containing '..'\");\n \n-\tp = glob;\n-\tslash = NULL;\n-\twhile ((c = *p++) != '\\0') {\n-\t\tif (c == '*')\n-\t\t\tbreak;\n-\t\tif (c == '/')\n-\t\t\tslash = p;\n-\t}\n-\tif (slash) {\n-\t\tint len = slash - glob;\n-\t\tchar *newprefix = xmalloc(len + prefix_len + 1);\n-\t\tmemcpy(newprefix, prefix, prefix_len);\n-\t\tmemcpy(newprefix + prefix_len, glob, len);\n-\t\tprefix_len += len;\n-\t\tnewprefix[prefix_len] = 0;\n-\t\tprefix = newprefix;\n-\t\tglob = *slash ? slash : NULL;\n+\treal_prefix = NULL;\n+\tprefix_len = max;\n+\tif (max) {\n+\t\treal_prefix = xmalloc(max + 1);\n+\t\tmemcpy(real_prefix, prev, max);\n+\t\treal_prefix[max] = 0;\n \t}\n+\tprefix = real_prefix;\n }\n \n static const char ls_files_usage[] =\n@@ -499,7 +534,7 @@ int main(int argc, char **argv)\n \n \tprefix = setup_git_directory();\n \tif (prefix)\n-\t\tprefix_offset = prefix_len = strlen(prefix);\n+\t\tprefix_offset = strlen(prefix);\n \n \tfor (i = 1; i < argc; i++) {\n \t\tchar *arg = argv[i];\n@@ -577,13 +612,16 @@ int main(int argc, char **argv)\n \t\t\tprefix_offset = 0;\n \t\t\tcontinue;\n \t\t}\n-\t\tif (glob || *arg == '-')\n+\t\tif (*arg == '-')\n \t\t\tusage(ls_files_usage);\n-\t\tglob = arg;\n+\t\tbreak;\n \t}\n \n-\tif (glob)\n-\t\textend_prefix();\n+\tpathspec = get_pathspec(prefix, argv + i);\n+\n+\t/* Verify that the pathspec matches the prefix */\n+\tif (pathspec)\n+\t\tverify_pathspec();\n \n \tif (show_ignored && !exc_given) {\n \t\tfprintf(stderr, \"%s: --ignored needs some exclude pattern\\n\",\n"}]}