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