##// END OF EJS Templates
convert: fix history topology when using hg.tagsbranch...
Patrick Mezard -
r9431:d1b135f2 default
parent child Browse files
Show More
@@ -0,0 +1,64 b''
1 #!/bin/sh
2
3 "$TESTDIR/hghave" git || exit 80
4
5 echo "[extensions]" >> $HGRCPATH
6 echo "convert=" >> $HGRCPATH
7 echo 'hgext.graphlog =' >> $HGRCPATH
8 echo '[convert]' >> $HGRCPATH
9 echo 'hg.usebranchnames = True' >> $HGRCPATH
10 echo 'hg.tagsbranch = tags-update' >> $HGRCPATH
11
12 GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
13 GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
14 GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0000"; export GIT_AUTHOR_DATE
15 GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
16 GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
17 GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
18
19 count=10
20 action()
21 {
22 GIT_AUTHOR_DATE="2007-01-01 00:00:$count +0000"
23 GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
24 git "$@" >/dev/null 2>/dev/null || echo "git command error"
25 count=`expr $count + 1`
26 }
27
28 glog()
29 {
30 hg glog --template '{rev} "{desc|firstline}" files: {files}\n' "$@"
31 }
32
33 convertrepo()
34 {
35 hg convert --datesort git-repo hg-repo
36 }
37
38 # Build a GIT repo with at least 1 tag
39 mkdir git-repo
40 cd git-repo
41 git init >/dev/null 2>&1
42 echo a > a
43 git add a
44 action commit -m "rev1"
45 action tag -m "tag1" tag1
46 cd ..
47
48 # Do a first conversion
49 convertrepo
50
51 # Simulate upstream updates after first conversion
52 cd git-repo
53 echo b > a
54 git add a
55 action commit -m "rev2"
56 action tag -m "tag2" tag2
57 cd ..
58
59 # Perform an incremental conversion
60 convertrepo
61
62 # Print the log
63 cd hg-repo
64 glog
@@ -0,0 +1,19 b''
1 initializing destination hg-repo repository
2 scanning source...
3 sorting...
4 converting...
5 0 rev1
6 updating tags
7 scanning source...
8 sorting...
9 converting...
10 0 rev2
11 updating tags
12 o 3 "update tags" files: .hgtags
13 |
14 | o 2 "rev2" files: a
15 | |
16 o | 1 "update tags" files: .hgtags
17 /
18 o 0 "rev1" files: a
19
@@ -1,389 +1,391 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, incorporated herein by reference.
7 7
8 8 import base64, errno
9 9 import os
10 10 import cPickle as pickle
11 11 from mercurial import util
12 12 from mercurial.i18n import _
13 13
14 14 def encodeargs(args):
15 15 def encodearg(s):
16 16 lines = base64.encodestring(s)
17 17 lines = [l.splitlines()[0] for l in lines]
18 18 return ''.join(lines)
19 19
20 20 s = pickle.dumps(args)
21 21 return encodearg(s)
22 22
23 23 def decodeargs(s):
24 24 s = base64.decodestring(s)
25 25 return pickle.loads(s)
26 26
27 27 class MissingTool(Exception): pass
28 28
29 29 def checktool(exe, name=None, abort=True):
30 30 name = name or exe
31 31 if not util.find_exe(exe):
32 32 exc = abort and util.Abort or MissingTool
33 33 raise exc(_('cannot find required "%s" tool') % name)
34 34
35 35 class NoRepo(Exception): pass
36 36
37 37 SKIPREV = 'SKIP'
38 38
39 39 class commit(object):
40 40 def __init__(self, author, date, desc, parents, branch=None, rev=None,
41 41 extra={}, sortkey=None):
42 42 self.author = author or 'unknown'
43 43 self.date = date or '0 0'
44 44 self.desc = desc
45 45 self.parents = parents
46 46 self.branch = branch
47 47 self.rev = rev
48 48 self.extra = extra
49 49 self.sortkey = sortkey
50 50
51 51 class converter_source(object):
52 52 """Conversion source interface"""
53 53
54 54 def __init__(self, ui, path=None, rev=None):
55 55 """Initialize conversion source (or raise NoRepo("message")
56 56 exception if path is not a valid repository)"""
57 57 self.ui = ui
58 58 self.path = path
59 59 self.rev = rev
60 60
61 61 self.encoding = 'utf-8'
62 62
63 63 def before(self):
64 64 pass
65 65
66 66 def after(self):
67 67 pass
68 68
69 69 def setrevmap(self, revmap):
70 70 """set the map of already-converted revisions"""
71 71 pass
72 72
73 73 def getheads(self):
74 74 """Return a list of this repository's heads"""
75 75 raise NotImplementedError()
76 76
77 77 def getfile(self, name, rev):
78 78 """Return file contents as a string. rev is the identifier returned
79 79 by a previous call to getchanges(). Raise IOError to indicate that
80 80 name was deleted in rev.
81 81 """
82 82 raise NotImplementedError()
83 83
84 84 def getmode(self, name, rev):
85 85 """Return file mode, eg. '', 'x', or 'l'. rev is the identifier
86 86 returned by a previous call to getchanges().
87 87 """
88 88 raise NotImplementedError()
89 89
90 90 def getchanges(self, version):
91 91 """Returns a tuple of (files, copies).
92 92
93 93 files is a sorted list of (filename, id) tuples for all files
94 94 changed between version and its first parent returned by
95 95 getcommit(). id is the source revision id of the file.
96 96
97 97 copies is a dictionary of dest: source
98 98 """
99 99 raise NotImplementedError()
100 100
101 101 def getcommit(self, version):
102 102 """Return the commit object for version"""
103 103 raise NotImplementedError()
104 104
105 105 def gettags(self):
106 106 """Return the tags as a dictionary of name: revision
107 107
108 108 Tag names must be UTF-8 strings.
109 109 """
110 110 raise NotImplementedError()
111 111
112 112 def recode(self, s, encoding=None):
113 113 if not encoding:
114 114 encoding = self.encoding or 'utf-8'
115 115
116 116 if isinstance(s, unicode):
117 117 return s.encode("utf-8")
118 118 try:
119 119 return s.decode(encoding).encode("utf-8")
120 120 except:
121 121 try:
122 122 return s.decode("latin-1").encode("utf-8")
123 123 except:
124 124 return s.decode(encoding, "replace").encode("utf-8")
125 125
126 126 def getchangedfiles(self, rev, i):
127 127 """Return the files changed by rev compared to parent[i].
128 128
129 129 i is an index selecting one of the parents of rev. The return
130 130 value should be the list of files that are different in rev and
131 131 this parent.
132 132
133 133 If rev has no parents, i is None.
134 134
135 135 This function is only needed to support --filemap
136 136 """
137 137 raise NotImplementedError()
138 138
139 139 def converted(self, rev, sinkrev):
140 140 '''Notify the source that a revision has been converted.'''
141 141 pass
142 142
143 143 def hasnativeorder(self):
144 144 """Return true if this source has a meaningful, native revision
145 145 order. For instance, Mercurial revisions are store sequentially
146 146 while there is no such global ordering with Darcs.
147 147 """
148 148 return False
149 149
150 150 def lookuprev(self, rev):
151 151 """If rev is a meaningful revision reference in source, return
152 152 the referenced identifier in the same format used by getcommit().
153 153 return None otherwise.
154 154 """
155 155 return None
156 156
157 157 class converter_sink(object):
158 158 """Conversion sink (target) interface"""
159 159
160 160 def __init__(self, ui, path):
161 161 """Initialize conversion sink (or raise NoRepo("message")
162 162 exception if path is not a valid repository)
163 163
164 164 created is a list of paths to remove if a fatal error occurs
165 165 later"""
166 166 self.ui = ui
167 167 self.path = path
168 168 self.created = []
169 169
170 170 def getheads(self):
171 171 """Return a list of this repository's heads"""
172 172 raise NotImplementedError()
173 173
174 174 def revmapfile(self):
175 175 """Path to a file that will contain lines
176 176 source_rev_id sink_rev_id
177 177 mapping equivalent revision identifiers for each system."""
178 178 raise NotImplementedError()
179 179
180 180 def authorfile(self):
181 181 """Path to a file that will contain lines
182 182 srcauthor=dstauthor
183 183 mapping equivalent authors identifiers for each system."""
184 184 return None
185 185
186 186 def putcommit(self, files, copies, parents, commit, source, revmap):
187 187 """Create a revision with all changed files listed in 'files'
188 188 and having listed parents. 'commit' is a commit object
189 189 containing at a minimum the author, date, and message for this
190 190 changeset. 'files' is a list of (path, version) tuples,
191 191 'copies' is a dictionary mapping destinations to sources,
192 192 'source' is the source repository, and 'revmap' is a mapfile
193 193 of source revisions to converted revisions. Only getfile(),
194 194 getmode(), and lookuprev() should be called on 'source'.
195 195
196 196 Note that the sink repository is not told to update itself to
197 197 a particular revision (or even what that revision would be)
198 198 before it receives the file data.
199 199 """
200 200 raise NotImplementedError()
201 201
202 202 def puttags(self, tags):
203 203 """Put tags into sink.
204 204
205 205 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
206 Return a pair (tag_revision, tag_parent_revision), or (None, None)
207 if nothing was changed.
206 208 """
207 209 raise NotImplementedError()
208 210
209 211 def setbranch(self, branch, pbranches):
210 212 """Set the current branch name. Called before the first putcommit
211 213 on the branch.
212 214 branch: branch name for subsequent commits
213 215 pbranches: (converted parent revision, parent branch) tuples"""
214 216 pass
215 217
216 218 def setfilemapmode(self, active):
217 219 """Tell the destination that we're using a filemap
218 220
219 221 Some converter_sources (svn in particular) can claim that a file
220 222 was changed in a revision, even if there was no change. This method
221 223 tells the destination that we're using a filemap and that it should
222 224 filter empty revisions.
223 225 """
224 226 pass
225 227
226 228 def before(self):
227 229 pass
228 230
229 231 def after(self):
230 232 pass
231 233
232 234
233 235 class commandline(object):
234 236 def __init__(self, ui, command):
235 237 self.ui = ui
236 238 self.command = command
237 239
238 240 def prerun(self):
239 241 pass
240 242
241 243 def postrun(self):
242 244 pass
243 245
244 246 def _cmdline(self, cmd, *args, **kwargs):
245 247 cmdline = [self.command, cmd] + list(args)
246 248 for k, v in kwargs.iteritems():
247 249 if len(k) == 1:
248 250 cmdline.append('-' + k)
249 251 else:
250 252 cmdline.append('--' + k.replace('_', '-'))
251 253 try:
252 254 if len(k) == 1:
253 255 cmdline.append('' + v)
254 256 else:
255 257 cmdline[-1] += '=' + v
256 258 except TypeError:
257 259 pass
258 260 cmdline = [util.shellquote(arg) for arg in cmdline]
259 261 if not self.ui.debugflag:
260 262 cmdline += ['2>', util.nulldev]
261 263 cmdline += ['<', util.nulldev]
262 264 cmdline = ' '.join(cmdline)
263 265 return cmdline
264 266
265 267 def _run(self, cmd, *args, **kwargs):
266 268 cmdline = self._cmdline(cmd, *args, **kwargs)
267 269 self.ui.debug(_('running: %s\n') % (cmdline,))
268 270 self.prerun()
269 271 try:
270 272 return util.popen(cmdline)
271 273 finally:
272 274 self.postrun()
273 275
274 276 def run(self, cmd, *args, **kwargs):
275 277 fp = self._run(cmd, *args, **kwargs)
276 278 output = fp.read()
277 279 self.ui.debug(output)
278 280 return output, fp.close()
279 281
280 282 def runlines(self, cmd, *args, **kwargs):
281 283 fp = self._run(cmd, *args, **kwargs)
282 284 output = fp.readlines()
283 285 self.ui.debug(''.join(output))
284 286 return output, fp.close()
285 287
286 288 def checkexit(self, status, output=''):
287 289 if status:
288 290 if output:
289 291 self.ui.warn(_('%s error:\n') % self.command)
290 292 self.ui.warn(output)
291 293 msg = util.explain_exit(status)[0]
292 294 raise util.Abort('%s %s' % (self.command, msg))
293 295
294 296 def run0(self, cmd, *args, **kwargs):
295 297 output, status = self.run(cmd, *args, **kwargs)
296 298 self.checkexit(status, output)
297 299 return output
298 300
299 301 def runlines0(self, cmd, *args, **kwargs):
300 302 output, status = self.runlines(cmd, *args, **kwargs)
301 303 self.checkexit(status, ''.join(output))
302 304 return output
303 305
304 306 def getargmax(self):
305 307 if '_argmax' in self.__dict__:
306 308 return self._argmax
307 309
308 310 # POSIX requires at least 4096 bytes for ARG_MAX
309 311 self._argmax = 4096
310 312 try:
311 313 self._argmax = os.sysconf("SC_ARG_MAX")
312 314 except:
313 315 pass
314 316
315 317 # Windows shells impose their own limits on command line length,
316 318 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
317 319 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
318 320 # details about cmd.exe limitations.
319 321
320 322 # Since ARG_MAX is for command line _and_ environment, lower our limit
321 323 # (and make happy Windows shells while doing this).
322 324
323 325 self._argmax = self._argmax/2 - 1
324 326 return self._argmax
325 327
326 328 def limit_arglist(self, arglist, cmd, *args, **kwargs):
327 329 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
328 330 bytes = 0
329 331 fl = []
330 332 for fn in arglist:
331 333 b = len(fn) + 3
332 334 if bytes + b < limit or len(fl) == 0:
333 335 fl.append(fn)
334 336 bytes += b
335 337 else:
336 338 yield fl
337 339 fl = [fn]
338 340 bytes = b
339 341 if fl:
340 342 yield fl
341 343
342 344 def xargs(self, arglist, cmd, *args, **kwargs):
343 345 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
344 346 self.run0(cmd, *(list(args) + l), **kwargs)
345 347
346 348 class mapfile(dict):
347 349 def __init__(self, ui, path):
348 350 super(mapfile, self).__init__()
349 351 self.ui = ui
350 352 self.path = path
351 353 self.fp = None
352 354 self.order = []
353 355 self._read()
354 356
355 357 def _read(self):
356 358 if not self.path:
357 359 return
358 360 try:
359 361 fp = open(self.path, 'r')
360 362 except IOError, err:
361 363 if err.errno != errno.ENOENT:
362 364 raise
363 365 return
364 366 for i, line in enumerate(fp):
365 367 try:
366 368 key, value = line[:-1].rsplit(' ', 1)
367 369 except ValueError:
368 370 raise util.Abort(_('syntax error in %s(%d): key/value pair expected')
369 371 % (self.path, i+1))
370 372 if key not in self:
371 373 self.order.append(key)
372 374 super(mapfile, self).__setitem__(key, value)
373 375 fp.close()
374 376
375 377 def __setitem__(self, key, value):
376 378 if self.fp is None:
377 379 try:
378 380 self.fp = open(self.path, 'a')
379 381 except IOError, err:
380 382 raise util.Abort(_('could not open map file %r: %s') %
381 383 (self.path, err.strerror))
382 384 self.fp.write('%s %s\n' % (key, value))
383 385 self.fp.flush()
384 386 super(mapfile, self).__setitem__(key, value)
385 387
386 388 def close(self):
387 389 if self.fp:
388 390 self.fp.close()
389 391 self.fp = None
@@ -1,396 +1,399 b''
1 1 # convcmd - convert extension commands definition
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 of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7
8 8 from common import NoRepo, MissingTool, SKIPREV, mapfile
9 9 from cvs import convert_cvs
10 10 from darcs import darcs_source
11 11 from git import convert_git
12 12 from hg import mercurial_source, mercurial_sink
13 13 from subversion import svn_source, svn_sink
14 14 from monotone import monotone_source
15 15 from gnuarch import gnuarch_source
16 16 from bzr import bzr_source
17 17 from p4 import p4_source
18 18 import filemap
19 19
20 20 import os, shutil
21 21 from mercurial import hg, util, encoding
22 22 from mercurial.i18n import _
23 23
24 24 orig_encoding = 'ascii'
25 25
26 26 def recode(s):
27 27 if isinstance(s, unicode):
28 28 return s.encode(orig_encoding, 'replace')
29 29 else:
30 30 return s.decode('utf-8').encode(orig_encoding, 'replace')
31 31
32 32 source_converters = [
33 33 ('cvs', convert_cvs, 'branchsort'),
34 34 ('git', convert_git, 'branchsort'),
35 35 ('svn', svn_source, 'branchsort'),
36 36 ('hg', mercurial_source, 'sourcesort'),
37 37 ('darcs', darcs_source, 'branchsort'),
38 38 ('mtn', monotone_source, 'branchsort'),
39 39 ('gnuarch', gnuarch_source, 'branchsort'),
40 40 ('bzr', bzr_source, 'branchsort'),
41 41 ('p4', p4_source, 'branchsort'),
42 42 ]
43 43
44 44 sink_converters = [
45 45 ('hg', mercurial_sink),
46 46 ('svn', svn_sink),
47 47 ]
48 48
49 49 def convertsource(ui, path, type, rev):
50 50 exceptions = []
51 51 for name, source, sortmode in source_converters:
52 52 try:
53 53 if not type or name == type:
54 54 return source(ui, path, rev), sortmode
55 55 except (NoRepo, MissingTool), inst:
56 56 exceptions.append(inst)
57 57 if not ui.quiet:
58 58 for inst in exceptions:
59 59 ui.write("%s\n" % inst)
60 60 raise util.Abort(_('%s: missing or unsupported repository') % path)
61 61
62 62 def convertsink(ui, path, type):
63 63 for name, sink in sink_converters:
64 64 try:
65 65 if not type or name == type:
66 66 return sink(ui, path)
67 67 except NoRepo, inst:
68 68 ui.note(_("convert: %s\n") % inst)
69 69 raise util.Abort(_('%s: unknown repository type') % path)
70 70
71 71 class converter(object):
72 72 def __init__(self, ui, source, dest, revmapfile, opts):
73 73
74 74 self.source = source
75 75 self.dest = dest
76 76 self.ui = ui
77 77 self.opts = opts
78 78 self.commitcache = {}
79 79 self.authors = {}
80 80 self.authorfile = None
81 81
82 82 # Record converted revisions persistently: maps source revision
83 83 # ID to target revision ID (both strings). (This is how
84 84 # incremental conversions work.)
85 85 self.map = mapfile(ui, revmapfile)
86 86
87 87 # Read first the dst author map if any
88 88 authorfile = self.dest.authorfile()
89 89 if authorfile and os.path.exists(authorfile):
90 90 self.readauthormap(authorfile)
91 91 # Extend/Override with new author map if necessary
92 92 if opts.get('authors'):
93 93 self.readauthormap(opts.get('authors'))
94 94 self.authorfile = self.dest.authorfile()
95 95
96 96 self.splicemap = mapfile(ui, opts.get('splicemap'))
97 97 self.branchmap = mapfile(ui, opts.get('branchmap'))
98 98
99 99 def walktree(self, heads):
100 100 '''Return a mapping that identifies the uncommitted parents of every
101 101 uncommitted changeset.'''
102 102 visit = heads
103 103 known = set()
104 104 parents = {}
105 105 while visit:
106 106 n = visit.pop(0)
107 107 if n in known or n in self.map: continue
108 108 known.add(n)
109 109 commit = self.cachecommit(n)
110 110 parents[n] = []
111 111 for p in commit.parents:
112 112 parents[n].append(p)
113 113 visit.append(p)
114 114
115 115 return parents
116 116
117 117 def toposort(self, parents, sortmode):
118 118 '''Return an ordering such that every uncommitted changeset is
119 119 preceeded by all its uncommitted ancestors.'''
120 120
121 121 def mapchildren(parents):
122 122 """Return a (children, roots) tuple where 'children' maps parent
123 123 revision identifiers to children ones, and 'roots' is the list of
124 124 revisions without parents. 'parents' must be a mapping of revision
125 125 identifier to its parents ones.
126 126 """
127 127 visit = parents.keys()
128 128 seen = set()
129 129 children = {}
130 130 roots = []
131 131
132 132 while visit:
133 133 n = visit.pop(0)
134 134 if n in seen:
135 135 continue
136 136 seen.add(n)
137 137 # Ensure that nodes without parents are present in the
138 138 # 'children' mapping.
139 139 children.setdefault(n, [])
140 140 hasparent = False
141 141 for p in parents[n]:
142 142 if not p in self.map:
143 143 visit.append(p)
144 144 hasparent = True
145 145 children.setdefault(p, []).append(n)
146 146 if not hasparent:
147 147 roots.append(n)
148 148
149 149 return children, roots
150 150
151 151 # Sort functions are supposed to take a list of revisions which
152 152 # can be converted immediately and pick one
153 153
154 154 def makebranchsorter():
155 155 """If the previously converted revision has a child in the
156 156 eligible revisions list, pick it. Return the list head
157 157 otherwise. Branch sort attempts to minimize branch
158 158 switching, which is harmful for Mercurial backend
159 159 compression.
160 160 """
161 161 prev = [None]
162 162 def picknext(nodes):
163 163 next = nodes[0]
164 164 for n in nodes:
165 165 if prev[0] in parents[n]:
166 166 next = n
167 167 break
168 168 prev[0] = next
169 169 return next
170 170 return picknext
171 171
172 172 def makesourcesorter():
173 173 """Source specific sort."""
174 174 keyfn = lambda n: self.commitcache[n].sortkey
175 175 def picknext(nodes):
176 176 return sorted(nodes, key=keyfn)[0]
177 177 return picknext
178 178
179 179 def makedatesorter():
180 180 """Sort revisions by date."""
181 181 dates = {}
182 182 def getdate(n):
183 183 if n not in dates:
184 184 dates[n] = util.parsedate(self.commitcache[n].date)
185 185 return dates[n]
186 186
187 187 def picknext(nodes):
188 188 return min([(getdate(n), n) for n in nodes])[1]
189 189
190 190 return picknext
191 191
192 192 if sortmode == 'branchsort':
193 193 picknext = makebranchsorter()
194 194 elif sortmode == 'datesort':
195 195 picknext = makedatesorter()
196 196 elif sortmode == 'sourcesort':
197 197 picknext = makesourcesorter()
198 198 else:
199 199 raise util.Abort(_('unknown sort mode: %s') % sortmode)
200 200
201 201 children, actives = mapchildren(parents)
202 202
203 203 s = []
204 204 pendings = {}
205 205 while actives:
206 206 n = picknext(actives)
207 207 actives.remove(n)
208 208 s.append(n)
209 209
210 210 # Update dependents list
211 211 for c in children.get(n, []):
212 212 if c not in pendings:
213 213 pendings[c] = [p for p in parents[c] if p not in self.map]
214 214 try:
215 215 pendings[c].remove(n)
216 216 except ValueError:
217 217 raise util.Abort(_('cycle detected between %s and %s')
218 218 % (recode(c), recode(n)))
219 219 if not pendings[c]:
220 220 # Parents are converted, node is eligible
221 221 actives.insert(0, c)
222 222 pendings[c] = None
223 223
224 224 if len(s) != len(parents):
225 225 raise util.Abort(_("not all revisions were sorted"))
226 226
227 227 return s
228 228
229 229 def writeauthormap(self):
230 230 authorfile = self.authorfile
231 231 if authorfile:
232 232 self.ui.status(_('Writing author map file %s\n') % authorfile)
233 233 ofile = open(authorfile, 'w+')
234 234 for author in self.authors:
235 235 ofile.write("%s=%s\n" % (author, self.authors[author]))
236 236 ofile.close()
237 237
238 238 def readauthormap(self, authorfile):
239 239 afile = open(authorfile, 'r')
240 240 for line in afile:
241 241
242 242 line = line.strip()
243 243 if not line or line.startswith('#'):
244 244 continue
245 245
246 246 try:
247 247 srcauthor, dstauthor = line.split('=', 1)
248 248 except ValueError:
249 249 msg = _('Ignoring bad line in author map file %s: %s\n')
250 250 self.ui.warn(msg % (authorfile, line.rstrip()))
251 251 continue
252 252
253 253 srcauthor = srcauthor.strip()
254 254 dstauthor = dstauthor.strip()
255 255 if self.authors.get(srcauthor) in (None, dstauthor):
256 256 msg = _('mapping author %s to %s\n')
257 257 self.ui.debug(msg % (srcauthor, dstauthor))
258 258 self.authors[srcauthor] = dstauthor
259 259 continue
260 260
261 261 m = _('overriding mapping for author %s, was %s, will be %s\n')
262 262 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
263 263
264 264 afile.close()
265 265
266 266 def cachecommit(self, rev):
267 267 commit = self.source.getcommit(rev)
268 268 commit.author = self.authors.get(commit.author, commit.author)
269 269 commit.branch = self.branchmap.get(commit.branch, commit.branch)
270 270 self.commitcache[rev] = commit
271 271 return commit
272 272
273 273 def copy(self, rev):
274 274 commit = self.commitcache[rev]
275 275
276 276 changes = self.source.getchanges(rev)
277 277 if isinstance(changes, basestring):
278 278 if changes == SKIPREV:
279 279 dest = SKIPREV
280 280 else:
281 281 dest = self.map[changes]
282 282 self.map[rev] = dest
283 283 return
284 284 files, copies = changes
285 285 pbranches = []
286 286 if commit.parents:
287 287 for prev in commit.parents:
288 288 if prev not in self.commitcache:
289 289 self.cachecommit(prev)
290 290 pbranches.append((self.map[prev],
291 291 self.commitcache[prev].branch))
292 292 self.dest.setbranch(commit.branch, pbranches)
293 293 try:
294 294 parents = self.splicemap[rev].replace(',', ' ').split()
295 295 self.ui.status(_('spliced in %s as parents of %s\n') %
296 296 (parents, rev))
297 297 parents = [self.map.get(p, p) for p in parents]
298 298 except KeyError:
299 299 parents = [b[0] for b in pbranches]
300 300 newnode = self.dest.putcommit(files, copies, parents, commit,
301 301 self.source, self.map)
302 302 self.source.converted(rev, newnode)
303 303 self.map[rev] = newnode
304 304
305 305 def convert(self, sortmode):
306 306 try:
307 307 self.source.before()
308 308 self.dest.before()
309 309 self.source.setrevmap(self.map)
310 310 self.ui.status(_("scanning source...\n"))
311 311 heads = self.source.getheads()
312 312 parents = self.walktree(heads)
313 313 self.ui.status(_("sorting...\n"))
314 314 t = self.toposort(parents, sortmode)
315 315 num = len(t)
316 316 c = None
317 317
318 318 self.ui.status(_("converting...\n"))
319 319 for c in t:
320 320 num -= 1
321 321 desc = self.commitcache[c].desc
322 322 if "\n" in desc:
323 323 desc = desc.splitlines()[0]
324 324 # convert log message to local encoding without using
325 325 # tolocal() because encoding.encoding conver() use it as
326 326 # 'utf-8'
327 327 self.ui.status("%d %s\n" % (num, recode(desc)))
328 328 self.ui.note(_("source: %s\n") % recode(c))
329 329 self.copy(c)
330 330
331 331 tags = self.source.gettags()
332 332 ctags = {}
333 333 for k in tags:
334 334 v = tags[k]
335 335 if self.map.get(v, SKIPREV) != SKIPREV:
336 336 ctags[k] = self.map[v]
337 337
338 338 if c and ctags:
339 nrev = self.dest.puttags(ctags)
340 # write another hash correspondence to override the previous
341 # one so we don't end up with extra tag heads
342 if nrev:
343 self.map[c] = nrev
339 nrev, tagsparent = self.dest.puttags(ctags)
340 if nrev and tagsparent:
341 # write another hash correspondence to override the previous
342 # one so we don't end up with extra tag heads
343 tagsparents = [e for e in self.map.iteritems()
344 if e[1] == tagsparent]
345 if tagsparents:
346 self.map[tagsparents[0][0]] = nrev
344 347
345 348 self.writeauthormap()
346 349 finally:
347 350 self.cleanup()
348 351
349 352 def cleanup(self):
350 353 try:
351 354 self.dest.after()
352 355 finally:
353 356 self.source.after()
354 357 self.map.close()
355 358
356 359 def convert(ui, src, dest=None, revmapfile=None, **opts):
357 360 global orig_encoding
358 361 orig_encoding = encoding.encoding
359 362 encoding.encoding = 'UTF-8'
360 363
361 364 if not dest:
362 365 dest = hg.defaultdest(src) + "-hg"
363 366 ui.status(_("assuming destination %s\n") % dest)
364 367
365 368 destc = convertsink(ui, dest, opts.get('dest_type'))
366 369
367 370 try:
368 371 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
369 372 opts.get('rev'))
370 373 except Exception:
371 374 for path in destc.created:
372 375 shutil.rmtree(path, True)
373 376 raise
374 377
375 378 sortmodes = ('branchsort', 'datesort', 'sourcesort')
376 379 sortmode = [m for m in sortmodes if opts.get(m)]
377 380 if len(sortmode) > 1:
378 381 raise util.Abort(_('more than one sort mode specified'))
379 382 sortmode = sortmode and sortmode[0] or defaultsort
380 383 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
381 384 raise util.Abort(_('--sourcesort is not supported by this data source'))
382 385
383 386 fmap = opts.get('filemap')
384 387 if fmap:
385 388 srcc = filemap.filemap_source(ui, srcc, fmap)
386 389 destc.setfilemapmode(True)
387 390
388 391 if not revmapfile:
389 392 try:
390 393 revmapfile = destc.revmapfile()
391 394 except:
392 395 revmapfile = os.path.join(destc, "map")
393 396
394 397 c = converter(ui, srcc, destc, revmapfile, opts)
395 398 c.convert(sortmode)
396 399
@@ -1,363 +1,363 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, incorporated herein by reference.
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, error
25 25
26 26 from common import NoRepo, commit, converter_source, converter_sink
27 27
28 28 class mercurial_sink(converter_sink):
29 29 def __init__(self, ui, path):
30 30 converter_sink.__init__(self, ui, path)
31 31 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
32 32 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
33 33 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
34 34 self.lastbranch = None
35 35 if os.path.isdir(path) and len(os.listdir(path)) > 0:
36 36 try:
37 37 self.repo = hg.repository(self.ui, path)
38 38 if not self.repo.local():
39 39 raise NoRepo(_('%s is not a local Mercurial repo') % path)
40 40 except error.RepoError, err:
41 41 ui.traceback()
42 42 raise NoRepo(err.args[0])
43 43 else:
44 44 try:
45 45 ui.status(_('initializing destination %s repository\n') % path)
46 46 self.repo = hg.repository(self.ui, path, create=True)
47 47 if not self.repo.local():
48 48 raise NoRepo(_('%s is not a local Mercurial repo') % path)
49 49 self.created.append(path)
50 50 except error.RepoError:
51 51 ui.traceback()
52 52 raise NoRepo("could not create hg repo %s as sink" % path)
53 53 self.lock = None
54 54 self.wlock = None
55 55 self.filemapmode = False
56 56
57 57 def before(self):
58 58 self.ui.debug(_('run hg sink pre-conversion action\n'))
59 59 self.wlock = self.repo.wlock()
60 60 self.lock = self.repo.lock()
61 61
62 62 def after(self):
63 63 self.ui.debug(_('run hg sink post-conversion action\n'))
64 64 self.lock.release()
65 65 self.wlock.release()
66 66
67 67 def revmapfile(self):
68 68 return os.path.join(self.path, ".hg", "shamap")
69 69
70 70 def authorfile(self):
71 71 return os.path.join(self.path, ".hg", "authormap")
72 72
73 73 def getheads(self):
74 74 h = self.repo.changelog.heads()
75 75 return [ hex(x) for x in h ]
76 76
77 77 def setbranch(self, branch, pbranches):
78 78 if not self.clonebranches:
79 79 return
80 80
81 81 setbranch = (branch != self.lastbranch)
82 82 self.lastbranch = branch
83 83 if not branch:
84 84 branch = 'default'
85 85 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
86 86 pbranch = pbranches and pbranches[0][1] or 'default'
87 87
88 88 branchpath = os.path.join(self.path, branch)
89 89 if setbranch:
90 90 self.after()
91 91 try:
92 92 self.repo = hg.repository(self.ui, branchpath)
93 93 except:
94 94 self.repo = hg.repository(self.ui, branchpath, create=True)
95 95 self.before()
96 96
97 97 # pbranches may bring revisions from other branches (merge parents)
98 98 # Make sure we have them, or pull them.
99 99 missings = {}
100 100 for b in pbranches:
101 101 try:
102 102 self.repo.lookup(b[0])
103 103 except:
104 104 missings.setdefault(b[1], []).append(b[0])
105 105
106 106 if missings:
107 107 self.after()
108 108 for pbranch, heads in missings.iteritems():
109 109 pbranchpath = os.path.join(self.path, pbranch)
110 110 prepo = hg.repository(self.ui, pbranchpath)
111 111 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
112 112 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
113 113 self.before()
114 114
115 115 def _rewritetags(self, source, revmap, data):
116 116 fp = cStringIO.StringIO()
117 117 for line in data.splitlines():
118 118 s = line.split(' ', 1)
119 119 if len(s) != 2:
120 120 continue
121 121 revid = revmap.get(source.lookuprev(s[0]))
122 122 if not revid:
123 123 continue
124 124 fp.write('%s %s\n' % (revid, s[1]))
125 125 return fp.getvalue()
126 126
127 127 def putcommit(self, files, copies, parents, commit, source, revmap):
128 128
129 129 files = dict(files)
130 130 def getfilectx(repo, memctx, f):
131 131 v = files[f]
132 132 data = source.getfile(f, v)
133 133 e = source.getmode(f, v)
134 134 if f == '.hgtags':
135 135 data = self._rewritetags(source, revmap, data)
136 136 return context.memfilectx(f, data, 'l' in e, 'x' in e, copies.get(f))
137 137
138 138 pl = []
139 139 for p in parents:
140 140 if p not in pl:
141 141 pl.append(p)
142 142 parents = pl
143 143 nparents = len(parents)
144 144 if self.filemapmode and nparents == 1:
145 145 m1node = self.repo.changelog.read(bin(parents[0]))[0]
146 146 parent = parents[0]
147 147
148 148 if len(parents) < 2: parents.append(nullid)
149 149 if len(parents) < 2: parents.append(nullid)
150 150 p2 = parents.pop(0)
151 151
152 152 text = commit.desc
153 153 extra = commit.extra.copy()
154 154 if self.branchnames and commit.branch:
155 155 extra['branch'] = commit.branch
156 156 if commit.rev:
157 157 extra['convert_revision'] = commit.rev
158 158
159 159 while parents:
160 160 p1 = p2
161 161 p2 = parents.pop(0)
162 162 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(), getfilectx,
163 163 commit.author, commit.date, extra)
164 164 self.repo.commitctx(ctx)
165 165 text = "(octopus merge fixup)\n"
166 166 p2 = hex(self.repo.changelog.tip())
167 167
168 168 if self.filemapmode and nparents == 1:
169 169 man = self.repo.manifest
170 170 mnode = self.repo.changelog.read(bin(p2))[0]
171 171 if not man.cmp(m1node, man.revision(mnode)):
172 172 self.ui.status(_("filtering out empty revision\n"))
173 173 self.repo.rollback()
174 174 return parent
175 175 return p2
176 176
177 177 def puttags(self, tags):
178 178 try:
179 179 parentctx = self.repo[self.tagsbranch]
180 180 tagparent = parentctx.node()
181 181 except error.RepoError:
182 182 parentctx = None
183 183 tagparent = nullid
184 184
185 185 try:
186 186 oldlines = sorted(parentctx['.hgtags'].data().splitlines(1))
187 187 except:
188 188 oldlines = []
189 189
190 190 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
191 191 if newlines == oldlines:
192 return None
192 return None, None
193 193 data = "".join(newlines)
194 194 def getfilectx(repo, memctx, f):
195 195 return context.memfilectx(f, data, False, False, None)
196 196
197 197 self.ui.status(_("updating tags\n"))
198 198 date = "%s 0" % int(time.mktime(time.gmtime()))
199 199 extra = {'branch': self.tagsbranch}
200 200 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
201 201 [".hgtags"], getfilectx, "convert-repo", date,
202 202 extra)
203 203 self.repo.commitctx(ctx)
204 return hex(self.repo.changelog.tip())
204 return hex(self.repo.changelog.tip()), hex(tagparent)
205 205
206 206 def setfilemapmode(self, active):
207 207 self.filemapmode = active
208 208
209 209 class mercurial_source(converter_source):
210 210 def __init__(self, ui, path, rev=None):
211 211 converter_source.__init__(self, ui, path, rev)
212 212 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
213 213 self.ignored = set()
214 214 self.saverev = ui.configbool('convert', 'hg.saverev', False)
215 215 try:
216 216 self.repo = hg.repository(self.ui, path)
217 217 # try to provoke an exception if this isn't really a hg
218 218 # repo, but some other bogus compatible-looking url
219 219 if not self.repo.local():
220 220 raise error.RepoError()
221 221 except error.RepoError:
222 222 ui.traceback()
223 223 raise NoRepo("%s is not a local Mercurial repo" % path)
224 224 self.lastrev = None
225 225 self.lastctx = None
226 226 self._changescache = None
227 227 self.convertfp = None
228 228 # Restrict converted revisions to startrev descendants
229 229 startnode = ui.config('convert', 'hg.startrev')
230 230 if startnode is not None:
231 231 try:
232 232 startnode = self.repo.lookup(startnode)
233 233 except error.RepoError:
234 234 raise util.Abort(_('%s is not a valid start revision')
235 235 % startnode)
236 236 startrev = self.repo.changelog.rev(startnode)
237 237 children = {startnode: 1}
238 238 for rev in self.repo.changelog.descendants(startrev):
239 239 children[self.repo.changelog.node(rev)] = 1
240 240 self.keep = children.__contains__
241 241 else:
242 242 self.keep = util.always
243 243
244 244 def changectx(self, rev):
245 245 if self.lastrev != rev:
246 246 self.lastctx = self.repo[rev]
247 247 self.lastrev = rev
248 248 return self.lastctx
249 249
250 250 def parents(self, ctx):
251 251 return [p.node() for p in ctx.parents()
252 252 if p and self.keep(p.node())]
253 253
254 254 def getheads(self):
255 255 if self.rev:
256 256 heads = [self.repo[self.rev].node()]
257 257 else:
258 258 heads = self.repo.heads()
259 259 return [hex(h) for h in heads if self.keep(h)]
260 260
261 261 def getfile(self, name, rev):
262 262 try:
263 263 return self.changectx(rev)[name].data()
264 264 except error.LookupError, err:
265 265 raise IOError(err)
266 266
267 267 def getmode(self, name, rev):
268 268 return self.changectx(rev).manifest().flags(name)
269 269
270 270 def getchanges(self, rev):
271 271 ctx = self.changectx(rev)
272 272 parents = self.parents(ctx)
273 273 if not parents:
274 274 files = sorted(ctx.manifest())
275 275 if self.ignoreerrors:
276 276 # calling getcopies() is a simple way to detect missing
277 277 # revlogs and populate self.ignored
278 278 self.getcopies(ctx, files)
279 279 return [(f, rev) for f in files if f not in self.ignored], {}
280 280 if self._changescache and self._changescache[0] == rev:
281 281 m, a, r = self._changescache[1]
282 282 else:
283 283 m, a, r = self.repo.status(parents[0], ctx.node())[:3]
284 284 # getcopies() detects missing revlogs early, run it before
285 285 # filtering the changes.
286 286 copies = self.getcopies(ctx, m + a)
287 287 changes = [(name, rev) for name in m + a + r
288 288 if name not in self.ignored]
289 289 return sorted(changes), copies
290 290
291 291 def getcopies(self, ctx, files):
292 292 copies = {}
293 293 for name in files:
294 294 if name in self.ignored:
295 295 continue
296 296 try:
297 297 copysource, copynode = ctx.filectx(name).renamed()
298 298 if copysource in self.ignored or not self.keep(copynode):
299 299 continue
300 300 copies[name] = copysource
301 301 except TypeError:
302 302 pass
303 303 except error.LookupError, e:
304 304 if not self.ignoreerrors:
305 305 raise
306 306 self.ignored.add(name)
307 307 self.ui.warn(_('ignoring: %s\n') % e)
308 308 return copies
309 309
310 310 def getcommit(self, rev):
311 311 ctx = self.changectx(rev)
312 312 parents = [hex(p) for p in self.parents(ctx)]
313 313 if self.saverev:
314 314 crev = rev
315 315 else:
316 316 crev = None
317 317 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
318 318 desc=ctx.description(), rev=crev, parents=parents,
319 319 branch=ctx.branch(), extra=ctx.extra(),
320 320 sortkey=ctx.rev())
321 321
322 322 def gettags(self):
323 323 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
324 324 return dict([(name, hex(node)) for name, node in tags
325 325 if self.keep(node)])
326 326
327 327 def getchangedfiles(self, rev, i):
328 328 ctx = self.changectx(rev)
329 329 parents = self.parents(ctx)
330 330 if not parents and i is None:
331 331 i = 0
332 332 changes = [], ctx.manifest().keys(), []
333 333 else:
334 334 i = i or 0
335 335 changes = self.repo.status(parents[i], ctx.node())[:3]
336 336 changes = [[f for f in l if f not in self.ignored] for l in changes]
337 337
338 338 if i == 0:
339 339 self._changescache = (rev, changes)
340 340
341 341 return changes[0] + changes[1] + changes[2]
342 342
343 343 def converted(self, rev, destrev):
344 344 if self.convertfp is None:
345 345 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
346 346 'a')
347 347 self.convertfp.write('%s %s\n' % (destrev, rev))
348 348 self.convertfp.flush()
349 349
350 350 def before(self):
351 351 self.ui.debug(_('run hg source pre-conversion action\n'))
352 352
353 353 def after(self):
354 354 self.ui.debug(_('run hg source post-conversion action\n'))
355 355
356 356 def hasnativeorder(self):
357 357 return True
358 358
359 359 def lookuprev(self, rev):
360 360 try:
361 361 return hex(self.repo.lookup(rev))
362 362 except error.RepoError:
363 363 return None
@@ -1,29 +1,29 b''
1 1 marked working directory as branch branch0
2 2 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
3 3 (branch merge, don't forget to commit)
4 4 % convert
5 5 3 adda
6 6 2 changea
7 7 1 addb
8 8 pulling from default into branch0
9 9 1 changesets found
10 10 0 mergeab
11 11 pulling from default into branch0
12 12 1 changesets found
13 13 marked working directory as branch branch1
14 14 marked working directory as branch branch2
15 15 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
16 16 (branch merge, don't forget to commit)
17 17 marked working directory as branch branch3
18 18 % incremental conversion
19 19 2 c1
20 20 pulling from branch0 into branch1
21 2 changesets found
21 4 changesets found
22 22 1 c2
23 23 pulling from branch0 into branch2
24 2 changesets found
24 4 changesets found
25 25 0 c3
26 26 pulling from branch2 into branch3
27 3 changesets found
27 5 changesets found
28 28 pulling from branch1 into branch3
29 29 1 changesets found
General Comments 0
You need to be logged in to leave comments. Login now