##// END OF EJS Templates
convert: add mapname parameter to checkrevformat...
Sean Farley -
r20373:e8203629 default
parent child Browse files
Show More
@@ -1,447 +1,447 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 def checkhexformat(self, revstr):
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 raise util.Abort(_('splicemap entry %s is not a valid revision'
72 ' identifier') % revstr)
71 raise util.Abort(_('%s entry %s is not a valid revision'
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 def checkrevformat(self, revstr):
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 211 def putcommit(self, files, copies, parents, commit, source, revmap):
212 212 """Create a revision with all changed files listed in 'files'
213 213 and having listed parents. 'commit' is a commit object
214 214 containing at a minimum the author, date, and message for this
215 215 changeset. 'files' is a list of (path, version) tuples,
216 216 'copies' is a dictionary mapping destinations to sources,
217 217 'source' is the source repository, and 'revmap' is a mapfile
218 218 of source revisions to converted revisions. Only getfile() and
219 219 lookuprev() should be called on 'source'.
220 220
221 221 Note that the sink repository is not told to update itself to
222 222 a particular revision (or even what that revision would be)
223 223 before it receives the file data.
224 224 """
225 225 raise NotImplementedError
226 226
227 227 def puttags(self, tags):
228 228 """Put tags into sink.
229 229
230 230 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
231 231 Return a pair (tag_revision, tag_parent_revision), or (None, None)
232 232 if nothing was changed.
233 233 """
234 234 raise NotImplementedError
235 235
236 236 def setbranch(self, branch, pbranches):
237 237 """Set the current branch name. Called before the first putcommit
238 238 on the branch.
239 239 branch: branch name for subsequent commits
240 240 pbranches: (converted parent revision, parent branch) tuples"""
241 241 pass
242 242
243 243 def setfilemapmode(self, active):
244 244 """Tell the destination that we're using a filemap
245 245
246 246 Some converter_sources (svn in particular) can claim that a file
247 247 was changed in a revision, even if there was no change. This method
248 248 tells the destination that we're using a filemap and that it should
249 249 filter empty revisions.
250 250 """
251 251 pass
252 252
253 253 def before(self):
254 254 pass
255 255
256 256 def after(self):
257 257 pass
258 258
259 259 def putbookmarks(self, bookmarks):
260 260 """Put bookmarks into sink.
261 261
262 262 bookmarks: {bookmarkname: sink_rev_id, ...}
263 263 where bookmarkname is an UTF-8 string.
264 264 """
265 265 pass
266 266
267 267 def hascommit(self, rev):
268 268 """Return True if the sink contains rev"""
269 269 raise NotImplementedError
270 270
271 271 class commandline(object):
272 272 def __init__(self, ui, command):
273 273 self.ui = ui
274 274 self.command = command
275 275
276 276 def prerun(self):
277 277 pass
278 278
279 279 def postrun(self):
280 280 pass
281 281
282 282 def _cmdline(self, cmd, *args, **kwargs):
283 283 cmdline = [self.command, cmd] + list(args)
284 284 for k, v in kwargs.iteritems():
285 285 if len(k) == 1:
286 286 cmdline.append('-' + k)
287 287 else:
288 288 cmdline.append('--' + k.replace('_', '-'))
289 289 try:
290 290 if len(k) == 1:
291 291 cmdline.append('' + v)
292 292 else:
293 293 cmdline[-1] += '=' + v
294 294 except TypeError:
295 295 pass
296 296 cmdline = [util.shellquote(arg) for arg in cmdline]
297 297 if not self.ui.debugflag:
298 298 cmdline += ['2>', os.devnull]
299 299 cmdline = ' '.join(cmdline)
300 300 return cmdline
301 301
302 302 def _run(self, cmd, *args, **kwargs):
303 303 def popen(cmdline):
304 304 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
305 305 close_fds=util.closefds,
306 306 stdout=subprocess.PIPE)
307 307 return p
308 308 return self._dorun(popen, cmd, *args, **kwargs)
309 309
310 310 def _run2(self, cmd, *args, **kwargs):
311 311 return self._dorun(util.popen2, cmd, *args, **kwargs)
312 312
313 313 def _dorun(self, openfunc, cmd, *args, **kwargs):
314 314 cmdline = self._cmdline(cmd, *args, **kwargs)
315 315 self.ui.debug('running: %s\n' % (cmdline,))
316 316 self.prerun()
317 317 try:
318 318 return openfunc(cmdline)
319 319 finally:
320 320 self.postrun()
321 321
322 322 def run(self, cmd, *args, **kwargs):
323 323 p = self._run(cmd, *args, **kwargs)
324 324 output = p.communicate()[0]
325 325 self.ui.debug(output)
326 326 return output, p.returncode
327 327
328 328 def runlines(self, cmd, *args, **kwargs):
329 329 p = self._run(cmd, *args, **kwargs)
330 330 output = p.stdout.readlines()
331 331 p.wait()
332 332 self.ui.debug(''.join(output))
333 333 return output, p.returncode
334 334
335 335 def checkexit(self, status, output=''):
336 336 if status:
337 337 if output:
338 338 self.ui.warn(_('%s error:\n') % self.command)
339 339 self.ui.warn(output)
340 340 msg = util.explainexit(status)[0]
341 341 raise util.Abort('%s %s' % (self.command, msg))
342 342
343 343 def run0(self, cmd, *args, **kwargs):
344 344 output, status = self.run(cmd, *args, **kwargs)
345 345 self.checkexit(status, output)
346 346 return output
347 347
348 348 def runlines0(self, cmd, *args, **kwargs):
349 349 output, status = self.runlines(cmd, *args, **kwargs)
350 350 self.checkexit(status, ''.join(output))
351 351 return output
352 352
353 353 @propertycache
354 354 def argmax(self):
355 355 # POSIX requires at least 4096 bytes for ARG_MAX
356 356 argmax = 4096
357 357 try:
358 358 argmax = os.sysconf("SC_ARG_MAX")
359 359 except (AttributeError, ValueError):
360 360 pass
361 361
362 362 # Windows shells impose their own limits on command line length,
363 363 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
364 364 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
365 365 # details about cmd.exe limitations.
366 366
367 367 # Since ARG_MAX is for command line _and_ environment, lower our limit
368 368 # (and make happy Windows shells while doing this).
369 369 return argmax // 2 - 1
370 370
371 371 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
372 372 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
373 373 limit = self.argmax - cmdlen
374 374 bytes = 0
375 375 fl = []
376 376 for fn in arglist:
377 377 b = len(fn) + 3
378 378 if bytes + b < limit or len(fl) == 0:
379 379 fl.append(fn)
380 380 bytes += b
381 381 else:
382 382 yield fl
383 383 fl = [fn]
384 384 bytes = b
385 385 if fl:
386 386 yield fl
387 387
388 388 def xargs(self, arglist, cmd, *args, **kwargs):
389 389 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
390 390 self.run0(cmd, *(list(args) + l), **kwargs)
391 391
392 392 class mapfile(dict):
393 393 def __init__(self, ui, path):
394 394 super(mapfile, self).__init__()
395 395 self.ui = ui
396 396 self.path = path
397 397 self.fp = None
398 398 self.order = []
399 399 self._read()
400 400
401 401 def _read(self):
402 402 if not self.path:
403 403 return
404 404 try:
405 405 fp = open(self.path, 'r')
406 406 except IOError, err:
407 407 if err.errno != errno.ENOENT:
408 408 raise
409 409 return
410 410 for i, line in enumerate(fp):
411 411 line = line.splitlines()[0].rstrip()
412 412 if not line:
413 413 # Ignore blank lines
414 414 continue
415 415 try:
416 416 key, value = line.rsplit(' ', 1)
417 417 except ValueError:
418 418 raise util.Abort(
419 419 _('syntax error in %s(%d): key/value pair expected')
420 420 % (self.path, i + 1))
421 421 if key not in self:
422 422 self.order.append(key)
423 423 super(mapfile, self).__setitem__(key, value)
424 424 fp.close()
425 425
426 426 def __setitem__(self, key, value):
427 427 if self.fp is None:
428 428 try:
429 429 self.fp = open(self.path, 'a')
430 430 except IOError, err:
431 431 raise util.Abort(_('could not open map file %r: %s') %
432 432 (self.path, err.strerror))
433 433 self.fp.write('%s %s\n' % (key, value))
434 434 self.fp.flush()
435 435 super(mapfile, self).__setitem__(key, value)
436 436
437 437 def close(self):
438 438 if self.fp:
439 439 self.fp.close()
440 440 self.fp = None
441 441
442 442 def makedatetimestamp(t):
443 443 """Like util.makedate() but for time t instead of current time"""
444 444 delta = (datetime.datetime.utcfromtimestamp(t) -
445 445 datetime.datetime.fromtimestamp(t))
446 446 tz = delta.days * 86400 + delta.seconds
447 447 return t, tz
@@ -1,303 +1,303 b''
1 1 # git.py - git support 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 os
9 9 import subprocess
10 10 from mercurial import util, config
11 11 from mercurial.node import hex, nullid
12 12 from mercurial.i18n import _
13 13
14 14 from common import NoRepo, commit, converter_source, checktool
15 15
16 16 class submodule(object):
17 17 def __init__(self, path, node, url):
18 18 self.path = path
19 19 self.node = node
20 20 self.url = url
21 21
22 22 def hgsub(self):
23 23 return "%s = [git]%s" % (self.path, self.url)
24 24
25 25 def hgsubstate(self):
26 26 return "%s %s" % (self.node, self.path)
27 27
28 28 class convert_git(converter_source):
29 29 # Windows does not support GIT_DIR= construct while other systems
30 30 # cannot remove environment variable. Just assume none have
31 31 # both issues.
32 32 if util.safehasattr(os, 'unsetenv'):
33 33 def gitopen(self, s, err=None):
34 34 prevgitdir = os.environ.get('GIT_DIR')
35 35 os.environ['GIT_DIR'] = self.path
36 36 try:
37 37 if err == subprocess.PIPE:
38 38 (stdin, stdout, stderr) = util.popen3(s)
39 39 return stdout
40 40 elif err == subprocess.STDOUT:
41 41 return self.popen_with_stderr(s)
42 42 else:
43 43 return util.popen(s, 'rb')
44 44 finally:
45 45 if prevgitdir is None:
46 46 del os.environ['GIT_DIR']
47 47 else:
48 48 os.environ['GIT_DIR'] = prevgitdir
49 49 else:
50 50 def gitopen(self, s, err=None):
51 51 if err == subprocess.PIPE:
52 52 (sin, so, se) = util.popen3('GIT_DIR=%s %s' % (self.path, s))
53 53 return so
54 54 elif err == subprocess.STDOUT:
55 55 return self.popen_with_stderr(s)
56 56 else:
57 57 return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')
58 58
59 59 def popen_with_stderr(self, s):
60 60 p = subprocess.Popen(s, shell=True, bufsize=-1,
61 61 close_fds=util.closefds,
62 62 stdin=subprocess.PIPE,
63 63 stdout=subprocess.PIPE,
64 64 stderr=subprocess.STDOUT,
65 65 universal_newlines=False,
66 66 env=None)
67 67 return p.stdout
68 68
69 69 def gitread(self, s):
70 70 fh = self.gitopen(s)
71 71 data = fh.read()
72 72 return data, fh.close()
73 73
74 74 def __init__(self, ui, path, rev=None):
75 75 super(convert_git, self).__init__(ui, path, rev=rev)
76 76
77 77 if os.path.isdir(path + "/.git"):
78 78 path += "/.git"
79 79 if not os.path.exists(path + "/objects"):
80 80 raise NoRepo(_("%s does not look like a Git repository") % path)
81 81
82 82 checktool('git', 'git')
83 83
84 84 self.path = path
85 85 self.submodules = []
86 86
87 87 def getheads(self):
88 88 if not self.rev:
89 89 heads, ret = self.gitread('git rev-parse --branches --remotes')
90 90 heads = heads.splitlines()
91 91 else:
92 92 heads, ret = self.gitread("git rev-parse --verify %s" % self.rev)
93 93 heads = [heads[:-1]]
94 94 if ret:
95 95 raise util.Abort(_('cannot retrieve git heads'))
96 96 return heads
97 97
98 98 def catfile(self, rev, type):
99 99 if rev == hex(nullid):
100 100 raise IOError
101 101 data, ret = self.gitread("git cat-file %s %s" % (type, rev))
102 102 if ret:
103 103 raise util.Abort(_('cannot read %r object at %s') % (type, rev))
104 104 return data
105 105
106 106 def getfile(self, name, rev):
107 107 if name == '.hgsub':
108 108 data = '\n'.join([m.hgsub() for m in self.submoditer()])
109 109 mode = ''
110 110 elif name == '.hgsubstate':
111 111 data = '\n'.join([m.hgsubstate() for m in self.submoditer()])
112 112 mode = ''
113 113 else:
114 114 data = self.catfile(rev, "blob")
115 115 mode = self.modecache[(name, rev)]
116 116 return data, mode
117 117
118 118 def submoditer(self):
119 119 null = hex(nullid)
120 120 for m in sorted(self.submodules, key=lambda p: p.path):
121 121 if m.node != null:
122 122 yield m
123 123
124 124 def parsegitmodules(self, content):
125 125 """Parse the formatted .gitmodules file, example file format:
126 126 [submodule "sub"]\n
127 127 \tpath = sub\n
128 128 \turl = git://giturl\n
129 129 """
130 130 self.submodules = []
131 131 c = config.config()
132 132 # Each item in .gitmodules starts with \t that cant be parsed
133 133 c.parse('.gitmodules', content.replace('\t',''))
134 134 for sec in c.sections():
135 135 s = c[sec]
136 136 if 'url' in s and 'path' in s:
137 137 self.submodules.append(submodule(s['path'], '', s['url']))
138 138
139 139 def retrievegitmodules(self, version):
140 140 modules, ret = self.gitread("git show %s:%s" % (version, '.gitmodules'))
141 141 if ret:
142 142 raise util.Abort(_('cannot read submodules config file in %s') %
143 143 version)
144 144 self.parsegitmodules(modules)
145 145 for m in self.submodules:
146 146 node, ret = self.gitread("git rev-parse %s:%s" % (version, m.path))
147 147 if ret:
148 148 continue
149 149 m.node = node.strip()
150 150
151 151 def getchanges(self, version):
152 152 self.modecache = {}
153 153 fh = self.gitopen("git diff-tree -z --root -m -r %s" % version)
154 154 changes = []
155 155 seen = set()
156 156 entry = None
157 157 subexists = False
158 158 for l in fh.read().split('\x00'):
159 159 if not entry:
160 160 if not l.startswith(':'):
161 161 continue
162 162 entry = l
163 163 continue
164 164 f = l
165 165 if f not in seen:
166 166 seen.add(f)
167 167 entry = entry.split()
168 168 h = entry[3]
169 169 p = (entry[1] == "100755")
170 170 s = (entry[1] == "120000")
171 171
172 172 if f == '.gitmodules':
173 173 subexists = True
174 174 changes.append(('.hgsub', ''))
175 175 elif entry[1] == '160000' or entry[0] == ':160000':
176 176 subexists = True
177 177 else:
178 178 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
179 179 changes.append((f, h))
180 180 entry = None
181 181 if fh.close():
182 182 raise util.Abort(_('cannot read changes in %s') % version)
183 183
184 184 if subexists:
185 185 self.retrievegitmodules(version)
186 186 changes.append(('.hgsubstate', ''))
187 187 return (changes, {})
188 188
189 189 def getcommit(self, version):
190 190 c = self.catfile(version, "commit") # read the commit hash
191 191 end = c.find("\n\n")
192 192 message = c[end + 2:]
193 193 message = self.recode(message)
194 194 l = c[:end].splitlines()
195 195 parents = []
196 196 author = committer = None
197 197 for e in l[1:]:
198 198 n, v = e.split(" ", 1)
199 199 if n == "author":
200 200 p = v.split()
201 201 tm, tz = p[-2:]
202 202 author = " ".join(p[:-2])
203 203 if author[0] == "<": author = author[1:-1]
204 204 author = self.recode(author)
205 205 if n == "committer":
206 206 p = v.split()
207 207 tm, tz = p[-2:]
208 208 committer = " ".join(p[:-2])
209 209 if committer[0] == "<": committer = committer[1:-1]
210 210 committer = self.recode(committer)
211 211 if n == "parent":
212 212 parents.append(v)
213 213
214 214 if committer and committer != author:
215 215 message += "\ncommitter: %s\n" % committer
216 216 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
217 217 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
218 218 date = tm + " " + str(tz)
219 219
220 220 c = commit(parents=parents, date=date, author=author, desc=message,
221 221 rev=version)
222 222 return c
223 223
224 224 def gettags(self):
225 225 tags = {}
226 226 alltags = {}
227 227 fh = self.gitopen('git ls-remote --tags "%s"' % self.path,
228 228 err=subprocess.STDOUT)
229 229 prefix = 'refs/tags/'
230 230
231 231 # Build complete list of tags, both annotated and bare ones
232 232 for line in fh:
233 233 line = line.strip()
234 234 if line.startswith("error:") or line.startswith("fatal:"):
235 235 raise util.Abort(_('cannot read tags from %s') % self.path)
236 236 node, tag = line.split(None, 1)
237 237 if not tag.startswith(prefix):
238 238 continue
239 239 alltags[tag[len(prefix):]] = node
240 240 if fh.close():
241 241 raise util.Abort(_('cannot read tags from %s') % self.path)
242 242
243 243 # Filter out tag objects for annotated tag refs
244 244 for tag in alltags:
245 245 if tag.endswith('^{}'):
246 246 tags[tag[:-3]] = alltags[tag]
247 247 else:
248 248 if tag + '^{}' in alltags:
249 249 continue
250 250 else:
251 251 tags[tag] = alltags[tag]
252 252
253 253 return tags
254 254
255 255 def getchangedfiles(self, version, i):
256 256 changes = []
257 257 if i is None:
258 258 fh = self.gitopen("git diff-tree --root -m -r %s" % version)
259 259 for l in fh:
260 260 if "\t" not in l:
261 261 continue
262 262 m, f = l[:-1].split("\t")
263 263 changes.append(f)
264 264 else:
265 265 fh = self.gitopen('git diff-tree --name-only --root -r %s '
266 266 '"%s^%s" --' % (version, version, i + 1))
267 267 changes = [f.rstrip('\n') for f in fh]
268 268 if fh.close():
269 269 raise util.Abort(_('cannot read changes in %s') % version)
270 270
271 271 return changes
272 272
273 273 def getbookmarks(self):
274 274 bookmarks = {}
275 275
276 276 # Interesting references in git are prefixed
277 277 prefix = 'refs/heads/'
278 278 prefixlen = len(prefix)
279 279
280 280 # factor two commands
281 281 gitcmd = { 'remote/': 'git ls-remote --heads origin',
282 282 '': 'git show-ref'}
283 283
284 284 # Origin heads
285 285 for reftype in gitcmd:
286 286 try:
287 287 fh = self.gitopen(gitcmd[reftype], err=subprocess.PIPE)
288 288 for line in fh:
289 289 line = line.strip()
290 290 rev, name = line.split(None, 1)
291 291 if not name.startswith(prefix):
292 292 continue
293 293 name = '%s%s' % (reftype, name[prefixlen:])
294 294 bookmarks[name] = rev
295 295 except Exception:
296 296 pass
297 297
298 298 return bookmarks
299 299
300 def checkrevformat(self, revstr):
300 def checkrevformat(self, revstr, mapname='splicemap'):
301 301 """ git revision string is a 40 byte hex """
302 self.checkhexformat(revstr)
302 self.checkhexformat(revstr, mapname)
303 303
@@ -1,428 +1,428 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 123 def _rewritetags(self, source, revmap, 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 132 fp.write('%s %s\n' % (revid, s[1]))
133 133 return fp.getvalue()
134 134
135 135 def putcommit(self, files, copies, parents, commit, source, revmap):
136 136
137 137 files = dict(files)
138 138 def getfilectx(repo, memctx, f):
139 139 v = files[f]
140 140 data, mode = source.getfile(f, v)
141 141 if f == '.hgtags':
142 142 data = self._rewritetags(source, revmap, data)
143 143 return context.memfilectx(f, data, 'l' in mode, 'x' in mode,
144 144 copies.get(f))
145 145
146 146 pl = []
147 147 for p in parents:
148 148 if p not in pl:
149 149 pl.append(p)
150 150 parents = pl
151 151 nparents = len(parents)
152 152 if self.filemapmode and nparents == 1:
153 153 m1node = self.repo.changelog.read(bin(parents[0]))[0]
154 154 parent = parents[0]
155 155
156 156 if len(parents) < 2:
157 157 parents.append(nullid)
158 158 if len(parents) < 2:
159 159 parents.append(nullid)
160 160 p2 = parents.pop(0)
161 161
162 162 text = commit.desc
163 163
164 164 sha1s = re.findall(sha1re, text)
165 165 for sha1 in sha1s:
166 166 oldrev = source.lookuprev(sha1)
167 167 newrev = revmap.get(oldrev)
168 168 if newrev is not None:
169 169 text = text.replace(sha1, newrev[:len(sha1)])
170 170
171 171 extra = commit.extra.copy()
172 172 if self.branchnames and commit.branch:
173 173 extra['branch'] = commit.branch
174 174 if commit.rev:
175 175 extra['convert_revision'] = commit.rev
176 176
177 177 while parents:
178 178 p1 = p2
179 179 p2 = parents.pop(0)
180 180 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(),
181 181 getfilectx, commit.author, commit.date, extra)
182 182 self.repo.commitctx(ctx)
183 183 text = "(octopus merge fixup)\n"
184 184 p2 = hex(self.repo.changelog.tip())
185 185
186 186 if self.filemapmode and nparents == 1:
187 187 man = self.repo.manifest
188 188 mnode = self.repo.changelog.read(bin(p2))[0]
189 189 closed = 'close' in commit.extra
190 190 if not closed and not man.cmp(m1node, man.revision(mnode)):
191 191 self.ui.status(_("filtering out empty revision\n"))
192 192 self.repo.rollback(force=True)
193 193 return parent
194 194 return p2
195 195
196 196 def puttags(self, tags):
197 197 try:
198 198 parentctx = self.repo[self.tagsbranch]
199 199 tagparent = parentctx.node()
200 200 except error.RepoError:
201 201 parentctx = None
202 202 tagparent = nullid
203 203
204 204 try:
205 205 oldlines = sorted(parentctx['.hgtags'].data().splitlines(True))
206 206 except Exception:
207 207 oldlines = []
208 208
209 209 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
210 210 if newlines == oldlines:
211 211 return None, None
212 212 data = "".join(newlines)
213 213 def getfilectx(repo, memctx, f):
214 214 return context.memfilectx(f, data, False, False, None)
215 215
216 216 self.ui.status(_("updating tags\n"))
217 217 date = "%s 0" % int(time.mktime(time.gmtime()))
218 218 extra = {'branch': self.tagsbranch}
219 219 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
220 220 [".hgtags"], getfilectx, "convert-repo", date,
221 221 extra)
222 222 self.repo.commitctx(ctx)
223 223 return hex(self.repo.changelog.tip()), hex(tagparent)
224 224
225 225 def setfilemapmode(self, active):
226 226 self.filemapmode = active
227 227
228 228 def putbookmarks(self, updatedbookmark):
229 229 if not len(updatedbookmark):
230 230 return
231 231
232 232 self.ui.status(_("updating bookmarks\n"))
233 233 destmarks = self.repo._bookmarks
234 234 for bookmark in updatedbookmark:
235 235 destmarks[bookmark] = bin(updatedbookmark[bookmark])
236 236 destmarks.write()
237 237
238 238 def hascommit(self, rev):
239 239 if rev not in self.repo and self.clonebranches:
240 240 raise util.Abort(_('revision %s not found in destination '
241 241 'repository (lookups with clonebranches=true '
242 242 'are not implemented)') % rev)
243 243 return rev in self.repo
244 244
245 245 class mercurial_source(converter_source):
246 246 def __init__(self, ui, path, rev=None):
247 247 converter_source.__init__(self, ui, path, rev)
248 248 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
249 249 self.ignored = set()
250 250 self.saverev = ui.configbool('convert', 'hg.saverev', False)
251 251 try:
252 252 self.repo = hg.repository(self.ui, path)
253 253 # try to provoke an exception if this isn't really a hg
254 254 # repo, but some other bogus compatible-looking url
255 255 if not self.repo.local():
256 256 raise error.RepoError
257 257 except error.RepoError:
258 258 ui.traceback()
259 259 raise NoRepo(_("%s is not a local Mercurial repository") % path)
260 260 self.lastrev = None
261 261 self.lastctx = None
262 262 self._changescache = None
263 263 self.convertfp = None
264 264 # Restrict converted revisions to startrev descendants
265 265 startnode = ui.config('convert', 'hg.startrev')
266 266 hgrevs = ui.config('convert', 'hg.revs')
267 267 if hgrevs is None:
268 268 if startnode is not None:
269 269 try:
270 270 startnode = self.repo.lookup(startnode)
271 271 except error.RepoError:
272 272 raise util.Abort(_('%s is not a valid start revision')
273 273 % startnode)
274 274 startrev = self.repo.changelog.rev(startnode)
275 275 children = {startnode: 1}
276 276 for r in self.repo.changelog.descendants([startrev]):
277 277 children[self.repo.changelog.node(r)] = 1
278 278 self.keep = children.__contains__
279 279 else:
280 280 self.keep = util.always
281 281 if rev:
282 282 self._heads = [self.repo[rev].node()]
283 283 else:
284 284 self._heads = self.repo.heads()
285 285 else:
286 286 if rev or startnode is not None:
287 287 raise util.Abort(_('hg.revs cannot be combined with '
288 288 'hg.startrev or --rev'))
289 289 nodes = set()
290 290 parents = set()
291 291 for r in scmutil.revrange(self.repo, [hgrevs]):
292 292 ctx = self.repo[r]
293 293 nodes.add(ctx.node())
294 294 parents.update(p.node() for p in ctx.parents())
295 295 self.keep = nodes.__contains__
296 296 self._heads = nodes - parents
297 297
298 298 def changectx(self, rev):
299 299 if self.lastrev != rev:
300 300 self.lastctx = self.repo[rev]
301 301 self.lastrev = rev
302 302 return self.lastctx
303 303
304 304 def parents(self, ctx):
305 305 return [p for p in ctx.parents() if p and self.keep(p.node())]
306 306
307 307 def getheads(self):
308 308 return [hex(h) for h in self._heads if self.keep(h)]
309 309
310 310 def getfile(self, name, rev):
311 311 try:
312 312 fctx = self.changectx(rev)[name]
313 313 return fctx.data(), fctx.flags()
314 314 except error.LookupError, err:
315 315 raise IOError(err)
316 316
317 317 def getchanges(self, rev):
318 318 ctx = self.changectx(rev)
319 319 parents = self.parents(ctx)
320 320 if not parents:
321 321 files = sorted(ctx.manifest())
322 322 # getcopies() is not needed for roots, but it is a simple way to
323 323 # detect missing revlogs and abort on errors or populate
324 324 # self.ignored
325 325 self.getcopies(ctx, parents, files)
326 326 return [(f, rev) for f in files if f not in self.ignored], {}
327 327 if self._changescache and self._changescache[0] == rev:
328 328 m, a, r = self._changescache[1]
329 329 else:
330 330 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
331 331 # getcopies() detects missing revlogs early, run it before
332 332 # filtering the changes.
333 333 copies = self.getcopies(ctx, parents, m + a)
334 334 changes = [(name, rev) for name in m + a + r
335 335 if name not in self.ignored]
336 336 return sorted(changes), copies
337 337
338 338 def getcopies(self, ctx, parents, files):
339 339 copies = {}
340 340 for name in files:
341 341 if name in self.ignored:
342 342 continue
343 343 try:
344 344 copysource, _copynode = ctx.filectx(name).renamed()
345 345 if copysource in self.ignored:
346 346 continue
347 347 # Ignore copy sources not in parent revisions
348 348 found = False
349 349 for p in parents:
350 350 if copysource in p:
351 351 found = True
352 352 break
353 353 if not found:
354 354 continue
355 355 copies[name] = copysource
356 356 except TypeError:
357 357 pass
358 358 except error.LookupError, e:
359 359 if not self.ignoreerrors:
360 360 raise
361 361 self.ignored.add(name)
362 362 self.ui.warn(_('ignoring: %s\n') % e)
363 363 return copies
364 364
365 365 def getcommit(self, rev):
366 366 ctx = self.changectx(rev)
367 367 parents = [p.hex() for p in self.parents(ctx)]
368 368 if self.saverev:
369 369 crev = rev
370 370 else:
371 371 crev = None
372 372 return commit(author=ctx.user(),
373 373 date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
374 374 desc=ctx.description(), rev=crev, parents=parents,
375 375 branch=ctx.branch(), extra=ctx.extra(),
376 376 sortkey=ctx.rev())
377 377
378 378 def gettags(self):
379 379 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
380 380 return dict([(name, hex(node)) for name, node in tags
381 381 if self.keep(node)])
382 382
383 383 def getchangedfiles(self, rev, i):
384 384 ctx = self.changectx(rev)
385 385 parents = self.parents(ctx)
386 386 if not parents and i is None:
387 387 i = 0
388 388 changes = [], ctx.manifest().keys(), []
389 389 else:
390 390 i = i or 0
391 391 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
392 392 changes = [[f for f in l if f not in self.ignored] for l in changes]
393 393
394 394 if i == 0:
395 395 self._changescache = (rev, changes)
396 396
397 397 return changes[0] + changes[1] + changes[2]
398 398
399 399 def converted(self, rev, destrev):
400 400 if self.convertfp is None:
401 401 self.convertfp = open(self.repo.join('shamap'), 'a')
402 402 self.convertfp.write('%s %s\n' % (destrev, rev))
403 403 self.convertfp.flush()
404 404
405 405 def before(self):
406 406 self.ui.debug('run hg source pre-conversion action\n')
407 407
408 408 def after(self):
409 409 self.ui.debug('run hg source post-conversion action\n')
410 410
411 411 def hasnativeorder(self):
412 412 return True
413 413
414 414 def hasnativeclose(self):
415 415 return True
416 416
417 417 def lookuprev(self, rev):
418 418 try:
419 419 return hex(self.repo.lookup(rev))
420 420 except error.RepoError:
421 421 return None
422 422
423 423 def getbookmarks(self):
424 424 return bookmarks.listbookmarks(self.repo)
425 425
426 def checkrevformat(self, revstr):
426 def checkrevformat(self, revstr, mapname='splicemap'):
427 427 """ Mercurial, revision string is a 40 byte hex """
428 self.checkhexformat(revstr)
428 self.checkhexformat(revstr, mapname)
@@ -1,1266 +1,1266 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 def checkrevformat(self, revstr):
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 raise util.Abort(_('splicemap entry %s is not a valid revision'
463 ' identifier') % revstr)
462 raise util.Abort(_('%s entry %s is not a valid revision'
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 1186 def putcommit(self, files, copies, parents, commit, source, revmap):
1187 1187 for parent in parents:
1188 1188 try:
1189 1189 return self.revid(self.childmap[parent])
1190 1190 except KeyError:
1191 1191 pass
1192 1192
1193 1193 # Apply changes to working copy
1194 1194 for f, v in files:
1195 1195 try:
1196 1196 data, mode = source.getfile(f, v)
1197 1197 except IOError:
1198 1198 self.delete.append(f)
1199 1199 else:
1200 1200 self.putfile(f, mode, data)
1201 1201 if f in copies:
1202 1202 self.copies.append([copies[f], f])
1203 1203 files = [f[0] for f in files]
1204 1204
1205 1205 entries = set(self.delete)
1206 1206 files = frozenset(files)
1207 1207 entries.update(self.add_dirs(files.difference(entries)))
1208 1208 if self.copies:
1209 1209 for s, d in self.copies:
1210 1210 self._copyfile(s, d)
1211 1211 self.copies = []
1212 1212 if self.delete:
1213 1213 self.xargs(self.delete, 'delete')
1214 1214 for f in self.delete:
1215 1215 self.manifest.remove(f)
1216 1216 self.delete = []
1217 1217 entries.update(self.add_files(files.difference(entries)))
1218 1218 entries.update(self.tidy_dirs(entries))
1219 1219 if self.delexec:
1220 1220 self.xargs(self.delexec, 'propdel', 'svn:executable')
1221 1221 self.delexec = []
1222 1222 if self.setexec:
1223 1223 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1224 1224 self.setexec = []
1225 1225
1226 1226 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1227 1227 fp = os.fdopen(fd, 'w')
1228 1228 fp.write(commit.desc)
1229 1229 fp.close()
1230 1230 try:
1231 1231 output = self.run0('commit',
1232 1232 username=util.shortuser(commit.author),
1233 1233 file=messagefile,
1234 1234 encoding='utf-8')
1235 1235 try:
1236 1236 rev = self.commit_re.search(output).group(1)
1237 1237 except AttributeError:
1238 1238 if not files:
1239 1239 return parents[0]
1240 1240 self.ui.warn(_('unexpected svn output:\n'))
1241 1241 self.ui.warn(output)
1242 1242 raise util.Abort(_('unable to cope with svn output'))
1243 1243 if commit.rev:
1244 1244 self.run('propset', 'hg:convert-rev', commit.rev,
1245 1245 revprop=True, revision=rev)
1246 1246 if commit.branch and commit.branch != 'default':
1247 1247 self.run('propset', 'hg:convert-branch', commit.branch,
1248 1248 revprop=True, revision=rev)
1249 1249 for parent in parents:
1250 1250 self.addchild(parent, rev)
1251 1251 return self.revid(rev)
1252 1252 finally:
1253 1253 os.unlink(messagefile)
1254 1254
1255 1255 def puttags(self, tags):
1256 1256 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1257 1257 return None, None
1258 1258
1259 1259 def hascommit(self, rev):
1260 1260 # This is not correct as one can convert to an existing subversion
1261 1261 # repository and childmap would not list all revisions. Too bad.
1262 1262 if rev in self.childmap:
1263 1263 return True
1264 1264 raise util.Abort(_('splice map revision %s not found in subversion '
1265 1265 'child map (revision lookups are not implemented)')
1266 1266 % rev)
General Comments 0
You need to be logged in to leave comments. Login now