--- a/mercurial/commands.py Wed Jul 20 11:40:27 2005 -0500
+++ b/mercurial/commands.py Wed Jul 20 20:00:29 2005 -0500
@@ -14,6 +14,9 @@
class UnknownCommand(Exception):
"""Exception raised if command is not in the command table."""
+class Abort(Exception):
+ """Raised if a command needs to print an error and exit."""
+
def filterfiles(filters, files):
l = [x for x in files if x in filters]
@@ -36,6 +39,41 @@
for x in args]
return args
+def matchpats(ui, cwd, pats = [], opts = {}, emptyok = True):
+ if not pats and not emptyok:
+ raise Abort('at least one file name or pattern required')
+ head = ''
+ if opts.get('rootless'): head = '(?:.*/|)'
+ def reify(name, tail):
+ if name.startswith('re:'):
+ return name[3:]
+ elif name.startswith('glob:'):
+ return head + util.globre(name[5:], '', tail)
+ elif name.startswith('path:'):
+ return '^' + re.escape(name[5:]) + '$'
+ return head + util.globre(name, '', tail)
+ cwdsep = cwd + os.sep
+ def under(fn):
+ if not cwd or fn.startswith(cwdsep): return True
+ def matchfn(pats, tail, ifempty = util.always):
+ if not pats: return ifempty
+ pat = '(?:%s)' % '|'.join([reify(p, tail) for p in pats])
+ if cwd: pat = re.escape(cwd + os.sep) + pat
+ ui.debug('regexp: %s\n' % pat)
+ return re.compile(pat).match
+ patmatch = matchfn(pats, '$')
+ incmatch = matchfn(opts.get('include'), '(?:/|$)', under)
+ excmatch = matchfn(opts.get('exclude'), '(?:/|$)', util.never)
+ return lambda fn: (incmatch(fn) and not excmatch(fn) and
+ (fn.endswith('/') or patmatch(fn)))
+
+def walk(repo, pats, opts, emptyok = True):
+ cwd = repo.getcwd()
+ if cwd: c = len(cwd) + 1
+ for src, fn in repo.walk(match = matchpats(repo.ui, cwd, pats, opts, emptyok)):
+ if cwd: yield src, fn, fn[c:]
+ else: yield src, fn, fn
+
revrangesep = ':'
def revrange(ui, repo, revs, revlog=None):
@@ -60,8 +98,7 @@
try:
num = revlog.rev(revlog.lookup(val))
except KeyError:
- ui.warn('abort: invalid revision identifier %s\n' % val)
- sys.exit(1)
+ raise Abort('invalid revision identifier %s', val)
return num
for spec in revs:
if spec.find(revrangesep) >= 0:
@@ -91,29 +128,45 @@
'b': lambda: os.path.basename(repo.root),
}
- if node:
- expander.update(node_expander)
- if node and revwidth is not None:
- expander['r'] = lambda: str(r.rev(node)).zfill(revwidth)
- if total is not None:
- expander['N'] = lambda: str(total)
- if seqno is not None:
- expander['n'] = lambda: str(seqno)
- if total is not None and seqno is not None:
- expander['n'] = lambda:str(seqno).zfill(len(str(total)))
+ try:
+ if node:
+ expander.update(node_expander)
+ if node and revwidth is not None:
+ expander['r'] = lambda: str(r.rev(node)).zfill(revwidth)
+ if total is not None:
+ expander['N'] = lambda: str(total)
+ if seqno is not None:
+ expander['n'] = lambda: str(seqno)
+ if total is not None and seqno is not None:
+ expander['n'] = lambda:str(seqno).zfill(len(str(total)))
- newname = []
- patlen = len(pat)
- i = 0
- while i < patlen:
- c = pat[i]
- if c == '%':
+ newname = []
+ patlen = len(pat)
+ i = 0
+ while i < patlen:
+ c = pat[i]
+ if c == '%':
+ i += 1
+ c = pat[i]
+ c = expander[c]()
+ newname.append(c)
i += 1
- c = pat[i]
- c = expander[c]()
- newname.append(c)
- i += 1
- return ''.join(newname)
+ return ''.join(newname)
+ except KeyError, inst:
+ raise Abort("invalid format spec '%%%s' in output file name",
+ inst.args[0])
+
+def make_file(repo, r, pat, node=None,
+ total=None, seqno=None, revwidth=None, mode='wb'):
+ if not pat or pat == '-':
+ if 'w' in mode: return sys.stdout
+ else: return sys.stdin
+ if hasattr(pat, 'write') and 'w' in mode:
+ return pat
+ if hasattr(pat, 'read') and 'r' in mode:
+ return pat
+ return open(make_filename(repo, r, pat, node, total, seqno, revwidth),
+ mode)
def dodiff(fp, ui, repo, files=None, node1=None, node2=None):
def date(c):
@@ -288,9 +341,17 @@
# Commands start here, listed alphabetically
-def add(ui, repo, file1, *files):
+def add(ui, repo, *pats, **opts):
'''add the specified files on the next commit'''
- repo.add(relpath(repo, (file1,) + files))
+ names = []
+ q = dict(zip(pats, pats))
+ for src, abs, rel in walk(repo, pats, opts):
+ if rel in q or abs in q:
+ names.append(abs)
+ elif repo.dirstate.state(abs) == '?':
+ ui.status('adding %s\n' % rel)
+ names.append(abs)
+ repo.add(names)
def addremove(ui, repo, *files):
"""add all new files, delete all missing files"""
@@ -307,11 +368,11 @@
elif s not in 'nmai' and isfile:
u.append(f)
else:
- (c, a, d, u) = repo.changes(None, None)
+ (c, a, d, u) = repo.changes()
repo.add(u)
repo.remove(d)
-def annotate(ui, repo, file1, *files, **opts):
+def annotate(ui, repo, *pats, **opts):
"""show changeset information per file line"""
def getnode(rev):
return hg.short(repo.changelog.node(rev))
@@ -342,8 +403,8 @@
node = repo.dirstate.parents()[0]
change = repo.changelog.read(node)
mmap = repo.manifest.read(change[0])
- for f in relpath(repo, (file1,) + files):
- lines = repo.file(f).annotate(mmap[f])
+ for src, abs, rel in walk(repo, pats, opts, emptyok = False):
+ lines = repo.file(abs).annotate(mmap[abs])
pieces = []
for o, f in opmap:
@@ -362,16 +423,7 @@
n = r.lookup(rev)
else:
n = r.tip()
- if opts['output'] and opts['output'] != '-':
- try:
- outname = make_filename(repo, r, opts['output'], node=n)
- fp = open(outname, 'wb')
- except KeyError, inst:
- ui.warn("error: invlaid format spec '%%%s' in output file name\n" %
- inst.args[0])
- sys.exit(1);
- else:
- fp = sys.stdout
+ fp = make_file(repo, r, opts['output'], node=n)
fp.write(r.read(n))
def clone(ui, source, dest=None, **opts):
@@ -475,8 +527,7 @@
ui.warn("%s in manifest1, but listed as state %s" % (f, state))
errors += 1
if errors:
- ui.warn(".hg/dirstate inconsistent with current parent's manifest\n")
- sys.exit(1)
+ raise Abort(".hg/dirstate inconsistent with current parent's manifest")
def debugstate(ui, repo):
"""show the contents of the current dirstate"""
@@ -509,21 +560,18 @@
ui.write("\t%d -> %d\n" % (r.rev(e[5]), i))
ui.write("}\n")
-def diff(ui, repo, *files, **opts):
+def diff(ui, repo, *pats, **opts):
"""diff working directory (or selected files)"""
revs = []
if opts['rev']:
revs = map(lambda x: repo.lookup(x), opts['rev'])
if len(revs) > 2:
- ui.warn("too many revisions to diff\n")
- sys.exit(1)
+ raise Abort("too many revisions to diff")
- if files:
- files = relpath(repo, files)
- else:
- files = relpath(repo, [""])
-
+ files = []
+ for src, abs, rel in walk(repo, pats, opts):
+ files.append(abs)
dodiff(sys.stdout, ui, repo, files, *revs)
def doexport(ui, repo, changeset, seqno, total, revwidth, opts):
@@ -531,19 +579,11 @@
prev, other = repo.changelog.parents(node)
change = repo.changelog.read(node)
- if opts['output'] and opts['output'] != '-':
- try:
- outname = make_filename(repo, repo.changelog, opts['output'],
- node=node, total=total, seqno=seqno,
- revwidth=revwidth)
- ui.note("Exporting patch to '%s'.\n" % outname)
- fp = open(outname, 'wb')
- except KeyError, inst:
- ui.warn("error: invalid format spec '%%%s' in output file name\n" %
- inst.args[0])
- sys.exit(1)
- else:
- fp = sys.stdout
+ fp = make_file(repo, repo.changelog, opts['output'],
+ node=node, total=total, seqno=seqno,
+ revwidth=revwidth)
+ if fp != sys.stdout:
+ ui.note("Exporting patch to '%s'.\n" % fp.name)
fp.write("# HG changeset patch\n")
fp.write("# User %s\n" % change[1])
@@ -555,12 +595,12 @@
fp.write("\n\n")
dodiff(fp, ui, repo, None, prev, node)
+ if fp != sys.stdout: fp.close()
def export(ui, repo, *changesets, **opts):
"""dump the header and diffs for one or more changesets"""
if not changesets:
- ui.warn("error: export requires at least one changeset\n")
- sys.exit(1)
+ raise Abort("export requires at least one changeset")
seqno = 0
revs = list(revrange(ui, repo, changesets))
total = len(revs)
@@ -586,7 +626,7 @@
return
hexfunc = ui.verbose and hg.hex or hg.short
- (c, a, d, u) = repo.changes(None, None)
+ (c, a, d, u) = repo.changes()
output = ["%s%s" % ('+'.join([hexfunc(parent) for parent in parents]),
(c or a or d) and "+" or "")]
@@ -654,8 +694,7 @@
files.append(pf)
patcherr = f.close()
if patcherr:
- sys.stderr.write("patch failed")
- sys.exit(1)
+ raise Abort("patch failed")
if len(files) > 0:
addremove(ui, repo, *files)
@@ -665,52 +704,20 @@
"""create a new repository in the current directory"""
if source:
- ui.warn("no longer supported: use \"hg clone\" instead\n")
- sys.exit(1)
+ raise Abort("no longer supported: use \"hg clone\" instead")
hg.repository(ui, ".", create=1)
def locate(ui, repo, *pats, **opts):
"""locate files matching specific patterns"""
- if [p for p in pats if os.sep in p]:
- ui.warn("error: patterns may not contain '%s'\n" % os.sep)
- ui.warn("use '-i <dir>' instead\n")
- sys.exit(1)
- def compile(pats, head='^', tail=os.sep, on_empty=True):
- if not pats:
- class c:
- def match(self, x):
- return on_empty
- return c()
- fnpats = [fnmatch.translate(os.path.normpath(os.path.normcase(p)))[:-1]
- for p in pats]
- regexp = r'%s(?:%s)%s' % (head, '|'.join(fnpats), tail)
- return re.compile(regexp)
- exclude = compile(opts['exclude'], on_empty=False)
- include = compile(opts['include'])
- pat = compile(pats, head='', tail='$')
- end = opts['print0'] and '\0' or '\n'
- if opts['rev']:
- node = repo.manifest.lookup(opts['rev'])
- else:
- node = repo.manifest.tip()
- manifest = repo.manifest.read(node)
- cwd = repo.getcwd()
- cwd_plus = cwd and (cwd + os.sep)
- found = []
- for f in manifest:
- f = os.path.normcase(f)
- if exclude.match(f) or not(include.match(f) and
- f.startswith(cwd_plus) and
- pat.match(os.path.basename(f))):
- continue
+ if opts['print0']: end = '\0'
+ else: end = '\n'
+ opts['rootless'] = True
+ for src, abs, rel in walk(repo, pats, opts):
+ if repo.dirstate.state(abs) == '?': continue
if opts['fullpath']:
- f = os.path.join(repo.root, f)
- elif cwd:
- f = f[len(cwd_plus):]
- found.append(f)
- found.sort()
- for f in found:
- ui.write(f, end)
+ ui.write(os.path.join(repo.root, abs), end)
+ else:
+ ui.write(rel, end)
def log(ui, repo, f=None, **opts):
"""show the revision history of the repository or a single file"""
@@ -746,6 +753,11 @@
dodiff(sys.stdout, ui, repo, files, prev, changenode)
ui.write("\n\n")
+def ls(ui, repo, *pats, **opts):
+ """list files"""
+ for src, abs, rel in walk(repo, pats, opts):
+ ui.write(rel, '\n')
+
def manifest(ui, repo, rev=None):
"""output the latest or given revision of the project manifest"""
if rev:
@@ -978,7 +990,7 @@
ui.status('listening at http://%s/\n' % addr)
httpd.serve_forever()
-def status(ui, repo):
+def status(ui, repo, *pats, **opts):
'''show changed files in the working directory
C = changed
@@ -986,7 +998,8 @@
R = removed
? = not tracked'''
- (c, a, d, u) = repo.changes(None, None)
+ (c, a, d, u) = repo.changes(match = matchpats(ui, repo.getcwd(),
+ pats, opts))
(c, a, d, u) = map(lambda x: relfilter(repo, x), (c, a, d, u))
for f in c:
@@ -1017,7 +1030,7 @@
repo.opener("localtags", "a").write("%s %s\n" % (r, name))
return
- (c, a, d, u) = repo.changes(None, None)
+ (c, a, d, u) = repo.changes()
for x in (c, a, d, u):
if ".hgtags" in x:
ui.warn("abort: working copy of .hgtags is changed!\n")
@@ -1088,11 +1101,16 @@
# Command options and aliases are listed here, alphabetically
table = {
- "^add": (add, [], "hg add FILE..."),
- "addremove": (addremove, [], "hg addremove [FILE]..."),
+ "^add": (add,
+ [('I', 'include', [], 'include path in search'),
+ ('X', 'exclude', [], 'exclude path from search')],
+ "hg add [OPTIONS] [FILES]"),
+ "addremove": (addremove, [], "hg addremove [FILES]"),
"^annotate":
(annotate,
- [('r', 'rev', '', 'revision'),
+ [('I', 'include', [], 'include path in search'),
+ ('X', 'exclude', [], 'exclude path from search'),
+ ('r', 'rev', '', 'revision'),
('u', 'user', None, 'show user'),
('n', 'number', None, 'show revision number'),
('c', 'changeset', None, 'show changeset')],
@@ -1120,7 +1138,9 @@
"debugindexdot": (debugindexdot, [], 'debugindexdot FILE'),
"^diff":
(diff,
- [('r', 'rev', [], 'revision')],
+ [('I', 'include', [], 'include path in search'),
+ ('X', 'exclude', [], 'exclude path from search'),
+ ('r', 'rev', [], 'revision')],
'hg diff [-r REV1 [-r REV2]] [FILE]...'),
"^export":
(export,
@@ -1140,15 +1160,19 @@
(locate,
[('0', 'print0', None, 'end records with NUL'),
('f', 'fullpath', None, 'print complete paths'),
- ('i', 'include', [], 'include path in search'),
+ ('I', 'include', [], 'include path in search'),
('r', 'rev', '', 'revision'),
- ('x', 'exclude', [], 'exclude path from search')],
+ ('X', 'exclude', [], 'exclude path from search')],
'hg locate [OPTION]... [PATTERN]...'),
"^log|history":
(log,
[('r', 'rev', [], 'revision'),
('p', 'patch', None, 'show patch')],
'hg log [-r REV1 [-r REV2]] [-p] [FILE]'),
+ "list|ls": (ls,
+ [('I', 'include', [], 'include path in search'),
+ ('X', 'exclude', [], 'exclude path from search')],
+ "hg ls [OPTION]... [PATTERN]...."),
"manifest": (manifest, [], 'hg manifest [REV]'),
"parents": (parents, [], 'hg parents [REV]'),
"^pull":
@@ -1183,7 +1207,10 @@
('', 'stdio', None, 'for remote clients'),
('t', 'templates', "", 'template map')],
"hg serve [OPTION]..."),
- "^status": (status, [], 'hg status'),
+ "^status": (status,
+ [('I', 'include', [], 'include path in search'),
+ ('X', 'exclude', [], 'exclude path from search')],
+ 'hg status [OPTION]... [FILE]...'),
"tag":
(tag,
[('l', 'local', None, 'make the tag local'),
@@ -1344,6 +1371,9 @@
u.warn("abort: %s: %s\n" % (inst.strerror, inst.filename))
else:
u.warn("abort: %s\n" % inst.strerror)
+ except Abort, inst:
+ u.warn('abort: ', inst.args[0] % inst.args[1:], '\n')
+ sys.exit(1)
except TypeError, inst:
# was this an argument error?
tb = traceback.extract_tb(sys.exc_info()[2])
--- a/mercurial/hg.py Wed Jul 20 11:40:27 2005 -0500
+++ b/mercurial/hg.py Wed Jul 20 20:00:29 2005 -0500
@@ -277,6 +277,36 @@
self.map = None
self.pl = None
self.copies = {}
+ self.ignorefunc = None
+
+ def wjoin(self, f):
+ return os.path.join(self.root, f)
+
+ def ignore(self, f):
+ if not self.ignorefunc:
+ bigpat = []
+ try:
+ l = file(self.wjoin(".hgignore"))
+ for pat in l:
+ if pat != "\n":
+ p = util.pconvert(pat[:-1])
+ try:
+ r = re.compile(p)
+ except:
+ self.ui.warn("ignoring invalid ignore"
+ + " regular expression '%s'\n" % p)
+ else:
+ bigpat.append(util.pconvert(pat[:-1]))
+ except IOError: pass
+
+ if bigpat:
+ s = "(?:%s)" % (")|(?:".join(bigpat))
+ r = re.compile(s)
+ self.ignorefunc = r.search
+ else:
+ self.ignorefunc = util.never
+
+ return self.ignorefunc(f)
def __del__(self):
if self.dirty:
@@ -298,8 +328,12 @@
self.read()
return self.pl
+ def markdirty(self):
+ if not self.dirty:
+ self.dirty = 1
+
def setparents(self, p1, p2 = nullid):
- self.dirty = 1
+ self.markdirty()
self.pl = p1, p2
def state(self, key):
@@ -334,7 +368,7 @@
def copy(self, source, dest):
self.read()
- self.dirty = 1
+ self.markdirty()
self.copies[dest] = source
def copied(self, file):
@@ -349,7 +383,7 @@
if not files: return
self.read()
- self.dirty = 1
+ self.markdirty()
for f in files:
if state == "r":
self.map[f] = ('r', 0, 0, 0)
@@ -360,7 +394,7 @@
def forget(self, files):
if not files: return
self.read()
- self.dirty = 1
+ self.markdirty()
for f in files:
try:
del self.map[f]
@@ -370,7 +404,7 @@
def clear(self):
self.map = {}
- self.dirty = 1
+ self.markdirty()
def write(self):
st = self.opener("dirstate", "w")
@@ -383,34 +417,50 @@
st.write(e + f)
self.dirty = 0
- def changes(self, files, ignore):
+ def walk(self, files = None, match = util.always):
self.read()
dc = self.map.copy()
- lookup, changed, added, unknown = [], [], [], []
-
- # compare all files by default
+ # walk all files by default
if not files: files = [self.root]
-
- # recursive generator of all files listed
- def walk(files):
+ def traverse():
for f in util.unique(files):
f = os.path.join(self.root, f)
if os.path.isdir(f):
for dir, subdirs, fl in os.walk(f):
d = dir[len(self.root) + 1:]
+ if d == '.hg':
+ subdirs[:] = []
+ continue
for sd in subdirs:
- if ignore(os.path.join(d, sd +'/')):
+ ds = os.path.join(d, sd +'/')
+ if self.ignore(ds) or not match(ds):
subdirs.remove(sd)
for fn in fl:
fn = util.pconvert(os.path.join(d, fn))
- yield fn
+ yield 'f', fn
else:
- yield f[len(self.root) + 1:]
+ yield 'f', f[len(self.root) + 1:]
for k in dc.keys():
- yield k
+ yield 'm', k
+
+ # yield only files that match: all in dirstate, others only if
+ # not in .hgignore
- for fn in util.unique(walk(files)):
+ for src, fn in util.unique(traverse()):
+ if fn in dc:
+ del dc[fn]
+ elif self.ignore(fn):
+ continue
+ if match(fn):
+ yield src, fn
+
+ def changes(self, files = None, match = util.always):
+ self.read()
+ dc = self.map.copy()
+ lookup, changed, added, unknown = [], [], [], []
+
+ for src, fn in self.walk(files, match):
try: s = os.stat(os.path.join(self.root, fn))
except: continue
@@ -429,9 +479,9 @@
elif c[1] != s.st_mode or c[3] != s.st_mtime:
lookup.append(fn)
else:
- if not ignore(fn): unknown.append(fn)
+ if match(fn): unknown.append(fn)
- return (lookup, changed, added, dc.keys(), unknown)
+ return (lookup, changed, added, filter(match, dc.keys()), unknown)
# used to avoid circular references so destructors work
def opener(base):
@@ -493,7 +543,6 @@
self.wopener = opener(self.root)
self.manifest = manifest(self.opener)
self.changelog = changelog(self.opener)
- self.ignorefunc = None
self.tagscache = None
self.nodetagscache = None
@@ -503,29 +552,6 @@
self.ui.readconfig(self.opener("hgrc"))
except IOError: pass
- def ignore(self, f):
- if not self.ignorefunc:
- bigpat = ["^.hg/$"]
- try:
- l = file(self.wjoin(".hgignore"))
- for pat in l:
- if pat != "\n":
- p = util.pconvert(pat[:-1])
- try:
- r = re.compile(p)
- except:
- self.ui.warn("ignoring invalid ignore"
- + " regular expression '%s'\n" % p)
- else:
- bigpat.append(util.pconvert(pat[:-1]))
- except IOError: pass
-
- s = "(?:%s)" % (")|(?:".join(bigpat))
- r = re.compile(s)
- self.ignorefunc = r.search
-
- return self.ignorefunc(f)
-
def hook(self, name, **args):
s = self.ui.config("hooks", name)
if s:
@@ -738,7 +764,7 @@
else:
self.ui.warn("%s not tracked!\n" % f)
else:
- (c, a, d, u) = self.changes(None, None)
+ (c, a, d, u) = self.changes()
commit = c + a
remove = d
@@ -815,7 +841,16 @@
if not self.hook("commit", node=hex(n)):
return 1
- def changes(self, node1, node2, files=None):
+ def walk(self, node = None, files = [], match = util.always):
+ if node:
+ for fn in self.manifest.read(self.changelog.read(node)[0]):
+ yield 'm', fn
+ else:
+ for src, fn in self.dirstate.walk(files, match):
+ yield src, fn
+
+ def changes(self, node1 = None, node2 = None, files = [],
+ match = util.always):
mf2, u = None, []
def fcmp(fn, mf):
@@ -823,16 +858,23 @@
t2 = self.file(fn).revision(mf[fn])
return cmp(t1, t2)
+ def mfmatches(node):
+ mf = dict(self.manifest.read(node))
+ for fn in mf.keys():
+ if not match(fn):
+ del mf[fn]
+ return mf
+
# are we comparing the working directory?
if not node2:
- l, c, a, d, u = self.dirstate.changes(files, self.ignore)
+ l, c, a, d, u = self.dirstate.changes(files, match)
# are we comparing working dir against its parent?
if not node1:
if l:
# do a full compare of any files that might have changed
change = self.changelog.read(self.dirstate.parents()[0])
- mf2 = self.manifest.read(change[0])
+ mf2 = mfmatches(change[0])
for f in l:
if fcmp(f, mf2):
c.append(f)
@@ -847,20 +889,20 @@
if not node2:
if not mf2:
change = self.changelog.read(self.dirstate.parents()[0])
- mf2 = self.manifest.read(change[0]).copy()
+ mf2 = mfmatches(change[0])
for f in a + c + l:
mf2[f] = ""
for f in d:
if f in mf2: del mf2[f]
else:
change = self.changelog.read(node2)
- mf2 = self.manifest.read(change[0])
+ mf2 = mfmatches(change[0])
# flush lists from dirstate before comparing manifests
c, a = [], []
change = self.changelog.read(node1)
- mf1 = self.manifest.read(change[0]).copy()
+ mf1 = mfmatches(change[0])
for fn in mf2:
if mf1.has_key(fn):
@@ -885,7 +927,7 @@
self.ui.warn("%s does not exist!\n" % f)
elif not os.path.isfile(p):
self.ui.warn("%s not added: mercurial only supports files currently\n" % f)
- elif self.dirstate.state(f) == 'n':
+ elif self.dirstate.state(f) in 'an':
self.ui.warn("%s already tracked!\n" % f)
else:
self.dirstate.update([f], "a")
@@ -1268,7 +1310,7 @@
ma = self.manifest.read(man)
mfa = self.manifest.readflags(man)
- (c, a, d, u) = self.changes(None, None)
+ (c, a, d, u) = self.changes()
# is this a jump, or a merge? i.e. is there a linear path
# from p1 to p2?