135
|
1 # transaction.py - simple journalling scheme for mercurial
|
|
2 #
|
|
3 # This transaction scheme is intended to gracefully handle program
|
|
4 # errors and interruptions. More serious failures like system crashes
|
|
5 # can be recovered with an fsck-like tool. As the whole repository is
|
|
6 # effectively log-structured, this should amount to simply truncating
|
|
7 # anything that isn't referenced in the changelog.
|
|
8 #
|
|
9 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
|
|
10 #
|
|
11 # This software may be used and distributed according to the terms of the
|
|
12 # GNU General Public License version 2, incorporated herein by reference.
|
|
13
|
|
14 from i18n import _
|
|
15 import os, errno
|
|
16 import error
|
|
17
|
|
18 def active(func):
|
|
19 def _active(self, *args, **kwds):
|
|
20 if self.count == 0:
|
|
21 raise error.Abort(_(
|
|
22 'cannot use transaction when it is already committed/aborted'))
|
|
23 return func(self, *args, **kwds)
|
|
24 return _active
|
|
25
|
|
26 def _playback(journal, report, opener, entries, unlink=True):
|
|
27 for f, o, ignore in entries:
|
|
28 if o or not unlink:
|
|
29 try:
|
|
30 opener(f, 'a').truncate(o)
|
|
31 except:
|
|
32 report(_("failed to truncate %s\n") % f)
|
|
33 raise
|
|
34 else:
|
|
35 try:
|
|
36 fn = opener(f).name
|
|
37 os.unlink(fn)
|
|
38 except IOError, inst:
|
|
39 if inst.errno != errno.ENOENT:
|
|
40 raise
|
|
41 os.unlink(journal)
|
|
42
|
|
43 class transaction(object):
|
|
44 def __init__(self, report, opener, journal, after=None, createmode=None):
|
|
45 self.journal = None
|
|
46
|
|
47 self.count = 1
|
|
48 self.report = report
|
|
49 self.opener = opener
|
|
50 self.after = after
|
|
51 self.entries = []
|
|
52 self.map = {}
|
|
53 self.journal = journal
|
|
54 self._queue = []
|
|
55
|
|
56 self.file = open(self.journal, "w")
|
|
57 if createmode is not None:
|
|
58 os.chmod(self.journal, createmode & 0666)
|
|
59
|
|
60 def __del__(self):
|
|
61 if self.journal:
|
|
62 if self.entries: self._abort()
|
|
63 self.file.close()
|
|
64
|
|
65 @active
|
|
66 def startgroup(self):
|
|
67 self._queue.append([])
|
|
68
|
|
69 @active
|
|
70 def endgroup(self):
|
|
71 q = self._queue.pop()
|
|
72 d = ''.join(['%s\0%d\n' % (x[0], x[1]) for x in q])
|
|
73 self.entries.extend(q)
|
|
74 self.file.write(d)
|
|
75 self.file.flush()
|
|
76
|
|
77 @active
|
|
78 def add(self, file, offset, data=None):
|
|
79 if file in self.map: return
|
|
80
|
|
81 if self._queue:
|
|
82 self._queue[-1].append((file, offset, data))
|
|
83 return
|
|
84
|
|
85 self.entries.append((file, offset, data))
|
|
86 self.map[file] = len(self.entries) - 1
|
|
87 # add enough data to the journal to do the truncate
|
|
88 self.file.write("%s\0%d\n" % (file, offset))
|
|
89 self.file.flush()
|
|
90
|
|
91 @active
|
|
92 def find(self, file):
|
|
93 if file in self.map:
|
|
94 return self.entries[self.map[file]]
|
|
95 return None
|
|
96
|
|
97 @active
|
|
98 def replace(self, file, offset, data=None):
|
|
99 '''
|
|
100 replace can only replace already committed entries
|
|
101 that are not pending in the queue
|
|
102 '''
|
|
103
|
|
104 if file not in self.map:
|
|
105 raise KeyError(file)
|
|
106 index = self.map[file]
|
|
107 self.entries[index] = (file, offset, data)
|
|
108 self.file.write("%s\0%d\n" % (file, offset))
|
|
109 self.file.flush()
|
|
110
|
|
111 @active
|
|
112 def nest(self):
|
|
113 self.count += 1
|
|
114 return self
|
|
115
|
|
116 def running(self):
|
|
117 return self.count > 0
|
|
118
|
|
119 @active
|
|
120 def close(self):
|
|
121 self.count -= 1
|
|
122 if self.count != 0:
|
|
123 return
|
|
124 self.file.close()
|
|
125 self.entries = []
|
|
126 if self.after:
|
|
127 self.after()
|
|
128 else:
|
|
129 os.unlink(self.journal)
|
|
130 self.journal = None
|
|
131
|
|
132 @active
|
|
133 def abort(self):
|
|
134 self._abort()
|
|
135
|
|
136 def _abort(self):
|
|
137 self.count = 0
|
|
138 self.file.close()
|
|
139
|
|
140 if not self.entries: return
|
|
141
|
|
142 self.report(_("transaction abort!\n"))
|
|
143
|
|
144 try:
|
|
145 try:
|
|
146 _playback(self.journal, self.report, self.opener, self.entries, False)
|
|
147 self.report(_("rollback completed\n"))
|
|
148 except:
|
|
149 self.report(_("rollback failed - please run hg recover\n"))
|
|
150 finally:
|
|
151 self.journal = None
|
|
152
|
|
153
|
|
154 def rollback(opener, file, report):
|
|
155 entries = []
|
|
156
|
|
157 for l in open(file).readlines():
|
|
158 f, o = l.split('\0')
|
|
159 entries.append((f, int(o), None))
|
|
160
|
|
161 _playback(file, report, opener, entries)
|