Patchwork fsmonitor: hook up state-enter, state-leave signals

login
register
mail settings
Submitter Martijn Pieters
Date March 10, 2016, 4:31 p.m.
Message ID <330f21db633bb08e428c.1457627502@mjpieters-mbp>
Download mbox | patch
Permalink /patch/13742/
State Accepted
Delegated to: Laurent Charignon
Headers show

Comments

Martijn Pieters - March 10, 2016, 4:31 p.m.
# HG changeset patch
# User Martijn Pieters <mjpieters@fb.com>
# Date 1457625849 0
#      Thu Mar 10 16:04:09 2016 +0000
# Node ID 330f21db633bb08e428ca45eb54efd6805f05534
# Parent  911122ff47b7de30c991eebee5f2e5ea9ef0505c
fsmonitor: hook up state-enter, state-leave signals

Keeping the codebase in sync with upstream:

Watchman 4.4 introduced an advanced settling feature that allows publishing
tools to notify subscribing tools of the boundaries for important filesystem
operations.

https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
has more information about how this feature works.

This diff connects a signal that we're calling `hg.update` to the mercurial
update function so that mercurial can indirectly notify tools (such as IDEs or
build machinery) when it is changing the working copy.  This will allow those
tools to pause their normal actions as the files are changing and defer them
until the end of the operation.

In addition to sending the enter/leave signals for the state, we are able to
publish useful metadata along the same channel.  In this case we are passing
the following pieces of information:

1. destination revision hash
2. An estimate of the distance between the current state and the target state
3. A success indicator.
4. Whether it is a partial update

The distance is estimate may be useful to tools that wish to change their
strategy after the update has complete.  For example, a large update may be
efficient to deal with by walking some internal state in the subscriber rather
than feeding every individual file notification through its normal (small)
delta mechanism.

We estimate the distance by comparing the repository revision number.  In some
cases we cannot come up with a number so we report 0.  This is ok; we're
offering this for informational purposes only and don't guarantee its accuracy.

The success indicator is only really meaningful when we generate the
state-leave notification; it indicates the overall success of the update.
Pierre-Yves David - March 10, 2016, 5:06 p.m.
On 03/10/2016 04:31 PM, Martijn Pieters wrote:
> # HG changeset patch
> # User Martijn Pieters <mjpieters@fb.com>
> # Date 1457625849 0
> #      Thu Mar 10 16:04:09 2016 +0000
> # Node ID 330f21db633bb08e428ca45eb54efd6805f05534
> # Parent  911122ff47b7de30c991eebee5f2e5ea9ef0505c
> fsmonitor: hook up state-enter, state-leave signals

Pushed to the clowncopter, thanks.

Patch

diff --git a/hgext/fsmonitor/__init__.py b/hgext/fsmonitor/__init__.py
--- a/hgext/fsmonitor/__init__.py
+++ b/hgext/fsmonitor/__init__.py
@@ -99,6 +99,7 @@ 
     context,
     extensions,
     localrepo,
+    merge,
     pathutil,
     scmutil,
     util,
@@ -547,6 +548,8 @@ 
         # An assist for avoiding the dangling-symlink fsevents bug
         extensions.wrapfunction(os, 'symlink', wrapsymlink)
 
+    extensions.wrapfunction(merge, 'update', wrapupdate)
+
 def wrapsymlink(orig, source, link_name):
     ''' if we create a dangling symlink, also touch the parent dir
     to encourage fsevents notifications to work more correctly '''
@@ -558,6 +561,72 @@ 
         except OSError:
             pass
 
+class state_update(object):
+    ''' This context mananger is responsible for dispatching the state-enter
+        and state-leave signals to the watchman service '''
+
+    def __init__(self, repo, node, distance, partial):
+        self.repo = repo
+        self.node = node
+        self.distance = distance
+        self.partial = partial
+
+    def __enter__(self):
+        self._state('state-enter')
+        return self
+
+    def __exit__(self, type_, value, tb):
+        status = 'ok' if type_ is None else 'failed'
+        self._state('state-leave', status=status)
+
+    def _state(self, cmd, status='ok'):
+        if not util.safehasattr(self.repo, '_watchmanclient'):
+            return
+        try:
+            commithash = self.repo[self.node].hex()
+            self.repo._watchmanclient.command(cmd, {
+                'name': 'hg.update',
+                'metadata': {
+                    # the target revision
+                    'rev': commithash,
+                    # approximate number of commits between current and target
+                    'distance': self.distance,
+                    # success/failure (only really meaningful for state-leave)
+                    'status': status,
+                    # whether the working copy parent is changing
+                    'partial': self.partial,
+            }})
+        except Exception as e:
+            # Swallow any errors; fire and forget
+            self.repo.ui.log(
+                'watchman', 'Exception %s while running %s\n', e, cmd)
+
+
+# Bracket working copy updates with calls to the watchman state-enter
+# and state-leave commands.  This allows clients to perform more intelligent
+# settling during bulk file change scenarios
+# https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
+def wrapupdate(orig, repo, node, branchmerge, force, ancestor=None,
+               mergeancestor=False, labels=None, matcher=None, **kwargs):
+
+    distance = 0
+    partial = True
+    if matcher is None or matcher.always():
+        partial = False
+        wc = repo[None]
+        parents = wc.parents()
+        if len(parents) == 2:
+            anc = repo.changelog.ancestor(parents[0].node(), parents[1].node())
+            ancrev = repo[anc].rev()
+            distance = abs(repo[node].rev() - ancrev)
+        elif len(parents) == 1:
+            distance = abs(repo[node].rev() - parents[0].rev())
+
+    with state_update(repo, node, distance, partial):
+        return orig(
+            repo, node, branchmerge, force, ancestor, mergeancestor,
+            labels, matcher, *kwargs)
+
 def reposetup(ui, repo):
     # We don't work with largefiles or inotify
     exts = extensions.enabled()