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