fsmonitor: add new watchman notifications to fsmonitor extension
authorEamonn Kent <ekent@fb.com>
Mon, 09 Oct 2017 10:09:36 -0700
changeset 34565 4aa57627692a
parent 34564 b79f59425964
child 34566 60213a2eca81
fsmonitor: add new watchman notifications to fsmonitor extension The fsmonitor extension currently sends state-enter and state-leave notifications to watchman on the update operation. This commit creates additional notifications for the following events : - transaction creation and commit/abort. A state-enter notification will be sent when a transaction is created. It will provide the working copy parent's hash. A state-leave notification will be sent when the transaction is committed or aborted. It will provide the working copy parent's hash. - calls to set-parent will cause state-enter and state-leave notifications to be sent. The state-enter notification will be sent prior to the set-parent operation and the working copy parent's hash will be provided at this time. The state-leave notification will be sent after the set-parent operation completes providing the working copy parents hash. Test Plan: tested on dev server to check that necessary notifications were sent/received Differential Revision: https://phab.mercurial-scm.org/D989
hgext/fsmonitor/__init__.py
--- a/hgext/fsmonitor/__init__.py	Mon Oct 09 10:09:36 2017 -0700
+++ b/hgext/fsmonitor/__init__.py	Mon Oct 09 10:09:36 2017 -0700
@@ -96,8 +96,14 @@
 import os
 import stat
 import sys
+import weakref
 
 from mercurial.i18n import _
