##// END OF EJS Templates
convert: always track the hg source revision in the internal commit object...
Matt Harbison -
r25570:7cc1d33f default
parent child Browse files
Show More
@@ -1,469 +1,470 b''
1 1 # common.py - common code for the 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 import base64, errno, subprocess, os, datetime, re
9 9 import cPickle as pickle
10 10 from mercurial import util
11 11 from mercurial.i18n import _
12 12
13 13 propertycache = util.propertycache
14 14
15 15 def encodeargs(args):
16 16 def encodearg(s):
17 17 lines = base64.encodestring(s)
18 18 lines = [l.splitlines()[0] for l in lines]
19 19 return ''.join(lines)
20 20
21 21 s = pickle.dumps(args)
22 22 return encodearg(s)
23 23
24 24 def decodeargs(s):
25 25 s = base64.decodestring(s)
26 26 return pickle.loads(s)
27 27
28 28 class MissingTool(Exception):
29 29 pass
30 30
31 31 def checktool(exe, name=None, abort=True):
32 32 name = name or exe
33 33 if not util.findexe(exe):
34 34 if abort:
35 35 exc = util.Abort
36 36 else:
37 37 exc = MissingTool
38 38 raise exc(_('cannot find required "%s" tool') % name)
39 39
40 40 class NoRepo(Exception):
41 41 pass
42 42
43 43 SKIPREV = 'SKIP'
44 44
45 45 class commit(object):
46 46 def __init__(self, author, date, desc, parents, branch=None, rev=None,
47 extra={}, sortkey=None):
47 extra={}, sortkey=None, saverev=True):
48 48 self.author = author or 'unknown'
49 49 self.date = date or '0 0'
50 50 self.desc = desc
51 51 self.parents = parents
52 52 self.branch = branch
53 53 self.rev = rev
54 54 self.extra = extra
55 55 self.sortkey = sortkey
56 self.saverev = saverev
56 57
57 58 class converter_source(object):
58 59 """Conversion source interface"""
59 60
60 61 def __init__(self, ui, path=None, rev=None):
61 62 """Initialize conversion source (or raise NoRepo("message")
62 63 exception if path is not a valid repository)"""
63 64 self.ui = ui
64 65 self.path = path
65 66 self.rev = rev
66 67
67 68 self.encoding = 'utf-8'
68 69
69 70 def checkhexformat(self, revstr, mapname='splicemap'):
70 71 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
71 72 such format for their revision numbering
72 73 """
73 74 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
74 75 raise util.Abort(_('%s entry %s is not a valid revision'
75 76 ' identifier') % (mapname, revstr))
76 77
77 78 def before(self):
78 79 pass
79 80
80 81 def after(self):
81 82 pass
82 83
83 84 def setrevmap(self, revmap):
84 85 """set the map of already-converted revisions"""
85 86 pass
86 87
87 88 def getheads(self):
88 89 """Return a list of this repository's heads"""
89 90 raise NotImplementedError
90 91
91 92 def getfile(self, name, rev):
92 93 """Return a pair (data, mode) where data is the file content
93 94 as a string and mode one of '', 'x' or 'l'. rev is the
94 95 identifier returned by a previous call to getchanges().
95 96 Data is None if file is missing/deleted in rev.
96 97 """
97 98 raise NotImplementedError
98 99
99 100 def getchanges(self, version, full):
100 101 """Returns a tuple of (files, copies, cleanp2).
101 102
102 103 files is a sorted list of (filename, id) tuples for all files
103 104 changed between version and its first parent returned by
104 105 getcommit(). If full, all files in that revision is returned.
105 106 id is the source revision id of the file.
106 107
107 108 copies is a dictionary of dest: source
108 109
109 110 cleanp2 is the set of files filenames that are clean against p2.
110 111 (Files that are clean against p1 are already not in files (unless
111 112 full). This makes it possible to handle p2 clean files similarly.)
112 113 """
113 114 raise NotImplementedError
114 115
115 116 def getcommit(self, version):
116 117 """Return the commit object for version"""
117 118 raise NotImplementedError
118 119
119 120 def numcommits(self):
120 121 """Return the number of commits in this source.
121 122
122 123 If unknown, return None.
123 124 """
124 125 return None
125 126
126 127 def gettags(self):
127 128 """Return the tags as a dictionary of name: revision
128 129
129 130 Tag names must be UTF-8 strings.
130 131 """
131 132 raise NotImplementedError
132 133
133 134 def recode(self, s, encoding=None):
134 135 if not encoding:
135 136 encoding = self.encoding or 'utf-8'
136 137
137 138 if isinstance(s, unicode):
138 139 return s.encode("utf-8")
139 140 try:
140 141 return s.decode(encoding).encode("utf-8")
141 142 except UnicodeError:
142 143 try:
143 144 return s.decode("latin-1").encode("utf-8")
144 145 except UnicodeError:
145 146 return s.decode(encoding, "replace").encode("utf-8")
146 147
147 148 def getchangedfiles(self, rev, i):
148 149 """Return the files changed by rev compared to parent[i].
149 150
150 151 i is an index selecting one of the parents of rev. The return
151 152 value should be the list of files that are different in rev and
152 153 this parent.
153 154
154 155 If rev has no parents, i is None.
155 156
156 157 This function is only needed to support --filemap
157 158 """
158 159 raise NotImplementedError
159 160
160 161 def converted(self, rev, sinkrev):
161 162 '''Notify the source that a revision has been converted.'''
162 163 pass
163 164
164 165 def hasnativeorder(self):
165 166 """Return true if this source has a meaningful, native revision
166 167 order. For instance, Mercurial revisions are store sequentially
167 168 while there is no such global ordering with Darcs.
168 169 """
169 170 return False
170 171
171 172 def hasnativeclose(self):
172 173 """Return true if this source has ability to close branch.
173 174 """
174 175 return False
175 176
176 177 def lookuprev(self, rev):
177 178 """If rev is a meaningful revision reference in source, return
178 179 the referenced identifier in the same format used by getcommit().
179 180 return None otherwise.
180 181 """
181 182 return None
182 183
183 184 def getbookmarks(self):
184 185 """Return the bookmarks as a dictionary of name: revision
185 186
186 187 Bookmark names are to be UTF-8 strings.
187 188 """
188 189 return {}
189 190
190 191 def checkrevformat(self, revstr, mapname='splicemap'):
191 192 """revstr is a string that describes a revision in the given
192 193 source control system. Return true if revstr has correct
193 194 format.
194 195 """
195 196 return True
196 197
197 198 class converter_sink(object):
198 199 """Conversion sink (target) interface"""
199 200
200 201 def __init__(self, ui, path):
201 202 """Initialize conversion sink (or raise NoRepo("message")
202 203 exception if path is not a valid repository)
203 204
204 205 created is a list of paths to remove if a fatal error occurs
205 206 later"""
206 207 self.ui = ui
207 208 self.path = path
208 209 self.created = []
209 210
210 211 def revmapfile(self):
211 212 """Path to a file that will contain lines
212 213 source_rev_id sink_rev_id
213 214 mapping equivalent revision identifiers for each system."""
214 215 raise NotImplementedError
215 216
216 217 def authorfile(self):
217 218 """Path to a file that will contain lines
218 219 srcauthor=dstauthor
219 220 mapping equivalent authors identifiers for each system."""
220 221 return None
221 222
222 223 def putcommit(self, files, copies, parents, commit, source, revmap, full,
223 224 cleanp2):
224 225 """Create a revision with all changed files listed in 'files'
225 226 and having listed parents. 'commit' is a commit object
226 227 containing at a minimum the author, date, and message for this
227 228 changeset. 'files' is a list of (path, version) tuples,
228 229 'copies' is a dictionary mapping destinations to sources,
229 230 'source' is the source repository, and 'revmap' is a mapfile
230 231 of source revisions to converted revisions. Only getfile() and
231 232 lookuprev() should be called on 'source'. 'full' means that 'files'
232 233 is complete and all other files should be removed.
233 234 'cleanp2' is a set of the filenames that are unchanged from p2
234 235 (only in the common merge case where there two parents).
235 236
236 237 Note that the sink repository is not told to update itself to
237 238 a particular revision (or even what that revision would be)
238 239 before it receives the file data.
239 240 """
240 241 raise NotImplementedError
241 242
242 243 def puttags(self, tags):
243 244 """Put tags into sink.
244 245
245 246 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
246 247 Return a pair (tag_revision, tag_parent_revision), or (None, None)
247 248 if nothing was changed.
248 249 """
249 250 raise NotImplementedError
250 251
251 252 def setbranch(self, branch, pbranches):
252 253 """Set the current branch name. Called before the first putcommit
253 254 on the branch.
254 255 branch: branch name for subsequent commits
255 256 pbranches: (converted parent revision, parent branch) tuples"""
256 257 pass
257 258
258 259 def setfilemapmode(self, active):
259 260 """Tell the destination that we're using a filemap
260 261
261 262 Some converter_sources (svn in particular) can claim that a file
262 263 was changed in a revision, even if there was no change. This method
263 264 tells the destination that we're using a filemap and that it should
264 265 filter empty revisions.
265 266 """
266 267 pass
267 268
268 269 def before(self):
269 270 pass
270 271
271 272 def after(self):
272 273 pass
273 274
274 275 def putbookmarks(self, bookmarks):
275 276 """Put bookmarks into sink.
276 277
277 278 bookmarks: {bookmarkname: sink_rev_id, ...}
278 279 where bookmarkname is an UTF-8 string.
279 280 """
280 281 pass
281 282
282 283 def hascommitfrommap(self, rev):
283 284 """Return False if a rev mentioned in a filemap is known to not be
284 285 present."""
285 286 raise NotImplementedError
286 287
287 288 def hascommitforsplicemap(self, rev):
288 289 """This method is for the special needs for splicemap handling and not
289 290 for general use. Returns True if the sink contains rev, aborts on some
290 291 special cases."""
291 292 raise NotImplementedError
292 293
293 294 class commandline(object):
294 295 def __init__(self, ui, command):
295 296 self.ui = ui
296 297 self.command = command
297 298
298 299 def prerun(self):
299 300 pass
300 301
301 302 def postrun(self):
302 303 pass
303 304
304 305 def _cmdline(self, cmd, *args, **kwargs):
305 306 cmdline = [self.command, cmd] + list(args)
306 307 for k, v in kwargs.iteritems():
307 308 if len(k) == 1:
308 309 cmdline.append('-' + k)
309 310 else:
310 311 cmdline.append('--' + k.replace('_', '-'))
311 312 try:
312 313 if len(k) == 1:
313 314 cmdline.append('' + v)
314 315 else:
315 316 cmdline[-1] += '=' + v
316 317 except TypeError:
317 318 pass
318 319 cmdline = [util.shellquote(arg) for arg in cmdline]
319 320 if not self.ui.debugflag:
320 321 cmdline += ['2>', os.devnull]
321 322 cmdline = ' '.join(cmdline)
322 323 return cmdline
323 324
324 325 def _run(self, cmd, *args, **kwargs):
325 326 def popen(cmdline):
326 327 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
327 328 close_fds=util.closefds,
328 329 stdout=subprocess.PIPE)
329 330 return p
330 331 return self._dorun(popen, cmd, *args, **kwargs)
331 332
332 333 def _run2(self, cmd, *args, **kwargs):
333 334 return self._dorun(util.popen2, cmd, *args, **kwargs)
334 335
335 336 def _dorun(self, openfunc, cmd, *args, **kwargs):
336 337 cmdline = self._cmdline(cmd, *args, **kwargs)
337 338 self.ui.debug('running: %s\n' % (cmdline,))
338 339 self.prerun()
339 340 try:
340 341 return openfunc(cmdline)
341 342 finally:
342 343 self.postrun()
343 344
344 345 def run(self, cmd, *args, **kwargs):
345 346 p = self._run(cmd, *args, **kwargs)
346 347 output = p.communicate()[0]
347 348 self.ui.debug(output)
348 349 return output, p.returncode
349 350
350 351 def runlines(self, cmd, *args, **kwargs):
351 352 p = self._run(cmd, *args, **kwargs)
352 353 output = p.stdout.readlines()
353 354 p.wait()
354 355 self.ui.debug(''.join(output))
355 356 return output, p.returncode
356 357
357 358 def checkexit(self, status, output=''):
358 359 if status:
359 360 if output:
360 361 self.ui.warn(_('%s error:\n') % self.command)
361 362 self.ui.warn(output)
362 363 msg = util.explainexit(status)[0]
363 364 raise util.Abort('%s %s' % (self.command, msg))
364 365
365 366 def run0(self, cmd, *args, **kwargs):
366 367 output, status = self.run(cmd, *args, **kwargs)
367 368 self.checkexit(status, output)
368 369 return output
369 370
370 371 def runlines0(self, cmd, *args, **kwargs):
371 372 output, status = self.runlines(cmd, *args, **kwargs)
372 373 self.checkexit(status, ''.join(output))
373 374 return output
374 375
375 376 @propertycache
376 377 def argmax(self):
377 378 # POSIX requires at least 4096 bytes for ARG_MAX
378 379 argmax = 4096
379 380 try:
380 381 argmax = os.sysconf("SC_ARG_MAX")
381 382 except (AttributeError, ValueError):
382 383 pass
383 384
384 385 # Windows shells impose their own limits on command line length,
385 386 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
386 387 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
387 388 # details about cmd.exe limitations.
388 389
389 390 # Since ARG_MAX is for command line _and_ environment, lower our limit
390 391 # (and make happy Windows shells while doing this).
391 392 return argmax // 2 - 1
392 393
393 394 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
394 395 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
395 396 limit = self.argmax - cmdlen
396 397 bytes = 0
397 398 fl = []
398 399 for fn in arglist:
399 400 b = len(fn) + 3
400 401 if bytes + b < limit or len(fl) == 0:
401 402 fl.append(fn)
402 403 bytes += b
403 404 else:
404 405 yield fl
405 406 fl = [fn]
406 407 bytes = b
407 408 if fl:
408 409 yield fl
409 410
410 411 def xargs(self, arglist, cmd, *args, **kwargs):
411 412 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
412 413 self.run0(cmd, *(list(args) + l), **kwargs)
413 414
414 415 class mapfile(dict):
415 416 def __init__(self, ui, path):
416 417 super(mapfile, self).__init__()
417 418 self.ui = ui
418 419 self.path = path
419 420 self.fp = None
420 421 self.order = []
421 422 self._read()
422 423
423 424 def _read(self):
424 425 if not self.path:
425 426 return
426 427 try:
427 428 fp = open(self.path, 'r')
428 429 except IOError, err:
429 430 if err.errno != errno.ENOENT:
430 431 raise
431 432 return
432 433 for i, line in enumerate(fp):
433 434 line = line.splitlines()[0].rstrip()
434 435 if not line:
435 436 # Ignore blank lines
436 437 continue
437 438 try:
438 439 key, value = line.rsplit(' ', 1)
439 440 except ValueError:
440 441 raise util.Abort(
441 442 _('syntax error in %s(%d): key/value pair expected')
442 443 % (self.path, i + 1))
443 444 if key not in self:
444 445 self.order.append(key)
445 446 super(mapfile, self).__setitem__(key, value)
446 447 fp.close()
447 448
448 449 def __setitem__(self, key, value):
449 450 if self.fp is None:
450 451 try:
451 452 self.fp = open(self.path, 'a')
452 453 except IOError, err:
453 454 raise util.Abort(_('could not open map file %r: %s') %
454 455 (self.path, err.strerror))
455 456 self.fp.write('%s %s\n' % (key, value))
456 457 self.fp.flush()
457 458 super(mapfile, self).__setitem__(key, value)
458 459
459 460 def close(self):
460 461 if self.fp:
461 462 self.fp.close()
462 463 self.fp = None
463 464
464 465 def makedatetimestamp(t):
465 466 """Like util.makedate() but for time t instead of current time"""
466 467 delta = (datetime.datetime.utcfromtimestamp(t) -
467 468 datetime.datetime.fromtimestamp(t))
468 469 tz = delta.days * 86400 + delta.seconds
469 470 return t, tz
@@ -1,538 +1,536 b''
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, exchange
25 25
26 26 from common import NoRepo, commit, converter_source, converter_sink, mapfile
27 27
28 28 import re
29 29 sha1re = re.compile(r'\b[0-9a-f]{12,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 self.subrevmaps = {}
63 63
64 64 def before(self):
65 65 self.ui.debug('run hg sink pre-conversion action\n')
66 66 self.wlock = self.repo.wlock()
67 67 self.lock = self.repo.lock()
68 68
69 69 def after(self):
70 70 self.ui.debug('run hg sink post-conversion action\n')
71 71 if self.lock:
72 72 self.lock.release()
73 73 if self.wlock:
74 74 self.wlock.release()
75 75
76 76 def revmapfile(self):
77 77 return self.repo.join("shamap")
78 78
79 79 def authorfile(self):
80 80 return self.repo.join("authormap")
81 81
82 82 def setbranch(self, branch, pbranches):
83 83 if not self.clonebranches:
84 84 return
85 85
86 86 setbranch = (branch != self.lastbranch)
87 87 self.lastbranch = branch
88 88 if not branch:
89 89 branch = 'default'
90 90 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
91 91 if pbranches:
92 92 pbranch = pbranches[0][1]
93 93 else:
94 94 pbranch = 'default'
95 95
96 96 branchpath = os.path.join(self.path, branch)
97 97 if setbranch:
98 98 self.after()
99 99 try:
100 100 self.repo = hg.repository(self.ui, branchpath)
101 101 except Exception:
102 102 self.repo = hg.repository(self.ui, branchpath, create=True)
103 103 self.before()
104 104
105 105 # pbranches may bring revisions from other branches (merge parents)
106 106 # Make sure we have them, or pull them.
107 107 missings = {}
108 108 for b in pbranches:
109 109 try:
110 110 self.repo.lookup(b[0])
111 111 except Exception:
112 112 missings.setdefault(b[1], []).append(b[0])
113 113
114 114 if missings:
115 115 self.after()
116 116 for pbranch, heads in sorted(missings.iteritems()):
117 117 pbranchpath = os.path.join(self.path, pbranch)
118 118 prepo = hg.peer(self.ui, {}, pbranchpath)
119 119 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
120 120 exchange.pull(self.repo, prepo,
121 121 [prepo.lookup(h) for h in heads])
122 122 self.before()
123 123
124 124 def _rewritetags(self, source, revmap, data):
125 125 fp = cStringIO.StringIO()
126 126 for line in data.splitlines():
127 127 s = line.split(' ', 1)
128 128 if len(s) != 2:
129 129 continue
130 130 revid = revmap.get(source.lookuprev(s[0]))
131 131 if not revid:
132 132 if s[0] == hex(nullid):
133 133 revid = s[0]
134 134 else:
135 135 continue
136 136 fp.write('%s %s\n' % (revid, s[1]))
137 137 return fp.getvalue()
138 138
139 139 def _rewritesubstate(self, source, data):
140 140 fp = cStringIO.StringIO()
141 141 for line in data.splitlines():
142 142 s = line.split(' ', 1)
143 143 if len(s) != 2:
144 144 continue
145 145
146 146 revid = s[0]
147 147 subpath = s[1]
148 148 if revid != hex(nullid):
149 149 revmap = self.subrevmaps.get(subpath)
150 150 if revmap is None:
151 151 revmap = mapfile(self.ui,
152 152 self.repo.wjoin(subpath, '.hg/shamap'))
153 153 self.subrevmaps[subpath] = revmap
154 154
155 155 # It is reasonable that one or more of the subrepos don't
156 156 # need to be converted, in which case they can be cloned
157 157 # into place instead of converted. Therefore, only warn
158 158 # once.
159 159 msg = _('no ".hgsubstate" updates will be made for "%s"\n')
160 160 if len(revmap) == 0:
161 161 sub = self.repo.wvfs.reljoin(subpath, '.hg')
162 162
163 163 if self.repo.wvfs.exists(sub):
164 164 self.ui.warn(msg % subpath)
165 165
166 166 newid = revmap.get(revid)
167 167 if not newid:
168 168 if len(revmap) > 0:
169 169 self.ui.warn(_("%s is missing from %s/.hg/shamap\n") %
170 170 (revid, subpath))
171 171 else:
172 172 revid = newid
173 173
174 174 fp.write('%s %s\n' % (revid, subpath))
175 175
176 176 return fp.getvalue()
177 177
178 178 def putcommit(self, files, copies, parents, commit, source, revmap, full,
179 179 cleanp2):
180 180 files = dict(files)
181 181
182 182 def getfilectx(repo, memctx, f):
183 183 if p2ctx and f in cleanp2 and f not in copies:
184 184 self.ui.debug('reusing %s from p2\n' % f)
185 185 return p2ctx[f]
186 186 try:
187 187 v = files[f]
188 188 except KeyError:
189 189 return None
190 190 data, mode = source.getfile(f, v)
191 191 if data is None:
192 192 return None
193 193 if f == '.hgtags':
194 194 data = self._rewritetags(source, revmap, data)
195 195 if f == '.hgsubstate':
196 196 data = self._rewritesubstate(source, data)
197 197 return context.memfilectx(self.repo, f, data, 'l' in mode,
198 198 'x' in mode, copies.get(f))
199 199
200 200 pl = []
201 201 for p in parents:
202 202 if p not in pl:
203 203 pl.append(p)
204 204 parents = pl
205 205 nparents = len(parents)
206 206 if self.filemapmode and nparents == 1:
207 207 m1node = self.repo.changelog.read(bin(parents[0]))[0]
208 208 parent = parents[0]
209 209
210 210 if len(parents) < 2:
211 211 parents.append(nullid)
212 212 if len(parents) < 2:
213 213 parents.append(nullid)
214 214 p2 = parents.pop(0)
215 215
216 216 text = commit.desc
217 217
218 218 sha1s = re.findall(sha1re, text)
219 219 for sha1 in sha1s:
220 220 oldrev = source.lookuprev(sha1)
221 221 newrev = revmap.get(oldrev)
222 222 if newrev is not None:
223 223 text = text.replace(sha1, newrev[:len(sha1)])
224 224
225 225 extra = commit.extra.copy()
226 226
227 227 for label in ('source', 'transplant_source', 'rebase_source'):
228 228 node = extra.get(label)
229 229
230 230 if node is None:
231 231 continue
232 232
233 233 # Only transplant stores its reference in binary
234 234 if label == 'transplant_source':
235 235 node = hex(node)
236 236
237 237 newrev = revmap.get(node)
238 238 if newrev is not None:
239 239 if label == 'transplant_source':
240 240 newrev = bin(newrev)
241 241
242 242 extra[label] = newrev
243 243
244 244 if self.branchnames and commit.branch:
245 245 extra['branch'] = commit.branch
246 if commit.rev:
246 if commit.rev and commit.saverev:
247 247 extra['convert_revision'] = commit.rev
248 248
249 249 while parents:
250 250 p1 = p2
251 251 p2 = parents.pop(0)
252 252 p2ctx = None
253 253 if p2 != nullid:
254 254 p2ctx = self.repo[p2]
255 255 fileset = set(files)
256 256 if full:
257 257 fileset.update(self.repo[p1])
258 258 fileset.update(self.repo[p2])
259 259 ctx = context.memctx(self.repo, (p1, p2), text, fileset,
260 260 getfilectx, commit.author, commit.date, extra)
261 261 self.repo.commitctx(ctx)
262 262 text = "(octopus merge fixup)\n"
263 263 p2 = hex(self.repo.changelog.tip())
264 264
265 265 if self.filemapmode and nparents == 1:
266 266 man = self.repo.manifest
267 267 mnode = self.repo.changelog.read(bin(p2))[0]
268 268 closed = 'close' in commit.extra
269 269 if not closed and not man.cmp(m1node, man.revision(mnode)):
270 270 self.ui.status(_("filtering out empty revision\n"))
271 271 self.repo.rollback(force=True)
272 272 return parent
273 273 return p2
274 274
275 275 def puttags(self, tags):
276 276 try:
277 277 parentctx = self.repo[self.tagsbranch]
278 278 tagparent = parentctx.node()
279 279 except error.RepoError:
280 280 parentctx = None
281 281 tagparent = nullid
282 282
283 283 oldlines = set()
284 284 for branch, heads in self.repo.branchmap().iteritems():
285 285 for h in heads:
286 286 if '.hgtags' in self.repo[h]:
287 287 oldlines.update(
288 288 set(self.repo[h]['.hgtags'].data().splitlines(True)))
289 289 oldlines = sorted(list(oldlines))
290 290
291 291 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
292 292 if newlines == oldlines:
293 293 return None, None
294 294
295 295 # if the old and new tags match, then there is nothing to update
296 296 oldtags = set()
297 297 newtags = set()
298 298 for line in oldlines:
299 299 s = line.strip().split(' ', 1)
300 300 if len(s) != 2:
301 301 continue
302 302 oldtags.add(s[1])
303 303 for line in newlines:
304 304 s = line.strip().split(' ', 1)
305 305 if len(s) != 2:
306 306 continue
307 307 if s[1] not in oldtags:
308 308 newtags.add(s[1].strip())
309 309
310 310 if not newtags:
311 311 return None, None
312 312
313 313 data = "".join(newlines)
314 314 def getfilectx(repo, memctx, f):
315 315 return context.memfilectx(repo, f, data, False, False, None)
316 316
317 317 self.ui.status(_("updating tags\n"))
318 318 date = "%s 0" % int(time.mktime(time.gmtime()))
319 319 extra = {'branch': self.tagsbranch}
320 320 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
321 321 [".hgtags"], getfilectx, "convert-repo", date,
322 322 extra)
323 323 self.repo.commitctx(ctx)
324 324 return hex(self.repo.changelog.tip()), hex(tagparent)
325 325
326 326 def setfilemapmode(self, active):
327 327 self.filemapmode = active
328 328
329 329 def putbookmarks(self, updatedbookmark):
330 330 if not len(updatedbookmark):
331 331 return
332 332
333 333 self.ui.status(_("updating bookmarks\n"))
334 334 destmarks = self.repo._bookmarks
335 335 for bookmark in updatedbookmark:
336 336 destmarks[bookmark] = bin(updatedbookmark[bookmark])
337 337 destmarks.write()
338 338
339 339 def hascommitfrommap(self, rev):
340 340 # the exact semantics of clonebranches is unclear so we can't say no
341 341 return rev in self.repo or self.clonebranches
342 342
343 343 def hascommitforsplicemap(self, rev):
344 344 if rev not in self.repo and self.clonebranches:
345 345 raise util.Abort(_('revision %s not found in destination '
346 346 'repository (lookups with clonebranches=true '
347 347 'are not implemented)') % rev)
348 348 return rev in self.repo
349 349
350 350 class mercurial_source(converter_source):
351 351 def __init__(self, ui, path, rev=None):
352 352 converter_source.__init__(self, ui, path, rev)
353 353 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
354 354 self.ignored = set()
355 355 self.saverev = ui.configbool('convert', 'hg.saverev', False)
356 356 try:
357 357 self.repo = hg.repository(self.ui, path)
358 358 # try to provoke an exception if this isn't really a hg
359 359 # repo, but some other bogus compatible-looking url
360 360 if not self.repo.local():
361 361 raise error.RepoError
362 362 except error.RepoError:
363 363 ui.traceback()
364 364 raise NoRepo(_("%s is not a local Mercurial repository") % path)
365 365 self.lastrev = None
366 366 self.lastctx = None
367 367 self._changescache = None, None
368 368 self.convertfp = None
369 369 # Restrict converted revisions to startrev descendants
370 370 startnode = ui.config('convert', 'hg.startrev')
371 371 hgrevs = ui.config('convert', 'hg.revs')
372 372 if hgrevs is None:
373 373 if startnode is not None:
374 374 try:
375 375 startnode = self.repo.lookup(startnode)
376 376 except error.RepoError:
377 377 raise util.Abort(_('%s is not a valid start revision')
378 378 % startnode)
379 379 startrev = self.repo.changelog.rev(startnode)
380 380 children = {startnode: 1}
381 381 for r in self.repo.changelog.descendants([startrev]):
382 382 children[self.repo.changelog.node(r)] = 1
383 383 self.keep = children.__contains__
384 384 else:
385 385 self.keep = util.always
386 386 if rev:
387 387 self._heads = [self.repo[rev].node()]
388 388 else:
389 389 self._heads = self.repo.heads()
390 390 else:
391 391 if rev or startnode is not None:
392 392 raise util.Abort(_('hg.revs cannot be combined with '
393 393 'hg.startrev or --rev'))
394 394 nodes = set()
395 395 parents = set()
396 396 for r in scmutil.revrange(self.repo, [hgrevs]):
397 397 ctx = self.repo[r]
398 398 nodes.add(ctx.node())
399 399 parents.update(p.node() for p in ctx.parents())
400 400 self.keep = nodes.__contains__
401 401 self._heads = nodes - parents
402 402
403 403 def changectx(self, rev):
404 404 if self.lastrev != rev:
405 405 self.lastctx = self.repo[rev]
406 406 self.lastrev = rev
407 407 return self.lastctx
408 408
409 409 def parents(self, ctx):
410 410 return [p for p in ctx.parents() if p and self.keep(p.node())]
411 411
412 412 def getheads(self):
413 413 return [hex(h) for h in self._heads if self.keep(h)]
414 414
415 415 def getfile(self, name, rev):
416 416 try:
417 417 fctx = self.changectx(rev)[name]
418 418 return fctx.data(), fctx.flags()
419 419 except error.LookupError:
420 420 return None, None
421 421
422 422 def getchanges(self, rev, full):
423 423 ctx = self.changectx(rev)
424 424 parents = self.parents(ctx)
425 425 if full or not parents:
426 426 files = copyfiles = ctx.manifest()
427 427 if parents:
428 428 if self._changescache[0] == rev:
429 429 m, a, r = self._changescache[1]
430 430 else:
431 431 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
432 432 if not full:
433 433 files = m + a + r
434 434 copyfiles = m + a
435 435 # getcopies() is also run for roots and before filtering so missing
436 436 # revlogs are detected early
437 437 copies = self.getcopies(ctx, parents, copyfiles)
438 438 cleanp2 = set()
439 439 if len(parents) == 2:
440 440 cleanp2.update(self.repo.status(parents[1].node(), ctx.node(),
441 441 clean=True).clean)
442 442 changes = [(f, rev) for f in files if f not in self.ignored]
443 443 changes.sort()
444 444 return changes, copies, cleanp2
445 445
446 446 def getcopies(self, ctx, parents, files):
447 447 copies = {}
448 448 for name in files:
449 449 if name in self.ignored:
450 450 continue
451 451 try:
452 452 copysource, _copynode = ctx.filectx(name).renamed()
453 453 if copysource in self.ignored:
454 454 continue
455 455 # Ignore copy sources not in parent revisions
456 456 found = False
457 457 for p in parents:
458 458 if copysource in p:
459 459 found = True
460 460 break
461 461 if not found:
462 462 continue
463 463 copies[name] = copysource
464 464 except TypeError:
465 465 pass
466 466 except error.LookupError, e:
467 467 if not self.ignoreerrors:
468 468 raise
469 469 self.ignored.add(name)
470 470 self.ui.warn(_('ignoring: %s\n') % e)
471 471 return copies
472 472
473 473 def getcommit(self, rev):
474 474 ctx = self.changectx(rev)
475 475 parents = [p.hex() for p in self.parents(ctx)]
476 if self.saverev:
477 crev = rev
478 else:
479 crev = None
476 crev = rev
477
480 478 return commit(author=ctx.user(),
481 479 date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
482 480 desc=ctx.description(), rev=crev, parents=parents,
483 481 branch=ctx.branch(), extra=ctx.extra(),
484 sortkey=ctx.rev())
482 sortkey=ctx.rev(), saverev=self.saverev)
485 483
486 484 def gettags(self):
487 485 # This will get written to .hgtags, filter non global tags out.
488 486 tags = [t for t in self.repo.tagslist()
489 487 if self.repo.tagtype(t[0]) == 'global']
490 488 return dict([(name, hex(node)) for name, node in tags
491 489 if self.keep(node)])
492 490
493 491 def getchangedfiles(self, rev, i):
494 492 ctx = self.changectx(rev)
495 493 parents = self.parents(ctx)
496 494 if not parents and i is None:
497 495 i = 0
498 496 changes = [], ctx.manifest().keys(), []
499 497 else:
500 498 i = i or 0
501 499 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
502 500 changes = [[f for f in l if f not in self.ignored] for l in changes]
503 501
504 502 if i == 0:
505 503 self._changescache = (rev, changes)
506 504
507 505 return changes[0] + changes[1] + changes[2]
508 506
509 507 def converted(self, rev, destrev):
510 508 if self.convertfp is None:
511 509 self.convertfp = open(self.repo.join('shamap'), 'a')
512 510 self.convertfp.write('%s %s\n' % (destrev, rev))
513 511 self.convertfp.flush()
514 512
515 513 def before(self):
516 514 self.ui.debug('run hg source pre-conversion action\n')
517 515
518 516 def after(self):
519 517 self.ui.debug('run hg source post-conversion action\n')
520 518
521 519 def hasnativeorder(self):
522 520 return True
523 521
524 522 def hasnativeclose(self):
525 523 return True
526 524
527 525 def lookuprev(self, rev):
528 526 try:
529 527 return hex(self.repo.lookup(rev))
530 528 except (error.RepoError, error.LookupError):
531 529 return None
532 530
533 531 def getbookmarks(self):
534 532 return bookmarks.listbookmarks(self.repo)
535 533
536 534 def checkrevformat(self, revstr, mapname='splicemap'):
537 535 """ Mercurial, revision string is a 40 byte hex """
538 536 self.checkhexformat(revstr, mapname)
General Comments 0
You need to be logged in to leave comments. Login now