Patchwork [3,of,4] match: enable 'relinclude:' syntax

login
register
mail settings
Submitter Durham Goode
Date May 20, 2015, 9:57 p.m.
Message ID <3bbfdda372e8e4204a54.1432159055@dev2000.prn2.facebook.com>
Download mbox | patch
Permalink /patch/9201/
State Changes Requested
Delegated to: Pierre-Yves David
Headers show

Comments

Durham Goode - May 20, 2015, 9:57 p.m.
# HG changeset patch
# User Durham Goode <durham@fb.com>
# Date 1431818705 25200
#      Sat May 16 16:25:05 2015 -0700
# Node ID 3bbfdda372e8e4204a5440009bfbc9e81f2a93e9
# Parent  28ac58249dbc906622e368357daadd4814f9c71c
match: enable 'relinclude:' syntax

This adds a new rule syntax that allows the user to include a pattern file, but
only have those patterns match against files underneath the subdirectory of the
pattern file.

This is useful when you have nested projects in a repository and the inner
projects wants to set up ignore rules that won't affect other projects in the
repository. It is also useful in high commit rate repositories for removing the
root .hgignore as a point of contention.
Martin von Zweigbergk - May 21, 2015, 6:11 p.m.
On Wed, May 20, 2015 at 3:01 PM Durham Goode <durham@fb.com> wrote:

> # HG changeset patch
> # User Durham Goode <durham@fb.com>
> # Date 1431818705 25200
> #      Sat May 16 16:25:05 2015 -0700
> # Node ID 3bbfdda372e8e4204a5440009bfbc9e81f2a93e9
> # Parent  28ac58249dbc906622e368357daadd4814f9c71c
> match: enable 'relinclude:' syntax
>
> This adds a new rule syntax that allows the user to include a pattern
> file, but
> only have those patterns match against files underneath the subdirectory
> of the
> pattern file.
>
> This is useful when you have nested projects in a repository and the inner
> projects wants to set up ignore rules that won't affect other projects in
> the
> repository. It is also useful in high commit rate repositories for
> removing the
> root .hgignore as a point of contention.
>
> diff --git a/mercurial/match.py b/mercurial/match.py
> --- a/mercurial/match.py
> +++ b/mercurial/match.py
> @@ -5,7 +5,7 @@
>  # This software may be used and distributed according to the terms of the
>  # GNU General Public License version 2 or any later version.
>
> -import re
> +import os, re
>  import util, pathutil
>  from i18n import _
>
> @@ -42,6 +42,25 @@ def _expandsets(kindpats, ctx, listsubre
>          other.append((kind, pat, source))
>      return fset, other
>
> +def _expandsubinclude(kindpats, root):
> +    '''Returns the list of subinclude matchers and the kindpats without
> the
> +    subincludes in it.'''
> +    relmatchers = []
> +    other = []
> +
> +    for kind, pat, source in kindpats:
> +        if kind == 'subinclude':
> +            sourceroot = os.path.dirname(source)
> +            path = os.path.join(sourceroot, pat)
> +            newroot = os.path.dirname(path)
> +            relmatcher = match(newroot, '', [], ['include:%s' % path])
> +            prefix = os.path.relpath(newroot, root) + '/'
> +            relmatchers.append((prefix, relmatcher))
> +        else:
> +            other.append((kind, pat, source))
> +
> +    return relmatchers, other
> +
>  def _kindpatsalwaysmatch(kindpats):
>      """"Checks whether the kindspats match everything, as e.g.
>      'relpath:.' does.
> @@ -76,6 +95,8 @@ class match(object):
>          'relre:<regexp>' - a regexp that needn't match the start of a name
>          'set:<fileset>' - a fileset expression
>          'include:<path>' - a file of patterns to read and include
> +        'subinclude:<path>' - a file of patterns to match against files
> under
> +                              the same directory
>          '<something>' - a pattern of the specified default type
>          """
>
> @@ -349,7 +370,7 @@ def _patsplit(pattern, default):
>      if ':' in pattern:
>          kind, pat = pattern.split(':', 1)
>          if kind in ('re', 'glob', 'path', 'relglob', 'relpath', 'relre',
> -                    'listfile', 'listfile0', 'set', 'include'):
> +                    'listfile', 'listfile0', 'set', 'include',
> 'subinclude'):
>              return kind, pat
>      return default, pattern
>
> @@ -455,6 +476,15 @@ def _buildmatch(ctx, kindpats, globsuffi
>      globsuffix is appended to the regexp of globs.'''
>      matchfuncs = []
>
> +    subincludes, kindpats = _expandsubinclude(kindpats, root)
> +    if subincludes:
> +        def matchsubinclude(f):
> +            for prefix, mf in subincludes:
> +                if f.startswith(prefix) and mf(f[len(prefix):]):
> +                    return True
> +            return False
> +        matchfuncs.append(matchsubinclude)
> +
>      fset, kindpats = _expandsets(kindpats, ctx, listsubrepos)
>      if fset:
>          matchfuncs.append(fset.__contains__)
> @@ -551,7 +581,7 @@ def readpatternfile(filepath, warn):
>      pattern        # pattern of the current default type'''
>
>      syntaxes = {'re': 'relre:', 'regexp': 'relre:', 'glob': 'relglob:',
> -                'include': 'include'}
> +                'include': 'include', 'subinclude': 'subinclude'}
>      syntax = 'relre:'
>      patterns = []
>
> diff --git a/tests/test-hgignore.t b/tests/test-hgignore.t
> --- a/tests/test-hgignore.t
> +++ b/tests/test-hgignore.t
> @@ -190,7 +190,45 @@ Check recursive uses of 'include:'
>    $ hg status
>    A dir/b.o
>
> +  $ cp otherignore goodignore
>    $ echo "include:badignore" >> otherignore
>    $ hg status
>    skipping unreadable pattern file 'badignore': No such file or directory
>    A dir/b.o
> +
> +  $ mv goodignore otherignore
> +
> +Check including subincludes
>

