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