##// END OF EJS Templates
convert: refactor hg getchanges and caching
Mads Kiilerich -
r22299:98aafdf4 default
parent child Browse files
Show More
@@ -1,472 +1,470
1 1 # hg.py - hg backend for convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # Notes for hg->hg conversion:
9 9 #
10 10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 11 # of commit messages, but new versions do. Changesets created by
12 12 # those older versions, then converted, may thus have different
13 13 # hashes for changesets that are otherwise identical.
14 14 #
15 15 # * Using "--config convert.hg.saverev=true" will make the source
16 16 # identifier to be stored in the converted revision. This will cause
17 17 # the converted revision to have a different identity than the
18 18 # source.
19 19
20 20
21 21 import os, time, cStringIO
22 22 from mercurial.i18n import _
23 23 from mercurial.node import bin, hex, nullid
24 24 from mercurial import hg, util, context, bookmarks, error, scmutil
25 25
26 26 from common import NoRepo, commit, converter_source, converter_sink
27 27
28 28 import re
29 29 sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
30 30
31 31 class mercurial_sink(converter_sink):
32 32 def __init__(self, ui, path):
33 33 converter_sink.__init__(self, ui, path)
34 34 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
35 35 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
36 36 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
37 37 self.lastbranch = None
38 38 if os.path.isdir(path) and len(os.listdir(path)) > 0:
39 39 try:
40 40 self.repo = hg.repository(self.ui, path)
41 41 if not self.repo.local():
42 42 raise NoRepo(_('%s is not a local Mercurial repository')
43 43 % path)
44 44 except error.RepoError, err:
45 45 ui.traceback()
46 46 raise NoRepo(err.args[0])
47 47 else:
48 48 try:
49 49 ui.status(_('initializing destination %s repository\n') % path)
50 50 self.repo = hg.repository(self.ui, path, create=True)
51 51 if not self.repo.local():
52 52 raise NoRepo(_('%s is not a local Mercurial repository')
53 53 % path)
54 54 self.created.append(path)
55 55 except error.RepoError:
56 56 ui.traceback()
57 57 raise NoRepo(_("could not create hg repository %s as sink")
58 58 % path)
59 59 self.lock = None
60 60 self.wlock = None
61 61 self.filemapmode = False
62 62
63 63 def before(self):
64 64 self.ui.debug('run hg sink pre-conversion action\n')
65 65 self.wlock = self.repo.wlock()
66 66 self.lock = self.repo.lock()
67 67
68 68 def after(self):
69 69 self.ui.debug('run hg sink post-conversion action\n')
70 70 if self.lock:
71 71 self.lock.release()
72 72 if self.wlock:
73 73 self.wlock.release()
74 74
75 75 def revmapfile(self):
76 76 return self.repo.join("shamap")
77 77
78 78 def authorfile(self):
79 79 return self.repo.join("authormap")
80 80
81 81 def setbranch(self, branch, pbranches):
82 82 if not self.clonebranches:
83 83 return
84 84
85 85 setbranch = (branch != self.lastbranch)
86 86 self.lastbranch = branch
87 87 if not branch:
88 88 branch = 'default'
89 89 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
90 90 pbranch = pbranches and pbranches[0][1] or 'default'
91 91
92 92 branchpath = os.path.join(self.path, branch)
93 93 if setbranch:
94 94 self.after()
95 95 try:
96 96 self.repo = hg.repository(self.ui, branchpath)
97 97 except Exception:
98 98 self.repo = hg.repository(self.ui, branchpath, create=True)
99 99 self.before()
100 100
101 101 # pbranches may bring revisions from other branches (merge parents)
102 102 # Make sure we have them, or pull them.
103 103 missings = {}
104 104 for b in pbranches:
105 105 try:
106 106 self.repo.lookup(b[0])
107 107 except Exception:
108 108 missings.setdefault(b[1], []).append(b[0])
109 109
110 110 if missings:
111 111 self.after()
112 112 for pbranch, heads in sorted(missings.iteritems()):
113 113 pbranchpath = os.path.join(self.path, pbranch)
114 114 prepo = hg.peer(self.ui, {}, pbranchpath)
115 115 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
116 116 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
117 117 self.before()
118 118
119 119 def _rewritetags(self, source, revmap, data):
120 120 fp = cStringIO.StringIO()
121 121 for line in data.splitlines():
122 122 s = line.split(' ', 1)
123 123 if len(s) != 2:
124 124 continue
125 125 revid = revmap.get(source.lookuprev(s[0]))
126 126 if not revid:
127 127 continue
128 128 fp.write('%s %s\n' % (revid, s[1]))
129 129 return fp.getvalue()
130 130
131 131 def putcommit(self, files, copies, parents, commit, source, revmap):
132 132
133 133 files = dict(files)
134 134 def getfilectx(repo, memctx, f):
135 135 v = files[f]
136 136 data, mode = source.getfile(f, v)
137 137 if data is None:
138 138 return None
139 139 if f == '.hgtags':
140 140 data = self._rewritetags(source, revmap, data)
141 141 return context.memfilectx(self.repo, f, data, 'l' in mode,
142 142 'x' in mode, copies.get(f))
143 143
144 144 pl = []
145 145 for p in parents:
146 146 if p not in pl:
147 147 pl.append(p)
148 148 parents = pl
149 149 nparents = len(parents)
150 150 if self.filemapmode and nparents == 1:
151 151 m1node = self.repo.changelog.read(bin(parents[0]))[0]
152 152 parent = parents[0]
153 153
154 154 if len(parents) < 2:
155 155 parents.append(nullid)
156 156 if len(parents) < 2:
157 157 parents.append(nullid)
158 158 p2 = parents.pop(0)
159 159
160 160 text = commit.desc
161 161
162 162 sha1s = re.findall(sha1re, text)
163 163 for sha1 in sha1s:
164 164 oldrev = source.lookuprev(sha1)
165 165 newrev = revmap.get(oldrev)
166 166 if newrev is not None:
167 167 text = text.replace(sha1, newrev[:len(sha1)])
168 168
169 169 extra = commit.extra.copy()
170 170
171 171 for label in ('source', 'transplant_source', 'rebase_source'):
172 172 node = extra.get(label)
173 173
174 174 if node is None:
175 175 continue
176 176
177 177 # Only transplant stores its reference in binary
178 178 if label == 'transplant_source':
179 179 node = hex(node)
180 180
181 181 newrev = revmap.get(node)
182 182 if newrev is not None:
183 183 if label == 'transplant_source':
184 184 newrev = bin(newrev)
185 185
186 186 extra[label] = newrev
187 187
188 188 if self.branchnames and commit.branch:
189 189 extra['branch'] = commit.branch
190 190 if commit.rev:
191 191 extra['convert_revision'] = commit.rev
192 192
193 193 while parents:
194 194 p1 = p2
195 195 p2 = parents.pop(0)
196 196 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(),
197 197 getfilectx, commit.author, commit.date, extra)
198 198 self.repo.commitctx(ctx)
199 199 text = "(octopus merge fixup)\n"
200 200 p2 = hex(self.repo.changelog.tip())
201 201
202 202 if self.filemapmode and nparents == 1:
203 203 man = self.repo.manifest
204 204 mnode = self.repo.changelog.read(bin(p2))[0]
205 205 closed = 'close' in commit.extra
206 206 if not closed and not man.cmp(m1node, man.revision(mnode)):
207 207 self.ui.status(_("filtering out empty revision\n"))
208 208 self.repo.rollback(force=True)
209 209 return parent
210 210 return p2
211 211
212 212 def puttags(self, tags):
213 213 try:
214 214 parentctx = self.repo[self.tagsbranch]
215 215 tagparent = parentctx.node()
216 216 except error.RepoError:
217 217 parentctx = None
218 218 tagparent = nullid
219 219
220 220 oldlines = set()
221 221 for branch, heads in self.repo.branchmap().iteritems():
222 222 for h in heads:
223 223 if '.hgtags' in self.repo[h]:
224 224 oldlines.update(
225 225 set(self.repo[h]['.hgtags'].data().splitlines(True)))
226 226 oldlines = sorted(list(oldlines))
227 227
228 228 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
229 229 if newlines == oldlines:
230 230 return None, None
231 231
232 232 # if the old and new tags match, then there is nothing to update
233 233 oldtags = set()
234 234 newtags = set()
235 235 for line in oldlines:
236 236 s = line.strip().split(' ', 1)
237 237 if len(s) != 2:
238 238 continue
239 239 oldtags.add(s[1])
240 240 for line in newlines:
241 241 s = line.strip().split(' ', 1)
242 242 if len(s) != 2:
243 243 continue
244 244 if s[1] not in oldtags:
245 245 newtags.add(s[1].strip())
246 246
247 247 if not newtags:
248 248 return None, None
249 249
250 250 data = "".join(newlines)
251 251 def getfilectx(repo, memctx, f):
252 252 return context.memfilectx(repo, f, data, False, False, None)
253 253
254 254 self.ui.status(_("updating tags\n"))
255 255 date = "%s 0" % int(time.mktime(time.gmtime()))
256 256 extra = {'branch': self.tagsbranch}
257 257 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
258 258 [".hgtags"], getfilectx, "convert-repo", date,
259 259 extra)
260 260 self.repo.commitctx(ctx)
261 261 return hex(self.repo.changelog.tip()), hex(tagparent)
262 262
263 263 def setfilemapmode(self, active):
264 264 self.filemapmode = active
265 265
266 266 def putbookmarks(self, updatedbookmark):
267 267 if not len(updatedbookmark):
268 268 return
269 269
270 270 self.ui.status(_("updating bookmarks\n"))
271 271 destmarks = self.repo._bookmarks
272 272 for bookmark in updatedbookmark:
273 273 destmarks[bookmark] = bin(updatedbookmark[bookmark])
274 274 destmarks.write()
275 275
276 276 def hascommitfrommap(self, rev):
277 277 # the exact semantics of clonebranches is unclear so we can't say no
278 278 return rev in self.repo or self.clonebranches
279 279
280 280 def hascommitforsplicemap(self, rev):
281 281 if rev not in self.repo and self.clonebranches:
282 282 raise util.Abort(_('revision %s not found in destination '
283 283 'repository (lookups with clonebranches=true '
284 284 'are not implemented)') % rev)
285 285 return rev in self.repo
286 286
287 287 class mercurial_source(converter_source):
288 288 def __init__(self, ui, path, rev=None):
289 289 converter_source.__init__(self, ui, path, rev)
290 290 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
291 291 self.ignored = set()
292 292 self.saverev = ui.configbool('convert', 'hg.saverev', False)
293 293 try:
294 294 self.repo = hg.repository(self.ui, path)
295 295 # try to provoke an exception if this isn't really a hg
296 296 # repo, but some other bogus compatible-looking url
297 297 if not self.repo.local():
298 298 raise error.RepoError
299 299 except error.RepoError:
300 300 ui.traceback()
301 301 raise NoRepo(_("%s is not a local Mercurial repository") % path)
302 302 self.lastrev = None
303 303 self.lastctx = None
304 self._changescache = None
304 self._changescache = None, None
305 305 self.convertfp = None
306 306 # Restrict converted revisions to startrev descendants
307 307 startnode = ui.config('convert', 'hg.startrev')
308 308 hgrevs = ui.config('convert', 'hg.revs')
309 309 if hgrevs is None:
310 310 if startnode is not None:
311 311 try:
312 312 startnode = self.repo.lookup(startnode)
313 313 except error.RepoError:
314 314 raise util.Abort(_('%s is not a valid start revision')
315 315 % startnode)
316 316 startrev = self.repo.changelog.rev(startnode)
317 317 children = {startnode: 1}
318 318 for r in self.repo.changelog.descendants([startrev]):
319 319 children[self.repo.changelog.node(r)] = 1
320 320 self.keep = children.__contains__
321 321 else:
322 322 self.keep = util.always
323 323 if rev:
324 324 self._heads = [self.repo[rev].node()]
325 325 else:
326 326 self._heads = self.repo.heads()
327 327 else:
328 328 if rev or startnode is not None:
329 329 raise util.Abort(_('hg.revs cannot be combined with '
330 330 'hg.startrev or --rev'))
331 331 nodes = set()
332 332 parents = set()
333 333 for r in scmutil.revrange(self.repo, [hgrevs]):
334 334 ctx = self.repo[r]
335 335 nodes.add(ctx.node())
336 336 parents.update(p.node() for p in ctx.parents())
337 337 self.keep = nodes.__contains__
338 338 self._heads = nodes - parents
339 339
340 340 def changectx(self, rev):
341 341 if self.lastrev != rev:
342 342 self.lastctx = self.repo[rev]
343 343 self.lastrev = rev
344 344 return self.lastctx
345 345
346 346 def parents(self, ctx):
347 347 return [p for p in ctx.parents() if p and self.keep(p.node())]
348 348
349 349 def getheads(self):
350 350 return [hex(h) for h in self._heads if self.keep(h)]
351 351
352 352 def getfile(self, name, rev):
353 353 try:
354 354 fctx = self.changectx(rev)[name]
355 355 return fctx.data(), fctx.flags()
356 356 except error.LookupError:
357 357 return None, None
358 358
359 359 def getchanges(self, rev):
360 360 ctx = self.changectx(rev)
361 361 parents = self.parents(ctx)
362 362 if not parents:
363 files = sorted(ctx.manifest())
364 # getcopies() is not needed for roots, but it is a simple way to
365 # detect missing revlogs and abort on errors or populate
366 # self.ignored
367 self.getcopies(ctx, parents, files)
368 return [(f, rev) for f in files if f not in self.ignored], {}
369 if self._changescache and self._changescache[0] == rev:
370 m, a, r = self._changescache[1]
363 files = copyfiles = ctx.manifest()
371 364 else:
372 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
373 # getcopies() detects missing revlogs early, run it before
374 # filtering the changes.
375 copies = self.getcopies(ctx, parents, m + a)
376 changes = [(name, rev) for name in m + a + r
377 if name not in self.ignored]
378 return sorted(changes), copies
365 if self._changescache[0] == rev:
366 m, a, r = self._changescache[1]
367 else:
368 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
369 files = m + a + r
370 copyfiles = m + a
371 # getcopies() is also run for roots and before filtering so missing
372 # revlogs are detected early
373 copies = self.getcopies(ctx, parents, copyfiles)
374 changes = [(f, rev) for f in files if f not in self.ignored]
375 changes.sort()
376 return changes, copies
379 377
380 378 def getcopies(self, ctx, parents, files):
381 379 copies = {}
382 380 for name in files:
383 381 if name in self.ignored:
384 382 continue
385 383 try:
386 384 copysource, _copynode = ctx.filectx(name).renamed()
387 385 if copysource in self.ignored:
388 386 continue
389 387 # Ignore copy sources not in parent revisions
390 388 found = False
391 389 for p in parents:
392 390 if copysource in p:
393 391 found = True
394 392 break
395 393 if not found:
396 394 continue
397 395 copies[name] = copysource
398 396 except TypeError:
399 397 pass
400 398 except error.LookupError, e:
401 399 if not self.ignoreerrors:
402 400 raise
403 401 self.ignored.add(name)
404 402 self.ui.warn(_('ignoring: %s\n') % e)
405 403 return copies
406 404
407 405 def getcommit(self, rev):
408 406 ctx = self.changectx(rev)
409 407 parents = [p.hex() for p in self.parents(ctx)]
410 408 if self.saverev:
411 409 crev = rev
412 410 else:
413 411 crev = None
414 412 return commit(author=ctx.user(),
415 413 date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
416 414 desc=ctx.description(), rev=crev, parents=parents,
417 415 branch=ctx.branch(), extra=ctx.extra(),
418 416 sortkey=ctx.rev())
419 417
420 418 def gettags(self):
421 419 # This will get written to .hgtags, filter non global tags out.
422 420 tags = [t for t in self.repo.tagslist()
423 421 if self.repo.tagtype(t[0]) == 'global']
424 422 return dict([(name, hex(node)) for name, node in tags
425 423 if self.keep(node)])
426 424
427 425 def getchangedfiles(self, rev, i):
428 426 ctx = self.changectx(rev)
429 427 parents = self.parents(ctx)
430 428 if not parents and i is None:
431 429 i = 0
432 430 changes = [], ctx.manifest().keys(), []
433 431 else:
434 432 i = i or 0
435 433 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
436 434 changes = [[f for f in l if f not in self.ignored] for l in changes]
437 435
438 436 if i == 0:
439 437 self._changescache = (rev, changes)
440 438
441 439 return changes[0] + changes[1] + changes[2]
442 440
443 441 def converted(self, rev, destrev):
444 442 if self.convertfp is None:
445 443 self.convertfp = open(self.repo.join('shamap'), 'a')
446 444 self.convertfp.write('%s %s\n' % (destrev, rev))
447 445 self.convertfp.flush()
448 446
449 447 def before(self):
450 448 self.ui.debug('run hg source pre-conversion action\n')
451 449
452 450 def after(self):
453 451 self.ui.debug('run hg source post-conversion action\n')
454 452
455 453 def hasnativeorder(self):
456 454 return True
457 455
458 456 def hasnativeclose(self):
459 457 return True
460 458
461 459 def lookuprev(self, rev):
462 460 try:
463 461 return hex(self.repo.lookup(rev))
464 462 except error.RepoError:
465 463 return None
466 464
467 465 def getbookmarks(self):
468 466 return bookmarks.listbookmarks(self.repo)
469 467
470 468 def checkrevformat(self, revstr, mapname='splicemap'):
471 469 """ Mercurial, revision string is a 40 byte hex """
472 470 self.checkhexformat(revstr, mapname)
General Comments 0
You need to be logged in to leave comments. Login now