121
|
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, incorporated herein by reference.
|
|
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 class changelog(revlog.revlog):
|
|
91 def __init__(self, opener):
|
|
92 self._realopener = opener
|
|
93 self._delayed = False
|
|
94 revlog.revlog.__init__(self, self._delayopener, "00changelog.i")
|
|
95
|
|
96 def delayupdate(self):
|
|
97 "delay visibility of index updates to other readers"
|
|
98 self._delayed = True
|
|
99 self._delaycount = len(self)
|
|
100 self._delaybuf = []
|
|
101 self._delayname = None
|
|
102
|
|
103 def finalize(self, tr):
|
|
104 "finalize index updates"
|
|
105 self._delayed = False
|
|
106 # move redirected index data back into place
|
|
107 if self._delayname:
|
|
108 util.rename(self._delayname + ".a", self._delayname)
|
|
109 elif self._delaybuf:
|
|
110 fp = self.opener(self.indexfile, 'a')
|
|
111 fp.write("".join(self._delaybuf))
|
|
112 fp.close()
|
|
113 self._delaybuf = []
|
|
114 # split when we're done
|
|
115 self.checkinlinesize(tr)
|
|
116
|
|
117 def _delayopener(self, name, mode='r'):
|
|
118 fp = self._realopener(name, mode)
|
|
119 # only divert the index
|
|
120 if not self._delayed or not name == self.indexfile:
|
|
121 return fp
|
|
122 # if we're doing an initial clone, divert to another file
|
|
123 if self._delaycount == 0:
|
|
124 self._delayname = fp.name
|
|
125 if not len(self):
|
|
126 # make sure to truncate the file
|
|
127 mode = mode.replace('a', 'w')
|
|
128 return self._realopener(name + ".a", mode)
|
|
129 # otherwise, divert to memory
|
|
130 return appender(fp, self._delaybuf)
|
|
131
|
|
132 def readpending(self, file):
|
|
133 r = revlog.revlog(self.opener, file)
|
|
134 self.index = r.index
|
|
135 self.nodemap = r.nodemap
|
|
136 self._chunkcache = r._chunkcache
|
|
137
|
|
138 def writepending(self):
|
|
139 "create a file containing the unfinalized state for pretxnchangegroup"
|
|
140 if self._delaybuf:
|
|
141 # make a temporary copy of the index
|
|
142 fp1 = self._realopener(self.indexfile)
|
|
143 fp2 = self._realopener(self.indexfile + ".a", "w")
|
|
144 fp2.write(fp1.read())
|
|
145 # add pending data
|
|
146 fp2.write("".join(self._delaybuf))
|
|
147 fp2.close()
|
|
148 # switch modes so finalize can simply rename
|
|
149 self._delaybuf = []
|
|
150 self._delayname = fp1.name
|
|
151
|
|
152 if self._delayname:
|
|
153 return True
|
|
154
|
|
155 return False
|
|
156
|
|
157 def checkinlinesize(self, tr, fp=None):
|
|
158 if self.opener == self._delayopener:
|
|
159 return
|
|
160 return revlog.revlog.checkinlinesize(self, tr, fp)
|
|
161
|
|
162 def read(self, node):
|
|
163 """
|
|
164 format used:
|
|
165 nodeid\n : manifest node in ascii
|
|
166 user\n : user, no \n or \r allowed
|
|
167 time tz extra\n : date (time is int or float, timezone is int)
|
|
168 : extra is metadatas, encoded and separated by '\0'
|
|
169 : older versions ignore it
|
|
170 files\n\n : files modified by the cset, no \n or \r allowed
|
|
171 (.*) : comment (free text, ideally utf-8)
|
|
172
|
|
173 changelog v0 doesn't use extra
|
|
174 """
|
|
175 text = self.revision(node)
|
|
176 if not text:
|
|
177 return (nullid, "", (0, 0), [], "", {'branch': 'default'})
|
|
178 last = text.index("\n\n")
|
|
179 desc = encoding.tolocal(text[last + 2:])
|
|
180 l = text[:last].split('\n')
|
|
181 manifest = bin(l[0])
|
|
182 user = encoding.tolocal(l[1])
|
|
183
|
|
184 extra_data = l[2].split(' ', 2)
|
|
185 if len(extra_data) != 3:
|
|
186 time = float(extra_data.pop(0))
|
|
187 try:
|
|
188 # various tools did silly things with the time zone field.
|
|
189 timezone = int(extra_data[0])
|
|
190 except:
|
|
191 timezone = 0
|
|
192 extra = {}
|
|
193 else:
|
|
194 time, timezone, extra = extra_data
|
|
195 time, timezone = float(time), int(timezone)
|
|
196 extra = decodeextra(extra)
|
|
197 if not extra.get('branch'):
|
|
198 extra['branch'] = 'default'
|
|
199 files = l[3:]
|
|
200 return (manifest, user, (time, timezone), files, desc, extra)
|
|
201
|
|
202 def add(self, manifest, files, desc, transaction, p1, p2,
|
|
203 user, date=None, extra={}):
|
|
204 user = user.strip()
|
|
205 # An empty username or a username with a "\n" will make the
|
|
206 # revision text contain two "\n\n" sequences -> corrupt
|
|
207 # repository since read cannot unpack the revision.
|
|
208 if not user:
|
|
209 raise error.RevlogError(_("empty username"))
|
|
210 if "\n" in user:
|
|
211 raise error.RevlogError(_("username %s contains a newline")
|
|
212 % repr(user))
|
|
213
|
|
214 # strip trailing whitespace and leading and trailing empty lines
|
|
215 desc = '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n')
|
|
216
|
|
217 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
|
|
218
|
|
219 if date:
|
|
220 parseddate = "%d %d" % util.parsedate(date)
|
|
221 else:
|
|
222 parseddate = "%d %d" % util.makedate()
|
|
223 if extra and extra.get("branch") in ("default", ""):
|
|
224 del extra["branch"]
|
|
225 if extra:
|
|
226 extra = encodeextra(extra)
|
|
227 parseddate = "%s %s" % (parseddate, extra)
|
|
228 l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc]
|
|
229 text = "\n".join(l)
|
|
230 return self.addrevision(text, transaction, len(self), p1, p2)
|