+from mercurial.node import (
+    hex,
+    nullid,
+)
+
 from mercurial import (
     context,
     encoding,
@@ -555,11 +561,12 @@
 
 def makedirstate(repo, dirstate):
     class fsmonitordirstate(dirstate.__class__):
-        def _fsmonitorinit(self, fsmonitorstate, watchmanclient):
+        def _fsmonitorinit(self, repo):
             # _fsmonitordisable is used in paranoid mode
             self._fsmonitordisable = False
-            self._fsmonitorstate = fsmonitorstate
-            self._watchmanclient = watchmanclient
+            self._fsmonitorstate = repo._fsmonitorstate
+            self._watchmanclient = repo._watchmanclient
+            self._repo = weakref.proxy(repo)
 
         def walk(self, *args, **kwargs):
             orig = super(fsmonitordirstate, self).walk
@@ -575,8 +582,16 @@
             self._fsmonitorstate.invalidate()
             return super(fsmonitordirstate, self).invalidate(*args, **kwargs)
 
+        if dirstate._ui.configbool(
+            "experimental", "fsmonitor.wc_change_notify"):
+            def setparents(self, p1, p2=nullid):
+                with state_update(self._repo, name="hg.wc_change",
+                                  oldnode=self._pl[0], newnode=p1,
+                                  partial=False):
+                    return super(fsmonitordirstate, self).setparents(p1, p2)
+
     dirstate.__class__ = fsmonitordirstate
-    dirstate._fsmonitorinit(repo._fsmonitorstate, repo._watchmanclient)
+    dirstate._fsmonitorinit(repo)
 
 def wrapdirstate(orig, self):
     ds = orig(self)
@@ -607,47 +622,74 @@
 
 class state_update(object):
     ''' This context manager is responsible for dispatching the state-enter
-        and state-leave signals to the watchman service '''
+        and state-leave signals to the watchman service. The enter and leave
+        methods can be invoked manually (for scenarios where context manager
+        semantics are not possible). If parameters oldnode and newnode are None,
+        they will be populated based on current working copy in enter and
+        leave, respectively. Similarly, if the distance is none, it will be
+        calculated based on the oldnode and newnode in the leave method.'''
 
-    def __init__(self, repo, node, distance, partial):
-        self.repo = repo
-        self.node = node
+    def __init__(self, repo, name, oldnode=None, newnode=None, distance=None,
+                 partial=False):
+        self.repo = repo.unfiltered()
+        self.name = name
+        self.oldnode = oldnode
+        self.newnode = newnode
         self.distance = distance
         self.partial = partial
         self._lock = None
         self.need_leave = False
 
     def __enter__(self):
+        self.enter()
+
+    def enter(self):
         # We explicitly need to take a lock here, before we proceed to update
         # watchman about the update operation, so that we don't race with
         # some other actor.  merge.update is going to take the wlock almost
         # immediately anyway, so this is effectively extending the lock
         # around a couple of short sanity checks.
+        if self.oldnode is None:
+            self.oldnode = self.repo['.'].node()
         self._lock = self.repo.wlock()
-        self.need_leave = self._state('state-enter')
+        self.need_leave = self._state(
+            'state-enter',
+            hex(self.oldnode))
         return self
 
     def __exit__(self, type_, value, tb):
+        abort = True if type_ else False
+        self.exit(abort=abort)
+
+    def exit(self, abort=False):
         try:
             if self.need_leave:
-                status = 'ok' if type_ is None else 'failed'
-                self._state('state-leave', status=status)
+                status = 'failed' if abort else 'ok'
+                if self.newnode is None:
+                    self.newnode = self.repo['.'].node()
+                if self.distance is None:
+                    self.distance = calcdistance(
+                        self.repo, self.oldnode, self.newnode)
+                self._state(
+                    'state-leave',
+                    hex(self.newnode),
+                    status=status)
         finally:
+            self.need_leave = False
             if self._lock:
                 self._lock.release()
 
-    def _state(self, cmd, status='ok'):
+    def _state(self, cmd, commithash, status='ok'):
         if not util.safehasattr(self.repo, '_watchmanclient'):
             return False
         try:
-            commithash = self.repo[self.node].hex()
             self.repo._watchmanclient.command(cmd, {
-                'name': 'hg.update',
+                'name': self.name,
                 'metadata': {
                     # the target revision
                     'rev': commithash,
                     # approximate number of commits between current and target
-                    'distance': self.distance,
+                    'distance': self.distance if self.distance else 0,
                     # success/failure (only really meaningful for state-leave)
                     'status': status,
                     # whether the working copy parent is changing
@@ -677,12 +719,14 @@
 
     distance = 0
     partial = True
+    oldnode = repo['.'].node()
+    newnode = repo[node].node()
     if matcher is None or matcher.always():
         partial = False
-        distance = calcdistance(repo.unfiltered(), repo['.'].node(),
-                                repo[node].node())
+        distance = calcdistance(repo.unfiltered(), oldnode, newnode)
 
-    with state_update(repo, node, distance, partial):
+    with state_update(repo, name="hg.update", oldnode=oldnode, newnode=newnode,
+                      distance=distance, partial=partial):
         return orig(
             repo, node, branchmerge, force, ancestor, mergeancestor,
             labels, matcher, **kwargs)
@@ -728,4 +772,32 @@
                 orig = super(fsmonitorrepo, self).status
                 return overridestatus(orig, self, *args, **kwargs)
 
+            if ui.configbool("experimental", "fsmonitor.transaction_notify"):
+                def transaction(self, *args, **kwargs):
+                    tr = super(fsmonitorrepo, self).transaction(
+                               *args, **kwargs)
+                    if tr.count != 1:
+                        return tr
+                    stateupdate = state_update(self, name="hg.transaction")
+                    stateupdate.enter()
+
+                    class fsmonitortrans(tr.__class__):
+                        def _abort(self):
+                            try:
+                                result = super(fsmonitortrans, self)._abort()
+                            finally:
+                                stateupdate.exit(abort=True)
+                            return result
+
+                        def close(self):
+                            try:
+                                result = super(fsmonitortrans, self).close()
+                            finally:
+                                if self.count == 0:
+                                    stateupdate.exit()
+                            return result
+
+                    tr.__class__ = fsmonitortrans
+                    return tr
+
         repo.__class__ = fsmonitorrepo