##// END OF EJS Templates
convert: fix mercurial_sink.putcommit...
Alexis S. L. Carvalho -
r5195:33015dac default
parent child Browse files
Show More
@@ -1,455 +1,461 b''
1 1 # convert.py Foreign SCM converter
2 2 #
3 3 # Copyright 2005-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 common import NoRepo, converter_source, converter_sink
9 9 from cvs import convert_cvs
10 10 from git import convert_git
11 11 from hg import mercurial_source, mercurial_sink
12 12 from subversion import convert_svn, debugsvnlog
13 13
14 14 import os, shlex, shutil
15 15 from mercurial import hg, ui, util, commands
16 16 from mercurial.i18n import _
17 17
18 18 commands.norepo += " convert debugsvnlog"
19 19
20 20 converters = [convert_cvs, convert_git, convert_svn, mercurial_source,
21 21 mercurial_sink]
22 22
23 23 def convertsource(ui, path, **opts):
24 24 for c in converters:
25 25 try:
26 26 return c.getcommit and c(ui, path, **opts)
27 27 except (AttributeError, NoRepo):
28 28 pass
29 29 raise util.Abort('%s: unknown repository type' % path)
30 30
31 31 def convertsink(ui, path):
32 32 if not os.path.isdir(path):
33 33 raise util.Abort("%s: not a directory" % path)
34 34 for c in converters:
35 35 try:
36 36 return c.putcommit and c(ui, path)
37 37 except (AttributeError, NoRepo):
38 38 pass
39 39 raise util.Abort('%s: unknown repository type' % path)
40 40
41 41 class convert(object):
42 42 def __init__(self, ui, source, dest, revmapfile, filemapper, opts):
43 43
44 44 self.source = source
45 45 self.dest = dest
46 46 self.ui = ui
47 47 self.opts = opts
48 48 self.commitcache = {}
49 49 self.revmapfile = revmapfile
50 50 self.revmapfilefd = None
51 51 self.authors = {}
52 52 self.authorfile = None
53 53 self.mapfile = filemapper
54 54
55 55 self.map = {}
56 56 try:
57 57 origrevmapfile = open(self.revmapfile, 'r')
58 58 for l in origrevmapfile:
59 59 sv, dv = l[:-1].split()
60 60 self.map[sv] = dv
61 61 origrevmapfile.close()
62 62 except IOError:
63 63 pass
64 64
65 65 # Read first the dst author map if any
66 66 authorfile = self.dest.authorfile()
67 67 if authorfile and os.path.exists(authorfile):
68 68 self.readauthormap(authorfile)
69 69 # Extend/Override with new author map if necessary
70 70 if opts.get('authors'):
71 71 self.readauthormap(opts.get('authors'))
72 72 self.authorfile = self.dest.authorfile()
73 73
74 74 def walktree(self, heads):
75 75 '''Return a mapping that identifies the uncommitted parents of every
76 76 uncommitted changeset.'''
77 77 visit = heads
78 78 known = {}
79 79 parents = {}
80 80 while visit:
81 81 n = visit.pop(0)
82 82 if n in known or n in self.map: continue
83 83 known[n] = 1
84 84 self.commitcache[n] = self.source.getcommit(n)
85 85 cp = self.commitcache[n].parents
86 86 parents[n] = []
87 87 for p in cp:
88 88 parents[n].append(p)
89 89 visit.append(p)
90 90
91 91 return parents
92 92
93 93 def toposort(self, parents):
94 94 '''Return an ordering such that every uncommitted changeset is
95 95 preceeded by all its uncommitted ancestors.'''
96 96 visit = parents.keys()
97 97 seen = {}
98 98 children = {}
99 99
100 100 while visit:
101 101 n = visit.pop(0)
102 102 if n in seen: continue
103 103 seen[n] = 1
104 104 # Ensure that nodes without parents are present in the 'children'
105 105 # mapping.
106 106 children.setdefault(n, [])
107 107 for p in parents[n]:
108 108 if not p in self.map:
109 109 visit.append(p)
110 110 children.setdefault(p, []).append(n)
111 111
112 112 s = []
113 113 removed = {}
114 114 visit = children.keys()
115 115 while visit:
116 116 n = visit.pop(0)
117 117 if n in removed: continue
118 118 dep = 0
119 119 if n in parents:
120 120 for p in parents[n]:
121 121 if p in self.map: continue
122 122 if p not in removed:
123 123 # we're still dependent
124 124 visit.append(n)
125 125 dep = 1
126 126 break
127 127
128 128 if not dep:
129 129 # all n's parents are in the list
130 130 removed[n] = 1
131 131 if n not in self.map:
132 132 s.append(n)
133 133 if n in children:
134 134 for c in children[n]:
135 135 visit.insert(0, c)
136 136
137 137 if self.opts.get('datesort'):
138 138 depth = {}
139 139 for n in s:
140 140 depth[n] = 0
141 141 pl = [p for p in self.commitcache[n].parents
142 142 if p not in self.map]
143 143 if pl:
144 144 depth[n] = max([depth[p] for p in pl]) + 1
145 145
146 146 s = [(depth[n], self.commitcache[n].date, n) for n in s]
147 147 s.sort()
148 148 s = [e[2] for e in s]
149 149
150 150 return s
151 151
152 152 def mapentry(self, src, dst):
153 153 if self.revmapfilefd is None:
154 154 try:
155 155 self.revmapfilefd = open(self.revmapfile, "a")
156 156 except IOError, (errno, strerror):
157 157 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror))
158 158 self.map[src] = dst
159 159 self.revmapfilefd.write("%s %s\n" % (src, dst))
160 160 self.revmapfilefd.flush()
161 161
162 162 def writeauthormap(self):
163 163 authorfile = self.authorfile
164 164 if authorfile:
165 165 self.ui.status('Writing author map file %s\n' % authorfile)
166 166 ofile = open(authorfile, 'w+')
167 167 for author in self.authors:
168 168 ofile.write("%s=%s\n" % (author, self.authors[author]))
169 169 ofile.close()
170 170
171 171 def readauthormap(self, authorfile):
172 172 afile = open(authorfile, 'r')
173 173 for line in afile:
174 174 try:
175 175 srcauthor = line.split('=')[0].strip()
176 176 dstauthor = line.split('=')[1].strip()
177 177 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
178 178 self.ui.status(
179 179 'Overriding mapping for author %s, was %s, will be %s\n'
180 180 % (srcauthor, self.authors[srcauthor], dstauthor))
181 181 else:
182 182 self.ui.debug('Mapping author %s to %s\n'
183 183 % (srcauthor, dstauthor))
184 184 self.authors[srcauthor] = dstauthor
185 185 except IndexError:
186 186 self.ui.warn(
187 187 'Ignoring bad line in author file map %s: %s\n'
188 188 % (authorfile, line))
189 189 afile.close()
190 190
191 191 def copy(self, rev):
192 192 commit = self.commitcache[rev]
193 193 do_copies = hasattr(self.dest, 'copyfile')
194 194 filenames = []
195 195
196 196 files, copies = self.source.getchanges(rev)
197 197 parents = [self.map[r] for r in commit.parents]
198 198 if commit.parents:
199 199 pbranch = self.commitcache[commit.parents[0]].branch
200 200 else:
201 201 pbranch = None
202 202 self.dest.setbranch(commit.branch, pbranch, parents)
203 203 for f, v in files:
204 204 newf = self.mapfile(f)
205 205 if not newf:
206 206 continue
207 207 filenames.append(newf)
208 208 try:
209 209 data = self.source.getfile(f, v)
210 210 except IOError, inst:
211 211 self.dest.delfile(newf)
212 212 else:
213 213 e = self.source.getmode(f, v)
214 214 self.dest.putfile(newf, e, data)
215 215 if do_copies:
216 216 if f in copies:
217 217 copyf = self.mapfile(copies[f])
218 218 if copyf:
219 219 # Merely marks that a copy happened.
220 220 self.dest.copyfile(copyf, newf)
221 221
222 newnode = self.dest.putcommit(filenames, parents, commit)
222 if not filenames and self.mapfile.active():
223 newnode = parents[0]
224 else:
225 newnode = self.dest.putcommit(filenames, parents, commit)
223 226 self.mapentry(rev, newnode)
224 227
225 228 def convert(self):
226 229 try:
227 230 self.dest.before()
228 231 self.source.setrevmap(self.map)
229 232 self.ui.status("scanning source...\n")
230 233 heads = self.source.getheads()
231 234 parents = self.walktree(heads)
232 235 self.ui.status("sorting...\n")
233 236 t = self.toposort(parents)
234 237 num = len(t)
235 238 c = None
236 239
237 240 self.ui.status("converting...\n")
238 241 for c in t:
239 242 num -= 1
240 243 desc = self.commitcache[c].desc
241 244 if "\n" in desc:
242 245 desc = desc.splitlines()[0]
243 246 author = self.commitcache[c].author
244 247 author = self.authors.get(author, author)
245 248 self.commitcache[c].author = author
246 249 self.ui.status("%d %s\n" % (num, desc))
247 250 self.copy(c)
248 251
249 252 tags = self.source.gettags()
250 253 ctags = {}
251 254 for k in tags:
252 255 v = tags[k]
253 256 if v in self.map:
254 257 ctags[k] = self.map[v]
255 258
256 259 if c and ctags:
257 260 nrev = self.dest.puttags(ctags)
258 261 # write another hash correspondence to override the previous
259 262 # one so we don't end up with extra tag heads
260 263 if nrev:
261 264 self.mapentry(c, nrev)
262 265
263 266 self.writeauthormap()
264 267 finally:
265 268 self.cleanup()
266 269
267 270 def cleanup(self):
268 271 self.dest.after()
269 272 if self.revmapfilefd:
270 273 self.revmapfilefd.close()
271 274
272 275 def rpairs(name):
273 276 e = len(name)
274 277 while e != -1:
275 278 yield name[:e], name[e+1:]
276 279 e = name.rfind('/', 0, e)
277 280
278 281 class filemapper(object):
279 282 '''Map and filter filenames when importing.
280 283 A name can be mapped to itself, a new name, or None (omit from new
281 284 repository).'''
282 285
283 286 def __init__(self, ui, path=None):
284 287 self.ui = ui
285 288 self.include = {}
286 289 self.exclude = {}
287 290 self.rename = {}
288 291 if path:
289 292 if self.parse(path):
290 293 raise util.Abort(_('errors in filemap'))
291 294
292 295 def parse(self, path):
293 296 errs = 0
294 297 def check(name, mapping, listname):
295 298 if name in mapping:
296 299 self.ui.warn(_('%s:%d: %r already in %s list\n') %
297 300 (lex.infile, lex.lineno, name, listname))
298 301 return 1
299 302 return 0
300 303 lex = shlex.shlex(open(path), path, True)
301 304 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
302 305 cmd = lex.get_token()
303 306 while cmd:
304 307 if cmd == 'include':
305 308 name = lex.get_token()
306 309 errs += check(name, self.exclude, 'exclude')
307 310 self.include[name] = name
308 311 elif cmd == 'exclude':
309 312 name = lex.get_token()
310 313 errs += check(name, self.include, 'include')
311 314 errs += check(name, self.rename, 'rename')
312 315 self.exclude[name] = name
313 316 elif cmd == 'rename':
314 317 src = lex.get_token()
315 318 dest = lex.get_token()
316 319 errs += check(src, self.exclude, 'exclude')
317 320 self.rename[src] = dest
318 321 elif cmd == 'source':
319 322 errs += self.parse(lex.get_token())
320 323 else:
321 324 self.ui.warn(_('%s:%d: unknown directive %r\n') %
322 325 (lex.infile, lex.lineno, cmd))
323 326 errs += 1
324 327 cmd = lex.get_token()
325 328 return errs
326 329
327 330 def lookup(self, name, mapping):
328 331 for pre, suf in rpairs(name):
329 332 try:
330 333 return mapping[pre], pre, suf
331 334 except KeyError, err:
332 335 pass
333 336 return '', name, ''
334 337
335 338 def __call__(self, name):
336 339 if self.include:
337 340 inc = self.lookup(name, self.include)[0]
338 341 else:
339 342 inc = name
340 343 if self.exclude:
341 344 exc = self.lookup(name, self.exclude)[0]
342 345 else:
343 346 exc = ''
344 347 if not inc or exc:
345 348 return None
346 349 newpre, pre, suf = self.lookup(name, self.rename)
347 350 if newpre:
348 351 if newpre == '.':
349 352 return suf
350 353 if suf:
351 354 return newpre + '/' + suf
352 355 return newpre
353 356 return name
354 357
358 def active(self):
359 return bool(self.include or self.exclude or self.rename)
360
355 361 def _convert(ui, src, dest=None, revmapfile=None, **opts):
356 362 """Convert a foreign SCM repository to a Mercurial one.
357 363
358 364 Accepted source formats:
359 365 - GIT
360 366 - CVS
361 367 - SVN
362 368
363 369 Accepted destination formats:
364 370 - Mercurial
365 371
366 372 If no revision is given, all revisions will be converted. Otherwise,
367 373 convert will only import up to the named revision (given in a format
368 374 understood by the source).
369 375
370 376 If no destination directory name is specified, it defaults to the
371 377 basename of the source with '-hg' appended. If the destination
372 378 repository doesn't exist, it will be created.
373 379
374 380 If <revmapfile> isn't given, it will be put in a default location
375 381 (<dest>/.hg/shamap by default). The <revmapfile> is a simple text
376 382 file that maps each source commit ID to the destination ID for
377 383 that revision, like so:
378 384 <source ID> <destination ID>
379 385
380 386 If the file doesn't exist, it's automatically created. It's updated
381 387 on each commit copied, so convert-repo can be interrupted and can
382 388 be run repeatedly to copy new commits.
383 389
384 390 The [username mapping] file is a simple text file that maps each source
385 391 commit author to a destination commit author. It is handy for source SCMs
386 392 that use unix logins to identify authors (eg: CVS). One line per author
387 393 mapping and the line format is:
388 394 srcauthor=whatever string you want
389 395 """
390 396
391 397 util._encoding = 'UTF-8'
392 398
393 399 if not dest:
394 400 dest = hg.defaultdest(src) + "-hg"
395 401 ui.status("assuming destination %s\n" % dest)
396 402
397 403 # Try to be smart and initalize things when required
398 404 created = False
399 405 if os.path.isdir(dest):
400 406 if len(os.listdir(dest)) > 0:
401 407 try:
402 408 hg.repository(ui, dest)
403 409 ui.status("destination %s is a Mercurial repository\n" % dest)
404 410 except hg.RepoError:
405 411 raise util.Abort(
406 412 "destination directory %s is not empty.\n"
407 413 "Please specify an empty directory to be initialized\n"
408 414 "or an already initialized mercurial repository"
409 415 % dest)
410 416 else:
411 417 ui.status("initializing destination %s repository\n" % dest)
412 418 hg.repository(ui, dest, create=True)
413 419 created = True
414 420 elif os.path.exists(dest):
415 421 raise util.Abort("destination %s exists and is not a directory" % dest)
416 422 else:
417 423 ui.status("initializing destination %s repository\n" % dest)
418 424 hg.repository(ui, dest, create=True)
419 425 created = True
420 426
421 427 destc = convertsink(ui, dest)
422 428
423 429 try:
424 430 srcc = convertsource(ui, src, rev=opts.get('rev'))
425 431 except Exception:
426 432 if created:
427 433 shutil.rmtree(dest, True)
428 434 raise
429 435
430 436 if not revmapfile:
431 437 try:
432 438 revmapfile = destc.revmapfile()
433 439 except:
434 440 revmapfile = os.path.join(destc, "map")
435 441
436 442
437 443 c = convert(ui, srcc, destc, revmapfile, filemapper(ui, opts['filemap']),
438 444 opts)
439 445 c.convert()
440 446
441 447
442 448 cmdtable = {
443 449 "convert":
444 450 (_convert,
445 451 [('A', 'authors', '', 'username mapping filename'),
446 452 ('', 'filemap', '', 'remap file names using contents of file'),
447 453 ('r', 'rev', '', 'import up to target revision REV'),
448 454 ('', 'datesort', None, 'try to sort changesets by date')],
449 455 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
450 456 "debugsvnlog":
451 457 (debugsvnlog,
452 458 [],
453 459 'hg debugsvnlog'),
454 460 }
455 461
@@ -1,204 +1,201 b''
1 1 # hg backend for convert extension
2 2
3 3 # Note for hg->hg conversion: Old versions of Mercurial didn't trim
4 4 # the whitespace from the ends of commit messages, but new versions
5 5 # do. Changesets created by those older versions, then converted, may
6 6 # thus have different hashes for changesets that are otherwise
7 7 # identical.
8 8
9 9
10 10 import os, time
11 11 from mercurial.i18n import _
12 12 from mercurial.node import *
13 13 from mercurial import hg, lock, revlog, util
14 14
15 15 from common import NoRepo, commit, converter_source, converter_sink
16 16
17 17 class mercurial_sink(converter_sink):
18 18 def __init__(self, ui, path):
19 19 self.path = path
20 20 self.ui = ui
21 21 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
22 22 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
23 23 self.lastbranch = None
24 24 try:
25 25 self.repo = hg.repository(self.ui, path)
26 26 except:
27 27 raise NoRepo("could not open hg repo %s as sink" % path)
28 28 self.lock = None
29 29 self.wlock = None
30 30
31 31 def before(self):
32 32 self.wlock = self.repo.wlock()
33 33 self.lock = self.repo.lock()
34 34
35 35 def after(self):
36 36 self.lock = None
37 37 self.wlock = None
38 38
39 39 def revmapfile(self):
40 40 return os.path.join(self.path, ".hg", "shamap")
41 41
42 42 def authorfile(self):
43 43 return os.path.join(self.path, ".hg", "authormap")
44 44
45 45 def getheads(self):
46 46 h = self.repo.changelog.heads()
47 47 return [ hex(x) for x in h ]
48 48
49 49 def putfile(self, f, e, data):
50 50 self.repo.wwrite(f, data, e)
51 51 if f not in self.repo.dirstate:
52 52 self.repo.dirstate.add(f)
53 53
54 54 def copyfile(self, source, dest):
55 55 self.repo.copy(source, dest)
56 56
57 57 def delfile(self, f):
58 58 try:
59 59 os.unlink(self.repo.wjoin(f))
60 60 #self.repo.remove([f])
61 61 except:
62 62 pass
63 63
64 64 def setbranch(self, branch, pbranch, parents):
65 65 if (not self.clonebranches) or (branch == self.lastbranch):
66 66 return
67 67
68 68 self.lastbranch = branch
69 69 self.after()
70 70 if not branch:
71 71 branch = 'default'
72 72 if not pbranch:
73 73 pbranch = 'default'
74 74
75 75 branchpath = os.path.join(self.path, branch)
76 76 try:
77 77 self.repo = hg.repository(self.ui, branchpath)
78 78 except:
79 79 if not parents:
80 80 self.repo = hg.repository(self.ui, branchpath, create=True)
81 81 else:
82 82 self.ui.note(_('cloning branch %s to %s\n') % (pbranch, branch))
83 83 hg.clone(self.ui, os.path.join(self.path, pbranch),
84 84 branchpath, rev=parents, update=False,
85 85 stream=True)
86 86 self.repo = hg.repository(self.ui, branchpath)
87 87
88 88 def putcommit(self, files, parents, commit):
89 if not files:
90 return hex(self.repo.changelog.tip())
91
92 seen = {hex(nullid): 1}
89 seen = {}
93 90 pl = []
94 91 for p in parents:
95 92 if p not in seen:
96 93 pl.append(p)
97 94 seen[p] = 1
98 95 parents = pl
99 96
100 97 if len(parents) < 2: parents.append("0" * 40)
101 98 if len(parents) < 2: parents.append("0" * 40)
102 99 p2 = parents.pop(0)
103 100
104 101 text = commit.desc
105 102 extra = {}
106 103 if self.branchnames and commit.branch:
107 104 extra['branch'] = commit.branch
108 105 if commit.rev:
109 106 extra['convert_revision'] = commit.rev
110 107
111 108 while parents:
112 109 p1 = p2
113 110 p2 = parents.pop(0)
114 111 a = self.repo.rawcommit(files, text, commit.author, commit.date,
115 112 bin(p1), bin(p2), extra=extra)
116 113 self.repo.dirstate.invalidate()
117 114 text = "(octopus merge fixup)\n"
118 115 p2 = hg.hex(self.repo.changelog.tip())
119 116
120 117 return p2
121 118
122 119 def puttags(self, tags):
123 120 try:
124 121 old = self.repo.wfile(".hgtags").read()
125 122 oldlines = old.splitlines(1)
126 123 oldlines.sort()
127 124 except:
128 125 oldlines = []
129 126
130 127 k = tags.keys()
131 128 k.sort()
132 129 newlines = []
133 130 for tag in k:
134 131 newlines.append("%s %s\n" % (tags[tag], tag))
135 132
136 133 newlines.sort()
137 134
138 135 if newlines != oldlines:
139 136 self.ui.status("updating tags\n")
140 137 f = self.repo.wfile(".hgtags", "w")
141 138 f.write("".join(newlines))
142 139 f.close()
143 140 if not oldlines: self.repo.add([".hgtags"])
144 141 date = "%s 0" % int(time.mktime(time.gmtime()))
145 142 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
146 143 date, self.repo.changelog.tip(), nullid)
147 144 return hex(self.repo.changelog.tip())
148 145
149 146 class mercurial_source(converter_source):
150 147 def __init__(self, ui, path, rev=None):
151 148 converter_source.__init__(self, ui, path, rev)
152 149 self.repo = hg.repository(self.ui, path)
153 150 self.lastrev = None
154 151 self.lastctx = None
155 152
156 153 def changectx(self, rev):
157 154 if self.lastrev != rev:
158 155 self.lastctx = self.repo.changectx(rev)
159 156 self.lastrev = rev
160 157 return self.lastctx
161 158
162 159 def getheads(self):
163 160 if self.rev:
164 161 return [hex(self.repo.changectx(self.rev).node())]
165 162 else:
166 163 return [hex(node) for node in self.repo.heads()]
167 164
168 165 def getfile(self, name, rev):
169 166 try:
170 167 return self.changectx(rev).filectx(name).data()
171 168 except revlog.LookupError, err:
172 169 raise IOError(err)
173 170
174 171 def getmode(self, name, rev):
175 172 m = self.changectx(rev).manifest()
176 173 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
177 174
178 175 def getchanges(self, rev):
179 176 ctx = self.changectx(rev)
180 177 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
181 178 changes = [(name, rev) for name in m + a + r]
182 179 changes.sort()
183 180 return (changes, self.getcopies(ctx))
184 181
185 182 def getcopies(self, ctx):
186 183 added = self.repo.status(ctx.parents()[0].node(), ctx.node())[1]
187 184 copies = {}
188 185 for name in added:
189 186 try:
190 187 copies[name] = ctx.filectx(name).renamed()[0]
191 188 except TypeError:
192 189 pass
193 190 return copies
194 191
195 192 def getcommit(self, rev):
196 193 ctx = self.changectx(rev)
197 194 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
198 195 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
199 196 desc=ctx.description(), parents=parents,
200 197 branch=ctx.branch())
201 198
202 199 def gettags(self):
203 200 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
204 201 return dict([(name, hex(node)) for name, node in tags])
General Comments 0
You need to be logged in to leave comments. Login now