narrow: detect if narrowspec was changed in a different share
authorMartin von Zweigbergk <martinvonz@google.com>
Fri, 21 Dec 2018 10:13:49 -0800
changeset 41043 ce0bc2952e2a
parent 41042 7db1619af061
child 41044 fe606f2dcae9
narrow: detect if narrowspec was changed in a different share With this commit, `hg share` should be usable with narrow repos. Design explained on https://www.mercurial-scm.org/wiki/NarrowSharePlan I was running into cache invalidation problems when updating the narrowspec. After spending a day trying to figure out a good solution, I resorted to just assigning repo.narrowpats and repo._narrowmatch after invalidating them. Differential Revision: https://phab.mercurial-scm.org/D5278
hgext/narrow/TODO.rst
hgext/narrow/narrowcommands.py
mercurial/hg.py
mercurial/localrepo.py
mercurial/narrowspec.py
tests/test-narrow-debugcommands.t
tests/test-narrow-share.t
tests/test-narrow-trackedcmd.t
--- a/hgext/narrow/TODO.rst	Fri Jul 13 11:26:46 2018 -0700
+++ b/hgext/narrow/TODO.rst	Fri Dec 21 10:13:49 2018 -0800
@@ -1,6 +1,3 @@
-Integration with the share extension needs improvement. Right now
-we've seen some odd bugs.
-
 Address commentary in manifest.excludedmanifestrevlog.add -
 specifically we should improve the collaboration with core so that
 add() never gets called on an excluded directory and we can improve
--- a/hgext/narrow/narrowcommands.py	Fri Jul 13 11:26:46 2018 -0700
+++ b/hgext/narrow/narrowcommands.py	Fri Dec 21 10:13:49 2018 -0800
@@ -339,6 +339,8 @@
      ('', 'clear', False, _('whether to replace the existing narrowspec')),
      ('', 'force-delete-local-changes', False,
        _('forces deletion of local changes when narrowing')),
+     ('', 'update-working-copy', False,
+      _('update working copy when the store has changed')),
     ] + commands.remoteopts,
     _('[OPTIONS]... [REMOTE]'),
     inferrepo=True)
@@ -398,8 +400,9 @@
     addedexcludes = narrowspec.parsepatterns(opts['addexclude'])
     removedexcludes = narrowspec.parsepatterns(opts['removeexclude'])
 
+    update_working_copy = opts['update_working_copy']
     only_show = not (addedincludes or removedincludes or addedexcludes or
-                     removedexcludes or newrules)
+                     removedexcludes or newrules or update_working_copy)
 
     oldincludes, oldexcludes = repo.narrowpats
 
@@ -428,6 +431,12 @@
         fm.end()
         return 0
 
+    if update_working_copy:
+        with repo.wlock(), repo.lock(), repo.transaction('narrow-wc') as tr:
+            narrowspec.updateworkingcopy(repo, tr)
+            narrowspec.copytoworkingcopy(repo, tr)
+        return 0
+
     if not widening and not narrowing:
         ui.status(_("nothing to widen or narrow\n"))
         return 0
--- a/mercurial/hg.py	Fri Jul 13 11:26:46 2018 -0700
+++ b/mercurial/hg.py	Fri Dec 21 10:13:49 2018 -0800
@@ -38,6 +38,7 @@
     narrowspec,
     node,
     phases,
+    repository as repositorymod,
     scmutil,
     sshpeer,
     statichttprepo,
@@ -331,6 +332,9 @@
         template = ('[paths]\n'
                     'default = %s\n')
         destrepo.vfs.write('hgrc', util.tonativeeol(template % default))