Should we also have a test with "subinclude:foo" (i.e. in the current
directory)?


> +
> +  $ hg revert -q --all
> +  $ hg purge --all --config extensions.purge=
> +  $ echo ".hgignore" > .hgignore
> +  $ mkdir dir1 dir2
> +  $ touch dir1/file1 dir1/file2 dir2/file1 dir2/file2
> +  $ echo "subinclude:dir2/.hgignore" >> .hgignore
> +  $ echo "glob:file*2" > dir2/.hgignore
> +  $ hg status
> +  ? dir1/file1
> +  ? dir1/file2
> +  ? dir2/file1
> +
> +Check including subincludes with regexs
> +
> +  $ echo "subinclude:dir1/.hgignore" >> .hgignore
> +  $ echo "regexp:f.le1" > dir1/.hgignore
> +
> +  $ hg status
> +  ? dir1/file2
> +  ? dir2/file1
> +
> +Check multiple levels of sub-ignores
> +
> +  $ mkdir dir1/subdir
> +  $ touch dir1/subdir/subfile1 dir1/subdir/subfile3 dir1/subdir/subfile4
> +  $ echo "subinclude:subdir/.hgignore" >> dir1/.hgignore
> +  $ echo "glob:subfil*3" >> dir1/subdir/.hgignore
> +
> +  $ hg status
> +  ? dir1/file2
> +  ? dir1/subdir/subfile4
> +  ? dir2/file1
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@selenic.com
> http://selenic.com/mailman/listinfo/mercurial-devel
>
Martin von Zweigbergk - May 21, 2015, 6:20 p.m.
On Thu, May 21, 2015 at 11:11 AM Martin von Zweigbergk <
martinvonz@google.com> wrote:

