##// END OF EJS Templates
scmutil: avoid using basestring and add explicit handling of unicodes...
Augie Fackler -
r36679:b76248e5 default
parent child Browse files
Show More
@@ -1,1422 +1,1425 b''
1 # scmutil.py - Mercurial core utility functions
1 # scmutil.py - Mercurial core utility functions
2 #
2 #
3 # Copyright Matt Mackall <mpm@selenic.com>
3 # Copyright Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import glob
11 import glob
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import re
14 import re
15 import socket
15 import socket
16 import subprocess
16 import subprocess
17 import weakref
17 import weakref
18
18
19 from .i18n import _
19 from .i18n import _
20 from .node import (
20 from .node import (
21 hex,
21 hex,
22 nullid,
22 nullid,
23 short,
23 short,
24 wdirid,
24 wdirid,
25 wdirrev,
25 wdirrev,
26 )
26 )
27
27
28 from . import (
28 from . import (
29 encoding,
29 encoding,
30 error,
30 error,
31 match as matchmod,
31 match as matchmod,
32 obsolete,
32 obsolete,
33 obsutil,
33 obsutil,
34 pathutil,
34 pathutil,
35 phases,
35 phases,
36 pycompat,
36 pycompat,
37 revsetlang,
37 revsetlang,
38 similar,
38 similar,
39 url,
39 url,
40 util,
40 util,
41 vfs,
41 vfs,
42 )
42 )
43
43
44 if pycompat.iswindows:
44 if pycompat.iswindows:
45 from . import scmwindows as scmplatform
45 from . import scmwindows as scmplatform
46 else:
46 else:
47 from . import scmposix as scmplatform
47 from . import scmposix as scmplatform
48
48
49 termsize = scmplatform.termsize
49 termsize = scmplatform.termsize
50
50
51 class status(tuple):
51 class status(tuple):
52 '''Named tuple with a list of files per status. The 'deleted', 'unknown'
52 '''Named tuple with a list of files per status. The 'deleted', 'unknown'
53 and 'ignored' properties are only relevant to the working copy.
53 and 'ignored' properties are only relevant to the working copy.
54 '''
54 '''
55
55
56 __slots__ = ()
56 __slots__ = ()
57
57
58 def __new__(cls, modified, added, removed, deleted, unknown, ignored,
58 def __new__(cls, modified, added, removed, deleted, unknown, ignored,
59 clean):
59 clean):
60 return tuple.__new__(cls, (modified, added, removed, deleted, unknown,
60 return tuple.__new__(cls, (modified, added, removed, deleted, unknown,
61 ignored, clean))
61 ignored, clean))
62
62
63 @property
63 @property
64 def modified(self):
64 def modified(self):
65 '''files that have been modified'''
65 '''files that have been modified'''
66 return self[0]
66 return self[0]
67
67
68 @property
68 @property
69 def added(self):
69 def added(self):
70 '''files that have been added'''
70 '''files that have been added'''
71 return self[1]
71 return self[1]
72
72
73 @property
73 @property
74 def removed(self):
74 def removed(self):
75 '''files that have been removed'''
75 '''files that have been removed'''
76 return self[2]
76 return self[2]
77
77
78 @property
78 @property
79 def deleted(self):
79 def deleted(self):
80 '''files that are in the dirstate, but have been deleted from the
80 '''files that are in the dirstate, but have been deleted from the
81 working copy (aka "missing")
81 working copy (aka "missing")
82 '''
82 '''
83 return self[3]
83 return self[3]
84
84
85 @property
85 @property
86 def unknown(self):
86 def unknown(self):
87 '''files not in the dirstate that are not ignored'''
87 '''files not in the dirstate that are not ignored'''
88 return self[4]
88 return self[4]
89
89
90 @property
90 @property
91 def ignored(self):
91 def ignored(self):
92 '''files not in the dirstate that are ignored (by _dirignore())'''
92 '''files not in the dirstate that are ignored (by _dirignore())'''
93 return self[5]
93 return self[5]
94
94
95 @property
95 @property
96 def clean(self):
96 def clean(self):
97 '''files that have not been modified'''
97 '''files that have not been modified'''
98 return self[6]
98 return self[6]
99
99
100 def __repr__(self, *args, **kwargs):
100 def __repr__(self, *args, **kwargs):
101 return (('<status modified=%r, added=%r, removed=%r, deleted=%r, '
101 return (('<status modified=%r, added=%r, removed=%r, deleted=%r, '
102 'unknown=%r, ignored=%r, clean=%r>') % self)
102 'unknown=%r, ignored=%r, clean=%r>') % self)
103
103
104 def itersubrepos(ctx1, ctx2):
104 def itersubrepos(ctx1, ctx2):
105 """find subrepos in ctx1 or ctx2"""
105 """find subrepos in ctx1 or ctx2"""
106 # Create a (subpath, ctx) mapping where we prefer subpaths from
106 # Create a (subpath, ctx) mapping where we prefer subpaths from
107 # ctx1. The subpaths from ctx2 are important when the .hgsub file
107 # ctx1. The subpaths from ctx2 are important when the .hgsub file
108 # has been modified (in ctx2) but not yet committed (in ctx1).
108 # has been modified (in ctx2) but not yet committed (in ctx1).
109 subpaths = dict.fromkeys(ctx2.substate, ctx2)
109 subpaths = dict.fromkeys(ctx2.substate, ctx2)
110 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
110 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
111
111
112 missing = set()
112 missing = set()
113
113
114 for subpath in ctx2.substate:
114 for subpath in ctx2.substate:
115 if subpath not in ctx1.substate:
115 if subpath not in ctx1.substate:
116 del subpaths[subpath]
116 del subpaths[subpath]
117 missing.add(subpath)
117 missing.add(subpath)
118
118
119 for subpath, ctx in sorted(subpaths.iteritems()):
119 for subpath, ctx in sorted(subpaths.iteritems()):
120 yield subpath, ctx.sub(subpath)
120 yield subpath, ctx.sub(subpath)
121
121
122 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
122 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
123 # status and diff will have an accurate result when it does
123 # status and diff will have an accurate result when it does
124 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
124 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
125 # against itself.
125 # against itself.
126 for subpath in missing:
126 for subpath in missing:
127 yield subpath, ctx2.nullsub(subpath, ctx1)
127 yield subpath, ctx2.nullsub(subpath, ctx1)
128
128
129 def nochangesfound(ui, repo, excluded=None):
129 def nochangesfound(ui, repo, excluded=None):
130 '''Report no changes for push/pull, excluded is None or a list of
130 '''Report no changes for push/pull, excluded is None or a list of
131 nodes excluded from the push/pull.
131 nodes excluded from the push/pull.
132 '''
132 '''
133 secretlist = []
133 secretlist = []
134 if excluded:
134 if excluded:
135 for n in excluded:
135 for n in excluded:
136 ctx = repo[n]
136 ctx = repo[n]
137 if ctx.phase() >= phases.secret and not ctx.extinct():
137 if ctx.phase() >= phases.secret and not ctx.extinct():
138 secretlist.append(n)
138 secretlist.append(n)
139
139
140 if secretlist:
140 if secretlist:
141 ui.status(_("no changes found (ignored %d secret changesets)\n")
141 ui.status(_("no changes found (ignored %d secret changesets)\n")
142 % len(secretlist))
142 % len(secretlist))
143 else:
143 else:
144 ui.status(_("no changes found\n"))
144 ui.status(_("no changes found\n"))
145
145
146 def callcatch(ui, func):
146 def callcatch(ui, func):
147 """call func() with global exception handling
147 """call func() with global exception handling
148
148
149 return func() if no exception happens. otherwise do some error handling
149 return func() if no exception happens. otherwise do some error handling
150 and return an exit code accordingly. does not handle all exceptions.
150 and return an exit code accordingly. does not handle all exceptions.
151 """
151 """
152 try:
152 try:
153 try:
153 try:
154 return func()
154 return func()
155 except: # re-raises
155 except: # re-raises
156 ui.traceback()
156 ui.traceback()
157 raise
157 raise
158 # Global exception handling, alphabetically
158 # Global exception handling, alphabetically
159 # Mercurial-specific first, followed by built-in and library exceptions
159 # Mercurial-specific first, followed by built-in and library exceptions
160 except error.LockHeld as inst:
160 except error.LockHeld as inst:
161 if inst.errno == errno.ETIMEDOUT:
161 if inst.errno == errno.ETIMEDOUT:
162 reason = _('timed out waiting for lock held by %r') % inst.locker
162 reason = _('timed out waiting for lock held by %r') % inst.locker
163 else:
163 else:
164 reason = _('lock held by %r') % inst.locker
164 reason = _('lock held by %r') % inst.locker
165 ui.warn(_("abort: %s: %s\n")
165 ui.warn(_("abort: %s: %s\n")
166 % (inst.desc or util.forcebytestr(inst.filename), reason))
166 % (inst.desc or util.forcebytestr(inst.filename), reason))
167 if not inst.locker:
167 if not inst.locker:
168 ui.warn(_("(lock might be very busy)\n"))
168 ui.warn(_("(lock might be very busy)\n"))
169 except error.LockUnavailable as inst:
169 except error.LockUnavailable as inst:
170 ui.warn(_("abort: could not lock %s: %s\n") %
170 ui.warn(_("abort: could not lock %s: %s\n") %
171 (inst.desc or util.forcebytestr(inst.filename),
171 (inst.desc or util.forcebytestr(inst.filename),
172 encoding.strtolocal(inst.strerror)))
172 encoding.strtolocal(inst.strerror)))
173 except error.OutOfBandError as inst:
173 except error.OutOfBandError as inst:
174 if inst.args:
174 if inst.args:
175 msg = _("abort: remote error:\n")
175 msg = _("abort: remote error:\n")
176 else:
176 else:
177 msg = _("abort: remote error\n")
177 msg = _("abort: remote error\n")
178 ui.warn(msg)
178 ui.warn(msg)
179 if inst.args:
179 if inst.args:
180 ui.warn(''.join(inst.args))
180 ui.warn(''.join(inst.args))
181 if inst.hint:
181 if inst.hint:
182 ui.warn('(%s)\n' % inst.hint)
182 ui.warn('(%s)\n' % inst.hint)
183 except error.RepoError as inst:
183 except error.RepoError as inst:
184 ui.warn(_("abort: %s!\n") % inst)
184 ui.warn(_("abort: %s!\n") % inst)
185 if inst.hint:
185 if inst.hint:
186 ui.warn(_("(%s)\n") % inst.hint)
186 ui.warn(_("(%s)\n") % inst.hint)
187 except error.ResponseError as inst:
187 except error.ResponseError as inst:
188 ui.warn(_("abort: %s") % inst.args[0])
188 ui.warn(_("abort: %s") % inst.args[0])
189 if not isinstance(inst.args[1], basestring):
189 msg = inst.args[1]
190 if isinstance(msg, type(u'')):
191 msg = pycompat.sysbytes(msg)
192 elif not isinstance(inst.args[1], bytes):
190 ui.warn(" %r\n" % (inst.args[1],))
193 ui.warn(" %r\n" % (inst.args[1],))
191 elif not inst.args[1]:
194 elif not inst.args[1]:
192 ui.warn(_(" empty string\n"))
195 ui.warn(_(" empty string\n"))
193 else:
196 else:
194 ui.warn("\n%r\n" % util.ellipsis(inst.args[1]))
197 ui.warn("\n%r\n" % util.ellipsis(inst.args[1]))
195 except error.CensoredNodeError as inst:
198 except error.CensoredNodeError as inst:
196 ui.warn(_("abort: file censored %s!\n") % inst)
199 ui.warn(_("abort: file censored %s!\n") % inst)
197 except error.RevlogError as inst:
200 except error.RevlogError as inst:
198 ui.warn(_("abort: %s!\n") % inst)
201 ui.warn(_("abort: %s!\n") % inst)
199 except error.InterventionRequired as inst:
202 except error.InterventionRequired as inst:
200 ui.warn("%s\n" % inst)
203 ui.warn("%s\n" % inst)
201 if inst.hint:
204 if inst.hint:
202 ui.warn(_("(%s)\n") % inst.hint)
205 ui.warn(_("(%s)\n") % inst.hint)
203 return 1
206 return 1
204 except error.WdirUnsupported:
207 except error.WdirUnsupported:
205 ui.warn(_("abort: working directory revision cannot be specified\n"))
208 ui.warn(_("abort: working directory revision cannot be specified\n"))
206 except error.Abort as inst:
209 except error.Abort as inst:
207 ui.warn(_("abort: %s\n") % inst)
210 ui.warn(_("abort: %s\n") % inst)
208 if inst.hint:
211 if inst.hint:
209 ui.warn(_("(%s)\n") % inst.hint)
212 ui.warn(_("(%s)\n") % inst.hint)
210 except ImportError as inst:
213 except ImportError as inst:
211 ui.warn(_("abort: %s!\n") % util.forcebytestr(inst))
214 ui.warn(_("abort: %s!\n") % util.forcebytestr(inst))
212 m = util.forcebytestr(inst).split()[-1]
215 m = util.forcebytestr(inst).split()[-1]
213 if m in "mpatch bdiff".split():
216 if m in "mpatch bdiff".split():
214 ui.warn(_("(did you forget to compile extensions?)\n"))
217 ui.warn(_("(did you forget to compile extensions?)\n"))
215 elif m in "zlib".split():
218 elif m in "zlib".split():
216 ui.warn(_("(is your Python install correct?)\n"))
219 ui.warn(_("(is your Python install correct?)\n"))
217 except IOError as inst:
220 except IOError as inst:
218 if util.safehasattr(inst, "code"):
221 if util.safehasattr(inst, "code"):
219 ui.warn(_("abort: %s\n") % util.forcebytestr(inst))
222 ui.warn(_("abort: %s\n") % util.forcebytestr(inst))
220 elif util.safehasattr(inst, "reason"):
223 elif util.safehasattr(inst, "reason"):
221 try: # usually it is in the form (errno, strerror)
224 try: # usually it is in the form (errno, strerror)
222 reason = inst.reason.args[1]
225 reason = inst.reason.args[1]
223 except (AttributeError, IndexError):
226 except (AttributeError, IndexError):
224 # it might be anything, for example a string
227 # it might be anything, for example a string
225 reason = inst.reason
228 reason = inst.reason
226 if isinstance(reason, unicode):
229 if isinstance(reason, unicode):
227 # SSLError of Python 2.7.9 contains a unicode
230 # SSLError of Python 2.7.9 contains a unicode
228 reason = encoding.unitolocal(reason)
231 reason = encoding.unitolocal(reason)
229 ui.warn(_("abort: error: %s\n") % reason)
232 ui.warn(_("abort: error: %s\n") % reason)
230 elif (util.safehasattr(inst, "args")
233 elif (util.safehasattr(inst, "args")
231 and inst.args and inst.args[0] == errno.EPIPE):
234 and inst.args and inst.args[0] == errno.EPIPE):
232 pass
235 pass
233 elif getattr(inst, "strerror", None):
236 elif getattr(inst, "strerror", None):
234 if getattr(inst, "filename", None):
237 if getattr(inst, "filename", None):
235 ui.warn(_("abort: %s: %s\n") % (
238 ui.warn(_("abort: %s: %s\n") % (
236 encoding.strtolocal(inst.strerror),
239 encoding.strtolocal(inst.strerror),
237 util.forcebytestr(inst.filename)))
240 util.forcebytestr(inst.filename)))
238 else:
241 else:
239 ui.warn(_("abort: %s\n") % encoding.strtolocal(inst.strerror))
242 ui.warn(_("abort: %s\n") % encoding.strtolocal(inst.strerror))
240 else:
243 else:
241 raise
244 raise
242 except OSError as inst:
245 except OSError as inst:
243 if getattr(inst, "filename", None) is not None:
246 if getattr(inst, "filename", None) is not None:
244 ui.warn(_("abort: %s: '%s'\n") % (
247 ui.warn(_("abort: %s: '%s'\n") % (
245 encoding.strtolocal(inst.strerror),
248 encoding.strtolocal(inst.strerror),
246 util.forcebytestr(inst.filename)))
249 util.forcebytestr(inst.filename)))
247 else:
250 else:
248 ui.warn(_("abort: %s\n") % encoding.strtolocal(inst.strerror))
251 ui.warn(_("abort: %s\n") % encoding.strtolocal(inst.strerror))
249 except MemoryError:
252 except MemoryError:
250 ui.warn(_("abort: out of memory\n"))
253 ui.warn(_("abort: out of memory\n"))
251 except SystemExit as inst:
254 except SystemExit as inst:
252 # Commands shouldn't sys.exit directly, but give a return code.
255 # Commands shouldn't sys.exit directly, but give a return code.
253 # Just in case catch this and and pass exit code to caller.
256 # Just in case catch this and and pass exit code to caller.
254 return inst.code
257 return inst.code
255 except socket.error as inst:
258 except socket.error as inst:
256 ui.warn(_("abort: %s\n") % util.forcebytestr(inst.args[-1]))
259 ui.warn(_("abort: %s\n") % util.forcebytestr(inst.args[-1]))
257
260
258 return -1
261 return -1
259
262
260 def checknewlabel(repo, lbl, kind):
263 def checknewlabel(repo, lbl, kind):
261 # Do not use the "kind" parameter in ui output.
264 # Do not use the "kind" parameter in ui output.
262 # It makes strings difficult to translate.
265 # It makes strings difficult to translate.
263 if lbl in ['tip', '.', 'null']:
266 if lbl in ['tip', '.', 'null']:
264 raise error.Abort(_("the name '%s' is reserved") % lbl)
267 raise error.Abort(_("the name '%s' is reserved") % lbl)
265 for c in (':', '\0', '\n', '\r'):
268 for c in (':', '\0', '\n', '\r'):
266 if c in lbl:
269 if c in lbl:
267 raise error.Abort(
270 raise error.Abort(
268 _("%r cannot be used in a name") % pycompat.bytestr(c))
271 _("%r cannot be used in a name") % pycompat.bytestr(c))
269 try:
272 try:
270 int(lbl)
273 int(lbl)
271 raise error.Abort(_("cannot use an integer as a name"))
274 raise error.Abort(_("cannot use an integer as a name"))
272 except ValueError:
275 except ValueError:
273 pass
276 pass
274 if lbl.strip() != lbl:
277 if lbl.strip() != lbl:
275 raise error.Abort(_("leading or trailing whitespace in name %r") % lbl)
278 raise error.Abort(_("leading or trailing whitespace in name %r") % lbl)
276
279
277 def checkfilename(f):
280 def checkfilename(f):
278 '''Check that the filename f is an acceptable filename for a tracked file'''
281 '''Check that the filename f is an acceptable filename for a tracked file'''
279 if '\r' in f or '\n' in f:
282 if '\r' in f or '\n' in f:
280 raise error.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
283 raise error.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
281
284
282 def checkportable(ui, f):
285 def checkportable(ui, f):
283 '''Check if filename f is portable and warn or abort depending on config'''
286 '''Check if filename f is portable and warn or abort depending on config'''
284 checkfilename(f)
287 checkfilename(f)
285 abort, warn = checkportabilityalert(ui)
288 abort, warn = checkportabilityalert(ui)
286 if abort or warn:
289 if abort or warn:
287 msg = util.checkwinfilename(f)
290 msg = util.checkwinfilename(f)
288 if msg:
291 if msg:
289 msg = "%s: %s" % (msg, util.shellquote(f))
292 msg = "%s: %s" % (msg, util.shellquote(f))
290 if abort:
293 if abort:
291 raise error.Abort(msg)
294 raise error.Abort(msg)
292 ui.warn(_("warning: %s\n") % msg)
295 ui.warn(_("warning: %s\n") % msg)
293
296
294 def checkportabilityalert(ui):
297 def checkportabilityalert(ui):
295 '''check if the user's config requests nothing, a warning, or abort for
298 '''check if the user's config requests nothing, a warning, or abort for
296 non-portable filenames'''
299 non-portable filenames'''
297 val = ui.config('ui', 'portablefilenames')
300 val = ui.config('ui', 'portablefilenames')
298 lval = val.lower()
301 lval = val.lower()
299 bval = util.parsebool(val)
302 bval = util.parsebool(val)
300 abort = pycompat.iswindows or lval == 'abort'
303 abort = pycompat.iswindows or lval == 'abort'
301 warn = bval or lval == 'warn'
304 warn = bval or lval == 'warn'
302 if bval is None and not (warn or abort or lval == 'ignore'):
305 if bval is None and not (warn or abort or lval == 'ignore'):
303 raise error.ConfigError(
306 raise error.ConfigError(
304 _("ui.portablefilenames value is invalid ('%s')") % val)
307 _("ui.portablefilenames value is invalid ('%s')") % val)
305 return abort, warn
308 return abort, warn
306
309
307 class casecollisionauditor(object):
310 class casecollisionauditor(object):
308 def __init__(self, ui, abort, dirstate):
311 def __init__(self, ui, abort, dirstate):
309 self._ui = ui
312 self._ui = ui
310 self._abort = abort
313 self._abort = abort
311 allfiles = '\0'.join(dirstate._map)
314 allfiles = '\0'.join(dirstate._map)
312 self._loweredfiles = set(encoding.lower(allfiles).split('\0'))
315 self._loweredfiles = set(encoding.lower(allfiles).split('\0'))
313 self._dirstate = dirstate
316 self._dirstate = dirstate
314 # The purpose of _newfiles is so that we don't complain about
317 # The purpose of _newfiles is so that we don't complain about
315 # case collisions if someone were to call this object with the
318 # case collisions if someone were to call this object with the
316 # same filename twice.
319 # same filename twice.
317 self._newfiles = set()
320 self._newfiles = set()
318
321
319 def __call__(self, f):
322 def __call__(self, f):
320 if f in self._newfiles:
323 if f in self._newfiles:
321 return
324 return
322 fl = encoding.lower(f)
325 fl = encoding.lower(f)
323 if fl in self._loweredfiles and f not in self._dirstate:
326 if fl in self._loweredfiles and f not in self._dirstate:
324 msg = _('possible case-folding collision for %s') % f
327 msg = _('possible case-folding collision for %s') % f
325 if self._abort:
328 if self._abort:
326 raise error.Abort(msg)
329 raise error.Abort(msg)
327 self._ui.warn(_("warning: %s\n") % msg)
330 self._ui.warn(_("warning: %s\n") % msg)
328 self._loweredfiles.add(fl)
331 self._loweredfiles.add(fl)
329 self._newfiles.add(f)
332 self._newfiles.add(f)
330
333
331 def filteredhash(repo, maxrev):
334 def filteredhash(repo, maxrev):
332 """build hash of filtered revisions in the current repoview.
335 """build hash of filtered revisions in the current repoview.
333
336
334 Multiple caches perform up-to-date validation by checking that the
337 Multiple caches perform up-to-date validation by checking that the
335 tiprev and tipnode stored in the cache file match the current repository.
338 tiprev and tipnode stored in the cache file match the current repository.
336 However, this is not sufficient for validating repoviews because the set
339 However, this is not sufficient for validating repoviews because the set
337 of revisions in the view may change without the repository tiprev and
340 of revisions in the view may change without the repository tiprev and
338 tipnode changing.
341 tipnode changing.
339
342
340 This function hashes all the revs filtered from the view and returns
343 This function hashes all the revs filtered from the view and returns
341 that SHA-1 digest.
344 that SHA-1 digest.
342 """
345 """
343 cl = repo.changelog
346 cl = repo.changelog
344 if not cl.filteredrevs:
347 if not cl.filteredrevs:
345 return None
348 return None
346 key = None
349 key = None
347 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
350 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
348 if revs:
351 if revs:
349 s = hashlib.sha1()
352 s = hashlib.sha1()
350 for rev in revs:
353 for rev in revs:
351 s.update('%d;' % rev)
354 s.update('%d;' % rev)
352 key = s.digest()
355 key = s.digest()
353 return key
356 return key
354
357
355 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
358 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
356 '''yield every hg repository under path, always recursively.
359 '''yield every hg repository under path, always recursively.
357 The recurse flag will only control recursion into repo working dirs'''
360 The recurse flag will only control recursion into repo working dirs'''
358 def errhandler(err):
361 def errhandler(err):
359 if err.filename == path:
362 if err.filename == path:
360 raise err
363 raise err
361 samestat = getattr(os.path, 'samestat', None)
364 samestat = getattr(os.path, 'samestat', None)
362 if followsym and samestat is not None:
365 if followsym and samestat is not None:
363 def adddir(dirlst, dirname):
366 def adddir(dirlst, dirname):
364 dirstat = os.stat(dirname)
367 dirstat = os.stat(dirname)
365 match = any(samestat(dirstat, lstdirstat) for lstdirstat in dirlst)
368 match = any(samestat(dirstat, lstdirstat) for lstdirstat in dirlst)
366 if not match:
369 if not match:
367 dirlst.append(dirstat)
370 dirlst.append(dirstat)
368 return not match
371 return not match
369 else:
372 else:
370 followsym = False
373 followsym = False
371
374
372 if (seen_dirs is None) and followsym:
375 if (seen_dirs is None) and followsym:
373 seen_dirs = []
376 seen_dirs = []
374 adddir(seen_dirs, path)
377 adddir(seen_dirs, path)
375 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
378 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
376 dirs.sort()
379 dirs.sort()
377 if '.hg' in dirs:
380 if '.hg' in dirs:
378 yield root # found a repository
381 yield root # found a repository
379 qroot = os.path.join(root, '.hg', 'patches')
382 qroot = os.path.join(root, '.hg', 'patches')
380 if os.path.isdir(os.path.join(qroot, '.hg')):
383 if os.path.isdir(os.path.join(qroot, '.hg')):
381 yield qroot # we have a patch queue repo here
384 yield qroot # we have a patch queue repo here
382 if recurse:
385 if recurse:
383 # avoid recursing inside the .hg directory
386 # avoid recursing inside the .hg directory
384 dirs.remove('.hg')
387 dirs.remove('.hg')
385 else:
388 else:
386 dirs[:] = [] # don't descend further
389 dirs[:] = [] # don't descend further
387 elif followsym:
390 elif followsym:
388 newdirs = []
391 newdirs = []
389 for d in dirs:
392 for d in dirs:
390 fname = os.path.join(root, d)
393 fname = os.path.join(root, d)
391 if adddir(seen_dirs, fname):
394 if adddir(seen_dirs, fname):
392 if os.path.islink(fname):
395 if os.path.islink(fname):
393 for hgname in walkrepos(fname, True, seen_dirs):
396 for hgname in walkrepos(fname, True, seen_dirs):
394 yield hgname
397 yield hgname
395 else:
398 else:
396 newdirs.append(d)
399 newdirs.append(d)
397 dirs[:] = newdirs
400 dirs[:] = newdirs
398
401
399 def binnode(ctx):
402 def binnode(ctx):
400 """Return binary node id for a given basectx"""
403 """Return binary node id for a given basectx"""
401 node = ctx.node()
404 node = ctx.node()
402 if node is None:
405 if node is None:
403 return wdirid
406 return wdirid
404 return node
407 return node
405
408
406 def intrev(ctx):
409 def intrev(ctx):
407 """Return integer for a given basectx that can be used in comparison or
410 """Return integer for a given basectx that can be used in comparison or
408 arithmetic operation"""
411 arithmetic operation"""
409 rev = ctx.rev()
412 rev = ctx.rev()
410 if rev is None:
413 if rev is None:
411 return wdirrev
414 return wdirrev
412 return rev
415 return rev
413
416
414 def formatchangeid(ctx):
417 def formatchangeid(ctx):
415 """Format changectx as '{rev}:{node|formatnode}', which is the default
418 """Format changectx as '{rev}:{node|formatnode}', which is the default
416 template provided by logcmdutil.changesettemplater"""
419 template provided by logcmdutil.changesettemplater"""
417 repo = ctx.repo()
420 repo = ctx.repo()
418 return formatrevnode(repo.ui, intrev(ctx), binnode(ctx))
421 return formatrevnode(repo.ui, intrev(ctx), binnode(ctx))
419
422
420 def formatrevnode(ui, rev, node):
423 def formatrevnode(ui, rev, node):
421 """Format given revision and node depending on the current verbosity"""
424 """Format given revision and node depending on the current verbosity"""
422 if ui.debugflag:
425 if ui.debugflag:
423 hexfunc = hex
426 hexfunc = hex
424 else:
427 else:
425 hexfunc = short
428 hexfunc = short
426 return '%d:%s' % (rev, hexfunc(node))
429 return '%d:%s' % (rev, hexfunc(node))
427
430
428 def revsingle(repo, revspec, default='.', localalias=None):
431 def revsingle(repo, revspec, default='.', localalias=None):
429 if not revspec and revspec != 0:
432 if not revspec and revspec != 0:
430 return repo[default]
433 return repo[default]
431
434
432 l = revrange(repo, [revspec], localalias=localalias)
435 l = revrange(repo, [revspec], localalias=localalias)
433 if not l:
436 if not l:
434 raise error.Abort(_('empty revision set'))
437 raise error.Abort(_('empty revision set'))
435 return repo[l.last()]
438 return repo[l.last()]
436
439
437 def _pairspec(revspec):
440 def _pairspec(revspec):
438 tree = revsetlang.parse(revspec)
441 tree = revsetlang.parse(revspec)
439 return tree and tree[0] in ('range', 'rangepre', 'rangepost', 'rangeall')
442 return tree and tree[0] in ('range', 'rangepre', 'rangepost', 'rangeall')
440
443
441 def revpair(repo, revs):
444 def revpair(repo, revs):
442 if not revs:
445 if not revs:
443 return repo.dirstate.p1(), None
446 return repo.dirstate.p1(), None
444
447
445 l = revrange(repo, revs)
448 l = revrange(repo, revs)
446
449
447 if not l:
450 if not l:
448 first = second = None
451 first = second = None
449 elif l.isascending():
452 elif l.isascending():
450 first = l.min()
453 first = l.min()
451 second = l.max()
454 second = l.max()
452 elif l.isdescending():
455 elif l.isdescending():
453 first = l.max()
456 first = l.max()
454 second = l.min()
457 second = l.min()
455 else:
458 else:
456 first = l.first()
459 first = l.first()
457 second = l.last()
460 second = l.last()
458
461
459 if first is None:
462 if first is None:
460 raise error.Abort(_('empty revision range'))
463 raise error.Abort(_('empty revision range'))
461 if (first == second and len(revs) >= 2
464 if (first == second and len(revs) >= 2
462 and not all(revrange(repo, [r]) for r in revs)):
465 and not all(revrange(repo, [r]) for r in revs)):
463 raise error.Abort(_('empty revision on one side of range'))
466 raise error.Abort(_('empty revision on one side of range'))
464
467
465 # if top-level is range expression, the result must always be a pair
468 # if top-level is range expression, the result must always be a pair
466 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
469 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
467 return repo.lookup(first), None
470 return repo.lookup(first), None
468
471
469 return repo.lookup(first), repo.lookup(second)
472 return repo.lookup(first), repo.lookup(second)
470
473
471 def revrange(repo, specs, localalias=None):
474 def revrange(repo, specs, localalias=None):
472 """Execute 1 to many revsets and return the union.
475 """Execute 1 to many revsets and return the union.
473
476
474 This is the preferred mechanism for executing revsets using user-specified
477 This is the preferred mechanism for executing revsets using user-specified
475 config options, such as revset aliases.
478 config options, such as revset aliases.
476
479
477 The revsets specified by ``specs`` will be executed via a chained ``OR``
480 The revsets specified by ``specs`` will be executed via a chained ``OR``
478 expression. If ``specs`` is empty, an empty result is returned.
481 expression. If ``specs`` is empty, an empty result is returned.
479
482
480 ``specs`` can contain integers, in which case they are assumed to be
483 ``specs`` can contain integers, in which case they are assumed to be
481 revision numbers.
484 revision numbers.
482
485
483 It is assumed the revsets are already formatted. If you have arguments
486 It is assumed the revsets are already formatted. If you have arguments
484 that need to be expanded in the revset, call ``revsetlang.formatspec()``
487 that need to be expanded in the revset, call ``revsetlang.formatspec()``
485 and pass the result as an element of ``specs``.
488 and pass the result as an element of ``specs``.
486
489
487 Specifying a single revset is allowed.
490 Specifying a single revset is allowed.
488
491
489 Returns a ``revset.abstractsmartset`` which is a list-like interface over
492 Returns a ``revset.abstractsmartset`` which is a list-like interface over
490 integer revisions.
493 integer revisions.
491 """
494 """
492 allspecs = []
495 allspecs = []
493 for spec in specs:
496 for spec in specs:
494 if isinstance(spec, int):
497 if isinstance(spec, int):
495 spec = revsetlang.formatspec('rev(%d)', spec)
498 spec = revsetlang.formatspec('rev(%d)', spec)
496 allspecs.append(spec)
499 allspecs.append(spec)
497 return repo.anyrevs(allspecs, user=True, localalias=localalias)
500 return repo.anyrevs(allspecs, user=True, localalias=localalias)
498
501
499 def meaningfulparents(repo, ctx):
502 def meaningfulparents(repo, ctx):
500 """Return list of meaningful (or all if debug) parentrevs for rev.
503 """Return list of meaningful (or all if debug) parentrevs for rev.
501
504
502 For merges (two non-nullrev revisions) both parents are meaningful.
505 For merges (two non-nullrev revisions) both parents are meaningful.
503 Otherwise the first parent revision is considered meaningful if it
506 Otherwise the first parent revision is considered meaningful if it
504 is not the preceding revision.
507 is not the preceding revision.
505 """
508 """
506 parents = ctx.parents()
509 parents = ctx.parents()
507 if len(parents) > 1:
510 if len(parents) > 1:
508 return parents
511 return parents
509 if repo.ui.debugflag:
512 if repo.ui.debugflag:
510 return [parents[0], repo['null']]
513 return [parents[0], repo['null']]
511 if parents[0].rev() >= intrev(ctx) - 1:
514 if parents[0].rev() >= intrev(ctx) - 1:
512 return []
515 return []
513 return parents
516 return parents
514
517
515 def expandpats(pats):
518 def expandpats(pats):
516 '''Expand bare globs when running on windows.
519 '''Expand bare globs when running on windows.
517 On posix we assume it already has already been done by sh.'''
520 On posix we assume it already has already been done by sh.'''
518 if not util.expandglobs:
521 if not util.expandglobs:
519 return list(pats)
522 return list(pats)
520 ret = []
523 ret = []
521 for kindpat in pats:
524 for kindpat in pats:
522 kind, pat = matchmod._patsplit(kindpat, None)
525 kind, pat = matchmod._patsplit(kindpat, None)
523 if kind is None:
526 if kind is None:
524 try:
527 try:
525 globbed = glob.glob(pat)
528 globbed = glob.glob(pat)
526 except re.error:
529 except re.error:
527 globbed = [pat]
530 globbed = [pat]
528 if globbed:
531 if globbed:
529 ret.extend(globbed)
532 ret.extend(globbed)
530 continue
533 continue
531 ret.append(kindpat)
534 ret.append(kindpat)
532 return ret
535 return ret
533
536
534 def matchandpats(ctx, pats=(), opts=None, globbed=False, default='relpath',
537 def matchandpats(ctx, pats=(), opts=None, globbed=False, default='relpath',
535 badfn=None):
538 badfn=None):
536 '''Return a matcher and the patterns that were used.
539 '''Return a matcher and the patterns that were used.
537 The matcher will warn about bad matches, unless an alternate badfn callback
540 The matcher will warn about bad matches, unless an alternate badfn callback
538 is provided.'''
541 is provided.'''
539 if pats == ("",):
542 if pats == ("",):
540 pats = []
543 pats = []
541 if opts is None:
544 if opts is None:
542 opts = {}
545 opts = {}
543 if not globbed and default == 'relpath':
546 if not globbed and default == 'relpath':
544 pats = expandpats(pats or [])
547 pats = expandpats(pats or [])
545
548
546 def bad(f, msg):
549 def bad(f, msg):
547 ctx.repo().ui.warn("%s: %s\n" % (m.rel(f), msg))
550 ctx.repo().ui.warn("%s: %s\n" % (m.rel(f), msg))
548
551
549 if badfn is None:
552 if badfn is None:
550 badfn = bad
553 badfn = bad
551
554
552 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
555 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
553 default, listsubrepos=opts.get('subrepos'), badfn=badfn)
556 default, listsubrepos=opts.get('subrepos'), badfn=badfn)
554
557
555 if m.always():
558 if m.always():
556 pats = []
559 pats = []
557 return m, pats
560 return m, pats
558
561
559 def match(ctx, pats=(), opts=None, globbed=False, default='relpath',
562 def match(ctx, pats=(), opts=None, globbed=False, default='relpath',
560 badfn=None):
563 badfn=None):
561 '''Return a matcher that will warn about bad matches.'''
564 '''Return a matcher that will warn about bad matches.'''
562 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
565 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
563
566
564 def matchall(repo):
567 def matchall(repo):
565 '''Return a matcher that will efficiently match everything.'''
568 '''Return a matcher that will efficiently match everything.'''
566 return matchmod.always(repo.root, repo.getcwd())
569 return matchmod.always(repo.root, repo.getcwd())
567
570
568 def matchfiles(repo, files, badfn=None):
571 def matchfiles(repo, files, badfn=None):
569 '''Return a matcher that will efficiently match exactly these files.'''
572 '''Return a matcher that will efficiently match exactly these files.'''
570 return matchmod.exact(repo.root, repo.getcwd(), files, badfn=badfn)
573 return matchmod.exact(repo.root, repo.getcwd(), files, badfn=badfn)
571
574
572 def parsefollowlinespattern(repo, rev, pat, msg):
575 def parsefollowlinespattern(repo, rev, pat, msg):
573 """Return a file name from `pat` pattern suitable for usage in followlines
576 """Return a file name from `pat` pattern suitable for usage in followlines
574 logic.
577 logic.
575 """
578 """
576 if not matchmod.patkind(pat):
579 if not matchmod.patkind(pat):
577 return pathutil.canonpath(repo.root, repo.getcwd(), pat)
580 return pathutil.canonpath(repo.root, repo.getcwd(), pat)
578 else:
581 else:
579 ctx = repo[rev]
582 ctx = repo[rev]
580 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=ctx)
583 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=ctx)
581 files = [f for f in ctx if m(f)]
584 files = [f for f in ctx if m(f)]
582 if len(files) != 1:
585 if len(files) != 1:
583 raise error.ParseError(msg)
586 raise error.ParseError(msg)
584 return files[0]
587 return files[0]
585
588
586 def origpath(ui, repo, filepath):
589 def origpath(ui, repo, filepath):
587 '''customize where .orig files are created
590 '''customize where .orig files are created
588
591
589 Fetch user defined path from config file: [ui] origbackuppath = <path>
592 Fetch user defined path from config file: [ui] origbackuppath = <path>
590 Fall back to default (filepath with .orig suffix) if not specified
593 Fall back to default (filepath with .orig suffix) if not specified
591 '''
594 '''
592 origbackuppath = ui.config('ui', 'origbackuppath')
595 origbackuppath = ui.config('ui', 'origbackuppath')
593 if not origbackuppath:
596 if not origbackuppath:
594 return filepath + ".orig"
597 return filepath + ".orig"
595
598
596 # Convert filepath from an absolute path into a path inside the repo.
599 # Convert filepath from an absolute path into a path inside the repo.
597 filepathfromroot = util.normpath(os.path.relpath(filepath,
600 filepathfromroot = util.normpath(os.path.relpath(filepath,
598 start=repo.root))
601 start=repo.root))
599
602
600 origvfs = vfs.vfs(repo.wjoin(origbackuppath))
603 origvfs = vfs.vfs(repo.wjoin(origbackuppath))
601 origbackupdir = origvfs.dirname(filepathfromroot)
604 origbackupdir = origvfs.dirname(filepathfromroot)
602 if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir):
605 if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir):
603 ui.note(_('creating directory: %s\n') % origvfs.join(origbackupdir))
606 ui.note(_('creating directory: %s\n') % origvfs.join(origbackupdir))
604
607
605 # Remove any files that conflict with the backup file's path
608 # Remove any files that conflict with the backup file's path
606 for f in reversed(list(util.finddirs(filepathfromroot))):
609 for f in reversed(list(util.finddirs(filepathfromroot))):
607 if origvfs.isfileorlink(f):
610 if origvfs.isfileorlink(f):
608 ui.note(_('removing conflicting file: %s\n')
611 ui.note(_('removing conflicting file: %s\n')
609 % origvfs.join(f))
612 % origvfs.join(f))
610 origvfs.unlink(f)
613 origvfs.unlink(f)
611 break
614 break
612
615
613 origvfs.makedirs(origbackupdir)
616 origvfs.makedirs(origbackupdir)
614
617
615 if origvfs.isdir(filepathfromroot) and not origvfs.islink(filepathfromroot):
618 if origvfs.isdir(filepathfromroot) and not origvfs.islink(filepathfromroot):
616 ui.note(_('removing conflicting directory: %s\n')
619 ui.note(_('removing conflicting directory: %s\n')
617 % origvfs.join(filepathfromroot))
620 % origvfs.join(filepathfromroot))
618 origvfs.rmtree(filepathfromroot, forcibly=True)
621 origvfs.rmtree(filepathfromroot, forcibly=True)
619
622
620 return origvfs.join(filepathfromroot)
623 return origvfs.join(filepathfromroot)
621
624
622 class _containsnode(object):
625 class _containsnode(object):
623 """proxy __contains__(node) to container.__contains__ which accepts revs"""
626 """proxy __contains__(node) to container.__contains__ which accepts revs"""
624
627
625 def __init__(self, repo, revcontainer):
628 def __init__(self, repo, revcontainer):
626 self._torev = repo.changelog.rev
629 self._torev = repo.changelog.rev
627 self._revcontains = revcontainer.__contains__
630 self._revcontains = revcontainer.__contains__
628
631
629 def __contains__(self, node):
632 def __contains__(self, node):
630 return self._revcontains(self._torev(node))
633 return self._revcontains(self._torev(node))
631
634
632 def cleanupnodes(repo, replacements, operation, moves=None, metadata=None):
635 def cleanupnodes(repo, replacements, operation, moves=None, metadata=None):
633 """do common cleanups when old nodes are replaced by new nodes
636 """do common cleanups when old nodes are replaced by new nodes
634
637
635 That includes writing obsmarkers or stripping nodes, and moving bookmarks.
638 That includes writing obsmarkers or stripping nodes, and moving bookmarks.
636 (we might also want to move working directory parent in the future)
639 (we might also want to move working directory parent in the future)
637
640
638 By default, bookmark moves are calculated automatically from 'replacements',
641 By default, bookmark moves are calculated automatically from 'replacements',
639 but 'moves' can be used to override that. Also, 'moves' may include
642 but 'moves' can be used to override that. Also, 'moves' may include
640 additional bookmark moves that should not have associated obsmarkers.
643 additional bookmark moves that should not have associated obsmarkers.
641
644
642 replacements is {oldnode: [newnode]} or a iterable of nodes if they do not
645 replacements is {oldnode: [newnode]} or a iterable of nodes if they do not
643 have replacements. operation is a string, like "rebase".
646 have replacements. operation is a string, like "rebase".
644
647
645 metadata is dictionary containing metadata to be stored in obsmarker if
648 metadata is dictionary containing metadata to be stored in obsmarker if
646 obsolescence is enabled.
649 obsolescence is enabled.
647 """
650 """
648 if not replacements and not moves:
651 if not replacements and not moves:
649 return
652 return
650
653
651 # translate mapping's other forms
654 # translate mapping's other forms
652 if not util.safehasattr(replacements, 'items'):
655 if not util.safehasattr(replacements, 'items'):
653 replacements = {n: () for n in replacements}
656 replacements = {n: () for n in replacements}
654
657
655 # Calculate bookmark movements
658 # Calculate bookmark movements
656 if moves is None:
659 if moves is None:
657 moves = {}
660 moves = {}
658 # Unfiltered repo is needed since nodes in replacements might be hidden.
661 # Unfiltered repo is needed since nodes in replacements might be hidden.
659 unfi = repo.unfiltered()
662 unfi = repo.unfiltered()
660 for oldnode, newnodes in replacements.items():
663 for oldnode, newnodes in replacements.items():
661 if oldnode in moves:
664 if oldnode in moves:
662 continue
665 continue
663 if len(newnodes) > 1:
666 if len(newnodes) > 1:
664 # usually a split, take the one with biggest rev number
667 # usually a split, take the one with biggest rev number
665 newnode = next(unfi.set('max(%ln)', newnodes)).node()
668 newnode = next(unfi.set('max(%ln)', newnodes)).node()
666 elif len(newnodes) == 0:
669 elif len(newnodes) == 0:
667 # move bookmark backwards
670 # move bookmark backwards
668 roots = list(unfi.set('max((::%n) - %ln)', oldnode,
671 roots = list(unfi.set('max((::%n) - %ln)', oldnode,
669 list(replacements)))
672 list(replacements)))
670 if roots:
673 if roots:
671 newnode = roots[0].node()
674 newnode = roots[0].node()
672 else:
675 else:
673 newnode = nullid
676 newnode = nullid
674 else:
677 else:
675 newnode = newnodes[0]
678 newnode = newnodes[0]
676 moves[oldnode] = newnode
679 moves[oldnode] = newnode
677
680
678 with repo.transaction('cleanup') as tr:
681 with repo.transaction('cleanup') as tr:
679 # Move bookmarks
682 # Move bookmarks
680 bmarks = repo._bookmarks
683 bmarks = repo._bookmarks
681 bmarkchanges = []
684 bmarkchanges = []
682 allnewnodes = [n for ns in replacements.values() for n in ns]
685 allnewnodes = [n for ns in replacements.values() for n in ns]
683 for oldnode, newnode in moves.items():
686 for oldnode, newnode in moves.items():
684 oldbmarks = repo.nodebookmarks(oldnode)
687 oldbmarks = repo.nodebookmarks(oldnode)
685 if not oldbmarks:
688 if not oldbmarks:
686 continue
689 continue
687 from . import bookmarks # avoid import cycle
690 from . import bookmarks # avoid import cycle
688 repo.ui.debug('moving bookmarks %r from %s to %s\n' %
691 repo.ui.debug('moving bookmarks %r from %s to %s\n' %
689 (oldbmarks, hex(oldnode), hex(newnode)))
692 (oldbmarks, hex(oldnode), hex(newnode)))
690 # Delete divergent bookmarks being parents of related newnodes
693 # Delete divergent bookmarks being parents of related newnodes
691 deleterevs = repo.revs('parents(roots(%ln & (::%n))) - parents(%n)',
694 deleterevs = repo.revs('parents(roots(%ln & (::%n))) - parents(%n)',
692 allnewnodes, newnode, oldnode)
695 allnewnodes, newnode, oldnode)
693 deletenodes = _containsnode(repo, deleterevs)
696 deletenodes = _containsnode(repo, deleterevs)
694 for name in oldbmarks:
697 for name in oldbmarks:
695 bmarkchanges.append((name, newnode))
698 bmarkchanges.append((name, newnode))
696 for b in bookmarks.divergent2delete(repo, deletenodes, name):
699 for b in bookmarks.divergent2delete(repo, deletenodes, name):
697 bmarkchanges.append((b, None))
700 bmarkchanges.append((b, None))
698
701
699 if bmarkchanges:
702 if bmarkchanges:
700 bmarks.applychanges(repo, tr, bmarkchanges)
703 bmarks.applychanges(repo, tr, bmarkchanges)
701
704
702 # Obsolete or strip nodes
705 # Obsolete or strip nodes
703 if obsolete.isenabled(repo, obsolete.createmarkersopt):
706 if obsolete.isenabled(repo, obsolete.createmarkersopt):
704 # If a node is already obsoleted, and we want to obsolete it
707 # If a node is already obsoleted, and we want to obsolete it
705 # without a successor, skip that obssolete request since it's
708 # without a successor, skip that obssolete request since it's
706 # unnecessary. That's the "if s or not isobs(n)" check below.
709 # unnecessary. That's the "if s or not isobs(n)" check below.
707 # Also sort the node in topology order, that might be useful for
710 # Also sort the node in topology order, that might be useful for
708 # some obsstore logic.
711 # some obsstore logic.
709 # NOTE: the filtering and sorting might belong to createmarkers.
712 # NOTE: the filtering and sorting might belong to createmarkers.
710 isobs = unfi.obsstore.successors.__contains__
713 isobs = unfi.obsstore.successors.__contains__
711 torev = unfi.changelog.rev
714 torev = unfi.changelog.rev
712 sortfunc = lambda ns: torev(ns[0])
715 sortfunc = lambda ns: torev(ns[0])
713 rels = [(unfi[n], tuple(unfi[m] for m in s))
716 rels = [(unfi[n], tuple(unfi[m] for m in s))
714 for n, s in sorted(replacements.items(), key=sortfunc)
717 for n, s in sorted(replacements.items(), key=sortfunc)
715 if s or not isobs(n)]
718 if s or not isobs(n)]
716 if rels:
719 if rels:
717 obsolete.createmarkers(repo, rels, operation=operation,
720 obsolete.createmarkers(repo, rels, operation=operation,
718 metadata=metadata)
721 metadata=metadata)
719 else:
722 else:
720 from . import repair # avoid import cycle
723 from . import repair # avoid import cycle
721 tostrip = list(replacements)
724 tostrip = list(replacements)
722 if tostrip:
725 if tostrip:
723 repair.delayedstrip(repo.ui, repo, tostrip, operation)
726 repair.delayedstrip(repo.ui, repo, tostrip, operation)
724
727
725 def addremove(repo, matcher, prefix, opts=None, dry_run=None, similarity=None):
728 def addremove(repo, matcher, prefix, opts=None, dry_run=None, similarity=None):
726 if opts is None:
729 if opts is None:
727 opts = {}
730 opts = {}
728 m = matcher
731 m = matcher
729 if dry_run is None:
732 if dry_run is None:
730 dry_run = opts.get('dry_run')
733 dry_run = opts.get('dry_run')
731 if similarity is None:
734 if similarity is None:
732 similarity = float(opts.get('similarity') or 0)
735 similarity = float(opts.get('similarity') or 0)
733
736
734 ret = 0
737 ret = 0
735 join = lambda f: os.path.join(prefix, f)
738 join = lambda f: os.path.join(prefix, f)
736
739
737 wctx = repo[None]
740 wctx = repo[None]
738 for subpath in sorted(wctx.substate):
741 for subpath in sorted(wctx.substate):
739 submatch = matchmod.subdirmatcher(subpath, m)
742 submatch = matchmod.subdirmatcher(subpath, m)
740 if opts.get('subrepos') or m.exact(subpath) or any(submatch.files()):
743 if opts.get('subrepos') or m.exact(subpath) or any(submatch.files()):
741 sub = wctx.sub(subpath)
744 sub = wctx.sub(subpath)
742 try:
745 try:
743 if sub.addremove(submatch, prefix, opts, dry_run, similarity):
746 if sub.addremove(submatch, prefix, opts, dry_run, similarity):
744 ret = 1
747 ret = 1
745 except error.LookupError:
748 except error.LookupError:
746 repo.ui.status(_("skipping missing subrepository: %s\n")
749 repo.ui.status(_("skipping missing subrepository: %s\n")
747 % join(subpath))
750 % join(subpath))
748
751
749 rejected = []
752 rejected = []
750 def badfn(f, msg):
753 def badfn(f, msg):
751 if f in m.files():
754 if f in m.files():
752 m.bad(f, msg)
755 m.bad(f, msg)
753 rejected.append(f)
756 rejected.append(f)
754
757
755 badmatch = matchmod.badmatch(m, badfn)
758 badmatch = matchmod.badmatch(m, badfn)
756 added, unknown, deleted, removed, forgotten = _interestingfiles(repo,
759 added, unknown, deleted, removed, forgotten = _interestingfiles(repo,
757 badmatch)
760 badmatch)
758
761
759 unknownset = set(unknown + forgotten)
762 unknownset = set(unknown + forgotten)
760 toprint = unknownset.copy()
763 toprint = unknownset.copy()
761 toprint.update(deleted)
764 toprint.update(deleted)
762 for abs in sorted(toprint):
765 for abs in sorted(toprint):
763 if repo.ui.verbose or not m.exact(abs):
766 if repo.ui.verbose or not m.exact(abs):
764 if abs in unknownset:
767 if abs in unknownset:
765 status = _('adding %s\n') % m.uipath(abs)
768 status = _('adding %s\n') % m.uipath(abs)
766 else:
769 else:
767 status = _('removing %s\n') % m.uipath(abs)
770 status = _('removing %s\n') % m.uipath(abs)
768 repo.ui.status(status)
771 repo.ui.status(status)
769
772
770 renames = _findrenames(repo, m, added + unknown, removed + deleted,
773 renames = _findrenames(repo, m, added + unknown, removed + deleted,
771 similarity)
774 similarity)
772
775
773 if not dry_run:
776 if not dry_run:
774 _markchanges(repo, unknown + forgotten, deleted, renames)
777 _markchanges(repo, unknown + forgotten, deleted, renames)
775
778
776 for f in rejected:
779 for f in rejected:
777 if f in m.files():
780 if f in m.files():
778 return 1
781 return 1
779 return ret
782 return ret
780
783
781 def marktouched(repo, files, similarity=0.0):
784 def marktouched(repo, files, similarity=0.0):
782 '''Assert that files have somehow been operated upon. files are relative to
785 '''Assert that files have somehow been operated upon. files are relative to
783 the repo root.'''
786 the repo root.'''
784 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
787 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
785 rejected = []
788 rejected = []
786
789
787 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
790 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
788
791
789 if repo.ui.verbose:
792 if repo.ui.verbose:
790 unknownset = set(unknown + forgotten)
793 unknownset = set(unknown + forgotten)
791 toprint = unknownset.copy()
794 toprint = unknownset.copy()
792 toprint.update(deleted)
795 toprint.update(deleted)
793 for abs in sorted(toprint):
796 for abs in sorted(toprint):
794 if abs in unknownset:
797 if abs in unknownset:
795 status = _('adding %s\n') % abs
798 status = _('adding %s\n') % abs
796 else:
799 else:
797 status = _('removing %s\n') % abs
800 status = _('removing %s\n') % abs
798 repo.ui.status(status)
801 repo.ui.status(status)
799
802
800 renames = _findrenames(repo, m, added + unknown, removed + deleted,
803 renames = _findrenames(repo, m, added + unknown, removed + deleted,
801 similarity)
804 similarity)
802
805
803 _markchanges(repo, unknown + forgotten, deleted, renames)
806 _markchanges(repo, unknown + forgotten, deleted, renames)
804
807
805 for f in rejected:
808 for f in rejected:
806 if f in m.files():
809 if f in m.files():
807 return 1
810 return 1
808 return 0
811 return 0
809
812
810 def _interestingfiles(repo, matcher):
813 def _interestingfiles(repo, matcher):
811 '''Walk dirstate with matcher, looking for files that addremove would care
814 '''Walk dirstate with matcher, looking for files that addremove would care
812 about.
815 about.
813
816
814 This is different from dirstate.status because it doesn't care about
817 This is different from dirstate.status because it doesn't care about
815 whether files are modified or clean.'''
818 whether files are modified or clean.'''
816 added, unknown, deleted, removed, forgotten = [], [], [], [], []
819 added, unknown, deleted, removed, forgotten = [], [], [], [], []
817 audit_path = pathutil.pathauditor(repo.root, cached=True)
820 audit_path = pathutil.pathauditor(repo.root, cached=True)
818
821
819 ctx = repo[None]
822 ctx = repo[None]
820 dirstate = repo.dirstate
823 dirstate = repo.dirstate
821 walkresults = dirstate.walk(matcher, subrepos=sorted(ctx.substate),
824 walkresults = dirstate.walk(matcher, subrepos=sorted(ctx.substate),
822 unknown=True, ignored=False, full=False)
825 unknown=True, ignored=False, full=False)
823 for abs, st in walkresults.iteritems():
826 for abs, st in walkresults.iteritems():
824 dstate = dirstate[abs]
827 dstate = dirstate[abs]
825 if dstate == '?' and audit_path.check(abs):
828 if dstate == '?' and audit_path.check(abs):
826 unknown.append(abs)
829 unknown.append(abs)
827 elif dstate != 'r' and not st:
830 elif dstate != 'r' and not st:
828 deleted.append(abs)
831 deleted.append(abs)
829 elif dstate == 'r' and st:
832 elif dstate == 'r' and st:
830 forgotten.append(abs)
833 forgotten.append(abs)
831 # for finding renames
834 # for finding renames
832 elif dstate == 'r' and not st:
835 elif dstate == 'r' and not st:
833 removed.append(abs)
836 removed.append(abs)
834 elif dstate == 'a':
837 elif dstate == 'a':
835 added.append(abs)
838 added.append(abs)
836
839
837 return added, unknown, deleted, removed, forgotten
840 return added, unknown, deleted, removed, forgotten
838
841
839 def _findrenames(repo, matcher, added, removed, similarity):
842 def _findrenames(repo, matcher, added, removed, similarity):
840 '''Find renames from removed files to added ones.'''
843 '''Find renames from removed files to added ones.'''
841 renames = {}
844 renames = {}
842 if similarity > 0:
845 if similarity > 0:
843 for old, new, score in similar.findrenames(repo, added, removed,
846 for old, new, score in similar.findrenames(repo, added, removed,
844 similarity):
847 similarity):
845 if (repo.ui.verbose or not matcher.exact(old)
848 if (repo.ui.verbose or not matcher.exact(old)
846 or not matcher.exact(new)):
849 or not matcher.exact(new)):
847 repo.ui.status(_('recording removal of %s as rename to %s '
850 repo.ui.status(_('recording removal of %s as rename to %s '
848 '(%d%% similar)\n') %
851 '(%d%% similar)\n') %
849 (matcher.rel(old), matcher.rel(new),
852 (matcher.rel(old), matcher.rel(new),
850 score * 100))
853 score * 100))
851 renames[new] = old
854 renames[new] = old
852 return renames
855 return renames
853
856
854 def _markchanges(repo, unknown, deleted, renames):
857 def _markchanges(repo, unknown, deleted, renames):
855 '''Marks the files in unknown as added, the files in deleted as removed,
858 '''Marks the files in unknown as added, the files in deleted as removed,
856 and the files in renames as copied.'''
859 and the files in renames as copied.'''
857 wctx = repo[None]
860 wctx = repo[None]
858 with repo.wlock():
861 with repo.wlock():
859 wctx.forget(deleted)
862 wctx.forget(deleted)
860 wctx.add(unknown)
863 wctx.add(unknown)
861 for new, old in renames.iteritems():
864 for new, old in renames.iteritems():
862 wctx.copy(old, new)
865 wctx.copy(old, new)
863
866
864 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
867 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
865 """Update the dirstate to reflect the intent of copying src to dst. For
868 """Update the dirstate to reflect the intent of copying src to dst. For
866 different reasons it might not end with dst being marked as copied from src.
869 different reasons it might not end with dst being marked as copied from src.
867 """
870 """
868 origsrc = repo.dirstate.copied(src) or src
871 origsrc = repo.dirstate.copied(src) or src
869 if dst == origsrc: # copying back a copy?
872 if dst == origsrc: # copying back a copy?
870 if repo.dirstate[dst] not in 'mn' and not dryrun:
873 if repo.dirstate[dst] not in 'mn' and not dryrun:
871 repo.dirstate.normallookup(dst)
874 repo.dirstate.normallookup(dst)
872 else:
875 else:
873 if repo.dirstate[origsrc] == 'a' and origsrc == src:
876 if repo.dirstate[origsrc] == 'a' and origsrc == src:
874 if not ui.quiet:
877 if not ui.quiet:
875 ui.warn(_("%s has not been committed yet, so no copy "
878 ui.warn(_("%s has not been committed yet, so no copy "
876 "data will be stored for %s.\n")
879 "data will be stored for %s.\n")
877 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
880 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
878 if repo.dirstate[dst] in '?r' and not dryrun:
881 if repo.dirstate[dst] in '?r' and not dryrun:
879 wctx.add([dst])
882 wctx.add([dst])
880 elif not dryrun:
883 elif not dryrun:
881 wctx.copy(origsrc, dst)
884 wctx.copy(origsrc, dst)
882
885
883 def readrequires(opener, supported):
886 def readrequires(opener, supported):
884 '''Reads and parses .hg/requires and checks if all entries found
887 '''Reads and parses .hg/requires and checks if all entries found
885 are in the list of supported features.'''
888 are in the list of supported features.'''
886 requirements = set(opener.read("requires").splitlines())
889 requirements = set(opener.read("requires").splitlines())
887 missings = []
890 missings = []
888 for r in requirements:
891 for r in requirements:
889 if r not in supported:
892 if r not in supported:
890 if not r or not r[0:1].isalnum():
893 if not r or not r[0:1].isalnum():
891 raise error.RequirementError(_(".hg/requires file is corrupt"))
894 raise error.RequirementError(_(".hg/requires file is corrupt"))
892 missings.append(r)
895 missings.append(r)
893 missings.sort()
896 missings.sort()
894 if missings:
897 if missings:
895 raise error.RequirementError(
898 raise error.RequirementError(
896 _("repository requires features unknown to this Mercurial: %s")
899 _("repository requires features unknown to this Mercurial: %s")
897 % " ".join(missings),
900 % " ".join(missings),
898 hint=_("see https://mercurial-scm.org/wiki/MissingRequirement"
901 hint=_("see https://mercurial-scm.org/wiki/MissingRequirement"
899 " for more information"))
902 " for more information"))
900 return requirements
903 return requirements
901
904
902 def writerequires(opener, requirements):
905 def writerequires(opener, requirements):
903 with opener('requires', 'w') as fp:
906 with opener('requires', 'w') as fp:
904 for r in sorted(requirements):
907 for r in sorted(requirements):
905 fp.write("%s\n" % r)
908 fp.write("%s\n" % r)
906
909
907 class filecachesubentry(object):
910 class filecachesubentry(object):
908 def __init__(self, path, stat):
911 def __init__(self, path, stat):
909 self.path = path
912 self.path = path
910 self.cachestat = None
913 self.cachestat = None
911 self._cacheable = None
914 self._cacheable = None
912
915
913 if stat:
916 if stat:
914 self.cachestat = filecachesubentry.stat(self.path)
917 self.cachestat = filecachesubentry.stat(self.path)
915
918
916 if self.cachestat:
919 if self.cachestat:
917 self._cacheable = self.cachestat.cacheable()
920 self._cacheable = self.cachestat.cacheable()
918 else:
921 else:
919 # None means we don't know yet
922 # None means we don't know yet
920 self._cacheable = None
923 self._cacheable = None
921
924
922 def refresh(self):
925 def refresh(self):
923 if self.cacheable():
926 if self.cacheable():
924 self.cachestat = filecachesubentry.stat(self.path)
927 self.cachestat = filecachesubentry.stat(self.path)
925
928
926 def cacheable(self):
929 def cacheable(self):
927 if self._cacheable is not None:
930 if self._cacheable is not None:
928 return self._cacheable
931 return self._cacheable
929
932
930 # we don't know yet, assume it is for now
933 # we don't know yet, assume it is for now
931 return True
934 return True
932
935
933 def changed(self):
936 def changed(self):
934 # no point in going further if we can't cache it
937 # no point in going further if we can't cache it
935 if not self.cacheable():
938 if not self.cacheable():
936 return True
939 return True
937
940
938 newstat = filecachesubentry.stat(self.path)
941 newstat = filecachesubentry.stat(self.path)
939
942
940 # we may not know if it's cacheable yet, check again now
943 # we may not know if it's cacheable yet, check again now
941 if newstat and self._cacheable is None:
944 if newstat and self._cacheable is None:
942 self._cacheable = newstat.cacheable()
945 self._cacheable = newstat.cacheable()
943
946
944 # check again
947 # check again
945 if not self._cacheable:
948 if not self._cacheable:
946 return True
949 return True
947
950
948 if self.cachestat != newstat:
951 if self.cachestat != newstat:
949 self.cachestat = newstat
952 self.cachestat = newstat
950 return True
953 return True
951 else:
954 else:
952 return False
955 return False
953
956
954 @staticmethod
957 @staticmethod
955 def stat(path):
958 def stat(path):
956 try:
959 try:
957 return util.cachestat(path)
960 return util.cachestat(path)
958 except OSError as e:
961 except OSError as e:
959 if e.errno != errno.ENOENT:
962 if e.errno != errno.ENOENT:
960 raise
963 raise
961
964
962 class filecacheentry(object):
965 class filecacheentry(object):
963 def __init__(self, paths, stat=True):
966 def __init__(self, paths, stat=True):
964 self._entries = []
967 self._entries = []
965 for path in paths:
968 for path in paths:
966 self._entries.append(filecachesubentry(path, stat))
969 self._entries.append(filecachesubentry(path, stat))
967
970
968 def changed(self):
971 def changed(self):
969 '''true if any entry has changed'''
972 '''true if any entry has changed'''
970 for entry in self._entries:
973 for entry in self._entries:
971 if entry.changed():
974 if entry.changed():
972 return True
975 return True
973 return False
976 return False
974
977
975 def refresh(self):
978 def refresh(self):
976 for entry in self._entries:
979 for entry in self._entries:
977 entry.refresh()
980 entry.refresh()
978
981
979 class filecache(object):
982 class filecache(object):
980 '''A property like decorator that tracks files under .hg/ for updates.
983 '''A property like decorator that tracks files under .hg/ for updates.
981
984
982 Records stat info when called in _filecache.
985 Records stat info when called in _filecache.
983
986
984 On subsequent calls, compares old stat info with new info, and recreates the
987 On subsequent calls, compares old stat info with new info, and recreates the
985 object when any of the files changes, updating the new stat info in
988 object when any of the files changes, updating the new stat info in
986 _filecache.
989 _filecache.
987
990
988 Mercurial either atomic renames or appends for files under .hg,
991 Mercurial either atomic renames or appends for files under .hg,
989 so to ensure the cache is reliable we need the filesystem to be able
992 so to ensure the cache is reliable we need the filesystem to be able
990 to tell us if a file has been replaced. If it can't, we fallback to
993 to tell us if a file has been replaced. If it can't, we fallback to
991 recreating the object on every call (essentially the same behavior as
994 recreating the object on every call (essentially the same behavior as
992 propertycache).
995 propertycache).
993
996
994 '''
997 '''
995 def __init__(self, *paths):
998 def __init__(self, *paths):
996 self.paths = paths
999 self.paths = paths
997
1000
998 def join(self, obj, fname):
1001 def join(self, obj, fname):
999 """Used to compute the runtime path of a cached file.
1002 """Used to compute the runtime path of a cached file.
1000
1003
1001 Users should subclass filecache and provide their own version of this
1004 Users should subclass filecache and provide their own version of this
1002 function to call the appropriate join function on 'obj' (an instance
1005 function to call the appropriate join function on 'obj' (an instance
1003 of the class that its member function was decorated).
1006 of the class that its member function was decorated).
1004 """
1007 """
1005 raise NotImplementedError
1008 raise NotImplementedError
1006
1009
1007 def __call__(self, func):
1010 def __call__(self, func):
1008 self.func = func
1011 self.func = func
1009 self.name = func.__name__.encode('ascii')
1012 self.name = func.__name__.encode('ascii')
1010 return self
1013 return self
1011
1014
1012 def __get__(self, obj, type=None):
1015 def __get__(self, obj, type=None):
1013 # if accessed on the class, return the descriptor itself.
1016 # if accessed on the class, return the descriptor itself.
1014 if obj is None:
1017 if obj is None:
1015 return self
1018 return self
1016 # do we need to check if the file changed?
1019 # do we need to check if the file changed?
1017 if self.name in obj.__dict__:
1020 if self.name in obj.__dict__:
1018 assert self.name in obj._filecache, self.name
1021 assert self.name in obj._filecache, self.name
1019 return obj.__dict__[self.name]
1022 return obj.__dict__[self.name]
1020
1023
1021 entry = obj._filecache.get(self.name)
1024 entry = obj._filecache.get(self.name)
1022
1025
1023 if entry:
1026 if entry:
1024 if entry.changed():
1027 if entry.changed():
1025 entry.obj = self.func(obj)
1028 entry.obj = self.func(obj)
1026 else:
1029 else:
1027 paths = [self.join(obj, path) for path in self.paths]
1030 paths = [self.join(obj, path) for path in self.paths]
1028
1031
1029 # We stat -before- creating the object so our cache doesn't lie if
1032 # We stat -before- creating the object so our cache doesn't lie if
1030 # a writer modified between the time we read and stat
1033 # a writer modified between the time we read and stat
1031 entry = filecacheentry(paths, True)
1034 entry = filecacheentry(paths, True)
1032 entry.obj = self.func(obj)
1035 entry.obj = self.func(obj)
1033
1036
1034 obj._filecache[self.name] = entry
1037 obj._filecache[self.name] = entry
1035
1038
1036 obj.__dict__[self.name] = entry.obj
1039 obj.__dict__[self.name] = entry.obj
1037 return entry.obj
1040 return entry.obj
1038
1041
1039 def __set__(self, obj, value):
1042 def __set__(self, obj, value):
1040 if self.name not in obj._filecache:
1043 if self.name not in obj._filecache:
1041 # we add an entry for the missing value because X in __dict__
1044 # we add an entry for the missing value because X in __dict__
1042 # implies X in _filecache
1045 # implies X in _filecache
1043 paths = [self.join(obj, path) for path in self.paths]
1046 paths = [self.join(obj, path) for path in self.paths]
1044 ce = filecacheentry(paths, False)
1047 ce = filecacheentry(paths, False)
1045 obj._filecache[self.name] = ce
1048 obj._filecache[self.name] = ce
1046 else:
1049 else:
1047 ce = obj._filecache[self.name]
1050 ce = obj._filecache[self.name]
1048
1051
1049 ce.obj = value # update cached copy
1052 ce.obj = value # update cached copy
1050 obj.__dict__[self.name] = value # update copy returned by obj.x
1053 obj.__dict__[self.name] = value # update copy returned by obj.x
1051
1054
1052 def __delete__(self, obj):
1055 def __delete__(self, obj):
1053 try:
1056 try:
1054 del obj.__dict__[self.name]
1057 del obj.__dict__[self.name]
1055 except KeyError:
1058 except KeyError:
1056 raise AttributeError(self.name)
1059 raise AttributeError(self.name)
1057
1060
1058 def extdatasource(repo, source):
1061 def extdatasource(repo, source):
1059 """Gather a map of rev -> value dict from the specified source
1062 """Gather a map of rev -> value dict from the specified source
1060
1063
1061 A source spec is treated as a URL, with a special case shell: type
1064 A source spec is treated as a URL, with a special case shell: type
1062 for parsing the output from a shell command.
1065 for parsing the output from a shell command.
1063
1066
1064 The data is parsed as a series of newline-separated records where
1067 The data is parsed as a series of newline-separated records where
1065 each record is a revision specifier optionally followed by a space
1068 each record is a revision specifier optionally followed by a space
1066 and a freeform string value. If the revision is known locally, it
1069 and a freeform string value. If the revision is known locally, it
1067 is converted to a rev, otherwise the record is skipped.
1070 is converted to a rev, otherwise the record is skipped.
1068
1071
1069 Note that both key and value are treated as UTF-8 and converted to
1072 Note that both key and value are treated as UTF-8 and converted to
1070 the local encoding. This allows uniformity between local and
1073 the local encoding. This allows uniformity between local and
1071 remote data sources.
1074 remote data sources.
1072 """
1075 """
1073
1076
1074 spec = repo.ui.config("extdata", source)
1077 spec = repo.ui.config("extdata", source)
1075 if not spec:
1078 if not spec:
1076 raise error.Abort(_("unknown extdata source '%s'") % source)
1079 raise error.Abort(_("unknown extdata source '%s'") % source)
1077
1080
1078 data = {}
1081 data = {}
1079 src = proc = None
1082 src = proc = None
1080 try:
1083 try:
1081 if spec.startswith("shell:"):
1084 if spec.startswith("shell:"):
1082 # external commands should be run relative to the repo root
1085 # external commands should be run relative to the repo root
1083 cmd = spec[6:]
1086 cmd = spec[6:]
1084 proc = subprocess.Popen(cmd, shell=True, bufsize=-1,
1087 proc = subprocess.Popen(cmd, shell=True, bufsize=-1,
1085 close_fds=util.closefds,
1088 close_fds=util.closefds,
1086 stdout=subprocess.PIPE, cwd=repo.root)
1089 stdout=subprocess.PIPE, cwd=repo.root)
1087 src = proc.stdout
1090 src = proc.stdout
1088 else:
1091 else:
1089 # treat as a URL or file
1092 # treat as a URL or file
1090 src = url.open(repo.ui, spec)
1093 src = url.open(repo.ui, spec)
1091 for l in src:
1094 for l in src:
1092 if " " in l:
1095 if " " in l:
1093 k, v = l.strip().split(" ", 1)
1096 k, v = l.strip().split(" ", 1)
1094 else:
1097 else:
1095 k, v = l.strip(), ""
1098 k, v = l.strip(), ""
1096
1099
1097 k = encoding.tolocal(k)
1100 k = encoding.tolocal(k)
1098 try:
1101 try:
1099 data[repo[k].rev()] = encoding.tolocal(v)
1102 data[repo[k].rev()] = encoding.tolocal(v)
1100 except (error.LookupError, error.RepoLookupError):
1103 except (error.LookupError, error.RepoLookupError):
1101 pass # we ignore data for nodes that don't exist locally
1104 pass # we ignore data for nodes that don't exist locally
1102 finally:
1105 finally:
1103 if proc:
1106 if proc:
1104 proc.communicate()
1107 proc.communicate()
1105 if src:
1108 if src:
1106 src.close()
1109 src.close()
1107 if proc and proc.returncode != 0:
1110 if proc and proc.returncode != 0:
1108 raise error.Abort(_("extdata command '%s' failed: %s")
1111 raise error.Abort(_("extdata command '%s' failed: %s")
1109 % (cmd, util.explainexit(proc.returncode)[0]))
1112 % (cmd, util.explainexit(proc.returncode)[0]))
1110
1113
1111 return data
1114 return data
1112
1115
1113 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
1116 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
1114 if lock is None:
1117 if lock is None:
1115 raise error.LockInheritanceContractViolation(
1118 raise error.LockInheritanceContractViolation(
1116 'lock can only be inherited while held')
1119 'lock can only be inherited while held')
1117 if environ is None:
1120 if environ is None:
1118 environ = {}
1121 environ = {}
1119 with lock.inherit() as locker:
1122 with lock.inherit() as locker:
1120 environ[envvar] = locker
1123 environ[envvar] = locker
1121 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
1124 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
1122
1125
1123 def wlocksub(repo, cmd, *args, **kwargs):
1126 def wlocksub(repo, cmd, *args, **kwargs):
1124 """run cmd as a subprocess that allows inheriting repo's wlock
1127 """run cmd as a subprocess that allows inheriting repo's wlock
1125
1128
1126 This can only be called while the wlock is held. This takes all the
1129 This can only be called while the wlock is held. This takes all the
1127 arguments that ui.system does, and returns the exit code of the
1130 arguments that ui.system does, and returns the exit code of the
1128 subprocess."""
1131 subprocess."""
1129 return _locksub(repo, repo.currentwlock(), 'HG_WLOCK_LOCKER', cmd, *args,
1132 return _locksub(repo, repo.currentwlock(), 'HG_WLOCK_LOCKER', cmd, *args,
1130 **kwargs)
1133 **kwargs)
1131
1134
1132 def gdinitconfig(ui):
1135 def gdinitconfig(ui):
1133 """helper function to know if a repo should be created as general delta
1136 """helper function to know if a repo should be created as general delta
1134 """
1137 """
1135 # experimental config: format.generaldelta
1138 # experimental config: format.generaldelta
1136 return (ui.configbool('format', 'generaldelta')
1139 return (ui.configbool('format', 'generaldelta')
1137 or ui.configbool('format', 'usegeneraldelta'))
1140 or ui.configbool('format', 'usegeneraldelta'))
1138
1141
1139 def gddeltaconfig(ui):
1142 def gddeltaconfig(ui):
1140 """helper function to know if incoming delta should be optimised
1143 """helper function to know if incoming delta should be optimised
1141 """
1144 """
1142 # experimental config: format.generaldelta
1145 # experimental config: format.generaldelta
1143 return ui.configbool('format', 'generaldelta')
1146 return ui.configbool('format', 'generaldelta')
1144
1147
1145 class simplekeyvaluefile(object):
1148 class simplekeyvaluefile(object):
1146 """A simple file with key=value lines
1149 """A simple file with key=value lines
1147
1150
1148 Keys must be alphanumerics and start with a letter, values must not
1151 Keys must be alphanumerics and start with a letter, values must not
1149 contain '\n' characters"""
1152 contain '\n' characters"""
1150 firstlinekey = '__firstline'
1153 firstlinekey = '__firstline'
1151
1154
1152 def __init__(self, vfs, path, keys=None):
1155 def __init__(self, vfs, path, keys=None):
1153 self.vfs = vfs
1156 self.vfs = vfs
1154 self.path = path
1157 self.path = path
1155
1158
1156 def read(self, firstlinenonkeyval=False):
1159 def read(self, firstlinenonkeyval=False):
1157 """Read the contents of a simple key-value file
1160 """Read the contents of a simple key-value file
1158
1161
1159 'firstlinenonkeyval' indicates whether the first line of file should
1162 'firstlinenonkeyval' indicates whether the first line of file should
1160 be treated as a key-value pair or reuturned fully under the
1163 be treated as a key-value pair or reuturned fully under the
1161 __firstline key."""
1164 __firstline key."""
1162 lines = self.vfs.readlines(self.path)
1165 lines = self.vfs.readlines(self.path)
1163 d = {}
1166 d = {}
1164 if firstlinenonkeyval:
1167 if firstlinenonkeyval:
1165 if not lines:
1168 if not lines:
1166 e = _("empty simplekeyvalue file")
1169 e = _("empty simplekeyvalue file")
1167 raise error.CorruptedState(e)
1170 raise error.CorruptedState(e)
1168 # we don't want to include '\n' in the __firstline
1171 # we don't want to include '\n' in the __firstline
1169 d[self.firstlinekey] = lines[0][:-1]
1172 d[self.firstlinekey] = lines[0][:-1]
1170 del lines[0]
1173 del lines[0]
1171
1174
1172 try:
1175 try:
1173 # the 'if line.strip()' part prevents us from failing on empty
1176 # the 'if line.strip()' part prevents us from failing on empty
1174 # lines which only contain '\n' therefore are not skipped
1177 # lines which only contain '\n' therefore are not skipped
1175 # by 'if line'
1178 # by 'if line'
1176 updatedict = dict(line[:-1].split('=', 1) for line in lines
1179 updatedict = dict(line[:-1].split('=', 1) for line in lines
1177 if line.strip())
1180 if line.strip())
1178 if self.firstlinekey in updatedict:
1181 if self.firstlinekey in updatedict:
1179 e = _("%r can't be used as a key")
1182 e = _("%r can't be used as a key")
1180 raise error.CorruptedState(e % self.firstlinekey)
1183 raise error.CorruptedState(e % self.firstlinekey)
1181 d.update(updatedict)
1184 d.update(updatedict)
1182 except ValueError as e:
1185 except ValueError as e:
1183 raise error.CorruptedState(str(e))
1186 raise error.CorruptedState(str(e))
1184 return d
1187 return d
1185
1188
1186 def write(self, data, firstline=None):
1189 def write(self, data, firstline=None):
1187 """Write key=>value mapping to a file
1190 """Write key=>value mapping to a file
1188 data is a dict. Keys must be alphanumerical and start with a letter.
1191 data is a dict. Keys must be alphanumerical and start with a letter.
1189 Values must not contain newline characters.
1192 Values must not contain newline characters.
1190
1193
1191 If 'firstline' is not None, it is written to file before
1194 If 'firstline' is not None, it is written to file before
1192 everything else, as it is, not in a key=value form"""
1195 everything else, as it is, not in a key=value form"""
1193 lines = []
1196 lines = []
1194 if firstline is not None:
1197 if firstline is not None:
1195 lines.append('%s\n' % firstline)
1198 lines.append('%s\n' % firstline)
1196
1199
1197 for k, v in data.items():
1200 for k, v in data.items():
1198 if k == self.firstlinekey:
1201 if k == self.firstlinekey:
1199 e = "key name '%s' is reserved" % self.firstlinekey
1202 e = "key name '%s' is reserved" % self.firstlinekey
1200 raise error.ProgrammingError(e)
1203 raise error.ProgrammingError(e)
1201 if not k[0:1].isalpha():
1204 if not k[0:1].isalpha():
1202 e = "keys must start with a letter in a key-value file"
1205 e = "keys must start with a letter in a key-value file"
1203 raise error.ProgrammingError(e)
1206 raise error.ProgrammingError(e)
1204 if not k.isalnum():
1207 if not k.isalnum():
1205 e = "invalid key name in a simple key-value file"
1208 e = "invalid key name in a simple key-value file"
1206 raise error.ProgrammingError(e)
1209 raise error.ProgrammingError(e)
1207 if '\n' in v:
1210 if '\n' in v:
1208 e = "invalid value in a simple key-value file"
1211 e = "invalid value in a simple key-value file"
1209 raise error.ProgrammingError(e)
1212 raise error.ProgrammingError(e)
1210 lines.append("%s=%s\n" % (k, v))
1213 lines.append("%s=%s\n" % (k, v))
1211 with self.vfs(self.path, mode='wb', atomictemp=True) as fp:
1214 with self.vfs(self.path, mode='wb', atomictemp=True) as fp:
1212 fp.write(''.join(lines))
1215 fp.write(''.join(lines))
1213
1216
1214 _reportobsoletedsource = [
1217 _reportobsoletedsource = [
1215 'debugobsolete',
1218 'debugobsolete',
1216 'pull',
1219 'pull',
1217 'push',
1220 'push',
1218 'serve',
1221 'serve',
1219 'unbundle',
1222 'unbundle',
1220 ]
1223 ]
1221
1224
1222 _reportnewcssource = [
1225 _reportnewcssource = [
1223 'pull',
1226 'pull',
1224 'unbundle',
1227 'unbundle',
1225 ]
1228 ]
1226
1229
1227 # a list of (repo, ctx, files) functions called by various commands to allow
1230 # a list of (repo, ctx, files) functions called by various commands to allow
1228 # extensions to ensure the corresponding files are available locally, before the
1231 # extensions to ensure the corresponding files are available locally, before the
1229 # command uses them.
1232 # command uses them.
1230 fileprefetchhooks = util.hooks()
1233 fileprefetchhooks = util.hooks()
1231
1234
1232 # A marker that tells the evolve extension to suppress its own reporting
1235 # A marker that tells the evolve extension to suppress its own reporting
1233 _reportstroubledchangesets = True
1236 _reportstroubledchangesets = True
1234
1237
1235 def registersummarycallback(repo, otr, txnname=''):
1238 def registersummarycallback(repo, otr, txnname=''):
1236 """register a callback to issue a summary after the transaction is closed
1239 """register a callback to issue a summary after the transaction is closed
1237 """
1240 """
1238 def txmatch(sources):
1241 def txmatch(sources):
1239 return any(txnname.startswith(source) for source in sources)
1242 return any(txnname.startswith(source) for source in sources)
1240
1243
1241 categories = []
1244 categories = []
1242
1245
1243 def reportsummary(func):
1246 def reportsummary(func):
1244 """decorator for report callbacks."""
1247 """decorator for report callbacks."""
1245 # The repoview life cycle is shorter than the one of the actual
1248 # The repoview life cycle is shorter than the one of the actual
1246 # underlying repository. So the filtered object can die before the
1249 # underlying repository. So the filtered object can die before the
1247 # weakref is used leading to troubles. We keep a reference to the
1250 # weakref is used leading to troubles. We keep a reference to the
1248 # unfiltered object and restore the filtering when retrieving the
1251 # unfiltered object and restore the filtering when retrieving the
1249 # repository through the weakref.
1252 # repository through the weakref.
1250 filtername = repo.filtername
1253 filtername = repo.filtername
1251 reporef = weakref.ref(repo.unfiltered())
1254 reporef = weakref.ref(repo.unfiltered())
1252 def wrapped(tr):
1255 def wrapped(tr):
1253 repo = reporef()
1256 repo = reporef()
1254 if filtername:
1257 if filtername:
1255 repo = repo.filtered(filtername)
1258 repo = repo.filtered(filtername)
1256 func(repo, tr)
1259 func(repo, tr)
1257 newcat = '%02i-txnreport' % len(categories)
1260 newcat = '%02i-txnreport' % len(categories)
1258 otr.addpostclose(newcat, wrapped)
1261 otr.addpostclose(newcat, wrapped)
1259 categories.append(newcat)
1262 categories.append(newcat)
1260 return wrapped
1263 return wrapped
1261
1264
1262 if txmatch(_reportobsoletedsource):
1265 if txmatch(_reportobsoletedsource):
1263 @reportsummary
1266 @reportsummary
1264 def reportobsoleted(repo, tr):
1267 def reportobsoleted(repo, tr):
1265 obsoleted = obsutil.getobsoleted(repo, tr)
1268 obsoleted = obsutil.getobsoleted(repo, tr)
1266 if obsoleted:
1269 if obsoleted:
1267 repo.ui.status(_('obsoleted %i changesets\n')
1270 repo.ui.status(_('obsoleted %i changesets\n')
1268 % len(obsoleted))
1271 % len(obsoleted))
1269
1272
1270 if (obsolete.isenabled(repo, obsolete.createmarkersopt) and
1273 if (obsolete.isenabled(repo, obsolete.createmarkersopt) and
1271 repo.ui.configbool('experimental', 'evolution.report-instabilities')):
1274 repo.ui.configbool('experimental', 'evolution.report-instabilities')):
1272 instabilitytypes = [
1275 instabilitytypes = [
1273 ('orphan', 'orphan'),
1276 ('orphan', 'orphan'),
1274 ('phase-divergent', 'phasedivergent'),
1277 ('phase-divergent', 'phasedivergent'),
1275 ('content-divergent', 'contentdivergent'),
1278 ('content-divergent', 'contentdivergent'),
1276 ]
1279 ]
1277
1280
1278 def getinstabilitycounts(repo):
1281 def getinstabilitycounts(repo):
1279 filtered = repo.changelog.filteredrevs
1282 filtered = repo.changelog.filteredrevs
1280 counts = {}
1283 counts = {}
1281 for instability, revset in instabilitytypes:
1284 for instability, revset in instabilitytypes:
1282 counts[instability] = len(set(obsolete.getrevs(repo, revset)) -
1285 counts[instability] = len(set(obsolete.getrevs(repo, revset)) -
1283 filtered)
1286 filtered)
1284 return counts
1287 return counts
1285
1288
1286 oldinstabilitycounts = getinstabilitycounts(repo)
1289 oldinstabilitycounts = getinstabilitycounts(repo)
1287 @reportsummary
1290 @reportsummary
1288 def reportnewinstabilities(repo, tr):
1291 def reportnewinstabilities(repo, tr):
1289 newinstabilitycounts = getinstabilitycounts(repo)
1292 newinstabilitycounts = getinstabilitycounts(repo)
1290 for instability, revset in instabilitytypes:
1293 for instability, revset in instabilitytypes:
1291 delta = (newinstabilitycounts[instability] -
1294 delta = (newinstabilitycounts[instability] -
1292 oldinstabilitycounts[instability])
1295 oldinstabilitycounts[instability])
1293 if delta > 0:
1296 if delta > 0:
1294 repo.ui.warn(_('%i new %s changesets\n') %
1297 repo.ui.warn(_('%i new %s changesets\n') %
1295 (delta, instability))
1298 (delta, instability))
1296
1299
1297 if txmatch(_reportnewcssource):
1300 if txmatch(_reportnewcssource):
1298 @reportsummary
1301 @reportsummary
1299 def reportnewcs(repo, tr):
1302 def reportnewcs(repo, tr):
1300 """Report the range of new revisions pulled/unbundled."""
1303 """Report the range of new revisions pulled/unbundled."""
1301 newrevs = tr.changes.get('revs', xrange(0, 0))
1304 newrevs = tr.changes.get('revs', xrange(0, 0))
1302 if not newrevs:
1305 if not newrevs:
1303 return
1306 return
1304
1307
1305 # Compute the bounds of new revisions' range, excluding obsoletes.
1308 # Compute the bounds of new revisions' range, excluding obsoletes.
1306 unfi = repo.unfiltered()
1309 unfi = repo.unfiltered()
1307 revs = unfi.revs('%ld and not obsolete()', newrevs)
1310 revs = unfi.revs('%ld and not obsolete()', newrevs)
1308 if not revs:
1311 if not revs:
1309 # Got only obsoletes.
1312 # Got only obsoletes.
1310 return
1313 return
1311 minrev, maxrev = repo[revs.min()], repo[revs.max()]
1314 minrev, maxrev = repo[revs.min()], repo[revs.max()]
1312
1315
1313 if minrev == maxrev:
1316 if minrev == maxrev:
1314 revrange = minrev
1317 revrange = minrev
1315 else:
1318 else:
1316 revrange = '%s:%s' % (minrev, maxrev)
1319 revrange = '%s:%s' % (minrev, maxrev)
1317 repo.ui.status(_('new changesets %s\n') % revrange)
1320 repo.ui.status(_('new changesets %s\n') % revrange)
1318
1321
1319 def nodesummaries(repo, nodes, maxnumnodes=4):
1322 def nodesummaries(repo, nodes, maxnumnodes=4):
1320 if len(nodes) <= maxnumnodes or repo.ui.verbose:
1323 if len(nodes) <= maxnumnodes or repo.ui.verbose:
1321 return ' '.join(short(h) for h in nodes)
1324 return ' '.join(short(h) for h in nodes)
1322 first = ' '.join(short(h) for h in nodes[:maxnumnodes])
1325 first = ' '.join(short(h) for h in nodes[:maxnumnodes])
1323 return _("%s and %d others") % (first, len(nodes) - maxnumnodes)
1326 return _("%s and %d others") % (first, len(nodes) - maxnumnodes)
1324
1327
1325 def enforcesinglehead(repo, tr, desc):
1328 def enforcesinglehead(repo, tr, desc):
1326 """check that no named branch has multiple heads"""
1329 """check that no named branch has multiple heads"""
1327 if desc in ('strip', 'repair'):
1330 if desc in ('strip', 'repair'):
1328 # skip the logic during strip
1331 # skip the logic during strip
1329 return
1332 return
1330 visible = repo.filtered('visible')
1333 visible = repo.filtered('visible')
1331 # possible improvement: we could restrict the check to affected branch
1334 # possible improvement: we could restrict the check to affected branch
1332 for name, heads in visible.branchmap().iteritems():
1335 for name, heads in visible.branchmap().iteritems():
1333 if len(heads) > 1:
1336 if len(heads) > 1:
1334 msg = _('rejecting multiple heads on branch "%s"')
1337 msg = _('rejecting multiple heads on branch "%s"')
1335 msg %= name
1338 msg %= name
1336 hint = _('%d heads: %s')
1339 hint = _('%d heads: %s')
1337 hint %= (len(heads), nodesummaries(repo, heads))
1340 hint %= (len(heads), nodesummaries(repo, heads))
1338 raise error.Abort(msg, hint=hint)
1341 raise error.Abort(msg, hint=hint)
1339
1342
1340 def wrapconvertsink(sink):
1343 def wrapconvertsink(sink):
1341 """Allow extensions to wrap the sink returned by convcmd.convertsink()
1344 """Allow extensions to wrap the sink returned by convcmd.convertsink()
1342 before it is used, whether or not the convert extension was formally loaded.
1345 before it is used, whether or not the convert extension was formally loaded.
1343 """
1346 """
1344 return sink
1347 return sink
1345
1348
1346 def unhidehashlikerevs(repo, specs, hiddentype):
1349 def unhidehashlikerevs(repo, specs, hiddentype):
1347 """parse the user specs and unhide changesets whose hash or revision number
1350 """parse the user specs and unhide changesets whose hash or revision number
1348 is passed.
1351 is passed.
1349
1352
1350 hiddentype can be: 1) 'warn': warn while unhiding changesets
1353 hiddentype can be: 1) 'warn': warn while unhiding changesets
1351 2) 'nowarn': don't warn while unhiding changesets
1354 2) 'nowarn': don't warn while unhiding changesets
1352
1355
1353 returns a repo object with the required changesets unhidden
1356 returns a repo object with the required changesets unhidden
1354 """
1357 """
1355 if not repo.filtername or not repo.ui.configbool('experimental',
1358 if not repo.filtername or not repo.ui.configbool('experimental',
1356 'directaccess'):
1359 'directaccess'):
1357 return repo
1360 return repo
1358
1361
1359 if repo.filtername not in ('visible', 'visible-hidden'):
1362 if repo.filtername not in ('visible', 'visible-hidden'):
1360 return repo
1363 return repo
1361
1364
1362 symbols = set()
1365 symbols = set()
1363 for spec in specs:
1366 for spec in specs:
1364 try:
1367 try:
1365 tree = revsetlang.parse(spec)
1368 tree = revsetlang.parse(spec)
1366 except error.ParseError: # will be reported by scmutil.revrange()
1369 except error.ParseError: # will be reported by scmutil.revrange()
1367 continue
1370 continue
1368
1371
1369 symbols.update(revsetlang.gethashlikesymbols(tree))
1372 symbols.update(revsetlang.gethashlikesymbols(tree))
1370
1373
1371 if not symbols:
1374 if not symbols:
1372 return repo
1375 return repo
1373
1376
1374 revs = _getrevsfromsymbols(repo, symbols)
1377 revs = _getrevsfromsymbols(repo, symbols)
1375
1378
1376 if not revs:
1379 if not revs:
1377 return repo
1380 return repo
1378
1381
1379 if hiddentype == 'warn':
1382 if hiddentype == 'warn':
1380 unfi = repo.unfiltered()
1383 unfi = repo.unfiltered()
1381 revstr = ", ".join([pycompat.bytestr(unfi[l]) for l in revs])
1384 revstr = ", ".join([pycompat.bytestr(unfi[l]) for l in revs])
1382 repo.ui.warn(_("warning: accessing hidden changesets for write "
1385 repo.ui.warn(_("warning: accessing hidden changesets for write "
1383 "operation: %s\n") % revstr)
1386 "operation: %s\n") % revstr)
1384
1387
1385 # we have to use new filtername to separate branch/tags cache until we can
1388 # we have to use new filtername to separate branch/tags cache until we can
1386 # disbale these cache when revisions are dynamically pinned.
1389 # disbale these cache when revisions are dynamically pinned.
1387 return repo.filtered('visible-hidden', revs)
1390 return repo.filtered('visible-hidden', revs)
1388
1391
1389 def _getrevsfromsymbols(repo, symbols):
1392 def _getrevsfromsymbols(repo, symbols):
1390 """parse the list of symbols and returns a set of revision numbers of hidden
1393 """parse the list of symbols and returns a set of revision numbers of hidden
1391 changesets present in symbols"""
1394 changesets present in symbols"""
1392 revs = set()
1395 revs = set()
1393 unfi = repo.unfiltered()
1396 unfi = repo.unfiltered()
1394 unficl = unfi.changelog
1397 unficl = unfi.changelog
1395 cl = repo.changelog
1398 cl = repo.changelog
1396 tiprev = len(unficl)
1399 tiprev = len(unficl)
1397 pmatch = unficl._partialmatch
1400 pmatch = unficl._partialmatch
1398 allowrevnums = repo.ui.configbool('experimental', 'directaccess.revnums')
1401 allowrevnums = repo.ui.configbool('experimental', 'directaccess.revnums')
1399 for s in symbols:
1402 for s in symbols:
1400 try:
1403 try:
1401 n = int(s)
1404 n = int(s)
1402 if n <= tiprev:
1405 if n <= tiprev:
1403 if not allowrevnums:
1406 if not allowrevnums:
1404 continue
1407 continue
1405 else:
1408 else:
1406 if n not in cl:
1409 if n not in cl:
1407 revs.add(n)
1410 revs.add(n)
1408 continue
1411 continue
1409 except ValueError:
1412 except ValueError:
1410 pass
1413 pass
1411
1414
1412 try:
1415 try:
1413 s = pmatch(s)
1416 s = pmatch(s)
1414 except error.LookupError:
1417 except error.LookupError:
1415 s = None
1418 s = None
1416
1419
1417 if s is not None:
1420 if s is not None:
1418 rev = unficl.rev(s)
1421 rev = unficl.rev(s)
1419 if rev not in cl:
1422 if rev not in cl:
1420 revs.add(rev)
1423 revs.add(rev)
1421
1424
1422 return revs
1425 return revs
General Comments 0
You need to be logged in to leave comments. Login now