##// END OF EJS Templates
Merge with crew-stable
Patrick Mezard -
r7519:992d7898 merge default
parent child Browse files
Show More
@@ -1,356 +1,353 b''
1 1 # CVS conversion code inspired by hg-cvs-import and git-cvsimport
2 2
3 3 import os, locale, re, socket, errno
4 4 from cStringIO import StringIO
5 5 from mercurial import util
6 6 from mercurial.i18n import _
7 7
8 8 from common import NoRepo, commit, converter_source, checktool
9 9 import cvsps
10 10
11 11 class convert_cvs(converter_source):
12 12 def __init__(self, ui, path, rev=None):
13 13 super(convert_cvs, self).__init__(ui, path, rev=rev)
14 14
15 15 cvs = os.path.join(path, "CVS")
16 16 if not os.path.exists(cvs):
17 17 raise NoRepo("%s does not look like a CVS checkout" % path)
18 18
19 19 checktool('cvs')
20 20 self.cmd = ui.config('convert', 'cvsps', 'builtin')
21 21 cvspsexe = self.cmd.split(None, 1)[0]
22 22 self.builtin = cvspsexe == 'builtin'
23 23
24 24 if not self.builtin:
25 25 checktool(cvspsexe)
26 26
27 27 self.changeset = {}
28 28 self.files = {}
29 29 self.tags = {}
30 30 self.lastbranch = {}
31 31 self.parent = {}
32 32 self.socket = None
33 33 self.cvsroot = file(os.path.join(cvs, "Root")).read()[:-1]
34 34 self.cvsrepo = file(os.path.join(cvs, "Repository")).read()[:-1]
35 35 self.encoding = locale.getpreferredencoding()
36 36
37 37 self._parse(ui)
38 38 self._connect()
39 39
40 40 def _parse(self, ui):
41 41 if self.changeset:
42 42 return
43 43
44 44 maxrev = 0
45 45 cmd = self.cmd
46 46 if self.rev:
47 47 # TODO: handle tags
48 48 try:
49 49 # patchset number?
50 50 maxrev = int(self.rev)
51 51 except ValueError:
52 52 try:
53 53 # date
54 54 util.parsedate(self.rev, ['%Y/%m/%d %H:%M:%S'])
55 55 cmd = '%s -d "1970/01/01 00:00:01" -d "%s"' % (cmd, self.rev)
56 56 except util.Abort:
57 57 raise util.Abort(_('revision %s is not a patchset number or date') % self.rev)
58 58
59 59 d = os.getcwd()
60 60 try:
61 61 os.chdir(self.path)
62 62 id = None
63 63 state = 0
64 64 filerevids = {}
65 65
66 66 if self.builtin:
67 67 # builtin cvsps code
68 68 ui.status(_('using builtin cvsps\n'))
69 69
70 70 db = cvsps.createlog(ui, cache='update')
71 71 db = cvsps.createchangeset(ui, db,
72 72 fuzz=int(ui.config('convert', 'cvsps.fuzz', 60)),
73 73 mergeto=ui.config('convert', 'cvsps.mergeto', None),
74 74 mergefrom=ui.config('convert', 'cvsps.mergefrom', None))
75 75
76 76 for cs in db:
77 77 if maxrev and cs.id>maxrev:
78 78 break
79 79 id = str(cs.id)
80 80 cs.author = self.recode(cs.author)
81 81 self.lastbranch[cs.branch] = id
82 82 cs.comment = self.recode(cs.comment)
83 83 date = util.datestr(cs.date)
84 84 self.tags.update(dict.fromkeys(cs.tags, id))
85 85
86 86 files = {}
87 87 for f in cs.entries:
88 88 files[f.file] = "%s%s" % ('.'.join([str(x) for x in f.revision]),
89 89 ['', '(DEAD)'][f.dead])
90 90
91 91 # add current commit to set
92 92 c = commit(author=cs.author, date=date,
93 93 parents=[str(p.id) for p in cs.parents],
94 94 desc=cs.comment, branch=cs.branch or '')
95 95 self.changeset[id] = c
96 96 self.files[id] = files
97 97 else:
98 98 # external cvsps
99 99 for l in util.popen(cmd):
100 100 if state == 0: # header
101 101 if l.startswith("PatchSet"):
102 102 id = l[9:-2]
103 103 if maxrev and int(id) > maxrev:
104 104 # ignore everything
105 105 state = 3
106 106 elif l.startswith("Date:"):
107 107 date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"])
108 108 date = util.datestr(date)
109 109 elif l.startswith("Branch:"):
110 110 branch = l[8:-1]
111 111 self.parent[id] = self.lastbranch.get(branch, 'bad')
112 112 self.lastbranch[branch] = id
113 113 elif l.startswith("Ancestor branch:"):
114 114 ancestor = l[17:-1]
115 115 # figure out the parent later
116 116 self.parent[id] = self.lastbranch[ancestor]
117 117 elif l.startswith("Author:"):
118 118 author = self.recode(l[8:-1])
119 119 elif l.startswith("Tag:") or l.startswith("Tags:"):
120 120 t = l[l.index(':')+1:]
121 121 t = [ut.strip() for ut in t.split(',')]
122 122 if (len(t) > 1) or (t[0] and (t[0] != "(none)")):
123 123 self.tags.update(dict.fromkeys(t, id))
124 124 elif l.startswith("Log:"):
125 125 # switch to gathering log
126 126 state = 1
127 127 log = ""
128 128 elif state == 1: # log
129 129 if l == "Members: \n":
130 130 # switch to gathering members
131 131 files = {}
132 132 oldrevs = []
133 133 log = self.recode(log[:-1])
134 134 state = 2
135 135 else:
136 136 # gather log
137 137 log += l
138 138 elif state == 2: # members
139 139 if l == "\n": # start of next entry
140 140 state = 0
141 141 p = [self.parent[id]]
142 142 if id == "1":
143 143 p = []
144 144 if branch == "HEAD":
145 145 branch = ""
146 146 if branch:
147 147 latest = 0
148 148 # the last changeset that contains a base
149 149 # file is our parent
150 150 for r in oldrevs:
151 151 latest = max(filerevids.get(r, 0), latest)
152 152 if latest:
153 153 p = [latest]
154 154
155 155 # add current commit to set
156 156 c = commit(author=author, date=date, parents=p,
157 157 desc=log, branch=branch)
158 158 self.changeset[id] = c
159 159 self.files[id] = files
160 160 else:
161 161 colon = l.rfind(':')
162 162 file = l[1:colon]
163 163 rev = l[colon+1:-2]
164 164 oldrev, rev = rev.split("->")
165 165 files[file] = rev
166 166
167 167 # save some information for identifying branch points
168 168 oldrevs.append("%s:%s" % (oldrev, file))
169 169 filerevids["%s:%s" % (rev, file)] = id
170 170 elif state == 3:
171 171 # swallow all input
172 172 continue
173 173
174 174 self.heads = self.lastbranch.values()
175 175 finally:
176 176 os.chdir(d)
177 177
178 178 def _connect(self):
179 179 root = self.cvsroot
180 180 conntype = None
181 181 user, host = None, None
182 182 cmd = ['cvs', 'server']
183 183
184 184 self.ui.status(_("connecting to %s\n") % root)
185 185
186 186 if root.startswith(":pserver:"):
187 187 root = root[9:]
188 188 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
189 189 root)
190 190 if m:
191 191 conntype = "pserver"
192 192 user, passw, serv, port, root = m.groups()
193 193 if not user:
194 194 user = "anonymous"
195 195 if not port:
196 196 port = 2401
197 197 else:
198 198 port = int(port)
199 199 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
200 200 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
201 201
202 202 if not passw:
203 203 passw = "A"
204 204 cvspass = os.path.expanduser("~/.cvspass")
205 205 try:
206 206 pf = open(cvspass)
207 207 for line in pf.read().splitlines():
208 208 part1, part2 = line.split(' ', 1)
209 209 if part1 == '/1':
210 210 # /1 :pserver:user@example.com:2401/cvsroot/foo Ah<Z
211 211 part1, part2 = part2.split(' ', 1)
212 212 format = format1
213 213 else:
214 214 # :pserver:user@example.com:/cvsroot/foo Ah<Z
215 215 format = format0
216 216 if part1 == format:
217 217 passw = part2
218 218 break
219 219 pf.close()
220 220 except IOError, inst:
221 221 if inst.errno != errno.ENOENT:
222 222 if not getattr(inst, 'filename', None):
223 223 inst.filename = cvspass
224 224 raise
225 225
226 226 sck = socket.socket()
227 227 sck.connect((serv, port))
228 228 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
229 229 "END AUTH REQUEST", ""]))
230 230 if sck.recv(128) != "I LOVE YOU\n":
231 231 raise util.Abort(_("CVS pserver authentication failed"))
232 232
233 233 self.writep = self.readp = sck.makefile('r+')
234 234
235 235 if not conntype and root.startswith(":local:"):
236 236 conntype = "local"
237 237 root = root[7:]
238 238
239 239 if not conntype:
240 240 # :ext:user@host/home/user/path/to/cvsroot
241 241 if root.startswith(":ext:"):
242 242 root = root[5:]
243 243 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
244 244 # Do not take Windows path "c:\foo\bar" for a connection strings
245 245 if os.path.isdir(root) or not m:
246 246 conntype = "local"
247 247 else:
248 248 conntype = "rsh"
249 249 user, host, root = m.group(1), m.group(2), m.group(3)
250 250
251 251 if conntype != "pserver":
252 252 if conntype == "rsh":
253 253 rsh = os.environ.get("CVS_RSH") or "ssh"
254 254 if user:
255 255 cmd = [rsh, '-l', user, host] + cmd
256 256 else:
257 257 cmd = [rsh, host] + cmd
258 258
259 259 # popen2 does not support argument lists under Windows
260 260 cmd = [util.shellquote(arg) for arg in cmd]
261 261 cmd = util.quotecommand(' '.join(cmd))
262 262 self.writep, self.readp = util.popen2(cmd, 'b')
263 263
264 264 self.realroot = root
265 265
266 266 self.writep.write("Root %s\n" % root)
267 267 self.writep.write("Valid-responses ok error Valid-requests Mode"
268 268 " M Mbinary E Checked-in Created Updated"
269 269 " Merged Removed\n")
270 270 self.writep.write("valid-requests\n")
271 271 self.writep.flush()
272 272 r = self.readp.readline()
273 273 if not r.startswith("Valid-requests"):
274 274 raise util.Abort(_("server sucks"))
275 275 if "UseUnchanged" in r:
276 276 self.writep.write("UseUnchanged\n")
277 277 self.writep.flush()
278 278 r = self.readp.readline()
279 279
280 280 def getheads(self):
281 281 return self.heads
282 282
283 283 def _getfile(self, name, rev):
284 284
285 285 def chunkedread(fp, count):
286 286 # file-objects returned by socked.makefile() do not handle
287 287 # large read() requests very well.
288 288 chunksize = 65536
289 289 output = StringIO()
290 290 while count > 0:
291 291 data = fp.read(min(count, chunksize))
292 292 if not data:
293 293 raise util.Abort(_("%d bytes missing from remote file") % count)
294 294 count -= len(data)
295 295 output.write(data)
296 296 return output.getvalue()
297 297
298 298 if rev.endswith("(DEAD)"):
299 299 raise IOError
300 300
301 301 args = ("-N -P -kk -r %s --" % rev).split()
302 302 args.append(self.cvsrepo + '/' + name)
303 303 for x in args:
304 304 self.writep.write("Argument %s\n" % x)
305 305 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
306 306 self.writep.flush()
307 307
308 308 data = ""
309 309 while 1:
310 310 line = self.readp.readline()
311 311 if line.startswith("Created ") or line.startswith("Updated "):
312 312 self.readp.readline() # path
313 313 self.readp.readline() # entries
314 314 mode = self.readp.readline()[:-1]
315 315 count = int(self.readp.readline()[:-1])
316 316 data = chunkedread(self.readp, count)
317 317 elif line.startswith(" "):
318 318 data += line[1:]
319 319 elif line.startswith("M "):
320 320 pass
321 321 elif line.startswith("Mbinary "):
322 322 count = int(self.readp.readline()[:-1])
323 323 data = chunkedread(self.readp, count)
324 324 else:
325 325 if line == "ok\n":
326 326 return (data, "x" in mode and "x" or "")
327 327 elif line.startswith("E "):
328 328 self.ui.warn(_("cvs server: %s\n") % line[2:])
329 329 elif line.startswith("Remove"):
330 330 l = self.readp.readline()
331 l = self.readp.readline()
332 if l != "ok\n":
333 raise util.Abort(_("unknown CVS response: %s") % l)
334 331 else:
335 332 raise util.Abort(_("unknown CVS response: %s") % line)
336 333
337 334 def getfile(self, file, rev):
338 335 data, mode = self._getfile(file, rev)
339 336 self.modecache[(file, rev)] = mode
340 337 return data
341 338
342 339 def getmode(self, file, rev):
343 340 return self.modecache[(file, rev)]
344 341
345 342 def getchanges(self, rev):
346 343 self.modecache = {}
347 344 return util.sort(self.files[rev].items()), {}
348 345
349 346 def getcommit(self, rev):
350 347 return self.changeset[rev]
351 348
352 349 def gettags(self):
353 350 return self.tags
354 351
355 352 def getchangedfiles(self, rev, i):
356 353 return util.sort(self.files[rev].keys())
@@ -1,806 +1,806 b''
1 1 # context.py - changeset and file context objects for mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import nullid, nullrev, short, hex
9 9 from i18n import _
10 10 import ancestor, bdiff, revlog, util, os, errno
11 11
12 12 class propertycache(object):
13 13 def __init__(self, func):
14 14 self.func = func
15 15 self.name = func.__name__
16 16 def __get__(self, obj, type=None):
17 17 result = self.func(obj)
18 18 setattr(obj, self.name, result)
19 19 return result
20 20
21 21 class changectx(object):
22 22 """A changecontext object makes access to data related to a particular
23 23 changeset convenient."""
24 24 def __init__(self, repo, changeid=''):
25 25 """changeid is a revision number, node, or tag"""
26 26 if changeid == '':
27 27 changeid = '.'
28 28 self._repo = repo
29 29 if isinstance(changeid, (long, int)):
30 30 self._rev = changeid
31 31 self._node = self._repo.changelog.node(changeid)
32 32 else:
33 33 self._node = self._repo.lookup(changeid)
34 34 self._rev = self._repo.changelog.rev(self._node)
35 35
36 36 def __str__(self):
37 37 return short(self.node())
38 38
39 39 def __int__(self):
40 40 return self.rev()
41 41
42 42 def __repr__(self):
43 43 return "<changectx %s>" % str(self)
44 44
45 45 def __hash__(self):
46 46 try:
47 47 return hash(self._rev)
48 48 except AttributeError:
49 49 return id(self)
50 50
51 51 def __eq__(self, other):
52 52 try:
53 53 return self._rev == other._rev
54 54 except AttributeError:
55 55 return False
56 56
57 57 def __ne__(self, other):
58 58 return not (self == other)
59 59
60 60 def __nonzero__(self):
61 61 return self._rev != nullrev
62 62
63 63 def _changeset(self):
64 64 return self._repo.changelog.read(self.node())
65 65 _changeset = propertycache(_changeset)
66 66
67 67 def _manifest(self):
68 68 return self._repo.manifest.read(self._changeset[0])
69 69 _manifest = propertycache(_manifest)
70 70
71 71 def _manifestdelta(self):
72 72 return self._repo.manifest.readdelta(self._changeset[0])
73 73 _manifestdelta = propertycache(_manifestdelta)
74 74
75 75 def _parents(self):
76 76 p = self._repo.changelog.parentrevs(self._rev)
77 77 if p[1] == nullrev:
78 78 p = p[:-1]
79 79 return [changectx(self._repo, x) for x in p]
80 80 _parents = propertycache(_parents)
81 81
82 82 def __contains__(self, key):
83 83 return key in self._manifest
84 84
85 85 def __getitem__(self, key):
86 86 return self.filectx(key)
87 87
88 88 def __iter__(self):
89 89 for f in util.sort(self._manifest):
90 90 yield f
91 91
92 92 def changeset(self): return self._changeset
93 93 def manifest(self): return self._manifest
94 94
95 95 def rev(self): return self._rev
96 96 def node(self): return self._node
97 97 def hex(self): return hex(self._node)
98 98 def user(self): return self._changeset[1]
99 99 def date(self): return self._changeset[2]
100 100 def files(self): return self._changeset[3]
101 101 def description(self): return self._changeset[4]
102 102 def branch(self): return self._changeset[5].get("branch")
103 103 def extra(self): return self._changeset[5]
104 104 def tags(self): return self._repo.nodetags(self._node)
105 105
106 106 def parents(self):
107 107 """return contexts for each parent changeset"""
108 108 return self._parents
109 109
110 110 def children(self):
111 111 """return contexts for each child changeset"""
112 112 c = self._repo.changelog.children(self._node)
113 113 return [changectx(self._repo, x) for x in c]
114 114
115 115 def ancestors(self):
116 116 for a in self._repo.changelog.ancestors(self._rev):
117 117 yield changectx(self._repo, a)
118 118
119 119 def descendants(self):
120 120 for d in self._repo.changelog.descendants(self._rev):
121 121 yield changectx(self._repo, d)
122 122
123 123 def _fileinfo(self, path):
124 124 if '_manifest' in self.__dict__:
125 125 try:
126 126 return self._manifest[path], self._manifest.flags(path)
127 127 except KeyError:
128 128 raise revlog.LookupError(self._node, path,
129 129 _('not found in manifest'))
130 130 if '_manifestdelta' in self.__dict__ or path in self.files():
131 131 if path in self._manifestdelta:
132 132 return self._manifestdelta[path], self._manifestdelta.flags(path)
133 133 node, flag = self._repo.manifest.find(self._changeset[0], path)
134 134 if not node:
135 135 raise revlog.LookupError(self._node, path,
136 136 _('not found in manifest'))
137 137
138 138 return node, flag
139 139
140 140 def filenode(self, path):
141 141 return self._fileinfo(path)[0]
142 142
143 143 def flags(self, path):
144 144 try:
145 145 return self._fileinfo(path)[1]
146 146 except revlog.LookupError:
147 147 return ''
148 148
149 149 def filectx(self, path, fileid=None, filelog=None):
150 150 """get a file context from this changeset"""
151 151 if fileid is None:
152 152 fileid = self.filenode(path)
153 153 return filectx(self._repo, path, fileid=fileid,
154 154 changectx=self, filelog=filelog)
155 155
156 156 def ancestor(self, c2):
157 157 """
158 158 return the ancestor context of self and c2
159 159 """
160 160 n = self._repo.changelog.ancestor(self._node, c2._node)
161 161 return changectx(self._repo, n)
162 162
163 163 def walk(self, match):
164 164 fdict = dict.fromkeys(match.files())
165 165 # for dirstate.walk, files=['.'] means "walk the whole tree".
166 166 # follow that here, too
167 167 fdict.pop('.', None)
168 168 for fn in self:
169 169 for ffn in fdict:
170 170 # match if the file is the exact name or a directory
171 171 if ffn == fn or fn.startswith("%s/" % ffn):
172 172 del fdict[ffn]
173 173 break
174 174 if match(fn):
175 175 yield fn
176 176 for fn in util.sort(fdict):
177 177 if match.bad(fn, 'No such file in rev ' + str(self)) and match(fn):
178 178 yield fn
179 179
180 180 class filectx(object):
181 181 """A filecontext object makes access to data related to a particular
182 182 filerevision convenient."""
183 183 def __init__(self, repo, path, changeid=None, fileid=None,
184 184 filelog=None, changectx=None):
185 185 """changeid can be a changeset revision, node, or tag.
186 186 fileid can be a file revision or node."""
187 187 self._repo = repo
188 188 self._path = path
189 189
190 190 assert (changeid is not None
191 191 or fileid is not None
192 192 or changectx is not None)
193 193
194 194 if filelog:
195 195 self._filelog = filelog
196 196
197 197 if changeid is not None:
198 198 self._changeid = changeid
199 199 if changectx is not None:
200 200 self._changectx = changectx
201 201 if fileid is not None:
202 202 self._fileid = fileid
203 203
204 204 def _changectx(self):
205 205 return changectx(self._repo, self._changeid)
206 206 _changectx = propertycache(_changectx)
207 207
208 208 def _filelog(self):
209 209 return self._repo.file(self._path)
210 210 _filelog = propertycache(_filelog)
211 211
212 212 def _changeid(self):
213 213 if '_changectx' in self.__dict__:
214 214 return self._changectx.rev()
215 215 else:
216 216 return self._filelog.linkrev(self._filerev)
217 217 _changeid = propertycache(_changeid)
218 218
219 219 def _filenode(self):
220 220 if '_fileid' in self.__dict__:
221 221 return self._filelog.lookup(self._fileid)
222 222 else:
223 223 return self._changectx.filenode(self._path)
224 224 _filenode = propertycache(_filenode)
225 225
226 226 def _filerev(self):
227 227 return self._filelog.rev(self._filenode)
228 228 _filerev = propertycache(_filerev)
229 229
230 230 def _repopath(self):
231 231 return self._path
232 232 _repopath = propertycache(_repopath)
233 233
234 234 def __nonzero__(self):
235 235 try:
236 236 n = self._filenode
237 237 return True
238 238 except revlog.LookupError:
239 239 # file is missing
240 240 return False
241 241
242 242 def __str__(self):
243 243 return "%s@%s" % (self.path(), short(self.node()))
244 244
245 245 def __repr__(self):
246 246 return "<filectx %s>" % str(self)
247 247
248 248 def __hash__(self):
249 249 try:
250 250 return hash((self._path, self._fileid))
251 251 except AttributeError:
252 252 return id(self)
253 253
254 254 def __eq__(self, other):
255 255 try:
256 256 return (self._path == other._path
257 257 and self._fileid == other._fileid)
258 258 except AttributeError:
259 259 return False
260 260
261 261 def __ne__(self, other):
262 262 return not (self == other)
263 263
264 264 def filectx(self, fileid):
265 265 '''opens an arbitrary revision of the file without
266 266 opening a new filelog'''
267 267 return filectx(self._repo, self._path, fileid=fileid,
268 268 filelog=self._filelog)
269 269
270 270 def filerev(self): return self._filerev
271 271 def filenode(self): return self._filenode
272 272 def flags(self): return self._changectx.flags(self._path)
273 273 def filelog(self): return self._filelog
274 274
275 275 def rev(self):
276 276 if '_changectx' in self.__dict__:
277 277 return self._changectx.rev()
278 278 if '_changeid' in self.__dict__:
279 279 return self._changectx.rev()
280 280 return self._filelog.linkrev(self._filerev)
281 281
282 282 def linkrev(self): return self._filelog.linkrev(self._filerev)
283 283 def node(self): return self._changectx.node()
284 284 def user(self): return self._changectx.user()
285 285 def date(self): return self._changectx.date()
286 286 def files(self): return self._changectx.files()
287 287 def description(self): return self._changectx.description()
288 288 def branch(self): return self._changectx.branch()
289 289 def manifest(self): return self._changectx.manifest()
290 290 def changectx(self): return self._changectx
291 291
292 292 def data(self): return self._filelog.read(self._filenode)
293 293 def path(self): return self._path
294 294 def size(self): return self._filelog.size(self._filerev)
295 295
296 296 def cmp(self, text): return self._filelog.cmp(self._filenode, text)
297 297
298 298 def renamed(self):
299 299 """check if file was actually renamed in this changeset revision
300 300
301 301 If rename logged in file revision, we report copy for changeset only
302 302 if file revisions linkrev points back to the changeset in question
303 303 or both changeset parents contain different file revisions.
304 304 """
305 305
306 306 renamed = self._filelog.renamed(self._filenode)
307 307 if not renamed:
308 308 return renamed
309 309
310 310 if self.rev() == self.linkrev():
311 311 return renamed
312 312
313 313 name = self.path()
314 314 fnode = self._filenode
315 315 for p in self._changectx.parents():
316 316 try:
317 317 if fnode == p.filenode(name):
318 318 return None
319 319 except revlog.LookupError:
320 320 pass
321 321 return renamed
322 322
323 323 def parents(self):
324 324 p = self._path
325 325 fl = self._filelog
326 326 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
327 327
328 328 r = self._filelog.renamed(self._filenode)
329 329 if r:
330 330 pl[0] = (r[0], r[1], None)
331 331
332 332 return [filectx(self._repo, p, fileid=n, filelog=l)
333 333 for p,n,l in pl if n != nullid]
334 334
335 335 def children(self):
336 336 # hard for renames
337 337 c = self._filelog.children(self._filenode)
338 338 return [filectx(self._repo, self._path, fileid=x,
339 339 filelog=self._filelog) for x in c]
340 340
341 341 def annotate(self, follow=False, linenumber=None):
342 342 '''returns a list of tuples of (ctx, line) for each line
343 343 in the file, where ctx is the filectx of the node where
344 344 that line was last changed.
345 345 This returns tuples of ((ctx, linenumber), line) for each line,
346 346 if "linenumber" parameter is NOT "None".
347 347 In such tuples, linenumber means one at the first appearance
348 348 in the managed file.
349 349 To reduce annotation cost,
350 350 this returns fixed value(False is used) as linenumber,
351 351 if "linenumber" parameter is "False".'''
352 352
353 353 def decorate_compat(text, rev):
354 354 return ([rev] * len(text.splitlines()), text)
355 355
356 356 def without_linenumber(text, rev):
357 357 return ([(rev, False)] * len(text.splitlines()), text)
358 358
359 359 def with_linenumber(text, rev):
360 360 size = len(text.splitlines())
361 361 return ([(rev, i) for i in xrange(1, size + 1)], text)
362 362
363 363 decorate = (((linenumber is None) and decorate_compat) or
364 364 (linenumber and with_linenumber) or
365 365 without_linenumber)
366 366
367 367 def pair(parent, child):
368 368 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
369 369 child[0][b1:b2] = parent[0][a1:a2]
370 370 return child
371 371
372 372 getlog = util.cachefunc(lambda x: self._repo.file(x))
373 373 def getctx(path, fileid):
374 374 log = path == self._path and self._filelog or getlog(path)
375 375 return filectx(self._repo, path, fileid=fileid, filelog=log)
376 376 getctx = util.cachefunc(getctx)
377 377
378 378 def parents(f):
379 379 # we want to reuse filectx objects as much as possible
380 380 p = f._path
381 381 if f._filerev is None: # working dir
382 382 pl = [(n.path(), n.filerev()) for n in f.parents()]
383 383 else:
384 384 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
385 385
386 386 if follow:
387 387 r = f.renamed()
388 388 if r:
389 389 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
390 390
391 391 return [getctx(p, n) for p, n in pl if n != nullrev]
392 392
393 393 # use linkrev to find the first changeset where self appeared
394 394 if self.rev() != self.linkrev():
395 395 base = self.filectx(self.filerev())
396 396 else:
397 397 base = self
398 398
399 399 # find all ancestors
400 400 needed = {base: 1}
401 401 visit = [base]
402 402 files = [base._path]
403 403 while visit:
404 404 f = visit.pop(0)
405 405 for p in parents(f):
406 406 if p not in needed:
407 407 needed[p] = 1
408 408 visit.append(p)
409 409 if p._path not in files:
410 410 files.append(p._path)
411 411 else:
412 412 # count how many times we'll use this
413 413 needed[p] += 1
414 414
415 415 # sort by revision (per file) which is a topological order
416 416 visit = []
417 417 for f in files:
418 418 fn = [(n.rev(), n) for n in needed if n._path == f]
419 419 visit.extend(fn)
420 420
421 421 hist = {}
422 422 for r, f in util.sort(visit):
423 423 curr = decorate(f.data(), f)
424 424 for p in parents(f):
425 425 if p != nullid:
426 426 curr = pair(hist[p], curr)
427 427 # trim the history of unneeded revs
428 428 needed[p] -= 1
429 429 if not needed[p]:
430 430 del hist[p]
431 431 hist[f] = curr
432 432
433 433 return zip(hist[f][0], hist[f][1].splitlines(1))
434 434
435 435 def ancestor(self, fc2):
436 436 """
437 437 find the common ancestor file context, if any, of self, and fc2
438 438 """
439 439
440 440 acache = {}
441 441
442 442 # prime the ancestor cache for the working directory
443 443 for c in (self, fc2):
444 444 if c._filerev == None:
445 445 pl = [(n.path(), n.filenode()) for n in c.parents()]
446 446 acache[(c._path, None)] = pl
447 447
448 448 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
449 449 def parents(vertex):
450 450 if vertex in acache:
451 451 return acache[vertex]
452 452 f, n = vertex
453 453 if f not in flcache:
454 454 flcache[f] = self._repo.file(f)
455 455 fl = flcache[f]
456 456 pl = [(f, p) for p in fl.parents(n) if p != nullid]
457 457 re = fl.renamed(n)
458 458 if re:
459 459 pl.append(re)
460 460 acache[vertex] = pl
461 461 return pl
462 462
463 463 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
464 464 v = ancestor.ancestor(a, b, parents)
465 465 if v:
466 466 f, n = v
467 467 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
468 468
469 469 return None
470 470
471 471 class workingctx(changectx):
472 472 """A workingctx object makes access to data related to
473 473 the current working directory convenient.
474 474 parents - a pair of parent nodeids, or None to use the dirstate.
475 475 date - any valid date string or (unixtime, offset), or None.
476 476 user - username string, or None.
477 477 extra - a dictionary of extra values, or None.
478 478 changes - a list of file lists as returned by localrepo.status()
479 479 or None to use the repository status.
480 480 """
481 481 def __init__(self, repo, parents=None, text="", user=None, date=None,
482 482 extra=None, changes=None):
483 483 self._repo = repo
484 484 self._rev = None
485 485 self._node = None
486 486 self._text = text
487 487 if date:
488 488 self._date = util.parsedate(date)
489 489 if user:
490 490 self._user = user
491 491 if parents:
492 492 self._parents = [changectx(self._repo, p) for p in parents]
493 493 if changes:
494 494 self._status = list(changes)
495 495
496 496 self._extra = {}
497 497 if extra:
498 498 self._extra = extra.copy()
499 499 if 'branch' not in self._extra:
500 500 branch = self._repo.dirstate.branch()
501 501 try:
502 502 branch = branch.decode('UTF-8').encode('UTF-8')
503 503 except UnicodeDecodeError:
504 504 raise util.Abort(_('branch name not in UTF-8!'))
505 505 self._extra['branch'] = branch
506 506 if self._extra['branch'] == '':
507 507 self._extra['branch'] = 'default'
508 508
509 509 def __str__(self):
510 510 return str(self._parents[0]) + "+"
511 511
512 512 def __nonzero__(self):
513 513 return True
514 514
515 515 def __contains__(self, key):
516 516 return self._dirstate[key] not in "?r"
517 517
518 518 def _manifest(self):
519 519 """generate a manifest corresponding to the working directory"""
520 520
521 521 man = self._parents[0].manifest().copy()
522 522 copied = self._repo.dirstate.copies()
523 523 cf = lambda x: man.flags(copied.get(x, x))
524 524 ff = self._repo.dirstate.flagfunc(cf)
525 525 modified, added, removed, deleted, unknown = self._status[:5]
526 526 for i, l in (("a", added), ("m", modified), ("u", unknown)):
527 527 for f in l:
528 528 man[f] = man.get(copied.get(f, f), nullid) + i
529 529 try:
530 530 man.set(f, ff(f))
531 531 except OSError:
532 532 pass
533 533
534 534 for f in deleted + removed:
535 535 if f in man:
536 536 del man[f]
537 537
538 538 return man
539 539 _manifest = propertycache(_manifest)
540 540
541 541 def _status(self):
542 542 return self._repo.status(unknown=True)
543 543 _status = propertycache(_status)
544 544
545 545 def _user(self):
546 546 return self._repo.ui.username()
547 547 _user = propertycache(_user)
548 548
549 549 def _date(self):
550 550 return util.makedate()
551 551 _date = propertycache(_date)
552 552
553 553 def _parents(self):
554 554 p = self._repo.dirstate.parents()
555 555 if p[1] == nullid:
556 556 p = p[:-1]
557 557 self._parents = [changectx(self._repo, x) for x in p]
558 558 return self._parents
559 559 _parents = propertycache(_parents)
560 560
561 561 def manifest(self): return self._manifest
562 562
563 563 def user(self): return self._user or self._repo.ui.username()
564 564 def date(self): return self._date
565 565 def description(self): return self._text
566 566 def files(self):
567 567 return util.sort(self._status[0] + self._status[1] + self._status[2])
568 568
569 569 def modified(self): return self._status[0]
570 570 def added(self): return self._status[1]
571 571 def removed(self): return self._status[2]
572 572 def deleted(self): return self._status[3]
573 573 def unknown(self): return self._status[4]
574 574 def clean(self): return self._status[5]
575 575 def branch(self): return self._extra['branch']
576 576 def extra(self): return self._extra
577 577
578 578 def tags(self):
579 579 t = []
580 580 [t.extend(p.tags()) for p in self.parents()]
581 581 return t
582 582
583 583 def children(self):
584 584 return []
585 585
586 586 def flags(self, path):
587 587 if '_manifest' in self.__dict__:
588 588 try:
589 589 return self._manifest.flags(path)
590 590 except KeyError:
591 591 return ''
592 592
593 593 pnode = self._parents[0].changeset()[0]
594 594 orig = self._repo.dirstate.copies().get(path, path)
595 595 node, flag = self._repo.manifest.find(pnode, orig)
596 596 try:
597 597 ff = self._repo.dirstate.flagfunc(lambda x: flag or '')
598 598 return ff(path)
599 599 except OSError:
600 600 pass
601 601
602 602 if not node or path in self.deleted() or path in self.removed():
603 603 return ''
604 604 return flag
605 605
606 606 def filectx(self, path, filelog=None):
607 607 """get a file context from the working directory"""
608 608 return workingfilectx(self._repo, path, workingctx=self,
609 609 filelog=filelog)
610 610
611 611 def ancestor(self, c2):
612 612 """return the ancestor context of self and c2"""
613 613 return self._parents[0].ancestor(c2) # punt on two parents for now
614 614
615 615 def walk(self, match):
616 616 return util.sort(self._repo.dirstate.walk(match, True, False).keys())
617 617
618 618 class workingfilectx(filectx):
619 619 """A workingfilectx object makes access to data related to a particular
620 620 file in the working directory convenient."""
621 621 def __init__(self, repo, path, filelog=None, workingctx=None):
622 622 """changeid can be a changeset revision, node, or tag.
623 623 fileid can be a file revision or node."""
624 624 self._repo = repo
625 625 self._path = path
626 626 self._changeid = None
627 627 self._filerev = self._filenode = None
628 628
629 629 if filelog:
630 630 self._filelog = filelog
631 631 if workingctx:
632 632 self._changectx = workingctx
633 633
634 634 def _changectx(self):
635 635 return workingctx(self._repo)
636 636 _changectx = propertycache(_changectx)
637 637
638 638 def _repopath(self):
639 639 return self._repo.dirstate.copied(self._path) or self._path
640 640 _repopath = propertycache(_repopath)
641 641
642 642 def _filelog(self):
643 643 return self._repo.file(self._repopath)
644 644 _filelog = propertycache(_filelog)
645 645
646 646 def __nonzero__(self):
647 647 return True
648 648
649 649 def __str__(self):
650 650 return "%s@%s" % (self.path(), self._changectx)
651 651
652 652 def filectx(self, fileid):
653 653 '''opens an arbitrary revision of the file without
654 654 opening a new filelog'''
655 655 return filectx(self._repo, self._repopath, fileid=fileid,
656 656 filelog=self._filelog)
657 657
658 658 def rev(self):
659 659 if '_changectx' in self.__dict__:
660 660 return self._changectx.rev()
661 661 return self._filelog.linkrev(self._filerev)
662 662
663 663 def data(self): return self._repo.wread(self._path)
664 664 def renamed(self):
665 665 rp = self._repopath
666 666 if rp == self._path:
667 667 return None
668 668 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
669 669
670 670 def parents(self):
671 671 '''return parent filectxs, following copies if necessary'''
672 672 p = self._path
673 673 rp = self._repopath
674 674 pcl = self._changectx._parents
675 675 fl = self._filelog
676 676 pl = [(rp, pcl[0]._manifest.get(rp, nullid), fl)]
677 677 if len(pcl) > 1:
678 678 if rp != p:
679 679 fl = None
680 680 pl.append((p, pcl[1]._manifest.get(p, nullid), fl))
681 681
682 682 return [filectx(self._repo, p, fileid=n, filelog=l)
683 683 for p,n,l in pl if n != nullid]
684 684
685 685 def children(self):
686 686 return []
687 687
688 688 def size(self): return os.stat(self._repo.wjoin(self._path)).st_size
689 689 def date(self):
690 690 t, tz = self._changectx.date()
691 691 try:
692 692 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
693 693 except OSError, err:
694 694 if err.errno != errno.ENOENT: raise
695 695 return (t, tz)
696 696
697 697 def cmp(self, text): return self._repo.wread(self._path) == text
698 698
699 699 class memctx(object):
700 700 """Use memctx to perform in-memory commits via localrepo.commitctx().
701 701
702 702 Revision information is supplied at initialization time while
703 703 related files data and is made available through a callback
704 704 mechanism. 'repo' is the current localrepo, 'parents' is a
705 705 sequence of two parent revisions identifiers (pass None for every
706 706 missing parent), 'text' is the commit message and 'files' lists
707 707 names of files touched by the revision (normalized and relative to
708 708 repository root).
709 709
710 710 filectxfn(repo, memctx, path) is a callable receiving the
711 711 repository, the current memctx object and the normalized path of
712 712 requested file, relative to repository root. It is fired by the
713 713 commit function for every file in 'files', but calls order is
714 714 undefined. If the file is available in the revision being
715 715 committed (updated or added), filectxfn returns a memfilectx
716 716 object. If the file was removed, filectxfn raises an
717 717 IOError. Moved files are represented by marking the source file
718 718 removed and the new file added with copy information (see
719 719 memfilectx).
720 720
721 721 user receives the committer name and defaults to current
722 722 repository username, date is the commit date in any format
723 723 supported by util.parsedate() and defaults to current date, extra
724 724 is a dictionary of metadata or is left empty.
725 725 """
726 726 def __init__(self, repo, parents, text, files, filectxfn, user=None,
727 727 date=None, extra=None):
728 728 self._repo = repo
729 729 self._rev = None
730 730 self._node = None
731 731 self._text = text
732 732 self._date = date and util.parsedate(date) or util.makedate()
733 733 self._user = user
734 734 parents = [(p or nullid) for p in parents]
735 735 p1, p2 = parents
736 736 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
737 files = util.sort(list(files))
737 files = util.sort(util.unique(files))
738 738 self._status = [files, [], [], [], []]
739 739 self._filectxfn = filectxfn
740 740
741 741 self._extra = extra and extra.copy() or {}
742 742 if 'branch' not in self._extra:
743 743 self._extra['branch'] = 'default'
744 744 elif self._extra.get('branch') == '':
745 745 self._extra['branch'] = 'default'
746 746
747 747 def __str__(self):
748 748 return str(self._parents[0]) + "+"
749 749
750 750 def __int__(self):
751 751 return self._rev
752 752
753 753 def __nonzero__(self):
754 754 return True
755 755
756 756 def user(self): return self._user or self._repo.ui.username()
757 757 def date(self): return self._date
758 758 def description(self): return self._text
759 759 def files(self): return self.modified()
760 760 def modified(self): return self._status[0]
761 761 def added(self): return self._status[1]
762 762 def removed(self): return self._status[2]
763 763 def deleted(self): return self._status[3]
764 764 def unknown(self): return self._status[4]
765 765 def clean(self): return self._status[5]
766 766 def branch(self): return self._extra['branch']
767 767 def extra(self): return self._extra
768 768 def flags(self, f): return self[f].flags()
769 769
770 770 def parents(self):
771 771 """return contexts for each parent changeset"""
772 772 return self._parents
773 773
774 774 def filectx(self, path, filelog=None):
775 775 """get a file context from the working directory"""
776 776 return self._filectxfn(self._repo, self, path)
777 777
778 778 class memfilectx(object):
779 779 """memfilectx represents an in-memory file to commit.
780 780
781 781 See memctx for more details.
782 782 """
783 783 def __init__(self, path, data, islink, isexec, copied):
784 784 """
785 785 path is the normalized file path relative to repository root.
786 786 data is the file content as a string.
787 787 islink is True if the file is a symbolic link.
788 788 isexec is True if the file is executable.
789 789 copied is the source file path if current file was copied in the
790 790 revision being committed, or None."""
791 791 self._path = path
792 792 self._data = data
793 793 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
794 794 self._copied = None
795 795 if copied:
796 796 self._copied = (copied, nullid)
797 797
798 798 def __nonzero__(self): return True
799 799 def __str__(self): return "%s@%s" % (self.path(), self._changectx)
800 800 def path(self): return self._path
801 801 def data(self): return self._data
802 802 def flags(self): return self._flags
803 803 def isexec(self): return 'x' in self._flags
804 804 def islink(self): return 'l' in self._flags
805 805 def renamed(self): return self._copied
806 806
@@ -1,291 +1,297 b''
1 1 # store.py - repository store handling for Mercurial
2 2 #
3 3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from i18n import _
9 9 import os, stat, osutil, util
10 10
11 11 _sha = util.sha1
12 12
13 13 def _buildencodefun():
14 14 e = '_'
15 15 win_reserved = [ord(x) for x in '\\:*?"<>|']
16 16 cmap = dict([ (chr(x), chr(x)) for x in xrange(127) ])
17 17 for x in (range(32) + range(126, 256) + win_reserved):
18 18 cmap[chr(x)] = "~%02x" % x
19 19 for x in range(ord("A"), ord("Z")+1) + [ord(e)]:
20 20 cmap[chr(x)] = e + chr(x).lower()
21 21 dmap = {}
22 22 for k, v in cmap.iteritems():
23 23 dmap[v] = k
24 24 def decode(s):
25 25 i = 0
26 26 while i < len(s):
27 27 for l in xrange(1, 4):
28 28 try:
29 29 yield dmap[s[i:i+l]]
30 30 i += l
31 31 break
32 32 except KeyError:
33 33 pass
34 34 else:
35 35 raise KeyError
36 36 return (lambda s: "".join([cmap[c] for c in s]),
37 37 lambda s: "".join(list(decode(s))))
38 38
39 39 encodefilename, decodefilename = _buildencodefun()
40 40
41 41 def _build_lower_encodefun():
42 42 win_reserved = [ord(x) for x in '\\:*?"<>|']
43 43 cmap = dict([ (chr(x), chr(x)) for x in xrange(127) ])
44 44 for x in (range(32) + range(126, 256) + win_reserved):
45 45 cmap[chr(x)] = "~%02x" % x
46 46 for x in range(ord("A"), ord("Z")+1):
47 47 cmap[chr(x)] = chr(x).lower()
48 48 return lambda s: "".join([cmap[c] for c in s])
49 49
50 50 lowerencode = _build_lower_encodefun()
51 51
52 52 _windows_reserved_filenames = '''con prn aux nul
53 53 com1 com2 com3 com4 com5 com6 com7 com8 com9
54 54 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
55 55 def auxencode(path):
56 56 res = []
57 57 for n in path.split('/'):
58 58 if n:
59 59 base = n.split('.')[0]
60 60 if base and (base in _windows_reserved_filenames):
61 61 # encode third letter ('aux' -> 'au~78')
62 62 ec = "~%02x" % ord(n[2])
63 63 n = n[0:2] + ec + n[3:]
64 if n[-1] in '. ':
65 # encode last period or space ('foo...' -> 'foo..~2e')
66 n = n[:-1] + "~%02x" % ord(n[-1])
64 67 res.append(n)
65 68 return '/'.join(res)
66 69
67 70 MAX_PATH_LEN_IN_HGSTORE = 120
68 71 DIR_PREFIX_LEN = 8
69 72 _MAX_SHORTENED_DIRS_LEN = 8 * (DIR_PREFIX_LEN + 1) - 4
70 73 def hybridencode(path):
71 74 '''encodes path with a length limit
72 75
73 76 Encodes all paths that begin with 'data/', according to the following.
74 77
75 78 Default encoding (reversible):
76 79
77 80 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
78 81 characters are encoded as '~xx', where xx is the two digit hex code
79 82 of the character (see encodefilename).
80 83 Relevant path components consisting of Windows reserved filenames are
81 84 masked by encoding the third character ('aux' -> 'au~78', see auxencode).
82 85
83 86 Hashed encoding (not reversible):
84 87
85 88 If the default-encoded path is longer than MAX_PATH_LEN_IN_HGSTORE, a
86 89 non-reversible hybrid hashing of the path is done instead.
87 90 This encoding uses up to DIR_PREFIX_LEN characters of all directory
88 91 levels of the lowerencoded path, but not more levels than can fit into
89 92 _MAX_SHORTENED_DIRS_LEN.
90 93 Then follows the filler followed by the sha digest of the full path.
91 94 The filler is the beginning of the basename of the lowerencoded path
92 95 (the basename is everything after the last path separator). The filler
93 96 is as long as possible, filling in characters from the basename until
94 97 the encoded path has MAX_PATH_LEN_IN_HGSTORE characters (or all chars
95 98 of the basename have been taken).
96 99 The extension (e.g. '.i' or '.d') is preserved.
97 100
98 101 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
99 102 encoding was used.
100 103 '''
101 104 if not path.startswith('data/'):
102 105 return path
103 106 ndpath = path[len('data/'):]
104 107 res = 'data/' + auxencode(encodefilename(ndpath))
105 108 if len(res) > MAX_PATH_LEN_IN_HGSTORE:
106 109 digest = _sha(path).hexdigest()
107 110 aep = auxencode(lowerencode(ndpath))
108 111 _root, ext = os.path.splitext(aep)
109 112 parts = aep.split('/')
110 113 basename = parts[-1]
111 114 sdirs = []
112 115 for p in parts[:-1]:
113 116 d = p[:DIR_PREFIX_LEN]
117 if d[-1] in '. ':
118 # Windows can't access dirs ending in period or space
119 d = d[:-1] + '_'
114 120 t = '/'.join(sdirs) + '/' + d
115 121 if len(t) > _MAX_SHORTENED_DIRS_LEN:
116 122 break
117 123 sdirs.append(d)
118 124 dirs = '/'.join(sdirs)
119 125 if len(dirs) > 0:
120 126 dirs += '/'
121 127 res = 'dh/' + dirs + digest + ext
122 128 space_left = MAX_PATH_LEN_IN_HGSTORE - len(res)
123 129 if space_left > 0:
124 130 filler = basename[:space_left]
125 131 res = 'dh/' + dirs + filler + digest + ext
126 132 return res
127 133
128 134 def _calcmode(path):
129 135 try:
130 136 # files in .hg/ will be created using this mode
131 137 mode = os.stat(path).st_mode
132 138 # avoid some useless chmods
133 139 if (0777 & ~util._umask) == (0777 & mode):
134 140 mode = None
135 141 except OSError:
136 142 mode = None
137 143 return mode
138 144
139 145 _data = 'data 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
140 146
141 147 class basicstore:
142 148 '''base class for local repository stores'''
143 149 def __init__(self, path, opener, pathjoiner):
144 150 self.pathjoiner = pathjoiner
145 151 self.path = path
146 152 self.createmode = _calcmode(path)
147 153 self.opener = opener(self.path)
148 154 self.opener.createmode = self.createmode
149 155
150 156 def join(self, f):
151 157 return self.pathjoiner(self.path, f)
152 158
153 159 def _walk(self, relpath, recurse):
154 160 '''yields (unencoded, encoded, size)'''
155 161 path = self.pathjoiner(self.path, relpath)
156 162 striplen = len(self.path) + len(os.sep)
157 163 l = []
158 164 if os.path.isdir(path):
159 165 visit = [path]
160 166 while visit:
161 167 p = visit.pop()
162 168 for f, kind, st in osutil.listdir(p, stat=True):
163 169 fp = self.pathjoiner(p, f)
164 170 if kind == stat.S_IFREG and f[-2:] in ('.d', '.i'):
165 171 n = util.pconvert(fp[striplen:])
166 172 l.append((n, n, st.st_size))
167 173 elif kind == stat.S_IFDIR and recurse:
168 174 visit.append(fp)
169 175 return util.sort(l)
170 176
171 177 def datafiles(self):
172 178 return self._walk('data', True)
173 179
174 180 def walk(self):
175 181 '''yields (unencoded, encoded, size)'''
176 182 # yield data files first
177 183 for x in self.datafiles():
178 184 yield x
179 185 # yield manifest before changelog
180 186 meta = self._walk('', False)
181 187 meta.reverse()
182 188 for x in meta:
183 189 yield x
184 190
185 191 def copylist(self):
186 192 return ['requires'] + _data.split()
187 193
188 194 class encodedstore(basicstore):
189 195 def __init__(self, path, opener, pathjoiner):
190 196 self.pathjoiner = pathjoiner
191 197 self.path = self.pathjoiner(path, 'store')
192 198 self.createmode = _calcmode(self.path)
193 199 op = opener(self.path)
194 200 op.createmode = self.createmode
195 201 self.opener = lambda f, *args, **kw: op(encodefilename(f), *args, **kw)
196 202
197 203 def datafiles(self):
198 204 for a, b, size in self._walk('data', True):
199 205 try:
200 206 a = decodefilename(a)
201 207 except KeyError:
202 208 a = None
203 209 yield a, b, size
204 210
205 211 def join(self, f):
206 212 return self.pathjoiner(self.path, encodefilename(f))
207 213
208 214 def copylist(self):
209 215 return (['requires', '00changelog.i'] +
210 216 [self.pathjoiner('store', f) for f in _data.split()])
211 217
212 218 def fncache(opener):
213 219 '''yields the entries in the fncache file'''
214 220 try:
215 221 fp = opener('fncache', mode='rb')
216 222 except IOError:
217 223 # skip nonexistent file
218 224 return
219 225 for n, line in enumerate(fp):
220 226 if (len(line) < 2) or (line[-1] != '\n'):
221 227 t = _('invalid entry in fncache, line %s') % (n + 1)
222 228 raise util.Abort(t)
223 229 yield line[:-1]
224 230 fp.close()
225 231
226 232 class fncacheopener(object):
227 233 def __init__(self, opener):
228 234 self.opener = opener
229 235 self.entries = None
230 236
231 237 def loadfncache(self):
232 238 self.entries = {}
233 239 for f in fncache(self.opener):
234 240 self.entries[f] = True
235 241
236 242 def __call__(self, path, mode='r', *args, **kw):
237 243 if mode not in ('r', 'rb') and path.startswith('data/'):
238 244 if self.entries is None:
239 245 self.loadfncache()
240 246 if path not in self.entries:
241 247 self.opener('fncache', 'ab').write(path + '\n')
242 248 # fncache may contain non-existent files after rollback / strip
243 249 self.entries[path] = True
244 250 return self.opener(hybridencode(path), mode, *args, **kw)
245 251
246 252 class fncachestore(basicstore):
247 253 def __init__(self, path, opener, pathjoiner):
248 254 self.pathjoiner = pathjoiner
249 255 self.path = self.pathjoiner(path, 'store')
250 256 self.createmode = _calcmode(self.path)
251 257 self._op = opener(self.path)
252 258 self._op.createmode = self.createmode
253 259 self.opener = fncacheopener(self._op)
254 260
255 261 def join(self, f):
256 262 return self.pathjoiner(self.path, hybridencode(f))
257 263
258 264 def datafiles(self):
259 265 rewrite = False
260 266 existing = []
261 267 pjoin = self.pathjoiner
262 268 spath = self.path
263 269 for f in fncache(self._op):
264 270 ef = hybridencode(f)
265 271 try:
266 272 st = os.stat(pjoin(spath, ef))
267 273 yield f, ef, st.st_size
268 274 existing.append(f)
269 275 except OSError:
270 276 # nonexistent entry
271 277 rewrite = True
272 278 if rewrite:
273 279 # rewrite fncache to remove nonexistent entries
274 280 # (may be caused by rollback / strip)
275 281 fp = self._op('fncache', mode='wb')
276 282 for p in existing:
277 283 fp.write(p + '\n')
278 284 fp.close()
279 285
280 286 def copylist(self):
281 287 d = _data + ' dh fncache'
282 288 return (['requires', '00changelog.i'] +
283 289 [self.pathjoiner('store', f) for f in d.split()])
284 290
285 291 def store(requirements, path, opener, pathjoiner=None):
286 292 pathjoiner = pathjoiner or os.path.join
287 293 if 'store' in requirements:
288 294 if 'fncache' in requirements:
289 295 return fncachestore(path, opener, pathjoiner)
290 296 return encodedstore(path, opener, pathjoiner)
291 297 return basicstore(path, opener, pathjoiner)
@@ -1,16 +1,19 b''
1 1 #!/usr/bin/env python
2 2
3 3 from mercurial import store
4 4
5 5 enc = store.hybridencode # used for fncache repo format
6 6
7 7 def show(s):
8 8 print "A = '%s'" % s
9 9 print "B = '%s'" % enc(s)
10 10 print
11 11
12 12 show('data/aux.bla/bla.aux/prn/PRN/lpt/com3/nul/coma/foo.NUL/normal.c.i')
13 13
14 14 show('data/AUX/SECOND/X.PRN/FOURTH/FI:FTH/SIXTH/SEVENTH/EIGHTH/NINETH/TENTH/ELEVENTH/LOREMIPSUM.TXT.i')
15 15 show('data/enterprise/openesbaddons/contrib-imola/corba-bc/netbeansplugin/wsdlExtension/src/main/java/META-INF/services/org.netbeans.modules.xml.wsdl.bindingsupport.spi.ExtensibilityElementTemplateProvider.i')
16 16 show('data/AUX.THE-QUICK-BROWN-FOX-JU:MPS-OVER-THE-LAZY-DOG-THE-QUICK-BROWN-FOX-JUMPS-OVER-THE-LAZY-DOG.TXT.i')
17 show('data/Project Planning/Resources/AnotherLongDirectoryName/Followedbyanother/AndAnother/AndThenAnExtremelyLongFileName.txt')
18 show('data/Project.Planning/Resources/AnotherLongDirectoryName/Followedbyanother/AndAnother/AndThenAnExtremelyLongFileName.txt')
19 show('data/foo.../foo / /a./_. /__/.x../ bla/something.i')
@@ -1,12 +1,21 b''
1 1 A = 'data/aux.bla/bla.aux/prn/PRN/lpt/com3/nul/coma/foo.NUL/normal.c.i'
2 2 B = 'data/au~78.bla/bla.aux/pr~6e/_p_r_n/lpt/co~6d3/nu~6c/coma/foo._n_u_l/normal.c.i'
3 3
4 4 A = 'data/AUX/SECOND/X.PRN/FOURTH/FI:FTH/SIXTH/SEVENTH/EIGHTH/NINETH/TENTH/ELEVENTH/LOREMIPSUM.TXT.i'
5 5 B = 'dh/au~78/second/x.prn/fourth/fi~3afth/sixth/seventh/eighth/nineth/tenth/loremia20419e358ddff1bf8751e38288aff1d7c32ec05.i'
6 6
7 7 A = 'data/enterprise/openesbaddons/contrib-imola/corba-bc/netbeansplugin/wsdlExtension/src/main/java/META-INF/services/org.netbeans.modules.xml.wsdl.bindingsupport.spi.ExtensibilityElementTemplateProvider.i'
8 8 B = 'dh/enterpri/openesba/contrib-/corba-bc/netbeans/wsdlexte/src/main/java/org.net7018f27961fdf338a598a40c4683429e7ffb9743.i'
9 9
10 10 A = 'data/AUX.THE-QUICK-BROWN-FOX-JU:MPS-OVER-THE-LAZY-DOG-THE-QUICK-BROWN-FOX-JUMPS-OVER-THE-LAZY-DOG.TXT.i'
11 11 B = 'dh/au~78.the-quick-brown-fox-ju~3amps-over-the-lazy-dog-the-quick-brown-fox-jud4dcadd033000ab2b26eb66bae1906bcb15d4a70.i'
12 12
13 A = 'data/Project Planning/Resources/AnotherLongDirectoryName/Followedbyanother/AndAnother/AndThenAnExtremelyLongFileName.txt'
14 B = 'dh/project_/resource/anotherl/followed/andanoth/andthenanextremelylongfilenaf93030515d9849cfdca52937c2204d19f83913e5.txt'
15
16 A = 'data/Project.Planning/Resources/AnotherLongDirectoryName/Followedbyanother/AndAnother/AndThenAnExtremelyLongFileName.txt'
17 B = 'dh/project_/resource/anotherl/followed/andanoth/andthenanextremelylongfilena0fd7c506f5c9d58204444fc67e9499006bd2d445.txt'
18
19 A = 'data/foo.../foo / /a./_. /__/.x../ bla/something.i'
20 B = 'data/foo..~2e/foo ~20/~20/a~2e/__.~20/____/.x.~2e/ bla/something.i'
21
General Comments 0
You need to be logged in to leave comments. Login now