> On Wed, May 20, 2015 at 3:01 PM Durham Goode <durham@fb.com> wrote:
>
>> # HG changeset patch
>> # User Durham Goode <durham@fb.com>
>> # Date 1431818705 25200
>> #      Sat May 16 16:25:05 2015 -0700
>> # Node ID 3bbfdda372e8e4204a5440009bfbc9e81f2a93e9
>> # Parent  28ac58249dbc906622e368357daadd4814f9c71c
>> match: enable 'relinclude:' syntax
>>
>> This adds a new rule syntax that allows the user to include a pattern
>> file, but
>> only have those patterns match against files underneath the subdirectory
>> of the
>> pattern file.
>>
>> This is useful when you have nested projects in a repository and the inner
>> projects wants to set up ignore rules that won't affect other projects in
>> the
>> repository. It is also useful in high commit rate repositories for
>> removing the
>> root .hgignore as a point of contention.
>>
>> diff --git a/mercurial/match.py b/mercurial/match.py
>> --- a/mercurial/match.py
>> +++ b/mercurial/match.py
>> @@ -5,7 +5,7 @@
>>  # This software may be used and distributed according to the terms of the
>>  # GNU General Public License version 2 or any later version.
>>
>> -import re
>> +import os, re
>>  import util, pathutil
>>  from i18n import _
>>
>> @@ -42,6 +42,25 @@ def _expandsets(kindpats, ctx, listsubre
>>          other.append((kind, pat, source))
>>      return fset, other
>>
>> +def _expandsubinclude(kindpats, root):
>> +    '''Returns the list of subinclude matchers and the kindpats without
>> the
>> +    subincludes in it.'''
>> +    relmatchers = []
>> +    other = []
>> +
>> +    for kind, pat, source in kindpats:
>> +        if kind == 'subinclude':
>> +            sourceroot = os.path.dirname(source)
>> +            path = os.path.join(sourceroot, pat)
>> +            newroot = os.path.dirname(path)
>> +            relmatcher = match(newroot, '', [], ['include:%s' % path])
>>
>
What will the "bad" message say? Will it be relative to newroot? Is it easy
to add a test for that too?


> +            prefix = os.path.relpath(newroot, root) + '/'
>> +            relmatchers.append((prefix, relmatcher))
>> +        else:
>> +            other.append((kind, pat, source))
>> +
>> +    return relmatchers, other
>> +
>>  def _kindpatsalwaysmatch(kindpats):
>>      """"Checks whether the kindspats match everything, as e.g.
>>      'relpath:.' does.
>> @@ -76,6 +95,8 @@ class match(object):
>>          'relre:<regexp>' - a regexp that needn't match the start of a
>> name
>>          'set:<fileset>' - a fileset expression
>>          'include:<path>' - a file of patterns to read and include
>> +        'subinclude:<path>' - a file of patterns to match against files
>> under
>> +                              the same directory
>>          '<something>' - a pattern of the specified default type
>>          """
>>
>> @@ -349,7 +370,7 @@ def _patsplit(pattern, default):
>>      if ':' in pattern:
>>          kind, pat = pattern.split(':', 1)
>>          if kind in ('re', 'glob', 'path', 'relglob', 'relpath', 'relre',
>> -                    'listfile', 'listfile0', 'set', 'include'):
>> +                    'listfile', 'listfile0', 'set', 'include',
>> 'subinclude'):
>>              return kind, pat
>>      return default, pattern
>>
>> @@ -455,6 +476,15 @@ def _buildmatch(ctx, kindpats, globsuffi
>>      globsuffix is appended to the regexp of globs.'''
>>      matchfuncs = []
>>
>> +    subincludes, kindpats = _expandsubinclude(kindpats, root)
>> +    if subincludes:
>> +        def matchsubinclude(f):
>> +            for prefix, mf in subincludes:
>> +                if f.startswith(prefix) and mf(f[len(prefix):]):
>> +                    return True
>> +            return False
>> +        matchfuncs.append(matchsubinclude)
>> +
>>      fset, kindpats = _expandsets(kindpats, ctx, listsubrepos)
>>      if fset:
>>          matchfuncs.append(fset.__contains__)
>> @@ -551,7 +581,7 @@ def readpatternfile(filepath, warn):
>>      pattern        # pattern of the current default type'''
>>
>>      syntaxes = {'re': 'relre:', 'regexp': 'relre:', 'glob': 'relglob:',
>> -                'include': 'include'}
>> +                'include': 'include', 'subinclude': 'subinclude'}
>>      syntax = 'relre:'
>>      patterns = []
>>
>> diff --git a/tests/test-hgignore.t b/tests/test-hgignore.t
>> --- a/tests/test-hgignore.t
>> +++ b/tests/test-hgignore.t
>> @@ -190,7 +190,45 @@ Check recursive uses of 'include:'
>>    $ hg status
>>    A dir/b.o
>>
>> +  $ cp otherignore goodignore
>>    $ echo "include:badignore" >> otherignore
>>    $ hg status
>>    skipping unreadable pattern file 'badignore': No such file or directory
>>    A dir/b.o
>> +
>> +  $ mv goodignore otherignore
>> +
>> +Check including subincludes
>>
>
> Should we also have a test with "subinclude:foo" (i.e. in the current
> directory)?
>
>
>> +
>> +  $ hg revert -q --all
>> +  $ hg purge --all --config extensions.purge=
>> +  $ echo ".hgignore" > .hgignore
>> +  $ mkdir dir1 dir2
>> +  $ touch dir1/file1 dir1/file2 dir2/file1 dir2/file2
>> +  $ echo "subinclude:dir2/.hgignore" >> .hgignore
>> +  $ echo "glob:file*2" > dir2/.hgignore
>> +  $ hg status
>> +  ? dir1/file1
>> +  ? dir1/file2
>> +  ? dir2/file1
>> +
>> +Check including subincludes with regexs
>> +
>> +  $ echo "subinclude:dir1/.hgignore" >> .hgignore
>> +  $ echo "regexp:f.le1" > dir1/.hgignore
>> +
>> +  $ hg status
>> +  ? dir1/file2
>> +  ? dir2/file1
>> +
>> +Check multiple levels of sub-ignores
>> +
>> +  $ mkdir dir1/subdir
>> +  $ touch dir1/subdir/subfile1 dir1/subdir/subfile3 dir1/subdir/subfile4
>> +  $ echo "subinclude:subdir/.hgignore" >> dir1/.hgignore
>> +  $ echo "glob:subfil*3" >> dir1/subdir/.hgignore
>> +
>> +  $ hg status
>> +  ? dir1/file2
>> +  ? dir1/subdir/subfile4
>> +  ? dir2/file1
>> _______________________________________________
>> Mercurial-devel mailing list
>> Mercurial-devel@selenic.com
>> http://selenic.com/mailman/listinfo/mercurial-devel
>>
>
Matt Mackall - May 21, 2015, 6:45 p.m.
On Wed, 2015-05-20 at 14:57 -0700, Durham Goode wrote:
> # HG changeset patch
> # User Durham Goode <durham@fb.com>
> # Date 1431818705 25200
> #      Sat May 16 16:25:05 2015 -0700
> # Node ID 3bbfdda372e8e4204a5440009bfbc9e81f2a93e9
> # Parent  28ac58249dbc906622e368357daadd4814f9c71c
> match: enable 'relinclude:' syntax

