Patchwork [1,of,2,RFC] completion: add a debugpathcomplete command

login
register
mail settings
Submitter Bryan O'Sullivan
Date March 13, 2013, 9:42 p.m.
Message ID <8d53d4c8d0e5b0b59fc8.1363210973@australite.local>
Download mbox | patch
Permalink /patch/1124/
State Superseded
Commit 10669e24eb6c664f13110cf6012e381778c730b3
Headers show

Comments

Bryan O'Sullivan - March 13, 2013, 9:42 p.m.
# HG changeset patch
# User Bryan O'Sullivan <bryano@fb.com>
# Date 1363210776 25200
# Node ID 8d53d4c8d0e5b0b59fc8dc1adb51802ec621975f
# Parent  cd2c82510aa230585fa50736a9e05f169c812dad
completion: add a debugpathcomplete command

The bash_completion code uses "hg status" to generate a list of
possible completions for commands that operate in the working
directory. In a large working directory, this results in a single
tab-completion being very slow (several seconds) as a result of
checking the status of every file, even when there are *no* possible
matches.

The new debugpathcomplete command does not check the status of
files, and is specialized to only complete pathnames using the
contents of the dirstate.

Since it never touches the working directory, it is much faster in
e.g. mozilla-central (~65,000 files):

  debugpathcomplete  0.17
  status             0.85
Bryan O'Sullivan - March 14, 2013, 3:52 p.m.
On Wed, Mar 13, 2013 at 6:53 PM, Kevin Bullock <
kbullock+mercurial@ringworld.org> wrote:

>
> dirstate has __getitem__, __iter__, and __contains__ implementations that
> should prevent you needing to frob its _map directly here. It'll even do
> the sorting.


I actually don't want the traversal to be sorted in an additional step,
because then I'm sorting 200,000 items instead of the handful that actually
match. As we know from painful experience, such a sort adds 0.2 seconds to
the execution time for no benefit.
Bryan O'Sullivan - March 14, 2013, 4:35 p.m.
On Thu, Mar 14, 2013 at 9:27 AM, Kevin Bullock <
kbullock+mercurial@ringworld.org> wrote:

> I figured you'd say that ;) Digging into the map directly is still ugly
> though.
>

Agreed.

Patch

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -2109,6 +2109,66 @@  def debugobsolete(ui, repo, precursor=No
                                          sorted(m.metadata().items()))))
             ui.write('\n')
 
+@command('debugpathcomplete',
+         [('', 'full', None, _('complete an entire path')),
+          ('n', 'normal', None, _('show only normal files')),
+          ('a', 'added', None, _('show only added files')),
+          ('r', 'removed', None, _('show only removed files'))],
+         _('FILESPEC...'))
+def debugpathcomplete(ui, repo, *specs, **opts):
+    '''complete part or all of a tracked path
+
+    This command supports shells that offer path name completion. It
+    currently completes only files already known to the dirstate.
+
+    Completion extends only to the next pathname segment unless
+    --full is specified.'''
+
+    def complete(path, acceptable):
+        dirstate = repo.dirstate
+        spec = os.path.normpath(os.path.join(os.getcwd(), path))
+        if not spec.startswith(dirstate._rootdir):
+            return []
+        if os.path.isdir(spec):
+            spec += '/'
+        spec = spec[len(dirstate._rootdir):]
+        speclen = len(spec)
+        if opts['full']:
+            return sorted(f for f in dirstate._map
+                          if (f.startswith(spec) and
+                              dirstate._map[f][0] in acceptable))
+        matches = set()
+        for f in dirstate._map:
+            if f.startswith(spec) and dirstate._map[f][0] in acceptable:
+                s = f.find('/', speclen)
+                if os.sep != '/':
+                    f = f.replace('/', os.sep)
+                if s >= 0:
+                    f = f[:s+1]
+                    # if a match is a directory, force the shell to
+                    # consider the match as ambiguous, otherwise it
+                    # will append a trailing space
+                    matches.add(f + 'a')
+                    matches.add(f + 'b')
+                else:
+                    matches.add(f)
+        return sorted(matches)
+
+    acceptable = ''
+    if opts['normal']:
+        acceptable += 'nm'
+    if opts['added']:
+        acceptable += 'a'
+    if opts['removed']:
+        acceptable += 'r'
+    cwd = repo.getcwd()
+    if not specs:
+        specs = ['.']
+
+    for spec in specs:
+        for p in complete(spec, acceptable or 'nmar?'):
+            ui.write(repo.pathto(p, cwd) + '\n')
+
 @command('debugpushkey', [], _('REPO NAMESPACE [KEY OLD NEW]'))
 def debugpushkey(ui, repopath, namespace, *keyinfo, **opts):
     '''access the pushkey key/value protocol