##// END OF EJS Templates
transaction: re-wrap line to avoid a black bug...
Augie Fackler -
r43320:a614f26d default
parent child Browse files
Show More
@@ -1,653 +1,655 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 Matt Mackall <mpm@selenic.com>
9 # Copyright 2005, 2006 Matt Mackall <mpm@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 from __future__ import absolute_import
14 from __future__ import absolute_import
15
15
16 import errno
16 import errno
17
17
18 from .i18n import _
18 from .i18n import _
19 from . import (
19 from . import (
20 error,
20 error,
21 pycompat,
21 pycompat,
22 util,
22 util,
23 )
23 )
24 from .utils import (
24 from .utils import (
25 stringutil,
25 stringutil,
26 )
26 )
27
27
28 version = 2
28 version = 2
29
29
30 # These are the file generators that should only be executed after the
30 # These are the file generators that should only be executed after the
31 # finalizers are done, since they rely on the output of the finalizers (like
31 # finalizers are done, since they rely on the output of the finalizers (like
32 # the changelog having been written).
32 # the changelog having been written).
33 postfinalizegenerators = {
33 postfinalizegenerators = {
34 'bookmarks',
34 'bookmarks',
35 'dirstate'
35 'dirstate'
36 }
36 }
37
37
38 gengroupall='all'
38 gengroupall='all'
39 gengroupprefinalize='prefinalize'
39 gengroupprefinalize='prefinalize'
40 gengrouppostfinalize='postfinalize'
40 gengrouppostfinalize='postfinalize'
41
41
42 def active(func):
42 def active(func):
43 def _active(self, *args, **kwds):
43 def _active(self, *args, **kwds):
44 if self._count == 0:
44 if self._count == 0:
45 raise error.Abort(_(
45 raise error.Abort(_(
46 'cannot use transaction when it is already committed/aborted'))
46 'cannot use transaction when it is already committed/aborted'))
47 return func(self, *args, **kwds)
47 return func(self, *args, **kwds)
48 return _active
48 return _active
49
49
50 def _playback(journal, report, opener, vfsmap, entries, backupentries,
50 def _playback(journal, report, opener, vfsmap, entries, backupentries,
51 unlink=True, checkambigfiles=None):
51 unlink=True, checkambigfiles=None):
52 for f, o, _ignore in entries:
52 for f, o, _ignore in entries:
53 if o or not unlink:
53 if o or not unlink:
54 checkambig = checkambigfiles and (f, '') in checkambigfiles
54 checkambig = checkambigfiles and (f, '') in checkambigfiles
55 try:
55 try:
56 fp = opener(f, 'a', checkambig=checkambig)
56 fp = opener(f, 'a', checkambig=checkambig)
57 if fp.tell() < o:
57 if fp.tell() < o:
58 raise error.Abort(_(
58 raise error.Abort(_(
59 "attempted to truncate %s to %d bytes, but it was "
59 "attempted to truncate %s to %d bytes, but it was "
60 "already %d bytes\n") % (f, o, fp.tell()))
60 "already %d bytes\n") % (f, o, fp.tell()))
61 fp.truncate(o)
61 fp.truncate(o)
62 fp.close()
62 fp.close()
63 except IOError:
63 except IOError:
64 report(_("failed to truncate %s\n") % f)
64 report(_("failed to truncate %s\n") % f)
65 raise
65 raise
66 else:
66 else:
67 try:
67 try:
68 opener.unlink(f)
68 opener.unlink(f)
69 except (IOError, OSError) as inst:
69 except (IOError, OSError) as inst:
70 if inst.errno != errno.ENOENT:
70 if inst.errno != errno.ENOENT:
71 raise
71 raise
72
72
73 backupfiles = []
73 backupfiles = []
74 for l, f, b, c in backupentries:
74 for l, f, b, c in backupentries:
75 if l not in vfsmap and c:
75 if l not in vfsmap and c:
76 report("couldn't handle %s: unknown cache location %s\n"
76 report("couldn't handle %s: unknown cache location %s\n"
77 % (b, l))
77 % (b, l))
78 vfs = vfsmap[l]
78 vfs = vfsmap[l]
79 try:
79 try:
80 if f and b:
80 if f and b:
81 filepath = vfs.join(f)
81 filepath = vfs.join(f)
82 backuppath = vfs.join(b)
82 backuppath = vfs.join(b)
83 checkambig = checkambigfiles and (f, l) in checkambigfiles
83 checkambig = checkambigfiles and (f, l) in checkambigfiles
84 try:
84 try:
85 util.copyfile(backuppath, filepath, checkambig=checkambig)
85 util.copyfile(backuppath, filepath, checkambig=checkambig)
86 backupfiles.append(b)
86 backupfiles.append(b)
87 except IOError:
87 except IOError:
88 report(_("failed to recover %s\n") % f)
88 report(_("failed to recover %s\n") % f)
89 else:
89 else:
90 target = f or b
90 target = f or b
91 try:
91 try:
92 vfs.unlink(target)
92 vfs.unlink(target)
93 except (IOError, OSError) as inst:
93 except (IOError, OSError) as inst:
94 if inst.errno != errno.ENOENT:
94 if inst.errno != errno.ENOENT:
95 raise
95 raise
96 except (IOError, OSError, error.Abort):
96 except (IOError, OSError, error.Abort):
97 if not c:
97 if not c:
98 raise
98 raise
99
99
100 backuppath = "%s.backupfiles" % journal
100 backuppath = "%s.backupfiles" % journal
101 if opener.exists(backuppath):
101 if opener.exists(backuppath):
102 opener.unlink(backuppath)
102 opener.unlink(backuppath)
103 opener.unlink(journal)
103 opener.unlink(journal)
104 try:
104 try:
105 for f in backupfiles:
105 for f in backupfiles:
106 if opener.exists(f):
106 if opener.exists(f):
107 opener.unlink(f)
107 opener.unlink(f)
108 except (IOError, OSError, error.Abort):
108 except (IOError, OSError, error.Abort):
109 # only pure backup file remains, it is sage to ignore any error
109 # only pure backup file remains, it is sage to ignore any error
110 pass
110 pass
111
111
112 class transaction(util.transactional):
112 class transaction(util.transactional):
113 def __init__(self, report, opener, vfsmap, journalname, undoname=None,
113 def __init__(self, report, opener, vfsmap, journalname, undoname=None,
114 after=None, createmode=None, validator=None, releasefn=None,
114 after=None, createmode=None, validator=None, releasefn=None,
115 checkambigfiles=None, name=r'<unnamed>'):
115 checkambigfiles=None, name=r'<unnamed>'):
116 """Begin a new transaction
116 """Begin a new transaction
117
117
118 Begins a new transaction that allows rolling back writes in the event of
118 Begins a new transaction that allows rolling back writes in the event of
119 an exception.
119 an exception.
120
120
121 * `after`: called after the transaction has been committed
121 * `after`: called after the transaction has been committed
122 * `createmode`: the mode of the journal file that will be created
122 * `createmode`: the mode of the journal file that will be created
123 * `releasefn`: called after releasing (with transaction and result)
123 * `releasefn`: called after releasing (with transaction and result)
124
124
125 `checkambigfiles` is a set of (path, vfs-location) tuples,
125 `checkambigfiles` is a set of (path, vfs-location) tuples,
126 which determine whether file stat ambiguity should be avoided
126 which determine whether file stat ambiguity should be avoided
127 for corresponded files.
127 for corresponded files.
128 """
128 """
129 self._count = 1
129 self._count = 1
130 self._usages = 1
130 self._usages = 1
131 self._report = report
131 self._report = report
132 # a vfs to the store content
132 # a vfs to the store content
133 self._opener = opener
133 self._opener = opener
134 # a map to access file in various {location -> vfs}
134 # a map to access file in various {location -> vfs}
135 vfsmap = vfsmap.copy()
135 vfsmap = vfsmap.copy()
136 vfsmap[''] = opener # set default value
136 vfsmap[''] = opener # set default value
137 self._vfsmap = vfsmap
137 self._vfsmap = vfsmap
138 self._after = after
138 self._after = after
139 self._entries = []
139 self._entries = []
140 self._map = {}
140 self._map = {}
141 self._journal = journalname
141 self._journal = journalname
142 self._undoname = undoname
142 self._undoname = undoname
143 self._queue = []
143 self._queue = []
144 # A callback to validate transaction content before closing it.
144 # A callback to validate transaction content before closing it.
145 # should raise exception is anything is wrong.
145 # should raise exception is anything is wrong.
146 # target user is repository hooks.
146 # target user is repository hooks.
147 if validator is None:
147 if validator is None:
148 validator = lambda tr: None
148 validator = lambda tr: None
149 self._validator = validator
149 self._validator = validator
150 # A callback to do something just after releasing transaction.
150 # A callback to do something just after releasing transaction.
151 if releasefn is None:
151 if releasefn is None:
152 releasefn = lambda tr, success: None
152 releasefn = lambda tr, success: None
153 self._releasefn = releasefn
153 self._releasefn = releasefn
154
154
155 self._checkambigfiles = set()
155 self._checkambigfiles = set()
156 if checkambigfiles:
156 if checkambigfiles:
157 self._checkambigfiles.update(checkambigfiles)
157 self._checkambigfiles.update(checkambigfiles)
158
158
159 self._names = [name]
159 self._names = [name]
160
160
161 # A dict dedicated to precisely tracking the changes introduced in the
161 # A dict dedicated to precisely tracking the changes introduced in the
162 # transaction.
162 # transaction.
163 self.changes = {}
163 self.changes = {}
164
164
165 # a dict of arguments to be passed to hooks
165 # a dict of arguments to be passed to hooks
166 self.hookargs = {}
166 self.hookargs = {}
167 self._file = opener.open(self._journal, "w")
167 self._file = opener.open(self._journal, "w")
168
168
169 # a list of ('location', 'path', 'backuppath', cache) entries.
169 # a list of ('location', 'path', 'backuppath', cache) entries.
170 # - if 'backuppath' is empty, no file existed at backup time
170 # - if 'backuppath' is empty, no file existed at backup time
171 # - if 'path' is empty, this is a temporary transaction file
171 # - if 'path' is empty, this is a temporary transaction file
172 # - if 'location' is not empty, the path is outside main opener reach.
172 # - if 'location' is not empty, the path is outside main opener reach.
173 # use 'location' value as a key in a vfsmap to find the right 'vfs'
173 # use 'location' value as a key in a vfsmap to find the right 'vfs'
174 # (cache is currently unused)
174 # (cache is currently unused)
175 self._backupentries = []
175 self._backupentries = []
176 self._backupmap = {}
176 self._backupmap = {}
177 self._backupjournal = "%s.backupfiles" % self._journal
177 self._backupjournal = "%s.backupfiles" % self._journal
178 self._backupsfile = opener.open(self._backupjournal, 'w')
178 self._backupsfile = opener.open(self._backupjournal, 'w')
179 self._backupsfile.write('%d\n' % version)
179 self._backupsfile.write('%d\n' % version)
180
180
181 if createmode is not None:
181 if createmode is not None:
182 opener.chmod(self._journal, createmode & 0o666)
182 opener.chmod(self._journal, createmode & 0o666)
183 opener.chmod(self._backupjournal, createmode & 0o666)
183 opener.chmod(self._backupjournal, createmode & 0o666)
184
184
185 # hold file generations to be performed on commit
185 # hold file generations to be performed on commit
186 self._filegenerators = {}
186 self._filegenerators = {}
187 # hold callback to write pending data for hooks
187 # hold callback to write pending data for hooks
188 self._pendingcallback = {}
188 self._pendingcallback = {}
189 # True is any pending data have been written ever
189 # True is any pending data have been written ever
190 self._anypending = False
190 self._anypending = False
191 # holds callback to call when writing the transaction
191 # holds callback to call when writing the transaction
192 self._finalizecallback = {}
192 self._finalizecallback = {}
193 # hold callback for post transaction close
193 # hold callback for post transaction close
194 self._postclosecallback = {}
194 self._postclosecallback = {}
195 # holds callbacks to call during abort
195 # holds callbacks to call during abort
196 self._abortcallback = {}
196 self._abortcallback = {}
197
197
198 def __repr__(self):
198 def __repr__(self):
199 name = r'/'.join(self._names)
199 name = r'/'.join(self._names)
200 return (r'<transaction name=%s, count=%d, usages=%d>' %
200 return (r'<transaction name=%s, count=%d, usages=%d>' %
201 (name, self._count, self._usages))
201 (name, self._count, self._usages))
202
202
203 def __del__(self):
203 def __del__(self):
204 if self._journal:
204 if self._journal:
205 self._abort()
205 self._abort()
206
206
207 @active
207 @active
208 def startgroup(self):
208 def startgroup(self):
209 """delay registration of file entry
209 """delay registration of file entry
210
210
211 This is used by strip to delay vision of strip offset. The transaction
211 This is used by strip to delay vision of strip offset. The transaction
212 sees either none or all of the strip actions to be done."""
212 sees either none or all of the strip actions to be done."""
213 self._queue.append([])
213 self._queue.append([])
214
214
215 @active
215 @active
216 def endgroup(self):
216 def endgroup(self):
217 """apply delayed registration of file entry.
217 """apply delayed registration of file entry.
218
218
219 This is used by strip to delay vision of strip offset. The transaction
219 This is used by strip to delay vision of strip offset. The transaction
220 sees either none or all of the strip actions to be done."""
220 sees either none or all of the strip actions to be done."""
221 q = self._queue.pop()
221 q = self._queue.pop()
222 for f, o, data in q:
222 for f, o, data in q:
223 self._addentry(f, o, data)
223 self._addentry(f, o, data)
224
224
225 @active
225 @active
226 def add(self, file, offset, data=None):
226 def add(self, file, offset, data=None):
227 """record the state of an append-only file before update"""
227 """record the state of an append-only file before update"""
228 if file in self._map or file in self._backupmap:
228 if file in self._map or file in self._backupmap:
229 return
229 return
230 if self._queue:
230 if self._queue:
231 self._queue[-1].append((file, offset, data))
231 self._queue[-1].append((file, offset, data))
232 return
232 return
233
233
234 self._addentry(file, offset, data)
234 self._addentry(file, offset, data)
235
235
236 def _addentry(self, file, offset, data):
236 def _addentry(self, file, offset, data):
237 """add a append-only entry to memory and on-disk state"""
237 """add a append-only entry to memory and on-disk state"""
238 if file in self._map or file in self._backupmap:
238 if file in self._map or file in self._backupmap:
239 return
239 return
240 self._entries.append((file, offset, data))
240 self._entries.append((file, offset, data))
241 self._map[file] = len(self._entries) - 1
241 self._map[file] = len(self._entries) - 1
242 # add enough data to the journal to do the truncate
242 # add enough data to the journal to do the truncate
243 self._file.write("%s\0%d\n" % (file, offset))
243 self._file.write("%s\0%d\n" % (file, offset))
244 self._file.flush()
244 self._file.flush()
245
245
246 @active
246 @active
247 def addbackup(self, file, hardlink=True, location=''):
247 def addbackup(self, file, hardlink=True, location=''):
248 """Adds a backup of the file to the transaction
248 """Adds a backup of the file to the transaction
249
249
250 Calling addbackup() creates a hardlink backup of the specified file
250 Calling addbackup() creates a hardlink backup of the specified file
251 that is used to recover the file in the event of the transaction
251 that is used to recover the file in the event of the transaction
252 aborting.
252 aborting.
253
253
254 * `file`: the file path, relative to .hg/store
254 * `file`: the file path, relative to .hg/store
255 * `hardlink`: use a hardlink to quickly create the backup
255 * `hardlink`: use a hardlink to quickly create the backup
256 """
256 """
257 if self._queue:
257 if self._queue:
258 msg = 'cannot use transaction.addbackup inside "group"'
258 msg = 'cannot use transaction.addbackup inside "group"'
259 raise error.ProgrammingError(msg)
259 raise error.ProgrammingError(msg)
260
260
261 if file in self._map or file in self._backupmap:
261 if file in self._map or file in self._backupmap:
262 return
262 return
263 vfs = self._vfsmap[location]
263 vfs = self._vfsmap[location]
264 dirname, filename = vfs.split(file)
264 dirname, filename = vfs.split(file)
265 backupfilename = "%s.backup.%s" % (self._journal, filename)
265 backupfilename = "%s.backup.%s" % (self._journal, filename)
266 backupfile = vfs.reljoin(dirname, backupfilename)
266 backupfile = vfs.reljoin(dirname, backupfilename)
267 if vfs.exists(file):
267 if vfs.exists(file):
268 filepath = vfs.join(file)
268 filepath = vfs.join(file)
269 backuppath = vfs.join(backupfile)
269 backuppath = vfs.join(backupfile)
270 util.copyfile(filepath, backuppath, hardlink=hardlink)
270 util.copyfile(filepath, backuppath, hardlink=hardlink)
271 else:
271 else:
272 backupfile = ''
272 backupfile = ''
273
273
274 self._addbackupentry((location, file, backupfile, False))
274 self._addbackupentry((location, file, backupfile, False))
275
275
276 def _addbackupentry(self, entry):
276 def _addbackupentry(self, entry):
277 """register a new backup entry and write it to disk"""
277 """register a new backup entry and write it to disk"""
278 self._backupentries.append(entry)
278 self._backupentries.append(entry)
279 self._backupmap[entry[1]] = len(self._backupentries) - 1
279 self._backupmap[entry[1]] = len(self._backupentries) - 1
280 self._backupsfile.write("%s\0%s\0%s\0%d\n" % entry)
280 self._backupsfile.write("%s\0%s\0%s\0%d\n" % entry)
281 self._backupsfile.flush()
281 self._backupsfile.flush()
282
282
283 @active
283 @active
284 def registertmp(self, tmpfile, location=''):
284 def registertmp(self, tmpfile, location=''):
285 """register a temporary transaction file
285 """register a temporary transaction file
286
286
287 Such files will be deleted when the transaction exits (on both
287 Such files will be deleted when the transaction exits (on both
288 failure and success).
288 failure and success).
289 """
289 """
290 self._addbackupentry((location, '', tmpfile, False))
290 self._addbackupentry((location, '', tmpfile, False))
291
291
292 @active
292 @active
293 def addfilegenerator(self, genid, filenames, genfunc, order=0,
293 def addfilegenerator(self, genid, filenames, genfunc, order=0,
294 location=''):
294 location=''):
295 """add a function to generates some files at transaction commit
295 """add a function to generates some files at transaction commit
296
296
297 The `genfunc` argument is a function capable of generating proper
297 The `genfunc` argument is a function capable of generating proper
298 content of each entry in the `filename` tuple.
298 content of each entry in the `filename` tuple.
299
299
300 At transaction close time, `genfunc` will be called with one file
300 At transaction close time, `genfunc` will be called with one file
301 object argument per entries in `filenames`.
301 object argument per entries in `filenames`.
302
302
303 The transaction itself is responsible for the backup, creation and
303 The transaction itself is responsible for the backup, creation and
304 final write of such file.
304 final write of such file.
305
305
306 The `genid` argument is used to ensure the same set of file is only
306 The `genid` argument is used to ensure the same set of file is only
307 generated once. Call to `addfilegenerator` for a `genid` already
307 generated once. Call to `addfilegenerator` for a `genid` already
308 present will overwrite the old entry.
308 present will overwrite the old entry.
309
309
310 The `order` argument may be used to control the order in which multiple
310 The `order` argument may be used to control the order in which multiple
311 generator will be executed.
311 generator will be executed.
312
312
313 The `location` arguments may be used to indicate the files are located
313 The `location` arguments may be used to indicate the files are located
314 outside of the the standard directory for transaction. It should match
314 outside of the the standard directory for transaction. It should match
315 one of the key of the `transaction.vfsmap` dictionary.
315 one of the key of the `transaction.vfsmap` dictionary.
316 """
316 """
317 # For now, we are unable to do proper backup and restore of custom vfs
317 # For now, we are unable to do proper backup and restore of custom vfs
318 # but for bookmarks that are handled outside this mechanism.
318 # but for bookmarks that are handled outside this mechanism.
319 self._filegenerators[genid] = (order, filenames, genfunc, location)
319 self._filegenerators[genid] = (order, filenames, genfunc, location)
320
320
321 @active
321 @active
322 def removefilegenerator(self, genid):
322 def removefilegenerator(self, genid):
323 """reverse of addfilegenerator, remove a file generator function"""
323 """reverse of addfilegenerator, remove a file generator function"""
324 if genid in self._filegenerators:
324 if genid in self._filegenerators:
325 del self._filegenerators[genid]
325 del self._filegenerators[genid]
326
326
327 def _generatefiles(self, suffix='', group=gengroupall):
327 def _generatefiles(self, suffix='', group=gengroupall):
328 # write files registered for generation
328 # write files registered for generation
329 any = False
329 any = False
330 for id, entry in sorted(self._filegenerators.iteritems()):
330 for id, entry in sorted(self._filegenerators.iteritems()):
331 any = True
331 any = True
332 order, filenames, genfunc, location = entry
332 order, filenames, genfunc, location = entry
333
333
334 # for generation at closing, check if it's before or after finalize
334 # for generation at closing, check if it's before or after finalize
335 postfinalize = group == gengrouppostfinalize
335 postfinalize = group == gengrouppostfinalize
336 if (group != gengroupall and
336 if (
337 (id in postfinalizegenerators) != (postfinalize)):
337 group != gengroupall
338 and (id in postfinalizegenerators) != postfinalize
339 ):
338 continue
340 continue
339
341
340 vfs = self._vfsmap[location]
342 vfs = self._vfsmap[location]
341 files = []
343 files = []
342 try:
344 try:
343 for name in filenames:
345 for name in filenames:
344 name += suffix
346 name += suffix
345 if suffix:
347 if suffix:
346 self.registertmp(name, location=location)
348 self.registertmp(name, location=location)
347 checkambig = False
349 checkambig = False
348 else:
350 else:
349 self.addbackup(name, location=location)
351 self.addbackup(name, location=location)
350 checkambig = (name, location) in self._checkambigfiles
352 checkambig = (name, location) in self._checkambigfiles
351 files.append(vfs(name, 'w', atomictemp=True,
353 files.append(vfs(name, 'w', atomictemp=True,
352 checkambig=checkambig))
354 checkambig=checkambig))
353 genfunc(*files)
355 genfunc(*files)
354 for f in files:
356 for f in files:
355 f.close()
357 f.close()
356 # skip discard() loop since we're sure no open file remains
358 # skip discard() loop since we're sure no open file remains
357 del files[:]
359 del files[:]
358 finally:
360 finally:
359 for f in files:
361 for f in files:
360 f.discard()
362 f.discard()
361 return any
363 return any
362
364
363 @active
365 @active
364 def find(self, file):
366 def find(self, file):
365 if file in self._map:
367 if file in self._map:
366 return self._entries[self._map[file]]
368 return self._entries[self._map[file]]
367 if file in self._backupmap:
369 if file in self._backupmap:
368 return self._backupentries[self._backupmap[file]]
370 return self._backupentries[self._backupmap[file]]
369 return None
371 return None
370
372
371 @active
373 @active
372 def replace(self, file, offset, data=None):
374 def replace(self, file, offset, data=None):
373 '''
375 '''
374 replace can only replace already committed entries
376 replace can only replace already committed entries
375 that are not pending in the queue
377 that are not pending in the queue
376 '''
378 '''
377
379
378 if file not in self._map:
380 if file not in self._map:
379 raise KeyError(file)
381 raise KeyError(file)
380 index = self._map[file]
382 index = self._map[file]
381 self._entries[index] = (file, offset, data)
383 self._entries[index] = (file, offset, data)
382 self._file.write("%s\0%d\n" % (file, offset))
384 self._file.write("%s\0%d\n" % (file, offset))
383 self._file.flush()
385 self._file.flush()
384
386
385 @active
387 @active
386 def nest(self, name=r'<unnamed>'):
388 def nest(self, name=r'<unnamed>'):
387 self._count += 1
389 self._count += 1
388 self._usages += 1
390 self._usages += 1
389 self._names.append(name)
391 self._names.append(name)
390 return self
392 return self
391
393
392 def release(self):
394 def release(self):
393 if self._count > 0:
395 if self._count > 0:
394 self._usages -= 1
396 self._usages -= 1
395 if self._names:
397 if self._names:
396 self._names.pop()
398 self._names.pop()
397 # if the transaction scopes are left without being closed, fail
399 # if the transaction scopes are left without being closed, fail
398 if self._count > 0 and self._usages == 0:
400 if self._count > 0 and self._usages == 0:
399 self._abort()
401 self._abort()
400
402
401 def running(self):
403 def running(self):
402 return self._count > 0
404 return self._count > 0
403
405
404 def addpending(self, category, callback):
406 def addpending(self, category, callback):
405 """add a callback to be called when the transaction is pending
407 """add a callback to be called when the transaction is pending
406
408
407 The transaction will be given as callback's first argument.
409 The transaction will be given as callback's first argument.
408
410
409 Category is a unique identifier to allow overwriting an old callback
411 Category is a unique identifier to allow overwriting an old callback
410 with a newer callback.
412 with a newer callback.
411 """
413 """
412 self._pendingcallback[category] = callback
414 self._pendingcallback[category] = callback
413
415
414 @active
416 @active
415 def writepending(self):
417 def writepending(self):
416 '''write pending file to temporary version
418 '''write pending file to temporary version
417
419
418 This is used to allow hooks to view a transaction before commit'''
420 This is used to allow hooks to view a transaction before commit'''
419 categories = sorted(self._pendingcallback)
421 categories = sorted(self._pendingcallback)
420 for cat in categories:
422 for cat in categories:
421 # remove callback since the data will have been flushed
423 # remove callback since the data will have been flushed
422 any = self._pendingcallback.pop(cat)(self)
424 any = self._pendingcallback.pop(cat)(self)
423 self._anypending = self._anypending or any
425 self._anypending = self._anypending or any
424 self._anypending |= self._generatefiles(suffix='.pending')
426 self._anypending |= self._generatefiles(suffix='.pending')
425 return self._anypending
427 return self._anypending
426
428
427 @active
429 @active
428 def addfinalize(self, category, callback):
430 def addfinalize(self, category, callback):
429 """add a callback to be called when the transaction is closed
431 """add a callback to be called when the transaction is closed
430
432
431 The transaction will be given as callback's first argument.
433 The transaction will be given as callback's first argument.
432
434
433 Category is a unique identifier to allow overwriting old callbacks with
435 Category is a unique identifier to allow overwriting old callbacks with
434 newer callbacks.
436 newer callbacks.
435 """
437 """
436 self._finalizecallback[category] = callback
438 self._finalizecallback[category] = callback
437
439
438 @active
440 @active
439 def addpostclose(self, category, callback):
441 def addpostclose(self, category, callback):
440 """add or replace a callback to be called after the transaction closed
442 """add or replace a callback to be called after the transaction closed
441
443
442 The transaction will be given as callback's first argument.
444 The transaction will be given as callback's first argument.
443
445
444 Category is a unique identifier to allow overwriting an old callback
446 Category is a unique identifier to allow overwriting an old callback
445 with a newer callback.
447 with a newer callback.
446 """
448 """
447 self._postclosecallback[category] = callback
449 self._postclosecallback[category] = callback
448
450
449 @active
451 @active
450 def getpostclose(self, category):
452 def getpostclose(self, category):
451 """return a postclose callback added before, or None"""
453 """return a postclose callback added before, or None"""
452 return self._postclosecallback.get(category, None)
454 return self._postclosecallback.get(category, None)
453
455
454 @active
456 @active
455 def addabort(self, category, callback):
457 def addabort(self, category, callback):
456 """add a callback to be called when the transaction is aborted.
458 """add a callback to be called when the transaction is aborted.
457
459
458 The transaction will be given as the first argument to the callback.
460 The transaction will be given as the first argument to the callback.
459
461
460 Category is a unique identifier to allow overwriting an old callback
462 Category is a unique identifier to allow overwriting an old callback
461 with a newer callback.
463 with a newer callback.
462 """
464 """
463 self._abortcallback[category] = callback
465 self._abortcallback[category] = callback
464
466
465 @active
467 @active
466 def close(self):
468 def close(self):
467 '''commit the transaction'''
469 '''commit the transaction'''
468 if self._count == 1:
470 if self._count == 1:
469 self._validator(self) # will raise exception if needed
471 self._validator(self) # will raise exception if needed
470 self._validator = None # Help prevent cycles.
472 self._validator = None # Help prevent cycles.
471 self._generatefiles(group=gengroupprefinalize)
473 self._generatefiles(group=gengroupprefinalize)
472 categories = sorted(self._finalizecallback)
474 categories = sorted(self._finalizecallback)
473 for cat in categories:
475 for cat in categories:
474 self._finalizecallback[cat](self)
476 self._finalizecallback[cat](self)
475 # Prevent double usage and help clear cycles.
477 # Prevent double usage and help clear cycles.
476 self._finalizecallback = None
478 self._finalizecallback = None
477 self._generatefiles(group=gengrouppostfinalize)
479 self._generatefiles(group=gengrouppostfinalize)
478
480
479 self._count -= 1
481 self._count -= 1
480 if self._count != 0:
482 if self._count != 0:
481 return
483 return
482 self._file.close()
484 self._file.close()
483 self._backupsfile.close()
485 self._backupsfile.close()
484 # cleanup temporary files
486 # cleanup temporary files
485 for l, f, b, c in self._backupentries:
487 for l, f, b, c in self._backupentries:
486 if l not in self._vfsmap and c:
488 if l not in self._vfsmap and c:
487 self._report("couldn't remove %s: unknown cache location %s\n"
489 self._report("couldn't remove %s: unknown cache location %s\n"
488 % (b, l))
490 % (b, l))
489 continue
491 continue
490 vfs = self._vfsmap[l]
492 vfs = self._vfsmap[l]
491 if not f and b and vfs.exists(b):
493 if not f and b and vfs.exists(b):
492 try:
494 try:
493 vfs.unlink(b)
495 vfs.unlink(b)
494 except (IOError, OSError, error.Abort) as inst:
496 except (IOError, OSError, error.Abort) as inst:
495 if not c:
497 if not c:
496 raise
498 raise
497 # Abort may be raise by read only opener
499 # Abort may be raise by read only opener
498 self._report("couldn't remove %s: %s\n"
500 self._report("couldn't remove %s: %s\n"
499 % (vfs.join(b), inst))
501 % (vfs.join(b), inst))
500 self._entries = []
502 self._entries = []
501 self._writeundo()
503 self._writeundo()
502 if self._after:
504 if self._after:
503 self._after()
505 self._after()
504 self._after = None # Help prevent cycles.
506 self._after = None # Help prevent cycles.
505 if self._opener.isfile(self._backupjournal):
507 if self._opener.isfile(self._backupjournal):
506 self._opener.unlink(self._backupjournal)
508 self._opener.unlink(self._backupjournal)
507 if self._opener.isfile(self._journal):
509 if self._opener.isfile(self._journal):
508 self._opener.unlink(self._journal)
510 self._opener.unlink(self._journal)
509 for l, _f, b, c in self._backupentries:
511 for l, _f, b, c in self._backupentries:
510 if l not in self._vfsmap and c:
512 if l not in self._vfsmap and c:
511 self._report("couldn't remove %s: unknown cache location"
513 self._report("couldn't remove %s: unknown cache location"
512 "%s\n" % (b, l))
514 "%s\n" % (b, l))
513 continue
515 continue
514 vfs = self._vfsmap[l]
516 vfs = self._vfsmap[l]
515 if b and vfs.exists(b):
517 if b and vfs.exists(b):
516 try:
518 try:
517 vfs.unlink(b)
519 vfs.unlink(b)
518 except (IOError, OSError, error.Abort) as inst:
520 except (IOError, OSError, error.Abort) as inst:
519 if not c:
521 if not c:
520 raise
522 raise
521 # Abort may be raise by read only opener
523 # Abort may be raise by read only opener
522 self._report("couldn't remove %s: %s\n"
524 self._report("couldn't remove %s: %s\n"
523 % (vfs.join(b), inst))
525 % (vfs.join(b), inst))
524 self._backupentries = []
526 self._backupentries = []
525 self._journal = None
527 self._journal = None
526
528
527 self._releasefn(self, True) # notify success of closing transaction
529 self._releasefn(self, True) # notify success of closing transaction
528 self._releasefn = None # Help prevent cycles.
530 self._releasefn = None # Help prevent cycles.
529
531
530 # run post close action
532 # run post close action
531 categories = sorted(self._postclosecallback)
533 categories = sorted(self._postclosecallback)
532 for cat in categories:
534 for cat in categories:
533 self._postclosecallback[cat](self)
535 self._postclosecallback[cat](self)
534 # Prevent double usage and help clear cycles.
536 # Prevent double usage and help clear cycles.
535 self._postclosecallback = None
537 self._postclosecallback = None
536
538
537 @active
539 @active
538 def abort(self):
540 def abort(self):
539 '''abort the transaction (generally called on error, or when the
541 '''abort the transaction (generally called on error, or when the
540 transaction is not explicitly committed before going out of
542 transaction is not explicitly committed before going out of
541 scope)'''
543 scope)'''
542 self._abort()
544 self._abort()
543
545
544 def _writeundo(self):
546 def _writeundo(self):
545 """write transaction data for possible future undo call"""
547 """write transaction data for possible future undo call"""
546 if self._undoname is None:
548 if self._undoname is None:
547 return
549 return
548 undobackupfile = self._opener.open("%s.backupfiles" % self._undoname,
550 undobackupfile = self._opener.open("%s.backupfiles" % self._undoname,
549 'w')
551 'w')
550 undobackupfile.write('%d\n' % version)
552 undobackupfile.write('%d\n' % version)
551 for l, f, b, c in self._backupentries:
553 for l, f, b, c in self._backupentries:
552 if not f: # temporary file
554 if not f: # temporary file
553 continue
555 continue
554 if not b:
556 if not b:
555 u = ''
557 u = ''
556 else:
558 else:
557 if l not in self._vfsmap and c:
559 if l not in self._vfsmap and c:
558 self._report("couldn't remove %s: unknown cache location"
560 self._report("couldn't remove %s: unknown cache location"
559 "%s\n" % (b, l))
561 "%s\n" % (b, l))
560 continue
562 continue
561 vfs = self._vfsmap[l]
563 vfs = self._vfsmap[l]
562 base, name = vfs.split(b)
564 base, name = vfs.split(b)
563 assert name.startswith(self._journal), name
565 assert name.startswith(self._journal), name
564 uname = name.replace(self._journal, self._undoname, 1)
566 uname = name.replace(self._journal, self._undoname, 1)
565 u = vfs.reljoin(base, uname)
567 u = vfs.reljoin(base, uname)
566 util.copyfile(vfs.join(b), vfs.join(u), hardlink=True)
568 util.copyfile(vfs.join(b), vfs.join(u), hardlink=True)
567 undobackupfile.write("%s\0%s\0%s\0%d\n" % (l, f, u, c))
569 undobackupfile.write("%s\0%s\0%s\0%d\n" % (l, f, u, c))
568 undobackupfile.close()
570 undobackupfile.close()
569
571
570
572
571 def _abort(self):
573 def _abort(self):
572 self._count = 0
574 self._count = 0
573 self._usages = 0
575 self._usages = 0
574 self._file.close()
576 self._file.close()
575 self._backupsfile.close()
577 self._backupsfile.close()
576
578
577 try:
579 try:
578 if not self._entries and not self._backupentries:
580 if not self._entries and not self._backupentries:
579 if self._backupjournal:
581 if self._backupjournal:
580 self._opener.unlink(self._backupjournal)
582 self._opener.unlink(self._backupjournal)
581 if self._journal:
583 if self._journal:
582 self._opener.unlink(self._journal)
584 self._opener.unlink(self._journal)
583 return
585 return
584
586
585 self._report(_("transaction abort!\n"))
587 self._report(_("transaction abort!\n"))
586
588
587 try:
589 try:
588 for cat in sorted(self._abortcallback):
590 for cat in sorted(self._abortcallback):
589 self._abortcallback[cat](self)
591 self._abortcallback[cat](self)
590 # Prevent double usage and help clear cycles.
592 # Prevent double usage and help clear cycles.
591 self._abortcallback = None
593 self._abortcallback = None
592 _playback(self._journal, self._report, self._opener,
594 _playback(self._journal, self._report, self._opener,
593 self._vfsmap, self._entries, self._backupentries,
595 self._vfsmap, self._entries, self._backupentries,
594 False, checkambigfiles=self._checkambigfiles)
596 False, checkambigfiles=self._checkambigfiles)
595 self._report(_("rollback completed\n"))
597 self._report(_("rollback completed\n"))
596 except BaseException as exc:
598 except BaseException as exc:
597 self._report(_("rollback failed - please run hg recover\n"))
599 self._report(_("rollback failed - please run hg recover\n"))
598 self._report(_("(failure reason: %s)\n")
600 self._report(_("(failure reason: %s)\n")
599 % stringutil.forcebytestr(exc))
601 % stringutil.forcebytestr(exc))
600 finally:
602 finally:
601 self._journal = None
603 self._journal = None
602 self._releasefn(self, False) # notify failure of transaction
604 self._releasefn(self, False) # notify failure of transaction
603 self._releasefn = None # Help prevent cycles.
605 self._releasefn = None # Help prevent cycles.
604
606
605 def rollback(opener, vfsmap, file, report, checkambigfiles=None):
607 def rollback(opener, vfsmap, file, report, checkambigfiles=None):
606 """Rolls back the transaction contained in the given file
608 """Rolls back the transaction contained in the given file
607
609
608 Reads the entries in the specified file, and the corresponding
610 Reads the entries in the specified file, and the corresponding
609 '*.backupfiles' file, to recover from an incomplete transaction.
611 '*.backupfiles' file, to recover from an incomplete transaction.
610
612
611 * `file`: a file containing a list of entries, specifying where
613 * `file`: a file containing a list of entries, specifying where
612 to truncate each file. The file should contain a list of
614 to truncate each file. The file should contain a list of
613 file\0offset pairs, delimited by newlines. The corresponding
615 file\0offset pairs, delimited by newlines. The corresponding
614 '*.backupfiles' file should contain a list of file\0backupfile
616 '*.backupfiles' file should contain a list of file\0backupfile
615 pairs, delimited by \0.
617 pairs, delimited by \0.
616
618
617 `checkambigfiles` is a set of (path, vfs-location) tuples,
619 `checkambigfiles` is a set of (path, vfs-location) tuples,
618 which determine whether file stat ambiguity should be avoided at
620 which determine whether file stat ambiguity should be avoided at
619 restoring corresponded files.
621 restoring corresponded files.
620 """
622 """
621 entries = []
623 entries = []
622 backupentries = []
624 backupentries = []
623
625
624 fp = opener.open(file)
626 fp = opener.open(file)
625 lines = fp.readlines()
627 lines = fp.readlines()
626 fp.close()
628 fp.close()
627 for l in lines:
629 for l in lines:
628 try:
630 try:
629 f, o = l.split('\0')
631 f, o = l.split('\0')
630 entries.append((f, int(o), None))
632 entries.append((f, int(o), None))
631 except ValueError:
633 except ValueError:
632 report(
634 report(
633 _("couldn't read journal entry %r!\n") % pycompat.bytestr(l))
635 _("couldn't read journal entry %r!\n") % pycompat.bytestr(l))
634
636
635 backupjournal = "%s.backupfiles" % file
637 backupjournal = "%s.backupfiles" % file
636 if opener.exists(backupjournal):
638 if opener.exists(backupjournal):
637 fp = opener.open(backupjournal)
639 fp = opener.open(backupjournal)
638 lines = fp.readlines()
640 lines = fp.readlines()
639 if lines:
641 if lines:
640 ver = lines[0][:-1]
642 ver = lines[0][:-1]
641 if ver == (b'%d' % version):
643 if ver == (b'%d' % version):
642 for line in lines[1:]:
644 for line in lines[1:]:
643 if line:
645 if line:
644 # Shave off the trailing newline
646 # Shave off the trailing newline
645 line = line[:-1]
647 line = line[:-1]
646 l, f, b, c = line.split('\0')
648 l, f, b, c = line.split('\0')
647 backupentries.append((l, f, b, bool(c)))
649 backupentries.append((l, f, b, bool(c)))
648 else:
650 else:
649 report(_("journal was created by a different version of "
651 report(_("journal was created by a different version of "
650 "Mercurial\n"))
652 "Mercurial\n"))
651
653
652 _playback(file, report, opener, vfsmap, entries, backupentries,
654 _playback(file, report, opener, vfsmap, entries, backupentries,
653 checkambigfiles=checkambigfiles)
655 checkambigfiles=checkambigfiles)
General Comments 0
You need to be logged in to leave comments. Login now