Forgot to amend.

> -import re
> +import os, re

Uh oh. We're slowly trying to purge all use of os.path from the core so
we can finally correctly support Unicode on Windows. Instead things
should use vfs, pathutil, util, or something else that encapsulates the
os.path calls.

We don't have a vfs handy here, so that's a bit of a nuisance..

> +def _expandsubinclude(kindpats, root):
> +    '''Returns the list of subinclude matchers and the kindpats without the
> +    subincludes in it.'''

Insofar as this is a "pure" function with no side effects, it'd actually
be better/faster/more convenient to do most of our poking at it with a
doctest rather than .t tests. 

> +    relmatchers = []
> +    other = []
> +
> +    for kind, pat, source in kindpats:
> +        if kind == 'subinclude':
> +            sourceroot = os.path.dirname(source)
> +            path = os.path.join(sourceroot, pat)

Generally wrong as join uses os.sep, which will mean the matcher will
only work against the working copy on Windows. Won't matter for ignore
rules, but will otherwise be broken. Might want to "renormalize" with
util.pconvert().

> +            newroot = os.path.dirname(path)
> +            relmatcher = match(newroot, '', [], ['include:%s' % path])
> +            prefix = os.path.relpath(newroot, root) + '/'

You probably want to use pathutil.canonpath?

> +            relmatchers.append((prefix, relmatcher))
> +        else:
> +            other.append((kind, pat, source))
Matt Mackall - May 21, 2015, 6:55 p.m.
On Wed, 2015-05-20 at 14:57 -0700, Durham Goode wrote:
> # HG changeset patch
> # User Durham Goode <durham@fb.com>
> # Date 1431818705 25200
> #      Sat May 16 16:25:05 2015 -0700
> # Node ID 3bbfdda372e8e4204a5440009bfbc9e81f2a93e9
> # Parent  28ac58249dbc906622e368357daadd4814f9c71c
> match: enable 'relinclude:' syntax

