##// END OF EJS Templates
revlog: use specialized exception for ambiguous prefix lookup...
Martin von Zweigbergk -
r38877:df0873ab default
parent child Browse files
Show More
@@ -1,307 +1,310 b''
1 1 # error.py - Mercurial exceptions
2 2 #
3 3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """Mercurial exceptions.
9 9
10 10 This allows us to catch exceptions at higher levels without forcing
11 11 imports.
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 # Do not import anything but pycompat here, please
17 17 from . import pycompat
18 18
19 19 def _tobytes(exc):
20 20 """Byte-stringify exception in the same way as BaseException_str()"""
21 21 if not exc.args:
22 22 return b''
23 23 if len(exc.args) == 1:
24 24 return pycompat.bytestr(exc.args[0])
25 25 return b'(%s)' % b', '.join(b"'%s'" % pycompat.bytestr(a) for a in exc.args)
26 26
27 27 class Hint(object):
28 28 """Mix-in to provide a hint of an error
29 29
30 30 This should come first in the inheritance list to consume a hint and
31 31 pass remaining arguments to the exception class.
32 32 """
33 33 def __init__(self, *args, **kw):
34 34 self.hint = kw.pop(r'hint', None)
35 35 super(Hint, self).__init__(*args, **kw)
36 36
37 37 class RevlogError(Hint, Exception):
38 38 __bytes__ = _tobytes
39 39
40 40 class FilteredIndexError(IndexError):
41 41 __bytes__ = _tobytes
42 42
43 43 class LookupError(RevlogError, KeyError):
44 44 def __init__(self, name, index, message):
45 45 self.name = name
46 46 self.index = index
47 47 # this can't be called 'message' because at least some installs of
48 48 # Python 2.6+ complain about the 'message' property being deprecated
49 49 self.lookupmessage = message
50 50 if isinstance(name, bytes) and len(name) == 20:
51 51 from .node import short
52 52 name = short(name)
53 53 RevlogError.__init__(self, '%s@%s: %s' % (index, name, message))
54 54
55 55 def __bytes__(self):
56 56 return RevlogError.__bytes__(self)
57 57
58 58 def __str__(self):
59 59 return RevlogError.__str__(self)
60 60
61 class AmbiguousPrefixLookupError(LookupError):
62 pass
63
61 64 class FilteredLookupError(LookupError):
62 65 pass
63 66
64 67 class ManifestLookupError(LookupError):
65 68 pass
66 69
67 70 class CommandError(Exception):
68 71 """Exception raised on errors in parsing the command line."""
69 72 __bytes__ = _tobytes
70 73
71 74 class InterventionRequired(Hint, Exception):
72 75 """Exception raised when a command requires human intervention."""
73 76 __bytes__ = _tobytes
74 77
75 78 class Abort(Hint, Exception):
76 79 """Raised if a command needs to print an error and exit."""
77 80 __bytes__ = _tobytes
78 81
79 82 class HookLoadError(Abort):
80 83 """raised when loading a hook fails, aborting an operation
81 84
82 85 Exists to allow more specialized catching."""
83 86
84 87 class HookAbort(Abort):
85 88 """raised when a validation hook fails, aborting an operation
86 89
87 90 Exists to allow more specialized catching."""
88 91
89 92 class ConfigError(Abort):
90 93 """Exception raised when parsing config files"""
91 94
92 95 class UpdateAbort(Abort):
93 96 """Raised when an update is aborted for destination issue"""
94 97
95 98 class MergeDestAbort(Abort):
96 99 """Raised when an update is aborted for destination issues"""
97 100
98 101 class NoMergeDestAbort(MergeDestAbort):
99 102 """Raised when an update is aborted because there is nothing to merge"""
100 103
101 104 class ManyMergeDestAbort(MergeDestAbort):
102 105 """Raised when an update is aborted because destination is ambiguous"""
103 106
104 107 class ResponseExpected(Abort):
105 108 """Raised when an EOF is received for a prompt"""
106 109 def __init__(self):
107 110 from .i18n import _
108 111 Abort.__init__(self, _('response expected'))
109 112
110 113 class OutOfBandError(Hint, Exception):
111 114 """Exception raised when a remote repo reports failure"""
112 115 __bytes__ = _tobytes
113 116
114 117 class ParseError(Hint, Exception):
115 118 """Raised when parsing config files and {rev,file}sets (msg[, pos])"""
116 119 __bytes__ = _tobytes
117 120
118 121 class PatchError(Exception):
119 122 __bytes__ = _tobytes
120 123
121 124 class UnknownIdentifier(ParseError):
122 125 """Exception raised when a {rev,file}set references an unknown identifier"""
123 126
124 127 def __init__(self, function, symbols):
125 128 from .i18n import _
126 129 ParseError.__init__(self, _("unknown identifier: %s") % function)
127 130 self.function = function
128 131 self.symbols = symbols
129 132
130 133 class RepoError(Hint, Exception):
131 134 __bytes__ = _tobytes
132 135
133 136 class RepoLookupError(RepoError):
134 137 pass
135 138
136 139 class FilteredRepoLookupError(RepoLookupError):
137 140 pass
138 141
139 142 class CapabilityError(RepoError):
140 143 pass
141 144
142 145 class RequirementError(RepoError):
143 146 """Exception raised if .hg/requires has an unknown entry."""
144 147
145 148 class StdioError(IOError):
146 149 """Raised if I/O to stdout or stderr fails"""
147 150
148 151 def __init__(self, err):
149 152 IOError.__init__(self, err.errno, err.strerror)
150 153
151 154 # no __bytes__() because error message is derived from the standard IOError
152 155
153 156 class UnsupportedMergeRecords(Abort):
154 157 def __init__(self, recordtypes):
155 158 from .i18n import _
156 159 self.recordtypes = sorted(recordtypes)
157 160 s = ' '.join(self.recordtypes)
158 161 Abort.__init__(
159 162 self, _('unsupported merge state records: %s') % s,
160 163 hint=_('see https://mercurial-scm.org/wiki/MergeStateRecords for '
161 164 'more information'))
162 165
163 166 class UnknownVersion(Abort):
164 167 """generic exception for aborting from an encounter with an unknown version
165 168 """
166 169
167 170 def __init__(self, msg, hint=None, version=None):
168 171 self.version = version
169 172 super(UnknownVersion, self).__init__(msg, hint=hint)
170 173
171 174 class LockError(IOError):
172 175 def __init__(self, errno, strerror, filename, desc):
173 176 IOError.__init__(self, errno, strerror, filename)
174 177 self.desc = desc
175 178
176 179 # no __bytes__() because error message is derived from the standard IOError
177 180
178 181 class LockHeld(LockError):
179 182 def __init__(self, errno, filename, desc, locker):
180 183 LockError.__init__(self, errno, 'Lock held', filename, desc)
181 184 self.locker = locker
182 185
183 186 class LockUnavailable(LockError):
184 187 pass
185 188
186 189 # LockError is for errors while acquiring the lock -- this is unrelated
187 190 class LockInheritanceContractViolation(RuntimeError):
188 191 __bytes__ = _tobytes
189 192
190 193 class ResponseError(Exception):
191 194 """Raised to print an error with part of output and exit."""
192 195 __bytes__ = _tobytes
193 196
194 197 class UnknownCommand(Exception):
195 198 """Exception raised if command is not in the command table."""
196 199 __bytes__ = _tobytes
197 200
198 201 class AmbiguousCommand(Exception):
199 202 """Exception raised if command shortcut matches more than one command."""
200 203 __bytes__ = _tobytes
201 204
202 205 # derived from KeyboardInterrupt to simplify some breakout code
203 206 class SignalInterrupt(KeyboardInterrupt):
204 207 """Exception raised on SIGTERM and SIGHUP."""
205 208
206 209 class SignatureError(Exception):
207 210 __bytes__ = _tobytes
208 211
209 212 class PushRaced(RuntimeError):
210 213 """An exception raised during unbundling that indicate a push race"""
211 214 __bytes__ = _tobytes
212 215
213 216 class ProgrammingError(Hint, RuntimeError):
214 217 """Raised if a mercurial (core or extension) developer made a mistake"""
215 218 __bytes__ = _tobytes
216 219
217 220 class WdirUnsupported(Exception):
218 221 """An exception which is raised when 'wdir()' is not supported"""
219 222 __bytes__ = _tobytes
220 223
221 224 # bundle2 related errors
222 225 class BundleValueError(ValueError):
223 226 """error raised when bundle2 cannot be processed"""
224 227 __bytes__ = _tobytes
225 228
226 229 class BundleUnknownFeatureError(BundleValueError):
227 230 def __init__(self, parttype=None, params=(), values=()):
228 231 self.parttype = parttype
229 232 self.params = params
230 233 self.values = values
231 234 if self.parttype is None:
232 235 msg = 'Stream Parameter'
233 236 else:
234 237 msg = parttype
235 238 entries = self.params
236 239 if self.params and self.values:
237 240 assert len(self.params) == len(self.values)
238 241 entries = []
239 242 for idx, par in enumerate(self.params):
240 243 val = self.values[idx]
241 244 if val is None:
242 245 entries.append(val)
243 246 else:
244 247 entries.append("%s=%r" % (par, pycompat.maybebytestr(val)))
245 248 if entries:
246 249 msg = '%s - %s' % (msg, ', '.join(entries))
247 250 ValueError.__init__(self, msg)
248 251
249 252 class ReadOnlyPartError(RuntimeError):
250 253 """error raised when code tries to alter a part being generated"""
251 254 __bytes__ = _tobytes
252 255
253 256 class PushkeyFailed(Abort):
254 257 """error raised when a pushkey part failed to update a value"""
255 258
256 259 def __init__(self, partid, namespace=None, key=None, new=None, old=None,
257 260 ret=None):
258 261 self.partid = partid
259 262 self.namespace = namespace
260 263 self.key = key
261 264 self.new = new
262 265 self.old = old
263 266 self.ret = ret
264 267 # no i18n expected to be processed into a better message
265 268 Abort.__init__(self, 'failed to update value for "%s/%s"'
266 269 % (namespace, key))
267 270
268 271 class CensoredNodeError(RevlogError):
269 272 """error raised when content verification fails on a censored node
270 273
271 274 Also contains the tombstone data substituted for the uncensored data.
272 275 """
273 276
274 277 def __init__(self, filename, node, tombstone):
275 278 from .node import short
276 279 RevlogError.__init__(self, '%s:%s' % (filename, short(node)))
277 280 self.tombstone = tombstone
278 281
279 282 class CensoredBaseError(RevlogError):
280 283 """error raised when a delta is rejected because its base is censored
281 284
282 285 A delta based on a censored revision must be formed as single patch
283 286 operation which replaces the entire base with new content. This ensures
284 287 the delta may be applied by clones which have not censored the base.
285 288 """
286 289
287 290 class InvalidBundleSpecification(Exception):
288 291 """error raised when a bundle specification is invalid.
289 292
290 293 This is used for syntax errors as opposed to support errors.
291 294 """
292 295 __bytes__ = _tobytes
293 296
294 297 class UnsupportedBundleSpecification(Exception):
295 298 """error raised when a bundle specification is not supported."""
296 299 __bytes__ = _tobytes
297 300
298 301 class CorruptedState(Exception):
299 302 """error raised when a command is not able to read its state from file"""
300 303 __bytes__ = _tobytes
301 304
302 305 class PeerTransportError(Abort):
303 306 """Transport-level I/O error when communicating with a peer repo."""
304 307
305 308 class InMemoryMergeConflictsError(Exception):
306 309 """Exception raised when merge conflicts arose during an in-memory merge."""
307 310 __bytes__ = _tobytes
@@ -1,2401 +1,2402 b''
1 1 # localrepo.py - read/write repository class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import hashlib
12 12 import os
13 13 import random
14 14 import sys
15 15 import time
16 16 import weakref
17 17
18 18 from .i18n import _
19 19 from .node import (
20 20 hex,
21 21 nullid,
22 22 short,
23 23 )
24 24 from . import (
25 25 bookmarks,
26 26 branchmap,
27 27 bundle2,
28 28 changegroup,
29 29 changelog,
30 30 color,
31 31 context,
32 32 dirstate,
33 33 dirstateguard,
34 34 discovery,
35 35 encoding,
36 36 error,
37 37 exchange,
38 38 extensions,
39 39 filelog,
40 40 hook,
41 41 lock as lockmod,
42 42 manifest,
43 43 match as matchmod,
44 44 merge as mergemod,
45 45 mergeutil,
46 46 namespaces,
47 47 narrowspec,
48 48 obsolete,
49 49 pathutil,
50 50 phases,
51 51 pushkey,
52 52 pycompat,
53 53 repository,
54 54 repoview,
55 55 revset,
56 56 revsetlang,
57 57 scmutil,
58 58 sparse,
59 59 store,
60 60 subrepoutil,
61 61 tags as tagsmod,
62 62 transaction,
63 63 txnutil,
64 64 util,
65 65 vfs as vfsmod,
66 66 )
67 67 from .utils import (
68 68 interfaceutil,
69 69 procutil,
70 70 stringutil,
71 71 )
72 72
73 73 release = lockmod.release
74 74 urlerr = util.urlerr
75 75 urlreq = util.urlreq
76 76
77 77 # set of (path, vfs-location) tuples. vfs-location is:
78 78 # - 'plain for vfs relative paths
79 79 # - '' for svfs relative paths
80 80 _cachedfiles = set()
81 81
82 82 class _basefilecache(scmutil.filecache):
83 83 """All filecache usage on repo are done for logic that should be unfiltered
84 84 """
85 85 def __get__(self, repo, type=None):
86 86 if repo is None:
87 87 return self
88 88 return super(_basefilecache, self).__get__(repo.unfiltered(), type)
89 89 def __set__(self, repo, value):
90 90 return super(_basefilecache, self).__set__(repo.unfiltered(), value)
91 91 def __delete__(self, repo):
92 92 return super(_basefilecache, self).__delete__(repo.unfiltered())
93 93
94 94 class repofilecache(_basefilecache):
95 95 """filecache for files in .hg but outside of .hg/store"""
96 96 def __init__(self, *paths):
97 97 super(repofilecache, self).__init__(*paths)
98 98 for path in paths:
99 99 _cachedfiles.add((path, 'plain'))
100 100
101 101 def join(self, obj, fname):
102 102 return obj.vfs.join(fname)
103 103
104 104 class storecache(_basefilecache):
105 105 """filecache for files in the store"""
106 106 def __init__(self, *paths):
107 107 super(storecache, self).__init__(*paths)
108 108 for path in paths:
109 109 _cachedfiles.add((path, ''))
110 110
111 111 def join(self, obj, fname):
112 112 return obj.sjoin(fname)
113 113
114 114 def isfilecached(repo, name):
115 115 """check if a repo has already cached "name" filecache-ed property
116 116
117 117 This returns (cachedobj-or-None, iscached) tuple.
118 118 """
119 119 cacheentry = repo.unfiltered()._filecache.get(name, None)
120 120 if not cacheentry:
121 121 return None, False
122 122 return cacheentry.obj, True
123 123
124 124 class unfilteredpropertycache(util.propertycache):
125 125 """propertycache that apply to unfiltered repo only"""
126 126
127 127 def __get__(self, repo, type=None):
128 128 unfi = repo.unfiltered()
129 129 if unfi is repo:
130 130 return super(unfilteredpropertycache, self).__get__(unfi)
131 131 return getattr(unfi, self.name)
132 132
133 133 class filteredpropertycache(util.propertycache):
134 134 """propertycache that must take filtering in account"""
135 135
136 136 def cachevalue(self, obj, value):
137 137 object.__setattr__(obj, self.name, value)
138 138
139 139
140 140 def hasunfilteredcache(repo, name):
141 141 """check if a repo has an unfilteredpropertycache value for <name>"""
142 142 return name in vars(repo.unfiltered())
143 143
144 144 def unfilteredmethod(orig):
145 145 """decorate method that always need to be run on unfiltered version"""
146 146 def wrapper(repo, *args, **kwargs):
147 147 return orig(repo.unfiltered(), *args, **kwargs)
148 148 return wrapper
149 149
150 150 moderncaps = {'lookup', 'branchmap', 'pushkey', 'known', 'getbundle',
151 151 'unbundle'}
152 152 legacycaps = moderncaps.union({'changegroupsubset'})
153 153
154 154 @interfaceutil.implementer(repository.ipeercommandexecutor)
155 155 class localcommandexecutor(object):
156 156 def __init__(self, peer):
157 157 self._peer = peer
158 158 self._sent = False
159 159 self._closed = False
160 160
161 161 def __enter__(self):
162 162 return self
163 163
164 164 def __exit__(self, exctype, excvalue, exctb):
165 165 self.close()
166 166
167 167 def callcommand(self, command, args):
168 168 if self._sent:
169 169 raise error.ProgrammingError('callcommand() cannot be used after '
170 170 'sendcommands()')
171 171
172 172 if self._closed:
173 173 raise error.ProgrammingError('callcommand() cannot be used after '
174 174 'close()')
175 175
176 176 # We don't need to support anything fancy. Just call the named
177 177 # method on the peer and return a resolved future.
178 178 fn = getattr(self._peer, pycompat.sysstr(command))
179 179
180 180 f = pycompat.futures.Future()
181 181
182 182 try:
183 183 result = fn(**pycompat.strkwargs(args))
184 184 except Exception:
185 185 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
186 186 else:
187 187 f.set_result(result)
188 188
189 189 return f
190 190
191 191 def sendcommands(self):
192 192 self._sent = True
193 193
194 194 def close(self):
195 195 self._closed = True
196 196
197 197 @interfaceutil.implementer(repository.ipeercommands)
198 198 class localpeer(repository.peer):
199 199 '''peer for a local repo; reflects only the most recent API'''
200 200
201 201 def __init__(self, repo, caps=None):
202 202 super(localpeer, self).__init__()
203 203
204 204 if caps is None:
205 205 caps = moderncaps.copy()
206 206 self._repo = repo.filtered('served')
207 207 self.ui = repo.ui
208 208 self._caps = repo._restrictcapabilities(caps)
209 209
210 210 # Begin of _basepeer interface.
211 211
212 212 def url(self):
213 213 return self._repo.url()
214 214
215 215 def local(self):
216 216 return self._repo
217 217
218 218 def peer(self):
219 219 return self
220 220
221 221 def canpush(self):
222 222 return True
223 223
224 224 def close(self):
225 225 self._repo.close()
226 226
227 227 # End of _basepeer interface.
228 228
229 229 # Begin of _basewirecommands interface.
230 230
231 231 def branchmap(self):
232 232 return self._repo.branchmap()
233 233
234 234 def capabilities(self):
235 235 return self._caps
236 236
237 237 def clonebundles(self):
238 238 return self._repo.tryread('clonebundles.manifest')
239 239
240 240 def debugwireargs(self, one, two, three=None, four=None, five=None):
241 241 """Used to test argument passing over the wire"""
242 242 return "%s %s %s %s %s" % (one, two, pycompat.bytestr(three),
243 243 pycompat.bytestr(four),
244 244 pycompat.bytestr(five))
245 245
246 246 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
247 247 **kwargs):
248 248 chunks = exchange.getbundlechunks(self._repo, source, heads=heads,
249 249 common=common, bundlecaps=bundlecaps,
250 250 **kwargs)[1]
251 251 cb = util.chunkbuffer(chunks)
252 252
253 253 if exchange.bundle2requested(bundlecaps):
254 254 # When requesting a bundle2, getbundle returns a stream to make the
255 255 # wire level function happier. We need to build a proper object
256 256 # from it in local peer.
257 257 return bundle2.getunbundler(self.ui, cb)
258 258 else:
259 259 return changegroup.getunbundler('01', cb, None)
260 260
261 261 def heads(self):
262 262 return self._repo.heads()
263 263
264 264 def known(self, nodes):
265 265 return self._repo.known(nodes)
266 266
267 267 def listkeys(self, namespace):
268 268 return self._repo.listkeys(namespace)
269 269
270 270 def lookup(self, key):
271 271 return self._repo.lookup(key)
272 272
273 273 def pushkey(self, namespace, key, old, new):
274 274 return self._repo.pushkey(namespace, key, old, new)
275 275
276 276 def stream_out(self):
277 277 raise error.Abort(_('cannot perform stream clone against local '
278 278 'peer'))
279 279
280 280 def unbundle(self, bundle, heads, url):
281 281 """apply a bundle on a repo
282 282
283 283 This function handles the repo locking itself."""
284 284 try:
285 285 try:
286 286 bundle = exchange.readbundle(self.ui, bundle, None)
287 287 ret = exchange.unbundle(self._repo, bundle, heads, 'push', url)
288 288 if util.safehasattr(ret, 'getchunks'):
289 289 # This is a bundle20 object, turn it into an unbundler.
290 290 # This little dance should be dropped eventually when the
291 291 # API is finally improved.
292 292 stream = util.chunkbuffer(ret.getchunks())
293 293 ret = bundle2.getunbundler(self.ui, stream)
294 294 return ret
295 295 except Exception as exc:
296 296 # If the exception contains output salvaged from a bundle2
297 297 # reply, we need to make sure it is printed before continuing
298 298 # to fail. So we build a bundle2 with such output and consume
299 299 # it directly.
300 300 #
301 301 # This is not very elegant but allows a "simple" solution for
302 302 # issue4594
303 303 output = getattr(exc, '_bundle2salvagedoutput', ())
304 304 if output:
305 305 bundler = bundle2.bundle20(self._repo.ui)
306 306 for out in output:
307 307 bundler.addpart(out)
308 308 stream = util.chunkbuffer(bundler.getchunks())
309 309 b = bundle2.getunbundler(self.ui, stream)
310 310 bundle2.processbundle(self._repo, b)
311 311 raise
312 312 except error.PushRaced as exc:
313 313 raise error.ResponseError(_('push failed:'),
314 314 stringutil.forcebytestr(exc))
315 315
316 316 # End of _basewirecommands interface.
317 317
318 318 # Begin of peer interface.
319 319
320 320 def commandexecutor(self):
321 321 return localcommandexecutor(self)
322 322
323 323 # End of peer interface.
324 324
325 325 @interfaceutil.implementer(repository.ipeerlegacycommands)
326 326 class locallegacypeer(localpeer):
327 327 '''peer extension which implements legacy methods too; used for tests with
328 328 restricted capabilities'''
329 329
330 330 def __init__(self, repo):
331 331 super(locallegacypeer, self).__init__(repo, caps=legacycaps)
332 332
333 333 # Begin of baselegacywirecommands interface.
334 334
335 335 def between(self, pairs):
336 336 return self._repo.between(pairs)
337 337
338 338 def branches(self, nodes):
339 339 return self._repo.branches(nodes)
340 340
341 341 def changegroup(self, nodes, source):
342 342 outgoing = discovery.outgoing(self._repo, missingroots=nodes,
343 343 missingheads=self._repo.heads())
344 344 return changegroup.makechangegroup(self._repo, outgoing, '01', source)
345 345
346 346 def changegroupsubset(self, bases, heads, source):
347 347 outgoing = discovery.outgoing(self._repo, missingroots=bases,
348 348 missingheads=heads)
349 349 return changegroup.makechangegroup(self._repo, outgoing, '01', source)
350 350
351 351 # End of baselegacywirecommands interface.
352 352
353 353 # Increment the sub-version when the revlog v2 format changes to lock out old
354 354 # clients.
355 355 REVLOGV2_REQUIREMENT = 'exp-revlogv2.0'
356 356
357 357 # A repository with the sparserevlog feature will have delta chains that
358 358 # can spread over a larger span. Sparse reading cuts these large spans into
359 359 # pieces, so that each piece isn't too big.
360 360 # Without the sparserevlog capability, reading from the repository could use
361 361 # huge amounts of memory, because the whole span would be read at once,
362 362 # including all the intermediate revisions that aren't pertinent for the chain.
363 363 # This is why once a repository has enabled sparse-read, it becomes required.
364 364 SPARSEREVLOG_REQUIREMENT = 'sparserevlog'
365 365
366 366 # Functions receiving (ui, features) that extensions can register to impact
367 367 # the ability to load repositories with custom requirements. Only
368 368 # functions defined in loaded extensions are called.
369 369 #
370 370 # The function receives a set of requirement strings that the repository
371 371 # is capable of opening. Functions will typically add elements to the
372 372 # set to reflect that the extension knows how to handle that requirements.
373 373 featuresetupfuncs = set()
374 374
375 375 @interfaceutil.implementer(repository.completelocalrepository)
376 376 class localrepository(object):
377 377
378 378 # obsolete experimental requirements:
379 379 # - manifestv2: An experimental new manifest format that allowed
380 380 # for stem compression of long paths. Experiment ended up not
381 381 # being successful (repository sizes went up due to worse delta
382 382 # chains), and the code was deleted in 4.6.
383 383 supportedformats = {
384 384 'revlogv1',
385 385 'generaldelta',
386 386 'treemanifest',
387 387 REVLOGV2_REQUIREMENT,
388 388 SPARSEREVLOG_REQUIREMENT,
389 389 }
390 390 _basesupported = supportedformats | {
391 391 'store',
392 392 'fncache',
393 393 'shared',
394 394 'relshared',
395 395 'dotencode',
396 396 'exp-sparse',
397 397 }
398 398 openerreqs = {
399 399 'revlogv1',
400 400 'generaldelta',
401 401 'treemanifest',
402 402 }
403 403
404 404 # list of prefix for file which can be written without 'wlock'
405 405 # Extensions should extend this list when needed
406 406 _wlockfreeprefix = {
407 407 # We migh consider requiring 'wlock' for the next
408 408 # two, but pretty much all the existing code assume
409 409 # wlock is not needed so we keep them excluded for
410 410 # now.
411 411 'hgrc',
412 412 'requires',
413 413 # XXX cache is a complicatged business someone
414 414 # should investigate this in depth at some point
415 415 'cache/',
416 416 # XXX shouldn't be dirstate covered by the wlock?
417 417 'dirstate',
418 418 # XXX bisect was still a bit too messy at the time
419 419 # this changeset was introduced. Someone should fix
420 420 # the remainig bit and drop this line
421 421 'bisect.state',
422 422 }
423 423
424 424 def __init__(self, baseui, path, create=False, intents=None):
425 425 self.requirements = set()
426 426 self.filtername = None
427 427 # wvfs: rooted at the repository root, used to access the working copy
428 428 self.wvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
429 429 # vfs: rooted at .hg, used to access repo files outside of .hg/store
430 430 self.vfs = None
431 431 # svfs: usually rooted at .hg/store, used to access repository history
432 432 # If this is a shared repository, this vfs may point to another
433 433 # repository's .hg/store directory.
434 434 self.svfs = None
435 435 self.root = self.wvfs.base
436 436 self.path = self.wvfs.join(".hg")
437 437 self.origroot = path
438 438 # This is only used by context.workingctx.match in order to
439 439 # detect files in subrepos.
440 440 self.auditor = pathutil.pathauditor(
441 441 self.root, callback=self._checknested)
442 442 # This is only used by context.basectx.match in order to detect
443 443 # files in subrepos.
444 444 self.nofsauditor = pathutil.pathauditor(
445 445 self.root, callback=self._checknested, realfs=False, cached=True)
446 446 self.baseui = baseui
447 447 self.ui = baseui.copy()
448 448 self.ui.copy = baseui.copy # prevent copying repo configuration
449 449 self.vfs = vfsmod.vfs(self.path, cacheaudited=True)
450 450 if (self.ui.configbool('devel', 'all-warnings') or
451 451 self.ui.configbool('devel', 'check-locks')):
452 452 self.vfs.audit = self._getvfsward(self.vfs.audit)
453 453 # A list of callback to shape the phase if no data were found.
454 454 # Callback are in the form: func(repo, roots) --> processed root.
455 455 # This list it to be filled by extension during repo setup
456 456 self._phasedefaults = []
457 457 try:
458 458 self.ui.readconfig(self.vfs.join("hgrc"), self.root)
459 459 self._loadextensions()
460 460 except IOError:
461 461 pass
462 462
463 463 if featuresetupfuncs:
464 464 self.supported = set(self._basesupported) # use private copy
465 465 extmods = set(m.__name__ for n, m
466 466 in extensions.extensions(self.ui))
467 467 for setupfunc in featuresetupfuncs:
468 468 if setupfunc.__module__ in extmods:
469 469 setupfunc(self.ui, self.supported)
470 470 else:
471 471 self.supported = self._basesupported
472 472 color.setup(self.ui)
473 473
474 474 # Add compression engines.
475 475 for name in util.compengines:
476 476 engine = util.compengines[name]
477 477 if engine.revlogheader():
478 478 self.supported.add('exp-compression-%s' % name)
479 479
480 480 if not self.vfs.isdir():
481 481 if create:
482 482 self.requirements = newreporequirements(self)
483 483
484 484 if not self.wvfs.exists():
485 485 self.wvfs.makedirs()
486 486 self.vfs.makedir(notindexed=True)
487 487
488 488 if 'store' in self.requirements:
489 489 self.vfs.mkdir("store")
490 490
491 491 # create an invalid changelog
492 492 self.vfs.append(
493 493 "00changelog.i",
494 494 '\0\0\0\2' # represents revlogv2
495 495 ' dummy changelog to prevent using the old repo layout'
496 496 )
497 497 else:
498 498 raise error.RepoError(_("repository %s not found") % path)
499 499 elif create:
500 500 raise error.RepoError(_("repository %s already exists") % path)
501 501 else:
502 502 try:
503 503 self.requirements = scmutil.readrequires(
504 504 self.vfs, self.supported)
505 505 except IOError as inst:
506 506 if inst.errno != errno.ENOENT:
507 507 raise
508 508
509 509 cachepath = self.vfs.join('cache')
510 510 self.sharedpath = self.path
511 511 try:
512 512 sharedpath = self.vfs.read("sharedpath").rstrip('\n')
513 513 if 'relshared' in self.requirements:
514 514 sharedpath = self.vfs.join(sharedpath)
515 515 vfs = vfsmod.vfs(sharedpath, realpath=True)
516 516 cachepath = vfs.join('cache')
517 517 s = vfs.base
518 518 if not vfs.exists():
519 519 raise error.RepoError(
520 520 _('.hg/sharedpath points to nonexistent directory %s') % s)
521 521 self.sharedpath = s
522 522 except IOError as inst:
523 523 if inst.errno != errno.ENOENT:
524 524 raise
525 525
526 526 if 'exp-sparse' in self.requirements and not sparse.enabled:
527 527 raise error.RepoError(_('repository is using sparse feature but '
528 528 'sparse is not enabled; enable the '
529 529 '"sparse" extensions to access'))
530 530
531 531 self.store = store.store(
532 532 self.requirements, self.sharedpath,
533 533 lambda base: vfsmod.vfs(base, cacheaudited=True))
534 534 self.spath = self.store.path
535 535 self.svfs = self.store.vfs
536 536 self.sjoin = self.store.join
537 537 self.vfs.createmode = self.store.createmode
538 538 self.cachevfs = vfsmod.vfs(cachepath, cacheaudited=True)
539 539 self.cachevfs.createmode = self.store.createmode
540 540 if (self.ui.configbool('devel', 'all-warnings') or
541 541 self.ui.configbool('devel', 'check-locks')):
542 542 if util.safehasattr(self.svfs, 'vfs'): # this is filtervfs
543 543 self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit)
544 544 else: # standard vfs
545 545 self.svfs.audit = self._getsvfsward(self.svfs.audit)
546 546 self._applyopenerreqs()
547 547 if create:
548 548 self._writerequirements()
549 549
550 550 self._dirstatevalidatewarned = False
551 551
552 552 self._branchcaches = {}
553 553 self._revbranchcache = None
554 554 self._filterpats = {}
555 555 self._datafilters = {}
556 556 self._transref = self._lockref = self._wlockref = None
557 557
558 558 # A cache for various files under .hg/ that tracks file changes,
559 559 # (used by the filecache decorator)
560 560 #
561 561 # Maps a property name to its util.filecacheentry
562 562 self._filecache = {}
563 563
564 564 # hold sets of revision to be filtered
565 565 # should be cleared when something might have changed the filter value:
566 566 # - new changesets,
567 567 # - phase change,
568 568 # - new obsolescence marker,
569 569 # - working directory parent change,
570 570 # - bookmark changes
571 571 self.filteredrevcache = {}
572 572
573 573 # post-dirstate-status hooks
574 574 self._postdsstatus = []
575 575
576 576 # generic mapping between names and nodes
577 577 self.names = namespaces.namespaces()
578 578
579 579 # Key to signature value.
580 580 self._sparsesignaturecache = {}
581 581 # Signature to cached matcher instance.
582 582 self._sparsematchercache = {}
583 583
584 584 def _getvfsward(self, origfunc):
585 585 """build a ward for self.vfs"""
586 586 rref = weakref.ref(self)
587 587 def checkvfs(path, mode=None):
588 588 ret = origfunc(path, mode=mode)
589 589 repo = rref()
590 590 if (repo is None
591 591 or not util.safehasattr(repo, '_wlockref')
592 592 or not util.safehasattr(repo, '_lockref')):
593 593 return
594 594 if mode in (None, 'r', 'rb'):
595 595 return
596 596 if path.startswith(repo.path):
597 597 # truncate name relative to the repository (.hg)
598 598 path = path[len(repo.path) + 1:]
599 599 if path.startswith('cache/'):
600 600 msg = 'accessing cache with vfs instead of cachevfs: "%s"'
601 601 repo.ui.develwarn(msg % path, stacklevel=2, config="cache-vfs")
602 602 if path.startswith('journal.'):
603 603 # journal is covered by 'lock'
604 604 if repo._currentlock(repo._lockref) is None:
605 605 repo.ui.develwarn('write with no lock: "%s"' % path,
606 606 stacklevel=2, config='check-locks')
607 607 elif repo._currentlock(repo._wlockref) is None:
608 608 # rest of vfs files are covered by 'wlock'
609 609 #
610 610 # exclude special files
611 611 for prefix in self._wlockfreeprefix:
612 612 if path.startswith(prefix):
613 613 return
614 614 repo.ui.develwarn('write with no wlock: "%s"' % path,
615 615 stacklevel=2, config='check-locks')
616 616 return ret
617 617 return checkvfs
618 618
619 619 def _getsvfsward(self, origfunc):
620 620 """build a ward for self.svfs"""
621 621 rref = weakref.ref(self)
622 622 def checksvfs(path, mode=None):
623 623 ret = origfunc(path, mode=mode)
624 624 repo = rref()
625 625 if repo is None or not util.safehasattr(repo, '_lockref'):
626 626 return
627 627 if mode in (None, 'r', 'rb'):
628 628 return
629 629 if path.startswith(repo.sharedpath):
630 630 # truncate name relative to the repository (.hg)
631 631 path = path[len(repo.sharedpath) + 1:]
632 632 if repo._currentlock(repo._lockref) is None:
633 633 repo.ui.develwarn('write with no lock: "%s"' % path,
634 634 stacklevel=3)
635 635 return ret
636 636 return checksvfs
637 637
638 638 def close(self):
639 639 self._writecaches()
640 640
641 641 def _loadextensions(self):
642 642 extensions.loadall(self.ui)
643 643
644 644 def _writecaches(self):
645 645 if self._revbranchcache:
646 646 self._revbranchcache.write()
647 647
648 648 def _restrictcapabilities(self, caps):
649 649 if self.ui.configbool('experimental', 'bundle2-advertise'):
650 650 caps = set(caps)
651 651 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self,
652 652 role='client'))
653 653 caps.add('bundle2=' + urlreq.quote(capsblob))
654 654 return caps
655 655
656 656 def _applyopenerreqs(self):
657 657 self.svfs.options = dict((r, 1) for r in self.requirements
658 658 if r in self.openerreqs)
659 659 # experimental config: format.chunkcachesize
660 660 chunkcachesize = self.ui.configint('format', 'chunkcachesize')
661 661 if chunkcachesize is not None:
662 662 self.svfs.options['chunkcachesize'] = chunkcachesize
663 663 # experimental config: format.maxchainlen
664 664 maxchainlen = self.ui.configint('format', 'maxchainlen')
665 665 if maxchainlen is not None:
666 666 self.svfs.options['maxchainlen'] = maxchainlen
667 667 # experimental config: format.manifestcachesize
668 668 manifestcachesize = self.ui.configint('format', 'manifestcachesize')
669 669 if manifestcachesize is not None:
670 670 self.svfs.options['manifestcachesize'] = manifestcachesize
671 671 deltabothparents = self.ui.configbool('storage',
672 672 'revlog.optimize-delta-parent-choice')
673 673 self.svfs.options['deltabothparents'] = deltabothparents
674 674 self.svfs.options['lazydeltabase'] = not scmutil.gddeltaconfig(self.ui)
675 675 chainspan = self.ui.configbytes('experimental', 'maxdeltachainspan')
676 676 if 0 <= chainspan:
677 677 self.svfs.options['maxdeltachainspan'] = chainspan
678 678 mmapindexthreshold = self.ui.configbytes('experimental',
679 679 'mmapindexthreshold')
680 680 if mmapindexthreshold is not None:
681 681 self.svfs.options['mmapindexthreshold'] = mmapindexthreshold
682 682 withsparseread = self.ui.configbool('experimental', 'sparse-read')
683 683 srdensitythres = float(self.ui.config('experimental',
684 684 'sparse-read.density-threshold'))
685 685 srmingapsize = self.ui.configbytes('experimental',
686 686 'sparse-read.min-gap-size')
687 687 self.svfs.options['with-sparse-read'] = withsparseread
688 688 self.svfs.options['sparse-read-density-threshold'] = srdensitythres
689 689 self.svfs.options['sparse-read-min-gap-size'] = srmingapsize
690 690 sparserevlog = SPARSEREVLOG_REQUIREMENT in self.requirements
691 691 self.svfs.options['sparse-revlog'] = sparserevlog
692 692 if sparserevlog:
693 693 self.svfs.options['generaldelta'] = True
694 694
695 695 for r in self.requirements:
696 696 if r.startswith('exp-compression-'):
697 697 self.svfs.options['compengine'] = r[len('exp-compression-'):]
698 698
699 699 # TODO move "revlogv2" to openerreqs once finalized.
700 700 if REVLOGV2_REQUIREMENT in self.requirements:
701 701 self.svfs.options['revlogv2'] = True
702 702
703 703 def _writerequirements(self):
704 704 scmutil.writerequires(self.vfs, self.requirements)
705 705
706 706 def _checknested(self, path):
707 707 """Determine if path is a legal nested repository."""
708 708 if not path.startswith(self.root):
709 709 return False
710 710 subpath = path[len(self.root) + 1:]
711 711 normsubpath = util.pconvert(subpath)
712 712
713 713 # XXX: Checking against the current working copy is wrong in
714 714 # the sense that it can reject things like
715 715 #
716 716 # $ hg cat -r 10 sub/x.txt
717 717 #
718 718 # if sub/ is no longer a subrepository in the working copy
719 719 # parent revision.
720 720 #
721 721 # However, it can of course also allow things that would have
722 722 # been rejected before, such as the above cat command if sub/
723 723 # is a subrepository now, but was a normal directory before.
724 724 # The old path auditor would have rejected by mistake since it
725 725 # panics when it sees sub/.hg/.
726 726 #
727 727 # All in all, checking against the working copy seems sensible
728 728 # since we want to prevent access to nested repositories on
729 729 # the filesystem *now*.
730 730 ctx = self[None]
731 731 parts = util.splitpath(subpath)
732 732 while parts:
733 733 prefix = '/'.join(parts)
734 734 if prefix in ctx.substate:
735 735 if prefix == normsubpath:
736 736 return True
737 737 else:
738 738 sub = ctx.sub(prefix)
739 739 return sub.checknested(subpath[len(prefix) + 1:])
740 740 else:
741 741 parts.pop()
742 742 return False
743 743
744 744 def peer(self):
745 745 return localpeer(self) # not cached to avoid reference cycle
746 746
747 747 def unfiltered(self):
748 748 """Return unfiltered version of the repository
749 749
750 750 Intended to be overwritten by filtered repo."""
751 751 return self
752 752
753 753 def filtered(self, name, visibilityexceptions=None):
754 754 """Return a filtered version of a repository"""
755 755 cls = repoview.newtype(self.unfiltered().__class__)
756 756 return cls(self, name, visibilityexceptions)
757 757
758 758 @repofilecache('bookmarks', 'bookmarks.current')
759 759 def _bookmarks(self):
760 760 return bookmarks.bmstore(self)
761 761
762 762 @property
763 763 def _activebookmark(self):
764 764 return self._bookmarks.active
765 765
766 766 # _phasesets depend on changelog. what we need is to call
767 767 # _phasecache.invalidate() if '00changelog.i' was changed, but it
768 768 # can't be easily expressed in filecache mechanism.
769 769 @storecache('phaseroots', '00changelog.i')
770 770 def _phasecache(self):
771 771 return phases.phasecache(self, self._phasedefaults)
772 772
773 773 @storecache('obsstore')
774 774 def obsstore(self):
775 775 return obsolete.makestore(self.ui, self)
776 776
777 777 @storecache('00changelog.i')
778 778 def changelog(self):
779 779 return changelog.changelog(self.svfs,
780 780 trypending=txnutil.mayhavepending(self.root))
781 781
782 782 def _constructmanifest(self):
783 783 # This is a temporary function while we migrate from manifest to
784 784 # manifestlog. It allows bundlerepo and unionrepo to intercept the
785 785 # manifest creation.
786 786 return manifest.manifestrevlog(self.svfs)
787 787
788 788 @storecache('00manifest.i')
789 789 def manifestlog(self):
790 790 return manifest.manifestlog(self.svfs, self)
791 791
792 792 @repofilecache('dirstate')
793 793 def dirstate(self):
794 794 return self._makedirstate()
795 795
796 796 def _makedirstate(self):
797 797 """Extension point for wrapping the dirstate per-repo."""
798 798 sparsematchfn = lambda: sparse.matcher(self)
799 799
800 800 return dirstate.dirstate(self.vfs, self.ui, self.root,
801 801 self._dirstatevalidate, sparsematchfn)
802 802
803 803 def _dirstatevalidate(self, node):
804 804 try:
805 805 self.changelog.rev(node)
806 806 return node
807 807 except error.LookupError:
808 808 if not self._dirstatevalidatewarned:
809 809 self._dirstatevalidatewarned = True
810 810 self.ui.warn(_("warning: ignoring unknown"
811 811 " working parent %s!\n") % short(node))
812 812 return nullid
813 813
814 814 @repofilecache(narrowspec.FILENAME)
815 815 def narrowpats(self):
816 816 """matcher patterns for this repository's narrowspec
817 817
818 818 A tuple of (includes, excludes).
819 819 """
820 820 source = self
821 821 if self.shared():
822 822 from . import hg
823 823 source = hg.sharedreposource(self)
824 824 return narrowspec.load(source)
825 825
826 826 @repofilecache(narrowspec.FILENAME)
827 827 def _narrowmatch(self):
828 828 if repository.NARROW_REQUIREMENT not in self.requirements:
829 829 return matchmod.always(self.root, '')
830 830 include, exclude = self.narrowpats
831 831 return narrowspec.match(self.root, include=include, exclude=exclude)
832 832
833 833 # TODO(martinvonz): make this property-like instead?
834 834 def narrowmatch(self):
835 835 return self._narrowmatch
836 836
837 837 def setnarrowpats(self, newincludes, newexcludes):
838 838 target = self
839 839 if self.shared():
840 840 from . import hg
841 841 target = hg.sharedreposource(self)
842 842 narrowspec.save(target, newincludes, newexcludes)
843 843 self.invalidate(clearfilecache=True)
844 844
845 845 def __getitem__(self, changeid):
846 846 if changeid is None:
847 847 return context.workingctx(self)
848 848 if isinstance(changeid, context.basectx):
849 849 return changeid
850 850 if isinstance(changeid, slice):
851 851 # wdirrev isn't contiguous so the slice shouldn't include it
852 852 return [context.changectx(self, i)
853 853 for i in pycompat.xrange(*changeid.indices(len(self)))
854 854 if i not in self.changelog.filteredrevs]
855 855 try:
856 856 return context.changectx(self, changeid)
857 857 except error.WdirUnsupported:
858 858 return context.workingctx(self)
859 859
860 860 def __contains__(self, changeid):
861 861 """True if the given changeid exists
862 862
863 error.LookupError is raised if an ambiguous node specified.
863 error.AmbiguousPrefixLookupError is raised if an ambiguous node
864 specified.
864 865 """
865 866 try:
866 867 self[changeid]
867 868 return True
868 869 except error.RepoLookupError:
869 870 return False
870 871
871 872 def __nonzero__(self):
872 873 return True
873 874
874 875 __bool__ = __nonzero__
875 876
876 877 def __len__(self):
877 878 # no need to pay the cost of repoview.changelog
878 879 unfi = self.unfiltered()
879 880 return len(unfi.changelog)
880 881
881 882 def __iter__(self):
882 883 return iter(self.changelog)
883 884
884 885 def revs(self, expr, *args):
885 886 '''Find revisions matching a revset.
886 887
887 888 The revset is specified as a string ``expr`` that may contain
888 889 %-formatting to escape certain types. See ``revsetlang.formatspec``.
889 890
890 891 Revset aliases from the configuration are not expanded. To expand
891 892 user aliases, consider calling ``scmutil.revrange()`` or
892 893 ``repo.anyrevs([expr], user=True)``.
893 894
894 895 Returns a revset.abstractsmartset, which is a list-like interface
895 896 that contains integer revisions.
896 897 '''
897 898 expr = revsetlang.formatspec(expr, *args)
898 899 m = revset.match(None, expr)
899 900 return m(self)
900 901
901 902 def set(self, expr, *args):
902 903 '''Find revisions matching a revset and emit changectx instances.
903 904
904 905 This is a convenience wrapper around ``revs()`` that iterates the
905 906 result and is a generator of changectx instances.
906 907
907 908 Revset aliases from the configuration are not expanded. To expand
908 909 user aliases, consider calling ``scmutil.revrange()``.
909 910 '''
910 911 for r in self.revs(expr, *args):
911 912 yield self[r]
912 913
913 914 def anyrevs(self, specs, user=False, localalias=None):
914 915 '''Find revisions matching one of the given revsets.
915 916
916 917 Revset aliases from the configuration are not expanded by default. To
917 918 expand user aliases, specify ``user=True``. To provide some local
918 919 definitions overriding user aliases, set ``localalias`` to
919 920 ``{name: definitionstring}``.
920 921 '''
921 922 if user:
922 923 m = revset.matchany(self.ui, specs,
923 924 lookup=revset.lookupfn(self),
924 925 localalias=localalias)
925 926 else:
926 927 m = revset.matchany(None, specs, localalias=localalias)
927 928 return m(self)
928 929
929 930 def url(self):
930 931 return 'file:' + self.root
931 932
932 933 def hook(self, name, throw=False, **args):
933 934 """Call a hook, passing this repo instance.
934 935
935 936 This a convenience method to aid invoking hooks. Extensions likely
936 937 won't call this unless they have registered a custom hook or are
937 938 replacing code that is expected to call a hook.
938 939 """
939 940 return hook.hook(self.ui, self, name, throw, **args)
940 941
941 942 @filteredpropertycache
942 943 def _tagscache(self):
943 944 '''Returns a tagscache object that contains various tags related
944 945 caches.'''
945 946
946 947 # This simplifies its cache management by having one decorated
947 948 # function (this one) and the rest simply fetch things from it.
948 949 class tagscache(object):
949 950 def __init__(self):
950 951 # These two define the set of tags for this repository. tags
951 952 # maps tag name to node; tagtypes maps tag name to 'global' or
952 953 # 'local'. (Global tags are defined by .hgtags across all
953 954 # heads, and local tags are defined in .hg/localtags.)
954 955 # They constitute the in-memory cache of tags.
955 956 self.tags = self.tagtypes = None
956 957
957 958 self.nodetagscache = self.tagslist = None
958 959
959 960 cache = tagscache()
960 961 cache.tags, cache.tagtypes = self._findtags()
961 962
962 963 return cache
963 964
964 965 def tags(self):
965 966 '''return a mapping of tag to node'''
966 967 t = {}
967 968 if self.changelog.filteredrevs:
968 969 tags, tt = self._findtags()
969 970 else:
970 971 tags = self._tagscache.tags
971 972 for k, v in tags.iteritems():
972 973 try:
973 974 # ignore tags to unknown nodes
974 975 self.changelog.rev(v)
975 976 t[k] = v
976 977 except (error.LookupError, ValueError):
977 978 pass
978 979 return t
979 980
980 981 def _findtags(self):
981 982 '''Do the hard work of finding tags. Return a pair of dicts
982 983 (tags, tagtypes) where tags maps tag name to node, and tagtypes
983 984 maps tag name to a string like \'global\' or \'local\'.
984 985 Subclasses or extensions are free to add their own tags, but
985 986 should be aware that the returned dicts will be retained for the
986 987 duration of the localrepo object.'''
987 988
988 989 # XXX what tagtype should subclasses/extensions use? Currently
989 990 # mq and bookmarks add tags, but do not set the tagtype at all.
990 991 # Should each extension invent its own tag type? Should there
991 992 # be one tagtype for all such "virtual" tags? Or is the status
992 993 # quo fine?
993 994
994 995
995 996 # map tag name to (node, hist)
996 997 alltags = tagsmod.findglobaltags(self.ui, self)
997 998 # map tag name to tag type
998 999 tagtypes = dict((tag, 'global') for tag in alltags)
999 1000
1000 1001 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
1001 1002
1002 1003 # Build the return dicts. Have to re-encode tag names because
1003 1004 # the tags module always uses UTF-8 (in order not to lose info
1004 1005 # writing to the cache), but the rest of Mercurial wants them in
1005 1006 # local encoding.
1006 1007 tags = {}
1007 1008 for (name, (node, hist)) in alltags.iteritems():
1008 1009 if node != nullid:
1009 1010 tags[encoding.tolocal(name)] = node
1010 1011 tags['tip'] = self.changelog.tip()
1011 1012 tagtypes = dict([(encoding.tolocal(name), value)
1012 1013 for (name, value) in tagtypes.iteritems()])
1013 1014 return (tags, tagtypes)
1014 1015
1015 1016 def tagtype(self, tagname):
1016 1017 '''
1017 1018 return the type of the given tag. result can be:
1018 1019
1019 1020 'local' : a local tag
1020 1021 'global' : a global tag
1021 1022 None : tag does not exist
1022 1023 '''
1023 1024
1024 1025 return self._tagscache.tagtypes.get(tagname)
1025 1026
1026 1027 def tagslist(self):
1027 1028 '''return a list of tags ordered by revision'''
1028 1029 if not self._tagscache.tagslist:
1029 1030 l = []
1030 1031 for t, n in self.tags().iteritems():
1031 1032 l.append((self.changelog.rev(n), t, n))
1032 1033 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
1033 1034
1034 1035 return self._tagscache.tagslist
1035 1036
1036 1037 def nodetags(self, node):
1037 1038 '''return the tags associated with a node'''
1038 1039 if not self._tagscache.nodetagscache:
1039 1040 nodetagscache = {}
1040 1041 for t, n in self._tagscache.tags.iteritems():
1041 1042 nodetagscache.setdefault(n, []).append(t)
1042 1043 for tags in nodetagscache.itervalues():
1043 1044 tags.sort()
1044 1045 self._tagscache.nodetagscache = nodetagscache
1045 1046 return self._tagscache.nodetagscache.get(node, [])
1046 1047
1047 1048 def nodebookmarks(self, node):
1048 1049 """return the list of bookmarks pointing to the specified node"""
1049 1050 return self._bookmarks.names(node)
1050 1051
1051 1052 def branchmap(self):
1052 1053 '''returns a dictionary {branch: [branchheads]} with branchheads
1053 1054 ordered by increasing revision number'''
1054 1055 branchmap.updatecache(self)
1055 1056 return self._branchcaches[self.filtername]
1056 1057
1057 1058 @unfilteredmethod
1058 1059 def revbranchcache(self):
1059 1060 if not self._revbranchcache:
1060 1061 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
1061 1062 return self._revbranchcache
1062 1063
1063 1064 def branchtip(self, branch, ignoremissing=False):
1064 1065 '''return the tip node for a given branch
1065 1066
1066 1067 If ignoremissing is True, then this method will not raise an error.
1067 1068 This is helpful for callers that only expect None for a missing branch
1068 1069 (e.g. namespace).
1069 1070
1070 1071 '''
1071 1072 try:
1072 1073 return self.branchmap().branchtip(branch)
1073 1074 except KeyError:
1074 1075 if not ignoremissing:
1075 1076 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
1076 1077 else:
1077 1078 pass
1078 1079
1079 1080 def lookup(self, key):
1080 1081 return scmutil.revsymbol(self, key).node()
1081 1082
1082 1083 def lookupbranch(self, key):
1083 1084 if key in self.branchmap():
1084 1085 return key
1085 1086
1086 1087 return scmutil.revsymbol(self, key).branch()
1087 1088
1088 1089 def known(self, nodes):
1089 1090 cl = self.changelog
1090 1091 nm = cl.nodemap
1091 1092 filtered = cl.filteredrevs
1092 1093 result = []
1093 1094 for n in nodes:
1094 1095 r = nm.get(n)
1095 1096 resp = not (r is None or r in filtered)
1096 1097 result.append(resp)
1097 1098 return result
1098 1099
1099 1100 def local(self):
1100 1101 return self
1101 1102
1102 1103 def publishing(self):
1103 1104 # it's safe (and desirable) to trust the publish flag unconditionally
1104 1105 # so that we don't finalize changes shared between users via ssh or nfs
1105 1106 return self.ui.configbool('phases', 'publish', untrusted=True)
1106 1107
1107 1108 def cancopy(self):
1108 1109 # so statichttprepo's override of local() works
1109 1110 if not self.local():
1110 1111 return False
1111 1112 if not self.publishing():
1112 1113 return True
1113 1114 # if publishing we can't copy if there is filtered content
1114 1115 return not self.filtered('visible').changelog.filteredrevs
1115 1116
1116 1117 def shared(self):
1117 1118 '''the type of shared repository (None if not shared)'''
1118 1119 if self.sharedpath != self.path:
1119 1120 return 'store'
1120 1121 return None
1121 1122
1122 1123 def wjoin(self, f, *insidef):
1123 1124 return self.vfs.reljoin(self.root, f, *insidef)
1124 1125
1125 1126 def file(self, f):
1126 1127 if f[0] == '/':
1127 1128 f = f[1:]
1128 1129 return filelog.filelog(self.svfs, f)
1129 1130
1130 1131 def setparents(self, p1, p2=nullid):
1131 1132 with self.dirstate.parentchange():
1132 1133 copies = self.dirstate.setparents(p1, p2)
1133 1134 pctx = self[p1]
1134 1135 if copies:
1135 1136 # Adjust copy records, the dirstate cannot do it, it
1136 1137 # requires access to parents manifests. Preserve them
1137 1138 # only for entries added to first parent.
1138 1139 for f in copies:
1139 1140 if f not in pctx and copies[f] in pctx:
1140 1141 self.dirstate.copy(copies[f], f)
1141 1142 if p2 == nullid:
1142 1143 for f, s in sorted(self.dirstate.copies().items()):
1143 1144 if f not in pctx and s not in pctx:
1144 1145 self.dirstate.copy(None, f)
1145 1146
1146 1147 def filectx(self, path, changeid=None, fileid=None, changectx=None):
1147 1148 """changeid can be a changeset revision, node, or tag.
1148 1149 fileid can be a file revision or node."""
1149 1150 return context.filectx(self, path, changeid, fileid,
1150 1151 changectx=changectx)
1151 1152
1152 1153 def getcwd(self):
1153 1154 return self.dirstate.getcwd()
1154 1155
1155 1156 def pathto(self, f, cwd=None):
1156 1157 return self.dirstate.pathto(f, cwd)
1157 1158
1158 1159 def _loadfilter(self, filter):
1159 1160 if filter not in self._filterpats:
1160 1161 l = []
1161 1162 for pat, cmd in self.ui.configitems(filter):
1162 1163 if cmd == '!':
1163 1164 continue
1164 1165 mf = matchmod.match(self.root, '', [pat])
1165 1166 fn = None
1166 1167 params = cmd
1167 1168 for name, filterfn in self._datafilters.iteritems():
1168 1169 if cmd.startswith(name):
1169 1170 fn = filterfn
1170 1171 params = cmd[len(name):].lstrip()
1171 1172 break
1172 1173 if not fn:
1173 1174 fn = lambda s, c, **kwargs: procutil.filter(s, c)
1174 1175 # Wrap old filters not supporting keyword arguments
1175 1176 if not pycompat.getargspec(fn)[2]:
1176 1177 oldfn = fn
1177 1178 fn = lambda s, c, **kwargs: oldfn(s, c)
1178 1179 l.append((mf, fn, params))
1179 1180 self._filterpats[filter] = l
1180 1181 return self._filterpats[filter]
1181 1182
1182 1183 def _filter(self, filterpats, filename, data):
1183 1184 for mf, fn, cmd in filterpats:
1184 1185 if mf(filename):
1185 1186 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
1186 1187 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
1187 1188 break
1188 1189
1189 1190 return data
1190 1191
1191 1192 @unfilteredpropertycache
1192 1193 def _encodefilterpats(self):
1193 1194 return self._loadfilter('encode')
1194 1195
1195 1196 @unfilteredpropertycache
1196 1197 def _decodefilterpats(self):
1197 1198 return self._loadfilter('decode')
1198 1199
1199 1200 def adddatafilter(self, name, filter):
1200 1201 self._datafilters[name] = filter
1201 1202
1202 1203 def wread(self, filename):
1203 1204 if self.wvfs.islink(filename):
1204 1205 data = self.wvfs.readlink(filename)
1205 1206 else:
1206 1207 data = self.wvfs.read(filename)
1207 1208 return self._filter(self._encodefilterpats, filename, data)
1208 1209
1209 1210 def wwrite(self, filename, data, flags, backgroundclose=False, **kwargs):
1210 1211 """write ``data`` into ``filename`` in the working directory
1211 1212
1212 1213 This returns length of written (maybe decoded) data.
1213 1214 """
1214 1215 data = self._filter(self._decodefilterpats, filename, data)
1215 1216 if 'l' in flags:
1216 1217 self.wvfs.symlink(data, filename)
1217 1218 else:
1218 1219 self.wvfs.write(filename, data, backgroundclose=backgroundclose,
1219 1220 **kwargs)
1220 1221 if 'x' in flags:
1221 1222 self.wvfs.setflags(filename, False, True)
1222 1223 else:
1223 1224 self.wvfs.setflags(filename, False, False)
1224 1225 return len(data)
1225 1226
1226 1227 def wwritedata(self, filename, data):
1227 1228 return self._filter(self._decodefilterpats, filename, data)
1228 1229
1229 1230 def currenttransaction(self):
1230 1231 """return the current transaction or None if non exists"""
1231 1232 if self._transref:
1232 1233 tr = self._transref()
1233 1234 else:
1234 1235 tr = None
1235 1236
1236 1237 if tr and tr.running():
1237 1238 return tr
1238 1239 return None
1239 1240
1240 1241 def transaction(self, desc, report=None):
1241 1242 if (self.ui.configbool('devel', 'all-warnings')
1242 1243 or self.ui.configbool('devel', 'check-locks')):
1243 1244 if self._currentlock(self._lockref) is None:
1244 1245 raise error.ProgrammingError('transaction requires locking')
1245 1246 tr = self.currenttransaction()
1246 1247 if tr is not None:
1247 1248 return tr.nest(name=desc)
1248 1249
1249 1250 # abort here if the journal already exists
1250 1251 if self.svfs.exists("journal"):
1251 1252 raise error.RepoError(
1252 1253 _("abandoned transaction found"),
1253 1254 hint=_("run 'hg recover' to clean up transaction"))
1254 1255
1255 1256 idbase = "%.40f#%f" % (random.random(), time.time())
1256 1257 ha = hex(hashlib.sha1(idbase).digest())
1257 1258 txnid = 'TXN:' + ha
1258 1259 self.hook('pretxnopen', throw=True, txnname=desc, txnid=txnid)
1259 1260
1260 1261 self._writejournal(desc)
1261 1262 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
1262 1263 if report:
1263 1264 rp = report
1264 1265 else:
1265 1266 rp = self.ui.warn
1266 1267 vfsmap = {'plain': self.vfs} # root of .hg/
1267 1268 # we must avoid cyclic reference between repo and transaction.
1268 1269 reporef = weakref.ref(self)
1269 1270 # Code to track tag movement
1270 1271 #
1271 1272 # Since tags are all handled as file content, it is actually quite hard
1272 1273 # to track these movement from a code perspective. So we fallback to a
1273 1274 # tracking at the repository level. One could envision to track changes
1274 1275 # to the '.hgtags' file through changegroup apply but that fails to
1275 1276 # cope with case where transaction expose new heads without changegroup
1276 1277 # being involved (eg: phase movement).
1277 1278 #
1278 1279 # For now, We gate the feature behind a flag since this likely comes
1279 1280 # with performance impacts. The current code run more often than needed
1280 1281 # and do not use caches as much as it could. The current focus is on
1281 1282 # the behavior of the feature so we disable it by default. The flag
1282 1283 # will be removed when we are happy with the performance impact.
1283 1284 #
1284 1285 # Once this feature is no longer experimental move the following
1285 1286 # documentation to the appropriate help section:
1286 1287 #
1287 1288 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
1288 1289 # tags (new or changed or deleted tags). In addition the details of
1289 1290 # these changes are made available in a file at:
1290 1291 # ``REPOROOT/.hg/changes/tags.changes``.
1291 1292 # Make sure you check for HG_TAG_MOVED before reading that file as it
1292 1293 # might exist from a previous transaction even if no tag were touched
1293 1294 # in this one. Changes are recorded in a line base format::
1294 1295 #
1295 1296 # <action> <hex-node> <tag-name>\n
1296 1297 #
1297 1298 # Actions are defined as follow:
1298 1299 # "-R": tag is removed,
1299 1300 # "+A": tag is added,
1300 1301 # "-M": tag is moved (old value),
1301 1302 # "+M": tag is moved (new value),
1302 1303 tracktags = lambda x: None
1303 1304 # experimental config: experimental.hook-track-tags
1304 1305 shouldtracktags = self.ui.configbool('experimental', 'hook-track-tags')
1305 1306 if desc != 'strip' and shouldtracktags:
1306 1307 oldheads = self.changelog.headrevs()
1307 1308 def tracktags(tr2):
1308 1309 repo = reporef()
1309 1310 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
1310 1311 newheads = repo.changelog.headrevs()
1311 1312 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
1312 1313 # notes: we compare lists here.
1313 1314 # As we do it only once buiding set would not be cheaper
1314 1315 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
1315 1316 if changes:
1316 1317 tr2.hookargs['tag_moved'] = '1'
1317 1318 with repo.vfs('changes/tags.changes', 'w',
1318 1319 atomictemp=True) as changesfile:
1319 1320 # note: we do not register the file to the transaction
1320 1321 # because we needs it to still exist on the transaction
1321 1322 # is close (for txnclose hooks)
1322 1323 tagsmod.writediff(changesfile, changes)
1323 1324 def validate(tr2):
1324 1325 """will run pre-closing hooks"""
1325 1326 # XXX the transaction API is a bit lacking here so we take a hacky
1326 1327 # path for now
1327 1328 #
1328 1329 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
1329 1330 # dict is copied before these run. In addition we needs the data
1330 1331 # available to in memory hooks too.
1331 1332 #
1332 1333 # Moreover, we also need to make sure this runs before txnclose
1333 1334 # hooks and there is no "pending" mechanism that would execute
1334 1335 # logic only if hooks are about to run.
1335 1336 #
1336 1337 # Fixing this limitation of the transaction is also needed to track
1337 1338 # other families of changes (bookmarks, phases, obsolescence).
1338 1339 #
1339 1340 # This will have to be fixed before we remove the experimental
1340 1341 # gating.
1341 1342 tracktags(tr2)
1342 1343 repo = reporef()
1343 1344 if repo.ui.configbool('experimental', 'single-head-per-branch'):
1344 1345 scmutil.enforcesinglehead(repo, tr2, desc)
1345 1346 if hook.hashook(repo.ui, 'pretxnclose-bookmark'):
1346 1347 for name, (old, new) in sorted(tr.changes['bookmarks'].items()):
1347 1348 args = tr.hookargs.copy()
1348 1349 args.update(bookmarks.preparehookargs(name, old, new))
1349 1350 repo.hook('pretxnclose-bookmark', throw=True,
1350 1351 txnname=desc,
1351 1352 **pycompat.strkwargs(args))
1352 1353 if hook.hashook(repo.ui, 'pretxnclose-phase'):
1353 1354 cl = repo.unfiltered().changelog
1354 1355 for rev, (old, new) in tr.changes['phases'].items():
1355 1356 args = tr.hookargs.copy()
1356 1357 node = hex(cl.node(rev))
1357 1358 args.update(phases.preparehookargs(node, old, new))
1358 1359 repo.hook('pretxnclose-phase', throw=True, txnname=desc,
1359 1360 **pycompat.strkwargs(args))
1360 1361
1361 1362 repo.hook('pretxnclose', throw=True,
1362 1363 txnname=desc, **pycompat.strkwargs(tr.hookargs))
1363 1364 def releasefn(tr, success):
1364 1365 repo = reporef()
1365 1366 if success:
1366 1367 # this should be explicitly invoked here, because
1367 1368 # in-memory changes aren't written out at closing
1368 1369 # transaction, if tr.addfilegenerator (via
1369 1370 # dirstate.write or so) isn't invoked while
1370 1371 # transaction running
1371 1372 repo.dirstate.write(None)
1372 1373 else:
1373 1374 # discard all changes (including ones already written
1374 1375 # out) in this transaction
1375 1376 repo.dirstate.restorebackup(None, 'journal.dirstate')
1376 1377
1377 1378 repo.invalidate(clearfilecache=True)
1378 1379
1379 1380 tr = transaction.transaction(rp, self.svfs, vfsmap,
1380 1381 "journal",
1381 1382 "undo",
1382 1383 aftertrans(renames),
1383 1384 self.store.createmode,
1384 1385 validator=validate,
1385 1386 releasefn=releasefn,
1386 1387 checkambigfiles=_cachedfiles,
1387 1388 name=desc)
1388 1389 tr.changes['revs'] = pycompat.xrange(0, 0)
1389 1390 tr.changes['obsmarkers'] = set()
1390 1391 tr.changes['phases'] = {}
1391 1392 tr.changes['bookmarks'] = {}
1392 1393
1393 1394 tr.hookargs['txnid'] = txnid
1394 1395 # note: writing the fncache only during finalize mean that the file is
1395 1396 # outdated when running hooks. As fncache is used for streaming clone,
1396 1397 # this is not expected to break anything that happen during the hooks.
1397 1398 tr.addfinalize('flush-fncache', self.store.write)
1398 1399 def txnclosehook(tr2):
1399 1400 """To be run if transaction is successful, will schedule a hook run
1400 1401 """
1401 1402 # Don't reference tr2 in hook() so we don't hold a reference.
1402 1403 # This reduces memory consumption when there are multiple
1403 1404 # transactions per lock. This can likely go away if issue5045
1404 1405 # fixes the function accumulation.
1405 1406 hookargs = tr2.hookargs
1406 1407
1407 1408 def hookfunc():
1408 1409 repo = reporef()
1409 1410 if hook.hashook(repo.ui, 'txnclose-bookmark'):
1410 1411 bmchanges = sorted(tr.changes['bookmarks'].items())
1411 1412 for name, (old, new) in bmchanges:
1412 1413 args = tr.hookargs.copy()
1413 1414 args.update(bookmarks.preparehookargs(name, old, new))
1414 1415 repo.hook('txnclose-bookmark', throw=False,
1415 1416 txnname=desc, **pycompat.strkwargs(args))
1416 1417
1417 1418 if hook.hashook(repo.ui, 'txnclose-phase'):
1418 1419 cl = repo.unfiltered().changelog
1419 1420 phasemv = sorted(tr.changes['phases'].items())
1420 1421 for rev, (old, new) in phasemv:
1421 1422 args = tr.hookargs.copy()
1422 1423 node = hex(cl.node(rev))
1423 1424 args.update(phases.preparehookargs(node, old, new))
1424 1425 repo.hook('txnclose-phase', throw=False, txnname=desc,
1425 1426 **pycompat.strkwargs(args))
1426 1427
1427 1428 repo.hook('txnclose', throw=False, txnname=desc,
1428 1429 **pycompat.strkwargs(hookargs))
1429 1430 reporef()._afterlock(hookfunc)
1430 1431 tr.addfinalize('txnclose-hook', txnclosehook)
1431 1432 # Include a leading "-" to make it happen before the transaction summary
1432 1433 # reports registered via scmutil.registersummarycallback() whose names
1433 1434 # are 00-txnreport etc. That way, the caches will be warm when the
1434 1435 # callbacks run.
1435 1436 tr.addpostclose('-warm-cache', self._buildcacheupdater(tr))
1436 1437 def txnaborthook(tr2):
1437 1438 """To be run if transaction is aborted
1438 1439 """
1439 1440 reporef().hook('txnabort', throw=False, txnname=desc,
1440 1441 **pycompat.strkwargs(tr2.hookargs))
1441 1442 tr.addabort('txnabort-hook', txnaborthook)
1442 1443 # avoid eager cache invalidation. in-memory data should be identical
1443 1444 # to stored data if transaction has no error.
1444 1445 tr.addpostclose('refresh-filecachestats', self._refreshfilecachestats)
1445 1446 self._transref = weakref.ref(tr)
1446 1447 scmutil.registersummarycallback(self, tr, desc)
1447 1448 return tr
1448 1449
1449 1450 def _journalfiles(self):
1450 1451 return ((self.svfs, 'journal'),
1451 1452 (self.vfs, 'journal.dirstate'),
1452 1453 (self.vfs, 'journal.branch'),
1453 1454 (self.vfs, 'journal.desc'),
1454 1455 (self.vfs, 'journal.bookmarks'),
1455 1456 (self.svfs, 'journal.phaseroots'))
1456 1457
1457 1458 def undofiles(self):
1458 1459 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
1459 1460
1460 1461 @unfilteredmethod
1461 1462 def _writejournal(self, desc):
1462 1463 self.dirstate.savebackup(None, 'journal.dirstate')
1463 1464 self.vfs.write("journal.branch",
1464 1465 encoding.fromlocal(self.dirstate.branch()))
1465 1466 self.vfs.write("journal.desc",
1466 1467 "%d\n%s\n" % (len(self), desc))
1467 1468 self.vfs.write("journal.bookmarks",
1468 1469 self.vfs.tryread("bookmarks"))
1469 1470 self.svfs.write("journal.phaseroots",
1470 1471 self.svfs.tryread("phaseroots"))
1471 1472
1472 1473 def recover(self):
1473 1474 with self.lock():
1474 1475 if self.svfs.exists("journal"):
1475 1476 self.ui.status(_("rolling back interrupted transaction\n"))
1476 1477 vfsmap = {'': self.svfs,
1477 1478 'plain': self.vfs,}
1478 1479 transaction.rollback(self.svfs, vfsmap, "journal",
1479 1480 self.ui.warn,
1480 1481 checkambigfiles=_cachedfiles)
1481 1482 self.invalidate()
1482 1483 return True
1483 1484 else:
1484 1485 self.ui.warn(_("no interrupted transaction available\n"))
1485 1486 return False
1486 1487
1487 1488 def rollback(self, dryrun=False, force=False):
1488 1489 wlock = lock = dsguard = None
1489 1490 try:
1490 1491 wlock = self.wlock()
1491 1492 lock = self.lock()
1492 1493 if self.svfs.exists("undo"):
1493 1494 dsguard = dirstateguard.dirstateguard(self, 'rollback')
1494 1495
1495 1496 return self._rollback(dryrun, force, dsguard)
1496 1497 else:
1497 1498 self.ui.warn(_("no rollback information available\n"))
1498 1499 return 1
1499 1500 finally:
1500 1501 release(dsguard, lock, wlock)
1501 1502
1502 1503 @unfilteredmethod # Until we get smarter cache management
1503 1504 def _rollback(self, dryrun, force, dsguard):
1504 1505 ui = self.ui
1505 1506 try:
1506 1507 args = self.vfs.read('undo.desc').splitlines()
1507 1508 (oldlen, desc, detail) = (int(args[0]), args[1], None)
1508 1509 if len(args) >= 3:
1509 1510 detail = args[2]
1510 1511 oldtip = oldlen - 1
1511 1512
1512 1513 if detail and ui.verbose:
1513 1514 msg = (_('repository tip rolled back to revision %d'
1514 1515 ' (undo %s: %s)\n')
1515 1516 % (oldtip, desc, detail))
1516 1517 else:
1517 1518 msg = (_('repository tip rolled back to revision %d'
1518 1519 ' (undo %s)\n')
1519 1520 % (oldtip, desc))
1520 1521 except IOError:
1521 1522 msg = _('rolling back unknown transaction\n')
1522 1523 desc = None
1523 1524
1524 1525 if not force and self['.'] != self['tip'] and desc == 'commit':
1525 1526 raise error.Abort(
1526 1527 _('rollback of last commit while not checked out '
1527 1528 'may lose data'), hint=_('use -f to force'))
1528 1529
1529 1530 ui.status(msg)
1530 1531 if dryrun:
1531 1532 return 0
1532 1533
1533 1534 parents = self.dirstate.parents()
1534 1535 self.destroying()
1535 1536 vfsmap = {'plain': self.vfs, '': self.svfs}
1536 1537 transaction.rollback(self.svfs, vfsmap, 'undo', ui.warn,
1537 1538 checkambigfiles=_cachedfiles)
1538 1539 if self.vfs.exists('undo.bookmarks'):
1539 1540 self.vfs.rename('undo.bookmarks', 'bookmarks', checkambig=True)
1540 1541 if self.svfs.exists('undo.phaseroots'):
1541 1542 self.svfs.rename('undo.phaseroots', 'phaseroots', checkambig=True)
1542 1543 self.invalidate()
1543 1544
1544 1545 parentgone = (parents[0] not in self.changelog.nodemap or
1545 1546 parents[1] not in self.changelog.nodemap)
1546 1547 if parentgone:
1547 1548 # prevent dirstateguard from overwriting already restored one
1548 1549 dsguard.close()
1549 1550
1550 1551 self.dirstate.restorebackup(None, 'undo.dirstate')
1551 1552 try:
1552 1553 branch = self.vfs.read('undo.branch')
1553 1554 self.dirstate.setbranch(encoding.tolocal(branch))
1554 1555 except IOError:
1555 1556 ui.warn(_('named branch could not be reset: '
1556 1557 'current branch is still \'%s\'\n')
1557 1558 % self.dirstate.branch())
1558 1559
1559 1560 parents = tuple([p.rev() for p in self[None].parents()])
1560 1561 if len(parents) > 1:
1561 1562 ui.status(_('working directory now based on '
1562 1563 'revisions %d and %d\n') % parents)
1563 1564 else:
1564 1565 ui.status(_('working directory now based on '
1565 1566 'revision %d\n') % parents)
1566 1567 mergemod.mergestate.clean(self, self['.'].node())
1567 1568
1568 1569 # TODO: if we know which new heads may result from this rollback, pass
1569 1570 # them to destroy(), which will prevent the branchhead cache from being
1570 1571 # invalidated.
1571 1572 self.destroyed()
1572 1573 return 0
1573 1574
1574 1575 def _buildcacheupdater(self, newtransaction):
1575 1576 """called during transaction to build the callback updating cache
1576 1577
1577 1578 Lives on the repository to help extension who might want to augment
1578 1579 this logic. For this purpose, the created transaction is passed to the
1579 1580 method.
1580 1581 """
1581 1582 # we must avoid cyclic reference between repo and transaction.
1582 1583 reporef = weakref.ref(self)
1583 1584 def updater(tr):
1584 1585 repo = reporef()
1585 1586 repo.updatecaches(tr)
1586 1587 return updater
1587 1588
1588 1589 @unfilteredmethod
1589 1590 def updatecaches(self, tr=None, full=False):
1590 1591 """warm appropriate caches
1591 1592
1592 1593 If this function is called after a transaction closed. The transaction
1593 1594 will be available in the 'tr' argument. This can be used to selectively
1594 1595 update caches relevant to the changes in that transaction.
1595 1596
1596 1597 If 'full' is set, make sure all caches the function knows about have
1597 1598 up-to-date data. Even the ones usually loaded more lazily.
1598 1599 """
1599 1600 if tr is not None and tr.hookargs.get('source') == 'strip':
1600 1601 # During strip, many caches are invalid but
1601 1602 # later call to `destroyed` will refresh them.
1602 1603 return
1603 1604
1604 1605 if tr is None or tr.changes['revs']:
1605 1606 # updating the unfiltered branchmap should refresh all the others,
1606 1607 self.ui.debug('updating the branch cache\n')
1607 1608 branchmap.updatecache(self.filtered('served'))
1608 1609
1609 1610 if full:
1610 1611 rbc = self.revbranchcache()
1611 1612 for r in self.changelog:
1612 1613 rbc.branchinfo(r)
1613 1614 rbc.write()
1614 1615
1615 1616 # ensure the working copy parents are in the manifestfulltextcache
1616 1617 for ctx in self['.'].parents():
1617 1618 ctx.manifest() # accessing the manifest is enough
1618 1619
1619 1620 def invalidatecaches(self):
1620 1621
1621 1622 if '_tagscache' in vars(self):
1622 1623 # can't use delattr on proxy
1623 1624 del self.__dict__['_tagscache']
1624 1625
1625 1626 self.unfiltered()._branchcaches.clear()
1626 1627 self.invalidatevolatilesets()
1627 1628 self._sparsesignaturecache.clear()
1628 1629
1629 1630 def invalidatevolatilesets(self):
1630 1631 self.filteredrevcache.clear()
1631 1632 obsolete.clearobscaches(self)
1632 1633
1633 1634 def invalidatedirstate(self):
1634 1635 '''Invalidates the dirstate, causing the next call to dirstate
1635 1636 to check if it was modified since the last time it was read,
1636 1637 rereading it if it has.
1637 1638
1638 1639 This is different to dirstate.invalidate() that it doesn't always
1639 1640 rereads the dirstate. Use dirstate.invalidate() if you want to
1640 1641 explicitly read the dirstate again (i.e. restoring it to a previous
1641 1642 known good state).'''
1642 1643 if hasunfilteredcache(self, 'dirstate'):
1643 1644 for k in self.dirstate._filecache:
1644 1645 try:
1645 1646 delattr(self.dirstate, k)
1646 1647 except AttributeError:
1647 1648 pass
1648 1649 delattr(self.unfiltered(), 'dirstate')
1649 1650
1650 1651 def invalidate(self, clearfilecache=False):
1651 1652 '''Invalidates both store and non-store parts other than dirstate
1652 1653
1653 1654 If a transaction is running, invalidation of store is omitted,
1654 1655 because discarding in-memory changes might cause inconsistency
1655 1656 (e.g. incomplete fncache causes unintentional failure, but
1656 1657 redundant one doesn't).
1657 1658 '''
1658 1659 unfiltered = self.unfiltered() # all file caches are stored unfiltered
1659 1660 for k in list(self._filecache.keys()):
1660 1661 # dirstate is invalidated separately in invalidatedirstate()
1661 1662 if k == 'dirstate':
1662 1663 continue
1663 1664 if (k == 'changelog' and
1664 1665 self.currenttransaction() and
1665 1666 self.changelog._delayed):
1666 1667 # The changelog object may store unwritten revisions. We don't
1667 1668 # want to lose them.
1668 1669 # TODO: Solve the problem instead of working around it.
1669 1670 continue
1670 1671
1671 1672 if clearfilecache:
1672 1673 del self._filecache[k]
1673 1674 try:
1674 1675 delattr(unfiltered, k)
1675 1676 except AttributeError:
1676 1677 pass
1677 1678 self.invalidatecaches()
1678 1679 if not self.currenttransaction():
1679 1680 # TODO: Changing contents of store outside transaction
1680 1681 # causes inconsistency. We should make in-memory store
1681 1682 # changes detectable, and abort if changed.
1682 1683 self.store.invalidatecaches()
1683 1684
1684 1685 def invalidateall(self):
1685 1686 '''Fully invalidates both store and non-store parts, causing the
1686 1687 subsequent operation to reread any outside changes.'''
1687 1688 # extension should hook this to invalidate its caches
1688 1689 self.invalidate()
1689 1690 self.invalidatedirstate()
1690 1691
1691 1692 @unfilteredmethod
1692 1693 def _refreshfilecachestats(self, tr):
1693 1694 """Reload stats of cached files so that they are flagged as valid"""
1694 1695 for k, ce in self._filecache.items():
1695 1696 k = pycompat.sysstr(k)
1696 1697 if k == r'dirstate' or k not in self.__dict__:
1697 1698 continue
1698 1699 ce.refresh()
1699 1700
1700 1701 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc,
1701 1702 inheritchecker=None, parentenvvar=None):
1702 1703 parentlock = None
1703 1704 # the contents of parentenvvar are used by the underlying lock to
1704 1705 # determine whether it can be inherited
1705 1706 if parentenvvar is not None:
1706 1707 parentlock = encoding.environ.get(parentenvvar)
1707 1708
1708 1709 timeout = 0
1709 1710 warntimeout = 0
1710 1711 if wait:
1711 1712 timeout = self.ui.configint("ui", "timeout")
1712 1713 warntimeout = self.ui.configint("ui", "timeout.warn")
1713 1714 # internal config: ui.signal-safe-lock
1714 1715 signalsafe = self.ui.configbool('ui', 'signal-safe-lock')
1715 1716
1716 1717 l = lockmod.trylock(self.ui, vfs, lockname, timeout, warntimeout,
1717 1718 releasefn=releasefn,
1718 1719 acquirefn=acquirefn, desc=desc,
1719 1720 inheritchecker=inheritchecker,
1720 1721 parentlock=parentlock,
1721 1722 signalsafe=signalsafe)
1722 1723 return l
1723 1724
1724 1725 def _afterlock(self, callback):
1725 1726 """add a callback to be run when the repository is fully unlocked
1726 1727
1727 1728 The callback will be executed when the outermost lock is released
1728 1729 (with wlock being higher level than 'lock')."""
1729 1730 for ref in (self._wlockref, self._lockref):
1730 1731 l = ref and ref()
1731 1732 if l and l.held:
1732 1733 l.postrelease.append(callback)
1733 1734 break
1734 1735 else: # no lock have been found.
1735 1736 callback()
1736 1737
1737 1738 def lock(self, wait=True):
1738 1739 '''Lock the repository store (.hg/store) and return a weak reference
1739 1740 to the lock. Use this before modifying the store (e.g. committing or
1740 1741 stripping). If you are opening a transaction, get a lock as well.)
1741 1742
1742 1743 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1743 1744 'wlock' first to avoid a dead-lock hazard.'''
1744 1745 l = self._currentlock(self._lockref)
1745 1746 if l is not None:
1746 1747 l.lock()
1747 1748 return l
1748 1749
1749 1750 l = self._lock(self.svfs, "lock", wait, None,
1750 1751 self.invalidate, _('repository %s') % self.origroot)
1751 1752 self._lockref = weakref.ref(l)
1752 1753 return l
1753 1754
1754 1755 def _wlockchecktransaction(self):
1755 1756 if self.currenttransaction() is not None:
1756 1757 raise error.LockInheritanceContractViolation(
1757 1758 'wlock cannot be inherited in the middle of a transaction')
1758 1759
1759 1760 def wlock(self, wait=True):
1760 1761 '''Lock the non-store parts of the repository (everything under
1761 1762 .hg except .hg/store) and return a weak reference to the lock.
1762 1763
1763 1764 Use this before modifying files in .hg.
1764 1765
1765 1766 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1766 1767 'wlock' first to avoid a dead-lock hazard.'''
1767 1768 l = self._wlockref and self._wlockref()
1768 1769 if l is not None and l.held:
1769 1770 l.lock()
1770 1771 return l
1771 1772
1772 1773 # We do not need to check for non-waiting lock acquisition. Such
1773 1774 # acquisition would not cause dead-lock as they would just fail.
1774 1775 if wait and (self.ui.configbool('devel', 'all-warnings')
1775 1776 or self.ui.configbool('devel', 'check-locks')):
1776 1777 if self._currentlock(self._lockref) is not None:
1777 1778 self.ui.develwarn('"wlock" acquired after "lock"')
1778 1779
1779 1780 def unlock():
1780 1781 if self.dirstate.pendingparentchange():
1781 1782 self.dirstate.invalidate()
1782 1783 else:
1783 1784 self.dirstate.write(None)
1784 1785
1785 1786 self._filecache['dirstate'].refresh()
1786 1787
1787 1788 l = self._lock(self.vfs, "wlock", wait, unlock,
1788 1789 self.invalidatedirstate, _('working directory of %s') %
1789 1790 self.origroot,
1790 1791 inheritchecker=self._wlockchecktransaction,
1791 1792 parentenvvar='HG_WLOCK_LOCKER')
1792 1793 self._wlockref = weakref.ref(l)
1793 1794 return l
1794 1795
1795 1796 def _currentlock(self, lockref):
1796 1797 """Returns the lock if it's held, or None if it's not."""
1797 1798 if lockref is None:
1798 1799 return None
1799 1800 l = lockref()
1800 1801 if l is None or not l.held:
1801 1802 return None
1802 1803 return l
1803 1804
1804 1805 def currentwlock(self):
1805 1806 """Returns the wlock if it's held, or None if it's not."""
1806 1807 return self._currentlock(self._wlockref)
1807 1808
1808 1809 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
1809 1810 """
1810 1811 commit an individual file as part of a larger transaction
1811 1812 """
1812 1813
1813 1814 fname = fctx.path()
1814 1815 fparent1 = manifest1.get(fname, nullid)
1815 1816 fparent2 = manifest2.get(fname, nullid)
1816 1817 if isinstance(fctx, context.filectx):
1817 1818 node = fctx.filenode()
1818 1819 if node in [fparent1, fparent2]:
1819 1820 self.ui.debug('reusing %s filelog entry\n' % fname)
1820 1821 if manifest1.flags(fname) != fctx.flags():
1821 1822 changelist.append(fname)
1822 1823 return node
1823 1824
1824 1825 flog = self.file(fname)
1825 1826 meta = {}
1826 1827 copy = fctx.renamed()
1827 1828 if copy and copy[0] != fname:
1828 1829 # Mark the new revision of this file as a copy of another
1829 1830 # file. This copy data will effectively act as a parent
1830 1831 # of this new revision. If this is a merge, the first
1831 1832 # parent will be the nullid (meaning "look up the copy data")
1832 1833 # and the second one will be the other parent. For example:
1833 1834 #
1834 1835 # 0 --- 1 --- 3 rev1 changes file foo
1835 1836 # \ / rev2 renames foo to bar and changes it
1836 1837 # \- 2 -/ rev3 should have bar with all changes and
1837 1838 # should record that bar descends from
1838 1839 # bar in rev2 and foo in rev1
1839 1840 #
1840 1841 # this allows this merge to succeed:
1841 1842 #
1842 1843 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
1843 1844 # \ / merging rev3 and rev4 should use bar@rev2
1844 1845 # \- 2 --- 4 as the merge base
1845 1846 #
1846 1847
1847 1848 cfname = copy[0]
1848 1849 crev = manifest1.get(cfname)
1849 1850 newfparent = fparent2
1850 1851
1851 1852 if manifest2: # branch merge
1852 1853 if fparent2 == nullid or crev is None: # copied on remote side
1853 1854 if cfname in manifest2:
1854 1855 crev = manifest2[cfname]
1855 1856 newfparent = fparent1
1856 1857
1857 1858 # Here, we used to search backwards through history to try to find
1858 1859 # where the file copy came from if the source of a copy was not in
1859 1860 # the parent directory. However, this doesn't actually make sense to
1860 1861 # do (what does a copy from something not in your working copy even
1861 1862 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
1862 1863 # the user that copy information was dropped, so if they didn't
1863 1864 # expect this outcome it can be fixed, but this is the correct
1864 1865 # behavior in this circumstance.
1865 1866
1866 1867 if crev:
1867 1868 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(crev)))
1868 1869 meta["copy"] = cfname
1869 1870 meta["copyrev"] = hex(crev)
1870 1871 fparent1, fparent2 = nullid, newfparent
1871 1872 else:
1872 1873 self.ui.warn(_("warning: can't find ancestor for '%s' "
1873 1874 "copied from '%s'!\n") % (fname, cfname))
1874 1875
1875 1876 elif fparent1 == nullid:
1876 1877 fparent1, fparent2 = fparent2, nullid
1877 1878 elif fparent2 != nullid:
1878 1879 # is one parent an ancestor of the other?
1879 1880 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
1880 1881 if fparent1 in fparentancestors:
1881 1882 fparent1, fparent2 = fparent2, nullid
1882 1883 elif fparent2 in fparentancestors:
1883 1884 fparent2 = nullid
1884 1885
1885 1886 # is the file changed?
1886 1887 text = fctx.data()
1887 1888 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
1888 1889 changelist.append(fname)
1889 1890 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
1890 1891 # are just the flags changed during merge?
1891 1892 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
1892 1893 changelist.append(fname)
1893 1894
1894 1895 return fparent1
1895 1896
1896 1897 def checkcommitpatterns(self, wctx, vdirs, match, status, fail):
1897 1898 """check for commit arguments that aren't committable"""
1898 1899 if match.isexact() or match.prefix():
1899 1900 matched = set(status.modified + status.added + status.removed)
1900 1901
1901 1902 for f in match.files():
1902 1903 f = self.dirstate.normalize(f)
1903 1904 if f == '.' or f in matched or f in wctx.substate:
1904 1905 continue
1905 1906 if f in status.deleted:
1906 1907 fail(f, _('file not found!'))
1907 1908 if f in vdirs: # visited directory
1908 1909 d = f + '/'
1909 1910 for mf in matched:
1910 1911 if mf.startswith(d):
1911 1912 break
1912 1913 else:
1913 1914 fail(f, _("no match under directory!"))
1914 1915 elif f not in self.dirstate:
1915 1916 fail(f, _("file not tracked!"))
1916 1917
1917 1918 @unfilteredmethod
1918 1919 def commit(self, text="", user=None, date=None, match=None, force=False,
1919 1920 editor=False, extra=None):
1920 1921 """Add a new revision to current repository.
1921 1922
1922 1923 Revision information is gathered from the working directory,
1923 1924 match can be used to filter the committed files. If editor is
1924 1925 supplied, it is called to get a commit message.
1925 1926 """
1926 1927 if extra is None:
1927 1928 extra = {}
1928 1929
1929 1930 def fail(f, msg):
1930 1931 raise error.Abort('%s: %s' % (f, msg))
1931 1932
1932 1933 if not match:
1933 1934 match = matchmod.always(self.root, '')
1934 1935
1935 1936 if not force:
1936 1937 vdirs = []
1937 1938 match.explicitdir = vdirs.append
1938 1939 match.bad = fail
1939 1940
1940 1941 wlock = lock = tr = None
1941 1942 try:
1942 1943 wlock = self.wlock()
1943 1944 lock = self.lock() # for recent changelog (see issue4368)
1944 1945
1945 1946 wctx = self[None]
1946 1947 merge = len(wctx.parents()) > 1
1947 1948
1948 1949 if not force and merge and not match.always():
1949 1950 raise error.Abort(_('cannot partially commit a merge '
1950 1951 '(do not specify files or patterns)'))
1951 1952
1952 1953 status = self.status(match=match, clean=force)
1953 1954 if force:
1954 1955 status.modified.extend(status.clean) # mq may commit clean files
1955 1956
1956 1957 # check subrepos
1957 1958 subs, commitsubs, newstate = subrepoutil.precommit(
1958 1959 self.ui, wctx, status, match, force=force)
1959 1960
1960 1961 # make sure all explicit patterns are matched
1961 1962 if not force:
1962 1963 self.checkcommitpatterns(wctx, vdirs, match, status, fail)
1963 1964
1964 1965 cctx = context.workingcommitctx(self, status,
1965 1966 text, user, date, extra)
1966 1967
1967 1968 # internal config: ui.allowemptycommit
1968 1969 allowemptycommit = (wctx.branch() != wctx.p1().branch()
1969 1970 or extra.get('close') or merge or cctx.files()
1970 1971 or self.ui.configbool('ui', 'allowemptycommit'))
1971 1972 if not allowemptycommit:
1972 1973 return None
1973 1974
1974 1975 if merge and cctx.deleted():
1975 1976 raise error.Abort(_("cannot commit merge with missing files"))
1976 1977
1977 1978 ms = mergemod.mergestate.read(self)
1978 1979 mergeutil.checkunresolved(ms)
1979 1980
1980 1981 if editor:
1981 1982 cctx._text = editor(self, cctx, subs)
1982 1983 edited = (text != cctx._text)
1983 1984
1984 1985 # Save commit message in case this transaction gets rolled back
1985 1986 # (e.g. by a pretxncommit hook). Leave the content alone on
1986 1987 # the assumption that the user will use the same editor again.
1987 1988 msgfn = self.savecommitmessage(cctx._text)
1988 1989
1989 1990 # commit subs and write new state
1990 1991 if subs:
1991 1992 for s in sorted(commitsubs):
1992 1993 sub = wctx.sub(s)
1993 1994 self.ui.status(_('committing subrepository %s\n') %
1994 1995 subrepoutil.subrelpath(sub))
1995 1996 sr = sub.commit(cctx._text, user, date)
1996 1997 newstate[s] = (newstate[s][0], sr)
1997 1998 subrepoutil.writestate(self, newstate)
1998 1999
1999 2000 p1, p2 = self.dirstate.parents()
2000 2001 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
2001 2002 try:
2002 2003 self.hook("precommit", throw=True, parent1=hookp1,
2003 2004 parent2=hookp2)
2004 2005 tr = self.transaction('commit')
2005 2006 ret = self.commitctx(cctx, True)
2006 2007 except: # re-raises
2007 2008 if edited:
2008 2009 self.ui.write(
2009 2010 _('note: commit message saved in %s\n') % msgfn)
2010 2011 raise
2011 2012 # update bookmarks, dirstate and mergestate
2012 2013 bookmarks.update(self, [p1, p2], ret)
2013 2014 cctx.markcommitted(ret)
2014 2015 ms.reset()
2015 2016 tr.close()
2016 2017
2017 2018 finally:
2018 2019 lockmod.release(tr, lock, wlock)
2019 2020
2020 2021 def commithook(node=hex(ret), parent1=hookp1, parent2=hookp2):
2021 2022 # hack for command that use a temporary commit (eg: histedit)
2022 2023 # temporary commit got stripped before hook release
2023 2024 if self.changelog.hasnode(ret):
2024 2025 self.hook("commit", node=node, parent1=parent1,
2025 2026 parent2=parent2)
2026 2027 self._afterlock(commithook)
2027 2028 return ret
2028 2029
2029 2030 @unfilteredmethod
2030 2031 def commitctx(self, ctx, error=False):
2031 2032 """Add a new revision to current repository.
2032 2033 Revision information is passed via the context argument.
2033 2034 """
2034 2035
2035 2036 tr = None
2036 2037 p1, p2 = ctx.p1(), ctx.p2()
2037 2038 user = ctx.user()
2038 2039
2039 2040 lock = self.lock()
2040 2041 try:
2041 2042 tr = self.transaction("commit")
2042 2043 trp = weakref.proxy(tr)
2043 2044
2044 2045 if ctx.manifestnode():
2045 2046 # reuse an existing manifest revision
2046 2047 mn = ctx.manifestnode()
2047 2048 files = ctx.files()
2048 2049 elif ctx.files():
2049 2050 m1ctx = p1.manifestctx()
2050 2051 m2ctx = p2.manifestctx()
2051 2052 mctx = m1ctx.copy()
2052 2053
2053 2054 m = mctx.read()
2054 2055 m1 = m1ctx.read()
2055 2056 m2 = m2ctx.read()
2056 2057
2057 2058 # check in files
2058 2059 added = []
2059 2060 changed = []
2060 2061 removed = list(ctx.removed())
2061 2062 linkrev = len(self)
2062 2063 self.ui.note(_("committing files:\n"))
2063 2064 for f in sorted(ctx.modified() + ctx.added()):
2064 2065 self.ui.note(f + "\n")
2065 2066 try:
2066 2067 fctx = ctx[f]
2067 2068 if fctx is None:
2068 2069 removed.append(f)
2069 2070 else:
2070 2071 added.append(f)
2071 2072 m[f] = self._filecommit(fctx, m1, m2, linkrev,
2072 2073 trp, changed)
2073 2074 m.setflag(f, fctx.flags())
2074 2075 except OSError as inst:
2075 2076 self.ui.warn(_("trouble committing %s!\n") % f)
2076 2077 raise
2077 2078 except IOError as inst:
2078 2079 errcode = getattr(inst, 'errno', errno.ENOENT)
2079 2080 if error or errcode and errcode != errno.ENOENT:
2080 2081 self.ui.warn(_("trouble committing %s!\n") % f)
2081 2082 raise
2082 2083
2083 2084 # update manifest
2084 2085 self.ui.note(_("committing manifest\n"))
2085 2086 removed = [f for f in sorted(removed) if f in m1 or f in m2]
2086 2087 drop = [f for f in removed if f in m]
2087 2088 for f in drop:
2088 2089 del m[f]
2089 2090 mn = mctx.write(trp, linkrev,
2090 2091 p1.manifestnode(), p2.manifestnode(),
2091 2092 added, drop)
2092 2093 files = changed + removed
2093 2094 else:
2094 2095 mn = p1.manifestnode()
2095 2096 files = []
2096 2097
2097 2098 # update changelog
2098 2099 self.ui.note(_("committing changelog\n"))
2099 2100 self.changelog.delayupdate(tr)
2100 2101 n = self.changelog.add(mn, files, ctx.description(),
2101 2102 trp, p1.node(), p2.node(),
2102 2103 user, ctx.date(), ctx.extra().copy())
2103 2104 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
2104 2105 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
2105 2106 parent2=xp2)
2106 2107 # set the new commit is proper phase
2107 2108 targetphase = subrepoutil.newcommitphase(self.ui, ctx)
2108 2109 if targetphase:
2109 2110 # retract boundary do not alter parent changeset.
2110 2111 # if a parent have higher the resulting phase will
2111 2112 # be compliant anyway
2112 2113 #
2113 2114 # if minimal phase was 0 we don't need to retract anything
2114 2115 phases.registernew(self, tr, targetphase, [n])
2115 2116 tr.close()
2116 2117 return n
2117 2118 finally:
2118 2119 if tr:
2119 2120 tr.release()
2120 2121 lock.release()
2121 2122
2122 2123 @unfilteredmethod
2123 2124 def destroying(self):
2124 2125 '''Inform the repository that nodes are about to be destroyed.
2125 2126 Intended for use by strip and rollback, so there's a common
2126 2127 place for anything that has to be done before destroying history.
2127 2128
2128 2129 This is mostly useful for saving state that is in memory and waiting
2129 2130 to be flushed when the current lock is released. Because a call to
2130 2131 destroyed is imminent, the repo will be invalidated causing those
2131 2132 changes to stay in memory (waiting for the next unlock), or vanish
2132 2133 completely.
2133 2134 '''
2134 2135 # When using the same lock to commit and strip, the phasecache is left
2135 2136 # dirty after committing. Then when we strip, the repo is invalidated,
2136 2137 # causing those changes to disappear.
2137 2138 if '_phasecache' in vars(self):
2138 2139 self._phasecache.write()
2139 2140
2140 2141 @unfilteredmethod
2141 2142 def destroyed(self):
2142 2143 '''Inform the repository that nodes have been destroyed.
2143 2144 Intended for use by strip and rollback, so there's a common
2144 2145 place for anything that has to be done after destroying history.
2145 2146 '''
2146 2147 # When one tries to:
2147 2148 # 1) destroy nodes thus calling this method (e.g. strip)
2148 2149 # 2) use phasecache somewhere (e.g. commit)
2149 2150 #
2150 2151 # then 2) will fail because the phasecache contains nodes that were
2151 2152 # removed. We can either remove phasecache from the filecache,
2152 2153 # causing it to reload next time it is accessed, or simply filter
2153 2154 # the removed nodes now and write the updated cache.
2154 2155 self._phasecache.filterunknown(self)
2155 2156 self._phasecache.write()
2156 2157
2157 2158 # refresh all repository caches
2158 2159 self.updatecaches()
2159 2160
2160 2161 # Ensure the persistent tag cache is updated. Doing it now
2161 2162 # means that the tag cache only has to worry about destroyed
2162 2163 # heads immediately after a strip/rollback. That in turn
2163 2164 # guarantees that "cachetip == currenttip" (comparing both rev
2164 2165 # and node) always means no nodes have been added or destroyed.
2165 2166
2166 2167 # XXX this is suboptimal when qrefresh'ing: we strip the current
2167 2168 # head, refresh the tag cache, then immediately add a new head.
2168 2169 # But I think doing it this way is necessary for the "instant
2169 2170 # tag cache retrieval" case to work.
2170 2171 self.invalidate()
2171 2172
2172 2173 def status(self, node1='.', node2=None, match=None,
2173 2174 ignored=False, clean=False, unknown=False,
2174 2175 listsubrepos=False):
2175 2176 '''a convenience method that calls node1.status(node2)'''
2176 2177 return self[node1].status(node2, match, ignored, clean, unknown,
2177 2178 listsubrepos)
2178 2179
2179 2180 def addpostdsstatus(self, ps):
2180 2181 """Add a callback to run within the wlock, at the point at which status
2181 2182 fixups happen.
2182 2183
2183 2184 On status completion, callback(wctx, status) will be called with the
2184 2185 wlock held, unless the dirstate has changed from underneath or the wlock
2185 2186 couldn't be grabbed.
2186 2187
2187 2188 Callbacks should not capture and use a cached copy of the dirstate --
2188 2189 it might change in the meanwhile. Instead, they should access the
2189 2190 dirstate via wctx.repo().dirstate.
2190 2191
2191 2192 This list is emptied out after each status run -- extensions should
2192 2193 make sure it adds to this list each time dirstate.status is called.
2193 2194 Extensions should also make sure they don't call this for statuses
2194 2195 that don't involve the dirstate.
2195 2196 """
2196 2197
2197 2198 # The list is located here for uniqueness reasons -- it is actually
2198 2199 # managed by the workingctx, but that isn't unique per-repo.
2199 2200 self._postdsstatus.append(ps)
2200 2201
2201 2202 def postdsstatus(self):
2202 2203 """Used by workingctx to get the list of post-dirstate-status hooks."""
2203 2204 return self._postdsstatus
2204 2205
2205 2206 def clearpostdsstatus(self):
2206 2207 """Used by workingctx to clear post-dirstate-status hooks."""
2207 2208 del self._postdsstatus[:]
2208 2209
2209 2210 def heads(self, start=None):
2210 2211 if start is None:
2211 2212 cl = self.changelog
2212 2213 headrevs = reversed(cl.headrevs())
2213 2214 return [cl.node(rev) for rev in headrevs]
2214 2215
2215 2216 heads = self.changelog.heads(start)
2216 2217 # sort the output in rev descending order
2217 2218 return sorted(heads, key=self.changelog.rev, reverse=True)
2218 2219
2219 2220 def branchheads(self, branch=None, start=None, closed=False):
2220 2221 '''return a (possibly filtered) list of heads for the given branch
2221 2222
2222 2223 Heads are returned in topological order, from newest to oldest.
2223 2224 If branch is None, use the dirstate branch.
2224 2225 If start is not None, return only heads reachable from start.
2225 2226 If closed is True, return heads that are marked as closed as well.
2226 2227 '''
2227 2228 if branch is None:
2228 2229 branch = self[None].branch()
2229 2230 branches = self.branchmap()
2230 2231 if branch not in branches:
2231 2232 return []
2232 2233 # the cache returns heads ordered lowest to highest
2233 2234 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
2234 2235 if start is not None:
2235 2236 # filter out the heads that cannot be reached from startrev
2236 2237 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
2237 2238 bheads = [h for h in bheads if h in fbheads]
2238 2239 return bheads
2239 2240
2240 2241 def branches(self, nodes):
2241 2242 if not nodes:
2242 2243 nodes = [self.changelog.tip()]
2243 2244 b = []
2244 2245 for n in nodes:
2245 2246 t = n
2246 2247 while True:
2247 2248 p = self.changelog.parents(n)
2248 2249 if p[1] != nullid or p[0] == nullid:
2249 2250 b.append((t, n, p[0], p[1]))
2250 2251 break
2251 2252 n = p[0]
2252 2253 return b
2253 2254
2254 2255 def between(self, pairs):
2255 2256 r = []
2256 2257
2257 2258 for top, bottom in pairs:
2258 2259 n, l, i = top, [], 0
2259 2260 f = 1
2260 2261
2261 2262 while n != bottom and n != nullid:
2262 2263 p = self.changelog.parents(n)[0]
2263 2264 if i == f:
2264 2265 l.append(n)
2265 2266 f = f * 2
2266 2267 n = p
2267 2268 i += 1
2268 2269
2269 2270 r.append(l)
2270 2271
2271 2272 return r
2272 2273
2273 2274 def checkpush(self, pushop):
2274 2275 """Extensions can override this function if additional checks have
2275 2276 to be performed before pushing, or call it if they override push
2276 2277 command.
2277 2278 """
2278 2279
2279 2280 @unfilteredpropertycache
2280 2281 def prepushoutgoinghooks(self):
2281 2282 """Return util.hooks consists of a pushop with repo, remote, outgoing
2282 2283 methods, which are called before pushing changesets.
2283 2284 """
2284 2285 return util.hooks()
2285 2286
2286 2287 def pushkey(self, namespace, key, old, new):
2287 2288 try:
2288 2289 tr = self.currenttransaction()
2289 2290 hookargs = {}
2290 2291 if tr is not None:
2291 2292 hookargs.update(tr.hookargs)
2292 2293 hookargs = pycompat.strkwargs(hookargs)
2293 2294 hookargs[r'namespace'] = namespace
2294 2295 hookargs[r'key'] = key
2295 2296 hookargs[r'old'] = old
2296 2297 hookargs[r'new'] = new
2297 2298 self.hook('prepushkey', throw=True, **hookargs)
2298 2299 except error.HookAbort as exc:
2299 2300 self.ui.write_err(_("pushkey-abort: %s\n") % exc)
2300 2301 if exc.hint:
2301 2302 self.ui.write_err(_("(%s)\n") % exc.hint)
2302 2303 return False
2303 2304 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
2304 2305 ret = pushkey.push(self, namespace, key, old, new)
2305 2306 def runhook():
2306 2307 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
2307 2308 ret=ret)
2308 2309 self._afterlock(runhook)
2309 2310 return ret
2310 2311
2311 2312 def listkeys(self, namespace):
2312 2313 self.hook('prelistkeys', throw=True, namespace=namespace)
2313 2314 self.ui.debug('listing keys for "%s"\n' % namespace)
2314 2315 values = pushkey.list(self, namespace)
2315 2316 self.hook('listkeys', namespace=namespace, values=values)
2316 2317 return values
2317 2318
2318 2319 def debugwireargs(self, one, two, three=None, four=None, five=None):
2319 2320 '''used to test argument passing over the wire'''
2320 2321 return "%s %s %s %s %s" % (one, two, pycompat.bytestr(three),
2321 2322 pycompat.bytestr(four),
2322 2323 pycompat.bytestr(five))
2323 2324
2324 2325 def savecommitmessage(self, text):
2325 2326 fp = self.vfs('last-message.txt', 'wb')
2326 2327 try:
2327 2328 fp.write(text)
2328 2329 finally:
2329 2330 fp.close()
2330 2331 return self.pathto(fp.name[len(self.root) + 1:])
2331 2332
2332 2333 # used to avoid circular references so destructors work
2333 2334 def aftertrans(files):
2334 2335 renamefiles = [tuple(t) for t in files]
2335 2336 def a():
2336 2337 for vfs, src, dest in renamefiles:
2337 2338 # if src and dest refer to a same file, vfs.rename is a no-op,
2338 2339 # leaving both src and dest on disk. delete dest to make sure
2339 2340 # the rename couldn't be such a no-op.
2340 2341 vfs.tryunlink(dest)
2341 2342 try:
2342 2343 vfs.rename(src, dest)
2343 2344 except OSError: # journal file does not yet exist
2344 2345 pass
2345 2346 return a
2346 2347
2347 2348 def undoname(fn):
2348 2349 base, name = os.path.split(fn)
2349 2350 assert name.startswith('journal')
2350 2351 return os.path.join(base, name.replace('journal', 'undo', 1))
2351 2352
2352 2353 def instance(ui, path, create, intents=None):
2353 2354 return localrepository(ui, util.urllocalpath(path), create,
2354 2355 intents=intents)
2355 2356
2356 2357 def islocal(path):
2357 2358 return True
2358 2359
2359 2360 def newreporequirements(repo):
2360 2361 """Determine the set of requirements for a new local repository.
2361 2362
2362 2363 Extensions can wrap this function to specify custom requirements for
2363 2364 new repositories.
2364 2365 """
2365 2366 ui = repo.ui
2366 2367 requirements = {'revlogv1'}
2367 2368 if ui.configbool('format', 'usestore'):
2368 2369 requirements.add('store')
2369 2370 if ui.configbool('format', 'usefncache'):
2370 2371 requirements.add('fncache')
2371 2372 if ui.configbool('format', 'dotencode'):
2372 2373 requirements.add('dotencode')
2373 2374
2374 2375 compengine = ui.config('experimental', 'format.compression')
2375 2376 if compengine not in util.compengines:
2376 2377 raise error.Abort(_('compression engine %s defined by '
2377 2378 'experimental.format.compression not available') %
2378 2379 compengine,
2379 2380 hint=_('run "hg debuginstall" to list available '
2380 2381 'compression engines'))
2381 2382
2382 2383 # zlib is the historical default and doesn't need an explicit requirement.
2383 2384 if compengine != 'zlib':
2384 2385 requirements.add('exp-compression-%s' % compengine)
2385 2386
2386 2387 if scmutil.gdinitconfig(ui):
2387 2388 requirements.add('generaldelta')
2388 2389 if ui.configbool('experimental', 'treemanifest'):
2389 2390 requirements.add('treemanifest')
2390 2391 # experimental config: format.sparse-revlog
2391 2392 if ui.configbool('format', 'sparse-revlog'):
2392 2393 requirements.add(SPARSEREVLOG_REQUIREMENT)
2393 2394
2394 2395 revlogv2 = ui.config('experimental', 'revlogv2')
2395 2396 if revlogv2 == 'enable-unstable-format-and-corrupt-my-data':
2396 2397 requirements.remove('revlogv1')
2397 2398 # generaldelta is implied by revlogv2.
2398 2399 requirements.discard('generaldelta')
2399 2400 requirements.add(REVLOGV2_REQUIREMENT)
2400 2401
2401 2402 return requirements
@@ -1,2963 +1,2964 b''
1 1 # revlog.py - storage back-end for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """Storage back-end for Mercurial.
9 9
10 10 This provides efficient delta storage with O(1) retrieve and append
11 11 and O(changes) merge between branches.
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import collections
17 17 import contextlib
18 18 import errno
19 19 import hashlib
20 20 import heapq
21 21 import os
22 22 import re
23 23 import struct
24 24 import zlib
25 25
26 26 # import stuff from node for others to import from revlog
27 27 from .node import (
28 28 bin,
29 29 hex,
30 30 nullid,
31 31 nullrev,
32 32 wdirfilenodeids,
33 33 wdirhex,
34 34 wdirid,
35 35 wdirrev,
36 36 )
37 37 from .i18n import _
38 38 from .thirdparty import (
39 39 attr,
40 40 )
41 41 from . import (
42 42 ancestor,
43 43 error,
44 44 mdiff,
45 45 policy,
46 46 pycompat,
47 47 templatefilters,
48 48 util,
49 49 )
50 50 from .utils import (
51 51 stringutil,
52 52 )
53 53
54 54 parsers = policy.importmod(r'parsers')
55 55
56 56 # Aliased for performance.
57 57 _zlibdecompress = zlib.decompress
58 58
59 59 # revlog header flags
60 60 REVLOGV0 = 0
61 61 REVLOGV1 = 1
62 62 # Dummy value until file format is finalized.
63 63 # Reminder: change the bounds check in revlog.__init__ when this is changed.
64 64 REVLOGV2 = 0xDEAD
65 65 FLAG_INLINE_DATA = (1 << 16)
66 66 FLAG_GENERALDELTA = (1 << 17)
67 67 REVLOG_DEFAULT_FLAGS = FLAG_INLINE_DATA
68 68 REVLOG_DEFAULT_FORMAT = REVLOGV1
69 69 REVLOG_DEFAULT_VERSION = REVLOG_DEFAULT_FORMAT | REVLOG_DEFAULT_FLAGS
70 70 REVLOGV1_FLAGS = FLAG_INLINE_DATA | FLAG_GENERALDELTA
71 71 REVLOGV2_FLAGS = REVLOGV1_FLAGS
72 72
73 73 # revlog index flags
74 74 REVIDX_ISCENSORED = (1 << 15) # revision has censor metadata, must be verified
75 75 REVIDX_ELLIPSIS = (1 << 14) # revision hash does not match data (narrowhg)
76 76 REVIDX_EXTSTORED = (1 << 13) # revision data is stored externally
77 77 REVIDX_DEFAULT_FLAGS = 0
78 78 # stable order in which flags need to be processed and their processors applied
79 79 REVIDX_FLAGS_ORDER = [
80 80 REVIDX_ISCENSORED,
81 81 REVIDX_ELLIPSIS,
82 82 REVIDX_EXTSTORED,
83 83 ]
84 84 REVIDX_KNOWN_FLAGS = util.bitsfrom(REVIDX_FLAGS_ORDER)
85 85 # bitmark for flags that could cause rawdata content change
86 86 REVIDX_RAWTEXT_CHANGING_FLAGS = REVIDX_ISCENSORED | REVIDX_EXTSTORED
87 87
88 88 # max size of revlog with inline data
89 89 _maxinline = 131072
90 90 _chunksize = 1048576
91 91
92 92 RevlogError = error.RevlogError
93 93 LookupError = error.LookupError
94 AmbiguousPrefixLookupError = error.AmbiguousPrefixLookupError
94 95 CensoredNodeError = error.CensoredNodeError
95 96 ProgrammingError = error.ProgrammingError
96 97
97 98 # Store flag processors (cf. 'addflagprocessor()' to register)
98 99 _flagprocessors = {
99 100 REVIDX_ISCENSORED: None,
100 101 }
101 102
102 103 _mdre = re.compile('\1\n')
103 104 def parsemeta(text):
104 105 """return (metadatadict, metadatasize)"""
105 106 # text can be buffer, so we can't use .startswith or .index
106 107 if text[:2] != '\1\n':
107 108 return None, None
108 109 s = _mdre.search(text, 2).start()
109 110 mtext = text[2:s]
110 111 meta = {}
111 112 for l in mtext.splitlines():
112 113 k, v = l.split(": ", 1)
113 114 meta[k] = v
114 115 return meta, (s + 2)
115 116
116 117 def packmeta(meta, text):
117 118 keys = sorted(meta)
118 119 metatext = "".join("%s: %s\n" % (k, meta[k]) for k in keys)
119 120 return "\1\n%s\1\n%s" % (metatext, text)
120 121
121 122 def _censoredtext(text):
122 123 m, offs = parsemeta(text)
123 124 return m and "censored" in m
124 125
125 126 def addflagprocessor(flag, processor):
126 127 """Register a flag processor on a revision data flag.
127 128
128 129 Invariant:
129 130 - Flags need to be defined in REVIDX_KNOWN_FLAGS and REVIDX_FLAGS_ORDER,
130 131 and REVIDX_RAWTEXT_CHANGING_FLAGS if they can alter rawtext.
131 132 - Only one flag processor can be registered on a specific flag.
132 133 - flagprocessors must be 3-tuples of functions (read, write, raw) with the
133 134 following signatures:
134 135 - (read) f(self, rawtext) -> text, bool
135 136 - (write) f(self, text) -> rawtext, bool
136 137 - (raw) f(self, rawtext) -> bool
137 138 "text" is presented to the user. "rawtext" is stored in revlog data, not
138 139 directly visible to the user.
139 140 The boolean returned by these transforms is used to determine whether
140 141 the returned text can be used for hash integrity checking. For example,
141 142 if "write" returns False, then "text" is used to generate hash. If
142 143 "write" returns True, that basically means "rawtext" returned by "write"
143 144 should be used to generate hash. Usually, "write" and "read" return
144 145 different booleans. And "raw" returns a same boolean as "write".
145 146
146 147 Note: The 'raw' transform is used for changegroup generation and in some
147 148 debug commands. In this case the transform only indicates whether the
148 149 contents can be used for hash integrity checks.
149 150 """
150 151 if not flag & REVIDX_KNOWN_FLAGS:
151 152 msg = _("cannot register processor on unknown flag '%#x'.") % (flag)
152 153 raise ProgrammingError(msg)
153 154 if flag not in REVIDX_FLAGS_ORDER:
154 155 msg = _("flag '%#x' undefined in REVIDX_FLAGS_ORDER.") % (flag)
155 156 raise ProgrammingError(msg)
156 157 if flag in _flagprocessors:
157 158 msg = _("cannot register multiple processors on flag '%#x'.") % (flag)
158 159 raise error.Abort(msg)
159 160 _flagprocessors[flag] = processor
160 161
161 162 def getoffset(q):
162 163 return int(q >> 16)
163 164
164 165 def gettype(q):
165 166 return int(q & 0xFFFF)
166 167
167 168 def offset_type(offset, type):
168 169 if (type & ~REVIDX_KNOWN_FLAGS) != 0:
169 170 raise ValueError('unknown revlog index flags')
170 171 return int(int(offset) << 16 | type)
171 172
172 173 _nullhash = hashlib.sha1(nullid)
173 174
174 175 def hash(text, p1, p2):
175 176 """generate a hash from the given text and its parent hashes
176 177
177 178 This hash combines both the current file contents and its history
178 179 in a manner that makes it easy to distinguish nodes with the same
179 180 content in the revision graph.
180 181 """
181 182 # As of now, if one of the parent node is null, p2 is null
182 183 if p2 == nullid:
183 184 # deep copy of a hash is faster than creating one
184 185 s = _nullhash.copy()
185 186 s.update(p1)
186 187 else:
187 188 # none of the parent nodes are nullid
188 189 if p1 < p2:
189 190 a = p1
190 191 b = p2
191 192 else:
192 193 a = p2
193 194 b = p1
194 195 s = hashlib.sha1(a)
195 196 s.update(b)
196 197 s.update(text)
197 198 return s.digest()
198 199
199 200 class _testrevlog(object):
200 201 """minimalist fake revlog to use in doctests"""
201 202
202 203 def __init__(self, data, density=0.5, mingap=0):
203 204 """data is an list of revision payload boundaries"""
204 205 self._data = data
205 206 self._srdensitythreshold = density
206 207 self._srmingapsize = mingap
207 208
208 209 def start(self, rev):
209 210 if rev == 0:
210 211 return 0
211 212 return self._data[rev - 1]
212 213
213 214 def end(self, rev):
214 215 return self._data[rev]
215 216
216 217 def length(self, rev):
217 218 return self.end(rev) - self.start(rev)
218 219
219 220 def __len__(self):
220 221 return len(self._data)
221 222
222 223 def _trimchunk(revlog, revs, startidx, endidx=None):
223 224 """returns revs[startidx:endidx] without empty trailing revs
224 225
225 226 Doctest Setup
226 227 >>> revlog = _testrevlog([
227 228 ... 5, #0
228 229 ... 10, #1
229 230 ... 12, #2
230 231 ... 12, #3 (empty)
231 232 ... 17, #4
232 233 ... 21, #5
233 234 ... 21, #6 (empty)
234 235 ... ])
235 236
236 237 Contiguous cases:
237 238 >>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 0)
238 239 [0, 1, 2, 3, 4, 5]
239 240 >>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 0, 5)
240 241 [0, 1, 2, 3, 4]
241 242 >>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 0, 4)
242 243 [0, 1, 2]
243 244 >>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 2, 4)
244 245 [2]
245 246 >>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 3)
246 247 [3, 4, 5]
247 248 >>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 3, 5)
248 249 [3, 4]
249 250
250 251 Discontiguous cases:
251 252 >>> _trimchunk(revlog, [1, 3, 5, 6], 0)
252 253 [1, 3, 5]
253 254 >>> _trimchunk(revlog, [1, 3, 5, 6], 0, 2)
254 255 [1]
255 256 >>> _trimchunk(revlog, [1, 3, 5, 6], 1, 3)
256 257 [3, 5]
257 258 >>> _trimchunk(revlog, [1, 3, 5, 6], 1)
258 259 [3, 5]
259 260 """
260 261 length = revlog.length
261 262
262 263 if endidx is None:
263 264 endidx = len(revs)
264 265
265 266 # Trim empty revs at the end, but never the very first revision of a chain
266 267 while endidx > 1 and endidx > startidx and length(revs[endidx - 1]) == 0:
267 268 endidx -= 1
268 269
269 270 return revs[startidx:endidx]
270 271
271 272 def _segmentspan(revlog, revs):
272 273 """Get the byte span of a segment of revisions
273 274
274 275 revs is a sorted array of revision numbers
275 276
276 277 >>> revlog = _testrevlog([
277 278 ... 5, #0
278 279 ... 10, #1
279 280 ... 12, #2
280 281 ... 12, #3 (empty)
281 282 ... 17, #4
282 283 ... ])
283 284
284 285 >>> _segmentspan(revlog, [0, 1, 2, 3, 4])
285 286 17
286 287 >>> _segmentspan(revlog, [0, 4])
287 288 17
288 289 >>> _segmentspan(revlog, [3, 4])
289 290 5
290 291 >>> _segmentspan(revlog, [1, 2, 3,])
291 292 7
292 293 >>> _segmentspan(revlog, [1, 3])
293 294 7
294 295 """
295 296 if not revs:
296 297 return 0
297 298 return revlog.end(revs[-1]) - revlog.start(revs[0])
298 299
299 300 def _slicechunk(revlog, revs, deltainfo=None, targetsize=None):
300 301 """slice revs to reduce the amount of unrelated data to be read from disk.
301 302
302 303 ``revs`` is sliced into groups that should be read in one time.
303 304 Assume that revs are sorted.
304 305
305 306 The initial chunk is sliced until the overall density (payload/chunks-span
306 307 ratio) is above `revlog._srdensitythreshold`. No gap smaller than
307 308 `revlog._srmingapsize` is skipped.
308 309
309 310 If `targetsize` is set, no chunk larger than `targetsize` will be yield.
310 311 For consistency with other slicing choice, this limit won't go lower than
311 312 `revlog._srmingapsize`.
312 313
313 314 If individual revisions chunk are larger than this limit, they will still
314 315 be raised individually.
315 316
316 317 >>> revlog = _testrevlog([
317 318 ... 5, #00 (5)
318 319 ... 10, #01 (5)
319 320 ... 12, #02 (2)
320 321 ... 12, #03 (empty)
321 322 ... 27, #04 (15)
322 323 ... 31, #05 (4)
323 324 ... 31, #06 (empty)
324 325 ... 42, #07 (11)
325 326 ... 47, #08 (5)
326 327 ... 47, #09 (empty)
327 328 ... 48, #10 (1)
328 329 ... 51, #11 (3)
329 330 ... 74, #12 (23)
330 331 ... 85, #13 (11)
331 332 ... 86, #14 (1)
332 333 ... 91, #15 (5)
333 334 ... ])
334 335
335 336 >>> list(_slicechunk(revlog, list(range(16))))
336 337 [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]]
337 338 >>> list(_slicechunk(revlog, [0, 15]))
338 339 [[0], [15]]
339 340 >>> list(_slicechunk(revlog, [0, 11, 15]))
340 341 [[0], [11], [15]]
341 342 >>> list(_slicechunk(revlog, [0, 11, 13, 15]))
342 343 [[0], [11, 13, 15]]
343 344 >>> list(_slicechunk(revlog, [1, 2, 3, 5, 8, 10, 11, 14]))
344 345 [[1, 2], [5, 8, 10, 11], [14]]
345 346
346 347 Slicing with a maximum chunk size
347 348 >>> list(_slicechunk(revlog, [0, 11, 13, 15], targetsize=15))
348 349 [[0], [11], [13], [15]]
349 350 >>> list(_slicechunk(revlog, [0, 11, 13, 15], targetsize=20))
350 351 [[0], [11], [13, 15]]
351 352 """
352 353 if targetsize is not None:
353 354 targetsize = max(targetsize, revlog._srmingapsize)
354 355 # targetsize should not be specified when evaluating delta candidates:
355 356 # * targetsize is used to ensure we stay within specification when reading,
356 357 # * deltainfo is used to pick are good delta chain when writing.
357 358 if not (deltainfo is None or targetsize is None):
358 359 msg = 'cannot use `targetsize` with a `deltainfo`'
359 360 raise error.ProgrammingError(msg)
360 361 for chunk in _slicechunktodensity(revlog, revs,
361 362 deltainfo,
362 363 revlog._srdensitythreshold,
363 364 revlog._srmingapsize):
364 365 for subchunk in _slicechunktosize(revlog, chunk, targetsize):
365 366 yield subchunk
366 367
367 368 def _slicechunktosize(revlog, revs, targetsize=None):
368 369 """slice revs to match the target size
369 370
370 371 This is intended to be used on chunk that density slicing selected by that
371 372 are still too large compared to the read garantee of revlog. This might
372 373 happens when "minimal gap size" interrupted the slicing or when chain are
373 374 built in a way that create large blocks next to each other.
374 375
375 376 >>> revlog = _testrevlog([
376 377 ... 3, #0 (3)
377 378 ... 5, #1 (2)
378 379 ... 6, #2 (1)
379 380 ... 8, #3 (2)
380 381 ... 8, #4 (empty)
381 382 ... 11, #5 (3)
382 383 ... 12, #6 (1)
383 384 ... 13, #7 (1)
384 385 ... 14, #8 (1)
385 386 ... ])
386 387
387 388 Cases where chunk is already small enough
388 389 >>> list(_slicechunktosize(revlog, [0], 3))
389 390 [[0]]
390 391 >>> list(_slicechunktosize(revlog, [6, 7], 3))
391 392 [[6, 7]]
392 393 >>> list(_slicechunktosize(revlog, [0], None))
393 394 [[0]]
394 395 >>> list(_slicechunktosize(revlog, [6, 7], None))
395 396 [[6, 7]]
396 397
397 398 cases where we need actual slicing
398 399 >>> list(_slicechunktosize(revlog, [0, 1], 3))
399 400 [[0], [1]]
400 401 >>> list(_slicechunktosize(revlog, [1, 3], 3))
401 402 [[1], [3]]
402 403 >>> list(_slicechunktosize(revlog, [1, 2, 3], 3))
403 404 [[1, 2], [3]]
404 405 >>> list(_slicechunktosize(revlog, [3, 5], 3))
405 406 [[3], [5]]
406 407 >>> list(_slicechunktosize(revlog, [3, 4, 5], 3))
407 408 [[3], [5]]
408 409 >>> list(_slicechunktosize(revlog, [5, 6, 7, 8], 3))
409 410 [[5], [6, 7, 8]]
410 411 >>> list(_slicechunktosize(revlog, [0, 1, 2, 3, 4, 5, 6, 7, 8], 3))
411 412 [[0], [1, 2], [3], [5], [6, 7, 8]]
412 413
413 414 Case with too large individual chunk (must return valid chunk)
414 415 >>> list(_slicechunktosize(revlog, [0, 1], 2))
415 416 [[0], [1]]
416 417 >>> list(_slicechunktosize(revlog, [1, 3], 1))
417 418 [[1], [3]]
418 419 >>> list(_slicechunktosize(revlog, [3, 4, 5], 2))
419 420 [[3], [5]]
420 421 """
421 422 assert targetsize is None or 0 <= targetsize
422 423 if targetsize is None or _segmentspan(revlog, revs) <= targetsize:
423 424 yield revs
424 425 return
425 426
426 427 startrevidx = 0
427 428 startdata = revlog.start(revs[0])
428 429 endrevidx = 0
429 430 iterrevs = enumerate(revs)
430 431 next(iterrevs) # skip first rev.
431 432 for idx, r in iterrevs:
432 433 span = revlog.end(r) - startdata
433 434 if span <= targetsize:
434 435 endrevidx = idx
435 436 else:
436 437 chunk = _trimchunk(revlog, revs, startrevidx, endrevidx + 1)
437 438 if chunk:
438 439 yield chunk
439 440 startrevidx = idx
440 441 startdata = revlog.start(r)
441 442 endrevidx = idx
442 443 yield _trimchunk(revlog, revs, startrevidx)
443 444
444 445 def _slicechunktodensity(revlog, revs, deltainfo=None, targetdensity=0.5,
445 446 mingapsize=0):
446 447 """slice revs to reduce the amount of unrelated data to be read from disk.
447 448
448 449 ``revs`` is sliced into groups that should be read in one time.
449 450 Assume that revs are sorted.
450 451
451 452 ``deltainfo`` is a _deltainfo instance of a revision that we would append
452 453 to the top of the revlog.
453 454
454 455 The initial chunk is sliced until the overall density (payload/chunks-span
455 456 ratio) is above `targetdensity`. No gap smaller than `mingapsize` is
456 457 skipped.
457 458
458 459 >>> revlog = _testrevlog([
459 460 ... 5, #00 (5)
460 461 ... 10, #01 (5)
461 462 ... 12, #02 (2)
462 463 ... 12, #03 (empty)
463 464 ... 27, #04 (15)
464 465 ... 31, #05 (4)
465 466 ... 31, #06 (empty)
466 467 ... 42, #07 (11)
467 468 ... 47, #08 (5)
468 469 ... 47, #09 (empty)
469 470 ... 48, #10 (1)
470 471 ... 51, #11 (3)
471 472 ... 74, #12 (23)
472 473 ... 85, #13 (11)
473 474 ... 86, #14 (1)
474 475 ... 91, #15 (5)
475 476 ... ])
476 477
477 478 >>> list(_slicechunktodensity(revlog, list(range(16))))
478 479 [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]]
479 480 >>> list(_slicechunktodensity(revlog, [0, 15]))
480 481 [[0], [15]]
481 482 >>> list(_slicechunktodensity(revlog, [0, 11, 15]))
482 483 [[0], [11], [15]]
483 484 >>> list(_slicechunktodensity(revlog, [0, 11, 13, 15]))
484 485 [[0], [11, 13, 15]]
485 486 >>> list(_slicechunktodensity(revlog, [1, 2, 3, 5, 8, 10, 11, 14]))
486 487 [[1, 2], [5, 8, 10, 11], [14]]
487 488 >>> list(_slicechunktodensity(revlog, [1, 2, 3, 5, 8, 10, 11, 14],
488 489 ... mingapsize=20))
489 490 [[1, 2, 3, 5, 8, 10, 11], [14]]
490 491 >>> list(_slicechunktodensity(revlog, [1, 2, 3, 5, 8, 10, 11, 14],
491 492 ... targetdensity=0.95))
492 493 [[1, 2], [5], [8, 10, 11], [14]]
493 494 >>> list(_slicechunktodensity(revlog, [1, 2, 3, 5, 8, 10, 11, 14],
494 495 ... targetdensity=0.95, mingapsize=12))
495 496 [[1, 2], [5, 8, 10, 11], [14]]
496 497 """
497 498 start = revlog.start
498 499 length = revlog.length
499 500
500 501 if len(revs) <= 1:
501 502 yield revs
502 503 return
503 504
504 505 nextrev = len(revlog)
505 506 nextoffset = revlog.end(nextrev - 1)
506 507
507 508 if deltainfo is None:
508 509 deltachainspan = _segmentspan(revlog, revs)
509 510 chainpayload = sum(length(r) for r in revs)
510 511 else:
511 512 deltachainspan = deltainfo.distance
512 513 chainpayload = deltainfo.compresseddeltalen
513 514
514 515 if deltachainspan < mingapsize:
515 516 yield revs
516 517 return
517 518
518 519 readdata = deltachainspan
519 520
520 521 if deltachainspan:
521 522 density = chainpayload / float(deltachainspan)
522 523 else:
523 524 density = 1.0
524 525
525 526 if density >= targetdensity:
526 527 yield revs
527 528 return
528 529
529 530 if deltainfo is not None:
530 531 revs = list(revs)
531 532 revs.append(nextrev)
532 533
533 534 # Store the gaps in a heap to have them sorted by decreasing size
534 535 gapsheap = []
535 536 heapq.heapify(gapsheap)
536 537 prevend = None
537 538 for i, rev in enumerate(revs):
538 539 if rev < nextrev:
539 540 revstart = start(rev)
540 541 revlen = length(rev)
541 542 else:
542 543 revstart = nextoffset
543 544 revlen = deltainfo.deltalen
544 545
545 546 # Skip empty revisions to form larger holes
546 547 if revlen == 0:
547 548 continue
548 549
549 550 if prevend is not None:
550 551 gapsize = revstart - prevend
551 552 # only consider holes that are large enough
552 553 if gapsize > mingapsize:
553 554 heapq.heappush(gapsheap, (-gapsize, i))
554 555
555 556 prevend = revstart + revlen
556 557
557 558 # Collect the indices of the largest holes until the density is acceptable
558 559 indicesheap = []
559 560 heapq.heapify(indicesheap)
560 561 while gapsheap and density < targetdensity:
561 562 oppgapsize, gapidx = heapq.heappop(gapsheap)
562 563
563 564 heapq.heappush(indicesheap, gapidx)
564 565
565 566 # the gap sizes are stored as negatives to be sorted decreasingly
566 567 # by the heap
567 568 readdata -= (-oppgapsize)
568 569 if readdata > 0:
569 570 density = chainpayload / float(readdata)
570 571 else:
571 572 density = 1.0
572 573
573 574 # Cut the revs at collected indices
574 575 previdx = 0
575 576 while indicesheap:
576 577 idx = heapq.heappop(indicesheap)
577 578
578 579 chunk = _trimchunk(revlog, revs, previdx, idx)
579 580 if chunk:
580 581 yield chunk
581 582
582 583 previdx = idx
583 584
584 585 chunk = _trimchunk(revlog, revs, previdx)
585 586 if chunk:
586 587 yield chunk
587 588
588 589 @attr.s(slots=True, frozen=True)
589 590 class _deltainfo(object):
590 591 distance = attr.ib()
591 592 deltalen = attr.ib()
592 593 data = attr.ib()
593 594 base = attr.ib()
594 595 chainbase = attr.ib()
595 596 chainlen = attr.ib()
596 597 compresseddeltalen = attr.ib()
597 598
598 599 class _deltacomputer(object):
599 600 def __init__(self, revlog):
600 601 self.revlog = revlog
601 602
602 603 def _getcandidaterevs(self, p1, p2, cachedelta):
603 604 """
604 605 Provides revisions that present an interest to be diffed against,
605 606 grouped by level of easiness.
606 607 """
607 608 revlog = self.revlog
608 609 gdelta = revlog._generaldelta
609 610 curr = len(revlog)
610 611 prev = curr - 1
611 612 p1r, p2r = revlog.rev(p1), revlog.rev(p2)
612 613
613 614 # should we try to build a delta?
614 615 if prev != nullrev and revlog.storedeltachains:
615 616 tested = set()
616 617 # This condition is true most of the time when processing
617 618 # changegroup data into a generaldelta repo. The only time it
618 619 # isn't true is if this is the first revision in a delta chain
619 620 # or if ``format.generaldelta=true`` disabled ``lazydeltabase``.
620 621 if cachedelta and gdelta and revlog._lazydeltabase:
621 622 # Assume what we received from the server is a good choice
622 623 # build delta will reuse the cache
623 624 yield (cachedelta[0],)
624 625 tested.add(cachedelta[0])
625 626
626 627 if gdelta:
627 628 # exclude already lazy tested base if any
628 629 parents = [p for p in (p1r, p2r)
629 630 if p != nullrev and p not in tested]
630 631
631 632 if not revlog._deltabothparents and len(parents) == 2:
632 633 parents.sort()
633 634 # To minimize the chance of having to build a fulltext,
634 635 # pick first whichever parent is closest to us (max rev)
635 636 yield (parents[1],)
636 637 # then the other one (min rev) if the first did not fit
637 638 yield (parents[0],)
638 639 tested.update(parents)
639 640 elif len(parents) > 0:
640 641 # Test all parents (1 or 2), and keep the best candidate
641 642 yield parents
642 643 tested.update(parents)
643 644
644 645 if prev not in tested:
645 646 # other approach failed try against prev to hopefully save us a
646 647 # fulltext.
647 648 yield (prev,)
648 649 tested.add(prev)
649 650
650 651 def buildtext(self, revinfo, fh):
651 652 """Builds a fulltext version of a revision
652 653
653 654 revinfo: _revisioninfo instance that contains all needed info
654 655 fh: file handle to either the .i or the .d revlog file,
655 656 depending on whether it is inlined or not
656 657 """
657 658 btext = revinfo.btext
658 659 if btext[0] is not None:
659 660 return btext[0]
660 661
661 662 revlog = self.revlog
662 663 cachedelta = revinfo.cachedelta
663 664 flags = revinfo.flags
664 665 node = revinfo.node
665 666
666 667 baserev = cachedelta[0]
667 668 delta = cachedelta[1]
668 669 # special case deltas which replace entire base; no need to decode
669 670 # base revision. this neatly avoids censored bases, which throw when
670 671 # they're decoded.
671 672 hlen = struct.calcsize(">lll")
672 673 if delta[:hlen] == mdiff.replacediffheader(revlog.rawsize(baserev),
673 674 len(delta) - hlen):
674 675 btext[0] = delta[hlen:]
675 676 else:
676 677 # deltabase is rawtext before changed by flag processors, which is
677 678 # equivalent to non-raw text
678 679 basetext = revlog.revision(baserev, _df=fh, raw=False)
679 680 btext[0] = mdiff.patch(basetext, delta)
680 681
681 682 try:
682 683 res = revlog._processflags(btext[0], flags, 'read', raw=True)
683 684 btext[0], validatehash = res
684 685 if validatehash:
685 686 revlog.checkhash(btext[0], node, p1=revinfo.p1, p2=revinfo.p2)
686 687 if flags & REVIDX_ISCENSORED:
687 688 raise RevlogError(_('node %s is not censored') % node)
688 689 except CensoredNodeError:
689 690 # must pass the censored index flag to add censored revisions
690 691 if not flags & REVIDX_ISCENSORED:
691 692 raise
692 693 return btext[0]
693 694
694 695 def _builddeltadiff(self, base, revinfo, fh):
695 696 revlog = self.revlog
696 697 t = self.buildtext(revinfo, fh)
697 698 if revlog.iscensored(base):
698 699 # deltas based on a censored revision must replace the
699 700 # full content in one patch, so delta works everywhere
700 701 header = mdiff.replacediffheader(revlog.rawsize(base), len(t))
701 702 delta = header + t
702 703 else:
703 704 ptext = revlog.revision(base, _df=fh, raw=True)
704 705 delta = mdiff.textdiff(ptext, t)
705 706
706 707 return delta
707 708
708 709 def _builddeltainfo(self, revinfo, base, fh):
709 710 # can we use the cached delta?
710 711 if revinfo.cachedelta and revinfo.cachedelta[0] == base:
711 712 delta = revinfo.cachedelta[1]
712 713 else:
713 714 delta = self._builddeltadiff(base, revinfo, fh)
714 715 revlog = self.revlog
715 716 header, data = revlog.compress(delta)
716 717 deltalen = len(header) + len(data)
717 718 chainbase = revlog.chainbase(base)
718 719 offset = revlog.end(len(revlog) - 1)
719 720 dist = deltalen + offset - revlog.start(chainbase)
720 721 if revlog._generaldelta:
721 722 deltabase = base
722 723 else:
723 724 deltabase = chainbase
724 725 chainlen, compresseddeltalen = revlog._chaininfo(base)
725 726 chainlen += 1
726 727 compresseddeltalen += deltalen
727 728 return _deltainfo(dist, deltalen, (header, data), deltabase,
728 729 chainbase, chainlen, compresseddeltalen)
729 730
730 731 def finddeltainfo(self, revinfo, fh):
731 732 """Find an acceptable delta against a candidate revision
732 733
733 734 revinfo: information about the revision (instance of _revisioninfo)
734 735 fh: file handle to either the .i or the .d revlog file,
735 736 depending on whether it is inlined or not
736 737
737 738 Returns the first acceptable candidate revision, as ordered by
738 739 _getcandidaterevs
739 740 """
740 741 cachedelta = revinfo.cachedelta
741 742 p1 = revinfo.p1
742 743 p2 = revinfo.p2
743 744 revlog = self.revlog
744 745
745 746 deltainfo = None
746 747 for candidaterevs in self._getcandidaterevs(p1, p2, cachedelta):
747 748 nominateddeltas = []
748 749 for candidaterev in candidaterevs:
749 750 # no delta for rawtext-changing revs (see "candelta" for why)
750 751 if revlog.flags(candidaterev) & REVIDX_RAWTEXT_CHANGING_FLAGS:
751 752 continue
752 753 candidatedelta = self._builddeltainfo(revinfo, candidaterev, fh)
753 754 if revlog._isgooddeltainfo(candidatedelta, revinfo):
754 755 nominateddeltas.append(candidatedelta)
755 756 if nominateddeltas:
756 757 deltainfo = min(nominateddeltas, key=lambda x: x.deltalen)
757 758 break
758 759
759 760 return deltainfo
760 761
761 762 @attr.s(slots=True, frozen=True)
762 763 class _revisioninfo(object):
763 764 """Information about a revision that allows building its fulltext
764 765 node: expected hash of the revision
765 766 p1, p2: parent revs of the revision
766 767 btext: built text cache consisting of a one-element list
767 768 cachedelta: (baserev, uncompressed_delta) or None
768 769 flags: flags associated to the revision storage
769 770
770 771 One of btext[0] or cachedelta must be set.
771 772 """
772 773 node = attr.ib()
773 774 p1 = attr.ib()
774 775 p2 = attr.ib()
775 776 btext = attr.ib()
776 777 textlen = attr.ib()
777 778 cachedelta = attr.ib()
778 779 flags = attr.ib()
779 780
780 781 # index v0:
781 782 # 4 bytes: offset
782 783 # 4 bytes: compressed length
783 784 # 4 bytes: base rev
784 785 # 4 bytes: link rev
785 786 # 20 bytes: parent 1 nodeid
786 787 # 20 bytes: parent 2 nodeid
787 788 # 20 bytes: nodeid
788 789 indexformatv0 = struct.Struct(">4l20s20s20s")
789 790 indexformatv0_pack = indexformatv0.pack
790 791 indexformatv0_unpack = indexformatv0.unpack
791 792
792 793 class revlogoldio(object):
793 794 def __init__(self):
794 795 self.size = indexformatv0.size
795 796
796 797 def parseindex(self, data, inline):
797 798 s = self.size
798 799 index = []
799 800 nodemap = {nullid: nullrev}
800 801 n = off = 0
801 802 l = len(data)
802 803 while off + s <= l:
803 804 cur = data[off:off + s]
804 805 off += s
805 806 e = indexformatv0_unpack(cur)
806 807 # transform to revlogv1 format
807 808 e2 = (offset_type(e[0], 0), e[1], -1, e[2], e[3],
808 809 nodemap.get(e[4], nullrev), nodemap.get(e[5], nullrev), e[6])
809 810 index.append(e2)
810 811 nodemap[e[6]] = n
811 812 n += 1
812 813
813 814 # add the magic null revision at -1
814 815 index.append((0, 0, 0, -1, -1, -1, -1, nullid))
815 816
816 817 return index, nodemap, None
817 818
818 819 def packentry(self, entry, node, version, rev):
819 820 if gettype(entry[0]):
820 821 raise RevlogError(_('index entry flags need revlog version 1'))
821 822 e2 = (getoffset(entry[0]), entry[1], entry[3], entry[4],
822 823 node(entry[5]), node(entry[6]), entry[7])
823 824 return indexformatv0_pack(*e2)
824 825
825 826 # index ng:
826 827 # 6 bytes: offset
827 828 # 2 bytes: flags
828 829 # 4 bytes: compressed length
829 830 # 4 bytes: uncompressed length
830 831 # 4 bytes: base rev
831 832 # 4 bytes: link rev
832 833 # 4 bytes: parent 1 rev
833 834 # 4 bytes: parent 2 rev
834 835 # 32 bytes: nodeid
835 836 indexformatng = struct.Struct(">Qiiiiii20s12x")
836 837 indexformatng_pack = indexformatng.pack
837 838 versionformat = struct.Struct(">I")
838 839 versionformat_pack = versionformat.pack
839 840 versionformat_unpack = versionformat.unpack
840 841
841 842 # corresponds to uncompressed length of indexformatng (2 gigs, 4-byte
842 843 # signed integer)
843 844 _maxentrysize = 0x7fffffff
844 845
845 846 class revlogio(object):
846 847 def __init__(self):
847 848 self.size = indexformatng.size
848 849
849 850 def parseindex(self, data, inline):
850 851 # call the C implementation to parse the index data
851 852 index, cache = parsers.parse_index2(data, inline)
852 853 return index, getattr(index, 'nodemap', None), cache
853 854
854 855 def packentry(self, entry, node, version, rev):
855 856 p = indexformatng_pack(*entry)
856 857 if rev == 0:
857 858 p = versionformat_pack(version) + p[4:]
858 859 return p
859 860
860 861 class revlog(object):
861 862 """
862 863 the underlying revision storage object
863 864
864 865 A revlog consists of two parts, an index and the revision data.
865 866
866 867 The index is a file with a fixed record size containing
867 868 information on each revision, including its nodeid (hash), the
868 869 nodeids of its parents, the position and offset of its data within
869 870 the data file, and the revision it's based on. Finally, each entry
870 871 contains a linkrev entry that can serve as a pointer to external
871 872 data.
872 873
873 874 The revision data itself is a linear collection of data chunks.
874 875 Each chunk represents a revision and is usually represented as a
875 876 delta against the previous chunk. To bound lookup time, runs of
876 877 deltas are limited to about 2 times the length of the original
877 878 version data. This makes retrieval of a version proportional to
878 879 its size, or O(1) relative to the number of revisions.
879 880
880 881 Both pieces of the revlog are written to in an append-only
881 882 fashion, which means we never need to rewrite a file to insert or
882 883 remove data, and can use some simple techniques to avoid the need
883 884 for locking while reading.
884 885
885 886 If checkambig, indexfile is opened with checkambig=True at
886 887 writing, to avoid file stat ambiguity.
887 888
888 889 If mmaplargeindex is True, and an mmapindexthreshold is set, the
889 890 index will be mmapped rather than read if it is larger than the
890 891 configured threshold.
891 892
892 893 If censorable is True, the revlog can have censored revisions.
893 894 """
894 895 def __init__(self, opener, indexfile, datafile=None, checkambig=False,
895 896 mmaplargeindex=False, censorable=False):
896 897 """
897 898 create a revlog object
898 899
899 900 opener is a function that abstracts the file opening operation
900 901 and can be used to implement COW semantics or the like.
901 902 """
902 903 self.indexfile = indexfile
903 904 self.datafile = datafile or (indexfile[:-2] + ".d")
904 905 self.opener = opener
905 906 # When True, indexfile is opened with checkambig=True at writing, to
906 907 # avoid file stat ambiguity.
907 908 self._checkambig = checkambig
908 909 self._censorable = censorable
909 910 # 3-tuple of (node, rev, text) for a raw revision.
910 911 self._cache = None
911 912 # Maps rev to chain base rev.
912 913 self._chainbasecache = util.lrucachedict(100)
913 914 # 2-tuple of (offset, data) of raw data from the revlog at an offset.
914 915 self._chunkcache = (0, '')
915 916 # How much data to read and cache into the raw revlog data cache.
916 917 self._chunkcachesize = 65536
917 918 self._maxchainlen = None
918 919 self._deltabothparents = True
919 920 self.index = []
920 921 # Mapping of partial identifiers to full nodes.
921 922 self._pcache = {}
922 923 # Mapping of revision integer to full node.
923 924 self._nodecache = {nullid: nullrev}
924 925 self._nodepos = None
925 926 self._compengine = 'zlib'
926 927 self._maxdeltachainspan = -1
927 928 self._withsparseread = False
928 929 self._sparserevlog = False
929 930 self._srdensitythreshold = 0.50
930 931 self._srmingapsize = 262144
931 932
932 933 mmapindexthreshold = None
933 934 v = REVLOG_DEFAULT_VERSION
934 935 opts = getattr(opener, 'options', None)
935 936 if opts is not None:
936 937 if 'revlogv2' in opts:
937 938 # version 2 revlogs always use generaldelta.
938 939 v = REVLOGV2 | FLAG_GENERALDELTA | FLAG_INLINE_DATA
939 940 elif 'revlogv1' in opts:
940 941 if 'generaldelta' in opts:
941 942 v |= FLAG_GENERALDELTA
942 943 else:
943 944 v = 0
944 945 if 'chunkcachesize' in opts:
945 946 self._chunkcachesize = opts['chunkcachesize']
946 947 if 'maxchainlen' in opts:
947 948 self._maxchainlen = opts['maxchainlen']
948 949 if 'deltabothparents' in opts:
949 950 self._deltabothparents = opts['deltabothparents']
950 951 self._lazydeltabase = bool(opts.get('lazydeltabase', False))
951 952 if 'compengine' in opts:
952 953 self._compengine = opts['compengine']
953 954 if 'maxdeltachainspan' in opts:
954 955 self._maxdeltachainspan = opts['maxdeltachainspan']
955 956 if mmaplargeindex and 'mmapindexthreshold' in opts:
956 957 mmapindexthreshold = opts['mmapindexthreshold']
957 958 self._sparserevlog = bool(opts.get('sparse-revlog', False))
958 959 withsparseread = bool(opts.get('with-sparse-read', False))
959 960 # sparse-revlog forces sparse-read
960 961 self._withsparseread = self._sparserevlog or withsparseread
961 962 if 'sparse-read-density-threshold' in opts:
962 963 self._srdensitythreshold = opts['sparse-read-density-threshold']
963 964 if 'sparse-read-min-gap-size' in opts:
964 965 self._srmingapsize = opts['sparse-read-min-gap-size']
965 966
966 967 if self._chunkcachesize <= 0:
967 968 raise RevlogError(_('revlog chunk cache size %r is not greater '
968 969 'than 0') % self._chunkcachesize)
969 970 elif self._chunkcachesize & (self._chunkcachesize - 1):
970 971 raise RevlogError(_('revlog chunk cache size %r is not a power '
971 972 'of 2') % self._chunkcachesize)
972 973
973 974 indexdata = ''
974 975 self._initempty = True
975 976 try:
976 977 with self._indexfp() as f:
977 978 if (mmapindexthreshold is not None and
978 979 self.opener.fstat(f).st_size >= mmapindexthreshold):
979 980 indexdata = util.buffer(util.mmapread(f))
980 981 else:
981 982 indexdata = f.read()
982 983 if len(indexdata) > 0:
983 984 v = versionformat_unpack(indexdata[:4])[0]
984 985 self._initempty = False
985 986 except IOError as inst:
986 987 if inst.errno != errno.ENOENT:
987 988 raise
988 989
989 990 self.version = v
990 991 self._inline = v & FLAG_INLINE_DATA
991 992 self._generaldelta = v & FLAG_GENERALDELTA
992 993 flags = v & ~0xFFFF
993 994 fmt = v & 0xFFFF
994 995 if fmt == REVLOGV0:
995 996 if flags:
996 997 raise RevlogError(_('unknown flags (%#04x) in version %d '
997 998 'revlog %s') %
998 999 (flags >> 16, fmt, self.indexfile))
999 1000 elif fmt == REVLOGV1:
1000 1001 if flags & ~REVLOGV1_FLAGS:
1001 1002 raise RevlogError(_('unknown flags (%#04x) in version %d '
1002 1003 'revlog %s') %
1003 1004 (flags >> 16, fmt, self.indexfile))
1004 1005 elif fmt == REVLOGV2:
1005 1006 if flags & ~REVLOGV2_FLAGS:
1006 1007 raise RevlogError(_('unknown flags (%#04x) in version %d '
1007 1008 'revlog %s') %
1008 1009 (flags >> 16, fmt, self.indexfile))
1009 1010 else:
1010 1011 raise RevlogError(_('unknown version (%d) in revlog %s') %
1011 1012 (fmt, self.indexfile))
1012 1013
1013 1014 self.storedeltachains = True
1014 1015
1015 1016 self._io = revlogio()
1016 1017 if self.version == REVLOGV0:
1017 1018 self._io = revlogoldio()
1018 1019 try:
1019 1020 d = self._io.parseindex(indexdata, self._inline)
1020 1021 except (ValueError, IndexError):
1021 1022 raise RevlogError(_("index %s is corrupted") % (self.indexfile))
1022 1023 self.index, nodemap, self._chunkcache = d
1023 1024 if nodemap is not None:
1024 1025 self.nodemap = self._nodecache = nodemap
1025 1026 if not self._chunkcache:
1026 1027 self._chunkclear()
1027 1028 # revnum -> (chain-length, sum-delta-length)
1028 1029 self._chaininfocache = {}
1029 1030 # revlog header -> revlog compressor
1030 1031 self._decompressors = {}
1031 1032
1032 1033 @util.propertycache
1033 1034 def _compressor(self):
1034 1035 return util.compengines[self._compengine].revlogcompressor()
1035 1036
1036 1037 def _indexfp(self, mode='r'):
1037 1038 """file object for the revlog's index file"""
1038 1039 args = {r'mode': mode}
1039 1040 if mode != 'r':
1040 1041 args[r'checkambig'] = self._checkambig
1041 1042 if mode == 'w':
1042 1043 args[r'atomictemp'] = True
1043 1044 return self.opener(self.indexfile, **args)
1044 1045
1045 1046 def _datafp(self, mode='r'):
1046 1047 """file object for the revlog's data file"""
1047 1048 return self.opener(self.datafile, mode=mode)
1048 1049
1049 1050 @contextlib.contextmanager
1050 1051 def _datareadfp(self, existingfp=None):
1051 1052 """file object suitable to read data"""
1052 1053 if existingfp is not None:
1053 1054 yield existingfp
1054 1055 else:
1055 1056 if self._inline:
1056 1057 func = self._indexfp
1057 1058 else:
1058 1059 func = self._datafp
1059 1060 with func() as fp:
1060 1061 yield fp
1061 1062
1062 1063 def tip(self):
1063 1064 return self.node(len(self.index) - 2)
1064 1065 def __contains__(self, rev):
1065 1066 return 0 <= rev < len(self)
1066 1067 def __len__(self):
1067 1068 return len(self.index) - 1
1068 1069 def __iter__(self):
1069 1070 return iter(pycompat.xrange(len(self)))
1070 1071 def revs(self, start=0, stop=None):
1071 1072 """iterate over all rev in this revlog (from start to stop)"""
1072 1073 step = 1
1073 1074 length = len(self)
1074 1075 if stop is not None:
1075 1076 if start > stop:
1076 1077 step = -1
1077 1078 stop += step
1078 1079 if stop > length:
1079 1080 stop = length
1080 1081 else:
1081 1082 stop = length
1082 1083 return pycompat.xrange(start, stop, step)
1083 1084
1084 1085 @util.propertycache
1085 1086 def nodemap(self):
1086 1087 self.rev(self.node(0))
1087 1088 return self._nodecache
1088 1089
1089 1090 def hasnode(self, node):
1090 1091 try:
1091 1092 self.rev(node)
1092 1093 return True
1093 1094 except KeyError:
1094 1095 return False
1095 1096
1096 1097 def candelta(self, baserev, rev):
1097 1098 """whether two revisions (baserev, rev) can be delta-ed or not"""
1098 1099 # Disable delta if either rev requires a content-changing flag
1099 1100 # processor (ex. LFS). This is because such flag processor can alter
1100 1101 # the rawtext content that the delta will be based on, and two clients
1101 1102 # could have a same revlog node with different flags (i.e. different
1102 1103 # rawtext contents) and the delta could be incompatible.
1103 1104 if ((self.flags(baserev) & REVIDX_RAWTEXT_CHANGING_FLAGS)
1104 1105 or (self.flags(rev) & REVIDX_RAWTEXT_CHANGING_FLAGS)):
1105 1106 return False
1106 1107 return True
1107 1108
1108 1109 def clearcaches(self):
1109 1110 self._cache = None
1110 1111 self._chainbasecache.clear()
1111 1112 self._chunkcache = (0, '')
1112 1113 self._pcache = {}
1113 1114
1114 1115 try:
1115 1116 self._nodecache.clearcaches()
1116 1117 except AttributeError:
1117 1118 self._nodecache = {nullid: nullrev}
1118 1119 self._nodepos = None
1119 1120
1120 1121 def rev(self, node):
1121 1122 try:
1122 1123 return self._nodecache[node]
1123 1124 except TypeError:
1124 1125 raise
1125 1126 except RevlogError:
1126 1127 # parsers.c radix tree lookup failed
1127 1128 if node == wdirid or node in wdirfilenodeids:
1128 1129 raise error.WdirUnsupported
1129 1130 raise LookupError(node, self.indexfile, _('no node'))
1130 1131 except KeyError:
1131 1132 # pure python cache lookup failed
1132 1133 n = self._nodecache
1133 1134 i = self.index
1134 1135 p = self._nodepos
1135 1136 if p is None:
1136 1137 p = len(i) - 2
1137 1138 else:
1138 1139 assert p < len(i)
1139 1140 for r in pycompat.xrange(p, -1, -1):
1140 1141 v = i[r][7]
1141 1142 n[v] = r
1142 1143 if v == node:
1143 1144 self._nodepos = r - 1
1144 1145 return r
1145 1146 if node == wdirid or node in wdirfilenodeids:
1146 1147 raise error.WdirUnsupported
1147 1148 raise LookupError(node, self.indexfile, _('no node'))
1148 1149
1149 1150 # Accessors for index entries.
1150 1151
1151 1152 # First tuple entry is 8 bytes. First 6 bytes are offset. Last 2 bytes
1152 1153 # are flags.
1153 1154 def start(self, rev):
1154 1155 return int(self.index[rev][0] >> 16)
1155 1156
1156 1157 def flags(self, rev):
1157 1158 return self.index[rev][0] & 0xFFFF
1158 1159
1159 1160 def length(self, rev):
1160 1161 return self.index[rev][1]
1161 1162
1162 1163 def rawsize(self, rev):
1163 1164 """return the length of the uncompressed text for a given revision"""
1164 1165 l = self.index[rev][2]
1165 1166 if l >= 0:
1166 1167 return l
1167 1168
1168 1169 t = self.revision(rev, raw=True)
1169 1170 return len(t)
1170 1171
1171 1172 def size(self, rev):
1172 1173 """length of non-raw text (processed by a "read" flag processor)"""
1173 1174 # fast path: if no "read" flag processor could change the content,
1174 1175 # size is rawsize. note: ELLIPSIS is known to not change the content.
1175 1176 flags = self.flags(rev)
1176 1177 if flags & (REVIDX_KNOWN_FLAGS ^ REVIDX_ELLIPSIS) == 0:
1177 1178 return self.rawsize(rev)
1178 1179
1179 1180 return len(self.revision(rev, raw=False))
1180 1181
1181 1182 def chainbase(self, rev):
1182 1183 base = self._chainbasecache.get(rev)
1183 1184 if base is not None:
1184 1185 return base
1185 1186
1186 1187 index = self.index
1187 1188 iterrev = rev
1188 1189 base = index[iterrev][3]
1189 1190 while base != iterrev:
1190 1191 iterrev = base
1191 1192 base = index[iterrev][3]
1192 1193
1193 1194 self._chainbasecache[rev] = base
1194 1195 return base
1195 1196
1196 1197 def linkrev(self, rev):
1197 1198 return self.index[rev][4]
1198 1199
1199 1200 def parentrevs(self, rev):
1200 1201 try:
1201 1202 entry = self.index[rev]
1202 1203 except IndexError:
1203 1204 if rev == wdirrev:
1204 1205 raise error.WdirUnsupported
1205 1206 raise
1206 1207
1207 1208 return entry[5], entry[6]
1208 1209
1209 1210 def node(self, rev):
1210 1211 try:
1211 1212 return self.index[rev][7]
1212 1213 except IndexError:
1213 1214 if rev == wdirrev:
1214 1215 raise error.WdirUnsupported
1215 1216 raise
1216 1217
1217 1218 # Derived from index values.
1218 1219
1219 1220 def end(self, rev):
1220 1221 return self.start(rev) + self.length(rev)
1221 1222
1222 1223 def parents(self, node):
1223 1224 i = self.index
1224 1225 d = i[self.rev(node)]
1225 1226 return i[d[5]][7], i[d[6]][7] # map revisions to nodes inline
1226 1227
1227 1228 def chainlen(self, rev):
1228 1229 return self._chaininfo(rev)[0]
1229 1230
1230 1231 def _chaininfo(self, rev):
1231 1232 chaininfocache = self._chaininfocache
1232 1233 if rev in chaininfocache:
1233 1234 return chaininfocache[rev]
1234 1235 index = self.index
1235 1236 generaldelta = self._generaldelta
1236 1237 iterrev = rev
1237 1238 e = index[iterrev]
1238 1239 clen = 0
1239 1240 compresseddeltalen = 0
1240 1241 while iterrev != e[3]:
1241 1242 clen += 1
1242 1243 compresseddeltalen += e[1]
1243 1244 if generaldelta:
1244 1245 iterrev = e[3]
1245 1246 else:
1246 1247 iterrev -= 1
1247 1248 if iterrev in chaininfocache:
1248 1249 t = chaininfocache[iterrev]
1249 1250 clen += t[0]
1250 1251 compresseddeltalen += t[1]
1251 1252 break
1252 1253 e = index[iterrev]
1253 1254 else:
1254 1255 # Add text length of base since decompressing that also takes
1255 1256 # work. For cache hits the length is already included.
1256 1257 compresseddeltalen += e[1]
1257 1258 r = (clen, compresseddeltalen)
1258 1259 chaininfocache[rev] = r
1259 1260 return r
1260 1261
1261 1262 def _deltachain(self, rev, stoprev=None):
1262 1263 """Obtain the delta chain for a revision.
1263 1264
1264 1265 ``stoprev`` specifies a revision to stop at. If not specified, we
1265 1266 stop at the base of the chain.
1266 1267
1267 1268 Returns a 2-tuple of (chain, stopped) where ``chain`` is a list of
1268 1269 revs in ascending order and ``stopped`` is a bool indicating whether
1269 1270 ``stoprev`` was hit.
1270 1271 """
1271 1272 # Try C implementation.
1272 1273 try:
1273 1274 return self.index.deltachain(rev, stoprev, self._generaldelta)
1274 1275 except AttributeError:
1275 1276 pass
1276 1277
1277 1278 chain = []
1278 1279
1279 1280 # Alias to prevent attribute lookup in tight loop.
1280 1281 index = self.index
1281 1282 generaldelta = self._generaldelta
1282 1283
1283 1284 iterrev = rev
1284 1285 e = index[iterrev]
1285 1286 while iterrev != e[3] and iterrev != stoprev:
1286 1287 chain.append(iterrev)
1287 1288 if generaldelta:
1288 1289 iterrev = e[3]
1289 1290 else:
1290 1291 iterrev -= 1
1291 1292 e = index[iterrev]
1292 1293
1293 1294 if iterrev == stoprev:
1294 1295 stopped = True
1295 1296 else:
1296 1297 chain.append(iterrev)
1297 1298 stopped = False
1298 1299
1299 1300 chain.reverse()
1300 1301 return chain, stopped
1301 1302
1302 1303 def ancestors(self, revs, stoprev=0, inclusive=False):
1303 1304 """Generate the ancestors of 'revs' in reverse topological order.
1304 1305 Does not generate revs lower than stoprev.
1305 1306
1306 1307 See the documentation for ancestor.lazyancestors for more details."""
1307 1308
1308 1309 return ancestor.lazyancestors(self.parentrevs, revs, stoprev=stoprev,
1309 1310 inclusive=inclusive)
1310 1311
1311 1312 def descendants(self, revs):
1312 1313 """Generate the descendants of 'revs' in revision order.
1313 1314
1314 1315 Yield a sequence of revision numbers starting with a child of
1315 1316 some rev in revs, i.e., each revision is *not* considered a
1316 1317 descendant of itself. Results are ordered by revision number (a
1317 1318 topological sort)."""
1318 1319 first = min(revs)
1319 1320 if first == nullrev:
1320 1321 for i in self:
1321 1322 yield i
1322 1323 return
1323 1324
1324 1325 seen = set(revs)
1325 1326 for i in self.revs(start=first + 1):
1326 1327 for x in self.parentrevs(i):
1327 1328 if x != nullrev and x in seen:
1328 1329 seen.add(i)
1329 1330 yield i
1330 1331 break
1331 1332
1332 1333 def findcommonmissing(self, common=None, heads=None):
1333 1334 """Return a tuple of the ancestors of common and the ancestors of heads
1334 1335 that are not ancestors of common. In revset terminology, we return the
1335 1336 tuple:
1336 1337
1337 1338 ::common, (::heads) - (::common)
1338 1339
1339 1340 The list is sorted by revision number, meaning it is
1340 1341 topologically sorted.
1341 1342
1342 1343 'heads' and 'common' are both lists of node IDs. If heads is
1343 1344 not supplied, uses all of the revlog's heads. If common is not
1344 1345 supplied, uses nullid."""
1345 1346 if common is None:
1346 1347 common = [nullid]
1347 1348 if heads is None:
1348 1349 heads = self.heads()
1349 1350
1350 1351 common = [self.rev(n) for n in common]
1351 1352 heads = [self.rev(n) for n in heads]
1352 1353
1353 1354 # we want the ancestors, but inclusive
1354 1355 class lazyset(object):
1355 1356 def __init__(self, lazyvalues):
1356 1357 self.addedvalues = set()
1357 1358 self.lazyvalues = lazyvalues
1358 1359
1359 1360 def __contains__(self, value):
1360 1361 return value in self.addedvalues or value in self.lazyvalues
1361 1362
1362 1363 def __iter__(self):
1363 1364 added = self.addedvalues
1364 1365 for r in added:
1365 1366 yield r
1366 1367 for r in self.lazyvalues:
1367 1368 if not r in added:
1368 1369 yield r
1369 1370
1370 1371 def add(self, value):
1371 1372 self.addedvalues.add(value)
1372 1373
1373 1374 def update(self, values):
1374 1375 self.addedvalues.update(values)
1375 1376
1376 1377 has = lazyset(self.ancestors(common))
1377 1378 has.add(nullrev)
1378 1379 has.update(common)
1379 1380
1380 1381 # take all ancestors from heads that aren't in has
1381 1382 missing = set()
1382 1383 visit = collections.deque(r for r in heads if r not in has)
1383 1384 while visit:
1384 1385 r = visit.popleft()
1385 1386 if r in missing:
1386 1387 continue
1387 1388 else:
1388 1389 missing.add(r)
1389 1390 for p in self.parentrevs(r):
1390 1391 if p not in has:
1391 1392 visit.append(p)
1392 1393 missing = list(missing)
1393 1394 missing.sort()
1394 1395 return has, [self.node(miss) for miss in missing]
1395 1396
1396 1397 def incrementalmissingrevs(self, common=None):
1397 1398 """Return an object that can be used to incrementally compute the
1398 1399 revision numbers of the ancestors of arbitrary sets that are not
1399 1400 ancestors of common. This is an ancestor.incrementalmissingancestors
1400 1401 object.
1401 1402
1402 1403 'common' is a list of revision numbers. If common is not supplied, uses
1403 1404 nullrev.
1404 1405 """
1405 1406 if common is None:
1406 1407 common = [nullrev]
1407 1408
1408 1409 return ancestor.incrementalmissingancestors(self.parentrevs, common)
1409 1410
1410 1411 def findmissingrevs(self, common=None, heads=None):
1411 1412 """Return the revision numbers of the ancestors of heads that
1412 1413 are not ancestors of common.
1413 1414
1414 1415 More specifically, return a list of revision numbers corresponding to
1415 1416 nodes N such that every N satisfies the following constraints:
1416 1417
1417 1418 1. N is an ancestor of some node in 'heads'
1418 1419 2. N is not an ancestor of any node in 'common'
1419 1420
1420 1421 The list is sorted by revision number, meaning it is
1421 1422 topologically sorted.
1422 1423
1423 1424 'heads' and 'common' are both lists of revision numbers. If heads is
1424 1425 not supplied, uses all of the revlog's heads. If common is not
1425 1426 supplied, uses nullid."""
1426 1427 if common is None:
1427 1428 common = [nullrev]
1428 1429 if heads is None:
1429 1430 heads = self.headrevs()
1430 1431
1431 1432 inc = self.incrementalmissingrevs(common=common)
1432 1433 return inc.missingancestors(heads)
1433 1434
1434 1435 def findmissing(self, common=None, heads=None):
1435 1436 """Return the ancestors of heads that are not ancestors of common.
1436 1437
1437 1438 More specifically, return a list of nodes N such that every N
1438 1439 satisfies the following constraints:
1439 1440
1440 1441 1. N is an ancestor of some node in 'heads'
1441 1442 2. N is not an ancestor of any node in 'common'
1442 1443
1443 1444 The list is sorted by revision number, meaning it is
1444 1445 topologically sorted.
1445 1446
1446 1447 'heads' and 'common' are both lists of node IDs. If heads is
1447 1448 not supplied, uses all of the revlog's heads. If common is not
1448 1449 supplied, uses nullid."""
1449 1450 if common is None:
1450 1451 common = [nullid]
1451 1452 if heads is None:
1452 1453 heads = self.heads()
1453 1454
1454 1455 common = [self.rev(n) for n in common]
1455 1456 heads = [self.rev(n) for n in heads]
1456 1457
1457 1458 inc = self.incrementalmissingrevs(common=common)
1458 1459 return [self.node(r) for r in inc.missingancestors(heads)]
1459 1460
1460 1461 def nodesbetween(self, roots=None, heads=None):
1461 1462 """Return a topological path from 'roots' to 'heads'.
1462 1463
1463 1464 Return a tuple (nodes, outroots, outheads) where 'nodes' is a
1464 1465 topologically sorted list of all nodes N that satisfy both of
1465 1466 these constraints:
1466 1467
1467 1468 1. N is a descendant of some node in 'roots'
1468 1469 2. N is an ancestor of some node in 'heads'
1469 1470
1470 1471 Every node is considered to be both a descendant and an ancestor
1471 1472 of itself, so every reachable node in 'roots' and 'heads' will be
1472 1473 included in 'nodes'.
1473 1474
1474 1475 'outroots' is the list of reachable nodes in 'roots', i.e., the
1475 1476 subset of 'roots' that is returned in 'nodes'. Likewise,
1476 1477 'outheads' is the subset of 'heads' that is also in 'nodes'.
1477 1478
1478 1479 'roots' and 'heads' are both lists of node IDs. If 'roots' is
1479 1480 unspecified, uses nullid as the only root. If 'heads' is
1480 1481 unspecified, uses list of all of the revlog's heads."""
1481 1482 nonodes = ([], [], [])
1482 1483 if roots is not None:
1483 1484 roots = list(roots)
1484 1485 if not roots:
1485 1486 return nonodes
1486 1487 lowestrev = min([self.rev(n) for n in roots])
1487 1488 else:
1488 1489 roots = [nullid] # Everybody's a descendant of nullid
1489 1490 lowestrev = nullrev
1490 1491 if (lowestrev == nullrev) and (heads is None):
1491 1492 # We want _all_ the nodes!
1492 1493 return ([self.node(r) for r in self], [nullid], list(self.heads()))
1493 1494 if heads is None:
1494 1495 # All nodes are ancestors, so the latest ancestor is the last
1495 1496 # node.
1496 1497 highestrev = len(self) - 1
1497 1498 # Set ancestors to None to signal that every node is an ancestor.
1498 1499 ancestors = None
1499 1500 # Set heads to an empty dictionary for later discovery of heads
1500 1501 heads = {}
1501 1502 else:
1502 1503 heads = list(heads)
1503 1504 if not heads:
1504 1505 return nonodes
1505 1506 ancestors = set()
1506 1507 # Turn heads into a dictionary so we can remove 'fake' heads.
1507 1508 # Also, later we will be using it to filter out the heads we can't
1508 1509 # find from roots.
1509 1510 heads = dict.fromkeys(heads, False)
1510 1511 # Start at the top and keep marking parents until we're done.
1511 1512 nodestotag = set(heads)
1512 1513 # Remember where the top was so we can use it as a limit later.
1513 1514 highestrev = max([self.rev(n) for n in nodestotag])
1514 1515 while nodestotag:
1515 1516 # grab a node to tag
1516 1517 n = nodestotag.pop()
1517 1518 # Never tag nullid
1518 1519 if n == nullid:
1519 1520 continue
1520 1521 # A node's revision number represents its place in a
1521 1522 # topologically sorted list of nodes.
1522 1523 r = self.rev(n)
1523 1524 if r >= lowestrev:
1524 1525 if n not in ancestors:
1525 1526 # If we are possibly a descendant of one of the roots
1526 1527 # and we haven't already been marked as an ancestor
1527 1528 ancestors.add(n) # Mark as ancestor
1528 1529 # Add non-nullid parents to list of nodes to tag.
1529 1530 nodestotag.update([p for p in self.parents(n) if
1530 1531 p != nullid])
1531 1532 elif n in heads: # We've seen it before, is it a fake head?
1532 1533 # So it is, real heads should not be the ancestors of
1533 1534 # any other heads.
1534 1535 heads.pop(n)
1535 1536 if not ancestors:
1536 1537 return nonodes
1537 1538 # Now that we have our set of ancestors, we want to remove any
1538 1539 # roots that are not ancestors.
1539 1540
1540 1541 # If one of the roots was nullid, everything is included anyway.
1541 1542 if lowestrev > nullrev:
1542 1543 # But, since we weren't, let's recompute the lowest rev to not
1543 1544 # include roots that aren't ancestors.
1544 1545
1545 1546 # Filter out roots that aren't ancestors of heads
1546 1547 roots = [root for root in roots if root in ancestors]
1547 1548 # Recompute the lowest revision
1548 1549 if roots:
1549 1550 lowestrev = min([self.rev(root) for root in roots])
1550 1551 else:
1551 1552 # No more roots? Return empty list
1552 1553 return nonodes
1553 1554 else:
1554 1555 # We are descending from nullid, and don't need to care about
1555 1556 # any other roots.
1556 1557 lowestrev = nullrev
1557 1558 roots = [nullid]
1558 1559 # Transform our roots list into a set.
1559 1560 descendants = set(roots)
1560 1561 # Also, keep the original roots so we can filter out roots that aren't
1561 1562 # 'real' roots (i.e. are descended from other roots).
1562 1563 roots = descendants.copy()
1563 1564 # Our topologically sorted list of output nodes.
1564 1565 orderedout = []
1565 1566 # Don't start at nullid since we don't want nullid in our output list,
1566 1567 # and if nullid shows up in descendants, empty parents will look like
1567 1568 # they're descendants.
1568 1569 for r in self.revs(start=max(lowestrev, 0), stop=highestrev + 1):
1569 1570 n = self.node(r)
1570 1571 isdescendant = False
1571 1572 if lowestrev == nullrev: # Everybody is a descendant of nullid
1572 1573 isdescendant = True
1573 1574 elif n in descendants:
1574 1575 # n is already a descendant
1575 1576 isdescendant = True
1576 1577 # This check only needs to be done here because all the roots
1577 1578 # will start being marked is descendants before the loop.
1578 1579 if n in roots:
1579 1580 # If n was a root, check if it's a 'real' root.
1580 1581 p = tuple(self.parents(n))
1581 1582 # If any of its parents are descendants, it's not a root.
1582 1583 if (p[0] in descendants) or (p[1] in descendants):
1583 1584 roots.remove(n)
1584 1585 else:
1585 1586 p = tuple(self.parents(n))
1586 1587 # A node is a descendant if either of its parents are
1587 1588 # descendants. (We seeded the dependents list with the roots
1588 1589 # up there, remember?)
1589 1590 if (p[0] in descendants) or (p[1] in descendants):
1590 1591 descendants.add(n)
1591 1592 isdescendant = True
1592 1593 if isdescendant and ((ancestors is None) or (n in ancestors)):
1593 1594 # Only include nodes that are both descendants and ancestors.
1594 1595 orderedout.append(n)
1595 1596 if (ancestors is not None) and (n in heads):
1596 1597 # We're trying to figure out which heads are reachable
1597 1598 # from roots.
1598 1599 # Mark this head as having been reached
1599 1600 heads[n] = True
1600 1601 elif ancestors is None:
1601 1602 # Otherwise, we're trying to discover the heads.
1602 1603 # Assume this is a head because if it isn't, the next step
1603 1604 # will eventually remove it.
1604 1605 heads[n] = True
1605 1606 # But, obviously its parents aren't.
1606 1607 for p in self.parents(n):
1607 1608 heads.pop(p, None)
1608 1609 heads = [head for head, flag in heads.iteritems() if flag]
1609 1610 roots = list(roots)
1610 1611 assert orderedout
1611 1612 assert roots
1612 1613 assert heads
1613 1614 return (orderedout, roots, heads)
1614 1615
1615 1616 def headrevs(self):
1616 1617 try:
1617 1618 return self.index.headrevs()
1618 1619 except AttributeError:
1619 1620 return self._headrevs()
1620 1621
1621 1622 def computephases(self, roots):
1622 1623 return self.index.computephasesmapsets(roots)
1623 1624
1624 1625 def _headrevs(self):
1625 1626 count = len(self)
1626 1627 if not count:
1627 1628 return [nullrev]
1628 1629 # we won't iter over filtered rev so nobody is a head at start
1629 1630 ishead = [0] * (count + 1)
1630 1631 index = self.index
1631 1632 for r in self:
1632 1633 ishead[r] = 1 # I may be an head
1633 1634 e = index[r]
1634 1635 ishead[e[5]] = ishead[e[6]] = 0 # my parent are not
1635 1636 return [r for r, val in enumerate(ishead) if val]
1636 1637
1637 1638 def heads(self, start=None, stop=None):
1638 1639 """return the list of all nodes that have no children
1639 1640
1640 1641 if start is specified, only heads that are descendants of
1641 1642 start will be returned
1642 1643 if stop is specified, it will consider all the revs from stop
1643 1644 as if they had no children
1644 1645 """
1645 1646 if start is None and stop is None:
1646 1647 if not len(self):
1647 1648 return [nullid]
1648 1649 return [self.node(r) for r in self.headrevs()]
1649 1650
1650 1651 if start is None:
1651 1652 start = nullid
1652 1653 if stop is None:
1653 1654 stop = []
1654 1655 stoprevs = set([self.rev(n) for n in stop])
1655 1656 startrev = self.rev(start)
1656 1657 reachable = {startrev}
1657 1658 heads = {startrev}
1658 1659
1659 1660 parentrevs = self.parentrevs
1660 1661 for r in self.revs(start=startrev + 1):
1661 1662 for p in parentrevs(r):
1662 1663 if p in reachable:
1663 1664 if r not in stoprevs:
1664 1665 reachable.add(r)
1665 1666 heads.add(r)
1666 1667 if p in heads and p not in stoprevs:
1667 1668 heads.remove(p)
1668 1669
1669 1670 return [self.node(r) for r in heads]
1670 1671
1671 1672 def children(self, node):
1672 1673 """find the children of a given node"""
1673 1674 c = []
1674 1675 p = self.rev(node)
1675 1676 for r in self.revs(start=p + 1):
1676 1677 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
1677 1678 if prevs:
1678 1679 for pr in prevs:
1679 1680 if pr == p:
1680 1681 c.append(self.node(r))
1681 1682 elif p == nullrev:
1682 1683 c.append(self.node(r))
1683 1684 return c
1684 1685
1685 1686 def commonancestorsheads(self, a, b):
1686 1687 """calculate all the heads of the common ancestors of nodes a and b"""
1687 1688 a, b = self.rev(a), self.rev(b)
1688 1689 ancs = self._commonancestorsheads(a, b)
1689 1690 return pycompat.maplist(self.node, ancs)
1690 1691
1691 1692 def _commonancestorsheads(self, *revs):
1692 1693 """calculate all the heads of the common ancestors of revs"""
1693 1694 try:
1694 1695 ancs = self.index.commonancestorsheads(*revs)
1695 1696 except (AttributeError, OverflowError): # C implementation failed
1696 1697 ancs = ancestor.commonancestorsheads(self.parentrevs, *revs)
1697 1698 return ancs
1698 1699
1699 1700 def isancestor(self, a, b):
1700 1701 """return True if node a is an ancestor of node b
1701 1702
1702 1703 A revision is considered an ancestor of itself."""
1703 1704 a, b = self.rev(a), self.rev(b)
1704 1705 return self.isancestorrev(a, b)
1705 1706
1706 1707 def descendant(self, a, b):
1707 1708 msg = 'revlog.descendant is deprecated, use revlog.isancestorrev'
1708 1709 util.nouideprecwarn(msg, '4.7')
1709 1710 return self.isancestorrev(a, b)
1710 1711
1711 1712 def isancestorrev(self, a, b):
1712 1713 """return True if revision a is an ancestor of revision b
1713 1714
1714 1715 A revision is considered an ancestor of itself.
1715 1716
1716 1717 The implementation of this is trivial but the use of
1717 1718 commonancestorsheads is not."""
1718 1719 if a == nullrev:
1719 1720 return True
1720 1721 elif a == b:
1721 1722 return True
1722 1723 elif a > b:
1723 1724 return False
1724 1725 return a in self._commonancestorsheads(a, b)
1725 1726
1726 1727 def ancestor(self, a, b):
1727 1728 """calculate the "best" common ancestor of nodes a and b"""
1728 1729
1729 1730 a, b = self.rev(a), self.rev(b)
1730 1731 try:
1731 1732 ancs = self.index.ancestors(a, b)
1732 1733 except (AttributeError, OverflowError):
1733 1734 ancs = ancestor.ancestors(self.parentrevs, a, b)
1734 1735 if ancs:
1735 1736 # choose a consistent winner when there's a tie
1736 1737 return min(map(self.node, ancs))
1737 1738 return nullid
1738 1739
1739 1740 def _match(self, id):
1740 1741 if isinstance(id, int):
1741 1742 # rev
1742 1743 return self.node(id)
1743 1744 if len(id) == 20:
1744 1745 # possibly a binary node
1745 1746 # odds of a binary node being all hex in ASCII are 1 in 10**25
1746 1747 try:
1747 1748 node = id
1748 1749 self.rev(node) # quick search the index
1749 1750 return node
1750 1751 except LookupError:
1751 1752 pass # may be partial hex id
1752 1753 try:
1753 1754 # str(rev)
1754 1755 rev = int(id)
1755 1756 if "%d" % rev != id:
1756 1757 raise ValueError
1757 1758 if rev < 0:
1758 1759 rev = len(self) + rev
1759 1760 if rev < 0 or rev >= len(self):
1760 1761 raise ValueError
1761 1762 return self.node(rev)
1762 1763 except (ValueError, OverflowError):
1763 1764 pass
1764 1765 if len(id) == 40:
1765 1766 try:
1766 1767 # a full hex nodeid?
1767 1768 node = bin(id)
1768 1769 self.rev(node)
1769 1770 return node
1770 1771 except (TypeError, LookupError):
1771 1772 pass
1772 1773
1773 1774 def _partialmatch(self, id):
1774 1775 # we don't care wdirfilenodeids as they should be always full hash
1775 1776 maybewdir = wdirhex.startswith(id)
1776 1777 try:
1777 1778 partial = self.index.partialmatch(id)
1778 1779 if partial and self.hasnode(partial):
1779 1780 if maybewdir:
1780 1781 # single 'ff...' match in radix tree, ambiguous with wdir
1781 1782 raise RevlogError
1782 1783 return partial
1783 1784 if maybewdir:
1784 1785 # no 'ff...' match in radix tree, wdir identified
1785 1786 raise error.WdirUnsupported
1786 1787 return None
1787 1788 except RevlogError:
1788 1789 # parsers.c radix tree lookup gave multiple matches
1789 1790 # fast path: for unfiltered changelog, radix tree is accurate
1790 1791 if not getattr(self, 'filteredrevs', None):
1791 raise LookupError(id, self.indexfile,
1792 _('ambiguous identifier'))
1792 raise AmbiguousPrefixLookupError(id, self.indexfile,
1793 _('ambiguous identifier'))
1793 1794 # fall through to slow path that filters hidden revisions
1794 1795 except (AttributeError, ValueError):
1795 1796 # we are pure python, or key was too short to search radix tree
1796 1797 pass
1797 1798
1798 1799 if id in self._pcache:
1799 1800 return self._pcache[id]
1800 1801
1801 1802 if len(id) <= 40:
1802 1803 try:
1803 1804 # hex(node)[:...]
1804 1805 l = len(id) // 2 # grab an even number of digits
1805 1806 prefix = bin(id[:l * 2])
1806 1807 nl = [e[7] for e in self.index if e[7].startswith(prefix)]
1807 1808 nl = [n for n in nl if hex(n).startswith(id) and
1808 1809 self.hasnode(n)]
1809 1810 if len(nl) > 0:
1810 1811 if len(nl) == 1 and not maybewdir:
1811 1812 self._pcache[id] = nl[0]
1812 1813 return nl[0]
1813 raise LookupError(id, self.indexfile,
1814 _('ambiguous identifier'))
1814 raise AmbiguousPrefixLookupError(id, self.indexfile,
1815 _('ambiguous identifier'))
1815 1816 if maybewdir:
1816 1817 raise error.WdirUnsupported
1817 1818 return None
1818 1819 except TypeError:
1819 1820 pass
1820 1821
1821 1822 def lookup(self, id):
1822 1823 """locate a node based on:
1823 1824 - revision number or str(revision number)
1824 1825 - nodeid or subset of hex nodeid
1825 1826 """
1826 1827 n = self._match(id)
1827 1828 if n is not None:
1828 1829 return n
1829 1830 n = self._partialmatch(id)
1830 1831 if n:
1831 1832 return n
1832 1833
1833 1834 raise LookupError(id, self.indexfile, _('no match found'))
1834 1835
1835 1836 def shortest(self, node, minlength=1):
1836 1837 """Find the shortest unambiguous prefix that matches node."""
1837 1838 def isvalid(prefix):
1838 1839 try:
1839 1840 node = self._partialmatch(prefix)
1840 1841 except error.RevlogError:
1841 1842 return False
1842 1843 except error.WdirUnsupported:
1843 1844 # single 'ff...' match
1844 1845 return True
1845 1846 if node is None:
1846 1847 raise LookupError(node, self.indexfile, _('no node'))
1847 1848 return True
1848 1849
1849 1850 def maybewdir(prefix):
1850 1851 return all(c == 'f' for c in prefix)
1851 1852
1852 1853 hexnode = hex(node)
1853 1854
1854 1855 def disambiguate(hexnode, minlength):
1855 1856 """Disambiguate against wdirid."""
1856 1857 for length in range(minlength, 41):
1857 1858 prefix = hexnode[:length]
1858 1859 if not maybewdir(prefix):
1859 1860 return prefix
1860 1861
1861 1862 if not getattr(self, 'filteredrevs', None):
1862 1863 try:
1863 1864 length = max(self.index.shortest(node), minlength)
1864 1865 return disambiguate(hexnode, length)
1865 1866 except RevlogError:
1866 1867 if node != wdirid:
1867 1868 raise LookupError(node, self.indexfile, _('no node'))
1868 1869 except AttributeError:
1869 1870 # Fall through to pure code
1870 1871 pass
1871 1872
1872 1873 if node == wdirid:
1873 1874 for length in range(minlength, 41):
1874 1875 prefix = hexnode[:length]
1875 1876 if isvalid(prefix):
1876 1877 return prefix
1877 1878
1878 1879 for length in range(minlength, 41):
1879 1880 prefix = hexnode[:length]
1880 1881 if isvalid(prefix):
1881 1882 return disambiguate(hexnode, length)
1882 1883
1883 1884 def cmp(self, node, text):
1884 1885 """compare text with a given file revision
1885 1886
1886 1887 returns True if text is different than what is stored.
1887 1888 """
1888 1889 p1, p2 = self.parents(node)
1889 1890 return hash(text, p1, p2) != node
1890 1891
1891 1892 def _cachesegment(self, offset, data):
1892 1893 """Add a segment to the revlog cache.
1893 1894
1894 1895 Accepts an absolute offset and the data that is at that location.
1895 1896 """
1896 1897 o, d = self._chunkcache
1897 1898 # try to add to existing cache
1898 1899 if o + len(d) == offset and len(d) + len(data) < _chunksize:
1899 1900 self._chunkcache = o, d + data
1900 1901 else:
1901 1902 self._chunkcache = offset, data
1902 1903
1903 1904 def _readsegment(self, offset, length, df=None):
1904 1905 """Load a segment of raw data from the revlog.
1905 1906
1906 1907 Accepts an absolute offset, length to read, and an optional existing
1907 1908 file handle to read from.
1908 1909
1909 1910 If an existing file handle is passed, it will be seeked and the
1910 1911 original seek position will NOT be restored.
1911 1912
1912 1913 Returns a str or buffer of raw byte data.
1913 1914 """
1914 1915 # Cache data both forward and backward around the requested
1915 1916 # data, in a fixed size window. This helps speed up operations
1916 1917 # involving reading the revlog backwards.
1917 1918 cachesize = self._chunkcachesize
1918 1919 realoffset = offset & ~(cachesize - 1)
1919 1920 reallength = (((offset + length + cachesize) & ~(cachesize - 1))
1920 1921 - realoffset)
1921 1922 with self._datareadfp(df) as df:
1922 1923 df.seek(realoffset)
1923 1924 d = df.read(reallength)
1924 1925 self._cachesegment(realoffset, d)
1925 1926 if offset != realoffset or reallength != length:
1926 1927 return util.buffer(d, offset - realoffset, length)
1927 1928 return d
1928 1929
1929 1930 def _getsegment(self, offset, length, df=None):
1930 1931 """Obtain a segment of raw data from the revlog.
1931 1932
1932 1933 Accepts an absolute offset, length of bytes to obtain, and an
1933 1934 optional file handle to the already-opened revlog. If the file
1934 1935 handle is used, it's original seek position will not be preserved.
1935 1936
1936 1937 Requests for data may be returned from a cache.
1937 1938
1938 1939 Returns a str or a buffer instance of raw byte data.
1939 1940 """
1940 1941 o, d = self._chunkcache
1941 1942 l = len(d)
1942 1943
1943 1944 # is it in the cache?
1944 1945 cachestart = offset - o
1945 1946 cacheend = cachestart + length
1946 1947 if cachestart >= 0 and cacheend <= l:
1947 1948 if cachestart == 0 and cacheend == l:
1948 1949 return d # avoid a copy
1949 1950 return util.buffer(d, cachestart, cacheend - cachestart)
1950 1951
1951 1952 return self._readsegment(offset, length, df=df)
1952 1953
1953 1954 def _getsegmentforrevs(self, startrev, endrev, df=None):
1954 1955 """Obtain a segment of raw data corresponding to a range of revisions.
1955 1956
1956 1957 Accepts the start and end revisions and an optional already-open
1957 1958 file handle to be used for reading. If the file handle is read, its
1958 1959 seek position will not be preserved.
1959 1960
1960 1961 Requests for data may be satisfied by a cache.
1961 1962
1962 1963 Returns a 2-tuple of (offset, data) for the requested range of
1963 1964 revisions. Offset is the integer offset from the beginning of the
1964 1965 revlog and data is a str or buffer of the raw byte data.
1965 1966
1966 1967 Callers will need to call ``self.start(rev)`` and ``self.length(rev)``
1967 1968 to determine where each revision's data begins and ends.
1968 1969 """
1969 1970 # Inlined self.start(startrev) & self.end(endrev) for perf reasons
1970 1971 # (functions are expensive).
1971 1972 index = self.index
1972 1973 istart = index[startrev]
1973 1974 start = int(istart[0] >> 16)
1974 1975 if startrev == endrev:
1975 1976 end = start + istart[1]
1976 1977 else:
1977 1978 iend = index[endrev]
1978 1979 end = int(iend[0] >> 16) + iend[1]
1979 1980
1980 1981 if self._inline:
1981 1982 start += (startrev + 1) * self._io.size
1982 1983 end += (endrev + 1) * self._io.size
1983 1984 length = end - start
1984 1985
1985 1986 return start, self._getsegment(start, length, df=df)
1986 1987
1987 1988 def _chunk(self, rev, df=None):
1988 1989 """Obtain a single decompressed chunk for a revision.
1989 1990
1990 1991 Accepts an integer revision and an optional already-open file handle
1991 1992 to be used for reading. If used, the seek position of the file will not
1992 1993 be preserved.
1993 1994
1994 1995 Returns a str holding uncompressed data for the requested revision.
1995 1996 """
1996 1997 return self.decompress(self._getsegmentforrevs(rev, rev, df=df)[1])
1997 1998
1998 1999 def _chunks(self, revs, df=None, targetsize=None):
1999 2000 """Obtain decompressed chunks for the specified revisions.
2000 2001
2001 2002 Accepts an iterable of numeric revisions that are assumed to be in
2002 2003 ascending order. Also accepts an optional already-open file handle
2003 2004 to be used for reading. If used, the seek position of the file will
2004 2005 not be preserved.
2005 2006
2006 2007 This function is similar to calling ``self._chunk()`` multiple times,
2007 2008 but is faster.
2008 2009
2009 2010 Returns a list with decompressed data for each requested revision.
2010 2011 """
2011 2012 if not revs:
2012 2013 return []
2013 2014 start = self.start
2014 2015 length = self.length
2015 2016 inline = self._inline
2016 2017 iosize = self._io.size
2017 2018 buffer = util.buffer
2018 2019
2019 2020 l = []
2020 2021 ladd = l.append
2021 2022
2022 2023 if not self._withsparseread:
2023 2024 slicedchunks = (revs,)
2024 2025 else:
2025 2026 slicedchunks = _slicechunk(self, revs, targetsize=targetsize)
2026 2027
2027 2028 for revschunk in slicedchunks:
2028 2029 firstrev = revschunk[0]
2029 2030 # Skip trailing revisions with empty diff
2030 2031 for lastrev in revschunk[::-1]:
2031 2032 if length(lastrev) != 0:
2032 2033 break
2033 2034
2034 2035 try:
2035 2036 offset, data = self._getsegmentforrevs(firstrev, lastrev, df=df)
2036 2037 except OverflowError:
2037 2038 # issue4215 - we can't cache a run of chunks greater than
2038 2039 # 2G on Windows
2039 2040 return [self._chunk(rev, df=df) for rev in revschunk]
2040 2041
2041 2042 decomp = self.decompress
2042 2043 for rev in revschunk:
2043 2044 chunkstart = start(rev)
2044 2045 if inline:
2045 2046 chunkstart += (rev + 1) * iosize
2046 2047 chunklength = length(rev)
2047 2048 ladd(decomp(buffer(data, chunkstart - offset, chunklength)))
2048 2049
2049 2050 return l
2050 2051
2051 2052 def _chunkclear(self):
2052 2053 """Clear the raw chunk cache."""
2053 2054 self._chunkcache = (0, '')
2054 2055
2055 2056 def deltaparent(self, rev):
2056 2057 """return deltaparent of the given revision"""
2057 2058 base = self.index[rev][3]
2058 2059 if base == rev:
2059 2060 return nullrev
2060 2061 elif self._generaldelta:
2061 2062 return base
2062 2063 else:
2063 2064 return rev - 1
2064 2065
2065 2066 def revdiff(self, rev1, rev2):
2066 2067 """return or calculate a delta between two revisions
2067 2068
2068 2069 The delta calculated is in binary form and is intended to be written to
2069 2070 revlog data directly. So this function needs raw revision data.
2070 2071 """
2071 2072 if rev1 != nullrev and self.deltaparent(rev2) == rev1:
2072 2073 return bytes(self._chunk(rev2))
2073 2074
2074 2075 return mdiff.textdiff(self.revision(rev1, raw=True),
2075 2076 self.revision(rev2, raw=True))
2076 2077
2077 2078 def revision(self, nodeorrev, _df=None, raw=False):
2078 2079 """return an uncompressed revision of a given node or revision
2079 2080 number.
2080 2081
2081 2082 _df - an existing file handle to read from. (internal-only)
2082 2083 raw - an optional argument specifying if the revision data is to be
2083 2084 treated as raw data when applying flag transforms. 'raw' should be set
2084 2085 to True when generating changegroups or in debug commands.
2085 2086 """
2086 2087 if isinstance(nodeorrev, int):
2087 2088 rev = nodeorrev
2088 2089 node = self.node(rev)
2089 2090 else:
2090 2091 node = nodeorrev
2091 2092 rev = None
2092 2093
2093 2094 cachedrev = None
2094 2095 flags = None
2095 2096 rawtext = None
2096 2097 if node == nullid:
2097 2098 return ""
2098 2099 if self._cache:
2099 2100 if self._cache[0] == node:
2100 2101 # _cache only stores rawtext
2101 2102 if raw:
2102 2103 return self._cache[2]
2103 2104 # duplicated, but good for perf
2104 2105 if rev is None:
2105 2106 rev = self.rev(node)
2106 2107 if flags is None:
2107 2108 flags = self.flags(rev)
2108 2109 # no extra flags set, no flag processor runs, text = rawtext
2109 2110 if flags == REVIDX_DEFAULT_FLAGS:
2110 2111 return self._cache[2]
2111 2112 # rawtext is reusable. need to run flag processor
2112 2113 rawtext = self._cache[2]
2113 2114
2114 2115 cachedrev = self._cache[1]
2115 2116
2116 2117 # look up what we need to read
2117 2118 if rawtext is None:
2118 2119 if rev is None:
2119 2120 rev = self.rev(node)
2120 2121
2121 2122 chain, stopped = self._deltachain(rev, stoprev=cachedrev)
2122 2123 if stopped:
2123 2124 rawtext = self._cache[2]
2124 2125
2125 2126 # drop cache to save memory
2126 2127 self._cache = None
2127 2128
2128 2129 targetsize = None
2129 2130 rawsize = self.index[rev][2]
2130 2131 if 0 <= rawsize:
2131 2132 targetsize = 4 * rawsize
2132 2133
2133 2134 bins = self._chunks(chain, df=_df, targetsize=targetsize)
2134 2135 if rawtext is None:
2135 2136 rawtext = bytes(bins[0])
2136 2137 bins = bins[1:]
2137 2138
2138 2139 rawtext = mdiff.patches(rawtext, bins)
2139 2140 self._cache = (node, rev, rawtext)
2140 2141
2141 2142 if flags is None:
2142 2143 if rev is None:
2143 2144 rev = self.rev(node)
2144 2145 flags = self.flags(rev)
2145 2146
2146 2147 text, validatehash = self._processflags(rawtext, flags, 'read', raw=raw)
2147 2148 if validatehash:
2148 2149 self.checkhash(text, node, rev=rev)
2149 2150
2150 2151 return text
2151 2152
2152 2153 def hash(self, text, p1, p2):
2153 2154 """Compute a node hash.
2154 2155
2155 2156 Available as a function so that subclasses can replace the hash
2156 2157 as needed.
2157 2158 """
2158 2159 return hash(text, p1, p2)
2159 2160
2160 2161 def _processflags(self, text, flags, operation, raw=False):
2161 2162 """Inspect revision data flags and applies transforms defined by
2162 2163 registered flag processors.
2163 2164
2164 2165 ``text`` - the revision data to process
2165 2166 ``flags`` - the revision flags
2166 2167 ``operation`` - the operation being performed (read or write)
2167 2168 ``raw`` - an optional argument describing if the raw transform should be
2168 2169 applied.
2169 2170
2170 2171 This method processes the flags in the order (or reverse order if
2171 2172 ``operation`` is 'write') defined by REVIDX_FLAGS_ORDER, applying the
2172 2173 flag processors registered for present flags. The order of flags defined
2173 2174 in REVIDX_FLAGS_ORDER needs to be stable to allow non-commutativity.
2174 2175
2175 2176 Returns a 2-tuple of ``(text, validatehash)`` where ``text`` is the
2176 2177 processed text and ``validatehash`` is a bool indicating whether the
2177 2178 returned text should be checked for hash integrity.
2178 2179
2179 2180 Note: If the ``raw`` argument is set, it has precedence over the
2180 2181 operation and will only update the value of ``validatehash``.
2181 2182 """
2182 2183 # fast path: no flag processors will run
2183 2184 if flags == 0:
2184 2185 return text, True
2185 2186 if not operation in ('read', 'write'):
2186 2187 raise ProgrammingError(_("invalid '%s' operation ") % (operation))
2187 2188 # Check all flags are known.
2188 2189 if flags & ~REVIDX_KNOWN_FLAGS:
2189 2190 raise RevlogError(_("incompatible revision flag '%#x'") %
2190 2191 (flags & ~REVIDX_KNOWN_FLAGS))
2191 2192 validatehash = True
2192 2193 # Depending on the operation (read or write), the order might be
2193 2194 # reversed due to non-commutative transforms.
2194 2195 orderedflags = REVIDX_FLAGS_ORDER
2195 2196 if operation == 'write':
2196 2197 orderedflags = reversed(orderedflags)
2197 2198
2198 2199 for flag in orderedflags:
2199 2200 # If a flagprocessor has been registered for a known flag, apply the
2200 2201 # related operation transform and update result tuple.
2201 2202 if flag & flags:
2202 2203 vhash = True
2203 2204
2204 2205 if flag not in _flagprocessors:
2205 2206 message = _("missing processor for flag '%#x'") % (flag)
2206 2207 raise RevlogError(message)
2207 2208
2208 2209 processor = _flagprocessors[flag]
2209 2210 if processor is not None:
2210 2211 readtransform, writetransform, rawtransform = processor
2211 2212
2212 2213 if raw:
2213 2214 vhash = rawtransform(self, text)
2214 2215 elif operation == 'read':
2215 2216 text, vhash = readtransform(self, text)
2216 2217 else: # write operation
2217 2218 text, vhash = writetransform(self, text)
2218 2219 validatehash = validatehash and vhash
2219 2220
2220 2221 return text, validatehash
2221 2222
2222 2223 def checkhash(self, text, node, p1=None, p2=None, rev=None):
2223 2224 """Check node hash integrity.
2224 2225
2225 2226 Available as a function so that subclasses can extend hash mismatch
2226 2227 behaviors as needed.
2227 2228 """
2228 2229 try:
2229 2230 if p1 is None and p2 is None:
2230 2231 p1, p2 = self.parents(node)
2231 2232 if node != self.hash(text, p1, p2):
2232 2233 revornode = rev
2233 2234 if revornode is None:
2234 2235 revornode = templatefilters.short(hex(node))
2235 2236 raise RevlogError(_("integrity check failed on %s:%s")
2236 2237 % (self.indexfile, pycompat.bytestr(revornode)))
2237 2238 except RevlogError:
2238 2239 if self._censorable and _censoredtext(text):
2239 2240 raise error.CensoredNodeError(self.indexfile, node, text)
2240 2241 raise
2241 2242
2242 2243 def _enforceinlinesize(self, tr, fp=None):
2243 2244 """Check if the revlog is too big for inline and convert if so.
2244 2245
2245 2246 This should be called after revisions are added to the revlog. If the
2246 2247 revlog has grown too large to be an inline revlog, it will convert it
2247 2248 to use multiple index and data files.
2248 2249 """
2249 2250 if not self._inline or (self.start(-2) + self.length(-2)) < _maxinline:
2250 2251 return
2251 2252
2252 2253 trinfo = tr.find(self.indexfile)
2253 2254 if trinfo is None:
2254 2255 raise RevlogError(_("%s not found in the transaction")
2255 2256 % self.indexfile)
2256 2257
2257 2258 trindex = trinfo[2]
2258 2259 if trindex is not None:
2259 2260 dataoff = self.start(trindex)
2260 2261 else:
2261 2262 # revlog was stripped at start of transaction, use all leftover data
2262 2263 trindex = len(self) - 1
2263 2264 dataoff = self.end(-2)
2264 2265
2265 2266 tr.add(self.datafile, dataoff)
2266 2267
2267 2268 if fp:
2268 2269 fp.flush()
2269 2270 fp.close()
2270 2271
2271 2272 with self._datafp('w') as df:
2272 2273 for r in self:
2273 2274 df.write(self._getsegmentforrevs(r, r)[1])
2274 2275
2275 2276 with self._indexfp('w') as fp:
2276 2277 self.version &= ~FLAG_INLINE_DATA
2277 2278 self._inline = False
2278 2279 io = self._io
2279 2280 for i in self:
2280 2281 e = io.packentry(self.index[i], self.node, self.version, i)
2281 2282 fp.write(e)
2282 2283
2283 2284 # the temp file replace the real index when we exit the context
2284 2285 # manager
2285 2286
2286 2287 tr.replace(self.indexfile, trindex * self._io.size)
2287 2288 self._chunkclear()
2288 2289
2289 2290 def addrevision(self, text, transaction, link, p1, p2, cachedelta=None,
2290 2291 node=None, flags=REVIDX_DEFAULT_FLAGS, deltacomputer=None):
2291 2292 """add a revision to the log
2292 2293
2293 2294 text - the revision data to add
2294 2295 transaction - the transaction object used for rollback
2295 2296 link - the linkrev data to add
2296 2297 p1, p2 - the parent nodeids of the revision
2297 2298 cachedelta - an optional precomputed delta
2298 2299 node - nodeid of revision; typically node is not specified, and it is
2299 2300 computed by default as hash(text, p1, p2), however subclasses might
2300 2301 use different hashing method (and override checkhash() in such case)
2301 2302 flags - the known flags to set on the revision
2302 2303 deltacomputer - an optional _deltacomputer instance shared between
2303 2304 multiple calls
2304 2305 """
2305 2306 if link == nullrev:
2306 2307 raise RevlogError(_("attempted to add linkrev -1 to %s")
2307 2308 % self.indexfile)
2308 2309
2309 2310 if flags:
2310 2311 node = node or self.hash(text, p1, p2)
2311 2312
2312 2313 rawtext, validatehash = self._processflags(text, flags, 'write')
2313 2314
2314 2315 # If the flag processor modifies the revision data, ignore any provided
2315 2316 # cachedelta.
2316 2317 if rawtext != text:
2317 2318 cachedelta = None
2318 2319
2319 2320 if len(rawtext) > _maxentrysize:
2320 2321 raise RevlogError(
2321 2322 _("%s: size of %d bytes exceeds maximum revlog storage of 2GiB")
2322 2323 % (self.indexfile, len(rawtext)))
2323 2324
2324 2325 node = node or self.hash(rawtext, p1, p2)
2325 2326 if node in self.nodemap:
2326 2327 return node
2327 2328
2328 2329 if validatehash:
2329 2330 self.checkhash(rawtext, node, p1=p1, p2=p2)
2330 2331
2331 2332 return self.addrawrevision(rawtext, transaction, link, p1, p2, node,
2332 2333 flags, cachedelta=cachedelta,
2333 2334 deltacomputer=deltacomputer)
2334 2335
2335 2336 def addrawrevision(self, rawtext, transaction, link, p1, p2, node, flags,
2336 2337 cachedelta=None, deltacomputer=None):
2337 2338 """add a raw revision with known flags, node and parents
2338 2339 useful when reusing a revision not stored in this revlog (ex: received
2339 2340 over wire, or read from an external bundle).
2340 2341 """
2341 2342 dfh = None
2342 2343 if not self._inline:
2343 2344 dfh = self._datafp("a+")
2344 2345 ifh = self._indexfp("a+")
2345 2346 try:
2346 2347 return self._addrevision(node, rawtext, transaction, link, p1, p2,
2347 2348 flags, cachedelta, ifh, dfh,
2348 2349 deltacomputer=deltacomputer)
2349 2350 finally:
2350 2351 if dfh:
2351 2352 dfh.close()
2352 2353 ifh.close()
2353 2354
2354 2355 def compress(self, data):
2355 2356 """Generate a possibly-compressed representation of data."""
2356 2357 if not data:
2357 2358 return '', data
2358 2359
2359 2360 compressed = self._compressor.compress(data)
2360 2361
2361 2362 if compressed:
2362 2363 # The revlog compressor added the header in the returned data.
2363 2364 return '', compressed
2364 2365
2365 2366 if data[0:1] == '\0':
2366 2367 return '', data
2367 2368 return 'u', data
2368 2369
2369 2370 def decompress(self, data):
2370 2371 """Decompress a revlog chunk.
2371 2372
2372 2373 The chunk is expected to begin with a header identifying the
2373 2374 format type so it can be routed to an appropriate decompressor.
2374 2375 """
2375 2376 if not data:
2376 2377 return data
2377 2378
2378 2379 # Revlogs are read much more frequently than they are written and many
2379 2380 # chunks only take microseconds to decompress, so performance is
2380 2381 # important here.
2381 2382 #
2382 2383 # We can make a few assumptions about revlogs:
2383 2384 #
2384 2385 # 1) the majority of chunks will be compressed (as opposed to inline
2385 2386 # raw data).
2386 2387 # 2) decompressing *any* data will likely by at least 10x slower than
2387 2388 # returning raw inline data.
2388 2389 # 3) we want to prioritize common and officially supported compression
2389 2390 # engines
2390 2391 #
2391 2392 # It follows that we want to optimize for "decompress compressed data
2392 2393 # when encoded with common and officially supported compression engines"
2393 2394 # case over "raw data" and "data encoded by less common or non-official
2394 2395 # compression engines." That is why we have the inline lookup first
2395 2396 # followed by the compengines lookup.
2396 2397 #
2397 2398 # According to `hg perfrevlogchunks`, this is ~0.5% faster for zlib
2398 2399 # compressed chunks. And this matters for changelog and manifest reads.
2399 2400 t = data[0:1]
2400 2401
2401 2402 if t == 'x':
2402 2403 try:
2403 2404 return _zlibdecompress(data)
2404 2405 except zlib.error as e:
2405 2406 raise RevlogError(_('revlog decompress error: %s') %
2406 2407 stringutil.forcebytestr(e))
2407 2408 # '\0' is more common than 'u' so it goes first.
2408 2409 elif t == '\0':
2409 2410 return data
2410 2411 elif t == 'u':
2411 2412 return util.buffer(data, 1)
2412 2413
2413 2414 try:
2414 2415 compressor = self._decompressors[t]
2415 2416 except KeyError:
2416 2417 try:
2417 2418 engine = util.compengines.forrevlogheader(t)
2418 2419 compressor = engine.revlogcompressor()
2419 2420 self._decompressors[t] = compressor
2420 2421 except KeyError:
2421 2422 raise RevlogError(_('unknown compression type %r') % t)
2422 2423
2423 2424 return compressor.decompress(data)
2424 2425
2425 2426 def _isgooddeltainfo(self, deltainfo, revinfo):
2426 2427 """Returns True if the given delta is good. Good means that it is within
2427 2428 the disk span, disk size, and chain length bounds that we know to be
2428 2429 performant."""
2429 2430 if deltainfo is None:
2430 2431 return False
2431 2432
2432 2433 # - 'deltainfo.distance' is the distance from the base revision --
2433 2434 # bounding it limits the amount of I/O we need to do.
2434 2435 # - 'deltainfo.compresseddeltalen' is the sum of the total size of
2435 2436 # deltas we need to apply -- bounding it limits the amount of CPU
2436 2437 # we consume.
2437 2438
2438 2439 if self._sparserevlog:
2439 2440 # As sparse-read will be used, we can consider that the distance,
2440 2441 # instead of being the span of the whole chunk,
2441 2442 # is the span of the largest read chunk
2442 2443 base = deltainfo.base
2443 2444
2444 2445 if base != nullrev:
2445 2446 deltachain = self._deltachain(base)[0]
2446 2447 else:
2447 2448 deltachain = []
2448 2449
2449 2450 chunks = _slicechunk(self, deltachain, deltainfo)
2450 2451 distance = max(map(lambda revs:_segmentspan(self, revs), chunks))
2451 2452 else:
2452 2453 distance = deltainfo.distance
2453 2454
2454 2455 textlen = revinfo.textlen
2455 2456 defaultmax = textlen * 4
2456 2457 maxdist = self._maxdeltachainspan
2457 2458 if not maxdist:
2458 2459 maxdist = distance # ensure the conditional pass
2459 2460 maxdist = max(maxdist, defaultmax)
2460 2461 if self._sparserevlog and maxdist < self._srmingapsize:
2461 2462 # In multiple place, we are ignoring irrelevant data range below a
2462 2463 # certain size. Be also apply this tradeoff here and relax span
2463 2464 # constraint for small enought content.
2464 2465 maxdist = self._srmingapsize
2465 2466 if (distance > maxdist or deltainfo.deltalen > textlen or
2466 2467 deltainfo.compresseddeltalen > textlen * 2 or
2467 2468 (self._maxchainlen and deltainfo.chainlen > self._maxchainlen)):
2468 2469 return False
2469 2470
2470 2471 return True
2471 2472
2472 2473 def _addrevision(self, node, rawtext, transaction, link, p1, p2, flags,
2473 2474 cachedelta, ifh, dfh, alwayscache=False,
2474 2475 deltacomputer=None):
2475 2476 """internal function to add revisions to the log
2476 2477
2477 2478 see addrevision for argument descriptions.
2478 2479
2479 2480 note: "addrevision" takes non-raw text, "_addrevision" takes raw text.
2480 2481
2481 2482 if "deltacomputer" is not provided or None, a defaultdeltacomputer will
2482 2483 be used.
2483 2484
2484 2485 invariants:
2485 2486 - rawtext is optional (can be None); if not set, cachedelta must be set.
2486 2487 if both are set, they must correspond to each other.
2487 2488 """
2488 2489 if node == nullid:
2489 2490 raise RevlogError(_("%s: attempt to add null revision") %
2490 2491 (self.indexfile))
2491 2492 if node == wdirid or node in wdirfilenodeids:
2492 2493 raise RevlogError(_("%s: attempt to add wdir revision") %
2493 2494 (self.indexfile))
2494 2495
2495 2496 if self._inline:
2496 2497 fh = ifh
2497 2498 else:
2498 2499 fh = dfh
2499 2500
2500 2501 btext = [rawtext]
2501 2502
2502 2503 curr = len(self)
2503 2504 prev = curr - 1
2504 2505 offset = self.end(prev)
2505 2506 p1r, p2r = self.rev(p1), self.rev(p2)
2506 2507
2507 2508 # full versions are inserted when the needed deltas
2508 2509 # become comparable to the uncompressed text
2509 2510 if rawtext is None:
2510 2511 # need rawtext size, before changed by flag processors, which is
2511 2512 # the non-raw size. use revlog explicitly to avoid filelog's extra
2512 2513 # logic that might remove metadata size.
2513 2514 textlen = mdiff.patchedsize(revlog.size(self, cachedelta[0]),
2514 2515 cachedelta[1])
2515 2516 else:
2516 2517 textlen = len(rawtext)
2517 2518
2518 2519 if deltacomputer is None:
2519 2520 deltacomputer = _deltacomputer(self)
2520 2521
2521 2522 revinfo = _revisioninfo(node, p1, p2, btext, textlen, cachedelta, flags)
2522 2523
2523 2524 # no delta for flag processor revision (see "candelta" for why)
2524 2525 # not calling candelta since only one revision needs test, also to
2525 2526 # avoid overhead fetching flags again.
2526 2527 if flags & REVIDX_RAWTEXT_CHANGING_FLAGS:
2527 2528 deltainfo = None
2528 2529 else:
2529 2530 deltainfo = deltacomputer.finddeltainfo(revinfo, fh)
2530 2531
2531 2532 if deltainfo is not None:
2532 2533 base = deltainfo.base
2533 2534 chainbase = deltainfo.chainbase
2534 2535 data = deltainfo.data
2535 2536 l = deltainfo.deltalen
2536 2537 else:
2537 2538 rawtext = deltacomputer.buildtext(revinfo, fh)
2538 2539 data = self.compress(rawtext)
2539 2540 l = len(data[1]) + len(data[0])
2540 2541 base = chainbase = curr
2541 2542
2542 2543 e = (offset_type(offset, flags), l, textlen,
2543 2544 base, link, p1r, p2r, node)
2544 2545 self.index.insert(-1, e)
2545 2546 self.nodemap[node] = curr
2546 2547
2547 2548 entry = self._io.packentry(e, self.node, self.version, curr)
2548 2549 self._writeentry(transaction, ifh, dfh, entry, data, link, offset)
2549 2550
2550 2551 if alwayscache and rawtext is None:
2551 2552 rawtext = deltacomputer._buildtext(revinfo, fh)
2552 2553
2553 2554 if type(rawtext) == bytes: # only accept immutable objects
2554 2555 self._cache = (node, curr, rawtext)
2555 2556 self._chainbasecache[curr] = chainbase
2556 2557 return node
2557 2558
2558 2559 def _writeentry(self, transaction, ifh, dfh, entry, data, link, offset):
2559 2560 # Files opened in a+ mode have inconsistent behavior on various
2560 2561 # platforms. Windows requires that a file positioning call be made
2561 2562 # when the file handle transitions between reads and writes. See
2562 2563 # 3686fa2b8eee and the mixedfilemodewrapper in windows.py. On other
2563 2564 # platforms, Python or the platform itself can be buggy. Some versions
2564 2565 # of Solaris have been observed to not append at the end of the file
2565 2566 # if the file was seeked to before the end. See issue4943 for more.
2566 2567 #
2567 2568 # We work around this issue by inserting a seek() before writing.
2568 2569 # Note: This is likely not necessary on Python 3.
2569 2570 ifh.seek(0, os.SEEK_END)
2570 2571 if dfh:
2571 2572 dfh.seek(0, os.SEEK_END)
2572 2573
2573 2574 curr = len(self) - 1
2574 2575 if not self._inline:
2575 2576 transaction.add(self.datafile, offset)
2576 2577 transaction.add(self.indexfile, curr * len(entry))
2577 2578 if data[0]:
2578 2579 dfh.write(data[0])
2579 2580 dfh.write(data[1])
2580 2581 ifh.write(entry)
2581 2582 else:
2582 2583 offset += curr * self._io.size
2583 2584 transaction.add(self.indexfile, offset, curr)
2584 2585 ifh.write(entry)
2585 2586 ifh.write(data[0])
2586 2587 ifh.write(data[1])
2587 2588 self._enforceinlinesize(transaction, ifh)
2588 2589
2589 2590 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None):
2590 2591 """
2591 2592 add a delta group
2592 2593
2593 2594 given a set of deltas, add them to the revision log. the
2594 2595 first delta is against its parent, which should be in our
2595 2596 log, the rest are against the previous delta.
2596 2597
2597 2598 If ``addrevisioncb`` is defined, it will be called with arguments of
2598 2599 this revlog and the node that was added.
2599 2600 """
2600 2601
2601 2602 nodes = []
2602 2603
2603 2604 r = len(self)
2604 2605 end = 0
2605 2606 if r:
2606 2607 end = self.end(r - 1)
2607 2608 ifh = self._indexfp("a+")
2608 2609 isize = r * self._io.size
2609 2610 if self._inline:
2610 2611 transaction.add(self.indexfile, end + isize, r)
2611 2612 dfh = None
2612 2613 else:
2613 2614 transaction.add(self.indexfile, isize, r)
2614 2615 transaction.add(self.datafile, end)
2615 2616 dfh = self._datafp("a+")
2616 2617 def flush():
2617 2618 if dfh:
2618 2619 dfh.flush()
2619 2620 ifh.flush()
2620 2621 try:
2621 2622 deltacomputer = _deltacomputer(self)
2622 2623 # loop through our set of deltas
2623 2624 for data in deltas:
2624 2625 node, p1, p2, linknode, deltabase, delta, flags = data
2625 2626 link = linkmapper(linknode)
2626 2627 flags = flags or REVIDX_DEFAULT_FLAGS
2627 2628
2628 2629 nodes.append(node)
2629 2630
2630 2631 if node in self.nodemap:
2631 2632 # this can happen if two branches make the same change
2632 2633 continue
2633 2634
2634 2635 for p in (p1, p2):
2635 2636 if p not in self.nodemap:
2636 2637 raise LookupError(p, self.indexfile,
2637 2638 _('unknown parent'))
2638 2639
2639 2640 if deltabase not in self.nodemap:
2640 2641 raise LookupError(deltabase, self.indexfile,
2641 2642 _('unknown delta base'))
2642 2643
2643 2644 baserev = self.rev(deltabase)
2644 2645
2645 2646 if baserev != nullrev and self.iscensored(baserev):
2646 2647 # if base is censored, delta must be full replacement in a
2647 2648 # single patch operation
2648 2649 hlen = struct.calcsize(">lll")
2649 2650 oldlen = self.rawsize(baserev)
2650 2651 newlen = len(delta) - hlen
2651 2652 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
2652 2653 raise error.CensoredBaseError(self.indexfile,
2653 2654 self.node(baserev))
2654 2655
2655 2656 if not flags and self._peek_iscensored(baserev, delta, flush):
2656 2657 flags |= REVIDX_ISCENSORED
2657 2658
2658 2659 # We assume consumers of addrevisioncb will want to retrieve
2659 2660 # the added revision, which will require a call to
2660 2661 # revision(). revision() will fast path if there is a cache
2661 2662 # hit. So, we tell _addrevision() to always cache in this case.
2662 2663 # We're only using addgroup() in the context of changegroup
2663 2664 # generation so the revision data can always be handled as raw
2664 2665 # by the flagprocessor.
2665 2666 self._addrevision(node, None, transaction, link,
2666 2667 p1, p2, flags, (baserev, delta),
2667 2668 ifh, dfh,
2668 2669 alwayscache=bool(addrevisioncb),
2669 2670 deltacomputer=deltacomputer)
2670 2671
2671 2672 if addrevisioncb:
2672 2673 addrevisioncb(self, node)
2673 2674
2674 2675 if not dfh and not self._inline:
2675 2676 # addrevision switched from inline to conventional
2676 2677 # reopen the index
2677 2678 ifh.close()
2678 2679 dfh = self._datafp("a+")
2679 2680 ifh = self._indexfp("a+")
2680 2681 finally:
2681 2682 if dfh:
2682 2683 dfh.close()
2683 2684 ifh.close()
2684 2685
2685 2686 return nodes
2686 2687
2687 2688 def iscensored(self, rev):
2688 2689 """Check if a file revision is censored."""
2689 2690 if not self._censorable:
2690 2691 return False
2691 2692
2692 2693 return self.flags(rev) & REVIDX_ISCENSORED
2693 2694
2694 2695 def _peek_iscensored(self, baserev, delta, flush):
2695 2696 """Quickly check if a delta produces a censored revision."""
2696 2697 if not self._censorable:
2697 2698 return False
2698 2699
2699 2700 # Fragile heuristic: unless new file meta keys are added alphabetically
2700 2701 # preceding "censored", all censored revisions are prefixed by
2701 2702 # "\1\ncensored:". A delta producing such a censored revision must be a
2702 2703 # full-replacement delta, so we inspect the first and only patch in the
2703 2704 # delta for this prefix.
2704 2705 hlen = struct.calcsize(">lll")
2705 2706 if len(delta) <= hlen:
2706 2707 return False
2707 2708
2708 2709 oldlen = self.rawsize(baserev)
2709 2710 newlen = len(delta) - hlen
2710 2711 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
2711 2712 return False
2712 2713
2713 2714 add = "\1\ncensored:"
2714 2715 addlen = len(add)
2715 2716 return newlen >= addlen and delta[hlen:hlen + addlen] == add
2716 2717
2717 2718 def getstrippoint(self, minlink):
2718 2719 """find the minimum rev that must be stripped to strip the linkrev
2719 2720
2720 2721 Returns a tuple containing the minimum rev and a set of all revs that
2721 2722 have linkrevs that will be broken by this strip.
2722 2723 """
2723 2724 brokenrevs = set()
2724 2725 strippoint = len(self)
2725 2726
2726 2727 heads = {}
2727 2728 futurelargelinkrevs = set()
2728 2729 for head in self.headrevs():
2729 2730 headlinkrev = self.linkrev(head)
2730 2731 heads[head] = headlinkrev
2731 2732 if headlinkrev >= minlink:
2732 2733 futurelargelinkrevs.add(headlinkrev)
2733 2734
2734 2735 # This algorithm involves walking down the rev graph, starting at the
2735 2736 # heads. Since the revs are topologically sorted according to linkrev,
2736 2737 # once all head linkrevs are below the minlink, we know there are
2737 2738 # no more revs that could have a linkrev greater than minlink.
2738 2739 # So we can stop walking.
2739 2740 while futurelargelinkrevs:
2740 2741 strippoint -= 1
2741 2742 linkrev = heads.pop(strippoint)
2742 2743
2743 2744 if linkrev < minlink:
2744 2745 brokenrevs.add(strippoint)
2745 2746 else:
2746 2747 futurelargelinkrevs.remove(linkrev)
2747 2748
2748 2749 for p in self.parentrevs(strippoint):
2749 2750 if p != nullrev:
2750 2751 plinkrev = self.linkrev(p)
2751 2752 heads[p] = plinkrev
2752 2753 if plinkrev >= minlink:
2753 2754 futurelargelinkrevs.add(plinkrev)
2754 2755
2755 2756 return strippoint, brokenrevs
2756 2757
2757 2758 def strip(self, minlink, transaction):
2758 2759 """truncate the revlog on the first revision with a linkrev >= minlink
2759 2760
2760 2761 This function is called when we're stripping revision minlink and
2761 2762 its descendants from the repository.
2762 2763
2763 2764 We have to remove all revisions with linkrev >= minlink, because
2764 2765 the equivalent changelog revisions will be renumbered after the
2765 2766 strip.
2766 2767
2767 2768 So we truncate the revlog on the first of these revisions, and
2768 2769 trust that the caller has saved the revisions that shouldn't be
2769 2770 removed and that it'll re-add them after this truncation.
2770 2771 """
2771 2772 if len(self) == 0:
2772 2773 return
2773 2774
2774 2775 rev, _ = self.getstrippoint(minlink)
2775 2776 if rev == len(self):
2776 2777 return
2777 2778
2778 2779 # first truncate the files on disk
2779 2780 end = self.start(rev)
2780 2781 if not self._inline:
2781 2782 transaction.add(self.datafile, end)
2782 2783 end = rev * self._io.size
2783 2784 else:
2784 2785 end += rev * self._io.size
2785 2786
2786 2787 transaction.add(self.indexfile, end)
2787 2788
2788 2789 # then reset internal state in memory to forget those revisions
2789 2790 self._cache = None
2790 2791 self._chaininfocache = {}
2791 2792 self._chunkclear()
2792 2793 for x in pycompat.xrange(rev, len(self)):
2793 2794 del self.nodemap[self.node(x)]
2794 2795
2795 2796 del self.index[rev:-1]
2796 2797 self._nodepos = None
2797 2798
2798 2799 def checksize(self):
2799 2800 expected = 0
2800 2801 if len(self):
2801 2802 expected = max(0, self.end(len(self) - 1))
2802 2803
2803 2804 try:
2804 2805 with self._datafp() as f:
2805 2806 f.seek(0, 2)
2806 2807 actual = f.tell()
2807 2808 dd = actual - expected
2808 2809 except IOError as inst:
2809 2810 if inst.errno != errno.ENOENT:
2810 2811 raise
2811 2812 dd = 0
2812 2813
2813 2814 try:
2814 2815 f = self.opener(self.indexfile)
2815 2816 f.seek(0, 2)
2816 2817 actual = f.tell()
2817 2818 f.close()
2818 2819 s = self._io.size
2819 2820 i = max(0, actual // s)
2820 2821 di = actual - (i * s)
2821 2822 if self._inline:
2822 2823 databytes = 0
2823 2824 for r in self:
2824 2825 databytes += max(0, self.length(r))
2825 2826 dd = 0
2826 2827 di = actual - len(self) * s - databytes
2827 2828 except IOError as inst:
2828 2829 if inst.errno != errno.ENOENT:
2829 2830 raise
2830 2831 di = 0
2831 2832
2832 2833 return (dd, di)
2833 2834
2834 2835 def files(self):
2835 2836 res = [self.indexfile]
2836 2837 if not self._inline:
2837 2838 res.append(self.datafile)
2838 2839 return res
2839 2840
2840 2841 DELTAREUSEALWAYS = 'always'
2841 2842 DELTAREUSESAMEREVS = 'samerevs'
2842 2843 DELTAREUSENEVER = 'never'
2843 2844
2844 2845 DELTAREUSEFULLADD = 'fulladd'
2845 2846
2846 2847 DELTAREUSEALL = {'always', 'samerevs', 'never', 'fulladd'}
2847 2848
2848 2849 def clone(self, tr, destrevlog, addrevisioncb=None,
2849 2850 deltareuse=DELTAREUSESAMEREVS, deltabothparents=None):
2850 2851 """Copy this revlog to another, possibly with format changes.
2851 2852
2852 2853 The destination revlog will contain the same revisions and nodes.
2853 2854 However, it may not be bit-for-bit identical due to e.g. delta encoding
2854 2855 differences.
2855 2856
2856 2857 The ``deltareuse`` argument control how deltas from the existing revlog
2857 2858 are preserved in the destination revlog. The argument can have the
2858 2859 following values:
2859 2860
2860 2861 DELTAREUSEALWAYS
2861 2862 Deltas will always be reused (if possible), even if the destination
2862 2863 revlog would not select the same revisions for the delta. This is the
2863 2864 fastest mode of operation.
2864 2865 DELTAREUSESAMEREVS
2865 2866 Deltas will be reused if the destination revlog would pick the same
2866 2867 revisions for the delta. This mode strikes a balance between speed
2867 2868 and optimization.
2868 2869 DELTAREUSENEVER
2869 2870 Deltas will never be reused. This is the slowest mode of execution.
2870 2871 This mode can be used to recompute deltas (e.g. if the diff/delta
2871 2872 algorithm changes).
2872 2873
2873 2874 Delta computation can be slow, so the choice of delta reuse policy can
2874 2875 significantly affect run time.
2875 2876
2876 2877 The default policy (``DELTAREUSESAMEREVS``) strikes a balance between
2877 2878 two extremes. Deltas will be reused if they are appropriate. But if the
2878 2879 delta could choose a better revision, it will do so. This means if you
2879 2880 are converting a non-generaldelta revlog to a generaldelta revlog,
2880 2881 deltas will be recomputed if the delta's parent isn't a parent of the
2881 2882 revision.
2882 2883
2883 2884 In addition to the delta policy, the ``deltabothparents`` argument
2884 2885 controls whether to compute deltas against both parents for merges.
2885 2886 By default, the current default is used.
2886 2887 """
2887 2888 if deltareuse not in self.DELTAREUSEALL:
2888 2889 raise ValueError(_('value for deltareuse invalid: %s') % deltareuse)
2889 2890
2890 2891 if len(destrevlog):
2891 2892 raise ValueError(_('destination revlog is not empty'))
2892 2893
2893 2894 if getattr(self, 'filteredrevs', None):
2894 2895 raise ValueError(_('source revlog has filtered revisions'))
2895 2896 if getattr(destrevlog, 'filteredrevs', None):
2896 2897 raise ValueError(_('destination revlog has filtered revisions'))
2897 2898
2898 2899 # lazydeltabase controls whether to reuse a cached delta, if possible.
2899 2900 oldlazydeltabase = destrevlog._lazydeltabase
2900 2901 oldamd = destrevlog._deltabothparents
2901 2902
2902 2903 try:
2903 2904 if deltareuse == self.DELTAREUSEALWAYS:
2904 2905 destrevlog._lazydeltabase = True
2905 2906 elif deltareuse == self.DELTAREUSESAMEREVS:
2906 2907 destrevlog._lazydeltabase = False
2907 2908
2908 2909 destrevlog._deltabothparents = deltabothparents or oldamd
2909 2910
2910 2911 populatecachedelta = deltareuse in (self.DELTAREUSEALWAYS,
2911 2912 self.DELTAREUSESAMEREVS)
2912 2913
2913 2914 deltacomputer = _deltacomputer(destrevlog)
2914 2915 index = self.index
2915 2916 for rev in self:
2916 2917 entry = index[rev]
2917 2918
2918 2919 # Some classes override linkrev to take filtered revs into
2919 2920 # account. Use raw entry from index.
2920 2921 flags = entry[0] & 0xffff
2921 2922 linkrev = entry[4]
2922 2923 p1 = index[entry[5]][7]
2923 2924 p2 = index[entry[6]][7]
2924 2925 node = entry[7]
2925 2926
2926 2927 # (Possibly) reuse the delta from the revlog if allowed and
2927 2928 # the revlog chunk is a delta.
2928 2929 cachedelta = None
2929 2930 rawtext = None
2930 2931 if populatecachedelta:
2931 2932 dp = self.deltaparent(rev)
2932 2933 if dp != nullrev:
2933 2934 cachedelta = (dp, bytes(self._chunk(rev)))
2934 2935
2935 2936 if not cachedelta:
2936 2937 rawtext = self.revision(rev, raw=True)
2937 2938
2938 2939
2939 2940 if deltareuse == self.DELTAREUSEFULLADD:
2940 2941 destrevlog.addrevision(rawtext, tr, linkrev, p1, p2,
2941 2942 cachedelta=cachedelta,
2942 2943 node=node, flags=flags,
2943 2944 deltacomputer=deltacomputer)
2944 2945 else:
2945 2946 ifh = destrevlog.opener(destrevlog.indexfile, 'a+',
2946 2947 checkambig=False)
2947 2948 dfh = None
2948 2949 if not destrevlog._inline:
2949 2950 dfh = destrevlog.opener(destrevlog.datafile, 'a+')
2950 2951 try:
2951 2952 destrevlog._addrevision(node, rawtext, tr, linkrev, p1,
2952 2953 p2, flags, cachedelta, ifh, dfh,
2953 2954 deltacomputer=deltacomputer)
2954 2955 finally:
2955 2956 if dfh:
2956 2957 dfh.close()
2957 2958 ifh.close()
2958 2959
2959 2960 if addrevisioncb:
2960 2961 addrevisioncb(self, rev, node)
2961 2962 finally:
2962 2963 destrevlog._lazydeltabase = oldlazydeltabase
2963 2964 destrevlog._deltabothparents = oldamd
@@ -1,1701 +1,1701 b''
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import glob
12 12 import hashlib
13 13 import os
14 14 import re
15 15 import socket
16 16 import subprocess
17 17 import weakref
18 18
19 19 from .i18n import _
20 20 from .node import (
21 21 bin,
22 22 hex,
23 23 nullid,
24 24 short,
25 25 wdirid,
26 26 wdirrev,
27 27 )
28 28
29 29 from . import (
30 30 encoding,
31 31 error,
32 32 match as matchmod,
33 33 obsolete,
34 34 obsutil,
35 35 pathutil,
36 36 phases,
37 37 pycompat,
38 38 revsetlang,
39 39 similar,
40 40 url,
41 41 util,
42 42 vfs,
43 43 )
44 44
45 45 from .utils import (
46 46 procutil,
47 47 stringutil,
48 48 )
49 49
50 50 if pycompat.iswindows:
51 51 from . import scmwindows as scmplatform
52 52 else:
53 53 from . import scmposix as scmplatform
54 54
55 55 termsize = scmplatform.termsize
56 56
57 57 class status(tuple):
58 58 '''Named tuple with a list of files per status. The 'deleted', 'unknown'
59 59 and 'ignored' properties are only relevant to the working copy.
60 60 '''
61 61
62 62 __slots__ = ()
63 63
64 64 def __new__(cls, modified, added, removed, deleted, unknown, ignored,
65 65 clean):
66 66 return tuple.__new__(cls, (modified, added, removed, deleted, unknown,
67 67 ignored, clean))
68 68
69 69 @property
70 70 def modified(self):
71 71 '''files that have been modified'''
72 72 return self[0]
73 73
74 74 @property
75 75 def added(self):
76 76 '''files that have been added'''
77 77 return self[1]
78 78
79 79 @property
80 80 def removed(self):
81 81 '''files that have been removed'''
82 82 return self[2]
83 83
84 84 @property
85 85 def deleted(self):
86 86 '''files that are in the dirstate, but have been deleted from the
87 87 working copy (aka "missing")
88 88 '''
89 89 return self[3]
90 90
91 91 @property
92 92 def unknown(self):
93 93 '''files not in the dirstate that are not ignored'''
94 94 return self[4]
95 95
96 96 @property
97 97 def ignored(self):
98 98 '''files not in the dirstate that are ignored (by _dirignore())'''
99 99 return self[5]
100 100
101 101 @property
102 102 def clean(self):
103 103 '''files that have not been modified'''
104 104 return self[6]
105 105
106 106 def __repr__(self, *args, **kwargs):
107 107 return ((r'<status modified=%s, added=%s, removed=%s, deleted=%s, '
108 108 r'unknown=%s, ignored=%s, clean=%s>') %
109 109 tuple(pycompat.sysstr(stringutil.pprint(v)) for v in self))
110 110
111 111 def itersubrepos(ctx1, ctx2):
112 112 """find subrepos in ctx1 or ctx2"""
113 113 # Create a (subpath, ctx) mapping where we prefer subpaths from
114 114 # ctx1. The subpaths from ctx2 are important when the .hgsub file
115 115 # has been modified (in ctx2) but not yet committed (in ctx1).
116 116 subpaths = dict.fromkeys(ctx2.substate, ctx2)
117 117 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
118 118
119 119 missing = set()
120 120
121 121 for subpath in ctx2.substate:
122 122 if subpath not in ctx1.substate:
123 123 del subpaths[subpath]
124 124 missing.add(subpath)
125 125
126 126 for subpath, ctx in sorted(subpaths.iteritems()):
127 127 yield subpath, ctx.sub(subpath)
128 128
129 129 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
130 130 # status and diff will have an accurate result when it does
131 131 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
132 132 # against itself.
133 133 for subpath in missing:
134 134 yield subpath, ctx2.nullsub(subpath, ctx1)
135 135
136 136 def nochangesfound(ui, repo, excluded=None):
137 137 '''Report no changes for push/pull, excluded is None or a list of
138 138 nodes excluded from the push/pull.
139 139 '''
140 140 secretlist = []
141 141 if excluded:
142 142 for n in excluded:
143 143 ctx = repo[n]
144 144 if ctx.phase() >= phases.secret and not ctx.extinct():
145 145 secretlist.append(n)
146 146
147 147 if secretlist:
148 148 ui.status(_("no changes found (ignored %d secret changesets)\n")
149 149 % len(secretlist))
150 150 else:
151 151 ui.status(_("no changes found\n"))
152 152
153 153 def callcatch(ui, func):
154 154 """call func() with global exception handling
155 155
156 156 return func() if no exception happens. otherwise do some error handling
157 157 and return an exit code accordingly. does not handle all exceptions.
158 158 """
159 159 try:
160 160 try:
161 161 return func()
162 162 except: # re-raises
163 163 ui.traceback()
164 164 raise
165 165 # Global exception handling, alphabetically
166 166 # Mercurial-specific first, followed by built-in and library exceptions
167 167 except error.LockHeld as inst:
168 168 if inst.errno == errno.ETIMEDOUT:
169 169 reason = _('timed out waiting for lock held by %r') % inst.locker
170 170 else:
171 171 reason = _('lock held by %r') % inst.locker
172 172 ui.error(_("abort: %s: %s\n") % (
173 173 inst.desc or stringutil.forcebytestr(inst.filename), reason))
174 174 if not inst.locker:
175 175 ui.error(_("(lock might be very busy)\n"))
176 176 except error.LockUnavailable as inst:
177 177 ui.error(_("abort: could not lock %s: %s\n") %
178 178 (inst.desc or stringutil.forcebytestr(inst.filename),
179 179 encoding.strtolocal(inst.strerror)))
180 180 except error.OutOfBandError as inst:
181 181 if inst.args:
182 182 msg = _("abort: remote error:\n")
183 183 else:
184 184 msg = _("abort: remote error\n")
185 185 ui.error(msg)
186 186 if inst.args:
187 187 ui.error(''.join(inst.args))
188 188 if inst.hint:
189 189 ui.error('(%s)\n' % inst.hint)
190 190 except error.RepoError as inst:
191 191 ui.error(_("abort: %s!\n") % inst)
192 192 if inst.hint:
193 193 ui.error(_("(%s)\n") % inst.hint)
194 194 except error.ResponseError as inst:
195 195 ui.error(_("abort: %s") % inst.args[0])
196 196 msg = inst.args[1]
197 197 if isinstance(msg, type(u'')):
198 198 msg = pycompat.sysbytes(msg)
199 199 if not isinstance(msg, bytes):
200 200 ui.error(" %r\n" % (msg,))
201 201 elif not msg:
202 202 ui.error(_(" empty string\n"))
203 203 else:
204 204 ui.error("\n%r\n" % pycompat.bytestr(stringutil.ellipsis(msg)))
205 205 except error.CensoredNodeError as inst:
206 206 ui.error(_("abort: file censored %s!\n") % inst)
207 207 except error.RevlogError as inst:
208 208 ui.error(_("abort: %s!\n") % inst)
209 209 except error.InterventionRequired as inst:
210 210 ui.error("%s\n" % inst)
211 211 if inst.hint:
212 212 ui.error(_("(%s)\n") % inst.hint)
213 213 return 1
214 214 except error.WdirUnsupported:
215 215 ui.error(_("abort: working directory revision cannot be specified\n"))
216 216 except error.Abort as inst:
217 217 ui.error(_("abort: %s\n") % inst)
218 218 if inst.hint:
219 219 ui.error(_("(%s)\n") % inst.hint)
220 220 except ImportError as inst:
221 221 ui.error(_("abort: %s!\n") % stringutil.forcebytestr(inst))
222 222 m = stringutil.forcebytestr(inst).split()[-1]
223 223 if m in "mpatch bdiff".split():
224 224 ui.error(_("(did you forget to compile extensions?)\n"))
225 225 elif m in "zlib".split():
226 226 ui.error(_("(is your Python install correct?)\n"))
227 227 except IOError as inst:
228 228 if util.safehasattr(inst, "code"):
229 229 ui.error(_("abort: %s\n") % stringutil.forcebytestr(inst))
230 230 elif util.safehasattr(inst, "reason"):
231 231 try: # usually it is in the form (errno, strerror)
232 232 reason = inst.reason.args[1]
233 233 except (AttributeError, IndexError):
234 234 # it might be anything, for example a string
235 235 reason = inst.reason
236 236 if isinstance(reason, pycompat.unicode):
237 237 # SSLError of Python 2.7.9 contains a unicode
238 238 reason = encoding.unitolocal(reason)
239 239 ui.error(_("abort: error: %s\n") % reason)
240 240 elif (util.safehasattr(inst, "args")
241 241 and inst.args and inst.args[0] == errno.EPIPE):
242 242 pass
243 243 elif getattr(inst, "strerror", None):
244 244 if getattr(inst, "filename", None):
245 245 ui.error(_("abort: %s: %s\n") % (
246 246 encoding.strtolocal(inst.strerror),
247 247 stringutil.forcebytestr(inst.filename)))
248 248 else:
249 249 ui.error(_("abort: %s\n") % encoding.strtolocal(inst.strerror))
250 250 else:
251 251 raise
252 252 except OSError as inst:
253 253 if getattr(inst, "filename", None) is not None:
254 254 ui.error(_("abort: %s: '%s'\n") % (
255 255 encoding.strtolocal(inst.strerror),
256 256 stringutil.forcebytestr(inst.filename)))
257 257 else:
258 258 ui.error(_("abort: %s\n") % encoding.strtolocal(inst.strerror))
259 259 except MemoryError:
260 260 ui.error(_("abort: out of memory\n"))
261 261 except SystemExit as inst:
262 262 # Commands shouldn't sys.exit directly, but give a return code.
263 263 # Just in case catch this and and pass exit code to caller.
264 264 return inst.code
265 265 except socket.error as inst:
266 266 ui.error(_("abort: %s\n") % stringutil.forcebytestr(inst.args[-1]))
267 267
268 268 return -1
269 269
270 270 def checknewlabel(repo, lbl, kind):
271 271 # Do not use the "kind" parameter in ui output.
272 272 # It makes strings difficult to translate.
273 273 if lbl in ['tip', '.', 'null']:
274 274 raise error.Abort(_("the name '%s' is reserved") % lbl)
275 275 for c in (':', '\0', '\n', '\r'):
276 276 if c in lbl:
277 277 raise error.Abort(
278 278 _("%r cannot be used in a name") % pycompat.bytestr(c))
279 279 try:
280 280 int(lbl)
281 281 raise error.Abort(_("cannot use an integer as a name"))
282 282 except ValueError:
283 283 pass
284 284 if lbl.strip() != lbl:
285 285 raise error.Abort(_("leading or trailing whitespace in name %r") % lbl)
286 286
287 287 def checkfilename(f):
288 288 '''Check that the filename f is an acceptable filename for a tracked file'''
289 289 if '\r' in f or '\n' in f:
290 290 raise error.Abort(_("'\\n' and '\\r' disallowed in filenames: %r")
291 291 % pycompat.bytestr(f))
292 292
293 293 def checkportable(ui, f):
294 294 '''Check if filename f is portable and warn or abort depending on config'''
295 295 checkfilename(f)
296 296 abort, warn = checkportabilityalert(ui)
297 297 if abort or warn:
298 298 msg = util.checkwinfilename(f)
299 299 if msg:
300 300 msg = "%s: %s" % (msg, procutil.shellquote(f))
301 301 if abort:
302 302 raise error.Abort(msg)
303 303 ui.warn(_("warning: %s\n") % msg)
304 304
305 305 def checkportabilityalert(ui):
306 306 '''check if the user's config requests nothing, a warning, or abort for
307 307 non-portable filenames'''
308 308 val = ui.config('ui', 'portablefilenames')
309 309 lval = val.lower()
310 310 bval = stringutil.parsebool(val)
311 311 abort = pycompat.iswindows or lval == 'abort'
312 312 warn = bval or lval == 'warn'
313 313 if bval is None and not (warn or abort or lval == 'ignore'):
314 314 raise error.ConfigError(
315 315 _("ui.portablefilenames value is invalid ('%s')") % val)
316 316 return abort, warn
317 317
318 318 class casecollisionauditor(object):
319 319 def __init__(self, ui, abort, dirstate):
320 320 self._ui = ui
321 321 self._abort = abort
322 322 allfiles = '\0'.join(dirstate._map)
323 323 self._loweredfiles = set(encoding.lower(allfiles).split('\0'))
324 324 self._dirstate = dirstate
325 325 # The purpose of _newfiles is so that we don't complain about
326 326 # case collisions if someone were to call this object with the
327 327 # same filename twice.
328 328 self._newfiles = set()
329 329
330 330 def __call__(self, f):
331 331 if f in self._newfiles:
332 332 return
333 333 fl = encoding.lower(f)
334 334 if fl in self._loweredfiles and f not in self._dirstate:
335 335 msg = _('possible case-folding collision for %s') % f
336 336 if self._abort:
337 337 raise error.Abort(msg)
338 338 self._ui.warn(_("warning: %s\n") % msg)
339 339 self._loweredfiles.add(fl)
340 340 self._newfiles.add(f)
341 341
342 342 def filteredhash(repo, maxrev):
343 343 """build hash of filtered revisions in the current repoview.
344 344
345 345 Multiple caches perform up-to-date validation by checking that the
346 346 tiprev and tipnode stored in the cache file match the current repository.
347 347 However, this is not sufficient for validating repoviews because the set
348 348 of revisions in the view may change without the repository tiprev and
349 349 tipnode changing.
350 350
351 351 This function hashes all the revs filtered from the view and returns
352 352 that SHA-1 digest.
353 353 """
354 354 cl = repo.changelog
355 355 if not cl.filteredrevs:
356 356 return None
357 357 key = None
358 358 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
359 359 if revs:
360 360 s = hashlib.sha1()
361 361 for rev in revs:
362 362 s.update('%d;' % rev)
363 363 key = s.digest()
364 364 return key
365 365
366 366 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
367 367 '''yield every hg repository under path, always recursively.
368 368 The recurse flag will only control recursion into repo working dirs'''
369 369 def errhandler(err):
370 370 if err.filename == path:
371 371 raise err
372 372 samestat = getattr(os.path, 'samestat', None)
373 373 if followsym and samestat is not None:
374 374 def adddir(dirlst, dirname):
375 375 dirstat = os.stat(dirname)
376 376 match = any(samestat(dirstat, lstdirstat) for lstdirstat in dirlst)
377 377 if not match:
378 378 dirlst.append(dirstat)
379 379 return not match
380 380 else:
381 381 followsym = False
382 382
383 383 if (seen_dirs is None) and followsym:
384 384 seen_dirs = []
385 385 adddir(seen_dirs, path)
386 386 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
387 387 dirs.sort()
388 388 if '.hg' in dirs:
389 389 yield root # found a repository
390 390 qroot = os.path.join(root, '.hg', 'patches')
391 391 if os.path.isdir(os.path.join(qroot, '.hg')):
392 392 yield qroot # we have a patch queue repo here
393 393 if recurse:
394 394 # avoid recursing inside the .hg directory
395 395 dirs.remove('.hg')
396 396 else:
397 397 dirs[:] = [] # don't descend further
398 398 elif followsym:
399 399 newdirs = []
400 400 for d in dirs:
401 401 fname = os.path.join(root, d)
402 402 if adddir(seen_dirs, fname):
403 403 if os.path.islink(fname):
404 404 for hgname in walkrepos(fname, True, seen_dirs):
405 405 yield hgname
406 406 else:
407 407 newdirs.append(d)
408 408 dirs[:] = newdirs
409 409
410 410 def binnode(ctx):
411 411 """Return binary node id for a given basectx"""
412 412 node = ctx.node()
413 413 if node is None:
414 414 return wdirid
415 415 return node
416 416
417 417 def intrev(ctx):
418 418 """Return integer for a given basectx that can be used in comparison or
419 419 arithmetic operation"""
420 420 rev = ctx.rev()
421 421 if rev is None:
422 422 return wdirrev
423 423 return rev
424 424
425 425 def formatchangeid(ctx):
426 426 """Format changectx as '{rev}:{node|formatnode}', which is the default
427 427 template provided by logcmdutil.changesettemplater"""
428 428 repo = ctx.repo()
429 429 return formatrevnode(repo.ui, intrev(ctx), binnode(ctx))
430 430
431 431 def formatrevnode(ui, rev, node):
432 432 """Format given revision and node depending on the current verbosity"""
433 433 if ui.debugflag:
434 434 hexfunc = hex
435 435 else:
436 436 hexfunc = short
437 437 return '%d:%s' % (rev, hexfunc(node))
438 438
439 439 def resolvehexnodeidprefix(repo, prefix):
440 440 # Uses unfiltered repo because it's faster when prefix is ambiguous/
441 441 # This matches the shortesthexnodeidprefix() function below.
442 442 node = repo.unfiltered().changelog._partialmatch(prefix)
443 443 if node is None:
444 444 return
445 445 repo.changelog.rev(node) # make sure node isn't filtered
446 446 return node
447 447
448 448 def shortesthexnodeidprefix(repo, node, minlength=1):
449 449 """Find the shortest unambiguous prefix that matches hexnode."""
450 450 # _partialmatch() of filtered changelog could take O(len(repo)) time,
451 451 # which would be unacceptably slow. so we look for hash collision in
452 452 # unfiltered space, which means some hashes may be slightly longer.
453 453 cl = repo.unfiltered().changelog
454 454
455 455 def isrev(prefix):
456 456 try:
457 457 i = int(prefix)
458 458 # if we are a pure int, then starting with zero will not be
459 459 # confused as a rev; or, obviously, if the int is larger
460 460 # than the value of the tip rev
461 461 if prefix[0:1] == b'0' or i > len(cl):
462 462 return False
463 463 return True
464 464 except ValueError:
465 465 return False
466 466
467 467 def disambiguate(prefix):
468 468 """Disambiguate against revnums."""
469 469 hexnode = hex(node)
470 470 for length in range(len(prefix), len(hexnode) + 1):
471 471 prefix = hexnode[:length]
472 472 if not isrev(prefix):
473 473 return prefix
474 474
475 475 try:
476 476 return disambiguate(cl.shortest(node, minlength))
477 477 except error.LookupError:
478 478 raise error.RepoLookupError()
479 479
480 480 def isrevsymbol(repo, symbol):
481 481 """Checks if a symbol exists in the repo.
482 482
483 See revsymbol() for details. Raises error.LookupError if the symbol is an
484 ambiguous nodeid prefix.
483 See revsymbol() for details. Raises error.AmbiguousPrefixLookupError if the
484 symbol is an ambiguous nodeid prefix.
485 485 """
486 486 try:
487 487 revsymbol(repo, symbol)
488 488 return True
489 489 except error.RepoLookupError:
490 490 return False
491 491
492 492 def revsymbol(repo, symbol):
493 493 """Returns a context given a single revision symbol (as string).
494 494
495 495 This is similar to revsingle(), but accepts only a single revision symbol,
496 496 i.e. things like ".", "tip", "1234", "deadbeef", "my-bookmark" work, but
497 497 not "max(public())".
498 498 """
499 499 if not isinstance(symbol, bytes):
500 500 msg = ("symbol (%s of type %s) was not a string, did you mean "
501 501 "repo[symbol]?" % (symbol, type(symbol)))
502 502 raise error.ProgrammingError(msg)
503 503 try:
504 504 if symbol in ('.', 'tip', 'null'):
505 505 return repo[symbol]
506 506
507 507 try:
508 508 r = int(symbol)
509 509 if '%d' % r != symbol:
510 510 raise ValueError
511 511 l = len(repo.changelog)
512 512 if r < 0:
513 513 r += l
514 514 if r < 0 or r >= l and r != wdirrev:
515 515 raise ValueError
516 516 return repo[r]
517 517 except error.FilteredIndexError:
518 518 raise
519 519 except (ValueError, OverflowError, IndexError):
520 520 pass
521 521
522 522 if len(symbol) == 40:
523 523 try:
524 524 node = bin(symbol)
525 525 rev = repo.changelog.rev(node)
526 526 return repo[rev]
527 527 except error.FilteredLookupError:
528 528 raise
529 529 except (TypeError, LookupError):
530 530 pass
531 531
532 532 # look up bookmarks through the name interface
533 533 try:
534 534 node = repo.names.singlenode(repo, symbol)
535 535 rev = repo.changelog.rev(node)
536 536 return repo[rev]
537 537 except KeyError:
538 538 pass
539 539
540 540 node = resolvehexnodeidprefix(repo, symbol)
541 541 if node is not None:
542 542 rev = repo.changelog.rev(node)
543 543 return repo[rev]
544 544
545 545 raise error.RepoLookupError(_("unknown revision '%s'") % symbol)
546 546
547 547 except error.WdirUnsupported:
548 548 return repo[None]
549 549 except (error.FilteredIndexError, error.FilteredLookupError,
550 550 error.FilteredRepoLookupError):
551 551 raise _filterederror(repo, symbol)
552 552
553 553 def _filterederror(repo, changeid):
554 554 """build an exception to be raised about a filtered changeid
555 555
556 556 This is extracted in a function to help extensions (eg: evolve) to
557 557 experiment with various message variants."""
558 558 if repo.filtername.startswith('visible'):
559 559
560 560 # Check if the changeset is obsolete
561 561 unfilteredrepo = repo.unfiltered()
562 562 ctx = revsymbol(unfilteredrepo, changeid)
563 563
564 564 # If the changeset is obsolete, enrich the message with the reason
565 565 # that made this changeset not visible
566 566 if ctx.obsolete():
567 567 msg = obsutil._getfilteredreason(repo, changeid, ctx)
568 568 else:
569 569 msg = _("hidden revision '%s'") % changeid
570 570
571 571 hint = _('use --hidden to access hidden revisions')
572 572
573 573 return error.FilteredRepoLookupError(msg, hint=hint)
574 574 msg = _("filtered revision '%s' (not in '%s' subset)")
575 575 msg %= (changeid, repo.filtername)
576 576 return error.FilteredRepoLookupError(msg)
577 577
578 578 def revsingle(repo, revspec, default='.', localalias=None):
579 579 if not revspec and revspec != 0:
580 580 return repo[default]
581 581
582 582 l = revrange(repo, [revspec], localalias=localalias)
583 583 if not l:
584 584 raise error.Abort(_('empty revision set'))
585 585 return repo[l.last()]
586 586
587 587 def _pairspec(revspec):
588 588 tree = revsetlang.parse(revspec)
589 589 return tree and tree[0] in ('range', 'rangepre', 'rangepost', 'rangeall')
590 590
591 591 def revpair(repo, revs):
592 592 if not revs:
593 593 return repo['.'], repo[None]
594 594
595 595 l = revrange(repo, revs)
596 596
597 597 if not l:
598 598 first = second = None
599 599 elif l.isascending():
600 600 first = l.min()
601 601 second = l.max()
602 602 elif l.isdescending():
603 603 first = l.max()
604 604 second = l.min()
605 605 else:
606 606 first = l.first()
607 607 second = l.last()
608 608
609 609 if first is None:
610 610 raise error.Abort(_('empty revision range'))
611 611 if (first == second and len(revs) >= 2
612 612 and not all(revrange(repo, [r]) for r in revs)):
613 613 raise error.Abort(_('empty revision on one side of range'))
614 614
615 615 # if top-level is range expression, the result must always be a pair
616 616 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
617 617 return repo[first], repo[None]
618 618
619 619 return repo[first], repo[second]
620 620
621 621 def revrange(repo, specs, localalias=None):
622 622 """Execute 1 to many revsets and return the union.
623 623
624 624 This is the preferred mechanism for executing revsets using user-specified
625 625 config options, such as revset aliases.
626 626
627 627 The revsets specified by ``specs`` will be executed via a chained ``OR``
628 628 expression. If ``specs`` is empty, an empty result is returned.
629 629
630 630 ``specs`` can contain integers, in which case they are assumed to be
631 631 revision numbers.
632 632
633 633 It is assumed the revsets are already formatted. If you have arguments
634 634 that need to be expanded in the revset, call ``revsetlang.formatspec()``
635 635 and pass the result as an element of ``specs``.
636 636
637 637 Specifying a single revset is allowed.
638 638
639 639 Returns a ``revset.abstractsmartset`` which is a list-like interface over
640 640 integer revisions.
641 641 """
642 642 allspecs = []
643 643 for spec in specs:
644 644 if isinstance(spec, int):
645 645 spec = revsetlang.formatspec('rev(%d)', spec)
646 646 allspecs.append(spec)
647 647 return repo.anyrevs(allspecs, user=True, localalias=localalias)
648 648
649 649 def meaningfulparents(repo, ctx):
650 650 """Return list of meaningful (or all if debug) parentrevs for rev.
651 651
652 652 For merges (two non-nullrev revisions) both parents are meaningful.
653 653 Otherwise the first parent revision is considered meaningful if it
654 654 is not the preceding revision.
655 655 """
656 656 parents = ctx.parents()
657 657 if len(parents) > 1:
658 658 return parents
659 659 if repo.ui.debugflag:
660 660 return [parents[0], repo['null']]
661 661 if parents[0].rev() >= intrev(ctx) - 1:
662 662 return []
663 663 return parents
664 664
665 665 def expandpats(pats):
666 666 '''Expand bare globs when running on windows.
667 667 On posix we assume it already has already been done by sh.'''
668 668 if not util.expandglobs:
669 669 return list(pats)
670 670 ret = []
671 671 for kindpat in pats:
672 672 kind, pat = matchmod._patsplit(kindpat, None)
673 673 if kind is None:
674 674 try:
675 675 globbed = glob.glob(pat)
676 676 except re.error:
677 677 globbed = [pat]
678 678 if globbed:
679 679 ret.extend(globbed)
680 680 continue
681 681 ret.append(kindpat)
682 682 return ret
683 683
684 684 def matchandpats(ctx, pats=(), opts=None, globbed=False, default='relpath',
685 685 badfn=None):
686 686 '''Return a matcher and the patterns that were used.
687 687 The matcher will warn about bad matches, unless an alternate badfn callback
688 688 is provided.'''
689 689 if pats == ("",):
690 690 pats = []
691 691 if opts is None:
692 692 opts = {}
693 693 if not globbed and default == 'relpath':
694 694 pats = expandpats(pats or [])
695 695
696 696 def bad(f, msg):
697 697 ctx.repo().ui.warn("%s: %s\n" % (m.rel(f), msg))
698 698
699 699 if badfn is None:
700 700 badfn = bad
701 701
702 702 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
703 703 default, listsubrepos=opts.get('subrepos'), badfn=badfn)
704 704
705 705 if m.always():
706 706 pats = []
707 707 return m, pats
708 708
709 709 def match(ctx, pats=(), opts=None, globbed=False, default='relpath',
710 710 badfn=None):
711 711 '''Return a matcher that will warn about bad matches.'''
712 712 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
713 713
714 714 def matchall(repo):
715 715 '''Return a matcher that will efficiently match everything.'''
716 716 return matchmod.always(repo.root, repo.getcwd())
717 717
718 718 def matchfiles(repo, files, badfn=None):
719 719 '''Return a matcher that will efficiently match exactly these files.'''
720 720 return matchmod.exact(repo.root, repo.getcwd(), files, badfn=badfn)
721 721
722 722 def parsefollowlinespattern(repo, rev, pat, msg):
723 723 """Return a file name from `pat` pattern suitable for usage in followlines
724 724 logic.
725 725 """
726 726 if not matchmod.patkind(pat):
727 727 return pathutil.canonpath(repo.root, repo.getcwd(), pat)
728 728 else:
729 729 ctx = repo[rev]
730 730 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=ctx)
731 731 files = [f for f in ctx if m(f)]
732 732 if len(files) != 1:
733 733 raise error.ParseError(msg)
734 734 return files[0]
735 735
736 736 def origpath(ui, repo, filepath):
737 737 '''customize where .orig files are created
738 738
739 739 Fetch user defined path from config file: [ui] origbackuppath = <path>
740 740 Fall back to default (filepath with .orig suffix) if not specified
741 741 '''
742 742 origbackuppath = ui.config('ui', 'origbackuppath')
743 743 if not origbackuppath:
744 744 return filepath + ".orig"
745 745
746 746 # Convert filepath from an absolute path into a path inside the repo.
747 747 filepathfromroot = util.normpath(os.path.relpath(filepath,
748 748 start=repo.root))
749 749
750 750 origvfs = vfs.vfs(repo.wjoin(origbackuppath))
751 751 origbackupdir = origvfs.dirname(filepathfromroot)
752 752 if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir):
753 753 ui.note(_('creating directory: %s\n') % origvfs.join(origbackupdir))
754 754
755 755 # Remove any files that conflict with the backup file's path
756 756 for f in reversed(list(util.finddirs(filepathfromroot))):
757 757 if origvfs.isfileorlink(f):
758 758 ui.note(_('removing conflicting file: %s\n')
759 759 % origvfs.join(f))
760 760 origvfs.unlink(f)
761 761 break
762 762
763 763 origvfs.makedirs(origbackupdir)
764 764
765 765 if origvfs.isdir(filepathfromroot) and not origvfs.islink(filepathfromroot):
766 766 ui.note(_('removing conflicting directory: %s\n')
767 767 % origvfs.join(filepathfromroot))
768 768 origvfs.rmtree(filepathfromroot, forcibly=True)
769 769
770 770 return origvfs.join(filepathfromroot)
771 771
772 772 class _containsnode(object):
773 773 """proxy __contains__(node) to container.__contains__ which accepts revs"""
774 774
775 775 def __init__(self, repo, revcontainer):
776 776 self._torev = repo.changelog.rev
777 777 self._revcontains = revcontainer.__contains__
778 778
779 779 def __contains__(self, node):
780 780 return self._revcontains(self._torev(node))
781 781
782 782 def cleanupnodes(repo, replacements, operation, moves=None, metadata=None,
783 783 fixphase=False, targetphase=None, backup=True):
784 784 """do common cleanups when old nodes are replaced by new nodes
785 785
786 786 That includes writing obsmarkers or stripping nodes, and moving bookmarks.
787 787 (we might also want to move working directory parent in the future)
788 788
789 789 By default, bookmark moves are calculated automatically from 'replacements',
790 790 but 'moves' can be used to override that. Also, 'moves' may include
791 791 additional bookmark moves that should not have associated obsmarkers.
792 792
793 793 replacements is {oldnode: [newnode]} or a iterable of nodes if they do not
794 794 have replacements. operation is a string, like "rebase".
795 795
796 796 metadata is dictionary containing metadata to be stored in obsmarker if
797 797 obsolescence is enabled.
798 798 """
799 799 assert fixphase or targetphase is None
800 800 if not replacements and not moves:
801 801 return
802 802
803 803 # translate mapping's other forms
804 804 if not util.safehasattr(replacements, 'items'):
805 805 replacements = {n: () for n in replacements}
806 806
807 807 # Calculate bookmark movements
808 808 if moves is None:
809 809 moves = {}
810 810 # Unfiltered repo is needed since nodes in replacements might be hidden.
811 811 unfi = repo.unfiltered()
812 812 for oldnode, newnodes in replacements.items():
813 813 if oldnode in moves:
814 814 continue
815 815 if len(newnodes) > 1:
816 816 # usually a split, take the one with biggest rev number
817 817 newnode = next(unfi.set('max(%ln)', newnodes)).node()
818 818 elif len(newnodes) == 0:
819 819 # move bookmark backwards
820 820 roots = list(unfi.set('max((::%n) - %ln)', oldnode,
821 821 list(replacements)))
822 822 if roots:
823 823 newnode = roots[0].node()
824 824 else:
825 825 newnode = nullid
826 826 else:
827 827 newnode = newnodes[0]
828 828 moves[oldnode] = newnode
829 829
830 830 allnewnodes = [n for ns in replacements.values() for n in ns]
831 831 toretract = {}
832 832 toadvance = {}
833 833 if fixphase:
834 834 precursors = {}
835 835 for oldnode, newnodes in replacements.items():
836 836 for newnode in newnodes:
837 837 precursors.setdefault(newnode, []).append(oldnode)
838 838
839 839 allnewnodes.sort(key=lambda n: unfi[n].rev())
840 840 newphases = {}
841 841 def phase(ctx):
842 842 return newphases.get(ctx.node(), ctx.phase())
843 843 for newnode in allnewnodes:
844 844 ctx = unfi[newnode]
845 845 parentphase = max(phase(p) for p in ctx.parents())
846 846 if targetphase is None:
847 847 oldphase = max(unfi[oldnode].phase()
848 848 for oldnode in precursors[newnode])
849 849 newphase = max(oldphase, parentphase)
850 850 else:
851 851 newphase = max(targetphase, parentphase)
852 852 newphases[newnode] = newphase
853 853 if newphase > ctx.phase():
854 854 toretract.setdefault(newphase, []).append(newnode)
855 855 elif newphase < ctx.phase():
856 856 toadvance.setdefault(newphase, []).append(newnode)
857 857
858 858 with repo.transaction('cleanup') as tr:
859 859 # Move bookmarks
860 860 bmarks = repo._bookmarks
861 861 bmarkchanges = []
862 862 for oldnode, newnode in moves.items():
863 863 oldbmarks = repo.nodebookmarks(oldnode)
864 864 if not oldbmarks:
865 865 continue
866 866 from . import bookmarks # avoid import cycle
867 867 repo.ui.debug('moving bookmarks %r from %s to %s\n' %
868 868 (pycompat.rapply(pycompat.maybebytestr, oldbmarks),
869 869 hex(oldnode), hex(newnode)))
870 870 # Delete divergent bookmarks being parents of related newnodes
871 871 deleterevs = repo.revs('parents(roots(%ln & (::%n))) - parents(%n)',
872 872 allnewnodes, newnode, oldnode)
873 873 deletenodes = _containsnode(repo, deleterevs)
874 874 for name in oldbmarks:
875 875 bmarkchanges.append((name, newnode))
876 876 for b in bookmarks.divergent2delete(repo, deletenodes, name):
877 877 bmarkchanges.append((b, None))
878 878
879 879 if bmarkchanges:
880 880 bmarks.applychanges(repo, tr, bmarkchanges)
881 881
882 882 for phase, nodes in toretract.items():
883 883 phases.retractboundary(repo, tr, phase, nodes)
884 884 for phase, nodes in toadvance.items():
885 885 phases.advanceboundary(repo, tr, phase, nodes)
886 886
887 887 # Obsolete or strip nodes
888 888 if obsolete.isenabled(repo, obsolete.createmarkersopt):
889 889 # If a node is already obsoleted, and we want to obsolete it
890 890 # without a successor, skip that obssolete request since it's
891 891 # unnecessary. That's the "if s or not isobs(n)" check below.
892 892 # Also sort the node in topology order, that might be useful for
893 893 # some obsstore logic.
894 894 # NOTE: the filtering and sorting might belong to createmarkers.
895 895 isobs = unfi.obsstore.successors.__contains__
896 896 torev = unfi.changelog.rev
897 897 sortfunc = lambda ns: torev(ns[0])
898 898 rels = [(unfi[n], tuple(unfi[m] for m in s))
899 899 for n, s in sorted(replacements.items(), key=sortfunc)
900 900 if s or not isobs(n)]
901 901 if rels:
902 902 obsolete.createmarkers(repo, rels, operation=operation,
903 903 metadata=metadata)
904 904 else:
905 905 from . import repair # avoid import cycle
906 906 tostrip = list(replacements)
907 907 if tostrip:
908 908 repair.delayedstrip(repo.ui, repo, tostrip, operation,
909 909 backup=backup)
910 910
911 911 def addremove(repo, matcher, prefix, opts=None):
912 912 if opts is None:
913 913 opts = {}
914 914 m = matcher
915 915 dry_run = opts.get('dry_run')
916 916 try:
917 917 similarity = float(opts.get('similarity') or 0)
918 918 except ValueError:
919 919 raise error.Abort(_('similarity must be a number'))
920 920 if similarity < 0 or similarity > 100:
921 921 raise error.Abort(_('similarity must be between 0 and 100'))
922 922 similarity /= 100.0
923 923
924 924 ret = 0
925 925 join = lambda f: os.path.join(prefix, f)
926 926
927 927 wctx = repo[None]
928 928 for subpath in sorted(wctx.substate):
929 929 submatch = matchmod.subdirmatcher(subpath, m)
930 930 if opts.get('subrepos') or m.exact(subpath) or any(submatch.files()):
931 931 sub = wctx.sub(subpath)
932 932 try:
933 933 if sub.addremove(submatch, prefix, opts):
934 934 ret = 1
935 935 except error.LookupError:
936 936 repo.ui.status(_("skipping missing subrepository: %s\n")
937 937 % join(subpath))
938 938
939 939 rejected = []
940 940 def badfn(f, msg):
941 941 if f in m.files():
942 942 m.bad(f, msg)
943 943 rejected.append(f)
944 944
945 945 badmatch = matchmod.badmatch(m, badfn)
946 946 added, unknown, deleted, removed, forgotten = _interestingfiles(repo,
947 947 badmatch)
948 948
949 949 unknownset = set(unknown + forgotten)
950 950 toprint = unknownset.copy()
951 951 toprint.update(deleted)
952 952 for abs in sorted(toprint):
953 953 if repo.ui.verbose or not m.exact(abs):
954 954 if abs in unknownset:
955 955 status = _('adding %s\n') % m.uipath(abs)
956 956 else:
957 957 status = _('removing %s\n') % m.uipath(abs)
958 958 repo.ui.status(status)
959 959
960 960 renames = _findrenames(repo, m, added + unknown, removed + deleted,
961 961 similarity)
962 962
963 963 if not dry_run:
964 964 _markchanges(repo, unknown + forgotten, deleted, renames)
965 965
966 966 for f in rejected:
967 967 if f in m.files():
968 968 return 1
969 969 return ret
970 970
971 971 def marktouched(repo, files, similarity=0.0):
972 972 '''Assert that files have somehow been operated upon. files are relative to
973 973 the repo root.'''
974 974 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
975 975 rejected = []
976 976
977 977 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
978 978
979 979 if repo.ui.verbose:
980 980 unknownset = set(unknown + forgotten)
981 981 toprint = unknownset.copy()
982 982 toprint.update(deleted)
983 983 for abs in sorted(toprint):
984 984 if abs in unknownset:
985 985 status = _('adding %s\n') % abs
986 986 else:
987 987 status = _('removing %s\n') % abs
988 988 repo.ui.status(status)
989 989
990 990 renames = _findrenames(repo, m, added + unknown, removed + deleted,
991 991 similarity)
992 992
993 993 _markchanges(repo, unknown + forgotten, deleted, renames)
994 994
995 995 for f in rejected:
996 996 if f in m.files():
997 997 return 1
998 998 return 0
999 999
1000 1000 def _interestingfiles(repo, matcher):
1001 1001 '''Walk dirstate with matcher, looking for files that addremove would care
1002 1002 about.
1003 1003
1004 1004 This is different from dirstate.status because it doesn't care about
1005 1005 whether files are modified or clean.'''
1006 1006 added, unknown, deleted, removed, forgotten = [], [], [], [], []
1007 1007 audit_path = pathutil.pathauditor(repo.root, cached=True)
1008 1008
1009 1009 ctx = repo[None]
1010 1010 dirstate = repo.dirstate
1011 1011 walkresults = dirstate.walk(matcher, subrepos=sorted(ctx.substate),
1012 1012 unknown=True, ignored=False, full=False)
1013 1013 for abs, st in walkresults.iteritems():
1014 1014 dstate = dirstate[abs]
1015 1015 if dstate == '?' and audit_path.check(abs):
1016 1016 unknown.append(abs)
1017 1017 elif dstate != 'r' and not st:
1018 1018 deleted.append(abs)
1019 1019 elif dstate == 'r' and st:
1020 1020 forgotten.append(abs)
1021 1021 # for finding renames
1022 1022 elif dstate == 'r' and not st:
1023 1023 removed.append(abs)
1024 1024 elif dstate == 'a':
1025 1025 added.append(abs)
1026 1026
1027 1027 return added, unknown, deleted, removed, forgotten
1028 1028
1029 1029 def _findrenames(repo, matcher, added, removed, similarity):
1030 1030 '''Find renames from removed files to added ones.'''
1031 1031 renames = {}
1032 1032 if similarity > 0:
1033 1033 for old, new, score in similar.findrenames(repo, added, removed,
1034 1034 similarity):
1035 1035 if (repo.ui.verbose or not matcher.exact(old)
1036 1036 or not matcher.exact(new)):
1037 1037 repo.ui.status(_('recording removal of %s as rename to %s '
1038 1038 '(%d%% similar)\n') %
1039 1039 (matcher.rel(old), matcher.rel(new),
1040 1040 score * 100))
1041 1041 renames[new] = old
1042 1042 return renames
1043 1043
1044 1044 def _markchanges(repo, unknown, deleted, renames):
1045 1045 '''Marks the files in unknown as added, the files in deleted as removed,
1046 1046 and the files in renames as copied.'''
1047 1047 wctx = repo[None]
1048 1048 with repo.wlock():
1049 1049 wctx.forget(deleted)
1050 1050 wctx.add(unknown)
1051 1051 for new, old in renames.iteritems():
1052 1052 wctx.copy(old, new)
1053 1053
1054 1054 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
1055 1055 """Update the dirstate to reflect the intent of copying src to dst. For
1056 1056 different reasons it might not end with dst being marked as copied from src.
1057 1057 """
1058 1058 origsrc = repo.dirstate.copied(src) or src
1059 1059 if dst == origsrc: # copying back a copy?
1060 1060 if repo.dirstate[dst] not in 'mn' and not dryrun:
1061 1061 repo.dirstate.normallookup(dst)
1062 1062 else:
1063 1063 if repo.dirstate[origsrc] == 'a' and origsrc == src:
1064 1064 if not ui.quiet:
1065 1065 ui.warn(_("%s has not been committed yet, so no copy "
1066 1066 "data will be stored for %s.\n")
1067 1067 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
1068 1068 if repo.dirstate[dst] in '?r' and not dryrun:
1069 1069 wctx.add([dst])
1070 1070 elif not dryrun:
1071 1071 wctx.copy(origsrc, dst)
1072 1072
1073 1073 def readrequires(opener, supported):
1074 1074 '''Reads and parses .hg/requires and checks if all entries found
1075 1075 are in the list of supported features.'''
1076 1076 requirements = set(opener.read("requires").splitlines())
1077 1077 missings = []
1078 1078 for r in requirements:
1079 1079 if r not in supported:
1080 1080 if not r or not r[0:1].isalnum():
1081 1081 raise error.RequirementError(_(".hg/requires file is corrupt"))
1082 1082 missings.append(r)
1083 1083 missings.sort()
1084 1084 if missings:
1085 1085 raise error.RequirementError(
1086 1086 _("repository requires features unknown to this Mercurial: %s")
1087 1087 % " ".join(missings),
1088 1088 hint=_("see https://mercurial-scm.org/wiki/MissingRequirement"
1089 1089 " for more information"))
1090 1090 return requirements
1091 1091
1092 1092 def writerequires(opener, requirements):
1093 1093 with opener('requires', 'w') as fp:
1094 1094 for r in sorted(requirements):
1095 1095 fp.write("%s\n" % r)
1096 1096
1097 1097 class filecachesubentry(object):
1098 1098 def __init__(self, path, stat):
1099 1099 self.path = path
1100 1100 self.cachestat = None
1101 1101 self._cacheable = None
1102 1102
1103 1103 if stat:
1104 1104 self.cachestat = filecachesubentry.stat(self.path)
1105 1105
1106 1106 if self.cachestat:
1107 1107 self._cacheable = self.cachestat.cacheable()
1108 1108 else:
1109 1109 # None means we don't know yet
1110 1110 self._cacheable = None
1111 1111
1112 1112 def refresh(self):
1113 1113 if self.cacheable():
1114 1114 self.cachestat = filecachesubentry.stat(self.path)
1115 1115
1116 1116 def cacheable(self):
1117 1117 if self._cacheable is not None:
1118 1118 return self._cacheable
1119 1119
1120 1120 # we don't know yet, assume it is for now
1121 1121 return True
1122 1122
1123 1123 def changed(self):
1124 1124 # no point in going further if we can't cache it
1125 1125 if not self.cacheable():
1126 1126 return True
1127 1127
1128 1128 newstat = filecachesubentry.stat(self.path)
1129 1129
1130 1130 # we may not know if it's cacheable yet, check again now
1131 1131 if newstat and self._cacheable is None:
1132 1132 self._cacheable = newstat.cacheable()
1133 1133
1134 1134 # check again
1135 1135 if not self._cacheable:
1136 1136 return True
1137 1137
1138 1138 if self.cachestat != newstat:
1139 1139 self.cachestat = newstat
1140 1140 return True
1141 1141 else:
1142 1142 return False
1143 1143
1144 1144 @staticmethod
1145 1145 def stat(path):
1146 1146 try:
1147 1147 return util.cachestat(path)
1148 1148 except OSError as e:
1149 1149 if e.errno != errno.ENOENT:
1150 1150 raise
1151 1151
1152 1152 class filecacheentry(object):
1153 1153 def __init__(self, paths, stat=True):
1154 1154 self._entries = []
1155 1155 for path in paths:
1156 1156 self._entries.append(filecachesubentry(path, stat))
1157 1157
1158 1158 def changed(self):
1159 1159 '''true if any entry has changed'''
1160 1160 for entry in self._entries:
1161 1161 if entry.changed():
1162 1162 return True
1163 1163 return False
1164 1164
1165 1165 def refresh(self):
1166 1166 for entry in self._entries:
1167 1167 entry.refresh()
1168 1168
1169 1169 class filecache(object):
1170 1170 """A property like decorator that tracks files under .hg/ for updates.
1171 1171
1172 1172 On first access, the files defined as arguments are stat()ed and the
1173 1173 results cached. The decorated function is called. The results are stashed
1174 1174 away in a ``_filecache`` dict on the object whose method is decorated.
1175 1175
1176 1176 On subsequent access, the cached result is returned.
1177 1177
1178 1178 On external property set operations, stat() calls are performed and the new
1179 1179 value is cached.
1180 1180
1181 1181 On property delete operations, cached data is removed.
1182 1182
1183 1183 When using the property API, cached data is always returned, if available:
1184 1184 no stat() is performed to check if the file has changed and if the function
1185 1185 needs to be called to reflect file changes.
1186 1186
1187 1187 Others can muck about with the state of the ``_filecache`` dict. e.g. they
1188 1188 can populate an entry before the property's getter is called. In this case,
1189 1189 entries in ``_filecache`` will be used during property operations,
1190 1190 if available. If the underlying file changes, it is up to external callers
1191 1191 to reflect this by e.g. calling ``delattr(obj, attr)`` to remove the cached
1192 1192 method result as well as possibly calling ``del obj._filecache[attr]`` to
1193 1193 remove the ``filecacheentry``.
1194 1194 """
1195 1195
1196 1196 def __init__(self, *paths):
1197 1197 self.paths = paths
1198 1198
1199 1199 def join(self, obj, fname):
1200 1200 """Used to compute the runtime path of a cached file.
1201 1201
1202 1202 Users should subclass filecache and provide their own version of this
1203 1203 function to call the appropriate join function on 'obj' (an instance
1204 1204 of the class that its member function was decorated).
1205 1205 """
1206 1206 raise NotImplementedError
1207 1207
1208 1208 def __call__(self, func):
1209 1209 self.func = func
1210 1210 self.sname = func.__name__
1211 1211 self.name = pycompat.sysbytes(self.sname)
1212 1212 return self
1213 1213
1214 1214 def __get__(self, obj, type=None):
1215 1215 # if accessed on the class, return the descriptor itself.
1216 1216 if obj is None:
1217 1217 return self
1218 1218 # do we need to check if the file changed?
1219 1219 if self.sname in obj.__dict__:
1220 1220 assert self.name in obj._filecache, self.name
1221 1221 return obj.__dict__[self.sname]
1222 1222
1223 1223 entry = obj._filecache.get(self.name)
1224 1224
1225 1225 if entry:
1226 1226 if entry.changed():
1227 1227 entry.obj = self.func(obj)
1228 1228 else:
1229 1229 paths = [self.join(obj, path) for path in self.paths]
1230 1230
1231 1231 # We stat -before- creating the object so our cache doesn't lie if
1232 1232 # a writer modified between the time we read and stat
1233 1233 entry = filecacheentry(paths, True)
1234 1234 entry.obj = self.func(obj)
1235 1235
1236 1236 obj._filecache[self.name] = entry
1237 1237
1238 1238 obj.__dict__[self.sname] = entry.obj
1239 1239 return entry.obj
1240 1240
1241 1241 def __set__(self, obj, value):
1242 1242 if self.name not in obj._filecache:
1243 1243 # we add an entry for the missing value because X in __dict__
1244 1244 # implies X in _filecache
1245 1245 paths = [self.join(obj, path) for path in self.paths]
1246 1246 ce = filecacheentry(paths, False)
1247 1247 obj._filecache[self.name] = ce
1248 1248 else:
1249 1249 ce = obj._filecache[self.name]
1250 1250
1251 1251 ce.obj = value # update cached copy
1252 1252 obj.__dict__[self.sname] = value # update copy returned by obj.x
1253 1253
1254 1254 def __delete__(self, obj):
1255 1255 try:
1256 1256 del obj.__dict__[self.sname]
1257 1257 except KeyError:
1258 1258 raise AttributeError(self.sname)
1259 1259
1260 1260 def extdatasource(repo, source):
1261 1261 """Gather a map of rev -> value dict from the specified source
1262 1262
1263 1263 A source spec is treated as a URL, with a special case shell: type
1264 1264 for parsing the output from a shell command.
1265 1265
1266 1266 The data is parsed as a series of newline-separated records where
1267 1267 each record is a revision specifier optionally followed by a space
1268 1268 and a freeform string value. If the revision is known locally, it
1269 1269 is converted to a rev, otherwise the record is skipped.
1270 1270
1271 1271 Note that both key and value are treated as UTF-8 and converted to
1272 1272 the local encoding. This allows uniformity between local and
1273 1273 remote data sources.
1274 1274 """
1275 1275
1276 1276 spec = repo.ui.config("extdata", source)
1277 1277 if not spec:
1278 1278 raise error.Abort(_("unknown extdata source '%s'") % source)
1279 1279
1280 1280 data = {}
1281 1281 src = proc = None
1282 1282 try:
1283 1283 if spec.startswith("shell:"):
1284 1284 # external commands should be run relative to the repo root
1285 1285 cmd = spec[6:]
1286 1286 proc = subprocess.Popen(cmd, shell=True, bufsize=-1,
1287 1287 close_fds=procutil.closefds,
1288 1288 stdout=subprocess.PIPE, cwd=repo.root)
1289 1289 src = proc.stdout
1290 1290 else:
1291 1291 # treat as a URL or file
1292 1292 src = url.open(repo.ui, spec)
1293 1293 for l in src:
1294 1294 if " " in l:
1295 1295 k, v = l.strip().split(" ", 1)
1296 1296 else:
1297 1297 k, v = l.strip(), ""
1298 1298
1299 1299 k = encoding.tolocal(k)
1300 1300 try:
1301 1301 data[revsingle(repo, k).rev()] = encoding.tolocal(v)
1302 1302 except (error.LookupError, error.RepoLookupError):
1303 1303 pass # we ignore data for nodes that don't exist locally
1304 1304 finally:
1305 1305 if proc:
1306 1306 proc.communicate()
1307 1307 if src:
1308 1308 src.close()
1309 1309 if proc and proc.returncode != 0:
1310 1310 raise error.Abort(_("extdata command '%s' failed: %s")
1311 1311 % (cmd, procutil.explainexit(proc.returncode)))
1312 1312
1313 1313 return data
1314 1314
1315 1315 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
1316 1316 if lock is None:
1317 1317 raise error.LockInheritanceContractViolation(
1318 1318 'lock can only be inherited while held')
1319 1319 if environ is None:
1320 1320 environ = {}
1321 1321 with lock.inherit() as locker:
1322 1322 environ[envvar] = locker
1323 1323 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
1324 1324
1325 1325 def wlocksub(repo, cmd, *args, **kwargs):
1326 1326 """run cmd as a subprocess that allows inheriting repo's wlock
1327 1327
1328 1328 This can only be called while the wlock is held. This takes all the
1329 1329 arguments that ui.system does, and returns the exit code of the
1330 1330 subprocess."""
1331 1331 return _locksub(repo, repo.currentwlock(), 'HG_WLOCK_LOCKER', cmd, *args,
1332 1332 **kwargs)
1333 1333
1334 1334 class progress(object):
1335 1335 def __init__(self, ui, topic, unit="", total=None):
1336 1336 self.ui = ui
1337 1337 self.pos = 0
1338 1338 self.topic = topic
1339 1339 self.unit = unit
1340 1340 self.total = total
1341 1341
1342 1342 def __enter__(self):
1343 1343 return self
1344 1344
1345 1345 def __exit__(self, exc_type, exc_value, exc_tb):
1346 1346 self.complete()
1347 1347
1348 1348 def update(self, pos, item="", total=None):
1349 1349 assert pos is not None
1350 1350 if total:
1351 1351 self.total = total
1352 1352 self.pos = pos
1353 1353 self._print(item)
1354 1354
1355 1355 def increment(self, step=1, item="", total=None):
1356 1356 self.update(self.pos + step, item, total)
1357 1357
1358 1358 def complete(self):
1359 1359 self.ui.progress(self.topic, None)
1360 1360
1361 1361 def _print(self, item):
1362 1362 self.ui.progress(self.topic, self.pos, item, self.unit,
1363 1363 self.total)
1364 1364
1365 1365 def gdinitconfig(ui):
1366 1366 """helper function to know if a repo should be created as general delta
1367 1367 """
1368 1368 # experimental config: format.generaldelta
1369 1369 return (ui.configbool('format', 'generaldelta')
1370 1370 or ui.configbool('format', 'usegeneraldelta')
1371 1371 or ui.configbool('format', 'sparse-revlog'))
1372 1372
1373 1373 def gddeltaconfig(ui):
1374 1374 """helper function to know if incoming delta should be optimised
1375 1375 """
1376 1376 # experimental config: format.generaldelta
1377 1377 return ui.configbool('format', 'generaldelta')
1378 1378
1379 1379 class simplekeyvaluefile(object):
1380 1380 """A simple file with key=value lines
1381 1381
1382 1382 Keys must be alphanumerics and start with a letter, values must not
1383 1383 contain '\n' characters"""
1384 1384 firstlinekey = '__firstline'
1385 1385
1386 1386 def __init__(self, vfs, path, keys=None):
1387 1387 self.vfs = vfs
1388 1388 self.path = path
1389 1389
1390 1390 def read(self, firstlinenonkeyval=False):
1391 1391 """Read the contents of a simple key-value file
1392 1392
1393 1393 'firstlinenonkeyval' indicates whether the first line of file should
1394 1394 be treated as a key-value pair or reuturned fully under the
1395 1395 __firstline key."""
1396 1396 lines = self.vfs.readlines(self.path)
1397 1397 d = {}
1398 1398 if firstlinenonkeyval:
1399 1399 if not lines:
1400 1400 e = _("empty simplekeyvalue file")
1401 1401 raise error.CorruptedState(e)
1402 1402 # we don't want to include '\n' in the __firstline
1403 1403 d[self.firstlinekey] = lines[0][:-1]
1404 1404 del lines[0]
1405 1405
1406 1406 try:
1407 1407 # the 'if line.strip()' part prevents us from failing on empty
1408 1408 # lines which only contain '\n' therefore are not skipped
1409 1409 # by 'if line'
1410 1410 updatedict = dict(line[:-1].split('=', 1) for line in lines
1411 1411 if line.strip())
1412 1412 if self.firstlinekey in updatedict:
1413 1413 e = _("%r can't be used as a key")
1414 1414 raise error.CorruptedState(e % self.firstlinekey)
1415 1415 d.update(updatedict)
1416 1416 except ValueError as e:
1417 1417 raise error.CorruptedState(str(e))
1418 1418 return d
1419 1419
1420 1420 def write(self, data, firstline=None):
1421 1421 """Write key=>value mapping to a file
1422 1422 data is a dict. Keys must be alphanumerical and start with a letter.
1423 1423 Values must not contain newline characters.
1424 1424
1425 1425 If 'firstline' is not None, it is written to file before
1426 1426 everything else, as it is, not in a key=value form"""
1427 1427 lines = []
1428 1428 if firstline is not None:
1429 1429 lines.append('%s\n' % firstline)
1430 1430
1431 1431 for k, v in data.items():
1432 1432 if k == self.firstlinekey:
1433 1433 e = "key name '%s' is reserved" % self.firstlinekey
1434 1434 raise error.ProgrammingError(e)
1435 1435 if not k[0:1].isalpha():
1436 1436 e = "keys must start with a letter in a key-value file"
1437 1437 raise error.ProgrammingError(e)
1438 1438 if not k.isalnum():
1439 1439 e = "invalid key name in a simple key-value file"
1440 1440 raise error.ProgrammingError(e)
1441 1441 if '\n' in v:
1442 1442 e = "invalid value in a simple key-value file"
1443 1443 raise error.ProgrammingError(e)
1444 1444 lines.append("%s=%s\n" % (k, v))
1445 1445 with self.vfs(self.path, mode='wb', atomictemp=True) as fp:
1446 1446 fp.write(''.join(lines))
1447 1447
1448 1448 _reportobsoletedsource = [
1449 1449 'debugobsolete',
1450 1450 'pull',
1451 1451 'push',
1452 1452 'serve',
1453 1453 'unbundle',
1454 1454 ]
1455 1455
1456 1456 _reportnewcssource = [
1457 1457 'pull',
1458 1458 'unbundle',
1459 1459 ]
1460 1460
1461 1461 def prefetchfiles(repo, revs, match):
1462 1462 """Invokes the registered file prefetch functions, allowing extensions to
1463 1463 ensure the corresponding files are available locally, before the command
1464 1464 uses them."""
1465 1465 if match:
1466 1466 # The command itself will complain about files that don't exist, so
1467 1467 # don't duplicate the message.
1468 1468 match = matchmod.badmatch(match, lambda fn, msg: None)
1469 1469 else:
1470 1470 match = matchall(repo)
1471 1471
1472 1472 fileprefetchhooks(repo, revs, match)
1473 1473
1474 1474 # a list of (repo, revs, match) prefetch functions
1475 1475 fileprefetchhooks = util.hooks()
1476 1476
1477 1477 # A marker that tells the evolve extension to suppress its own reporting
1478 1478 _reportstroubledchangesets = True
1479 1479
1480 1480 def registersummarycallback(repo, otr, txnname=''):
1481 1481 """register a callback to issue a summary after the transaction is closed
1482 1482 """
1483 1483 def txmatch(sources):
1484 1484 return any(txnname.startswith(source) for source in sources)
1485 1485
1486 1486 categories = []
1487 1487
1488 1488 def reportsummary(func):
1489 1489 """decorator for report callbacks."""
1490 1490 # The repoview life cycle is shorter than the one of the actual
1491 1491 # underlying repository. So the filtered object can die before the
1492 1492 # weakref is used leading to troubles. We keep a reference to the
1493 1493 # unfiltered object and restore the filtering when retrieving the
1494 1494 # repository through the weakref.
1495 1495 filtername = repo.filtername
1496 1496 reporef = weakref.ref(repo.unfiltered())
1497 1497 def wrapped(tr):
1498 1498 repo = reporef()
1499 1499 if filtername:
1500 1500 repo = repo.filtered(filtername)
1501 1501 func(repo, tr)
1502 1502 newcat = '%02i-txnreport' % len(categories)
1503 1503 otr.addpostclose(newcat, wrapped)
1504 1504 categories.append(newcat)
1505 1505 return wrapped
1506 1506
1507 1507 if txmatch(_reportobsoletedsource):
1508 1508 @reportsummary
1509 1509 def reportobsoleted(repo, tr):
1510 1510 obsoleted = obsutil.getobsoleted(repo, tr)
1511 1511 if obsoleted:
1512 1512 repo.ui.status(_('obsoleted %i changesets\n')
1513 1513 % len(obsoleted))
1514 1514
1515 1515 if (obsolete.isenabled(repo, obsolete.createmarkersopt) and
1516 1516 repo.ui.configbool('experimental', 'evolution.report-instabilities')):
1517 1517 instabilitytypes = [
1518 1518 ('orphan', 'orphan'),
1519 1519 ('phase-divergent', 'phasedivergent'),
1520 1520 ('content-divergent', 'contentdivergent'),
1521 1521 ]
1522 1522
1523 1523 def getinstabilitycounts(repo):
1524 1524 filtered = repo.changelog.filteredrevs
1525 1525 counts = {}
1526 1526 for instability, revset in instabilitytypes:
1527 1527 counts[instability] = len(set(obsolete.getrevs(repo, revset)) -
1528 1528 filtered)
1529 1529 return counts
1530 1530
1531 1531 oldinstabilitycounts = getinstabilitycounts(repo)
1532 1532 @reportsummary
1533 1533 def reportnewinstabilities(repo, tr):
1534 1534 newinstabilitycounts = getinstabilitycounts(repo)
1535 1535 for instability, revset in instabilitytypes:
1536 1536 delta = (newinstabilitycounts[instability] -
1537 1537 oldinstabilitycounts[instability])
1538 1538 msg = getinstabilitymessage(delta, instability)
1539 1539 if msg:
1540 1540 repo.ui.warn(msg)
1541 1541
1542 1542 if txmatch(_reportnewcssource):
1543 1543 @reportsummary
1544 1544 def reportnewcs(repo, tr):
1545 1545 """Report the range of new revisions pulled/unbundled."""
1546 1546 newrevs = tr.changes.get('revs', pycompat.xrange(0, 0))
1547 1547 if not newrevs:
1548 1548 return
1549 1549
1550 1550 # Compute the bounds of new revisions' range, excluding obsoletes.
1551 1551 unfi = repo.unfiltered()
1552 1552 revs = unfi.revs('%ld and not obsolete()', newrevs)
1553 1553 if not revs:
1554 1554 # Got only obsoletes.
1555 1555 return
1556 1556 minrev, maxrev = repo[revs.min()], repo[revs.max()]
1557 1557
1558 1558 if minrev == maxrev:
1559 1559 revrange = minrev
1560 1560 else:
1561 1561 revrange = '%s:%s' % (minrev, maxrev)
1562 1562 repo.ui.status(_('new changesets %s\n') % revrange)
1563 1563
1564 1564 @reportsummary
1565 1565 def reportphasechanges(repo, tr):
1566 1566 """Report statistics of phase changes for changesets pre-existing
1567 1567 pull/unbundle.
1568 1568 """
1569 1569 newrevs = tr.changes.get('revs', pycompat.xrange(0, 0))
1570 1570 phasetracking = tr.changes.get('phases', {})
1571 1571 if not phasetracking:
1572 1572 return
1573 1573 published = [
1574 1574 rev for rev, (old, new) in phasetracking.iteritems()
1575 1575 if new == phases.public and rev not in newrevs
1576 1576 ]
1577 1577 if not published:
1578 1578 return
1579 1579 repo.ui.status(_('%d local changesets published\n')
1580 1580 % len(published))
1581 1581
1582 1582 def getinstabilitymessage(delta, instability):
1583 1583 """function to return the message to show warning about new instabilities
1584 1584
1585 1585 exists as a separate function so that extension can wrap to show more
1586 1586 information like how to fix instabilities"""
1587 1587 if delta > 0:
1588 1588 return _('%i new %s changesets\n') % (delta, instability)
1589 1589
1590 1590 def nodesummaries(repo, nodes, maxnumnodes=4):
1591 1591 if len(nodes) <= maxnumnodes or repo.ui.verbose:
1592 1592 return ' '.join(short(h) for h in nodes)
1593 1593 first = ' '.join(short(h) for h in nodes[:maxnumnodes])
1594 1594 return _("%s and %d others") % (first, len(nodes) - maxnumnodes)
1595 1595
1596 1596 def enforcesinglehead(repo, tr, desc):
1597 1597 """check that no named branch has multiple heads"""
1598 1598 if desc in ('strip', 'repair'):
1599 1599 # skip the logic during strip
1600 1600 return
1601 1601 visible = repo.filtered('visible')
1602 1602 # possible improvement: we could restrict the check to affected branch
1603 1603 for name, heads in visible.branchmap().iteritems():
1604 1604 if len(heads) > 1:
1605 1605 msg = _('rejecting multiple heads on branch "%s"')
1606 1606 msg %= name
1607 1607 hint = _('%d heads: %s')
1608 1608 hint %= (len(heads), nodesummaries(repo, heads))
1609 1609 raise error.Abort(msg, hint=hint)
1610 1610
1611 1611 def wrapconvertsink(sink):
1612 1612 """Allow extensions to wrap the sink returned by convcmd.convertsink()
1613 1613 before it is used, whether or not the convert extension was formally loaded.
1614 1614 """
1615 1615 return sink
1616 1616
1617 1617 def unhidehashlikerevs(repo, specs, hiddentype):
1618 1618 """parse the user specs and unhide changesets whose hash or revision number
1619 1619 is passed.
1620 1620
1621 1621 hiddentype can be: 1) 'warn': warn while unhiding changesets
1622 1622 2) 'nowarn': don't warn while unhiding changesets
1623 1623
1624 1624 returns a repo object with the required changesets unhidden
1625 1625 """
1626 1626 if not repo.filtername or not repo.ui.configbool('experimental',
1627 1627 'directaccess'):
1628 1628 return repo
1629 1629
1630 1630 if repo.filtername not in ('visible', 'visible-hidden'):
1631 1631 return repo
1632 1632
1633 1633 symbols = set()
1634 1634 for spec in specs:
1635 1635 try:
1636 1636 tree = revsetlang.parse(spec)
1637 1637 except error.ParseError: # will be reported by scmutil.revrange()
1638 1638 continue
1639 1639
1640 1640 symbols.update(revsetlang.gethashlikesymbols(tree))
1641 1641
1642 1642 if not symbols:
1643 1643 return repo
1644 1644
1645 1645 revs = _getrevsfromsymbols(repo, symbols)
1646 1646
1647 1647 if not revs:
1648 1648 return repo
1649 1649
1650 1650 if hiddentype == 'warn':
1651 1651 unfi = repo.unfiltered()
1652 1652 revstr = ", ".join([pycompat.bytestr(unfi[l]) for l in revs])
1653 1653 repo.ui.warn(_("warning: accessing hidden changesets for write "
1654 1654 "operation: %s\n") % revstr)
1655 1655
1656 1656 # we have to use new filtername to separate branch/tags cache until we can
1657 1657 # disbale these cache when revisions are dynamically pinned.
1658 1658 return repo.filtered('visible-hidden', revs)
1659 1659
1660 1660 def _getrevsfromsymbols(repo, symbols):
1661 1661 """parse the list of symbols and returns a set of revision numbers of hidden
1662 1662 changesets present in symbols"""
1663 1663 revs = set()
1664 1664 unfi = repo.unfiltered()
1665 1665 unficl = unfi.changelog
1666 1666 cl = repo.changelog
1667 1667 tiprev = len(unficl)
1668 1668 allowrevnums = repo.ui.configbool('experimental', 'directaccess.revnums')
1669 1669 for s in symbols:
1670 1670 try:
1671 1671 n = int(s)
1672 1672 if n <= tiprev:
1673 1673 if not allowrevnums:
1674 1674 continue
1675 1675 else:
1676 1676 if n not in cl:
1677 1677 revs.add(n)
1678 1678 continue
1679 1679 except ValueError:
1680 1680 pass
1681 1681
1682 1682 try:
1683 1683 s = resolvehexnodeidprefix(unfi, s)
1684 1684 except (error.LookupError, error.WdirUnsupported):
1685 1685 s = None
1686 1686
1687 1687 if s is not None:
1688 1688 rev = unficl.rev(s)
1689 1689 if rev not in cl:
1690 1690 revs.add(rev)
1691 1691
1692 1692 return revs
1693 1693
1694 1694 def bookmarkrevs(repo, mark):
1695 1695 """
1696 1696 Select revisions reachable by a given bookmark
1697 1697 """
1698 1698 return repo.revs("ancestors(bookmark(%s)) - "
1699 1699 "ancestors(head() and not bookmark(%s)) - "
1700 1700 "ancestors(bookmark() and not bookmark(%s))",
1701 1701 mark, mark, mark)
General Comments 0
You need to be logged in to leave comments. Login now