##// END OF EJS Templates
transaction: abstract away the detection of an abandoned transaction...
Raphaël Gomès -
r51881:cf47b83d default
parent child Browse files
Show More
@@ -1,966 +1,971 b''
1 # transaction.py - simple journaling scheme for mercurial
1 # transaction.py - simple journaling scheme for mercurial
2 #
2 #
3 # This transaction scheme is intended to gracefully handle program
3 # This transaction scheme is intended to gracefully handle program
4 # errors and interruptions. More serious failures like system crashes
4 # errors and interruptions. More serious failures like system crashes
5 # can be recovered with an fsck-like tool. As the whole repository is
5 # can be recovered with an fsck-like tool. As the whole repository is
6 # effectively log-structured, this should amount to simply truncating
6 # effectively log-structured, this should amount to simply truncating
7 # anything that isn't referenced in the changelog.
7 # anything that isn't referenced in the changelog.
8 #
8 #
9 # Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
9 # Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
10 #
10 #
11 # This software may be used and distributed according to the terms of the
11 # This software may be used and distributed according to the terms of the
12 # GNU General Public License version 2 or any later version.
12 # GNU General Public License version 2 or any later version.
13
13
14 import errno
14 import errno
15 import os
15 import os
16
16
17 from .i18n import _
17 from .i18n import _
18 from . import (
18 from . import (
19 encoding,
19 encoding,
20 error,
20 error,
21 pycompat,
21 pycompat,
22 util,
22 util,
23 )
23 )
24 from .utils import stringutil
24 from .utils import stringutil
25
25
26 version = 2
26 version = 2
27
27
28 GEN_GROUP_ALL = b'all'
28 GEN_GROUP_ALL = b'all'
29 GEN_GROUP_PRE_FINALIZE = b'prefinalize'
29 GEN_GROUP_PRE_FINALIZE = b'prefinalize'
30 GEN_GROUP_POST_FINALIZE = b'postfinalize'
30 GEN_GROUP_POST_FINALIZE = b'postfinalize'
31
31
32
32
33 def active(func):
33 def active(func):
34 def _active(self, *args, **kwds):
34 def _active(self, *args, **kwds):
35 if self._count == 0:
35 if self._count == 0:
36 raise error.ProgrammingError(
36 raise error.ProgrammingError(
37 b'cannot use transaction when it is already committed/aborted'
37 b'cannot use transaction when it is already committed/aborted'
38 )
38 )
39 return func(self, *args, **kwds)
39 return func(self, *args, **kwds)
40
40
41 return _active
41 return _active
42
42
43
43
44 UNDO_BACKUP = b'%s.backupfiles'
44 UNDO_BACKUP = b'%s.backupfiles'
45
45
46 UNDO_FILES_MAY_NEED_CLEANUP = [
46 UNDO_FILES_MAY_NEED_CLEANUP = [
47 # legacy entries that might exists on disk from previous version:
47 # legacy entries that might exists on disk from previous version:
48 (b'store', b'%s.narrowspec'),
48 (b'store', b'%s.narrowspec'),
49 (b'plain', b'%s.narrowspec.dirstate'),
49 (b'plain', b'%s.narrowspec.dirstate'),
50 (b'plain', b'%s.branch'),
50 (b'plain', b'%s.branch'),
51 (b'plain', b'%s.bookmarks'),
51 (b'plain', b'%s.bookmarks'),
52 (b'store', b'%s.phaseroots'),
52 (b'store', b'%s.phaseroots'),
53 (b'plain', b'%s.dirstate'),
53 (b'plain', b'%s.dirstate'),
54 # files actually in uses today:
54 # files actually in uses today:
55 (b'plain', b'%s.desc'),
55 (b'plain', b'%s.desc'),
56 # Always delete undo last to make sure we detect that a clean up is needed if
56 # Always delete undo last to make sure we detect that a clean up is needed if
57 # the process is interrupted.
57 # the process is interrupted.
58 (b'store', b'%s'),
58 (b'store', b'%s'),
59 ]
59 ]
60
60
61
61
62 def has_abandoned_transaction(repo):
63 """Return True if the repo has an abandoned transaction"""
64 return os.path.exists(repo.sjoin(b"journal"))
65
66
62 def cleanup_undo_files(report, vfsmap, undo_prefix=b'undo'):
67 def cleanup_undo_files(report, vfsmap, undo_prefix=b'undo'):
63 """remove "undo" files used by the rollback logic
68 """remove "undo" files used by the rollback logic
64
69
65 This is useful to prevent rollback running in situation were it does not
70 This is useful to prevent rollback running in situation were it does not
66 make sense. For example after a strip.
71 make sense. For example after a strip.
67 """
72 """
68 backup_listing = UNDO_BACKUP % undo_prefix
73 backup_listing = UNDO_BACKUP % undo_prefix
69
74
70 backup_entries = []
75 backup_entries = []
71 undo_files = []
76 undo_files = []
72 svfs = vfsmap[b'store']
77 svfs = vfsmap[b'store']
73 try:
78 try:
74 with svfs(backup_listing) as f:
79 with svfs(backup_listing) as f:
75 backup_entries = read_backup_files(report, f)
80 backup_entries = read_backup_files(report, f)
76 except OSError as e:
81 except OSError as e:
77 if e.errno != errno.ENOENT:
82 if e.errno != errno.ENOENT:
78 msg = _(b'could not read %s: %s\n')
83 msg = _(b'could not read %s: %s\n')
79 msg %= (svfs.join(backup_listing), stringutil.forcebytestr(e))
84 msg %= (svfs.join(backup_listing), stringutil.forcebytestr(e))
80 report(msg)
85 report(msg)
81
86
82 for location, f, backup_path, c in backup_entries:
87 for location, f, backup_path, c in backup_entries:
83 if location in vfsmap and backup_path:
88 if location in vfsmap and backup_path:
84 undo_files.append((vfsmap[location], backup_path))
89 undo_files.append((vfsmap[location], backup_path))
85
90
86 undo_files.append((svfs, backup_listing))
91 undo_files.append((svfs, backup_listing))
87 for location, undo_path in UNDO_FILES_MAY_NEED_CLEANUP:
92 for location, undo_path in UNDO_FILES_MAY_NEED_CLEANUP:
88 undo_files.append((vfsmap[location], undo_path % undo_prefix))
93 undo_files.append((vfsmap[location], undo_path % undo_prefix))
89 for undovfs, undofile in undo_files:
94 for undovfs, undofile in undo_files:
90 try:
95 try:
91 undovfs.unlink(undofile)
96 undovfs.unlink(undofile)
92 except OSError as e:
97 except OSError as e:
93 if e.errno != errno.ENOENT:
98 if e.errno != errno.ENOENT:
94 msg = _(b'error removing %s: %s\n')
99 msg = _(b'error removing %s: %s\n')
95 msg %= (undovfs.join(undofile), stringutil.forcebytestr(e))
100 msg %= (undovfs.join(undofile), stringutil.forcebytestr(e))
96 report(msg)
101 report(msg)
97
102
98
103
99 def _playback(
104 def _playback(
100 journal,
105 journal,
101 report,
106 report,
102 opener,
107 opener,
103 vfsmap,
108 vfsmap,
104 entries,
109 entries,
105 backupentries,
110 backupentries,
106 unlink=True,
111 unlink=True,
107 checkambigfiles=None,
112 checkambigfiles=None,
108 ):
113 ):
109 """rollback a transaction :
114 """rollback a transaction :
110 - truncate files that have been appended to
115 - truncate files that have been appended to
111 - restore file backups
116 - restore file backups
112 - delete temporary files
117 - delete temporary files
113 """
118 """
114 backupfiles = []
119 backupfiles = []
115
120
116 def restore_one_backup(vfs, f, b, checkambig):
121 def restore_one_backup(vfs, f, b, checkambig):
117 filepath = vfs.join(f)
122 filepath = vfs.join(f)
118 backuppath = vfs.join(b)
123 backuppath = vfs.join(b)
119 try:
124 try:
120 util.copyfile(backuppath, filepath, checkambig=checkambig)
125 util.copyfile(backuppath, filepath, checkambig=checkambig)
121 backupfiles.append((vfs, b))
126 backupfiles.append((vfs, b))
122 except IOError as exc:
127 except IOError as exc:
123 e_msg = stringutil.forcebytestr(exc)
128 e_msg = stringutil.forcebytestr(exc)
124 report(_(b"failed to recover %s (%s)\n") % (f, e_msg))
129 report(_(b"failed to recover %s (%s)\n") % (f, e_msg))
125 raise
130 raise
126
131
127 # gather all backup files that impact the store
132 # gather all backup files that impact the store
128 # (we need this to detect files that are both backed up and truncated)
133 # (we need this to detect files that are both backed up and truncated)
129 store_backup = {}
134 store_backup = {}
130 for entry in backupentries:
135 for entry in backupentries:
131 location, file_path, backup_path, cache = entry
136 location, file_path, backup_path, cache = entry
132 vfs = vfsmap[location]
137 vfs = vfsmap[location]
133 is_store = vfs.join(b'') == opener.join(b'')
138 is_store = vfs.join(b'') == opener.join(b'')
134 if is_store and file_path and backup_path:
139 if is_store and file_path and backup_path:
135 store_backup[file_path] = entry
140 store_backup[file_path] = entry
136 copy_done = set()
141 copy_done = set()
137
142
138 # truncate all file `f` to offset `o`
143 # truncate all file `f` to offset `o`
139 for f, o in sorted(dict(entries).items()):
144 for f, o in sorted(dict(entries).items()):
140 # if we have a backup for `f`, we should restore it first and truncate
145 # if we have a backup for `f`, we should restore it first and truncate
141 # the restored file
146 # the restored file
142 bck_entry = store_backup.get(f)
147 bck_entry = store_backup.get(f)
143 if bck_entry is not None:
148 if bck_entry is not None:
144 location, file_path, backup_path, cache = bck_entry
149 location, file_path, backup_path, cache = bck_entry
145 checkambig = False
150 checkambig = False
146 if checkambigfiles:
151 if checkambigfiles:
147 checkambig = (file_path, location) in checkambigfiles
152 checkambig = (file_path, location) in checkambigfiles
148 restore_one_backup(opener, file_path, backup_path, checkambig)
153 restore_one_backup(opener, file_path, backup_path, checkambig)
149 copy_done.add(bck_entry)
154 copy_done.add(bck_entry)
150 # truncate the file to its pre-transaction size
155 # truncate the file to its pre-transaction size
151 if o or not unlink:
156 if o or not unlink:
152 checkambig = checkambigfiles and (f, b'') in checkambigfiles
157 checkambig = checkambigfiles and (f, b'') in checkambigfiles
153 try:
158 try:
154 fp = opener(f, b'a', checkambig=checkambig)
159 fp = opener(f, b'a', checkambig=checkambig)
155 if fp.tell() < o:
160 if fp.tell() < o:
156 raise error.Abort(
161 raise error.Abort(
157 _(
162 _(
158 b"attempted to truncate %s to %d bytes, but it was "
163 b"attempted to truncate %s to %d bytes, but it was "
159 b"already %d bytes\n"
164 b"already %d bytes\n"
160 )
165 )
161 % (f, o, fp.tell())
166 % (f, o, fp.tell())
162 )
167 )
163 fp.truncate(o)
168 fp.truncate(o)
164 fp.close()
169 fp.close()
165 except IOError:
170 except IOError:
166 report(_(b"failed to truncate %s\n") % f)
171 report(_(b"failed to truncate %s\n") % f)
167 raise
172 raise
168 else:
173 else:
169 # delete empty file
174 # delete empty file
170 try:
175 try:
171 opener.unlink(f)
176 opener.unlink(f)
172 except FileNotFoundError:
177 except FileNotFoundError:
173 pass
178 pass
174 # restore backed up files and clean up temporary files
179 # restore backed up files and clean up temporary files
175 for entry in backupentries:
180 for entry in backupentries:
176 if entry in copy_done:
181 if entry in copy_done:
177 continue
182 continue
178 l, f, b, c = entry
183 l, f, b, c = entry
179 if l not in vfsmap and c:
184 if l not in vfsmap and c:
180 report(b"couldn't handle %s: unknown cache location %s\n" % (b, l))
185 report(b"couldn't handle %s: unknown cache location %s\n" % (b, l))
181 vfs = vfsmap[l]
186 vfs = vfsmap[l]
182 try:
187 try:
183 checkambig = checkambigfiles and (f, l) in checkambigfiles
188 checkambig = checkambigfiles and (f, l) in checkambigfiles
184 if f and b:
189 if f and b:
185 restore_one_backup(vfs, f, b, checkambig)
190 restore_one_backup(vfs, f, b, checkambig)
186 else:
191 else:
187 target = f or b
192 target = f or b
188 try:
193 try:
189 vfs.unlink(target)
194 vfs.unlink(target)
190 except FileNotFoundError:
195 except FileNotFoundError:
191 # This is fine because
196 # This is fine because
192 #
197 #
193 # either we are trying to delete the main file, and it is
198 # either we are trying to delete the main file, and it is
194 # already deleted.
199 # already deleted.
195 #
200 #
196 # or we are trying to delete a temporary file and it is
201 # or we are trying to delete a temporary file and it is
197 # already deleted.
202 # already deleted.
198 #
203 #
199 # in both case, our target result (delete the file) is
204 # in both case, our target result (delete the file) is
200 # already achieved.
205 # already achieved.
201 pass
206 pass
202 except (IOError, OSError, error.Abort):
207 except (IOError, OSError, error.Abort):
203 if not c:
208 if not c:
204 raise
209 raise
205
210
206 # cleanup transaction state file and the backups file
211 # cleanup transaction state file and the backups file
207 backuppath = b"%s.backupfiles" % journal
212 backuppath = b"%s.backupfiles" % journal
208 if opener.exists(backuppath):
213 if opener.exists(backuppath):
209 opener.unlink(backuppath)
214 opener.unlink(backuppath)
210 opener.unlink(journal)
215 opener.unlink(journal)
211 try:
216 try:
212 for vfs, f in backupfiles:
217 for vfs, f in backupfiles:
213 if vfs.exists(f):
218 if vfs.exists(f):
214 vfs.unlink(f)
219 vfs.unlink(f)
215 except (IOError, OSError, error.Abort):
220 except (IOError, OSError, error.Abort):
216 # only pure backup file remains, it is sage to ignore any error
221 # only pure backup file remains, it is sage to ignore any error
217 pass
222 pass
218
223
219
224
220 class transaction(util.transactional):
225 class transaction(util.transactional):
221 def __init__(
226 def __init__(
222 self,
227 self,
223 report,
228 report,
224 opener,
229 opener,
225 vfsmap,
230 vfsmap,
226 journalname,
231 journalname,
227 undoname=None,
232 undoname=None,
228 after=None,
233 after=None,
229 createmode=None,
234 createmode=None,
230 validator=None,
235 validator=None,
231 releasefn=None,
236 releasefn=None,
232 checkambigfiles=None,
237 checkambigfiles=None,
233 name=b'<unnamed>',
238 name=b'<unnamed>',
234 ):
239 ):
235 """Begin a new transaction
240 """Begin a new transaction
236
241
237 Begins a new transaction that allows rolling back writes in the event of
242 Begins a new transaction that allows rolling back writes in the event of
238 an exception.
243 an exception.
239
244
240 * `after`: called after the transaction has been committed
245 * `after`: called after the transaction has been committed
241 * `createmode`: the mode of the journal file that will be created
246 * `createmode`: the mode of the journal file that will be created
242 * `releasefn`: called after releasing (with transaction and result)
247 * `releasefn`: called after releasing (with transaction and result)
243
248
244 `checkambigfiles` is a set of (path, vfs-location) tuples,
249 `checkambigfiles` is a set of (path, vfs-location) tuples,
245 which determine whether file stat ambiguity should be avoided
250 which determine whether file stat ambiguity should be avoided
246 for corresponded files.
251 for corresponded files.
247 """
252 """
248 self._count = 1
253 self._count = 1
249 self._usages = 1
254 self._usages = 1
250 self._report = report
255 self._report = report
251 # a vfs to the store content
256 # a vfs to the store content
252 self._opener = opener
257 self._opener = opener
253 # a map to access file in various {location -> vfs}
258 # a map to access file in various {location -> vfs}
254 vfsmap = vfsmap.copy()
259 vfsmap = vfsmap.copy()
255 vfsmap[b''] = opener # set default value
260 vfsmap[b''] = opener # set default value
256 self._vfsmap = vfsmap
261 self._vfsmap = vfsmap
257 self._after = after
262 self._after = after
258 self._offsetmap = {}
263 self._offsetmap = {}
259 self._newfiles = set()
264 self._newfiles = set()
260 self._journal = journalname
265 self._journal = journalname
261 self._journal_files = []
266 self._journal_files = []
262 self._undoname = undoname
267 self._undoname = undoname
263 self._queue = []
268 self._queue = []
264 # A callback to do something just after releasing transaction.
269 # A callback to do something just after releasing transaction.
265 if releasefn is None:
270 if releasefn is None:
266 releasefn = lambda tr, success: None
271 releasefn = lambda tr, success: None
267 self._releasefn = releasefn
272 self._releasefn = releasefn
268
273
269 self._checkambigfiles = set()
274 self._checkambigfiles = set()
270 if checkambigfiles:
275 if checkambigfiles:
271 self._checkambigfiles.update(checkambigfiles)
276 self._checkambigfiles.update(checkambigfiles)
272
277
273 self._names = [name]
278 self._names = [name]
274
279
275 # A dict dedicated to precisely tracking the changes introduced in the
280 # A dict dedicated to precisely tracking the changes introduced in the
276 # transaction.
281 # transaction.
277 self.changes = {}
282 self.changes = {}
278
283
279 # a dict of arguments to be passed to hooks
284 # a dict of arguments to be passed to hooks
280 self.hookargs = {}
285 self.hookargs = {}
281 self._file = opener.open(self._journal, b"w+")
286 self._file = opener.open(self._journal, b"w+")
282
287
283 # a list of ('location', 'path', 'backuppath', cache) entries.
288 # a list of ('location', 'path', 'backuppath', cache) entries.
284 # - if 'backuppath' is empty, no file existed at backup time
289 # - if 'backuppath' is empty, no file existed at backup time
285 # - if 'path' is empty, this is a temporary transaction file
290 # - if 'path' is empty, this is a temporary transaction file
286 # - if 'location' is not empty, the path is outside main opener reach.
291 # - if 'location' is not empty, the path is outside main opener reach.
287 # use 'location' value as a key in a vfsmap to find the right 'vfs'
292 # use 'location' value as a key in a vfsmap to find the right 'vfs'
288 # (cache is currently unused)
293 # (cache is currently unused)
289 self._backupentries = []
294 self._backupentries = []
290 self._backupmap = {}
295 self._backupmap = {}
291 self._backupjournal = b"%s.backupfiles" % self._journal
296 self._backupjournal = b"%s.backupfiles" % self._journal
292 self._backupsfile = opener.open(self._backupjournal, b'w')
297 self._backupsfile = opener.open(self._backupjournal, b'w')
293 self._backupsfile.write(b'%d\n' % version)
298 self._backupsfile.write(b'%d\n' % version)
294 # the set of temporary files
299 # the set of temporary files
295 self._tmp_files = set()
300 self._tmp_files = set()
296
301
297 if createmode is not None:
302 if createmode is not None:
298 opener.chmod(self._journal, createmode & 0o666)
303 opener.chmod(self._journal, createmode & 0o666)
299 opener.chmod(self._backupjournal, createmode & 0o666)
304 opener.chmod(self._backupjournal, createmode & 0o666)
300
305
301 # hold file generations to be performed on commit
306 # hold file generations to be performed on commit
302 self._filegenerators = {}
307 self._filegenerators = {}
303 # hold callback to write pending data for hooks
308 # hold callback to write pending data for hooks
304 self._pendingcallback = {}
309 self._pendingcallback = {}
305 # True is any pending data have been written ever
310 # True is any pending data have been written ever
306 self._anypending = False
311 self._anypending = False
307 # holds callback to call when writing the transaction
312 # holds callback to call when writing the transaction
308 self._finalizecallback = {}
313 self._finalizecallback = {}
309 # holds callback to call when validating the transaction
314 # holds callback to call when validating the transaction
310 # should raise exception if anything is wrong
315 # should raise exception if anything is wrong
311 self._validatecallback = {}
316 self._validatecallback = {}
312 if validator is not None:
317 if validator is not None:
313 self._validatecallback[b'001-userhooks'] = validator
318 self._validatecallback[b'001-userhooks'] = validator
314 # hold callback for post transaction close
319 # hold callback for post transaction close
315 self._postclosecallback = {}
320 self._postclosecallback = {}
316 # holds callbacks to call during abort
321 # holds callbacks to call during abort
317 self._abortcallback = {}
322 self._abortcallback = {}
318
323
319 def __repr__(self):
324 def __repr__(self):
320 name = b'/'.join(self._names)
325 name = b'/'.join(self._names)
321 return '<transaction name=%s, count=%d, usages=%d>' % (
326 return '<transaction name=%s, count=%d, usages=%d>' % (
322 encoding.strfromlocal(name),
327 encoding.strfromlocal(name),
323 self._count,
328 self._count,
324 self._usages,
329 self._usages,
325 )
330 )
326
331
327 def __del__(self):
332 def __del__(self):
328 if self._journal:
333 if self._journal:
329 self._abort()
334 self._abort()
330
335
331 @property
336 @property
332 def finalized(self):
337 def finalized(self):
333 return self._finalizecallback is None
338 return self._finalizecallback is None
334
339
335 @active
340 @active
336 def startgroup(self):
341 def startgroup(self):
337 """delay registration of file entry
342 """delay registration of file entry
338
343
339 This is used by strip to delay vision of strip offset. The transaction
344 This is used by strip to delay vision of strip offset. The transaction
340 sees either none or all of the strip actions to be done."""
345 sees either none or all of the strip actions to be done."""
341 self._queue.append([])
346 self._queue.append([])
342
347
343 @active
348 @active
344 def endgroup(self):
349 def endgroup(self):
345 """apply delayed registration of file entry.
350 """apply delayed registration of file entry.
346
351
347 This is used by strip to delay vision of strip offset. The transaction
352 This is used by strip to delay vision of strip offset. The transaction
348 sees either none or all of the strip actions to be done."""
353 sees either none or all of the strip actions to be done."""
349 q = self._queue.pop()
354 q = self._queue.pop()
350 for f, o in q:
355 for f, o in q:
351 self._addentry(f, o)
356 self._addentry(f, o)
352
357
353 @active
358 @active
354 def add(self, file, offset):
359 def add(self, file, offset):
355 """record the state of an append-only file before update"""
360 """record the state of an append-only file before update"""
356 if (
361 if (
357 file in self._newfiles
362 file in self._newfiles
358 or file in self._offsetmap
363 or file in self._offsetmap
359 or file in self._backupmap
364 or file in self._backupmap
360 or file in self._tmp_files
365 or file in self._tmp_files
361 ):
366 ):
362 return
367 return
363 if self._queue:
368 if self._queue:
364 self._queue[-1].append((file, offset))
369 self._queue[-1].append((file, offset))
365 return
370 return
366
371
367 self._addentry(file, offset)
372 self._addentry(file, offset)
368
373
369 def _addentry(self, file, offset):
374 def _addentry(self, file, offset):
370 """add a append-only entry to memory and on-disk state"""
375 """add a append-only entry to memory and on-disk state"""
371 if (
376 if (
372 file in self._newfiles
377 file in self._newfiles
373 or file in self._offsetmap
378 or file in self._offsetmap
374 or file in self._backupmap
379 or file in self._backupmap
375 or file in self._tmp_files
380 or file in self._tmp_files
376 ):
381 ):
377 return
382 return
378 if offset:
383 if offset:
379 self._offsetmap[file] = offset
384 self._offsetmap[file] = offset
380 else:
385 else:
381 self._newfiles.add(file)
386 self._newfiles.add(file)
382 # add enough data to the journal to do the truncate
387 # add enough data to the journal to do the truncate
383 self._file.write(b"%s\0%d\n" % (file, offset))
388 self._file.write(b"%s\0%d\n" % (file, offset))
384 self._file.flush()
389 self._file.flush()
385
390
386 @active
391 @active
387 def addbackup(self, file, hardlink=True, location=b'', for_offset=False):
392 def addbackup(self, file, hardlink=True, location=b'', for_offset=False):
388 """Adds a backup of the file to the transaction
393 """Adds a backup of the file to the transaction
389
394
390 Calling addbackup() creates a hardlink backup of the specified file
395 Calling addbackup() creates a hardlink backup of the specified file
391 that is used to recover the file in the event of the transaction
396 that is used to recover the file in the event of the transaction
392 aborting.
397 aborting.
393
398
394 * `file`: the file path, relative to .hg/store
399 * `file`: the file path, relative to .hg/store
395 * `hardlink`: use a hardlink to quickly create the backup
400 * `hardlink`: use a hardlink to quickly create the backup
396
401
397 If `for_offset` is set, we expect a offset for this file to have been previously recorded
402 If `for_offset` is set, we expect a offset for this file to have been previously recorded
398 """
403 """
399 if self._queue:
404 if self._queue:
400 msg = b'cannot use transaction.addbackup inside "group"'
405 msg = b'cannot use transaction.addbackup inside "group"'
401 raise error.ProgrammingError(msg)
406 raise error.ProgrammingError(msg)
402
407
403 if file in self._newfiles or file in self._backupmap:
408 if file in self._newfiles or file in self._backupmap:
404 return
409 return
405 elif file in self._offsetmap and not for_offset:
410 elif file in self._offsetmap and not for_offset:
406 return
411 return
407 elif for_offset and file not in self._offsetmap:
412 elif for_offset and file not in self._offsetmap:
408 msg = (
413 msg = (
409 'calling `addbackup` with `for_offmap=True`, '
414 'calling `addbackup` with `for_offmap=True`, '
410 'but no offset recorded: [%r] %r'
415 'but no offset recorded: [%r] %r'
411 )
416 )
412 msg %= (location, file)
417 msg %= (location, file)
413 raise error.ProgrammingError(msg)
418 raise error.ProgrammingError(msg)
414
419
415 vfs = self._vfsmap[location]
420 vfs = self._vfsmap[location]
416 dirname, filename = vfs.split(file)
421 dirname, filename = vfs.split(file)
417 backupfilename = b"%s.backup.%s.bck" % (self._journal, filename)
422 backupfilename = b"%s.backup.%s.bck" % (self._journal, filename)
418 backupfile = vfs.reljoin(dirname, backupfilename)
423 backupfile = vfs.reljoin(dirname, backupfilename)
419 if vfs.exists(file):
424 if vfs.exists(file):
420 filepath = vfs.join(file)
425 filepath = vfs.join(file)
421 backuppath = vfs.join(backupfile)
426 backuppath = vfs.join(backupfile)
422 # store encoding may result in different directory here.
427 # store encoding may result in different directory here.
423 # so we have to ensure the destination directory exist
428 # so we have to ensure the destination directory exist
424 final_dir_name = os.path.dirname(backuppath)
429 final_dir_name = os.path.dirname(backuppath)
425 util.makedirs(final_dir_name, mode=vfs.createmode, notindexed=True)
430 util.makedirs(final_dir_name, mode=vfs.createmode, notindexed=True)
426 # then we can copy the backup
431 # then we can copy the backup
427 util.copyfile(filepath, backuppath, hardlink=hardlink)
432 util.copyfile(filepath, backuppath, hardlink=hardlink)
428 else:
433 else:
429 backupfile = b''
434 backupfile = b''
430
435
431 self._addbackupentry((location, file, backupfile, False))
436 self._addbackupentry((location, file, backupfile, False))
432
437
433 def _addbackupentry(self, entry):
438 def _addbackupentry(self, entry):
434 """register a new backup entry and write it to disk"""
439 """register a new backup entry and write it to disk"""
435 self._backupentries.append(entry)
440 self._backupentries.append(entry)
436 self._backupmap[entry[1]] = len(self._backupentries) - 1
441 self._backupmap[entry[1]] = len(self._backupentries) - 1
437 self._backupsfile.write(b"%s\0%s\0%s\0%d\n" % entry)
442 self._backupsfile.write(b"%s\0%s\0%s\0%d\n" % entry)
438 self._backupsfile.flush()
443 self._backupsfile.flush()
439
444
440 @active
445 @active
441 def registertmp(self, tmpfile, location=b''):
446 def registertmp(self, tmpfile, location=b''):
442 """register a temporary transaction file
447 """register a temporary transaction file
443
448
444 Such files will be deleted when the transaction exits (on both
449 Such files will be deleted when the transaction exits (on both
445 failure and success).
450 failure and success).
446 """
451 """
447 self._tmp_files.add(tmpfile)
452 self._tmp_files.add(tmpfile)
448 self._addbackupentry((location, b'', tmpfile, False))
453 self._addbackupentry((location, b'', tmpfile, False))
449
454
450 @active
455 @active
451 def addfilegenerator(
456 def addfilegenerator(
452 self,
457 self,
453 genid,
458 genid,
454 filenames,
459 filenames,
455 genfunc,
460 genfunc,
456 order=0,
461 order=0,
457 location=b'',
462 location=b'',
458 post_finalize=False,
463 post_finalize=False,
459 ):
464 ):
460 """add a function to generates some files at transaction commit
465 """add a function to generates some files at transaction commit
461
466
462 The `genfunc` argument is a function capable of generating proper
467 The `genfunc` argument is a function capable of generating proper
463 content of each entry in the `filename` tuple.
468 content of each entry in the `filename` tuple.
464
469
465 At transaction close time, `genfunc` will be called with one file
470 At transaction close time, `genfunc` will be called with one file
466 object argument per entries in `filenames`.
471 object argument per entries in `filenames`.
467
472
468 The transaction itself is responsible for the backup, creation and
473 The transaction itself is responsible for the backup, creation and
469 final write of such file.
474 final write of such file.
470
475
471 The `genid` argument is used to ensure the same set of file is only
476 The `genid` argument is used to ensure the same set of file is only
472 generated once. Call to `addfilegenerator` for a `genid` already
477 generated once. Call to `addfilegenerator` for a `genid` already
473 present will overwrite the old entry.
478 present will overwrite the old entry.
474
479
475 The `order` argument may be used to control the order in which multiple
480 The `order` argument may be used to control the order in which multiple
476 generator will be executed.
481 generator will be executed.
477
482
478 The `location` arguments may be used to indicate the files are located
483 The `location` arguments may be used to indicate the files are located
479 outside of the the standard directory for transaction. It should match
484 outside of the the standard directory for transaction. It should match
480 one of the key of the `transaction.vfsmap` dictionary.
485 one of the key of the `transaction.vfsmap` dictionary.
481
486
482 The `post_finalize` argument can be set to `True` for file generation
487 The `post_finalize` argument can be set to `True` for file generation
483 that must be run after the transaction has been finalized.
488 that must be run after the transaction has been finalized.
484 """
489 """
485 # For now, we are unable to do proper backup and restore of custom vfs
490 # For now, we are unable to do proper backup and restore of custom vfs
486 # but for bookmarks that are handled outside this mechanism.
491 # but for bookmarks that are handled outside this mechanism.
487 entry = (order, filenames, genfunc, location, post_finalize)
492 entry = (order, filenames, genfunc, location, post_finalize)
488 self._filegenerators[genid] = entry
493 self._filegenerators[genid] = entry
489
494
490 @active
495 @active
491 def removefilegenerator(self, genid):
496 def removefilegenerator(self, genid):
492 """reverse of addfilegenerator, remove a file generator function"""
497 """reverse of addfilegenerator, remove a file generator function"""
493 if genid in self._filegenerators:
498 if genid in self._filegenerators:
494 del self._filegenerators[genid]
499 del self._filegenerators[genid]
495
500
496 def _generatefiles(self, suffix=b'', group=GEN_GROUP_ALL):
501 def _generatefiles(self, suffix=b'', group=GEN_GROUP_ALL):
497 # write files registered for generation
502 # write files registered for generation
498 any = False
503 any = False
499
504
500 if group == GEN_GROUP_ALL:
505 if group == GEN_GROUP_ALL:
501 skip_post = skip_pre = False
506 skip_post = skip_pre = False
502 else:
507 else:
503 skip_pre = group == GEN_GROUP_POST_FINALIZE
508 skip_pre = group == GEN_GROUP_POST_FINALIZE
504 skip_post = group == GEN_GROUP_PRE_FINALIZE
509 skip_post = group == GEN_GROUP_PRE_FINALIZE
505
510
506 for id, entry in sorted(self._filegenerators.items()):
511 for id, entry in sorted(self._filegenerators.items()):
507 any = True
512 any = True
508 order, filenames, genfunc, location, post_finalize = entry
513 order, filenames, genfunc, location, post_finalize = entry
509
514
510 # for generation at closing, check if it's before or after finalize
515 # for generation at closing, check if it's before or after finalize
511 if skip_post and post_finalize:
516 if skip_post and post_finalize:
512 continue
517 continue
513 elif skip_pre and not post_finalize:
518 elif skip_pre and not post_finalize:
514 continue
519 continue
515
520
516 vfs = self._vfsmap[location]
521 vfs = self._vfsmap[location]
517 files = []
522 files = []
518 try:
523 try:
519 for name in filenames:
524 for name in filenames:
520 name += suffix
525 name += suffix
521 if suffix:
526 if suffix:
522 self.registertmp(name, location=location)
527 self.registertmp(name, location=location)
523 checkambig = False
528 checkambig = False
524 else:
529 else:
525 self.addbackup(name, location=location)
530 self.addbackup(name, location=location)
526 checkambig = (name, location) in self._checkambigfiles
531 checkambig = (name, location) in self._checkambigfiles
527 files.append(
532 files.append(
528 vfs(name, b'w', atomictemp=True, checkambig=checkambig)
533 vfs(name, b'w', atomictemp=True, checkambig=checkambig)
529 )
534 )
530 genfunc(*files)
535 genfunc(*files)
531 for f in files:
536 for f in files:
532 f.close()
537 f.close()
533 # skip discard() loop since we're sure no open file remains
538 # skip discard() loop since we're sure no open file remains
534 del files[:]
539 del files[:]
535 finally:
540 finally:
536 for f in files:
541 for f in files:
537 f.discard()
542 f.discard()
538 return any
543 return any
539
544
540 @active
545 @active
541 def findoffset(self, file):
546 def findoffset(self, file):
542 if file in self._newfiles:
547 if file in self._newfiles:
543 return 0
548 return 0
544 return self._offsetmap.get(file)
549 return self._offsetmap.get(file)
545
550
546 @active
551 @active
547 def readjournal(self):
552 def readjournal(self):
548 self._file.seek(0)
553 self._file.seek(0)
549 entries = []
554 entries = []
550 for l in self._file.readlines():
555 for l in self._file.readlines():
551 file, troffset = l.split(b'\0')
556 file, troffset = l.split(b'\0')
552 entries.append((file, int(troffset)))
557 entries.append((file, int(troffset)))
553 return entries
558 return entries
554
559
555 @active
560 @active
556 def replace(self, file, offset):
561 def replace(self, file, offset):
557 """
562 """
558 replace can only replace already committed entries
563 replace can only replace already committed entries
559 that are not pending in the queue
564 that are not pending in the queue
560 """
565 """
561 if file in self._newfiles:
566 if file in self._newfiles:
562 if not offset:
567 if not offset:
563 return
568 return
564 self._newfiles.remove(file)
569 self._newfiles.remove(file)
565 self._offsetmap[file] = offset
570 self._offsetmap[file] = offset
566 elif file in self._offsetmap:
571 elif file in self._offsetmap:
567 if not offset:
572 if not offset:
568 del self._offsetmap[file]
573 del self._offsetmap[file]
569 self._newfiles.add(file)
574 self._newfiles.add(file)
570 else:
575 else:
571 self._offsetmap[file] = offset
576 self._offsetmap[file] = offset
572 else:
577 else:
573 raise KeyError(file)
578 raise KeyError(file)
574 self._file.write(b"%s\0%d\n" % (file, offset))
579 self._file.write(b"%s\0%d\n" % (file, offset))
575 self._file.flush()
580 self._file.flush()
576
581
577 @active
582 @active
578 def nest(self, name=b'<unnamed>'):
583 def nest(self, name=b'<unnamed>'):
579 self._count += 1
584 self._count += 1
580 self._usages += 1
585 self._usages += 1
581 self._names.append(name)
586 self._names.append(name)
582 return self
587 return self
583
588
584 def release(self):
589 def release(self):
585 if self._count > 0:
590 if self._count > 0:
586 self._usages -= 1
591 self._usages -= 1
587 if self._names:
592 if self._names:
588 self._names.pop()
593 self._names.pop()
589 # if the transaction scopes are left without being closed, fail
594 # if the transaction scopes are left without being closed, fail
590 if self._count > 0 and self._usages == 0:
595 if self._count > 0 and self._usages == 0:
591 self._abort()
596 self._abort()
592
597
593 def running(self):
598 def running(self):
594 return self._count > 0
599 return self._count > 0
595
600
596 def addpending(self, category, callback):
601 def addpending(self, category, callback):
597 """add a callback to be called when the transaction is pending
602 """add a callback to be called when the transaction is pending
598
603
599 The transaction will be given as callback's first argument.
604 The transaction will be given as callback's first argument.
600
605
601 Category is a unique identifier to allow overwriting an old callback
606 Category is a unique identifier to allow overwriting an old callback
602 with a newer callback.
607 with a newer callback.
603 """
608 """
604 self._pendingcallback[category] = callback
609 self._pendingcallback[category] = callback
605
610
606 @active
611 @active
607 def writepending(self):
612 def writepending(self):
608 """write pending file to temporary version
613 """write pending file to temporary version
609
614
610 This is used to allow hooks to view a transaction before commit"""
615 This is used to allow hooks to view a transaction before commit"""
611 categories = sorted(self._pendingcallback)
616 categories = sorted(self._pendingcallback)
612 for cat in categories:
617 for cat in categories:
613 # remove callback since the data will have been flushed
618 # remove callback since the data will have been flushed
614 any = self._pendingcallback.pop(cat)(self)
619 any = self._pendingcallback.pop(cat)(self)
615 self._anypending = self._anypending or any
620 self._anypending = self._anypending or any
616 self._anypending |= self._generatefiles(suffix=b'.pending')
621 self._anypending |= self._generatefiles(suffix=b'.pending')
617 return self._anypending
622 return self._anypending
618
623
619 @active
624 @active
620 def hasfinalize(self, category):
625 def hasfinalize(self, category):
621 """check is a callback already exist for a category"""
626 """check is a callback already exist for a category"""
622 return category in self._finalizecallback
627 return category in self._finalizecallback
623
628
624 @active
629 @active
625 def addfinalize(self, category, callback):
630 def addfinalize(self, category, callback):
626 """add a callback to be called when the transaction is closed
631 """add a callback to be called when the transaction is closed
627
632
628 The transaction will be given as callback's first argument.
633 The transaction will be given as callback's first argument.
629
634
630 Category is a unique identifier to allow overwriting old callbacks with
635 Category is a unique identifier to allow overwriting old callbacks with
631 newer callbacks.
636 newer callbacks.
632 """
637 """
633 self._finalizecallback[category] = callback
638 self._finalizecallback[category] = callback
634
639
635 @active
640 @active
636 def addpostclose(self, category, callback):
641 def addpostclose(self, category, callback):
637 """add or replace a callback to be called after the transaction closed
642 """add or replace a callback to be called after the transaction closed
638
643
639 The transaction will be given as callback's first argument.
644 The transaction will be given as callback's first argument.
640
645
641 Category is a unique identifier to allow overwriting an old callback
646 Category is a unique identifier to allow overwriting an old callback
642 with a newer callback.
647 with a newer callback.
643 """
648 """
644 self._postclosecallback[category] = callback
649 self._postclosecallback[category] = callback
645
650
646 @active
651 @active
647 def getpostclose(self, category):
652 def getpostclose(self, category):
648 """return a postclose callback added before, or None"""
653 """return a postclose callback added before, or None"""
649 return self._postclosecallback.get(category, None)
654 return self._postclosecallback.get(category, None)
650
655
651 @active
656 @active
652 def addabort(self, category, callback):
657 def addabort(self, category, callback):
653 """add a callback to be called when the transaction is aborted.
658 """add a callback to be called when the transaction is aborted.
654
659
655 The transaction will be given as the first argument to the callback.
660 The transaction will be given as the first argument to the callback.
656
661
657 Category is a unique identifier to allow overwriting an old callback
662 Category is a unique identifier to allow overwriting an old callback
658 with a newer callback.
663 with a newer callback.
659 """
664 """
660 self._abortcallback[category] = callback
665 self._abortcallback[category] = callback
661
666
662 @active
667 @active
663 def addvalidator(self, category, callback):
668 def addvalidator(self, category, callback):
664 """adds a callback to be called when validating the transaction.
669 """adds a callback to be called when validating the transaction.
665
670
666 The transaction will be given as the first argument to the callback.
671 The transaction will be given as the first argument to the callback.
667
672
668 callback should raise exception if to abort transaction"""
673 callback should raise exception if to abort transaction"""
669 self._validatecallback[category] = callback
674 self._validatecallback[category] = callback
670
675
671 @active
676 @active
672 def close(self):
677 def close(self):
673 '''commit the transaction'''
678 '''commit the transaction'''
674 if self._count == 1:
679 if self._count == 1:
675 for category in sorted(self._validatecallback):
680 for category in sorted(self._validatecallback):
676 self._validatecallback[category](self)
681 self._validatecallback[category](self)
677 self._validatecallback = None # Help prevent cycles.
682 self._validatecallback = None # Help prevent cycles.
678 self._generatefiles(group=GEN_GROUP_PRE_FINALIZE)
683 self._generatefiles(group=GEN_GROUP_PRE_FINALIZE)
679 while self._finalizecallback:
684 while self._finalizecallback:
680 callbacks = self._finalizecallback
685 callbacks = self._finalizecallback
681 self._finalizecallback = {}
686 self._finalizecallback = {}
682 categories = sorted(callbacks)
687 categories = sorted(callbacks)
683 for cat in categories:
688 for cat in categories:
684 callbacks[cat](self)
689 callbacks[cat](self)
685 # Prevent double usage and help clear cycles.
690 # Prevent double usage and help clear cycles.
686 self._finalizecallback = None
691 self._finalizecallback = None
687 self._generatefiles(group=GEN_GROUP_POST_FINALIZE)
692 self._generatefiles(group=GEN_GROUP_POST_FINALIZE)
688
693
689 self._count -= 1
694 self._count -= 1
690 if self._count != 0:
695 if self._count != 0:
691 return
696 return
692 self._file.close()
697 self._file.close()
693 self._backupsfile.close()
698 self._backupsfile.close()
694 # cleanup temporary files
699 # cleanup temporary files
695 for l, f, b, c in self._backupentries:
700 for l, f, b, c in self._backupentries:
696 if l not in self._vfsmap and c:
701 if l not in self._vfsmap and c:
697 self._report(
702 self._report(
698 b"couldn't remove %s: unknown cache location %s\n" % (b, l)
703 b"couldn't remove %s: unknown cache location %s\n" % (b, l)
699 )
704 )
700 continue
705 continue
701 vfs = self._vfsmap[l]
706 vfs = self._vfsmap[l]
702 if not f and b and vfs.exists(b):
707 if not f and b and vfs.exists(b):
703 try:
708 try:
704 vfs.unlink(b)
709 vfs.unlink(b)
705 except (IOError, OSError, error.Abort) as inst:
710 except (IOError, OSError, error.Abort) as inst:
706 if not c:
711 if not c:
707 raise
712 raise
708 # Abort may be raise by read only opener
713 # Abort may be raise by read only opener
709 self._report(
714 self._report(
710 b"couldn't remove %s: %s\n" % (vfs.join(b), inst)
715 b"couldn't remove %s: %s\n" % (vfs.join(b), inst)
711 )
716 )
712 self._offsetmap = {}
717 self._offsetmap = {}
713 self._newfiles = set()
718 self._newfiles = set()
714 self._writeundo()
719 self._writeundo()
715 if self._after:
720 if self._after:
716 self._after()
721 self._after()
717 self._after = None # Help prevent cycles.
722 self._after = None # Help prevent cycles.
718 if self._opener.isfile(self._backupjournal):
723 if self._opener.isfile(self._backupjournal):
719 self._opener.unlink(self._backupjournal)
724 self._opener.unlink(self._backupjournal)
720 if self._opener.isfile(self._journal):
725 if self._opener.isfile(self._journal):
721 self._opener.unlink(self._journal)
726 self._opener.unlink(self._journal)
722 for l, _f, b, c in self._backupentries:
727 for l, _f, b, c in self._backupentries:
723 if l not in self._vfsmap and c:
728 if l not in self._vfsmap and c:
724 self._report(
729 self._report(
725 b"couldn't remove %s: unknown cache location"
730 b"couldn't remove %s: unknown cache location"
726 b"%s\n" % (b, l)
731 b"%s\n" % (b, l)
727 )
732 )
728 continue
733 continue
729 vfs = self._vfsmap[l]
734 vfs = self._vfsmap[l]
730 if b and vfs.exists(b):
735 if b and vfs.exists(b):
731 try:
736 try:
732 vfs.unlink(b)
737 vfs.unlink(b)
733 except (IOError, OSError, error.Abort) as inst:
738 except (IOError, OSError, error.Abort) as inst:
734 if not c:
739 if not c:
735 raise
740 raise
736 # Abort may be raise by read only opener
741 # Abort may be raise by read only opener
737 self._report(
742 self._report(
738 b"couldn't remove %s: %s\n" % (vfs.join(b), inst)
743 b"couldn't remove %s: %s\n" % (vfs.join(b), inst)
739 )
744 )
740 self._backupentries = []
745 self._backupentries = []
741 self._journal = None
746 self._journal = None
742
747
743 self._releasefn(self, True) # notify success of closing transaction
748 self._releasefn(self, True) # notify success of closing transaction
744 self._releasefn = None # Help prevent cycles.
749 self._releasefn = None # Help prevent cycles.
745
750
746 # run post close action
751 # run post close action
747 categories = sorted(self._postclosecallback)
752 categories = sorted(self._postclosecallback)
748 for cat in categories:
753 for cat in categories:
749 self._postclosecallback[cat](self)
754 self._postclosecallback[cat](self)
750 # Prevent double usage and help clear cycles.
755 # Prevent double usage and help clear cycles.
751 self._postclosecallback = None
756 self._postclosecallback = None
752
757
753 @active
758 @active
754 def abort(self):
759 def abort(self):
755 """abort the transaction (generally called on error, or when the
760 """abort the transaction (generally called on error, or when the
756 transaction is not explicitly committed before going out of
761 transaction is not explicitly committed before going out of
757 scope)"""
762 scope)"""
758 self._abort()
763 self._abort()
759
764
760 @active
765 @active
761 def add_journal(self, vfs_id, path):
766 def add_journal(self, vfs_id, path):
762 self._journal_files.append((vfs_id, path))
767 self._journal_files.append((vfs_id, path))
763
768
764 def _writeundo(self):
769 def _writeundo(self):
765 """write transaction data for possible future undo call"""
770 """write transaction data for possible future undo call"""
766 if self._undoname is None:
771 if self._undoname is None:
767 return
772 return
768 cleanup_undo_files(
773 cleanup_undo_files(
769 self._report,
774 self._report,
770 self._vfsmap,
775 self._vfsmap,
771 undo_prefix=self._undoname,
776 undo_prefix=self._undoname,
772 )
777 )
773
778
774 def undoname(fn: bytes) -> bytes:
779 def undoname(fn: bytes) -> bytes:
775 base, name = os.path.split(fn)
780 base, name = os.path.split(fn)
776 assert name.startswith(self._journal)
781 assert name.startswith(self._journal)
777 new_name = name.replace(self._journal, self._undoname, 1)
782 new_name = name.replace(self._journal, self._undoname, 1)
778 return os.path.join(base, new_name)
783 return os.path.join(base, new_name)
779
784
780 undo_backup_path = b"%s.backupfiles" % self._undoname
785 undo_backup_path = b"%s.backupfiles" % self._undoname
781 undobackupfile = self._opener.open(undo_backup_path, b'w')
786 undobackupfile = self._opener.open(undo_backup_path, b'w')
782 undobackupfile.write(b'%d\n' % version)
787 undobackupfile.write(b'%d\n' % version)
783 for l, f, b, c in self._backupentries:
788 for l, f, b, c in self._backupentries:
784 if not f: # temporary file
789 if not f: # temporary file
785 continue
790 continue
786 if not b:
791 if not b:
787 u = b''
792 u = b''
788 else:
793 else:
789 if l not in self._vfsmap and c:
794 if l not in self._vfsmap and c:
790 self._report(
795 self._report(
791 b"couldn't remove %s: unknown cache location"
796 b"couldn't remove %s: unknown cache location"
792 b"%s\n" % (b, l)
797 b"%s\n" % (b, l)
793 )
798 )
794 continue
799 continue
795 vfs = self._vfsmap[l]
800 vfs = self._vfsmap[l]
796 u = undoname(b)
801 u = undoname(b)
797 util.copyfile(vfs.join(b), vfs.join(u), hardlink=True)
802 util.copyfile(vfs.join(b), vfs.join(u), hardlink=True)
798 undobackupfile.write(b"%s\0%s\0%s\0%d\n" % (l, f, u, c))
803 undobackupfile.write(b"%s\0%s\0%s\0%d\n" % (l, f, u, c))
799 undobackupfile.close()
804 undobackupfile.close()
800 for vfs, src in self._journal_files:
805 for vfs, src in self._journal_files:
801 dest = undoname(src)
806 dest = undoname(src)
802 # if src and dest refer to a same file, vfs.rename is a no-op,
807 # if src and dest refer to a same file, vfs.rename is a no-op,
803 # leaving both src and dest on disk. delete dest to make sure
808 # leaving both src and dest on disk. delete dest to make sure
804 # the rename couldn't be such a no-op.
809 # the rename couldn't be such a no-op.
805 vfs.tryunlink(dest)
810 vfs.tryunlink(dest)
806 try:
811 try:
807 vfs.rename(src, dest)
812 vfs.rename(src, dest)
808 except FileNotFoundError: # journal file does not yet exist
813 except FileNotFoundError: # journal file does not yet exist
809 pass
814 pass
810
815
811 def _abort(self):
816 def _abort(self):
812 entries = self.readjournal()
817 entries = self.readjournal()
813 self._count = 0
818 self._count = 0
814 self._usages = 0
819 self._usages = 0
815 self._file.close()
820 self._file.close()
816 self._backupsfile.close()
821 self._backupsfile.close()
817
822
818 quick = self._can_quick_abort(entries)
823 quick = self._can_quick_abort(entries)
819 try:
824 try:
820 if not quick:
825 if not quick:
821 self._report(_(b"transaction abort!\n"))
826 self._report(_(b"transaction abort!\n"))
822 for cat in sorted(self._abortcallback):
827 for cat in sorted(self._abortcallback):
823 self._abortcallback[cat](self)
828 self._abortcallback[cat](self)
824 # Prevent double usage and help clear cycles.
829 # Prevent double usage and help clear cycles.
825 self._abortcallback = None
830 self._abortcallback = None
826 if quick:
831 if quick:
827 self._do_quick_abort(entries)
832 self._do_quick_abort(entries)
828 else:
833 else:
829 self._do_full_abort(entries)
834 self._do_full_abort(entries)
830 finally:
835 finally:
831 self._journal = None
836 self._journal = None
832 self._releasefn(self, False) # notify failure of transaction
837 self._releasefn(self, False) # notify failure of transaction
833 self._releasefn = None # Help prevent cycles.
838 self._releasefn = None # Help prevent cycles.
834
839
835 def _can_quick_abort(self, entries):
840 def _can_quick_abort(self, entries):
836 """False if any semantic content have been written on disk
841 """False if any semantic content have been written on disk
837
842
838 True if nothing, except temporary files has been writen on disk."""
843 True if nothing, except temporary files has been writen on disk."""
839 if entries:
844 if entries:
840 return False
845 return False
841 for e in self._backupentries:
846 for e in self._backupentries:
842 if e[1]:
847 if e[1]:
843 return False
848 return False
844 return True
849 return True
845
850
846 def _do_quick_abort(self, entries):
851 def _do_quick_abort(self, entries):
847 """(Silently) do a quick cleanup (see _can_quick_abort)"""
852 """(Silently) do a quick cleanup (see _can_quick_abort)"""
848 assert self._can_quick_abort(entries)
853 assert self._can_quick_abort(entries)
849 tmp_files = [e for e in self._backupentries if not e[1]]
854 tmp_files = [e for e in self._backupentries if not e[1]]
850 for vfs_id, old_path, tmp_path, xxx in tmp_files:
855 for vfs_id, old_path, tmp_path, xxx in tmp_files:
851 vfs = self._vfsmap[vfs_id]
856 vfs = self._vfsmap[vfs_id]
852 try:
857 try:
853 vfs.unlink(tmp_path)
858 vfs.unlink(tmp_path)
854 except FileNotFoundError:
859 except FileNotFoundError:
855 pass
860 pass
856 if self._backupjournal:
861 if self._backupjournal:
857 self._opener.unlink(self._backupjournal)
862 self._opener.unlink(self._backupjournal)
858 if self._journal:
863 if self._journal:
859 self._opener.unlink(self._journal)
864 self._opener.unlink(self._journal)
860
865
861 def _do_full_abort(self, entries):
866 def _do_full_abort(self, entries):
862 """(Noisily) rollback all the change introduced by the transaction"""
867 """(Noisily) rollback all the change introduced by the transaction"""
863 try:
868 try:
864 _playback(
869 _playback(
865 self._journal,
870 self._journal,
866 self._report,
871 self._report,
867 self._opener,
872 self._opener,
868 self._vfsmap,
873 self._vfsmap,
869 entries,
874 entries,
870 self._backupentries,
875 self._backupentries,
871 unlink=True,
876 unlink=True,
872 checkambigfiles=self._checkambigfiles,
877 checkambigfiles=self._checkambigfiles,
873 )
878 )
874 self._report(_(b"rollback completed\n"))
879 self._report(_(b"rollback completed\n"))
875 except BaseException as exc:
880 except BaseException as exc:
876 self._report(_(b"rollback failed - please run hg recover\n"))
881 self._report(_(b"rollback failed - please run hg recover\n"))
877 self._report(
882 self._report(
878 _(b"(failure reason: %s)\n") % stringutil.forcebytestr(exc)
883 _(b"(failure reason: %s)\n") % stringutil.forcebytestr(exc)
879 )
884 )
880
885
881
886
882 BAD_VERSION_MSG = _(
887 BAD_VERSION_MSG = _(
883 b"journal was created by a different version of Mercurial\n"
888 b"journal was created by a different version of Mercurial\n"
884 )
889 )
885
890
886
891
887 def read_backup_files(report, fp):
892 def read_backup_files(report, fp):
888 """parse an (already open) backup file an return contained backup entries
893 """parse an (already open) backup file an return contained backup entries
889
894
890 entries are in the form: (location, file, backupfile, xxx)
895 entries are in the form: (location, file, backupfile, xxx)
891
896
892 :location: the vfs identifier (vfsmap's key)
897 :location: the vfs identifier (vfsmap's key)
893 :file: original file path (in the vfs)
898 :file: original file path (in the vfs)
894 :backupfile: path of the backup (in the vfs)
899 :backupfile: path of the backup (in the vfs)
895 :cache: a boolean currently always set to False
900 :cache: a boolean currently always set to False
896 """
901 """
897 lines = fp.readlines()
902 lines = fp.readlines()
898 backupentries = []
903 backupentries = []
899 if lines:
904 if lines:
900 ver = lines[0][:-1]
905 ver = lines[0][:-1]
901 if ver != (b'%d' % version):
906 if ver != (b'%d' % version):
902 report(BAD_VERSION_MSG)
907 report(BAD_VERSION_MSG)
903 else:
908 else:
904 for line in lines[1:]:
909 for line in lines[1:]:
905 if line:
910 if line:
906 # Shave off the trailing newline
911 # Shave off the trailing newline
907 line = line[:-1]
912 line = line[:-1]
908 l, f, b, c = line.split(b'\0')
913 l, f, b, c = line.split(b'\0')
909 backupentries.append((l, f, b, bool(c)))
914 backupentries.append((l, f, b, bool(c)))
910 return backupentries
915 return backupentries
911
916
912
917
913 def rollback(
918 def rollback(
914 opener,
919 opener,
915 vfsmap,
920 vfsmap,
916 file,
921 file,
917 report,
922 report,
918 checkambigfiles=None,
923 checkambigfiles=None,
919 skip_journal_pattern=None,
924 skip_journal_pattern=None,
920 ):
925 ):
921 """Rolls back the transaction contained in the given file
926 """Rolls back the transaction contained in the given file
922
927
923 Reads the entries in the specified file, and the corresponding
928 Reads the entries in the specified file, and the corresponding
924 '*.backupfiles' file, to recover from an incomplete transaction.
929 '*.backupfiles' file, to recover from an incomplete transaction.
925
930
926 * `file`: a file containing a list of entries, specifying where
931 * `file`: a file containing a list of entries, specifying where
927 to truncate each file. The file should contain a list of
932 to truncate each file. The file should contain a list of
928 file\0offset pairs, delimited by newlines. The corresponding
933 file\0offset pairs, delimited by newlines. The corresponding
929 '*.backupfiles' file should contain a list of file\0backupfile
934 '*.backupfiles' file should contain a list of file\0backupfile
930 pairs, delimited by \0.
935 pairs, delimited by \0.
931
936
932 `checkambigfiles` is a set of (path, vfs-location) tuples,
937 `checkambigfiles` is a set of (path, vfs-location) tuples,
933 which determine whether file stat ambiguity should be avoided at
938 which determine whether file stat ambiguity should be avoided at
934 restoring corresponded files.
939 restoring corresponded files.
935 """
940 """
936 entries = []
941 entries = []
937 backupentries = []
942 backupentries = []
938
943
939 with opener.open(file) as fp:
944 with opener.open(file) as fp:
940 lines = fp.readlines()
945 lines = fp.readlines()
941 for l in lines:
946 for l in lines:
942 try:
947 try:
943 f, o = l.split(b'\0')
948 f, o = l.split(b'\0')
944 entries.append((f, int(o)))
949 entries.append((f, int(o)))
945 except ValueError:
950 except ValueError:
946 report(
951 report(
947 _(b"couldn't read journal entry %r!\n") % pycompat.bytestr(l)
952 _(b"couldn't read journal entry %r!\n") % pycompat.bytestr(l)
948 )
953 )
949
954
950 backupjournal = b"%s.backupfiles" % file
955 backupjournal = b"%s.backupfiles" % file
951 if opener.exists(backupjournal):
956 if opener.exists(backupjournal):
952 with opener.open(backupjournal) as fp:
957 with opener.open(backupjournal) as fp:
953 backupentries = read_backup_files(report, fp)
958 backupentries = read_backup_files(report, fp)
954 if skip_journal_pattern is not None:
959 if skip_journal_pattern is not None:
955 keep = lambda x: not skip_journal_pattern.match(x[1])
960 keep = lambda x: not skip_journal_pattern.match(x[1])
956 backupentries = [x for x in backupentries if keep(x)]
961 backupentries = [x for x in backupentries if keep(x)]
957
962
958 _playback(
963 _playback(
959 file,
964 file,
960 report,
965 report,
961 opener,
966 opener,
962 vfsmap,
967 vfsmap,
963 entries,
968 entries,
964 backupentries,
969 backupentries,
965 checkambigfiles=checkambigfiles,
970 checkambigfiles=checkambigfiles,
966 )
971 )
@@ -1,627 +1,628 b''
1 # verify.py - repository integrity checking for Mercurial
1 # verify.py - repository integrity checking for Mercurial
2 #
2 #
3 # Copyright 2006, 2007 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2006, 2007 Olivia Mackall <olivia@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8
8
9 import os
9 import os
10
10
11 from .i18n import _
11 from .i18n import _
12 from .node import short
12 from .node import short
13 from .utils import stringutil
13 from .utils import stringutil
14
14
15 from . import (
15 from . import (
16 error,
16 error,
17 pycompat,
17 pycompat,
18 requirements,
18 requirements,
19 revlog,
19 revlog,
20 transaction,
20 util,
21 util,
21 )
22 )
22
23
23 VERIFY_DEFAULT = 0
24 VERIFY_DEFAULT = 0
24 VERIFY_FULL = 1
25 VERIFY_FULL = 1
25
26
26
27
27 def verify(repo, level=None):
28 def verify(repo, level=None):
28 with repo.lock():
29 with repo.lock():
29 v = verifier(repo, level)
30 v = verifier(repo, level)
30 return v.verify()
31 return v.verify()
31
32
32
33
33 def _normpath(f):
34 def _normpath(f):
34 # under hg < 2.4, convert didn't sanitize paths properly, so a
35 # under hg < 2.4, convert didn't sanitize paths properly, so a
35 # converted repo may contain repeated slashes
36 # converted repo may contain repeated slashes
36 while b'//' in f:
37 while b'//' in f:
37 f = f.replace(b'//', b'/')
38 f = f.replace(b'//', b'/')
38 return f
39 return f
39
40
40
41
41 HINT_FNCACHE = _(
42 HINT_FNCACHE = _(
42 b'hint: run "hg debugrebuildfncache" to recover from corrupt fncache\n'
43 b'hint: run "hg debugrebuildfncache" to recover from corrupt fncache\n'
43 )
44 )
44
45
45 WARN_PARENT_DIR_UNKNOWN_REV = _(
46 WARN_PARENT_DIR_UNKNOWN_REV = _(
46 b"parent-directory manifest refers to unknown revision %s"
47 b"parent-directory manifest refers to unknown revision %s"
47 )
48 )
48
49
49 WARN_UNKNOWN_COPY_SOURCE = _(
50 WARN_UNKNOWN_COPY_SOURCE = _(
50 b"warning: copy source of '%s' not in parents of %s"
51 b"warning: copy source of '%s' not in parents of %s"
51 )
52 )
52
53
53 WARN_NULLID_COPY_SOURCE = _(
54 WARN_NULLID_COPY_SOURCE = _(
54 b"warning: %s@%s: copy source revision is nullid %s:%s\n"
55 b"warning: %s@%s: copy source revision is nullid %s:%s\n"
55 )
56 )
56
57
57
58
58 class verifier:
59 class verifier:
59 def __init__(self, repo, level=None):
60 def __init__(self, repo, level=None):
60 self.repo = repo.unfiltered()
61 self.repo = repo.unfiltered()
61 self.ui = repo.ui
62 self.ui = repo.ui
62 self.match = repo.narrowmatch()
63 self.match = repo.narrowmatch()
63 if level is None:
64 if level is None:
64 level = VERIFY_DEFAULT
65 level = VERIFY_DEFAULT
65 self._level = level
66 self._level = level
66 self.badrevs = set()
67 self.badrevs = set()
67 self.errors = 0
68 self.errors = 0
68 self.warnings = 0
69 self.warnings = 0
69 self.havecl = len(repo.changelog) > 0
70 self.havecl = len(repo.changelog) > 0
70 self.havemf = len(repo.manifestlog.getstorage(b'')) > 0
71 self.havemf = len(repo.manifestlog.getstorage(b'')) > 0
71 self.revlogv1 = repo.changelog._format_version != revlog.REVLOGV0
72 self.revlogv1 = repo.changelog._format_version != revlog.REVLOGV0
72 self.lrugetctx = util.lrucachefunc(repo.unfiltered().__getitem__)
73 self.lrugetctx = util.lrucachefunc(repo.unfiltered().__getitem__)
73 self.refersmf = False
74 self.refersmf = False
74 self.fncachewarned = False
75 self.fncachewarned = False
75 # developer config: verify.skipflags
76 # developer config: verify.skipflags
76 self.skipflags = repo.ui.configint(b'verify', b'skipflags')
77 self.skipflags = repo.ui.configint(b'verify', b'skipflags')
77 self.warnorphanstorefiles = True
78 self.warnorphanstorefiles = True
78
79
79 def _warn(self, msg):
80 def _warn(self, msg):
80 """record a "warning" level issue"""
81 """record a "warning" level issue"""
81 self.ui.warn(msg + b"\n")
82 self.ui.warn(msg + b"\n")
82 self.warnings += 1
83 self.warnings += 1
83
84
84 def _err(self, linkrev, msg, filename=None):
85 def _err(self, linkrev, msg, filename=None):
85 """record a "error" level issue"""
86 """record a "error" level issue"""
86 if linkrev is not None:
87 if linkrev is not None:
87 self.badrevs.add(linkrev)
88 self.badrevs.add(linkrev)
88 linkrev = b"%d" % linkrev
89 linkrev = b"%d" % linkrev
89 else:
90 else:
90 linkrev = b'?'
91 linkrev = b'?'
91 msg = b"%s: %s" % (linkrev, msg)
92 msg = b"%s: %s" % (linkrev, msg)
92 if filename:
93 if filename:
93 msg = b"%s@%s" % (filename, msg)
94 msg = b"%s@%s" % (filename, msg)
94 self.ui.warn(b" " + msg + b"\n")
95 self.ui.warn(b" " + msg + b"\n")
95 self.errors += 1
96 self.errors += 1
96
97
97 def _exc(self, linkrev, msg, inst, filename=None):
98 def _exc(self, linkrev, msg, inst, filename=None):
98 """record exception raised during the verify process"""
99 """record exception raised during the verify process"""
99 fmsg = stringutil.forcebytestr(inst)
100 fmsg = stringutil.forcebytestr(inst)
100 if not fmsg:
101 if not fmsg:
101 fmsg = pycompat.byterepr(inst)
102 fmsg = pycompat.byterepr(inst)
102 self._err(linkrev, b"%s: %s" % (msg, fmsg), filename)
103 self._err(linkrev, b"%s: %s" % (msg, fmsg), filename)
103
104
104 def _checkrevlog(self, obj, name, linkrev):
105 def _checkrevlog(self, obj, name, linkrev):
105 """verify high level property of a revlog
106 """verify high level property of a revlog
106
107
107 - revlog is present,
108 - revlog is present,
108 - revlog is non-empty,
109 - revlog is non-empty,
109 - sizes (index and data) are correct,
110 - sizes (index and data) are correct,
110 - revlog's format version is correct.
111 - revlog's format version is correct.
111 """
112 """
112 if not len(obj) and (self.havecl or self.havemf):
113 if not len(obj) and (self.havecl or self.havemf):
113 self._err(linkrev, _(b"empty or missing %s") % name)
114 self._err(linkrev, _(b"empty or missing %s") % name)
114 return
115 return
115
116
116 d = obj.checksize()
117 d = obj.checksize()
117 if d[0]:
118 if d[0]:
118 self._err(None, _(b"data length off by %d bytes") % d[0], name)
119 self._err(None, _(b"data length off by %d bytes") % d[0], name)
119 if d[1]:
120 if d[1]:
120 self._err(None, _(b"index contains %d extra bytes") % d[1], name)
121 self._err(None, _(b"index contains %d extra bytes") % d[1], name)
121
122
122 if obj._format_version != revlog.REVLOGV0:
123 if obj._format_version != revlog.REVLOGV0:
123 if not self.revlogv1:
124 if not self.revlogv1:
124 self._warn(_(b"warning: `%s' uses revlog format 1") % name)
125 self._warn(_(b"warning: `%s' uses revlog format 1") % name)
125 elif self.revlogv1:
126 elif self.revlogv1:
126 self._warn(_(b"warning: `%s' uses revlog format 0") % name)
127 self._warn(_(b"warning: `%s' uses revlog format 0") % name)
127
128
128 def _checkentry(self, obj, i, node, seen, linkrevs, f):
129 def _checkentry(self, obj, i, node, seen, linkrevs, f):
129 """verify a single revlog entry
130 """verify a single revlog entry
130
131
131 arguments are:
132 arguments are:
132 - obj: the source revlog
133 - obj: the source revlog
133 - i: the revision number
134 - i: the revision number
134 - node: the revision node id
135 - node: the revision node id
135 - seen: nodes previously seen for this revlog
136 - seen: nodes previously seen for this revlog
136 - linkrevs: [changelog-revisions] introducing "node"
137 - linkrevs: [changelog-revisions] introducing "node"
137 - f: string label ("changelog", "manifest", or filename)
138 - f: string label ("changelog", "manifest", or filename)
138
139
139 Performs the following checks:
140 Performs the following checks:
140 - linkrev points to an existing changelog revision,
141 - linkrev points to an existing changelog revision,
141 - linkrev points to a changelog revision that introduces this revision,
142 - linkrev points to a changelog revision that introduces this revision,
142 - linkrev points to the lowest of these changesets,
143 - linkrev points to the lowest of these changesets,
143 - both parents exist in the revlog,
144 - both parents exist in the revlog,
144 - the revision is not duplicated.
145 - the revision is not duplicated.
145
146
146 Return the linkrev of the revision (or None for changelog's revisions).
147 Return the linkrev of the revision (or None for changelog's revisions).
147 """
148 """
148 lr = obj.linkrev(obj.rev(node))
149 lr = obj.linkrev(obj.rev(node))
149 if lr < 0 or (self.havecl and lr not in linkrevs):
150 if lr < 0 or (self.havecl and lr not in linkrevs):
150 if lr < 0 or lr >= len(self.repo.changelog):
151 if lr < 0 or lr >= len(self.repo.changelog):
151 msg = _(b"rev %d points to nonexistent changeset %d")
152 msg = _(b"rev %d points to nonexistent changeset %d")
152 else:
153 else:
153 msg = _(b"rev %d points to unexpected changeset %d")
154 msg = _(b"rev %d points to unexpected changeset %d")
154 self._err(None, msg % (i, lr), f)
155 self._err(None, msg % (i, lr), f)
155 if linkrevs:
156 if linkrevs:
156 if f and len(linkrevs) > 1:
157 if f and len(linkrevs) > 1:
157 try:
158 try:
158 # attempt to filter down to real linkrevs
159 # attempt to filter down to real linkrevs
159 linkrevs = []
160 linkrevs = []
160 for lr in linkrevs:
161 for lr in linkrevs:
161 if self.lrugetctx(lr)[f].filenode() == node:
162 if self.lrugetctx(lr)[f].filenode() == node:
162 linkrevs.append(lr)
163 linkrevs.append(lr)
163 except Exception:
164 except Exception:
164 pass
165 pass
165 msg = _(b" (expected %s)")
166 msg = _(b" (expected %s)")
166 msg %= b" ".join(map(pycompat.bytestr, linkrevs))
167 msg %= b" ".join(map(pycompat.bytestr, linkrevs))
167 self._warn(msg)
168 self._warn(msg)
168 lr = None # can't be trusted
169 lr = None # can't be trusted
169
170
170 try:
171 try:
171 p1, p2 = obj.parents(node)
172 p1, p2 = obj.parents(node)
172 if p1 not in seen and p1 != self.repo.nullid:
173 if p1 not in seen and p1 != self.repo.nullid:
173 msg = _(b"unknown parent 1 %s of %s") % (short(p1), short(node))
174 msg = _(b"unknown parent 1 %s of %s") % (short(p1), short(node))
174 self._err(lr, msg, f)
175 self._err(lr, msg, f)
175 if p2 not in seen and p2 != self.repo.nullid:
176 if p2 not in seen and p2 != self.repo.nullid:
176 msg = _(b"unknown parent 2 %s of %s") % (short(p2), short(node))
177 msg = _(b"unknown parent 2 %s of %s") % (short(p2), short(node))
177 self._err(lr, msg, f)
178 self._err(lr, msg, f)
178 except Exception as inst:
179 except Exception as inst:
179 self._exc(lr, _(b"checking parents of %s") % short(node), inst, f)
180 self._exc(lr, _(b"checking parents of %s") % short(node), inst, f)
180
181
181 if node in seen:
182 if node in seen:
182 self._err(lr, _(b"duplicate revision %d (%d)") % (i, seen[node]), f)
183 self._err(lr, _(b"duplicate revision %d (%d)") % (i, seen[node]), f)
183 seen[node] = i
184 seen[node] = i
184 return lr
185 return lr
185
186
186 def verify(self):
187 def verify(self):
187 """verify the content of the Mercurial repository
188 """verify the content of the Mercurial repository
188
189
189 This method run all verifications, displaying issues as they are found.
190 This method run all verifications, displaying issues as they are found.
190
191
191 return 1 if any error have been encountered, 0 otherwise."""
192 return 1 if any error have been encountered, 0 otherwise."""
192 # initial validation and generic report
193 # initial validation and generic report
193 repo = self.repo
194 repo = self.repo
194 ui = repo.ui
195 ui = repo.ui
195 if not repo.url().startswith(b'file:'):
196 if not repo.url().startswith(b'file:'):
196 raise error.Abort(_(b"cannot verify bundle or remote repos"))
197 raise error.Abort(_(b"cannot verify bundle or remote repos"))
197
198
198 if os.path.exists(repo.sjoin(b"journal")):
199 if transaction.has_abandoned_transaction(repo):
199 ui.warn(_(b"abandoned transaction found - run hg recover\n"))
200 ui.warn(_(b"abandoned transaction found - run hg recover\n"))
200
201
201 if ui.verbose or not self.revlogv1:
202 if ui.verbose or not self.revlogv1:
202 ui.status(
203 ui.status(
203 _(b"repository uses revlog format %d\n")
204 _(b"repository uses revlog format %d\n")
204 % (self.revlogv1 and 1 or 0)
205 % (self.revlogv1 and 1 or 0)
205 )
206 )
206
207
207 # data verification
208 # data verification
208 mflinkrevs, filelinkrevs = self._verifychangelog()
209 mflinkrevs, filelinkrevs = self._verifychangelog()
209 filenodes = self._verifymanifest(mflinkrevs)
210 filenodes = self._verifymanifest(mflinkrevs)
210 del mflinkrevs
211 del mflinkrevs
211 self._crosscheckfiles(filelinkrevs, filenodes)
212 self._crosscheckfiles(filelinkrevs, filenodes)
212 totalfiles, filerevisions = self._verifyfiles(filenodes, filelinkrevs)
213 totalfiles, filerevisions = self._verifyfiles(filenodes, filelinkrevs)
213
214
214 if self.errors:
215 if self.errors:
215 ui.warn(_(b"not checking dirstate because of previous errors\n"))
216 ui.warn(_(b"not checking dirstate because of previous errors\n"))
216 dirstate_errors = 0
217 dirstate_errors = 0
217 else:
218 else:
218 dirstate_errors = self._verify_dirstate()
219 dirstate_errors = self._verify_dirstate()
219
220
220 # final report
221 # final report
221 ui.status(
222 ui.status(
222 _(b"checked %d changesets with %d changes to %d files\n")
223 _(b"checked %d changesets with %d changes to %d files\n")
223 % (len(repo.changelog), filerevisions, totalfiles)
224 % (len(repo.changelog), filerevisions, totalfiles)
224 )
225 )
225 if self.warnings:
226 if self.warnings:
226 ui.warn(_(b"%d warnings encountered!\n") % self.warnings)
227 ui.warn(_(b"%d warnings encountered!\n") % self.warnings)
227 if self.fncachewarned:
228 if self.fncachewarned:
228 ui.warn(HINT_FNCACHE)
229 ui.warn(HINT_FNCACHE)
229 if self.errors:
230 if self.errors:
230 ui.warn(_(b"%d integrity errors encountered!\n") % self.errors)
231 ui.warn(_(b"%d integrity errors encountered!\n") % self.errors)
231 if self.badrevs:
232 if self.badrevs:
232 msg = _(b"(first damaged changeset appears to be %d)\n")
233 msg = _(b"(first damaged changeset appears to be %d)\n")
233 msg %= min(self.badrevs)
234 msg %= min(self.badrevs)
234 ui.warn(msg)
235 ui.warn(msg)
235 if dirstate_errors:
236 if dirstate_errors:
236 ui.warn(
237 ui.warn(
237 _(b"dirstate inconsistent with current parent's manifest\n")
238 _(b"dirstate inconsistent with current parent's manifest\n")
238 )
239 )
239 ui.warn(_(b"%d dirstate errors\n") % dirstate_errors)
240 ui.warn(_(b"%d dirstate errors\n") % dirstate_errors)
240 return 1
241 return 1
241 return 0
242 return 0
242
243
243 def _verifychangelog(self):
244 def _verifychangelog(self):
244 """verify the changelog of a repository
245 """verify the changelog of a repository
245
246
246 The following checks are performed:
247 The following checks are performed:
247 - all of `_checkrevlog` checks,
248 - all of `_checkrevlog` checks,
248 - all of `_checkentry` checks (for each revisions),
249 - all of `_checkentry` checks (for each revisions),
249 - each revision can be read.
250 - each revision can be read.
250
251
251 The function returns some of the data observed in the changesets as a
252 The function returns some of the data observed in the changesets as a
252 (mflinkrevs, filelinkrevs) tuples:
253 (mflinkrevs, filelinkrevs) tuples:
253 - mflinkrevs: is a { manifest-node -> [changelog-rev] } mapping
254 - mflinkrevs: is a { manifest-node -> [changelog-rev] } mapping
254 - filelinkrevs: is a { file-path -> [changelog-rev] } mapping
255 - filelinkrevs: is a { file-path -> [changelog-rev] } mapping
255
256
256 If a matcher was specified, filelinkrevs will only contains matched
257 If a matcher was specified, filelinkrevs will only contains matched
257 files.
258 files.
258 """
259 """
259 ui = self.ui
260 ui = self.ui
260 repo = self.repo
261 repo = self.repo
261 match = self.match
262 match = self.match
262 cl = repo.changelog
263 cl = repo.changelog
263
264
264 ui.status(_(b"checking changesets\n"))
265 ui.status(_(b"checking changesets\n"))
265 mflinkrevs = {}
266 mflinkrevs = {}
266 filelinkrevs = {}
267 filelinkrevs = {}
267 seen = {}
268 seen = {}
268 self._checkrevlog(cl, b"changelog", 0)
269 self._checkrevlog(cl, b"changelog", 0)
269 progress = ui.makeprogress(
270 progress = ui.makeprogress(
270 _(b'checking'), unit=_(b'changesets'), total=len(repo)
271 _(b'checking'), unit=_(b'changesets'), total=len(repo)
271 )
272 )
272 for i in repo:
273 for i in repo:
273 progress.update(i)
274 progress.update(i)
274 n = cl.node(i)
275 n = cl.node(i)
275 self._checkentry(cl, i, n, seen, [i], b"changelog")
276 self._checkentry(cl, i, n, seen, [i], b"changelog")
276
277
277 try:
278 try:
278 changes = cl.read(n)
279 changes = cl.read(n)
279 if changes[0] != self.repo.nullid:
280 if changes[0] != self.repo.nullid:
280 mflinkrevs.setdefault(changes[0], []).append(i)
281 mflinkrevs.setdefault(changes[0], []).append(i)
281 self.refersmf = True
282 self.refersmf = True
282 for f in changes[3]:
283 for f in changes[3]:
283 if match(f):
284 if match(f):
284 filelinkrevs.setdefault(_normpath(f), []).append(i)
285 filelinkrevs.setdefault(_normpath(f), []).append(i)
285 except Exception as inst:
286 except Exception as inst:
286 self.refersmf = True
287 self.refersmf = True
287 self._exc(i, _(b"unpacking changeset %s") % short(n), inst)
288 self._exc(i, _(b"unpacking changeset %s") % short(n), inst)
288 progress.complete()
289 progress.complete()
289 return mflinkrevs, filelinkrevs
290 return mflinkrevs, filelinkrevs
290
291
291 def _verifymanifest(
292 def _verifymanifest(
292 self, mflinkrevs, dir=b"", storefiles=None, subdirprogress=None
293 self, mflinkrevs, dir=b"", storefiles=None, subdirprogress=None
293 ):
294 ):
294 """verify the manifestlog content
295 """verify the manifestlog content
295
296
296 Inputs:
297 Inputs:
297 - mflinkrevs: a {manifest-node -> [changelog-revisions]} mapping
298 - mflinkrevs: a {manifest-node -> [changelog-revisions]} mapping
298 - dir: a subdirectory to check (for tree manifest repo)
299 - dir: a subdirectory to check (for tree manifest repo)
299 - storefiles: set of currently "orphan" files.
300 - storefiles: set of currently "orphan" files.
300 - subdirprogress: a progress object
301 - subdirprogress: a progress object
301
302
302 This function checks:
303 This function checks:
303 * all of `_checkrevlog` checks (for all manifest related revlogs)
304 * all of `_checkrevlog` checks (for all manifest related revlogs)
304 * all of `_checkentry` checks (for all manifest related revisions)
305 * all of `_checkentry` checks (for all manifest related revisions)
305 * nodes for subdirectory exists in the sub-directory manifest
306 * nodes for subdirectory exists in the sub-directory manifest
306 * each manifest entries have a file path
307 * each manifest entries have a file path
307 * each manifest node refered in mflinkrevs exist in the manifest log
308 * each manifest node refered in mflinkrevs exist in the manifest log
308
309
309 If tree manifest is in use and a matchers is specified, only the
310 If tree manifest is in use and a matchers is specified, only the
310 sub-directories matching it will be verified.
311 sub-directories matching it will be verified.
311
312
312 return a two level mapping:
313 return a two level mapping:
313 {"path" -> { filenode -> changelog-revision}}
314 {"path" -> { filenode -> changelog-revision}}
314
315
315 This mapping primarily contains entries for every files in the
316 This mapping primarily contains entries for every files in the
316 repository. In addition, when tree-manifest is used, it also contains
317 repository. In addition, when tree-manifest is used, it also contains
317 sub-directory entries.
318 sub-directory entries.
318
319
319 If a matcher is provided, only matching paths will be included.
320 If a matcher is provided, only matching paths will be included.
320 """
321 """
321 repo = self.repo
322 repo = self.repo
322 ui = self.ui
323 ui = self.ui
323 match = self.match
324 match = self.match
324 mfl = self.repo.manifestlog
325 mfl = self.repo.manifestlog
325 mf = mfl.getstorage(dir)
326 mf = mfl.getstorage(dir)
326
327
327 if not dir:
328 if not dir:
328 self.ui.status(_(b"checking manifests\n"))
329 self.ui.status(_(b"checking manifests\n"))
329
330
330 filenodes = {}
331 filenodes = {}
331 subdirnodes = {}
332 subdirnodes = {}
332 seen = {}
333 seen = {}
333 label = b"manifest"
334 label = b"manifest"
334 if dir:
335 if dir:
335 label = dir
336 label = dir
336 revlogfiles = mf.files()
337 revlogfiles = mf.files()
337 storefiles.difference_update(revlogfiles)
338 storefiles.difference_update(revlogfiles)
338 if subdirprogress: # should be true since we're in a subdirectory
339 if subdirprogress: # should be true since we're in a subdirectory
339 subdirprogress.increment()
340 subdirprogress.increment()
340 if self.refersmf:
341 if self.refersmf:
341 # Do not check manifest if there are only changelog entries with
342 # Do not check manifest if there are only changelog entries with
342 # null manifests.
343 # null manifests.
343 self._checkrevlog(mf._revlog, label, 0)
344 self._checkrevlog(mf._revlog, label, 0)
344 progress = ui.makeprogress(
345 progress = ui.makeprogress(
345 _(b'checking'), unit=_(b'manifests'), total=len(mf)
346 _(b'checking'), unit=_(b'manifests'), total=len(mf)
346 )
347 )
347 for i in mf:
348 for i in mf:
348 if not dir:
349 if not dir:
349 progress.update(i)
350 progress.update(i)
350 n = mf.node(i)
351 n = mf.node(i)
351 lr = self._checkentry(mf, i, n, seen, mflinkrevs.get(n, []), label)
352 lr = self._checkentry(mf, i, n, seen, mflinkrevs.get(n, []), label)
352 if n in mflinkrevs:
353 if n in mflinkrevs:
353 del mflinkrevs[n]
354 del mflinkrevs[n]
354 elif dir:
355 elif dir:
355 msg = _(b"%s not in parent-directory manifest") % short(n)
356 msg = _(b"%s not in parent-directory manifest") % short(n)
356 self._err(lr, msg, label)
357 self._err(lr, msg, label)
357 else:
358 else:
358 self._err(lr, _(b"%s not in changesets") % short(n), label)
359 self._err(lr, _(b"%s not in changesets") % short(n), label)
359
360
360 try:
361 try:
361 mfdelta = mfl.get(dir, n).readdelta(shallow=True)
362 mfdelta = mfl.get(dir, n).readdelta(shallow=True)
362 for f, fn, fl in mfdelta.iterentries():
363 for f, fn, fl in mfdelta.iterentries():
363 if not f:
364 if not f:
364 self._err(lr, _(b"entry without name in manifest"))
365 self._err(lr, _(b"entry without name in manifest"))
365 elif f == b"/dev/null": # ignore this in very old repos
366 elif f == b"/dev/null": # ignore this in very old repos
366 continue
367 continue
367 fullpath = dir + _normpath(f)
368 fullpath = dir + _normpath(f)
368 if fl == b't':
369 if fl == b't':
369 if not match.visitdir(fullpath):
370 if not match.visitdir(fullpath):
370 continue
371 continue
371 sdn = subdirnodes.setdefault(fullpath + b'/', {})
372 sdn = subdirnodes.setdefault(fullpath + b'/', {})
372 sdn.setdefault(fn, []).append(lr)
373 sdn.setdefault(fn, []).append(lr)
373 else:
374 else:
374 if not match(fullpath):
375 if not match(fullpath):
375 continue
376 continue
376 filenodes.setdefault(fullpath, {}).setdefault(fn, lr)
377 filenodes.setdefault(fullpath, {}).setdefault(fn, lr)
377 except Exception as inst:
378 except Exception as inst:
378 self._exc(lr, _(b"reading delta %s") % short(n), inst, label)
379 self._exc(lr, _(b"reading delta %s") % short(n), inst, label)
379 if self._level >= VERIFY_FULL:
380 if self._level >= VERIFY_FULL:
380 try:
381 try:
381 # Various issues can affect manifest. So we read each full
382 # Various issues can affect manifest. So we read each full
382 # text from storage. This triggers the checks from the core
383 # text from storage. This triggers the checks from the core
383 # code (eg: hash verification, filename are ordered, etc.)
384 # code (eg: hash verification, filename are ordered, etc.)
384 mfdelta = mfl.get(dir, n).read()
385 mfdelta = mfl.get(dir, n).read()
385 except Exception as inst:
386 except Exception as inst:
386 msg = _(b"reading full manifest %s") % short(n)
387 msg = _(b"reading full manifest %s") % short(n)
387 self._exc(lr, msg, inst, label)
388 self._exc(lr, msg, inst, label)
388
389
389 if not dir:
390 if not dir:
390 progress.complete()
391 progress.complete()
391
392
392 if self.havemf:
393 if self.havemf:
393 # since we delete entry in `mflinkrevs` during iteration, any
394 # since we delete entry in `mflinkrevs` during iteration, any
394 # remaining entries are "missing". We need to issue errors for them.
395 # remaining entries are "missing". We need to issue errors for them.
395 changesetpairs = [(c, m) for m in mflinkrevs for c in mflinkrevs[m]]
396 changesetpairs = [(c, m) for m in mflinkrevs for c in mflinkrevs[m]]
396 for c, m in sorted(changesetpairs):
397 for c, m in sorted(changesetpairs):
397 if dir:
398 if dir:
398 self._err(c, WARN_PARENT_DIR_UNKNOWN_REV % short(m), label)
399 self._err(c, WARN_PARENT_DIR_UNKNOWN_REV % short(m), label)
399 else:
400 else:
400 msg = _(b"changeset refers to unknown revision %s")
401 msg = _(b"changeset refers to unknown revision %s")
401 msg %= short(m)
402 msg %= short(m)
402 self._err(c, msg, label)
403 self._err(c, msg, label)
403
404
404 if not dir and subdirnodes:
405 if not dir and subdirnodes:
405 self.ui.status(_(b"checking directory manifests\n"))
406 self.ui.status(_(b"checking directory manifests\n"))
406 storefiles = set()
407 storefiles = set()
407 subdirs = set()
408 subdirs = set()
408 revlogv1 = self.revlogv1
409 revlogv1 = self.revlogv1
409 undecodable = []
410 undecodable = []
410 for entry in repo.store.data_entries(undecodable=undecodable):
411 for entry in repo.store.data_entries(undecodable=undecodable):
411 for file_ in entry.files():
412 for file_ in entry.files():
412 f = file_.unencoded_path
413 f = file_.unencoded_path
413 size = file_.file_size(repo.store.vfs)
414 size = file_.file_size(repo.store.vfs)
414 if (size > 0 or not revlogv1) and f.startswith(b'meta/'):
415 if (size > 0 or not revlogv1) and f.startswith(b'meta/'):
415 storefiles.add(_normpath(f))
416 storefiles.add(_normpath(f))
416 subdirs.add(os.path.dirname(f))
417 subdirs.add(os.path.dirname(f))
417 for f in undecodable:
418 for f in undecodable:
418 self._err(None, _(b"cannot decode filename '%s'") % f)
419 self._err(None, _(b"cannot decode filename '%s'") % f)
419 subdirprogress = ui.makeprogress(
420 subdirprogress = ui.makeprogress(
420 _(b'checking'), unit=_(b'manifests'), total=len(subdirs)
421 _(b'checking'), unit=_(b'manifests'), total=len(subdirs)
421 )
422 )
422
423
423 for subdir, linkrevs in subdirnodes.items():
424 for subdir, linkrevs in subdirnodes.items():
424 subdirfilenodes = self._verifymanifest(
425 subdirfilenodes = self._verifymanifest(
425 linkrevs, subdir, storefiles, subdirprogress
426 linkrevs, subdir, storefiles, subdirprogress
426 )
427 )
427 for f, onefilenodes in subdirfilenodes.items():
428 for f, onefilenodes in subdirfilenodes.items():
428 filenodes.setdefault(f, {}).update(onefilenodes)
429 filenodes.setdefault(f, {}).update(onefilenodes)
429
430
430 if not dir and subdirnodes:
431 if not dir and subdirnodes:
431 assert subdirprogress is not None # help pytype
432 assert subdirprogress is not None # help pytype
432 subdirprogress.complete()
433 subdirprogress.complete()
433 if self.warnorphanstorefiles:
434 if self.warnorphanstorefiles:
434 for f in sorted(storefiles):
435 for f in sorted(storefiles):
435 self._warn(_(b"warning: orphan data file '%s'") % f)
436 self._warn(_(b"warning: orphan data file '%s'") % f)
436
437
437 return filenodes
438 return filenodes
438
439
439 def _crosscheckfiles(self, filelinkrevs, filenodes):
440 def _crosscheckfiles(self, filelinkrevs, filenodes):
440 repo = self.repo
441 repo = self.repo
441 ui = self.ui
442 ui = self.ui
442 ui.status(_(b"crosschecking files in changesets and manifests\n"))
443 ui.status(_(b"crosschecking files in changesets and manifests\n"))
443
444
444 total = len(filelinkrevs) + len(filenodes)
445 total = len(filelinkrevs) + len(filenodes)
445 progress = ui.makeprogress(
446 progress = ui.makeprogress(
446 _(b'crosschecking'), unit=_(b'files'), total=total
447 _(b'crosschecking'), unit=_(b'files'), total=total
447 )
448 )
448 if self.havemf:
449 if self.havemf:
449 for f in sorted(filelinkrevs):
450 for f in sorted(filelinkrevs):
450 progress.increment()
451 progress.increment()
451 if f not in filenodes:
452 if f not in filenodes:
452 lr = filelinkrevs[f][0]
453 lr = filelinkrevs[f][0]
453 self._err(lr, _(b"in changeset but not in manifest"), f)
454 self._err(lr, _(b"in changeset but not in manifest"), f)
454
455
455 if self.havecl:
456 if self.havecl:
456 for f in sorted(filenodes):
457 for f in sorted(filenodes):
457 progress.increment()
458 progress.increment()
458 if f not in filelinkrevs:
459 if f not in filelinkrevs:
459 try:
460 try:
460 fl = repo.file(f)
461 fl = repo.file(f)
461 lr = min([fl.linkrev(fl.rev(n)) for n in filenodes[f]])
462 lr = min([fl.linkrev(fl.rev(n)) for n in filenodes[f]])
462 except Exception:
463 except Exception:
463 lr = None
464 lr = None
464 self._err(lr, _(b"in manifest but not in changeset"), f)
465 self._err(lr, _(b"in manifest but not in changeset"), f)
465
466
466 progress.complete()
467 progress.complete()
467
468
468 def _verifyfiles(self, filenodes, filelinkrevs):
469 def _verifyfiles(self, filenodes, filelinkrevs):
469 repo = self.repo
470 repo = self.repo
470 ui = self.ui
471 ui = self.ui
471 lrugetctx = self.lrugetctx
472 lrugetctx = self.lrugetctx
472 revlogv1 = self.revlogv1
473 revlogv1 = self.revlogv1
473 havemf = self.havemf
474 havemf = self.havemf
474 ui.status(_(b"checking files\n"))
475 ui.status(_(b"checking files\n"))
475
476
476 storefiles = set()
477 storefiles = set()
477 undecodable = []
478 undecodable = []
478 for entry in repo.store.data_entries(undecodable=undecodable):
479 for entry in repo.store.data_entries(undecodable=undecodable):
479 for file_ in entry.files():
480 for file_ in entry.files():
480 size = file_.file_size(repo.store.vfs)
481 size = file_.file_size(repo.store.vfs)
481 f = file_.unencoded_path
482 f = file_.unencoded_path
482 if (size > 0 or not revlogv1) and f.startswith(b'data/'):
483 if (size > 0 or not revlogv1) and f.startswith(b'data/'):
483 storefiles.add(_normpath(f))
484 storefiles.add(_normpath(f))
484 for f in undecodable:
485 for f in undecodable:
485 self._err(None, _(b"cannot decode filename '%s'") % f)
486 self._err(None, _(b"cannot decode filename '%s'") % f)
486
487
487 state = {
488 state = {
488 # TODO this assumes revlog storage for changelog.
489 # TODO this assumes revlog storage for changelog.
489 b'expectedversion': self.repo.changelog._format_version,
490 b'expectedversion': self.repo.changelog._format_version,
490 b'skipflags': self.skipflags,
491 b'skipflags': self.skipflags,
491 # experimental config: censor.policy
492 # experimental config: censor.policy
492 b'erroroncensored': ui.config(b'censor', b'policy') == b'abort',
493 b'erroroncensored': ui.config(b'censor', b'policy') == b'abort',
493 }
494 }
494
495
495 files = sorted(set(filenodes) | set(filelinkrevs))
496 files = sorted(set(filenodes) | set(filelinkrevs))
496 revisions = 0
497 revisions = 0
497 progress = ui.makeprogress(
498 progress = ui.makeprogress(
498 _(b'checking'), unit=_(b'files'), total=len(files)
499 _(b'checking'), unit=_(b'files'), total=len(files)
499 )
500 )
500 for i, f in enumerate(files):
501 for i, f in enumerate(files):
501 progress.update(i, item=f)
502 progress.update(i, item=f)
502 try:
503 try:
503 linkrevs = filelinkrevs[f]
504 linkrevs = filelinkrevs[f]
504 except KeyError:
505 except KeyError:
505 # in manifest but not in changelog
506 # in manifest but not in changelog
506 linkrevs = []
507 linkrevs = []
507
508
508 if linkrevs:
509 if linkrevs:
509 lr = linkrevs[0]
510 lr = linkrevs[0]
510 else:
511 else:
511 lr = None
512 lr = None
512
513
513 try:
514 try:
514 fl = repo.file(f)
515 fl = repo.file(f)
515 except error.StorageError as e:
516 except error.StorageError as e:
516 self._err(lr, _(b"broken revlog! (%s)") % e, f)
517 self._err(lr, _(b"broken revlog! (%s)") % e, f)
517 continue
518 continue
518
519
519 for ff in fl.files():
520 for ff in fl.files():
520 try:
521 try:
521 storefiles.remove(ff)
522 storefiles.remove(ff)
522 except KeyError:
523 except KeyError:
523 if self.warnorphanstorefiles:
524 if self.warnorphanstorefiles:
524 msg = _(b" warning: revlog '%s' not in fncache!")
525 msg = _(b" warning: revlog '%s' not in fncache!")
525 self._warn(msg % ff)
526 self._warn(msg % ff)
526 self.fncachewarned = True
527 self.fncachewarned = True
527
528
528 if not len(fl) and (self.havecl or self.havemf):
529 if not len(fl) and (self.havecl or self.havemf):
529 self._err(lr, _(b"empty or missing %s") % f)
530 self._err(lr, _(b"empty or missing %s") % f)
530 else:
531 else:
531 # Guard against implementations not setting this.
532 # Guard against implementations not setting this.
532 state[b'skipread'] = set()
533 state[b'skipread'] = set()
533 state[b'safe_renamed'] = set()
534 state[b'safe_renamed'] = set()
534
535
535 for problem in fl.verifyintegrity(state):
536 for problem in fl.verifyintegrity(state):
536 if problem.node is not None:
537 if problem.node is not None:
537 linkrev = fl.linkrev(fl.rev(problem.node))
538 linkrev = fl.linkrev(fl.rev(problem.node))
538 else:
539 else:
539 linkrev = None
540 linkrev = None
540
541
541 if problem.warning:
542 if problem.warning:
542 self._warn(problem.warning)
543 self._warn(problem.warning)
543 elif problem.error:
544 elif problem.error:
544 linkrev_msg = linkrev if linkrev is not None else lr
545 linkrev_msg = linkrev if linkrev is not None else lr
545 self._err(linkrev_msg, problem.error, f)
546 self._err(linkrev_msg, problem.error, f)
546 else:
547 else:
547 raise error.ProgrammingError(
548 raise error.ProgrammingError(
548 b'problem instance does not set warning or error '
549 b'problem instance does not set warning or error '
549 b'attribute: %s' % problem.msg
550 b'attribute: %s' % problem.msg
550 )
551 )
551
552
552 seen = {}
553 seen = {}
553 for i in fl:
554 for i in fl:
554 revisions += 1
555 revisions += 1
555 n = fl.node(i)
556 n = fl.node(i)
556 lr = self._checkentry(fl, i, n, seen, linkrevs, f)
557 lr = self._checkentry(fl, i, n, seen, linkrevs, f)
557 if f in filenodes:
558 if f in filenodes:
558 if havemf and n not in filenodes[f]:
559 if havemf and n not in filenodes[f]:
559 self._err(lr, _(b"%s not in manifests") % (short(n)), f)
560 self._err(lr, _(b"%s not in manifests") % (short(n)), f)
560 else:
561 else:
561 del filenodes[f][n]
562 del filenodes[f][n]
562
563
563 if n in state[b'skipread'] and n not in state[b'safe_renamed']:
564 if n in state[b'skipread'] and n not in state[b'safe_renamed']:
564 continue
565 continue
565
566
566 # check renames
567 # check renames
567 try:
568 try:
568 # This requires resolving fulltext (at least on revlogs,
569 # This requires resolving fulltext (at least on revlogs,
569 # though not with LFS revisions). We may want
570 # though not with LFS revisions). We may want
570 # ``verifyintegrity()`` to pass a set of nodes with
571 # ``verifyintegrity()`` to pass a set of nodes with
571 # rename metadata as an optimization.
572 # rename metadata as an optimization.
572 rp = fl.renamed(n)
573 rp = fl.renamed(n)
573 if rp:
574 if rp:
574 if lr is not None and ui.verbose:
575 if lr is not None and ui.verbose:
575 ctx = lrugetctx(lr)
576 ctx = lrugetctx(lr)
576 if not any(rp[0] in pctx for pctx in ctx.parents()):
577 if not any(rp[0] in pctx for pctx in ctx.parents()):
577 self._warn(WARN_UNKNOWN_COPY_SOURCE % (f, ctx))
578 self._warn(WARN_UNKNOWN_COPY_SOURCE % (f, ctx))
578 fl2 = repo.file(rp[0])
579 fl2 = repo.file(rp[0])
579 if not len(fl2):
580 if not len(fl2):
580 m = _(b"empty or missing copy source revlog %s:%s")
581 m = _(b"empty or missing copy source revlog %s:%s")
581 self._err(lr, m % (rp[0], short(rp[1])), f)
582 self._err(lr, m % (rp[0], short(rp[1])), f)
582 elif rp[1] == self.repo.nullid:
583 elif rp[1] == self.repo.nullid:
583 msg = WARN_NULLID_COPY_SOURCE
584 msg = WARN_NULLID_COPY_SOURCE
584 msg %= (f, lr, rp[0], short(rp[1]))
585 msg %= (f, lr, rp[0], short(rp[1]))
585 ui.note(msg)
586 ui.note(msg)
586 else:
587 else:
587 fl2.rev(rp[1])
588 fl2.rev(rp[1])
588 except Exception as inst:
589 except Exception as inst:
589 self._exc(
590 self._exc(
590 lr, _(b"checking rename of %s") % short(n), inst, f
591 lr, _(b"checking rename of %s") % short(n), inst, f
591 )
592 )
592
593
593 # cross-check
594 # cross-check
594 if f in filenodes:
595 if f in filenodes:
595 fns = [(v, k) for k, v in filenodes[f].items()]
596 fns = [(v, k) for k, v in filenodes[f].items()]
596 for lr, node in sorted(fns):
597 for lr, node in sorted(fns):
597 msg = _(b"manifest refers to unknown revision %s")
598 msg = _(b"manifest refers to unknown revision %s")
598 self._err(lr, msg % short(node), f)
599 self._err(lr, msg % short(node), f)
599 progress.complete()
600 progress.complete()
600
601
601 if self.warnorphanstorefiles:
602 if self.warnorphanstorefiles:
602 for f in sorted(storefiles):
603 for f in sorted(storefiles):
603 self._warn(_(b"warning: orphan data file '%s'") % f)
604 self._warn(_(b"warning: orphan data file '%s'") % f)
604
605
605 return len(files), revisions
606 return len(files), revisions
606
607
607 def _verify_dirstate(self):
608 def _verify_dirstate(self):
608 """Check that the dirstate is consistent with the parent's manifest"""
609 """Check that the dirstate is consistent with the parent's manifest"""
609 repo = self.repo
610 repo = self.repo
610 ui = self.ui
611 ui = self.ui
611 ui.status(_(b"checking dirstate\n"))
612 ui.status(_(b"checking dirstate\n"))
612
613
613 parent1, parent2 = repo.dirstate.parents()
614 parent1, parent2 = repo.dirstate.parents()
614 m1 = repo[parent1].manifest()
615 m1 = repo[parent1].manifest()
615 m2 = repo[parent2].manifest()
616 m2 = repo[parent2].manifest()
616 dirstate_errors = 0
617 dirstate_errors = 0
617
618
618 is_narrow = requirements.NARROW_REQUIREMENT in repo.requirements
619 is_narrow = requirements.NARROW_REQUIREMENT in repo.requirements
619 narrow_matcher = repo.narrowmatch() if is_narrow else None
620 narrow_matcher = repo.narrowmatch() if is_narrow else None
620
621
621 for err in repo.dirstate.verify(m1, m2, parent1, narrow_matcher):
622 for err in repo.dirstate.verify(m1, m2, parent1, narrow_matcher):
622 ui.error(err)
623 ui.error(err)
623 dirstate_errors += 1
624 dirstate_errors += 1
624
625
625 if dirstate_errors:
626 if dirstate_errors:
626 self.errors += dirstate_errors
627 self.errors += dirstate_errors
627 return dirstate_errors
628 return dirstate_errors
General Comments 0
You need to be logged in to leave comments. Login now