|
1 # changelog.py - changelog class for mercurial |
|
2 # |
|
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com> |
|
4 # |
|
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. |
|
7 |
|
8 from node import bin, hex, nullid |
|
9 from i18n import _ |
|
10 import util, error, revlog, encoding |
|
11 |
|
12 def _string_escape(text): |
|
13 """ |
|
14 >>> d = {'nl': chr(10), 'bs': chr(92), 'cr': chr(13), 'nul': chr(0)} |
|
15 >>> s = "ab%(nl)scd%(bs)s%(bs)sn%(nul)sab%(cr)scd%(bs)s%(nl)s" % d |
|
16 >>> s |
|
17 'ab\\ncd\\\\\\\\n\\x00ab\\rcd\\\\\\n' |
|
18 >>> res = _string_escape(s) |
|
19 >>> s == res.decode('string_escape') |
|
20 True |
|
21 """ |
|
22 # subset of the string_escape codec |
|
23 text = text.replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r') |
|
24 return text.replace('\0', '\\0') |
|
25 |
|
26 def decodeextra(text): |
|
27 extra = {} |
|
28 for l in text.split('\0'): |
|
29 if l: |
|
30 k, v = l.decode('string_escape').split(':', 1) |
|
31 extra[k] = v |
|
32 return extra |
|
33 |
|
34 def encodeextra(d): |
|
35 # keys must be sorted to produce a deterministic changelog entry |
|
36 items = [_string_escape('%s:%s' % (k, d[k])) for k in sorted(d)] |
|
37 return "\0".join(items) |
|
38 |
|
39 class appender(object): |
|
40 '''the changelog index must be updated last on disk, so we use this class |
|
41 to delay writes to it''' |
|
42 def __init__(self, fp, buf): |
|
43 self.data = buf |
|
44 self.fp = fp |
|
45 self.offset = fp.tell() |
|
46 self.size = util.fstat(fp).st_size |
|
47 |
|
48 def end(self): |
|
49 return self.size + len("".join(self.data)) |
|
50 def tell(self): |
|
51 return self.offset |
|
52 def flush(self): |
|
53 pass |
|
54 def close(self): |
|
55 self.fp.close() |
|
56 |
|
57 def seek(self, offset, whence=0): |
|
58 '''virtual file offset spans real file and data''' |
|
59 if whence == 0: |
|
60 self.offset = offset |
|
61 elif whence == 1: |
|
62 self.offset += offset |
|
63 elif whence == 2: |
|
64 self.offset = self.end() + offset |
|
65 if self.offset < self.size: |
|
66 self.fp.seek(self.offset) |
|
67 |
|
68 def read(self, count=-1): |
|
69 '''only trick here is reads that span real file and data''' |
|
70 ret = "" |
|
71 if self.offset < self.size: |
|
72 s = self.fp.read(count) |
|
73 ret = s |
|
74 self.offset += len(s) |
|
75 if count > 0: |
|
76 count -= len(s) |
|
77 if count != 0: |
|
78 doff = self.offset - self.size |
|
79 self.data.insert(0, "".join(self.data)) |
|
80 del self.data[1:] |
|
81 s = self.data[0][doff:doff + count] |
|
82 self.offset += len(s) |
|
83 ret += s |
|
84 return ret |
|
85 |
|
86 def write(self, s): |
|
87 self.data.append(str(s)) |
|
88 self.offset += len(s) |
|
89 |
|
90 def delayopener(opener, target, divert, buf): |
|
91 def o(name, mode='r'): |
|
92 if name != target: |
|
93 return opener(name, mode) |
|
94 if divert: |
|
95 return opener(name + ".a", mode.replace('a', 'w')) |
|
96 # otherwise, divert to memory |
|
97 return appender(opener(name, mode), buf) |
|
98 return o |
|
99 |
|
100 class changelog(revlog.revlog): |
|
101 def __init__(self, opener): |
|
102 revlog.revlog.__init__(self, opener, "00changelog.i") |
|
103 self._realopener = opener |
|
104 self._delayed = False |
|
105 self._divert = False |
|
106 |
|
107 def delayupdate(self): |
|
108 "delay visibility of index updates to other readers" |
|
109 self._delayed = True |
|
110 self._divert = (len(self) == 0) |
|
111 self._delaybuf = [] |
|
112 self.opener = delayopener(self._realopener, self.indexfile, |
|
113 self._divert, self._delaybuf) |
|
114 |
|
115 def finalize(self, tr): |
|
116 "finalize index updates" |
|
117 self._delayed = False |
|
118 self.opener = self._realopener |
|
119 # move redirected index data back into place |
|
120 if self._divert: |
|
121 n = self.opener(self.indexfile + ".a").name |
|
122 util.rename(n, n[:-2]) |
|
123 elif self._delaybuf: |
|
124 fp = self.opener(self.indexfile, 'a') |
|
125 fp.write("".join(self._delaybuf)) |
|
126 fp.close() |
|
127 self._delaybuf = [] |
|
128 # split when we're done |
|
129 self.checkinlinesize(tr) |
|
130 |
|
131 def readpending(self, file): |
|
132 r = revlog.revlog(self.opener, file) |
|
133 self.index = r.index |
|
134 self.nodemap = r.nodemap |
|
135 self._chunkcache = r._chunkcache |
|
136 |
|
137 def writepending(self): |
|
138 "create a file containing the unfinalized state for pretxnchangegroup" |
|
139 if self._delaybuf: |
|
140 # make a temporary copy of the index |
|
141 fp1 = self._realopener(self.indexfile) |
|
142 fp2 = self._realopener(self.indexfile + ".a", "w") |
|
143 fp2.write(fp1.read()) |
|
144 # add pending data |
|
145 fp2.write("".join(self._delaybuf)) |
|
146 fp2.close() |
|
147 # switch modes so finalize can simply rename |
|
148 self._delaybuf = [] |
|
149 self._divert = True |
|
150 |
|
151 if self._divert: |
|
152 return True |
|
153 |
|
154 return False |
|
155 |
|
156 def checkinlinesize(self, tr, fp=None): |
|
157 if not self._delayed: |
|
158 revlog.revlog.checkinlinesize(self, tr, fp) |
|
159 |
|
160 def read(self, node): |
|
161 """ |
|
162 format used: |
|
163 nodeid\n : manifest node in ascii |
|
164 user\n : user, no \n or \r allowed |
|
165 time tz extra\n : date (time is int or float, timezone is int) |
|
166 : extra is metadatas, encoded and separated by '\0' |
|
167 : older versions ignore it |
|
168 files\n\n : files modified by the cset, no \n or \r allowed |
|
169 (.*) : comment (free text, ideally utf-8) |
|
170 |
|
171 changelog v0 doesn't use extra |
|
172 """ |
|
173 text = self.revision(node) |
|
174 if not text: |
|
175 return (nullid, "", (0, 0), [], "", {'branch': 'default'}) |
|
176 last = text.index("\n\n") |
|
177 desc = encoding.tolocal(text[last + 2:]) |
|
178 l = text[:last].split('\n') |
|
179 manifest = bin(l[0]) |
|
180 user = encoding.tolocal(l[1]) |
|
181 |
|
182 extra_data = l[2].split(' ', 2) |
|
183 if len(extra_data) != 3: |
|
184 time = float(extra_data.pop(0)) |
|
185 try: |
|
186 # various tools did silly things with the time zone field. |
|
187 timezone = int(extra_data[0]) |
|
188 except: |
|
189 timezone = 0 |
|
190 extra = {} |
|
191 else: |
|
192 time, timezone, extra = extra_data |
|
193 time, timezone = float(time), int(timezone) |
|
194 extra = decodeextra(extra) |
|
195 if not extra.get('branch'): |
|
196 extra['branch'] = 'default' |
|
197 files = l[3:] |
|
198 return (manifest, user, (time, timezone), files, desc, extra) |
|
199 |
|
200 def add(self, manifest, files, desc, transaction, p1, p2, |
|
201 user, date=None, extra=None): |
|
202 user = user.strip() |
|
203 # An empty username or a username with a "\n" will make the |
|
204 # revision text contain two "\n\n" sequences -> corrupt |
|
205 # repository since read cannot unpack the revision. |
|
206 if not user: |
|
207 raise error.RevlogError(_("empty username")) |
|
208 if "\n" in user: |
|
209 raise error.RevlogError(_("username %s contains a newline") |
|
210 % repr(user)) |
|
211 |
|
212 # strip trailing whitespace and leading and trailing empty lines |
|
213 desc = '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n') |
|
214 |
|
215 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc) |
|
216 |
|
217 if date: |
|
218 parseddate = "%d %d" % util.parsedate(date) |
|
219 else: |
|
220 parseddate = "%d %d" % util.makedate() |
|
221 if extra: |
|
222 branch = extra.get("branch") |
|
223 if branch in ("default", ""): |
|
224 del extra["branch"] |
|
225 elif branch in (".", "null", "tip"): |
|
226 raise error.RevlogError(_('the name \'%s\' is reserved') |
|
227 % branch) |
|
228 if extra: |
|
229 extra = encodeextra(extra) |
|
230 parseddate = "%s %s" % (parseddate, extra) |
|
231 l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc] |
|
232 text = "\n".join(l) |
|
233 return self.addrevision(text, transaction, len(self), p1, p2) |