+    if repositorymod.NARROW_REQUIREMENT in sourcerepo.requirements:
+        with destrepo.wlock():
+            narrowspec.copytoworkingcopy(destrepo, None)
 
 def _postshareupdate(repo, update, checkout=None):
     """Maybe perform a working directory update after a shared repo is created.
@@ -731,7 +735,7 @@
             local = destpeer.local()
             if local:
                 if narrow:
-                    with local.lock():
+                    with local.wlock(), local.lock():
                         local.setnarrowpats(storeincludepats, storeexcludepats)
 
                 u = util.url(abspath)
--- a/mercurial/localrepo.py	Fri Jul 13 11:26:46 2018 -0700
+++ b/mercurial/localrepo.py	Fri Dec 21 10:13:49 2018 -0800
@@ -1227,6 +1227,7 @@
     def _narrowmatch(self):
         if repository.NARROW_REQUIREMENT not in self.requirements:
             return matchmod.always(self.root, '')
+        narrowspec.checkworkingcopynarrowspec(self)
         include, exclude = self.narrowpats
         return narrowspec.match(self.root, include=include, exclude=exclude)
 
@@ -1251,7 +1252,14 @@
 
     def setnarrowpats(self, newincludes, newexcludes):
         narrowspec.save(self, newincludes, newexcludes)
+        narrowspec.copytoworkingcopy(self, self.currenttransaction())
         self.invalidate(clearfilecache=True)
+        # So the next access won't be considered a conflict
+        # TODO: It seems like there should be a way of doing this that
+        # doesn't involve replacing these attributes.
+        self.narrowpats = newincludes, newexcludes
+        self._narrowmatch = narrowspec.match(self.root, include=newincludes,
+                                             exclude=newexcludes)
 
     def __getitem__(self, changeid):
         if changeid is None:
--- a/mercurial/narrowspec.py	Fri Jul 13 11:26:46 2018 -0700
+++ b/mercurial/narrowspec.py	Fri Dec 21 10:13:49 2018 -0800
@@ -13,12 +13,16 @@
 from . import (
     error,
     match as matchmod,
+    merge,
     repository,
     sparse,
     util,
 )
 
+# The file in .hg/store/ that indicates which paths exit in the store
 FILENAME = 'narrowspec'
+# The file in .hg/ that indicates which paths exit in the dirstate
+DIRSTATE_FILENAME = 'narrowspec.dirstate'
 
 # Pattern prefixes that are allowed in narrow patterns. This list MUST
 # only contain patterns that are fast and safe to evaluate. Keep in mind
@@ -157,6 +161,18 @@
     spec = format(includepats, excludepats)
     repo.svfs.write(FILENAME, spec)
 
+def copytoworkingcopy(repo, tr):
+    if tr:
+        def write(file):
+            spec = repo.svfs.read(FILENAME)
+            file.write(spec)
+            file.close()
+        tr.addfilegenerator('narrowspec', (DIRSTATE_FILENAME,), write,
+                            location='plain')
+    else:
+        spec = repo.svfs.read(FILENAME)
+        repo.vfs.write(DIRSTATE_FILENAME, spec)
+
 def savebackup(repo, backupname):
     if repository.NARROW_REQUIREMENT not in repo.requirements:
         return
@@ -226,3 +242,57 @@
     else:
         res_includes = set(req_includes)
     return res_includes, res_excludes, invalid_includes
+
+# These two are extracted for extensions (specifically for Google's CitC file
+# system)
+def _deletecleanfiles(repo, files):
+    for f in files:
+        repo.wvfs.unlinkpath(f)
+
+def _writeaddedfiles(repo, pctx, files):
+    actions = merge.emptyactions()
+    addgaction = actions['g'].append
+    mf = repo['.'].manifest()
+    for f in files:
+        if not repo.wvfs.exists(f):
+            addgaction((f, (mf.flags(f), False), "narrowspec updated"))
+    merge.applyupdates(repo, actions, wctx=repo[None],
+                       mctx=repo['.'], overwrite=False)
+
+def checkworkingcopynarrowspec(repo):
+    storespec = repo.svfs.tryread(FILENAME)
+    wcspec = repo.vfs.tryread(DIRSTATE_FILENAME)
+    if wcspec != storespec:
+        raise error.Abort(_("working copy's narrowspec is stale"),
+                          hint=_("run 'hg tracked --update-working-copy'"))
+
+def updateworkingcopy(repo, tr):
+    oldspec = repo.vfs.tryread(DIRSTATE_FILENAME)
+    newspec = repo.svfs.tryread(FILENAME)
+
+    oldincludes, oldexcludes = parseconfig(repo.ui, oldspec)
+    newincludes, newexcludes = parseconfig(repo.ui, newspec)
+    oldmatch = match(repo.root, include=oldincludes, exclude=oldexcludes)
+    newmatch = match(repo.root, include=newincludes, exclude=newexcludes)
+    addedmatch = matchmod.differencematcher(newmatch, oldmatch)
+    removedmatch = matchmod.differencematcher(oldmatch, newmatch)
+
+    ds = repo.dirstate
+    lookup, status = ds.status(removedmatch, subrepos=[], ignored=False,
+                               clean=True, unknown=False)
+    _deletecleanfiles(repo, status.clean)
+    trackeddirty = lookup + status.modified + status.added
+    for f in sorted(trackeddirty):
+        repo.ui.status(_('not deleting possibly dirty file %s\n') % f)
+    for f in status.clean + trackeddirty:
+        ds.drop(f)
+
+    repo.narrowpats = newincludes, newexcludes
+    repo._narrowmatch = newmatch
+    pctx = repo['.']
+    newfiles = [f for f in pctx.manifest().walk(addedmatch) if f not in ds]
+    for f in newfiles:
+        ds.normallookup(f)
+    _writeaddedfiles(repo, pctx, newfiles)
+
+    ds.write(tr)
--- a/tests/test-narrow-debugcommands.t	Fri Jul 13 11:26:46 2018 -0700
+++ b/tests/test-narrow-debugcommands.t	Fri Dec 21 10:13:49 2018 -0800
@@ -6,6 +6,7 @@
   > path:foo
   > [exclude]
   > EOF
+  $ cp .hg/store/narrowspec .hg/narrowspec.dirstate
   $ echo treemanifest >> .hg/requires
   $ echo narrowhg-experimental >> .hg/requires
   $ mkdir -p foo/bar
--- a/tests/test-narrow-share.t	Fri Jul 13 11:26:46 2018 -0700
+++ b/tests/test-narrow-share.t	Fri Dec 21 10:13:49 2018 -0800
@@ -75,13 +75,20 @@
   deleting meta/d5/00manifest.i (tree !)
   $ hg -R main tracked
   I path:d7
+  $ hg -R main files
+  abort: working copy's narrowspec is stale
+  (run 'hg tracked --update-working-copy')
+  [255]
+  $ hg -R main tracked --update-working-copy
+  not deleting possibly dirty file d3/f
+  not deleting possibly dirty file d3/g
+  not deleting possibly dirty file d5/f
 # d1/f, d3/f, d3/g and d5/f should no longer be reported
   $ hg -R main files
   main/d7/f
 # d1/f should no longer be there, d3/f should be since it was dirty, d3/g should be there since
 # it was added, and d5/f should be since we couldn't be sure it was clean
   $ find main/d* -type f | sort
-  main/d1/f
   main/d3/f
   main/d3/g
   main/d5/f
@@ -102,16 +109,20 @@
   I path:d1
   I path:d3
   I path:d7
+  $ hg -R main files
+  abort: working copy's narrowspec is stale
+  (run 'hg tracked --update-working-copy')
+  [255]
+  $ hg -R main tracked --update-working-copy
 # d1/f, d3/f should be back
   $ hg -R main files
   main/d1/f
   main/d3/f
-  main/d3/g
   main/d7/f
 # d3/f should be modified (not clobbered by the widening), and d3/g should be untracked
   $ hg -R main st --all
   M d3/f
-  A d3/g
+  ? d3/g
   C d1/f
   C d7/f
 
@@ -130,3 +141,30 @@
   checking files
   checked 11 changesets with 3 changes to 3 files
   $ cd ..
+
+Dirstate should be left alone when upgrading from version of hg that didn't support narrow+share
+
+  $ hg share main share-upgrade
+  updating working directory
+  3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd share-upgrade
+  $ echo x >> d1/f
+  $ echo y >> d3/g
+  $ hg add d3/g
+  $ hg rm d7/f
+  $ hg st
+  M d1/f
+  A d3/g
+  R d7/f
+Make it look like a repo from before narrow+share was supported
+  $ rm .hg/narrowspec.dirstate
+  $ hg st
+  abort: working copy's narrowspec is stale
+  (run 'hg tracked --update-working-copy')
+  [255]
+  $ hg tracked --update-working-copy
+  $ hg st
+  M d1/f
+  A d3/g
+  R d7/f
+  $ cd ..
--- a/tests/test-narrow-trackedcmd.t	Fri Jul 13 11:26:46 2018 -0700
+++ b/tests/test-narrow-trackedcmd.t	Fri Dec 21 10:13:49 2018 -0800
@@ -107,6 +107,8 @@
       --clear                      whether to replace the existing narrowspec
       --force-delete-local-changes forces deletion of local changes when
                                    narrowing
+      --update-working-copy        update working copy when the store has
+                                   changed
    -e --ssh CMD                    specify ssh command to use
       --remotecmd CMD              specify hg command to run on the remote side
       --insecure                   do not verify server certificate (ignoring