##// END OF EJS Templates
changelog: refuse to add revisions with empty usernames...
Martin Geisler -
r8424:c5b3d3e3 default
parent child Browse files
Show More
@@ -1,221 +1,226
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
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
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
10 import util, error, revlog
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 class appender:
26 class appender:
27 '''the changelog index must be updated last on disk, so we use this class
27 '''the changelog index must be updated last on disk, so we use this class
28 to delay writes to it'''
28 to delay writes to it'''
29 def __init__(self, fp, buf):
29 def __init__(self, fp, buf):
30 self.data = buf
30 self.data = buf
31 self.fp = fp
31 self.fp = fp
32 self.offset = fp.tell()
32 self.offset = fp.tell()
33 self.size = util.fstat(fp).st_size
33 self.size = util.fstat(fp).st_size
34
34
35 def end(self):
35 def end(self):
36 return self.size + len("".join(self.data))
36 return self.size + len("".join(self.data))
37 def tell(self):
37 def tell(self):
38 return self.offset
38 return self.offset
39 def flush(self):
39 def flush(self):
40 pass
40 pass
41 def close(self):
41 def close(self):
42 self.fp.close()
42 self.fp.close()
43
43
44 def seek(self, offset, whence=0):
44 def seek(self, offset, whence=0):
45 '''virtual file offset spans real file and data'''
45 '''virtual file offset spans real file and data'''
46 if whence == 0:
46 if whence == 0:
47 self.offset = offset
47 self.offset = offset
48 elif whence == 1:
48 elif whence == 1:
49 self.offset += offset
49 self.offset += offset
50 elif whence == 2:
50 elif whence == 2:
51 self.offset = self.end() + offset
51 self.offset = self.end() + offset
52 if self.offset < self.size:
52 if self.offset < self.size:
53 self.fp.seek(self.offset)
53 self.fp.seek(self.offset)
54
54
55 def read(self, count=-1):
55 def read(self, count=-1):
56 '''only trick here is reads that span real file and data'''
56 '''only trick here is reads that span real file and data'''
57 ret = ""
57 ret = ""
58 if self.offset < self.size:
58 if self.offset < self.size:
59 s = self.fp.read(count)
59 s = self.fp.read(count)
60 ret = s
60 ret = s
61 self.offset += len(s)
61 self.offset += len(s)
62 if count > 0:
62 if count > 0:
63 count -= len(s)
63 count -= len(s)
64 if count != 0:
64 if count != 0:
65 doff = self.offset - self.size
65 doff = self.offset - self.size
66 self.data.insert(0, "".join(self.data))
66 self.data.insert(0, "".join(self.data))
67 del self.data[1:]
67 del self.data[1:]
68 s = self.data[0][doff:doff+count]
68 s = self.data[0][doff:doff+count]
69 self.offset += len(s)
69 self.offset += len(s)
70 ret += s
70 ret += s
71 return ret
71 return ret
72
72
73 def write(self, s):
73 def write(self, s):
74 self.data.append(str(s))
74 self.data.append(str(s))
75 self.offset += len(s)
75 self.offset += len(s)
76
76
77 class changelog(revlog.revlog):
77 class changelog(revlog.revlog):
78 def __init__(self, opener):
78 def __init__(self, opener):
79 revlog.revlog.__init__(self, opener, "00changelog.i")
79 revlog.revlog.__init__(self, opener, "00changelog.i")
80
80
81 def delayupdate(self):
81 def delayupdate(self):
82 "delay visibility of index updates to other readers"
82 "delay visibility of index updates to other readers"
83 self._realopener = self.opener
83 self._realopener = self.opener
84 self.opener = self._delayopener
84 self.opener = self._delayopener
85 self._delaycount = len(self)
85 self._delaycount = len(self)
86 self._delaybuf = []
86 self._delaybuf = []
87 self._delayname = None
87 self._delayname = None
88
88
89 def finalize(self, tr):
89 def finalize(self, tr):
90 "finalize index updates"
90 "finalize index updates"
91 self.opener = self._realopener
91 self.opener = self._realopener
92 # move redirected index data back into place
92 # move redirected index data back into place
93 if self._delayname:
93 if self._delayname:
94 util.rename(self._delayname + ".a", self._delayname)
94 util.rename(self._delayname + ".a", self._delayname)
95 elif self._delaybuf:
95 elif self._delaybuf:
96 fp = self.opener(self.indexfile, 'a')
96 fp = self.opener(self.indexfile, 'a')
97 fp.write("".join(self._delaybuf))
97 fp.write("".join(self._delaybuf))
98 fp.close()
98 fp.close()
99 self._delaybuf = []
99 self._delaybuf = []
100 # split when we're done
100 # split when we're done
101 self.checkinlinesize(tr)
101 self.checkinlinesize(tr)
102
102
103 def _delayopener(self, name, mode='r'):
103 def _delayopener(self, name, mode='r'):
104 fp = self._realopener(name, mode)
104 fp = self._realopener(name, mode)
105 # only divert the index
105 # only divert the index
106 if not name == self.indexfile:
106 if not name == self.indexfile:
107 return fp
107 return fp
108 # if we're doing an initial clone, divert to another file
108 # if we're doing an initial clone, divert to another file
109 if self._delaycount == 0:
109 if self._delaycount == 0:
110 self._delayname = fp.name
110 self._delayname = fp.name
111 if not len(self):
111 if not len(self):
112 # make sure to truncate the file
112 # make sure to truncate the file
113 mode = mode.replace('a', 'w')
113 mode = mode.replace('a', 'w')
114 return self._realopener(name + ".a", mode)
114 return self._realopener(name + ".a", mode)
115 # otherwise, divert to memory
115 # otherwise, divert to memory
116 return appender(fp, self._delaybuf)
116 return appender(fp, self._delaybuf)
117
117
118 def readpending(self, file):
118 def readpending(self, file):
119 r = revlog.revlog(self.opener, file)
119 r = revlog.revlog(self.opener, file)
120 self.index = r.index
120 self.index = r.index
121 self.nodemap = r.nodemap
121 self.nodemap = r.nodemap
122 self._chunkcache = r._chunkcache
122 self._chunkcache = r._chunkcache
123
123
124 def writepending(self):
124 def writepending(self):
125 "create a file containing the unfinalized state for pretxnchangegroup"
125 "create a file containing the unfinalized state for pretxnchangegroup"
126 if self._delaybuf:
126 if self._delaybuf:
127 # make a temporary copy of the index
127 # make a temporary copy of the index
128 fp1 = self._realopener(self.indexfile)
128 fp1 = self._realopener(self.indexfile)
129 fp2 = self._realopener(self.indexfile + ".a", "w")
129 fp2 = self._realopener(self.indexfile + ".a", "w")
130 fp2.write(fp1.read())
130 fp2.write(fp1.read())
131 # add pending data
131 # add pending data
132 fp2.write("".join(self._delaybuf))
132 fp2.write("".join(self._delaybuf))
133 fp2.close()
133 fp2.close()
134 # switch modes so finalize can simply rename
134 # switch modes so finalize can simply rename
135 self._delaybuf = []
135 self._delaybuf = []
136 self._delayname = fp1.name
136 self._delayname = fp1.name
137
137
138 if self._delayname:
138 if self._delayname:
139 return True
139 return True
140
140
141 return False
141 return False
142
142
143 def checkinlinesize(self, tr, fp=None):
143 def checkinlinesize(self, tr, fp=None):
144 if self.opener == self._delayopener:
144 if self.opener == self._delayopener:
145 return
145 return
146 return revlog.revlog.checkinlinesize(self, tr, fp)
146 return revlog.revlog.checkinlinesize(self, tr, fp)
147
147
148 def decode_extra(self, text):
148 def decode_extra(self, text):
149 extra = {}
149 extra = {}
150 for l in text.split('\0'):
150 for l in text.split('\0'):
151 if l:
151 if l:
152 k, v = l.decode('string_escape').split(':', 1)
152 k, v = l.decode('string_escape').split(':', 1)
153 extra[k] = v
153 extra[k] = v
154 return extra
154 return extra
155
155
156 def encode_extra(self, d):
156 def encode_extra(self, d):
157 # keys must be sorted to produce a deterministic changelog entry
157 # keys must be sorted to produce a deterministic changelog entry
158 items = [_string_escape('%s:%s' % (k, d[k])) for k in util.sort(d)]
158 items = [_string_escape('%s:%s' % (k, d[k])) for k in util.sort(d)]
159 return "\0".join(items)
159 return "\0".join(items)
160
160
161 def read(self, node):
161 def read(self, node):
162 """
162 """
163 format used:
163 format used:
164 nodeid\n : manifest node in ascii
164 nodeid\n : manifest node in ascii
165 user\n : user, no \n or \r allowed
165 user\n : user, no \n or \r allowed
166 time tz extra\n : date (time is int or float, timezone is int)
166 time tz extra\n : date (time is int or float, timezone is int)
167 : extra is metadatas, encoded and separated by '\0'
167 : extra is metadatas, encoded and separated by '\0'
168 : older versions ignore it
168 : older versions ignore it
169 files\n\n : files modified by the cset, no \n or \r allowed
169 files\n\n : files modified by the cset, no \n or \r allowed
170 (.*) : comment (free text, ideally utf-8)
170 (.*) : comment (free text, ideally utf-8)
171
171
172 changelog v0 doesn't use extra
172 changelog v0 doesn't use extra
173 """
173 """
174 text = self.revision(node)
174 text = self.revision(node)
175 if not text:
175 if not text:
176 return (nullid, "", (0, 0), [], "", {'branch': 'default'})
176 return (nullid, "", (0, 0), [], "", {'branch': 'default'})
177 last = text.index("\n\n")
177 last = text.index("\n\n")
178 desc = util.tolocal(text[last + 2:])
178 desc = util.tolocal(text[last + 2:])
179 l = text[:last].split('\n')
179 l = text[:last].split('\n')
180 manifest = bin(l[0])
180 manifest = bin(l[0])
181 user = util.tolocal(l[1])
181 user = util.tolocal(l[1])
182
182
183 extra_data = l[2].split(' ', 2)
183 extra_data = l[2].split(' ', 2)
184 if len(extra_data) != 3:
184 if len(extra_data) != 3:
185 time = float(extra_data.pop(0))
185 time = float(extra_data.pop(0))
186 try:
186 try:
187 # various tools did silly things with the time zone field.
187 # various tools did silly things with the time zone field.
188 timezone = int(extra_data[0])
188 timezone = int(extra_data[0])
189 except:
189 except:
190 timezone = 0
190 timezone = 0
191 extra = {}
191 extra = {}
192 else:
192 else:
193 time, timezone, extra = extra_data
193 time, timezone, extra = extra_data
194 time, timezone = float(time), int(timezone)
194 time, timezone = float(time), int(timezone)
195 extra = self.decode_extra(extra)
195 extra = self.decode_extra(extra)
196 if not extra.get('branch'):
196 if not extra.get('branch'):
197 extra['branch'] = 'default'
197 extra['branch'] = 'default'
198 files = l[3:]
198 files = l[3:]
199 return (manifest, user, (time, timezone), files, desc, extra)
199 return (manifest, user, (time, timezone), files, desc, extra)
200
200
201 def add(self, manifest, files, desc, transaction, p1=None, p2=None,
201 def add(self, manifest, files, desc, transaction, p1=None, p2=None,
202 user=None, date=None, extra={}):
202 user=None, date=None, extra={}):
203
203
204 user = user.strip()
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"))
205 if "\n" in user:
210 if "\n" in user:
206 raise error.RevlogError(_("username %s contains a newline")
211 raise error.RevlogError(_("username %s contains a newline")
207 % repr(user))
212 % repr(user))
208 user, desc = util.fromlocal(user), util.fromlocal(desc)
213 user, desc = util.fromlocal(user), util.fromlocal(desc)
209
214
210 if date:
215 if date:
211 parseddate = "%d %d" % util.parsedate(date)
216 parseddate = "%d %d" % util.parsedate(date)
212 else:
217 else:
213 parseddate = "%d %d" % util.makedate()
218 parseddate = "%d %d" % util.makedate()
214 if extra and extra.get("branch") in ("default", ""):
219 if extra and extra.get("branch") in ("default", ""):
215 del extra["branch"]
220 del extra["branch"]
216 if extra:
221 if extra:
217 extra = self.encode_extra(extra)
222 extra = self.encode_extra(extra)
218 parseddate = "%s %s" % (parseddate, extra)
223 parseddate = "%s %s" % (parseddate, extra)
219 l = [hex(manifest), user, parseddate] + util.sort(files) + ["", desc]
224 l = [hex(manifest), user, parseddate] + util.sort(files) + ["", desc]
220 text = "\n".join(l)
225 text = "\n".join(l)
221 return self.addrevision(text, transaction, len(self), p1, p2)
226 return self.addrevision(text, transaction, len(self), p1, p2)
@@ -1,31 +1,36
1 #!/bin/sh
1 #!/bin/sh
2
2
3 unset HGUSER
3 unset HGUSER
4 EMAIL="My Name <myname@example.com>"
4 EMAIL="My Name <myname@example.com>"
5 export EMAIL
5 export EMAIL
6
6
7 hg init test
7 hg init test
8 cd test
8 cd test
9 touch asdf
9 touch asdf
10 hg add asdf
10 hg add asdf
11 hg commit -d '1000000 0' -m commit-1
11 hg commit -d '1000000 0' -m commit-1
12 hg tip
12 hg tip
13
13
14 unset EMAIL
14 unset EMAIL
15 echo 1234 > asdf
15 echo 1234 > asdf
16 hg commit -d '1000000 0' -u "foo@bar.com" -m commit-1
16 hg commit -d '1000000 0' -u "foo@bar.com" -m commit-1
17 hg tip
17 hg tip
18 echo "[ui]" >> .hg/hgrc
18 echo "[ui]" >> .hg/hgrc
19 echo "username = foobar <foo@bar.com>" >> .hg/hgrc
19 echo "username = foobar <foo@bar.com>" >> .hg/hgrc
20 echo 12 > asdf
20 echo 12 > asdf
21 hg commit -d '1000000 0' -m commit-1
21 hg commit -d '1000000 0' -m commit-1
22 hg tip
22 hg tip
23 echo 1 > asdf
23 echo 1 > asdf
24 hg commit -d '1000000 0' -u "foo@bar.com" -m commit-1
24 hg commit -d '1000000 0' -u "foo@bar.com" -m commit-1
25 hg tip
25 hg tip
26 echo 123 > asdf
26 echo 123 > asdf
27 echo "[ui]" > .hg/hgrc
27 echo "[ui]" > .hg/hgrc
28 echo "username = " >> .hg/hgrc
28 echo "username = " >> .hg/hgrc
29 hg commit -d '1000000 0' -m commit-1
29 hg commit -d '1000000 0' -m commit-1
30 rm .hg/hgrc
30 rm .hg/hgrc
31 hg commit -d '1000000 0' -m commit-1 2>&1 | sed -e "s/'[^']*'/user@host/"
31 hg commit -d '1000000 0' -m commit-1 2>&1 | sed -e "s/'[^']*'/user@host/"
32
33 echo space > asdf
34 hg commit -d '1000000 0' -u ' ' -m commit-1
35
36 true
@@ -1,26 +1,29
1 changeset: 0:9426b370c206
1 changeset: 0:9426b370c206
2 tag: tip
2 tag: tip
3 user: My Name <myname@example.com>
3 user: My Name <myname@example.com>
4 date: Mon Jan 12 13:46:40 1970 +0000
4 date: Mon Jan 12 13:46:40 1970 +0000
5 summary: commit-1
5 summary: commit-1
6
6
7 changeset: 1:4997f15a1b24
7 changeset: 1:4997f15a1b24
8 tag: tip
8 tag: tip
9 user: foo@bar.com
9 user: foo@bar.com
10 date: Mon Jan 12 13:46:40 1970 +0000
10 date: Mon Jan 12 13:46:40 1970 +0000
11 summary: commit-1
11 summary: commit-1
12
12
13 changeset: 2:72b8012b424e
13 changeset: 2:72b8012b424e
14 tag: tip
14 tag: tip
15 user: foobar <foo@bar.com>
15 user: foobar <foo@bar.com>
16 date: Mon Jan 12 13:46:40 1970 +0000
16 date: Mon Jan 12 13:46:40 1970 +0000
17 summary: commit-1
17 summary: commit-1
18
18
19 changeset: 3:35ff3067bedd
19 changeset: 3:35ff3067bedd
20 tag: tip
20 tag: tip
21 user: foo@bar.com
21 user: foo@bar.com
22 date: Mon Jan 12 13:46:40 1970 +0000
22 date: Mon Jan 12 13:46:40 1970 +0000
23 summary: commit-1
23 summary: commit-1
24
24
25 abort: Please specify a username.
25 abort: Please specify a username.
26 No username found, using user@host instead
26 No username found, using user@host instead
27 transaction abort!
28 rollback completed
29 abort: empty username!
General Comments 0
You need to be logged in to leave comments. Login now