##// END OF EJS Templates
convert: add tagmap logic...
Sean Farley -
r20378:9616b031 default
parent child Browse files
Show More
@@ -1,447 +1,448 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 exc = abort and util.Abort or MissingTool
35 35 raise exc(_('cannot find required "%s" tool') % name)
36 36
37 37 class NoRepo(Exception):
38 38 pass
39 39
40 40 SKIPREV = 'SKIP'
41 41
42 42 class commit(object):
43 43 def __init__(self, author, date, desc, parents, branch=None, rev=None,
44 44 extra={}, sortkey=None):
45 45 self.author = author or 'unknown'
46 46 self.date = date or '0 0'
47 47 self.desc = desc
48 48 self.parents = parents
49 49 self.branch = branch
50 50 self.rev = rev
51 51 self.extra = extra
52 52 self.sortkey = sortkey
53 53
54 54 class converter_source(object):
55 55 """Conversion source interface"""
56 56
57 57 def __init__(self, ui, path=None, rev=None):
58 58 """Initialize conversion source (or raise NoRepo("message")
59 59 exception if path is not a valid repository)"""
60 60 self.ui = ui
61 61 self.path = path
62 62 self.rev = rev
63 63
64 64 self.encoding = 'utf-8'
65 65
66 66 def checkhexformat(self, revstr, mapname='splicemap'):
67 67 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
68 68 such format for their revision numbering
69 69 """
70 70 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
71 71 raise util.Abort(_('%s entry %s is not a valid revision'
72 72 ' identifier') % (mapname, revstr))
73 73
74 74 def before(self):
75 75 pass
76 76
77 77 def after(self):
78 78 pass
79 79
80 80 def setrevmap(self, revmap):
81 81 """set the map of already-converted revisions"""
82 82 pass
83 83
84 84 def getheads(self):
85 85 """Return a list of this repository's heads"""
86 86 raise NotImplementedError
87 87
88 88 def getfile(self, name, rev):
89 89 """Return a pair (data, mode) where data is the file content
90 90 as a string and mode one of '', 'x' or 'l'. rev is the
91 91 identifier returned by a previous call to getchanges(). Raise
92 92 IOError to indicate that name was deleted in rev.
93 93 """
94 94 raise NotImplementedError
95 95
96 96 def getchanges(self, version):
97 97 """Returns a tuple of (files, copies).
98 98
99 99 files is a sorted list of (filename, id) tuples for all files
100 100 changed between version and its first parent returned by
101 101 getcommit(). id is the source revision id of the file.
102 102
103 103 copies is a dictionary of dest: source
104 104 """
105 105 raise NotImplementedError
106 106
107 107 def getcommit(self, version):
108 108 """Return the commit object for version"""
109 109 raise NotImplementedError
110 110
111 111 def gettags(self):
112 112 """Return the tags as a dictionary of name: revision
113 113
114 114 Tag names must be UTF-8 strings.
115 115 """
116 116 raise NotImplementedError
117 117
118 118 def recode(self, s, encoding=None):
119 119 if not encoding:
120 120 encoding = self.encoding or 'utf-8'
121 121
122 122 if isinstance(s, unicode):
123 123 return s.encode("utf-8")
124 124 try:
125 125 return s.decode(encoding).encode("utf-8")
126 126 except UnicodeError:
127 127 try:
128 128 return s.decode("latin-1").encode("utf-8")
129 129 except UnicodeError:
130 130 return s.decode(encoding, "replace").encode("utf-8")
131 131
132 132 def getchangedfiles(self, rev, i):
133 133 """Return the files changed by rev compared to parent[i].
134 134
135 135 i is an index selecting one of the parents of rev. The return
136 136 value should be the list of files that are different in rev and
137 137 this parent.
138 138
139 139 If rev has no parents, i is None.
140 140
141 141 This function is only needed to support --filemap
142 142 """
143 143 raise NotImplementedError
144 144
145 145 def converted(self, rev, sinkrev):
146 146 '''Notify the source that a revision has been converted.'''
147 147 pass
148 148
149 149 def hasnativeorder(self):
150 150 """Return true if this source has a meaningful, native revision
151 151 order. For instance, Mercurial revisions are store sequentially
152 152 while there is no such global ordering with Darcs.
153 153 """
154 154 return False
155 155
156 156 def hasnativeclose(self):
157 157 """Return true if this source has ability to close branch.
158 158 """
159 159 return False
160 160
161 161 def lookuprev(self, rev):
162 162 """If rev is a meaningful revision reference in source, return
163 163 the referenced identifier in the same format used by getcommit().
164 164 return None otherwise.
165 165 """
166 166 return None
167 167
168 168 def getbookmarks(self):
169 169 """Return the bookmarks as a dictionary of name: revision
170 170
171 171 Bookmark names are to be UTF-8 strings.
172 172 """
173 173 return {}
174 174
175 175 def checkrevformat(self, revstr, mapname='splicemap'):
176 176 """revstr is a string that describes a revision in the given
177 177 source control system. Return true if revstr has correct
178 178 format.
179 179 """
180 180 return True
181 181
182 182 class converter_sink(object):
183 183 """Conversion sink (target) interface"""
184 184
185 185 def __init__(self, ui, path):
186 186 """Initialize conversion sink (or raise NoRepo("message")
187 187 exception if path is not a valid repository)
188 188
189 189 created is a list of paths to remove if a fatal error occurs
190 190 later"""
191 191 self.ui = ui
192 192 self.path = path
193 193 self.created = []
194 194
195 195 def getheads(self):
196 196 """Return a list of this repository's heads"""
197 197 raise NotImplementedError
198 198
199 199 def revmapfile(self):
200 200 """Path to a file that will contain lines
201 201 source_rev_id sink_rev_id
202 202 mapping equivalent revision identifiers for each system."""
203 203 raise NotImplementedError
204 204
205 205 def authorfile(self):
206 206 """Path to a file that will contain lines
207 207 srcauthor=dstauthor
208 208 mapping equivalent authors identifiers for each system."""
209 209 return None
210 210
211 def putcommit(self, files, copies, parents, commit, source, revmap):
211 def putcommit(self, files, copies, parents, commit, source,
212 revmap, tagmap):
212 213 """Create a revision with all changed files listed in 'files'
213 214 and having listed parents. 'commit' is a commit object
214 215 containing at a minimum the author, date, and message for this
215 216 changeset. 'files' is a list of (path, version) tuples,
216 217 'copies' is a dictionary mapping destinations to sources,
217 218 'source' is the source repository, and 'revmap' is a mapfile
218 219 of source revisions to converted revisions. Only getfile() and
219 220 lookuprev() should be called on 'source'.
220 221
221 222 Note that the sink repository is not told to update itself to
222 223 a particular revision (or even what that revision would be)
223 224 before it receives the file data.
224 225 """
225 226 raise NotImplementedError
226 227
227 228 def puttags(self, tags):
228 229 """Put tags into sink.
229 230
230 231 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
231 232 Return a pair (tag_revision, tag_parent_revision), or (None, None)
232 233 if nothing was changed.
233 234 """
234 235 raise NotImplementedError
235 236
236 237 def setbranch(self, branch, pbranches):
237 238 """Set the current branch name. Called before the first putcommit
238 239 on the branch.
239 240 branch: branch name for subsequent commits
240 241 pbranches: (converted parent revision, parent branch) tuples"""
241 242 pass
242 243
243 244 def setfilemapmode(self, active):
244 245 """Tell the destination that we're using a filemap
245 246
246 247 Some converter_sources (svn in particular) can claim that a file
247 248 was changed in a revision, even if there was no change. This method
248 249 tells the destination that we're using a filemap and that it should
249 250 filter empty revisions.
250 251 """
251 252 pass
252 253
253 254 def before(self):
254 255 pass
255 256
256 257 def after(self):
257 258 pass
258 259
259 260 def putbookmarks(self, bookmarks):
260 261 """Put bookmarks into sink.
261 262
262 263 bookmarks: {bookmarkname: sink_rev_id, ...}
263 264 where bookmarkname is an UTF-8 string.
264 265 """
265 266 pass
266 267
267 268 def hascommit(self, rev):
268 269 """Return True if the sink contains rev"""
269 270 raise NotImplementedError
270 271
271 272 class commandline(object):
272 273 def __init__(self, ui, command):
273 274 self.ui = ui
274 275 self.command = command
275 276
276 277 def prerun(self):
277 278 pass
278 279
279 280 def postrun(self):
280 281 pass
281 282
282 283 def _cmdline(self, cmd, *args, **kwargs):
283 284 cmdline = [self.command, cmd] + list(args)
284 285 for k, v in kwargs.iteritems():
285 286 if len(k) == 1:
286 287 cmdline.append('-' + k)
287 288 else:
288 289 cmdline.append('--' + k.replace('_', '-'))
289 290 try:
290 291 if len(k) == 1:
291 292 cmdline.append('' + v)
292 293 else:
293 294 cmdline[-1] += '=' + v
294 295 except TypeError:
295 296 pass
296 297 cmdline = [util.shellquote(arg) for arg in cmdline]
297 298 if not self.ui.debugflag:
298 299 cmdline += ['2>', os.devnull]
299 300 cmdline = ' '.join(cmdline)
300 301 return cmdline
301 302
302 303 def _run(self, cmd, *args, **kwargs):
303 304 def popen(cmdline):
304 305 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
305 306 close_fds=util.closefds,
306 307 stdout=subprocess.PIPE)
307 308 return p
308 309 return self._dorun(popen, cmd, *args, **kwargs)
309 310
310 311 def _run2(self, cmd, *args, **kwargs):
311 312 return self._dorun(util.popen2, cmd, *args, **kwargs)
312 313
313 314 def _dorun(self, openfunc, cmd, *args, **kwargs):
314 315 cmdline = self._cmdline(cmd, *args, **kwargs)
315 316 self.ui.debug('running: %s\n' % (cmdline,))
316 317 self.prerun()
317 318 try:
318 319 return openfunc(cmdline)
319 320 finally:
320 321 self.postrun()
321 322
322 323 def run(self, cmd, *args, **kwargs):
323 324 p = self._run(cmd, *args, **kwargs)
324 325 output = p.communicate()[0]
325 326 self.ui.debug(output)
326 327 return output, p.returncode
327 328
328 329 def runlines(self, cmd, *args, **kwargs):
329 330 p = self._run(cmd, *args, **kwargs)
330 331 output = p.stdout.readlines()
331 332 p.wait()
332 333 self.ui.debug(''.join(output))
333 334 return output, p.returncode
334 335
335 336 def checkexit(self, status, output=''):
336 337 if status:
337 338 if output:
338 339 self.ui.warn(_('%s error:\n') % self.command)
339 340 self.ui.warn(output)
340 341 msg = util.explainexit(status)[0]
341 342 raise util.Abort('%s %s' % (self.command, msg))
342 343
343 344 def run0(self, cmd, *args, **kwargs):
344 345 output, status = self.run(cmd, *args, **kwargs)
345 346 self.checkexit(status, output)
346 347 return output
347 348
348 349 def runlines0(self, cmd, *args, **kwargs):
349 350 output, status = self.runlines(cmd, *args, **kwargs)
350 351 self.checkexit(status, ''.join(output))
351 352 return output
352 353
353 354 @propertycache
354 355 def argmax(self):
355 356 # POSIX requires at least 4096 bytes for ARG_MAX
356 357 argmax = 4096
357 358 try:
358 359 argmax = os.sysconf("SC_ARG_MAX")
359 360 except (AttributeError, ValueError):
360 361 pass
361 362
362 363 # Windows shells impose their own limits on command line length,
363 364 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
364 365 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
365 366 # details about cmd.exe limitations.
366 367
367 368 # Since ARG_MAX is for command line _and_ environment, lower our limit
368 369 # (and make happy Windows shells while doing this).
369 370 return argmax // 2 - 1
370 371
371 372 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
372 373 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
373 374 limit = self.argmax - cmdlen
374 375 bytes = 0
375 376 fl = []
376 377 for fn in arglist:
377 378 b = len(fn) + 3
378 379 if bytes + b < limit or len(fl) == 0:
379 380 fl.append(fn)
380 381 bytes += b
381 382 else:
382 383 yield fl
383 384 fl = [fn]
384 385 bytes = b
385 386 if fl:
386 387 yield fl
387 388
388 389 def xargs(self, arglist, cmd, *args, **kwargs):
389 390 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
390 391 self.run0(cmd, *(list(args) + l), **kwargs)
391 392
392 393 class mapfile(dict):
393 394 def __init__(self, ui, path):
394 395 super(mapfile, self).__init__()
395 396 self.ui = ui
396 397 self.path = path
397 398 self.fp = None
398 399 self.order = []
399 400 self._read()
400 401
401 402 def _read(self):
402 403 if not self.path:
403 404 return
404 405 try:
405 406 fp = open(self.path, 'r')
406 407 except IOError, err:
407 408 if err.errno != errno.ENOENT:
408 409 raise
409 410 return
410 411 for i, line in enumerate(fp):
411 412 line = line.splitlines()[0].rstrip()
412 413 if not line:
413 414 # Ignore blank lines
414 415 continue
415 416 try:
416 417 key, value = line.rsplit(' ', 1)
417 418 except ValueError:
418 419 raise util.Abort(
419 420 _('syntax error in %s(%d): key/value pair expected')
420 421 % (self.path, i + 1))
421 422 if key not in self:
422 423 self.order.append(key)
423 424 super(mapfile, self).__setitem__(key, value)
424 425 fp.close()
425 426
426 427 def __setitem__(self, key, value):
427 428 if self.fp is None:
428 429 try:
429 430 self.fp = open(self.path, 'a')
430 431 except IOError, err:
431 432 raise util.Abort(_('could not open map file %r: %s') %
432 433 (self.path, err.strerror))
433 434 self.fp.write('%s %s\n' % (key, value))
434 435 self.fp.flush()
435 436 super(mapfile, self).__setitem__(key, value)
436 437
437 438 def close(self):
438 439 if self.fp:
439 440 self.fp.close()
440 441 self.fp = None
441 442
442 443 def makedatetimestamp(t):
443 444 """Like util.makedate() but for time t instead of current time"""
444 445 delta = (datetime.datetime.utcfromtimestamp(t) -
445 446 datetime.datetime.fromtimestamp(t))
446 447 tz = delta.days * 86400 + delta.seconds
447 448 return t, tz
@@ -1,567 +1,571 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 or any later version.
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, shlex
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 if type and type not in [s[0] for s in source_converters]:
52 52 raise util.Abort(_('%s: invalid source repository type') % type)
53 53 for name, source, sortmode in source_converters:
54 54 try:
55 55 if not type or name == type:
56 56 return source(ui, path, rev), sortmode
57 57 except (NoRepo, MissingTool), inst:
58 58 exceptions.append(inst)
59 59 if not ui.quiet:
60 60 for inst in exceptions:
61 61 ui.write("%s\n" % inst)
62 62 raise util.Abort(_('%s: missing or unsupported repository') % path)
63 63
64 64 def convertsink(ui, path, type):
65 65 if type and type not in [s[0] for s in sink_converters]:
66 66 raise util.Abort(_('%s: invalid destination repository type') % type)
67 67 for name, sink in sink_converters:
68 68 try:
69 69 if not type or name == type:
70 70 return sink(ui, path)
71 71 except NoRepo, inst:
72 72 ui.note(_("convert: %s\n") % inst)
73 73 except MissingTool, inst:
74 74 raise util.Abort('%s\n' % inst)
75 75 raise util.Abort(_('%s: unknown repository type') % path)
76 76
77 77 class progresssource(object):
78 78 def __init__(self, ui, source, filecount):
79 79 self.ui = ui
80 80 self.source = source
81 81 self.filecount = filecount
82 82 self.retrieved = 0
83 83
84 84 def getfile(self, file, rev):
85 85 self.retrieved += 1
86 86 self.ui.progress(_('getting files'), self.retrieved,
87 87 item=file, total=self.filecount)
88 88 return self.source.getfile(file, rev)
89 89
90 90 def lookuprev(self, rev):
91 91 return self.source.lookuprev(rev)
92 92
93 93 def close(self):
94 94 self.ui.progress(_('getting files'), None)
95 95
96 96 class converter(object):
97 97 def __init__(self, ui, source, dest, revmapfile, opts):
98 98
99 99 self.source = source
100 100 self.dest = dest
101 101 self.ui = ui
102 102 self.opts = opts
103 103 self.commitcache = {}
104 104 self.authors = {}
105 105 self.authorfile = None
106 106
107 107 # Record converted revisions persistently: maps source revision
108 108 # ID to target revision ID (both strings). (This is how
109 109 # incremental conversions work.)
110 110 self.map = mapfile(ui, revmapfile)
111 111
112 112 # Read first the dst author map if any
113 113 authorfile = self.dest.authorfile()
114 114 if authorfile and os.path.exists(authorfile):
115 115 self.readauthormap(authorfile)
116 116 # Extend/Override with new author map if necessary
117 117 if opts.get('authormap'):
118 118 self.readauthormap(opts.get('authormap'))
119 119 self.authorfile = self.dest.authorfile()
120 120
121 121 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
122 122 self.branchmap = mapfile(ui, opts.get('branchmap'))
123 123 self.closemap = self.parseclosemap(opts.get('closemap'))
124 self.tagmap = mapfile(ui, opts.get('tagmap'))
124 125
125 126 def parseclosemap(self, path):
126 127 """ check and validate the closemap format and
127 128 return a list of revs to close.
128 129 Format checking has two parts.
129 130 1. generic format which is same across all source types
130 131 2. specific format checking which may be different for
131 132 different source type. This logic is implemented in
132 133 checkrevformat function in source files like
133 134 hg.py, subversion.py etc.
134 135 """
135 136
136 137 if not path:
137 138 return []
138 139 m = []
139 140 try:
140 141 fp = open(path, 'r')
141 142 for i, line in enumerate(fp):
142 143 line = line.splitlines()[0].rstrip()
143 144 if not line:
144 145 # Ignore blank lines
145 146 continue
146 147 # split line
147 148 lex = shlex.shlex(line, posix=True)
148 149 lex.whitespace_split = True
149 150 lex.whitespace += ','
150 151 line = list(lex)
151 152 for part in line:
152 153 self.source.checkrevformat(part, 'closemap')
153 154 m.extend(line)
154 155 # if file does not exist or error reading, exit
155 156 except IOError:
156 157 raise util.Abort(_('closemap file not found or error reading %s:')
157 158 % path)
158 159 return m
159 160
160 161 def parsesplicemap(self, path):
161 162 """ check and validate the splicemap format and
162 163 return a child/parents dictionary.
163 164 Format checking has two parts.
164 165 1. generic format which is same across all source types
165 166 2. specific format checking which may be different for
166 167 different source type. This logic is implemented in
167 168 checkrevformat function in source files like
168 169 hg.py, subversion.py etc.
169 170 """
170 171
171 172 if not path:
172 173 return {}
173 174 m = {}
174 175 try:
175 176 fp = open(path, 'r')
176 177 for i, line in enumerate(fp):
177 178 line = line.splitlines()[0].rstrip()
178 179 if not line:
179 180 # Ignore blank lines
180 181 continue
181 182 # split line
182 183 lex = shlex.shlex(line, posix=True)
183 184 lex.whitespace_split = True
184 185 lex.whitespace += ','
185 186 line = list(lex)
186 187 # check number of parents
187 188 if not (2 <= len(line) <= 3):
188 189 raise util.Abort(_('syntax error in %s(%d): child parent1'
189 190 '[,parent2] expected') % (path, i + 1))
190 191 for part in line:
191 192 self.source.checkrevformat(part)
192 193 child, p1, p2 = line[0], line[1:2], line[2:]
193 194 if p1 == p2:
194 195 m[child] = p1
195 196 else:
196 197 m[child] = p1 + p2
197 198 # if file does not exist or error reading, exit
198 199 except IOError:
199 200 raise util.Abort(_('splicemap file not found or error reading %s:')
200 201 % path)
201 202 return m
202 203
203 204
204 205 def walktree(self, heads):
205 206 '''Return a mapping that identifies the uncommitted parents of every
206 207 uncommitted changeset.'''
207 208 visit = heads
208 209 known = set()
209 210 parents = {}
210 211 while visit:
211 212 n = visit.pop(0)
212 213 if n in known or n in self.map:
213 214 continue
214 215 known.add(n)
215 216 self.ui.progress(_('scanning'), len(known), unit=_('revisions'))
216 217 commit = self.cachecommit(n)
217 218 parents[n] = []
218 219 for p in commit.parents:
219 220 parents[n].append(p)
220 221 visit.append(p)
221 222 self.ui.progress(_('scanning'), None)
222 223
223 224 return parents
224 225
225 226 def mergesplicemap(self, parents, splicemap):
226 227 """A splicemap redefines child/parent relationships. Check the
227 228 map contains valid revision identifiers and merge the new
228 229 links in the source graph.
229 230 """
230 231 for c in sorted(splicemap):
231 232 if c not in parents:
232 233 if not self.dest.hascommit(self.map.get(c, c)):
233 234 # Could be in source but not converted during this run
234 235 self.ui.warn(_('splice map revision %s is not being '
235 236 'converted, ignoring\n') % c)
236 237 continue
237 238 pc = []
238 239 for p in splicemap[c]:
239 240 # We do not have to wait for nodes already in dest.
240 241 if self.dest.hascommit(self.map.get(p, p)):
241 242 continue
242 243 # Parent is not in dest and not being converted, not good
243 244 if p not in parents:
244 245 raise util.Abort(_('unknown splice map parent: %s') % p)
245 246 pc.append(p)
246 247 parents[c] = pc
247 248
248 249 def toposort(self, parents, sortmode):
249 250 '''Return an ordering such that every uncommitted changeset is
250 251 preceded by all its uncommitted ancestors.'''
251 252
252 253 def mapchildren(parents):
253 254 """Return a (children, roots) tuple where 'children' maps parent
254 255 revision identifiers to children ones, and 'roots' is the list of
255 256 revisions without parents. 'parents' must be a mapping of revision
256 257 identifier to its parents ones.
257 258 """
258 259 visit = sorted(parents)
259 260 seen = set()
260 261 children = {}
261 262 roots = []
262 263
263 264 while visit:
264 265 n = visit.pop(0)
265 266 if n in seen:
266 267 continue
267 268 seen.add(n)
268 269 # Ensure that nodes without parents are present in the
269 270 # 'children' mapping.
270 271 children.setdefault(n, [])
271 272 hasparent = False
272 273 for p in parents[n]:
273 274 if p not in self.map:
274 275 visit.append(p)
275 276 hasparent = True
276 277 children.setdefault(p, []).append(n)
277 278 if not hasparent:
278 279 roots.append(n)
279 280
280 281 return children, roots
281 282
282 283 # Sort functions are supposed to take a list of revisions which
283 284 # can be converted immediately and pick one
284 285
285 286 def makebranchsorter():
286 287 """If the previously converted revision has a child in the
287 288 eligible revisions list, pick it. Return the list head
288 289 otherwise. Branch sort attempts to minimize branch
289 290 switching, which is harmful for Mercurial backend
290 291 compression.
291 292 """
292 293 prev = [None]
293 294 def picknext(nodes):
294 295 next = nodes[0]
295 296 for n in nodes:
296 297 if prev[0] in parents[n]:
297 298 next = n
298 299 break
299 300 prev[0] = next
300 301 return next
301 302 return picknext
302 303
303 304 def makesourcesorter():
304 305 """Source specific sort."""
305 306 keyfn = lambda n: self.commitcache[n].sortkey
306 307 def picknext(nodes):
307 308 return sorted(nodes, key=keyfn)[0]
308 309 return picknext
309 310
310 311 def makeclosesorter():
311 312 """Close order sort."""
312 313 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
313 314 self.commitcache[n].sortkey)
314 315 def picknext(nodes):
315 316 return sorted(nodes, key=keyfn)[0]
316 317 return picknext
317 318
318 319 def makedatesorter():
319 320 """Sort revisions by date."""
320 321 dates = {}
321 322 def getdate(n):
322 323 if n not in dates:
323 324 dates[n] = util.parsedate(self.commitcache[n].date)
324 325 return dates[n]
325 326
326 327 def picknext(nodes):
327 328 return min([(getdate(n), n) for n in nodes])[1]
328 329
329 330 return picknext
330 331
331 332 if sortmode == 'branchsort':
332 333 picknext = makebranchsorter()
333 334 elif sortmode == 'datesort':
334 335 picknext = makedatesorter()
335 336 elif sortmode == 'sourcesort':
336 337 picknext = makesourcesorter()
337 338 elif sortmode == 'closesort':
338 339 picknext = makeclosesorter()
339 340 else:
340 341 raise util.Abort(_('unknown sort mode: %s') % sortmode)
341 342
342 343 children, actives = mapchildren(parents)
343 344
344 345 s = []
345 346 pendings = {}
346 347 while actives:
347 348 n = picknext(actives)
348 349 actives.remove(n)
349 350 s.append(n)
350 351
351 352 # Update dependents list
352 353 for c in children.get(n, []):
353 354 if c not in pendings:
354 355 pendings[c] = [p for p in parents[c] if p not in self.map]
355 356 try:
356 357 pendings[c].remove(n)
357 358 except ValueError:
358 359 raise util.Abort(_('cycle detected between %s and %s')
359 360 % (recode(c), recode(n)))
360 361 if not pendings[c]:
361 362 # Parents are converted, node is eligible
362 363 actives.insert(0, c)
363 364 pendings[c] = None
364 365
365 366 if len(s) != len(parents):
366 367 raise util.Abort(_("not all revisions were sorted"))
367 368
368 369 return s
369 370
370 371 def writeauthormap(self):
371 372 authorfile = self.authorfile
372 373 if authorfile:
373 374 self.ui.status(_('writing author map file %s\n') % authorfile)
374 375 ofile = open(authorfile, 'w+')
375 376 for author in self.authors:
376 377 ofile.write("%s=%s\n" % (author, self.authors[author]))
377 378 ofile.close()
378 379
379 380 def readauthormap(self, authorfile):
380 381 afile = open(authorfile, 'r')
381 382 for line in afile:
382 383
383 384 line = line.strip()
384 385 if not line or line.startswith('#'):
385 386 continue
386 387
387 388 try:
388 389 srcauthor, dstauthor = line.split('=', 1)
389 390 except ValueError:
390 391 msg = _('ignoring bad line in author map file %s: %s\n')
391 392 self.ui.warn(msg % (authorfile, line.rstrip()))
392 393 continue
393 394
394 395 srcauthor = srcauthor.strip()
395 396 dstauthor = dstauthor.strip()
396 397 if self.authors.get(srcauthor) in (None, dstauthor):
397 398 msg = _('mapping author %s to %s\n')
398 399 self.ui.debug(msg % (srcauthor, dstauthor))
399 400 self.authors[srcauthor] = dstauthor
400 401 continue
401 402
402 403 m = _('overriding mapping for author %s, was %s, will be %s\n')
403 404 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
404 405
405 406 afile.close()
406 407
407 408 def cachecommit(self, rev):
408 409 commit = self.source.getcommit(rev)
409 410 commit.author = self.authors.get(commit.author, commit.author)
410 411 # If commit.branch is None, this commit is coming from the source
411 412 # repository's default branch and destined for the default branch in the
412 413 # destination repository. For such commits, passing a literal "None"
413 414 # string to branchmap.get() below allows the user to map "None" to an
414 415 # alternate default branch in the destination repository.
415 416 commit.branch = self.branchmap.get(str(commit.branch), commit.branch)
416 417 self.commitcache[rev] = commit
417 418 return commit
418 419
419 420 def copy(self, rev):
420 421 commit = self.commitcache[rev]
421 422
422 423 changes = self.source.getchanges(rev)
423 424 if isinstance(changes, basestring):
424 425 if changes == SKIPREV:
425 426 dest = SKIPREV
426 427 else:
427 428 dest = self.map[changes]
428 429 self.map[rev] = dest
429 430 return
430 431 files, copies = changes
431 432 pbranches = []
432 433 if commit.parents:
433 434 for prev in commit.parents:
434 435 if prev not in self.commitcache:
435 436 self.cachecommit(prev)
436 437 pbranches.append((self.map[prev],
437 438 self.commitcache[prev].branch))
438 439 self.dest.setbranch(commit.branch, pbranches)
439 440 try:
440 441 parents = self.splicemap[rev]
441 442 self.ui.status(_('spliced in %s as parents of %s\n') %
442 443 (parents, rev))
443 444 parents = [self.map.get(p, p) for p in parents]
444 445 except KeyError:
445 446 parents = [b[0] for b in pbranches]
446 447 source = progresssource(self.ui, self.source, len(files))
447 448 if self.closemap and rev in self.closemap:
448 449 commit.extra['close'] = 1
449 450
450 451 newnode = self.dest.putcommit(files, copies, parents, commit,
451 source, self.map)
452 source, self.map, self.tagmap)
452 453 source.close()
453 454 self.source.converted(rev, newnode)
454 455 self.map[rev] = newnode
455 456
456 457 def convert(self, sortmode):
457 458 try:
458 459 self.source.before()
459 460 self.dest.before()
460 461 self.source.setrevmap(self.map)
461 462 self.ui.status(_("scanning source...\n"))
462 463 heads = self.source.getheads()
463 464 parents = self.walktree(heads)
464 465 self.mergesplicemap(parents, self.splicemap)
465 466 self.ui.status(_("sorting...\n"))
466 467 t = self.toposort(parents, sortmode)
467 468 num = len(t)
468 469 c = None
469 470
470 471 self.ui.status(_("converting...\n"))
471 472 for i, c in enumerate(t):
472 473 num -= 1
473 474 desc = self.commitcache[c].desc
474 475 if "\n" in desc:
475 476 desc = desc.splitlines()[0]
476 477 # convert log message to local encoding without using
477 478 # tolocal() because the encoding.encoding convert()
478 479 # uses is 'utf-8'
479 480 self.ui.status("%d %s\n" % (num, recode(desc)))
480 481 self.ui.note(_("source: %s\n") % recode(c))
481 482 self.ui.progress(_('converting'), i, unit=_('revisions'),
482 483 total=len(t))
483 484 self.copy(c)
484 485 self.ui.progress(_('converting'), None)
485 486
486 487 tags = self.source.gettags()
488 tags = dict((self.tagmap.get(k, k), v)
489 for k, v in tags.iteritems())
490
487 491 ctags = {}
488 492 for k in tags:
489 493 v = tags[k]
490 494 if self.map.get(v, SKIPREV) != SKIPREV:
491 495 ctags[k] = self.map[v]
492 496
493 497 if c and ctags:
494 498 nrev, tagsparent = self.dest.puttags(ctags)
495 499 if nrev and tagsparent:
496 500 # write another hash correspondence to override the previous
497 501 # one so we don't end up with extra tag heads
498 502 tagsparents = [e for e in self.map.iteritems()
499 503 if e[1] == tagsparent]
500 504 if tagsparents:
501 505 self.map[tagsparents[0][0]] = nrev
502 506
503 507 bookmarks = self.source.getbookmarks()
504 508 cbookmarks = {}
505 509 for k in bookmarks:
506 510 v = bookmarks[k]
507 511 if self.map.get(v, SKIPREV) != SKIPREV:
508 512 cbookmarks[k] = self.map[v]
509 513
510 514 if c and cbookmarks:
511 515 self.dest.putbookmarks(cbookmarks)
512 516
513 517 self.writeauthormap()
514 518 finally:
515 519 self.cleanup()
516 520
517 521 def cleanup(self):
518 522 try:
519 523 self.dest.after()
520 524 finally:
521 525 self.source.after()
522 526 self.map.close()
523 527
524 528 def convert(ui, src, dest=None, revmapfile=None, **opts):
525 529 global orig_encoding
526 530 orig_encoding = encoding.encoding
527 531 encoding.encoding = 'UTF-8'
528 532
529 533 # support --authors as an alias for --authormap
530 534 if not opts.get('authormap'):
531 535 opts['authormap'] = opts.get('authors')
532 536
533 537 if not dest:
534 538 dest = hg.defaultdest(src) + "-hg"
535 539 ui.status(_("assuming destination %s\n") % dest)
536 540
537 541 destc = convertsink(ui, dest, opts.get('dest_type'))
538 542
539 543 try:
540 544 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
541 545 opts.get('rev'))
542 546 except Exception:
543 547 for path in destc.created:
544 548 shutil.rmtree(path, True)
545 549 raise
546 550
547 551 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
548 552 sortmode = [m for m in sortmodes if opts.get(m)]
549 553 if len(sortmode) > 1:
550 554 raise util.Abort(_('more than one sort mode specified'))
551 555 sortmode = sortmode and sortmode[0] or defaultsort
552 556 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
553 557 raise util.Abort(_('--sourcesort is not supported by this data source'))
554 558 if sortmode == 'closesort' and not srcc.hasnativeclose():
555 559 raise util.Abort(_('--closesort is not supported by this data source'))
556 560
557 561 fmap = opts.get('filemap')
558 562 if fmap:
559 563 srcc = filemap.filemap_source(ui, srcc, fmap)
560 564 destc.setfilemapmode(True)
561 565
562 566 if not revmapfile:
563 567 revmapfile = destc.revmapfile()
564 568
565 569 c = converter(ui, srcc, destc, revmapfile, opts)
566 570 c.convert(sortmode)
567 571
@@ -1,450 +1,451 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
25 25
26 26 from common import NoRepo, commit, converter_source, converter_sink
27 27
28 28 import re
29 29 sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
30 30
31 31 class mercurial_sink(converter_sink):
32 32 def __init__(self, ui, path):
33 33 converter_sink.__init__(self, ui, path)
34 34 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
35 35 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
36 36 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
37 37 self.lastbranch = None
38 38 if os.path.isdir(path) and len(os.listdir(path)) > 0:
39 39 try:
40 40 self.repo = hg.repository(self.ui, path)
41 41 if not self.repo.local():
42 42 raise NoRepo(_('%s is not a local Mercurial repository')
43 43 % path)
44 44 except error.RepoError, err:
45 45 ui.traceback()
46 46 raise NoRepo(err.args[0])
47 47 else:
48 48 try:
49 49 ui.status(_('initializing destination %s repository\n') % path)
50 50 self.repo = hg.repository(self.ui, path, create=True)
51 51 if not self.repo.local():
52 52 raise NoRepo(_('%s is not a local Mercurial repository')
53 53 % path)
54 54 self.created.append(path)
55 55 except error.RepoError:
56 56 ui.traceback()
57 57 raise NoRepo(_("could not create hg repository %s as sink")
58 58 % path)
59 59 self.lock = None
60 60 self.wlock = None
61 61 self.filemapmode = False
62 62
63 63 def before(self):
64 64 self.ui.debug('run hg sink pre-conversion action\n')
65 65 self.wlock = self.repo.wlock()
66 66 self.lock = self.repo.lock()
67 67
68 68 def after(self):
69 69 self.ui.debug('run hg sink post-conversion action\n')
70 70 if self.lock:
71 71 self.lock.release()
72 72 if self.wlock:
73 73 self.wlock.release()
74 74
75 75 def revmapfile(self):
76 76 return self.repo.join("shamap")
77 77
78 78 def authorfile(self):
79 79 return self.repo.join("authormap")
80 80
81 81 def getheads(self):
82 82 h = self.repo.changelog.heads()
83 83 return [hex(x) for x in h]
84 84
85 85 def setbranch(self, branch, pbranches):
86 86 if not self.clonebranches:
87 87 return
88 88
89 89 setbranch = (branch != self.lastbranch)
90 90 self.lastbranch = branch
91 91 if not branch:
92 92 branch = 'default'
93 93 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
94 94 pbranch = pbranches and pbranches[0][1] or '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 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
121 121 self.before()
122 122
123 def _rewritetags(self, source, revmap, data):
123 def _rewritetags(self, source, revmap, tagmap, data):
124 124 fp = cStringIO.StringIO()
125 125 for line in data.splitlines():
126 126 s = line.split(' ', 1)
127 127 if len(s) != 2:
128 128 continue
129 129 revid = revmap.get(source.lookuprev(s[0]))
130 130 if not revid:
131 131 continue
132 fp.write('%s %s\n' % (revid, s[1]))
132 fp.write('%s %s\n' % (revid, tagmap.get(s[1], s[1])))
133 133 return fp.getvalue()
134 134
135 def putcommit(self, files, copies, parents, commit, source, revmap):
135 def putcommit(self, files, copies, parents, commit, source,
136 revmap, tagmap):
136 137
137 138 files = dict(files)
138 139 def getfilectx(repo, memctx, f):
139 140 v = files[f]
140 141 data, mode = source.getfile(f, v)
141 142 if f == '.hgtags':
142 data = self._rewritetags(source, revmap, data)
143 data = self._rewritetags(source, revmap, tagmap, data)
143 144 return context.memfilectx(f, data, 'l' in mode, 'x' in mode,
144 145 copies.get(f))
145 146
146 147 pl = []
147 148 for p in parents:
148 149 if p not in pl:
149 150 pl.append(p)
150 151 parents = pl
151 152 nparents = len(parents)
152 153 if self.filemapmode and nparents == 1:
153 154 m1node = self.repo.changelog.read(bin(parents[0]))[0]
154 155 parent = parents[0]
155 156
156 157 if len(parents) < 2:
157 158 parents.append(nullid)
158 159 if len(parents) < 2:
159 160 parents.append(nullid)
160 161 p2 = parents.pop(0)
161 162
162 163 text = commit.desc
163 164
164 165 sha1s = re.findall(sha1re, text)
165 166 for sha1 in sha1s:
166 167 oldrev = source.lookuprev(sha1)
167 168 newrev = revmap.get(oldrev)
168 169 if newrev is not None:
169 170 text = text.replace(sha1, newrev[:len(sha1)])
170 171
171 172 extra = commit.extra.copy()
172 173 if self.branchnames and commit.branch:
173 174 extra['branch'] = commit.branch
174 175 if commit.rev:
175 176 extra['convert_revision'] = commit.rev
176 177
177 178 while parents:
178 179 p1 = p2
179 180 p2 = parents.pop(0)
180 181 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(),
181 182 getfilectx, commit.author, commit.date, extra)
182 183 self.repo.commitctx(ctx)
183 184 text = "(octopus merge fixup)\n"
184 185 p2 = hex(self.repo.changelog.tip())
185 186
186 187 if self.filemapmode and nparents == 1:
187 188 man = self.repo.manifest
188 189 mnode = self.repo.changelog.read(bin(p2))[0]
189 190 closed = 'close' in commit.extra
190 191 if not closed and not man.cmp(m1node, man.revision(mnode)):
191 192 self.ui.status(_("filtering out empty revision\n"))
192 193 self.repo.rollback(force=True)
193 194 return parent
194 195 return p2
195 196
196 197 def puttags(self, tags):
197 198 try:
198 199 parentctx = self.repo[self.tagsbranch]
199 200 tagparent = parentctx.node()
200 201 except error.RepoError:
201 202 parentctx = None
202 203 tagparent = nullid
203 204
204 205 oldlines = set()
205 206 for branch, heads in self.repo.branchmap().iteritems():
206 207 for h in heads:
207 208 if '.hgtags' in self.repo[h]:
208 209 oldlines.update(
209 210 set(self.repo[h]['.hgtags'].data().splitlines(True)))
210 211 oldlines = sorted(list(oldlines))
211 212
212 213 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
213 214 if newlines == oldlines:
214 215 return None, None
215 216
216 217 # if the old and new tags match, then there is nothing to update
217 218 oldtags = set()
218 219 newtags = set()
219 220 for line in oldlines:
220 221 s = line.strip().split(' ', 1)
221 222 if len(s) != 2:
222 223 continue
223 224 oldtags.add(s[1])
224 225 for line in newlines:
225 226 s = line.strip().split(' ', 1)
226 227 if len(s) != 2:
227 228 continue
228 229 if s[1] not in oldtags:
229 230 newtags.add(s[1].strip())
230 231
231 232 if not newtags:
232 233 return None, None
233 234
234 235 data = "".join(newlines)
235 236 def getfilectx(repo, memctx, f):
236 237 return context.memfilectx(f, data, False, False, None)
237 238
238 239 self.ui.status(_("updating tags\n"))
239 240 date = "%s 0" % int(time.mktime(time.gmtime()))
240 241 extra = {'branch': self.tagsbranch}
241 242 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
242 243 [".hgtags"], getfilectx, "convert-repo", date,
243 244 extra)
244 245 self.repo.commitctx(ctx)
245 246 return hex(self.repo.changelog.tip()), hex(tagparent)
246 247
247 248 def setfilemapmode(self, active):
248 249 self.filemapmode = active
249 250
250 251 def putbookmarks(self, updatedbookmark):
251 252 if not len(updatedbookmark):
252 253 return
253 254
254 255 self.ui.status(_("updating bookmarks\n"))
255 256 destmarks = self.repo._bookmarks
256 257 for bookmark in updatedbookmark:
257 258 destmarks[bookmark] = bin(updatedbookmark[bookmark])
258 259 destmarks.write()
259 260
260 261 def hascommit(self, rev):
261 262 if rev not in self.repo and self.clonebranches:
262 263 raise util.Abort(_('revision %s not found in destination '
263 264 'repository (lookups with clonebranches=true '
264 265 'are not implemented)') % rev)
265 266 return rev in self.repo
266 267
267 268 class mercurial_source(converter_source):
268 269 def __init__(self, ui, path, rev=None):
269 270 converter_source.__init__(self, ui, path, rev)
270 271 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
271 272 self.ignored = set()
272 273 self.saverev = ui.configbool('convert', 'hg.saverev', False)
273 274 try:
274 275 self.repo = hg.repository(self.ui, path)
275 276 # try to provoke an exception if this isn't really a hg
276 277 # repo, but some other bogus compatible-looking url
277 278 if not self.repo.local():
278 279 raise error.RepoError
279 280 except error.RepoError:
280 281 ui.traceback()
281 282 raise NoRepo(_("%s is not a local Mercurial repository") % path)
282 283 self.lastrev = None
283 284 self.lastctx = None
284 285 self._changescache = None
285 286 self.convertfp = None
286 287 # Restrict converted revisions to startrev descendants
287 288 startnode = ui.config('convert', 'hg.startrev')
288 289 hgrevs = ui.config('convert', 'hg.revs')
289 290 if hgrevs is None:
290 291 if startnode is not None:
291 292 try:
292 293 startnode = self.repo.lookup(startnode)
293 294 except error.RepoError:
294 295 raise util.Abort(_('%s is not a valid start revision')
295 296 % startnode)
296 297 startrev = self.repo.changelog.rev(startnode)
297 298 children = {startnode: 1}
298 299 for r in self.repo.changelog.descendants([startrev]):
299 300 children[self.repo.changelog.node(r)] = 1
300 301 self.keep = children.__contains__
301 302 else:
302 303 self.keep = util.always
303 304 if rev:
304 305 self._heads = [self.repo[rev].node()]
305 306 else:
306 307 self._heads = self.repo.heads()
307 308 else:
308 309 if rev or startnode is not None:
309 310 raise util.Abort(_('hg.revs cannot be combined with '
310 311 'hg.startrev or --rev'))
311 312 nodes = set()
312 313 parents = set()
313 314 for r in scmutil.revrange(self.repo, [hgrevs]):
314 315 ctx = self.repo[r]
315 316 nodes.add(ctx.node())
316 317 parents.update(p.node() for p in ctx.parents())
317 318 self.keep = nodes.__contains__
318 319 self._heads = nodes - parents
319 320
320 321 def changectx(self, rev):
321 322 if self.lastrev != rev:
322 323 self.lastctx = self.repo[rev]
323 324 self.lastrev = rev
324 325 return self.lastctx
325 326
326 327 def parents(self, ctx):
327 328 return [p for p in ctx.parents() if p and self.keep(p.node())]
328 329
329 330 def getheads(self):
330 331 return [hex(h) for h in self._heads if self.keep(h)]
331 332
332 333 def getfile(self, name, rev):
333 334 try:
334 335 fctx = self.changectx(rev)[name]
335 336 return fctx.data(), fctx.flags()
336 337 except error.LookupError, err:
337 338 raise IOError(err)
338 339
339 340 def getchanges(self, rev):
340 341 ctx = self.changectx(rev)
341 342 parents = self.parents(ctx)
342 343 if not parents:
343 344 files = sorted(ctx.manifest())
344 345 # getcopies() is not needed for roots, but it is a simple way to
345 346 # detect missing revlogs and abort on errors or populate
346 347 # self.ignored
347 348 self.getcopies(ctx, parents, files)
348 349 return [(f, rev) for f in files if f not in self.ignored], {}
349 350 if self._changescache and self._changescache[0] == rev:
350 351 m, a, r = self._changescache[1]
351 352 else:
352 353 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
353 354 # getcopies() detects missing revlogs early, run it before
354 355 # filtering the changes.
355 356 copies = self.getcopies(ctx, parents, m + a)
356 357 changes = [(name, rev) for name in m + a + r
357 358 if name not in self.ignored]
358 359 return sorted(changes), copies
359 360
360 361 def getcopies(self, ctx, parents, files):
361 362 copies = {}
362 363 for name in files:
363 364 if name in self.ignored:
364 365 continue
365 366 try:
366 367 copysource, _copynode = ctx.filectx(name).renamed()
367 368 if copysource in self.ignored:
368 369 continue
369 370 # Ignore copy sources not in parent revisions
370 371 found = False
371 372 for p in parents:
372 373 if copysource in p:
373 374 found = True
374 375 break
375 376 if not found:
376 377 continue
377 378 copies[name] = copysource
378 379 except TypeError:
379 380 pass
380 381 except error.LookupError, e:
381 382 if not self.ignoreerrors:
382 383 raise
383 384 self.ignored.add(name)
384 385 self.ui.warn(_('ignoring: %s\n') % e)
385 386 return copies
386 387
387 388 def getcommit(self, rev):
388 389 ctx = self.changectx(rev)
389 390 parents = [p.hex() for p in self.parents(ctx)]
390 391 if self.saverev:
391 392 crev = rev
392 393 else:
393 394 crev = None
394 395 return commit(author=ctx.user(),
395 396 date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
396 397 desc=ctx.description(), rev=crev, parents=parents,
397 398 branch=ctx.branch(), extra=ctx.extra(),
398 399 sortkey=ctx.rev())
399 400
400 401 def gettags(self):
401 402 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
402 403 return dict([(name, hex(node)) for name, node in tags
403 404 if self.keep(node)])
404 405
405 406 def getchangedfiles(self, rev, i):
406 407 ctx = self.changectx(rev)
407 408 parents = self.parents(ctx)
408 409 if not parents and i is None:
409 410 i = 0
410 411 changes = [], ctx.manifest().keys(), []
411 412 else:
412 413 i = i or 0
413 414 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
414 415 changes = [[f for f in l if f not in self.ignored] for l in changes]
415 416
416 417 if i == 0:
417 418 self._changescache = (rev, changes)
418 419
419 420 return changes[0] + changes[1] + changes[2]
420 421
421 422 def converted(self, rev, destrev):
422 423 if self.convertfp is None:
423 424 self.convertfp = open(self.repo.join('shamap'), 'a')
424 425 self.convertfp.write('%s %s\n' % (destrev, rev))
425 426 self.convertfp.flush()
426 427
427 428 def before(self):
428 429 self.ui.debug('run hg source pre-conversion action\n')
429 430
430 431 def after(self):
431 432 self.ui.debug('run hg source post-conversion action\n')
432 433
433 434 def hasnativeorder(self):
434 435 return True
435 436
436 437 def hasnativeclose(self):
437 438 return True
438 439
439 440 def lookuprev(self, rev):
440 441 try:
441 442 return hex(self.repo.lookup(rev))
442 443 except error.RepoError:
443 444 return None
444 445
445 446 def getbookmarks(self):
446 447 return bookmarks.listbookmarks(self.repo)
447 448
448 449 def checkrevformat(self, revstr, mapname='splicemap'):
449 450 """ Mercurial, revision string is a 40 byte hex """
450 451 self.checkhexformat(revstr, mapname)
@@ -1,1266 +1,1267 b''
1 1 # Subversion 1.4/1.5 Python API backend
2 2 #
3 3 # Copyright(C) 2007 Daniel Holth et al
4 4
5 5 import os, re, sys, tempfile, urllib, urllib2
6 6 import xml.dom.minidom
7 7 import cPickle as pickle
8 8
9 9 from mercurial import strutil, scmutil, util, encoding
10 10 from mercurial.i18n import _
11 11
12 12 propertycache = util.propertycache
13 13
14 14 # Subversion stuff. Works best with very recent Python SVN bindings
15 15 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
16 16 # these bindings.
17 17
18 18 from cStringIO import StringIO
19 19
20 20 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
21 21 from common import commandline, converter_source, converter_sink, mapfile
22 22 from common import makedatetimestamp
23 23
24 24 try:
25 25 from svn.core import SubversionException, Pool
26 26 import svn
27 27 import svn.client
28 28 import svn.core
29 29 import svn.ra
30 30 import svn.delta
31 31 import transport
32 32 import warnings
33 33 warnings.filterwarnings('ignore',
34 34 module='svn.core',
35 35 category=DeprecationWarning)
36 36
37 37 except ImportError:
38 38 svn = None
39 39
40 40 class SvnPathNotFound(Exception):
41 41 pass
42 42
43 43 def revsplit(rev):
44 44 """Parse a revision string and return (uuid, path, revnum)."""
45 45 url, revnum = rev.rsplit('@', 1)
46 46 parts = url.split('/', 1)
47 47 mod = ''
48 48 if len(parts) > 1:
49 49 mod = '/' + parts[1]
50 50 return parts[0][4:], mod, int(revnum)
51 51
52 52 def quote(s):
53 53 # As of svn 1.7, many svn calls expect "canonical" paths. In
54 54 # theory, we should call svn.core.*canonicalize() on all paths
55 55 # before passing them to the API. Instead, we assume the base url
56 56 # is canonical and copy the behaviour of svn URL encoding function
57 57 # so we can extend it safely with new components. The "safe"
58 58 # characters were taken from the "svn_uri__char_validity" table in
59 59 # libsvn_subr/path.c.
60 60 return urllib.quote(s, "!$&'()*+,-./:=@_~")
61 61
62 62 def geturl(path):
63 63 try:
64 64 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
65 65 except SubversionException:
66 66 # svn.client.url_from_path() fails with local repositories
67 67 pass
68 68 if os.path.isdir(path):
69 69 path = os.path.normpath(os.path.abspath(path))
70 70 if os.name == 'nt':
71 71 path = '/' + util.normpath(path)
72 72 # Module URL is later compared with the repository URL returned
73 73 # by svn API, which is UTF-8.
74 74 path = encoding.tolocal(path)
75 75 path = 'file://%s' % quote(path)
76 76 return svn.core.svn_path_canonicalize(path)
77 77
78 78 def optrev(number):
79 79 optrev = svn.core.svn_opt_revision_t()
80 80 optrev.kind = svn.core.svn_opt_revision_number
81 81 optrev.value.number = number
82 82 return optrev
83 83
84 84 class changedpath(object):
85 85 def __init__(self, p):
86 86 self.copyfrom_path = p.copyfrom_path
87 87 self.copyfrom_rev = p.copyfrom_rev
88 88 self.action = p.action
89 89
90 90 def get_log_child(fp, url, paths, start, end, limit=0,
91 91 discover_changed_paths=True, strict_node_history=False):
92 92 protocol = -1
93 93 def receiver(orig_paths, revnum, author, date, message, pool):
94 94 paths = {}
95 95 if orig_paths is not None:
96 96 for k, v in orig_paths.iteritems():
97 97 paths[k] = changedpath(v)
98 98 pickle.dump((paths, revnum, author, date, message),
99 99 fp, protocol)
100 100
101 101 try:
102 102 # Use an ra of our own so that our parent can consume
103 103 # our results without confusing the server.
104 104 t = transport.SvnRaTransport(url=url)
105 105 svn.ra.get_log(t.ra, paths, start, end, limit,
106 106 discover_changed_paths,
107 107 strict_node_history,
108 108 receiver)
109 109 except IOError:
110 110 # Caller may interrupt the iteration
111 111 pickle.dump(None, fp, protocol)
112 112 except Exception, inst:
113 113 pickle.dump(str(inst), fp, protocol)
114 114 else:
115 115 pickle.dump(None, fp, protocol)
116 116 fp.close()
117 117 # With large history, cleanup process goes crazy and suddenly
118 118 # consumes *huge* amount of memory. The output file being closed,
119 119 # there is no need for clean termination.
120 120 os._exit(0)
121 121
122 122 def debugsvnlog(ui, **opts):
123 123 """Fetch SVN log in a subprocess and channel them back to parent to
124 124 avoid memory collection issues.
125 125 """
126 126 if svn is None:
127 127 raise util.Abort(_('debugsvnlog could not load Subversion python '
128 128 'bindings'))
129 129
130 130 util.setbinary(sys.stdin)
131 131 util.setbinary(sys.stdout)
132 132 args = decodeargs(sys.stdin.read())
133 133 get_log_child(sys.stdout, *args)
134 134
135 135 class logstream(object):
136 136 """Interruptible revision log iterator."""
137 137 def __init__(self, stdout):
138 138 self._stdout = stdout
139 139
140 140 def __iter__(self):
141 141 while True:
142 142 try:
143 143 entry = pickle.load(self._stdout)
144 144 except EOFError:
145 145 raise util.Abort(_('Mercurial failed to run itself, check'
146 146 ' hg executable is in PATH'))
147 147 try:
148 148 orig_paths, revnum, author, date, message = entry
149 149 except (TypeError, ValueError):
150 150 if entry is None:
151 151 break
152 152 raise util.Abort(_("log stream exception '%s'") % entry)
153 153 yield entry
154 154
155 155 def close(self):
156 156 if self._stdout:
157 157 self._stdout.close()
158 158 self._stdout = None
159 159
160 160
161 161 # Check to see if the given path is a local Subversion repo. Verify this by
162 162 # looking for several svn-specific files and directories in the given
163 163 # directory.
164 164 def filecheck(ui, path, proto):
165 165 for x in ('locks', 'hooks', 'format', 'db'):
166 166 if not os.path.exists(os.path.join(path, x)):
167 167 return False
168 168 return True
169 169
170 170 # Check to see if a given path is the root of an svn repo over http. We verify
171 171 # this by requesting a version-controlled URL we know can't exist and looking
172 172 # for the svn-specific "not found" XML.
173 173 def httpcheck(ui, path, proto):
174 174 try:
175 175 opener = urllib2.build_opener()
176 176 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
177 177 data = rsp.read()
178 178 except urllib2.HTTPError, inst:
179 179 if inst.code != 404:
180 180 # Except for 404 we cannot know for sure this is not an svn repo
181 181 ui.warn(_('svn: cannot probe remote repository, assume it could '
182 182 'be a subversion repository. Use --source-type if you '
183 183 'know better.\n'))
184 184 return True
185 185 data = inst.fp.read()
186 186 except Exception:
187 187 # Could be urllib2.URLError if the URL is invalid or anything else.
188 188 return False
189 189 return '<m:human-readable errcode="160013">' in data
190 190
191 191 protomap = {'http': httpcheck,
192 192 'https': httpcheck,
193 193 'file': filecheck,
194 194 }
195 195 def issvnurl(ui, url):
196 196 try:
197 197 proto, path = url.split('://', 1)
198 198 if proto == 'file':
199 199 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
200 200 and path[2:6].lower() == '%3a/'):
201 201 path = path[:2] + ':/' + path[6:]
202 202 path = urllib.url2pathname(path)
203 203 except ValueError:
204 204 proto = 'file'
205 205 path = os.path.abspath(url)
206 206 if proto == 'file':
207 207 path = util.pconvert(path)
208 208 check = protomap.get(proto, lambda *args: False)
209 209 while '/' in path:
210 210 if check(ui, path, proto):
211 211 return True
212 212 path = path.rsplit('/', 1)[0]
213 213 return False
214 214
215 215 # SVN conversion code stolen from bzr-svn and tailor
216 216 #
217 217 # Subversion looks like a versioned filesystem, branches structures
218 218 # are defined by conventions and not enforced by the tool. First,
219 219 # we define the potential branches (modules) as "trunk" and "branches"
220 220 # children directories. Revisions are then identified by their
221 221 # module and revision number (and a repository identifier).
222 222 #
223 223 # The revision graph is really a tree (or a forest). By default, a
224 224 # revision parent is the previous revision in the same module. If the
225 225 # module directory is copied/moved from another module then the
226 226 # revision is the module root and its parent the source revision in
227 227 # the parent module. A revision has at most one parent.
228 228 #
229 229 class svn_source(converter_source):
230 230 def __init__(self, ui, url, rev=None):
231 231 super(svn_source, self).__init__(ui, url, rev=rev)
232 232
233 233 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
234 234 (os.path.exists(url) and
235 235 os.path.exists(os.path.join(url, '.svn'))) or
236 236 issvnurl(ui, url)):
237 237 raise NoRepo(_("%s does not look like a Subversion repository")
238 238 % url)
239 239 if svn is None:
240 240 raise MissingTool(_('could not load Subversion python bindings'))
241 241
242 242 try:
243 243 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
244 244 if version < (1, 4):
245 245 raise MissingTool(_('Subversion python bindings %d.%d found, '
246 246 '1.4 or later required') % version)
247 247 except AttributeError:
248 248 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
249 249 'or later required'))
250 250
251 251 self.lastrevs = {}
252 252
253 253 latest = None
254 254 try:
255 255 # Support file://path@rev syntax. Useful e.g. to convert
256 256 # deleted branches.
257 257 at = url.rfind('@')
258 258 if at >= 0:
259 259 latest = int(url[at + 1:])
260 260 url = url[:at]
261 261 except ValueError:
262 262 pass
263 263 self.url = geturl(url)
264 264 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
265 265 try:
266 266 self.transport = transport.SvnRaTransport(url=self.url)
267 267 self.ra = self.transport.ra
268 268 self.ctx = self.transport.client
269 269 self.baseurl = svn.ra.get_repos_root(self.ra)
270 270 # Module is either empty or a repository path starting with
271 271 # a slash and not ending with a slash.
272 272 self.module = urllib.unquote(self.url[len(self.baseurl):])
273 273 self.prevmodule = None
274 274 self.rootmodule = self.module
275 275 self.commits = {}
276 276 self.paths = {}
277 277 self.uuid = svn.ra.get_uuid(self.ra)
278 278 except SubversionException:
279 279 ui.traceback()
280 280 raise NoRepo(_("%s does not look like a Subversion repository")
281 281 % self.url)
282 282
283 283 if rev:
284 284 try:
285 285 latest = int(rev)
286 286 except ValueError:
287 287 raise util.Abort(_('svn: revision %s is not an integer') % rev)
288 288
289 289 self.trunkname = self.ui.config('convert', 'svn.trunk',
290 290 'trunk').strip('/')
291 291 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
292 292 try:
293 293 self.startrev = int(self.startrev)
294 294 if self.startrev < 0:
295 295 self.startrev = 0
296 296 except ValueError:
297 297 raise util.Abort(_('svn: start revision %s is not an integer')
298 298 % self.startrev)
299 299
300 300 try:
301 301 self.head = self.latest(self.module, latest)
302 302 except SvnPathNotFound:
303 303 self.head = None
304 304 if not self.head:
305 305 raise util.Abort(_('no revision found in module %s')
306 306 % self.module)
307 307 self.last_changed = self.revnum(self.head)
308 308
309 309 self._changescache = None
310 310
311 311 if os.path.exists(os.path.join(url, '.svn/entries')):
312 312 self.wc = url
313 313 else:
314 314 self.wc = None
315 315 self.convertfp = None
316 316
317 317 def setrevmap(self, revmap):
318 318 lastrevs = {}
319 319 for revid in revmap.iterkeys():
320 320 uuid, module, revnum = revsplit(revid)
321 321 lastrevnum = lastrevs.setdefault(module, revnum)
322 322 if revnum > lastrevnum:
323 323 lastrevs[module] = revnum
324 324 self.lastrevs = lastrevs
325 325
326 326 def exists(self, path, optrev):
327 327 try:
328 328 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
329 329 optrev, False, self.ctx)
330 330 return True
331 331 except SubversionException:
332 332 return False
333 333
334 334 def getheads(self):
335 335
336 336 def isdir(path, revnum):
337 337 kind = self._checkpath(path, revnum)
338 338 return kind == svn.core.svn_node_dir
339 339
340 340 def getcfgpath(name, rev):
341 341 cfgpath = self.ui.config('convert', 'svn.' + name)
342 342 if cfgpath is not None and cfgpath.strip() == '':
343 343 return None
344 344 path = (cfgpath or name).strip('/')
345 345 if not self.exists(path, rev):
346 346 if self.module.endswith(path) and name == 'trunk':
347 347 # we are converting from inside this directory
348 348 return None
349 349 if cfgpath:
350 350 raise util.Abort(_('expected %s to be at %r, but not found')
351 351 % (name, path))
352 352 return None
353 353 self.ui.note(_('found %s at %r\n') % (name, path))
354 354 return path
355 355
356 356 rev = optrev(self.last_changed)
357 357 oldmodule = ''
358 358 trunk = getcfgpath('trunk', rev)
359 359 self.tags = getcfgpath('tags', rev)
360 360 branches = getcfgpath('branches', rev)
361 361
362 362 # If the project has a trunk or branches, we will extract heads
363 363 # from them. We keep the project root otherwise.
364 364 if trunk:
365 365 oldmodule = self.module or ''
366 366 self.module += '/' + trunk
367 367 self.head = self.latest(self.module, self.last_changed)
368 368 if not self.head:
369 369 raise util.Abort(_('no revision found in module %s')
370 370 % self.module)
371 371
372 372 # First head in the list is the module's head
373 373 self.heads = [self.head]
374 374 if self.tags is not None:
375 375 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
376 376
377 377 # Check if branches bring a few more heads to the list
378 378 if branches:
379 379 rpath = self.url.strip('/')
380 380 branchnames = svn.client.ls(rpath + '/' + quote(branches),
381 381 rev, False, self.ctx)
382 382 for branch in sorted(branchnames):
383 383 module = '%s/%s/%s' % (oldmodule, branches, branch)
384 384 if not isdir(module, self.last_changed):
385 385 continue
386 386 brevid = self.latest(module, self.last_changed)
387 387 if not brevid:
388 388 self.ui.note(_('ignoring empty branch %s\n') % branch)
389 389 continue
390 390 self.ui.note(_('found branch %s at %d\n') %
391 391 (branch, self.revnum(brevid)))
392 392 self.heads.append(brevid)
393 393
394 394 if self.startrev and self.heads:
395 395 if len(self.heads) > 1:
396 396 raise util.Abort(_('svn: start revision is not supported '
397 397 'with more than one branch'))
398 398 revnum = self.revnum(self.heads[0])
399 399 if revnum < self.startrev:
400 400 raise util.Abort(
401 401 _('svn: no revision found after start revision %d')
402 402 % self.startrev)
403 403
404 404 return self.heads
405 405
406 406 def getchanges(self, rev):
407 407 if self._changescache and self._changescache[0] == rev:
408 408 return self._changescache[1]
409 409 self._changescache = None
410 410 (paths, parents) = self.paths[rev]
411 411 if parents:
412 412 files, self.removed, copies = self.expandpaths(rev, paths, parents)
413 413 else:
414 414 # Perform a full checkout on roots
415 415 uuid, module, revnum = revsplit(rev)
416 416 entries = svn.client.ls(self.baseurl + quote(module),
417 417 optrev(revnum), True, self.ctx)
418 418 files = [n for n, e in entries.iteritems()
419 419 if e.kind == svn.core.svn_node_file]
420 420 copies = {}
421 421 self.removed = set()
422 422
423 423 files.sort()
424 424 files = zip(files, [rev] * len(files))
425 425
426 426 # caller caches the result, so free it here to release memory
427 427 del self.paths[rev]
428 428 return (files, copies)
429 429
430 430 def getchangedfiles(self, rev, i):
431 431 changes = self.getchanges(rev)
432 432 self._changescache = (rev, changes)
433 433 return [f[0] for f in changes[0]]
434 434
435 435 def getcommit(self, rev):
436 436 if rev not in self.commits:
437 437 uuid, module, revnum = revsplit(rev)
438 438 self.module = module
439 439 self.reparent(module)
440 440 # We assume that:
441 441 # - requests for revisions after "stop" come from the
442 442 # revision graph backward traversal. Cache all of them
443 443 # down to stop, they will be used eventually.
444 444 # - requests for revisions before "stop" come to get
445 445 # isolated branches parents. Just fetch what is needed.
446 446 stop = self.lastrevs.get(module, 0)
447 447 if revnum < stop:
448 448 stop = revnum + 1
449 449 self._fetch_revisions(revnum, stop)
450 450 if rev not in self.commits:
451 451 raise util.Abort(_('svn: revision %s not found') % revnum)
452 452 commit = self.commits[rev]
453 453 # caller caches the result, so free it here to release memory
454 454 del self.commits[rev]
455 455 return commit
456 456
457 457 def checkrevformat(self, revstr, mapname='splicemap'):
458 458 """ fails if revision format does not match the correct format"""
459 459 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
460 460 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
461 461 '{12,12}(.*)\@[0-9]+$',revstr):
462 462 raise util.Abort(_('%s entry %s is not a valid revision'
463 463 ' identifier') % (mapname, revstr))
464 464
465 465 def gettags(self):
466 466 tags = {}
467 467 if self.tags is None:
468 468 return tags
469 469
470 470 # svn tags are just a convention, project branches left in a
471 471 # 'tags' directory. There is no other relationship than
472 472 # ancestry, which is expensive to discover and makes them hard
473 473 # to update incrementally. Worse, past revisions may be
474 474 # referenced by tags far away in the future, requiring a deep
475 475 # history traversal on every calculation. Current code
476 476 # performs a single backward traversal, tracking moves within
477 477 # the tags directory (tag renaming) and recording a new tag
478 478 # everytime a project is copied from outside the tags
479 479 # directory. It also lists deleted tags, this behaviour may
480 480 # change in the future.
481 481 pendings = []
482 482 tagspath = self.tags
483 483 start = svn.ra.get_latest_revnum(self.ra)
484 484 stream = self._getlog([self.tags], start, self.startrev)
485 485 try:
486 486 for entry in stream:
487 487 origpaths, revnum, author, date, message = entry
488 488 if not origpaths:
489 489 origpaths = []
490 490 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
491 491 in origpaths.iteritems() if e.copyfrom_path]
492 492 # Apply moves/copies from more specific to general
493 493 copies.sort(reverse=True)
494 494
495 495 srctagspath = tagspath
496 496 if copies and copies[-1][2] == tagspath:
497 497 # Track tags directory moves
498 498 srctagspath = copies.pop()[0]
499 499
500 500 for source, sourcerev, dest in copies:
501 501 if not dest.startswith(tagspath + '/'):
502 502 continue
503 503 for tag in pendings:
504 504 if tag[0].startswith(dest):
505 505 tagpath = source + tag[0][len(dest):]
506 506 tag[:2] = [tagpath, sourcerev]
507 507 break
508 508 else:
509 509 pendings.append([source, sourcerev, dest])
510 510
511 511 # Filter out tags with children coming from different
512 512 # parts of the repository like:
513 513 # /tags/tag.1 (from /trunk:10)
514 514 # /tags/tag.1/foo (from /branches/foo:12)
515 515 # Here/tags/tag.1 discarded as well as its children.
516 516 # It happens with tools like cvs2svn. Such tags cannot
517 517 # be represented in mercurial.
518 518 addeds = dict((p, e.copyfrom_path) for p, e
519 519 in origpaths.iteritems()
520 520 if e.action == 'A' and e.copyfrom_path)
521 521 badroots = set()
522 522 for destroot in addeds:
523 523 for source, sourcerev, dest in pendings:
524 524 if (not dest.startswith(destroot + '/')
525 525 or source.startswith(addeds[destroot] + '/')):
526 526 continue
527 527 badroots.add(destroot)
528 528 break
529 529
530 530 for badroot in badroots:
531 531 pendings = [p for p in pendings if p[2] != badroot
532 532 and not p[2].startswith(badroot + '/')]
533 533
534 534 # Tell tag renamings from tag creations
535 535 renamings = []
536 536 for source, sourcerev, dest in pendings:
537 537 tagname = dest.split('/')[-1]
538 538 if source.startswith(srctagspath):
539 539 renamings.append([source, sourcerev, tagname])
540 540 continue
541 541 if tagname in tags:
542 542 # Keep the latest tag value
543 543 continue
544 544 # From revision may be fake, get one with changes
545 545 try:
546 546 tagid = self.latest(source, sourcerev)
547 547 if tagid and tagname not in tags:
548 548 tags[tagname] = tagid
549 549 except SvnPathNotFound:
550 550 # It happens when we are following directories
551 551 # we assumed were copied with their parents
552 552 # but were really created in the tag
553 553 # directory.
554 554 pass
555 555 pendings = renamings
556 556 tagspath = srctagspath
557 557 finally:
558 558 stream.close()
559 559 return tags
560 560
561 561 def converted(self, rev, destrev):
562 562 if not self.wc:
563 563 return
564 564 if self.convertfp is None:
565 565 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
566 566 'a')
567 567 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
568 568 self.convertfp.flush()
569 569
570 570 def revid(self, revnum, module=None):
571 571 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
572 572
573 573 def revnum(self, rev):
574 574 return int(rev.split('@')[-1])
575 575
576 576 def latest(self, path, stop=None):
577 577 """Find the latest revid affecting path, up to stop revision
578 578 number. If stop is None, default to repository latest
579 579 revision. It may return a revision in a different module,
580 580 since a branch may be moved without a change being
581 581 reported. Return None if computed module does not belong to
582 582 rootmodule subtree.
583 583 """
584 584 def findchanges(path, start, stop=None):
585 585 stream = self._getlog([path], start, stop or 1)
586 586 try:
587 587 for entry in stream:
588 588 paths, revnum, author, date, message = entry
589 589 if stop is None and paths:
590 590 # We do not know the latest changed revision,
591 591 # keep the first one with changed paths.
592 592 break
593 593 if revnum <= stop:
594 594 break
595 595
596 596 for p in paths:
597 597 if (not path.startswith(p) or
598 598 not paths[p].copyfrom_path):
599 599 continue
600 600 newpath = paths[p].copyfrom_path + path[len(p):]
601 601 self.ui.debug("branch renamed from %s to %s at %d\n" %
602 602 (path, newpath, revnum))
603 603 path = newpath
604 604 break
605 605 if not paths:
606 606 revnum = None
607 607 return revnum, path
608 608 finally:
609 609 stream.close()
610 610
611 611 if not path.startswith(self.rootmodule):
612 612 # Requests on foreign branches may be forbidden at server level
613 613 self.ui.debug('ignoring foreign branch %r\n' % path)
614 614 return None
615 615
616 616 if stop is None:
617 617 stop = svn.ra.get_latest_revnum(self.ra)
618 618 try:
619 619 prevmodule = self.reparent('')
620 620 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
621 621 self.reparent(prevmodule)
622 622 except SubversionException:
623 623 dirent = None
624 624 if not dirent:
625 625 raise SvnPathNotFound(_('%s not found up to revision %d')
626 626 % (path, stop))
627 627
628 628 # stat() gives us the previous revision on this line of
629 629 # development, but it might be in *another module*. Fetch the
630 630 # log and detect renames down to the latest revision.
631 631 revnum, realpath = findchanges(path, stop, dirent.created_rev)
632 632 if revnum is None:
633 633 # Tools like svnsync can create empty revision, when
634 634 # synchronizing only a subtree for instance. These empty
635 635 # revisions created_rev still have their original values
636 636 # despite all changes having disappeared and can be
637 637 # returned by ra.stat(), at least when stating the root
638 638 # module. In that case, do not trust created_rev and scan
639 639 # the whole history.
640 640 revnum, realpath = findchanges(path, stop)
641 641 if revnum is None:
642 642 self.ui.debug('ignoring empty branch %r\n' % realpath)
643 643 return None
644 644
645 645 if not realpath.startswith(self.rootmodule):
646 646 self.ui.debug('ignoring foreign branch %r\n' % realpath)
647 647 return None
648 648 return self.revid(revnum, realpath)
649 649
650 650 def reparent(self, module):
651 651 """Reparent the svn transport and return the previous parent."""
652 652 if self.prevmodule == module:
653 653 return module
654 654 svnurl = self.baseurl + quote(module)
655 655 prevmodule = self.prevmodule
656 656 if prevmodule is None:
657 657 prevmodule = ''
658 658 self.ui.debug("reparent to %s\n" % svnurl)
659 659 svn.ra.reparent(self.ra, svnurl)
660 660 self.prevmodule = module
661 661 return prevmodule
662 662
663 663 def expandpaths(self, rev, paths, parents):
664 664 changed, removed = set(), set()
665 665 copies = {}
666 666
667 667 new_module, revnum = revsplit(rev)[1:]
668 668 if new_module != self.module:
669 669 self.module = new_module
670 670 self.reparent(self.module)
671 671
672 672 for i, (path, ent) in enumerate(paths):
673 673 self.ui.progress(_('scanning paths'), i, item=path,
674 674 total=len(paths))
675 675 entrypath = self.getrelpath(path)
676 676
677 677 kind = self._checkpath(entrypath, revnum)
678 678 if kind == svn.core.svn_node_file:
679 679 changed.add(self.recode(entrypath))
680 680 if not ent.copyfrom_path or not parents:
681 681 continue
682 682 # Copy sources not in parent revisions cannot be
683 683 # represented, ignore their origin for now
684 684 pmodule, prevnum = revsplit(parents[0])[1:]
685 685 if ent.copyfrom_rev < prevnum:
686 686 continue
687 687 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
688 688 if not copyfrom_path:
689 689 continue
690 690 self.ui.debug("copied to %s from %s@%s\n" %
691 691 (entrypath, copyfrom_path, ent.copyfrom_rev))
692 692 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
693 693 elif kind == 0: # gone, but had better be a deleted *file*
694 694 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
695 695 pmodule, prevnum = revsplit(parents[0])[1:]
696 696 parentpath = pmodule + "/" + entrypath
697 697 fromkind = self._checkpath(entrypath, prevnum, pmodule)
698 698
699 699 if fromkind == svn.core.svn_node_file:
700 700 removed.add(self.recode(entrypath))
701 701 elif fromkind == svn.core.svn_node_dir:
702 702 oroot = parentpath.strip('/')
703 703 nroot = path.strip('/')
704 704 children = self._iterfiles(oroot, prevnum)
705 705 for childpath in children:
706 706 childpath = childpath.replace(oroot, nroot)
707 707 childpath = self.getrelpath("/" + childpath, pmodule)
708 708 if childpath:
709 709 removed.add(self.recode(childpath))
710 710 else:
711 711 self.ui.debug('unknown path in revision %d: %s\n' % \
712 712 (revnum, path))
713 713 elif kind == svn.core.svn_node_dir:
714 714 if ent.action == 'M':
715 715 # If the directory just had a prop change,
716 716 # then we shouldn't need to look for its children.
717 717 continue
718 718 if ent.action == 'R' and parents:
719 719 # If a directory is replacing a file, mark the previous
720 720 # file as deleted
721 721 pmodule, prevnum = revsplit(parents[0])[1:]
722 722 pkind = self._checkpath(entrypath, prevnum, pmodule)
723 723 if pkind == svn.core.svn_node_file:
724 724 removed.add(self.recode(entrypath))
725 725 elif pkind == svn.core.svn_node_dir:
726 726 # We do not know what files were kept or removed,
727 727 # mark them all as changed.
728 728 for childpath in self._iterfiles(pmodule, prevnum):
729 729 childpath = self.getrelpath("/" + childpath)
730 730 if childpath:
731 731 changed.add(self.recode(childpath))
732 732
733 733 for childpath in self._iterfiles(path, revnum):
734 734 childpath = self.getrelpath("/" + childpath)
735 735 if childpath:
736 736 changed.add(self.recode(childpath))
737 737
738 738 # Handle directory copies
739 739 if not ent.copyfrom_path or not parents:
740 740 continue
741 741 # Copy sources not in parent revisions cannot be
742 742 # represented, ignore their origin for now
743 743 pmodule, prevnum = revsplit(parents[0])[1:]
744 744 if ent.copyfrom_rev < prevnum:
745 745 continue
746 746 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
747 747 if not copyfrompath:
748 748 continue
749 749 self.ui.debug("mark %s came from %s:%d\n"
750 750 % (path, copyfrompath, ent.copyfrom_rev))
751 751 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
752 752 for childpath in children:
753 753 childpath = self.getrelpath("/" + childpath, pmodule)
754 754 if not childpath:
755 755 continue
756 756 copytopath = path + childpath[len(copyfrompath):]
757 757 copytopath = self.getrelpath(copytopath)
758 758 copies[self.recode(copytopath)] = self.recode(childpath)
759 759
760 760 self.ui.progress(_('scanning paths'), None)
761 761 changed.update(removed)
762 762 return (list(changed), removed, copies)
763 763
764 764 def _fetch_revisions(self, from_revnum, to_revnum):
765 765 if from_revnum < to_revnum:
766 766 from_revnum, to_revnum = to_revnum, from_revnum
767 767
768 768 self.child_cset = None
769 769
770 770 def parselogentry(orig_paths, revnum, author, date, message):
771 771 """Return the parsed commit object or None, and True if
772 772 the revision is a branch root.
773 773 """
774 774 self.ui.debug("parsing revision %d (%d changes)\n" %
775 775 (revnum, len(orig_paths)))
776 776
777 777 branched = False
778 778 rev = self.revid(revnum)
779 779 # branch log might return entries for a parent we already have
780 780
781 781 if rev in self.commits or revnum < to_revnum:
782 782 return None, branched
783 783
784 784 parents = []
785 785 # check whether this revision is the start of a branch or part
786 786 # of a branch renaming
787 787 orig_paths = sorted(orig_paths.iteritems())
788 788 root_paths = [(p, e) for p, e in orig_paths
789 789 if self.module.startswith(p)]
790 790 if root_paths:
791 791 path, ent = root_paths[-1]
792 792 if ent.copyfrom_path:
793 793 branched = True
794 794 newpath = ent.copyfrom_path + self.module[len(path):]
795 795 # ent.copyfrom_rev may not be the actual last revision
796 796 previd = self.latest(newpath, ent.copyfrom_rev)
797 797 if previd is not None:
798 798 prevmodule, prevnum = revsplit(previd)[1:]
799 799 if prevnum >= self.startrev:
800 800 parents = [previd]
801 801 self.ui.note(
802 802 _('found parent of branch %s at %d: %s\n') %
803 803 (self.module, prevnum, prevmodule))
804 804 else:
805 805 self.ui.debug("no copyfrom path, don't know what to do.\n")
806 806
807 807 paths = []
808 808 # filter out unrelated paths
809 809 for path, ent in orig_paths:
810 810 if self.getrelpath(path) is None:
811 811 continue
812 812 paths.append((path, ent))
813 813
814 814 # Example SVN datetime. Includes microseconds.
815 815 # ISO-8601 conformant
816 816 # '2007-01-04T17:35:00.902377Z'
817 817 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
818 818 if self.ui.configbool('convert', 'localtimezone'):
819 819 date = makedatetimestamp(date[0])
820 820
821 821 log = message and self.recode(message) or ''
822 822 author = author and self.recode(author) or ''
823 823 try:
824 824 branch = self.module.split("/")[-1]
825 825 if branch == self.trunkname:
826 826 branch = None
827 827 except IndexError:
828 828 branch = None
829 829
830 830 cset = commit(author=author,
831 831 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
832 832 desc=log,
833 833 parents=parents,
834 834 branch=branch,
835 835 rev=rev)
836 836
837 837 self.commits[rev] = cset
838 838 # The parents list is *shared* among self.paths and the
839 839 # commit object. Both will be updated below.
840 840 self.paths[rev] = (paths, cset.parents)
841 841 if self.child_cset and not self.child_cset.parents:
842 842 self.child_cset.parents[:] = [rev]
843 843 self.child_cset = cset
844 844 return cset, branched
845 845
846 846 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
847 847 (self.module, from_revnum, to_revnum))
848 848
849 849 try:
850 850 firstcset = None
851 851 lastonbranch = False
852 852 stream = self._getlog([self.module], from_revnum, to_revnum)
853 853 try:
854 854 for entry in stream:
855 855 paths, revnum, author, date, message = entry
856 856 if revnum < self.startrev:
857 857 lastonbranch = True
858 858 break
859 859 if not paths:
860 860 self.ui.debug('revision %d has no entries\n' % revnum)
861 861 # If we ever leave the loop on an empty
862 862 # revision, do not try to get a parent branch
863 863 lastonbranch = lastonbranch or revnum == 0
864 864 continue
865 865 cset, lastonbranch = parselogentry(paths, revnum, author,
866 866 date, message)
867 867 if cset:
868 868 firstcset = cset
869 869 if lastonbranch:
870 870 break
871 871 finally:
872 872 stream.close()
873 873
874 874 if not lastonbranch and firstcset and not firstcset.parents:
875 875 # The first revision of the sequence (the last fetched one)
876 876 # has invalid parents if not a branch root. Find the parent
877 877 # revision now, if any.
878 878 try:
879 879 firstrevnum = self.revnum(firstcset.rev)
880 880 if firstrevnum > 1:
881 881 latest = self.latest(self.module, firstrevnum - 1)
882 882 if latest:
883 883 firstcset.parents.append(latest)
884 884 except SvnPathNotFound:
885 885 pass
886 886 except SubversionException, (inst, num):
887 887 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
888 888 raise util.Abort(_('svn: branch has no revision %s')
889 889 % to_revnum)
890 890 raise
891 891
892 892 def getfile(self, file, rev):
893 893 # TODO: ra.get_file transmits the whole file instead of diffs.
894 894 if file in self.removed:
895 895 raise IOError
896 896 mode = ''
897 897 try:
898 898 new_module, revnum = revsplit(rev)[1:]
899 899 if self.module != new_module:
900 900 self.module = new_module
901 901 self.reparent(self.module)
902 902 io = StringIO()
903 903 info = svn.ra.get_file(self.ra, file, revnum, io)
904 904 data = io.getvalue()
905 905 # ra.get_file() seems to keep a reference on the input buffer
906 906 # preventing collection. Release it explicitly.
907 907 io.close()
908 908 if isinstance(info, list):
909 909 info = info[-1]
910 910 mode = ("svn:executable" in info) and 'x' or ''
911 911 mode = ("svn:special" in info) and 'l' or mode
912 912 except SubversionException, e:
913 913 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
914 914 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
915 915 if e.apr_err in notfound: # File not found
916 916 raise IOError
917 917 raise
918 918 if mode == 'l':
919 919 link_prefix = "link "
920 920 if data.startswith(link_prefix):
921 921 data = data[len(link_prefix):]
922 922 return data, mode
923 923
924 924 def _iterfiles(self, path, revnum):
925 925 """Enumerate all files in path at revnum, recursively."""
926 926 path = path.strip('/')
927 927 pool = Pool()
928 928 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
929 929 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
930 930 if path:
931 931 path += '/'
932 932 return ((path + p) for p, e in entries.iteritems()
933 933 if e.kind == svn.core.svn_node_file)
934 934
935 935 def getrelpath(self, path, module=None):
936 936 if module is None:
937 937 module = self.module
938 938 # Given the repository url of this wc, say
939 939 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
940 940 # extract the "entry" portion (a relative path) from what
941 941 # svn log --xml says, i.e.
942 942 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
943 943 # that is to say "tests/PloneTestCase.py"
944 944 if path.startswith(module):
945 945 relative = path.rstrip('/')[len(module):]
946 946 if relative.startswith('/'):
947 947 return relative[1:]
948 948 elif relative == '':
949 949 return relative
950 950
951 951 # The path is outside our tracked tree...
952 952 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
953 953 return None
954 954
955 955 def _checkpath(self, path, revnum, module=None):
956 956 if module is not None:
957 957 prevmodule = self.reparent('')
958 958 path = module + '/' + path
959 959 try:
960 960 # ra.check_path does not like leading slashes very much, it leads
961 961 # to PROPFIND subversion errors
962 962 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
963 963 finally:
964 964 if module is not None:
965 965 self.reparent(prevmodule)
966 966
967 967 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
968 968 strict_node_history=False):
969 969 # Normalize path names, svn >= 1.5 only wants paths relative to
970 970 # supplied URL
971 971 relpaths = []
972 972 for p in paths:
973 973 if not p.startswith('/'):
974 974 p = self.module + '/' + p
975 975 relpaths.append(p.strip('/'))
976 976 args = [self.baseurl, relpaths, start, end, limit,
977 977 discover_changed_paths, strict_node_history]
978 978 arg = encodeargs(args)
979 979 hgexe = util.hgexecutable()
980 980 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
981 981 stdin, stdout = util.popen2(util.quotecommand(cmd))
982 982 stdin.write(arg)
983 983 try:
984 984 stdin.close()
985 985 except IOError:
986 986 raise util.Abort(_('Mercurial failed to run itself, check'
987 987 ' hg executable is in PATH'))
988 988 return logstream(stdout)
989 989
990 990 pre_revprop_change = '''#!/bin/sh
991 991
992 992 REPOS="$1"
993 993 REV="$2"
994 994 USER="$3"
995 995 PROPNAME="$4"
996 996 ACTION="$5"
997 997
998 998 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
999 999 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1000 1000 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1001 1001
1002 1002 echo "Changing prohibited revision property" >&2
1003 1003 exit 1
1004 1004 '''
1005 1005
1006 1006 class svn_sink(converter_sink, commandline):
1007 1007 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1008 1008 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1009 1009
1010 1010 def prerun(self):
1011 1011 if self.wc:
1012 1012 os.chdir(self.wc)
1013 1013
1014 1014 def postrun(self):
1015 1015 if self.wc:
1016 1016 os.chdir(self.cwd)
1017 1017
1018 1018 def join(self, name):
1019 1019 return os.path.join(self.wc, '.svn', name)
1020 1020
1021 1021 def revmapfile(self):
1022 1022 return self.join('hg-shamap')
1023 1023
1024 1024 def authorfile(self):
1025 1025 return self.join('hg-authormap')
1026 1026
1027 1027 def __init__(self, ui, path):
1028 1028
1029 1029 converter_sink.__init__(self, ui, path)
1030 1030 commandline.__init__(self, ui, 'svn')
1031 1031 self.delete = []
1032 1032 self.setexec = []
1033 1033 self.delexec = []
1034 1034 self.copies = []
1035 1035 self.wc = None
1036 1036 self.cwd = os.getcwd()
1037 1037
1038 1038 created = False
1039 1039 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1040 1040 self.wc = os.path.realpath(path)
1041 1041 self.run0('update')
1042 1042 else:
1043 1043 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1044 1044 path = os.path.realpath(path)
1045 1045 if os.path.isdir(os.path.dirname(path)):
1046 1046 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1047 1047 ui.status(_('initializing svn repository %r\n') %
1048 1048 os.path.basename(path))
1049 1049 commandline(ui, 'svnadmin').run0('create', path)
1050 1050 created = path
1051 1051 path = util.normpath(path)
1052 1052 if not path.startswith('/'):
1053 1053 path = '/' + path
1054 1054 path = 'file://' + path
1055 1055
1056 1056 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1057 1057 ui.status(_('initializing svn working copy %r\n')
1058 1058 % os.path.basename(wcpath))
1059 1059 self.run0('checkout', path, wcpath)
1060 1060
1061 1061 self.wc = wcpath
1062 1062 self.opener = scmutil.opener(self.wc)
1063 1063 self.wopener = scmutil.opener(self.wc)
1064 1064 self.childmap = mapfile(ui, self.join('hg-childmap'))
1065 1065 self.is_exec = util.checkexec(self.wc) and util.isexec or None
1066 1066
1067 1067 if created:
1068 1068 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1069 1069 fp = open(hook, 'w')
1070 1070 fp.write(pre_revprop_change)
1071 1071 fp.close()
1072 1072 util.setflags(hook, False, True)
1073 1073
1074 1074 output = self.run0('info')
1075 1075 self.uuid = self.uuid_re.search(output).group(1).strip()
1076 1076
1077 1077 def wjoin(self, *names):
1078 1078 return os.path.join(self.wc, *names)
1079 1079
1080 1080 @propertycache
1081 1081 def manifest(self):
1082 1082 # As of svn 1.7, the "add" command fails when receiving
1083 1083 # already tracked entries, so we have to track and filter them
1084 1084 # ourselves.
1085 1085 m = set()
1086 1086 output = self.run0('ls', recursive=True, xml=True)
1087 1087 doc = xml.dom.minidom.parseString(output)
1088 1088 for e in doc.getElementsByTagName('entry'):
1089 1089 for n in e.childNodes:
1090 1090 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1091 1091 continue
1092 1092 name = ''.join(c.data for c in n.childNodes
1093 1093 if c.nodeType == c.TEXT_NODE)
1094 1094 # Entries are compared with names coming from
1095 1095 # mercurial, so bytes with undefined encoding. Our
1096 1096 # best bet is to assume they are in local
1097 1097 # encoding. They will be passed to command line calls
1098 1098 # later anyway, so they better be.
1099 1099 m.add(encoding.tolocal(name.encode('utf-8')))
1100 1100 break
1101 1101 return m
1102 1102
1103 1103 def putfile(self, filename, flags, data):
1104 1104 if 'l' in flags:
1105 1105 self.wopener.symlink(data, filename)
1106 1106 else:
1107 1107 try:
1108 1108 if os.path.islink(self.wjoin(filename)):
1109 1109 os.unlink(filename)
1110 1110 except OSError:
1111 1111 pass
1112 1112 self.wopener.write(filename, data)
1113 1113
1114 1114 if self.is_exec:
1115 1115 if self.is_exec(self.wjoin(filename)):
1116 1116 if 'x' not in flags:
1117 1117 self.delexec.append(filename)
1118 1118 else:
1119 1119 if 'x' in flags:
1120 1120 self.setexec.append(filename)
1121 1121 util.setflags(self.wjoin(filename), False, 'x' in flags)
1122 1122
1123 1123 def _copyfile(self, source, dest):
1124 1124 # SVN's copy command pukes if the destination file exists, but
1125 1125 # our copyfile method expects to record a copy that has
1126 1126 # already occurred. Cross the semantic gap.
1127 1127 wdest = self.wjoin(dest)
1128 1128 exists = os.path.lexists(wdest)
1129 1129 if exists:
1130 1130 fd, tempname = tempfile.mkstemp(
1131 1131 prefix='hg-copy-', dir=os.path.dirname(wdest))
1132 1132 os.close(fd)
1133 1133 os.unlink(tempname)
1134 1134 os.rename(wdest, tempname)
1135 1135 try:
1136 1136 self.run0('copy', source, dest)
1137 1137 finally:
1138 1138 self.manifest.add(dest)
1139 1139 if exists:
1140 1140 try:
1141 1141 os.unlink(wdest)
1142 1142 except OSError:
1143 1143 pass
1144 1144 os.rename(tempname, wdest)
1145 1145
1146 1146 def dirs_of(self, files):
1147 1147 dirs = set()
1148 1148 for f in files:
1149 1149 if os.path.isdir(self.wjoin(f)):
1150 1150 dirs.add(f)
1151 1151 for i in strutil.rfindall(f, '/'):
1152 1152 dirs.add(f[:i])
1153 1153 return dirs
1154 1154
1155 1155 def add_dirs(self, files):
1156 1156 add_dirs = [d for d in sorted(self.dirs_of(files))
1157 1157 if d not in self.manifest]
1158 1158 if add_dirs:
1159 1159 self.manifest.update(add_dirs)
1160 1160 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1161 1161 return add_dirs
1162 1162
1163 1163 def add_files(self, files):
1164 1164 files = [f for f in files if f not in self.manifest]
1165 1165 if files:
1166 1166 self.manifest.update(files)
1167 1167 self.xargs(files, 'add', quiet=True)
1168 1168 return files
1169 1169
1170 1170 def tidy_dirs(self, names):
1171 1171 deleted = []
1172 1172 for d in sorted(self.dirs_of(names), reverse=True):
1173 1173 wd = self.wjoin(d)
1174 1174 if os.listdir(wd) == '.svn':
1175 1175 self.run0('delete', d)
1176 1176 self.manifest.remove(d)
1177 1177 deleted.append(d)
1178 1178 return deleted
1179 1179
1180 1180 def addchild(self, parent, child):
1181 1181 self.childmap[parent] = child
1182 1182
1183 1183 def revid(self, rev):
1184 1184 return u"svn:%s@%s" % (self.uuid, rev)
1185 1185
1186 def putcommit(self, files, copies, parents, commit, source, revmap):
1186 def putcommit(self, files, copies, parents, commit, source,
1187 revmap, tagmap):
1187 1188 for parent in parents:
1188 1189 try:
1189 1190 return self.revid(self.childmap[parent])
1190 1191 except KeyError:
1191 1192 pass
1192 1193
1193 1194 # Apply changes to working copy
1194 1195 for f, v in files:
1195 1196 try:
1196 1197 data, mode = source.getfile(f, v)
1197 1198 except IOError:
1198 1199 self.delete.append(f)
1199 1200 else:
1200 1201 self.putfile(f, mode, data)
1201 1202 if f in copies:
1202 1203 self.copies.append([copies[f], f])
1203 1204 files = [f[0] for f in files]
1204 1205
1205 1206 entries = set(self.delete)
1206 1207 files = frozenset(files)
1207 1208 entries.update(self.add_dirs(files.difference(entries)))
1208 1209 if self.copies:
1209 1210 for s, d in self.copies:
1210 1211 self._copyfile(s, d)
1211 1212 self.copies = []
1212 1213 if self.delete:
1213 1214 self.xargs(self.delete, 'delete')
1214 1215 for f in self.delete:
1215 1216 self.manifest.remove(f)
1216 1217 self.delete = []
1217 1218 entries.update(self.add_files(files.difference(entries)))
1218 1219 entries.update(self.tidy_dirs(entries))
1219 1220 if self.delexec:
1220 1221 self.xargs(self.delexec, 'propdel', 'svn:executable')
1221 1222 self.delexec = []
1222 1223 if self.setexec:
1223 1224 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1224 1225 self.setexec = []
1225 1226
1226 1227 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1227 1228 fp = os.fdopen(fd, 'w')
1228 1229 fp.write(commit.desc)
1229 1230 fp.close()
1230 1231 try:
1231 1232 output = self.run0('commit',
1232 1233 username=util.shortuser(commit.author),
1233 1234 file=messagefile,
1234 1235 encoding='utf-8')
1235 1236 try:
1236 1237 rev = self.commit_re.search(output).group(1)
1237 1238 except AttributeError:
1238 1239 if not files:
1239 1240 return parents[0]
1240 1241 self.ui.warn(_('unexpected svn output:\n'))
1241 1242 self.ui.warn(output)
1242 1243 raise util.Abort(_('unable to cope with svn output'))
1243 1244 if commit.rev:
1244 1245 self.run('propset', 'hg:convert-rev', commit.rev,
1245 1246 revprop=True, revision=rev)
1246 1247 if commit.branch and commit.branch != 'default':
1247 1248 self.run('propset', 'hg:convert-branch', commit.branch,
1248 1249 revprop=True, revision=rev)
1249 1250 for parent in parents:
1250 1251 self.addchild(parent, rev)
1251 1252 return self.revid(rev)
1252 1253 finally:
1253 1254 os.unlink(messagefile)
1254 1255
1255 1256 def puttags(self, tags):
1256 1257 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1257 1258 return None, None
1258 1259
1259 1260 def hascommit(self, rev):
1260 1261 # This is not correct as one can convert to an existing subversion
1261 1262 # repository and childmap would not list all revisions. Too bad.
1262 1263 if rev in self.childmap:
1263 1264 return True
1264 1265 raise util.Abort(_('splice map revision %s not found in subversion '
1265 1266 'child map (revision lookups are not implemented)')
1266 1267 % rev)
General Comments 0
You need to be logged in to leave comments. Login now