#pragma section-numbers 2 <> = Better Matcher API and File Patterns Plan = '''Status: Project''' '''Main proponents: KatsunoriFujiwara, RodrigoDamazio''' /!\ This is a speculative project and does not represent any firm decisions on future behavior. {X} Add a short summary of the idea here. <> == Goal == * Short term: add non-recursive globs ? * Long term: extensible matcher API ? == Detailed description == === Sprint Notes === {{{ Non-recursive globs (Rodrigo, spectral, Durham, : Issue is that * is sometimes recursive matcher API is a mess Should we re-write match.py or just add fileglob? Suggestion: add fileglob via a new, cleaner API, then migrate others over time Possible FB use case: pick parts of a tree to include and exclude (would add ordering dependency instead of excludes always trumping includes?) matcher API should be extensible matcher composition: anyof, allof, negate, per-file-type, etc. Inconsistencies in pattern behavior between hgignore, --include/--exclude, etc. FB: conversion between matchers and watchman expressions Proposal: wiki page, first group to have a use case proposes the initial API }}} === Current Status === ==== Summary of mode, relative-to, and recursion of each types ==== ||'''mode''' ||'''root-ed''' ||'''cwd-ed''' ||'''any-of-path''' ||'''control recursion by pattern''' ||'''context depend recursion''' || ||wildcard ||- ||`glob:` ||`relglob:` ||by ** ||o || ||regexp ||`re:` ||- ||`relre:` ||by $ ||x (*A) || ||raw string ||`path:` ||`relpath:` ||- ||(always) ||x || * (*A) "regexp" mode ignore pattern matches recursively (e.g. "`re:^foo$`" ignores file `foo/bar`). Detail is explained later. ==== The list of contexts, in which pattern is specified ==== ||'''pattern for''' ||'''default type''' ||'''recursion of wildcard''' ||'''related API''' || ||fileset ||`glob:` ||x ||ctx.match() || ||files() template function ||`glob:` ||x ||ctx.match() || ||diff() template function ||`glob:` ||o (*1) ||ctx.match() || ||file() revset predicate ||`glob:` ||x ||match.match() || ||follow() revset predicate ||`path:` (*3) ||x ||match.match() || ||--include/--exclude ||`glob:` ||o (*1) ||match.match() || ||hgignore ||`relre:` ||o (*1) ||match.match() || ||`archive` web command ||`path:` ||- (*2) ||scmutil.match() || ||`hg locate` ||`relglob:` ||x ||scmutil.match() || ||`hg log` ||`relpath:` ||x ||scmutil.matchandpats() || ||others (e.g. `hg files`) ||`relpath:` ||x ||scmutil.match() || * (*1) treated as `include`/`exclude` of match.match() (otherwise, treated as `pats` of match.match()) * (*2) no wildcard pattern matching occurs for `archive` web command, becuase `path:` is forcibly added to specified pattern in this case * (*3) can't be `relpath:` because of backward compatibility (Cset:5618858dce26) For "recursion of wildcard": * if "recursive of wildcard", pattern `glob:foo/bar` matches against file `foo/bar/baz`, for example * Inner context is used to decide "recursion of wildcard", if multiple contexts are combined For example, file `foo/bar/baz` is: * not matched at: `hg files glob:foo/bar` * not matched at: `hg files -I "set:'glob:foo/bar'"` * but matched at: `hg files -I glob:foo/bar` The last case seems to cause the issue mentioned by Rodrigo in "[[https://www.mercurial-scm.org/pipermail/mercurial-devel/2016-October/089003.html|match: adding non-recursive directory matching]]". And the second case can be used as instant work around for that issue. Table below re-summarizes about recursion (= matching against intermediate directory) of each modes. ||'''mode''' ||'''-I/-X''' ||'''in "set:"''' ||'''-I/-X with "set:"''' || ||wildcard ||always ||endswith("**") ||endswith("**") || ||regexp ||not endswith("$") ||not endswith("$") ||not endswith("$") || ||raw string ||always ||always ||always || "Recursion of wildcard" of the pattern from a file follows one of what tries to read that file in. For example: * wildcard pattern read in by "`-I listfile:FILE`" matches recursively, but * one read in by "`hg status listfile:FILE`" doesn't ==== Reading patterns from file ==== ||'''read in by''' ||'''type substitution''' ||'''default type for hgignore''' ||'''default type for otherwise''' || ||include:FILE ||o ||`relre:` ||`relre:` || ||listfile:FILE ||x ||(*X) ||(*Y) || * (*X) this is prohibited by match.readpatternfile() * (*Y) decision about "default type" depends on the context, in which `listfile:FILE` is used (e.g. `relglob:` for "`hg locate`", but `relpath:` for "`hg files`"). If "type substitution", substitutions below occur always at reading patterns from file. This is mentioned in "`hg help patterns`" and "`hg help hgignore`", but type `relglob:` and `relre:` themselves aren't explained. * `glob:` => `relglob:` * `re:` => `relre:` Reading from `.hgignore` and "`[ui] ignore`" is treated as a variant of `include:` internally (e.g. `include:$REPOROOT/.hgignore`) ==== Recursion of ignore patterns ==== As a ignore pattern, "wildcard" and "raw string" modes are obviously recursive, because: * treating as same as "`--include PATTERN`" makes "wildcard" mode recursive * "raw string" mode is always recursive, regardless of context On the other hand, "regexp" mode itself is non-recursive. For example, with "`re:^foo$`" in .hgignore, "`hg debugignore`" shows the regexp, which doesn't match against file `foo/bar`. But actually, "`re:^foo$`" in .hgignore ignores file `foo/bar`, because dirstate (and "`hg debugignore`") examines whether specified file does: * match against specified ignore patterns, or * exist under the directory, which matches against specified ignore patterns and that file is ignored, if one of conditions above is true. Therefore, "regexp" ignore pattern is recursive, even if it uses "`$`". In conclusion, '''all ignore patterns are treated as recursive, regardless of pattern types'''. This special recursion of "regexp" mode is specific for ignore patterns. In other cases, "regexp" mode pattern isn't recursive, if it uses "`$`". === Proposal by foozy === By introducing systematic new pattern types, both "start point" and "recursion" of matching can be fully controlled arbitrarily in any contexts (as PATTERN, -I/-X, and so on). ==== Control start point of matching arbitrarily ==== New types and their start point. ||'''mode''' ||'''root-ed''' ||'''cwd-ed''' ||'''any-of-path''' || ||wildcard ||`rootglob:` ||`cwdglob:` ||`anyglob:` || ||regexp ||`rootre:` ||`cwdre:` ||`anyre:` || ||raw string ||`rootpath:` ||`cwdpath:` ||`anypath:` || New "wildcard" and "regexp" types other than `anyglob:` match recursively, fully according to specified pattern, as below. `anyglob:` pattern should be always recursive, because this type is "any-of-path" matching. ||'''type''' ||'''recursive''' || ||`rootglob:` ||endswith("**") || ||`cwdglob:` ||endswith("**") || ||`anyglob:` ||always || ||`rootre:` ||not endswith("$") || ||`cwdre:` ||not endswith("$") || ||`anyre:` ||not endswith("$") || ||`rootpath:` ||always || ||`cwdpath:` ||always || ||`anypath:` ||always || ==== Emulate legacy types as an alias of new types ==== Current match.py implementation adds prefix/suffix regexp below to the specified pattern internally, according to what it is used for. See implementation of `_regex()` and `match._normalize()`, and `_buildmatch()` invocations in `match.__init__()` in match.py, for detail. ||'''type''' ||'''used for''' ||'''prefix''' ||'''suffix''' ||'''recursive''' || ||`glob:` ||pattern ||"`$CWD/`" ||"`$`" ||endswith("**") || || ||include/exclude ||"`$CWD/`" ||"`(?:/|$)`" ||always || ||`relglob:` ||pattern ||"`(?:|.*/)`" ||"`$`" ||endswith("**") || || ||include/exclude ||"`(?:|.*/)`" ||"`(?:/|$)`" ||always || ||`re:` ||(always) ||(none) ||(none) ||not endswith("$") || ||`relre:` ||(always) ||"`.*`" (*1) ||(none) ||not endswith("$") || ||`path:` ||(always) ||"`^`" (*2) ||"`(?:/|$)`" ||always || ||`relpath:` ||(always) ||"`$CWD/`" ||"`(?:/|$)`" ||always || * (*1) add this prefix, only if pattern doesn't start with "`^`" * (*2) (just nit picking) this may be redundant, because patterns are examined by `re.match()`, which requires matching from the beginning of a target string. So, at first, let assume that newly introduced types use additional prefix/suffix regexp below BY DEFAULT (now, controlling recursion in "wildcard" and "regexp" mode is user responsibility). ||'''type''' ||'''prefix''' ||'''suffix''' ||'''recursive''' || ||`rootglob:` ||(none) ||"`$`" ||endswith("**") || ||`cwdglob:` ||"`$CWD/`" ||"`$`" ||endswith("**") || ||`anyglob:` ||"`(?:|.*/)`" ||"`(?:/|$)`" ||always || ||`rootre:` ||(none) ||(none) ||not endswith("$") || ||`cwdre:` ||"`$CWD/`" ||(none) ||not endswith("$") || ||`anyre:` ||"`.*`" ||(none) ||not endswith("$") || ||`rootpath:` ||(none) ||"`(?:/|$)`" ||always || ||`cwdpath:` ||"`$CWD/`" ||"`(?:/|$)`" ||always || ||`anypath:` ||"`(?:|.*/)`" ||"`(?:/|$)`" ||always || Then, legacy types can be emulated as an alias of newly introduced type as below: ||'''type''' ||'''used as''' ||'''alias of''' ||'''needed suffix''' || ||`glob:` ||pattern ||`cwdglob:` ||"`$`" (= default of `cwdglob:`) || || ||include/exclude ||`cwdglob:` ||"`(?:/|$)`" || ||`relglob:` ||pattern ||`anyglob:` ||"`$`" || || ||include/exclude ||`anyglob:` ||"`(?:/|$)`" (= default of `anyglob:`) || ||`re:` ||(always) ||`rootre:` ||(none) (= default of `rootre:`) || ||`relre:` ||(always) ||`anyre:` ||(none) (= default of `anyre:`) || ||`path:` ||(always) ||`rootpath:` ||"`(?:/|$)`" (= default of `rootpath:`) || ||`relpath:` ||(always) ||`cwdpath:` ||"`(?:/|$)`" (= default of `cwdpath:`) || At this point, using suffix below forcibly for legacy `glob:` and `relglob:` is as same as current match.py implementation. * "`$`" for pattern * "`(?:/|$)`" for include/exclude Therefore,aliasing should be emulated easily. ==== Control recursion of matching arbitrarily ==== With current Mercurial (at least, 4.0 or earlier), recursion of each pattern types can be controlled by: ||'''mode''' ||'''recursive''' || ||wildcard ||endswith("**") || ||regexp ||not endswith("$") || ||raw string ||always || User can't control recursion of matching with "raw string" pattern arbitrarily (it matches against both directory and file). Therefore, how about introducing two more additional modes "raw file" and "raw dir" ? Additional suffix regexp can control these matching. ||'''mode''' ||'''recursive''' ||'''suffix''' || ||raw file name ||never ||"`$`" || ||raw dir name ||always, but matches against only directory ||"`/`" || After adding these modes, there are 5 (modes) x 3 (start points) = 15 types ||'''mode''' ||'''root-ed''' ||'''cwd-ed''' ||'''any-of-path''' || ||wildcard ||`rootglob:` ||`cwdglob:` ||`anyglob:` || ||regexp ||`rootre:` ||`cwdre:` ||`anyre:` || ||raw string ||`rootpath:` ||`cwdpath:` ||`anypath:` || ||raw file name ||`rootfile:` ||`cwdfile:` ||`anyfile:` || ||raw dir name ||`rootdir:` ||`cwddir:` ||`anydir:` || === Proposal by Rodrigo === Add `rootglob:` to get over the issue of `-I/-X` patterns. https://patchwork.mercurial-scm.org/patch/17311/ == Roadmap == {X} == See Also == * http://d.hatena.ne.jp/flying-foozy/20140107/1389087728 ---- CategoryDeveloper CategoryNewFeatures