(I've queued the first two here, thanks.)
Pierre-Yves David - May 22, 2015, 4:28 a.m.
On 05/21/2015 09:53 PM, Durham Goode wrote:
>
>
> On 5/21/15 11:45 AM, Matt Mackall wrote:
>> On Wed, 2015-05-20 at 14:57 -0700, Durham Goode wrote:
>>> # HG changeset patch
>>> # User Durham Goode <durham@fb.com>
>>> # Date 1431818705 25200
>>> #      Sat May 16 16:25:05 2015 -0700
>>> # Node ID 3bbfdda372e8e4204a5440009bfbc9e81f2a93e9
>>> # Parent  28ac58249dbc906622e368357daadd4814f9c71c
>>> match: enable 'relinclude:' syntax
>> Forgot to amend.
>>
>>> -import re
>>> +import os, re
>> Uh oh. We're slowly trying to purge all use of os.path from the core so
>> we can finally correctly support Unicode on Windows. Instead things
>> should use vfs, pathutil, util, or something else that encapsulates the
>> os.path calls.
>>
>> We don't have a vfs handy here, so that's a bit of a nuisance..
> I can use pconvert and pathutil.canonpath below to avoid os.path.join
> and os.path.relpath, but I don't see any vfs/util/pathutil support for
> dirname or join (the vfs seems to just call os.path.join).  dirname
> seems to work with both '/' and '\' on window, so that might be safe to
> use.  I can use pconvert after doing os.path.join to make that safer.
>
> Would you prefer I create pathutil.dirname() (which just calls
> os.path.dirname) and pathutil.join() (which calls os.path.join() +
> pconvert()) to abstract this stuff away?

 From previous interaction with Matt, I think that is what he mean. He 
wants to relinquish any 'import os' to low level module so we have a 
centralise point to add compatibility.

>>> +def _expandsubinclude(kindpats, root):
>>> +    '''Returns the list of subinclude matchers and the kindpats
>>> without the
>>> +    subincludes in it.'''
>> Insofar as this is a "pure" function with no side effects, it'd actually
>> be better/faster/more convenient to do most of our poking at it with a
>> doctest rather than .t tests.
> I attempted to do this.  This _expandsubinclude() requires having files
> exist on disk though, and I couldn't get any combination of
> os.mkdir("$TESTTMP/dir") or open("$TESTTMP/dir/foo") to work from inside
> a doc test.

doctest is a generic Python mechanism so using $TESTTMP looks like a 
layer violation. A way to get this to work would be to explicitly import 
the python's 'tempfile' module and play with it.
Matt Mackall - May 22, 2015, 2:55 p.m.
On Thu, 2015-05-21 at 19:53 -0700, Durham Goode wrote:
> >> +def _expandsubinclude(kindpats, root):
> >> +    '''Returns the list of subinclude matchers and the kindpats without the
> >> +    subincludes in it.'''
> > Insofar as this is a "pure" function with no side effects, it'd actually
> > be better/faster/more convenient to do most of our poking at it with a
> > doctest rather than .t tests.
> I attempted to do this.  This _expandsubinclude() requires having files 
> exist on disk though, and I couldn't get any combination of 
> os.mkdir("$TESTTMP/dir") or open("$TESTTMP/dir/foo") to work from inside 
> a doc test.

A pure function is one that has no inputs/outputs/effects aside from its
arguments and return value. Great examples are anything that does
straight string parsing. Doctesting these is great because you can get
directly at all the corner cases quickly and compactly, and it serves as
documentation. This function looked like mostly path manipulation, so a
plausible candidate.

But if it requires a file on disk... then it's not a pure function, and
it's not a good fit for doctesting. I guess I wasn't spotting that the
recursive call to match was implicitly reading a file.

Patch

diff --git a/mercurial/match.py b/mercurial/match.py
--- a/mercurial/match.py
+++ b/mercurial/match.py
@@ -5,7 +5,7 @@ 
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-import re
+import os, re
 import util, pathutil
 from i18n import _
 
@@ -42,6 +42,25 @@  def _expandsets(kindpats, ctx, listsubre
         other.append((kind, pat, source))
     return fset, other
 
+def _expandsubinclude(kindpats, root):
+    '''Returns the list of subinclude matchers and the kindpats without the
+    subincludes in it.'''
+    relmatchers = []
+    other = []
+
+    for kind, pat, source in kindpats:
+        if kind == 'subinclude':
+            sourceroot = os.path.dirname(source)
+            path = os.path.join(sourceroot, pat)
+            newroot = os.path.dirname(path)
+            relmatcher = match(newroot, '', [], ['include:%s' % path])
+            prefix = os.path.relpath(newroot, root) + '/'
+            relmatchers.append((prefix, relmatcher))
+        else:
+            other.append((kind, pat, source))
+
+    return relmatchers, other
+
 def _kindpatsalwaysmatch(kindpats):
     """"Checks whether the kindspats match everything, as e.g.
     'relpath:.' does.
@@ -76,6 +95,8 @@  class match(object):
         'relre:<regexp>' - a regexp that needn't match the start of a name
         'set:<fileset>' - a fileset expression
         'include:<path>' - a file of patterns to read and include
+        'subinclude:<path>' - a file of patterns to match against files under
+                              the same directory
         '<something>' - a pattern of the specified default type
         """
 
@@ -349,7 +370,7 @@  def _patsplit(pattern, default):
     if ':' in pattern:
         kind, pat = pattern.split(':', 1)
         if kind in ('re', 'glob', 'path', 'relglob', 'relpath', 'relre',
-                    'listfile', 'listfile0', 'set', 'include'):
+                    'listfile', 'listfile0', 'set', 'include', 'subinclude'):
             return kind, pat
     return default, pattern
 
@@ -455,6 +476,15 @@  def _buildmatch(ctx, kindpats, globsuffi
     globsuffix is appended to the regexp of globs.'''
     matchfuncs = []
 
+    subincludes, kindpats = _expandsubinclude(kindpats, root)
+    if subincludes:
+        def matchsubinclude(f):
+            for prefix, mf in subincludes:
+                if f.startswith(prefix) and mf(f[len(prefix):]):
+                    return True
+            return False
+        matchfuncs.append(matchsubinclude)
+
     fset, kindpats = _expandsets(kindpats, ctx, listsubrepos)
     if fset:
         matchfuncs.append(fset.__contains__)
@@ -551,7 +581,7 @@  def readpatternfile(filepath, warn):
     pattern        # pattern of the current default type'''
 
     syntaxes = {'re': 'relre:', 'regexp': 'relre:', 'glob': 'relglob:',
-                'include': 'include'}
+                'include': 'include', 'subinclude': 'subinclude'}
     syntax = 'relre:'
     patterns = []
 
diff --git a/tests/test-hgignore.t b/tests/test-hgignore.t
--- a/tests/test-hgignore.t
+++ b/tests/test-hgignore.t
@@ -190,7 +190,45 @@  Check recursive uses of 'include:'
   $ hg status
   A dir/b.o
 
+  $ cp otherignore goodignore
   $ echo "include:badignore" >> otherignore
   $ hg status
   skipping unreadable pattern file 'badignore': No such file or directory
   A dir/b.o
+
+  $ mv goodignore otherignore
+
+Check including subincludes
+
+  $ hg revert -q --all
+  $ hg purge --all --config extensions.purge=
+  $ echo ".hgignore" > .hgignore
+  $ mkdir dir1 dir2
+  $ touch dir1/file1 dir1/file2 dir2/file1 dir2/file2
+  $ echo "subinclude:dir2/.hgignore" >> .hgignore
+  $ echo "glob:file*2" > dir2/.hgignore
+  $ hg status
+  ? dir1/file1
+  ? dir1/file2
+  ? dir2/file1
+
+Check including subincludes with regexs
+
+  $ echo "subinclude:dir1/.hgignore" >> .hgignore
+  $ echo "regexp:f.le1" > dir1/.hgignore
+
+  $ hg status
+  ? dir1/file2
+  ? dir2/file1
+
+Check multiple levels of sub-ignores
+
+  $ mkdir dir1/subdir
+  $ touch dir1/subdir/subfile1 dir1/subdir/subfile3 dir1/subdir/subfile4
+  $ echo "subinclude:subdir/.hgignore" >> dir1/.hgignore
+  $ echo "glob:subfil*3" >> dir1/subdir/.hgignore
+
+  $ hg status
+  ? dir1/file2
+  ? dir1/subdir/subfile4
+  ? dir2/file1