completion: add a debugpathcomplete command
The bash_completion code uses "hg status" to generate a list of
possible completions for commands that operate on files in the
working directory. In a large working directory, this can result
in a single tab-completion being very slow (several seconds) as a
result of checking the status of every file, even when there is no
need to check status or no possible matches.
The new debugpathcomplete command gains performance in a few simple
ways:
* Allow completion to operate on just a single directory. When used
to complete the right commands, this considerably reduces the
number of completions returned, at no loss in functionality.
* Never check the status of files. For completions that really must
know if a file is modified, it is faster to use status:
hg status -nm 'glob:myprefix**'
Performance:
Here are the commands used by bash_completion to complete, run in
the root of the mozilla-central working dir (~77,000 files) and
another repo (~165,000 files):
All "normal state" files (used by e.g. remove, revert):
mozilla other
status -nmcd 'glob:**' 1.77 4.10 sec
debugpathcomplete -f -n 0.53 1.26
debugpathcomplete -n 0.17 0.41
("-f" means "complete full paths", rather than the current directory)
Tracked files matching "a":
mozilla other
status -nmcd 'glob:a**' 0.26 0.47
debugpathcomplete -f -n a 0.10 0.24
debugpathcomplete -n a 0.10 0.22
We should be able to further improve completion performance once
the critbit work lands. Right now, our performance is limited by
the need to iterate over all keys in the dirstate.
--- a/mercurial/commands.py Thu Mar 21 11:35:34 2013 -0700
+++ b/mercurial/commands.py Thu Mar 21 16:31:28 2013 -0700
@@ -2137,6 +2137,73 @@
sorted(m.metadata().items()))))
ui.write('\n')
+@command('debugpathcomplete',
+ [('f', '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 path segment unless
+ --full is specified, in which case entire paths are used.'''
+
+ def complete(path, acceptable):
+ dirstate = repo.dirstate
+ spec = os.path.normpath(os.path.join(os.getcwd(), path))
+ rootdir = repo.root + os.sep
+ if spec != repo.root and not spec.startswith(rootdir):
+ return [], []
+ if os.path.isdir(spec):
+ spec += '/'
+ spec = spec[len(rootdir):]
+ fixpaths = os.sep != '/'
+ if fixpaths:
+ spec = spec.replace(os.sep, '/')
+ speclen = len(spec)
+ fullpaths = opts['full']
+ files, dirs = set(), set()
+ adddir, addfile = dirs.add, files.add
+ for f, st in dirstate.iteritems():
+ if f.startswith(spec) and st[0] in acceptable:
+ if fixpaths:
+ f = f.replace('/', os.sep)
+ if fullpaths:
+ addfile(f)
+ continue
+ s = f.find(os.sep, speclen)
+ if s >= 0:
+ adddir(f[:s+1])
+ else:
+ addfile(f)
+ return files, dirs
+
+ acceptable = ''
+ if opts['normal']:
+ acceptable += 'nm'
+ if opts['added']:
+ acceptable += 'a'
+ if opts['removed']:
+ acceptable += 'r'
+ cwd = repo.getcwd()
+ if not specs:
+ specs = ['.']
+
+ files, dirs = set(), set()
+ for spec in specs:
+ f, d = complete(spec, acceptable or 'nmar')
+ files.update(f)
+ dirs.update(d)
+ for d in dirs:
+ files.add(d + 'a')
+ files.add(d + 'b')
+ ui.write('\n'.join(repo.pathto(p, cwd) for p in sorted(files)))
+ ui.write('\n')
+
@command('debugpushkey', [], _('REPO NAMESPACE [KEY OLD NEW]'))
def debugpushkey(ui, repopath, namespace, *keyinfo, **opts):
'''access the pushkey key/value protocol
--- a/mercurial/dirstate.py Thu Mar 21 11:35:34 2013 -0700
+++ b/mercurial/dirstate.py Thu Mar 21 16:31:28 2013 -0700
@@ -223,6 +223,9 @@
for x in sorted(self._map):
yield x
+ def iteritems(self):
+ return self._map.iteritems()
+
def parents(self):
return [self._validate(p) for p in self._pl]