##// END OF EJS Templates
merge with crew-stable
Dirkjan Ochtman -
r7185:67ba7493 merge default
parent child Browse files
Show More
@@ -1,1163 +1,1163 b''
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4 #
4 #
5 # Configuration options:
5 # Configuration options:
6 #
6 #
7 # convert.svn.trunk
7 # convert.svn.trunk
8 # Relative path to the trunk (default: "trunk")
8 # Relative path to the trunk (default: "trunk")
9 # convert.svn.branches
9 # convert.svn.branches
10 # Relative path to tree of branches (default: "branches")
10 # Relative path to tree of branches (default: "branches")
11 # convert.svn.tags
11 # convert.svn.tags
12 # Relative path to tree of tags (default: "tags")
12 # Relative path to tree of tags (default: "tags")
13 #
13 #
14 # Set these in a hgrc, or on the command line as follows:
14 # Set these in a hgrc, or on the command line as follows:
15 #
15 #
16 # hg convert --config convert.svn.trunk=wackoname [...]
16 # hg convert --config convert.svn.trunk=wackoname [...]
17
17
18 import locale
18 import locale
19 import os
19 import os
20 import re
20 import re
21 import sys
21 import sys
22 import cPickle as pickle
22 import cPickle as pickle
23 import tempfile
23 import tempfile
24 import urllib
24 import urllib
25
25
26 from mercurial import strutil, util
26 from mercurial import strutil, util
27 from mercurial.i18n import _
27 from mercurial.i18n import _
28
28
29 # Subversion stuff. Works best with very recent Python SVN bindings
29 # Subversion stuff. Works best with very recent Python SVN bindings
30 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
30 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
31 # these bindings.
31 # these bindings.
32
32
33 from cStringIO import StringIO
33 from cStringIO import StringIO
34
34
35 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
35 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
36 from common import commandline, converter_sink, mapfile
36 from common import commandline, converter_sink, mapfile
37
37
38 try:
38 try:
39 from svn.core import SubversionException, Pool
39 from svn.core import SubversionException, Pool
40 import svn
40 import svn
41 import svn.client
41 import svn.client
42 import svn.core
42 import svn.core
43 import svn.ra
43 import svn.ra
44 import svn.delta
44 import svn.delta
45 import transport
45 import transport
46 except ImportError:
46 except ImportError:
47 pass
47 pass
48
48
49 def geturl(path):
49 def geturl(path):
50 try:
50 try:
51 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
51 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
52 except SubversionException:
52 except SubversionException:
53 pass
53 pass
54 if os.path.isdir(path):
54 if os.path.isdir(path):
55 path = os.path.normpath(os.path.abspath(path))
55 path = os.path.normpath(os.path.abspath(path))
56 if os.name == 'nt':
56 if os.name == 'nt':
57 path = '/' + util.normpath(path)
57 path = '/' + util.normpath(path)
58 return 'file://%s' % urllib.quote(path)
58 return 'file://%s' % urllib.quote(path)
59 return path
59 return path
60
60
61 def optrev(number):
61 def optrev(number):
62 optrev = svn.core.svn_opt_revision_t()
62 optrev = svn.core.svn_opt_revision_t()
63 optrev.kind = svn.core.svn_opt_revision_number
63 optrev.kind = svn.core.svn_opt_revision_number
64 optrev.value.number = number
64 optrev.value.number = number
65 return optrev
65 return optrev
66
66
67 class changedpath(object):
67 class changedpath(object):
68 def __init__(self, p):
68 def __init__(self, p):
69 self.copyfrom_path = p.copyfrom_path
69 self.copyfrom_path = p.copyfrom_path
70 self.copyfrom_rev = p.copyfrom_rev
70 self.copyfrom_rev = p.copyfrom_rev
71 self.action = p.action
71 self.action = p.action
72
72
73 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
73 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
74 strict_node_history=False):
74 strict_node_history=False):
75 protocol = -1
75 protocol = -1
76 def receiver(orig_paths, revnum, author, date, message, pool):
76 def receiver(orig_paths, revnum, author, date, message, pool):
77 if orig_paths is not None:
77 if orig_paths is not None:
78 for k, v in orig_paths.iteritems():
78 for k, v in orig_paths.iteritems():
79 orig_paths[k] = changedpath(v)
79 orig_paths[k] = changedpath(v)
80 pickle.dump((orig_paths, revnum, author, date, message),
80 pickle.dump((orig_paths, revnum, author, date, message),
81 fp, protocol)
81 fp, protocol)
82
82
83 try:
83 try:
84 # Use an ra of our own so that our parent can consume
84 # Use an ra of our own so that our parent can consume
85 # our results without confusing the server.
85 # our results without confusing the server.
86 t = transport.SvnRaTransport(url=url)
86 t = transport.SvnRaTransport(url=url)
87 svn.ra.get_log(t.ra, paths, start, end, limit,
87 svn.ra.get_log(t.ra, paths, start, end, limit,
88 discover_changed_paths,
88 discover_changed_paths,
89 strict_node_history,
89 strict_node_history,
90 receiver)
90 receiver)
91 except SubversionException, (inst, num):
91 except SubversionException, (inst, num):
92 pickle.dump(num, fp, protocol)
92 pickle.dump(num, fp, protocol)
93 except IOError:
93 except IOError:
94 # Caller may interrupt the iteration
94 # Caller may interrupt the iteration
95 pickle.dump(None, fp, protocol)
95 pickle.dump(None, fp, protocol)
96 else:
96 else:
97 pickle.dump(None, fp, protocol)
97 pickle.dump(None, fp, protocol)
98 fp.close()
98 fp.close()
99 # With large history, cleanup process goes crazy and suddenly
99 # With large history, cleanup process goes crazy and suddenly
100 # consumes *huge* amount of memory. The output file being closed,
100 # consumes *huge* amount of memory. The output file being closed,
101 # there is no need for clean termination.
101 # there is no need for clean termination.
102 os._exit(0)
102 os._exit(0)
103
103
104 def debugsvnlog(ui, **opts):
104 def debugsvnlog(ui, **opts):
105 """Fetch SVN log in a subprocess and channel them back to parent to
105 """Fetch SVN log in a subprocess and channel them back to parent to
106 avoid memory collection issues.
106 avoid memory collection issues.
107 """
107 """
108 util.set_binary(sys.stdin)
108 util.set_binary(sys.stdin)
109 util.set_binary(sys.stdout)
109 util.set_binary(sys.stdout)
110 args = decodeargs(sys.stdin.read())
110 args = decodeargs(sys.stdin.read())
111 get_log_child(sys.stdout, *args)
111 get_log_child(sys.stdout, *args)
112
112
113 class logstream:
113 class logstream:
114 """Interruptible revision log iterator."""
114 """Interruptible revision log iterator."""
115 def __init__(self, stdout):
115 def __init__(self, stdout):
116 self._stdout = stdout
116 self._stdout = stdout
117
117
118 def __iter__(self):
118 def __iter__(self):
119 while True:
119 while True:
120 entry = pickle.load(self._stdout)
120 entry = pickle.load(self._stdout)
121 try:
121 try:
122 orig_paths, revnum, author, date, message = entry
122 orig_paths, revnum, author, date, message = entry
123 except:
123 except:
124 if entry is None:
124 if entry is None:
125 break
125 break
126 raise SubversionException("child raised exception", entry)
126 raise SubversionException("child raised exception", entry)
127 yield entry
127 yield entry
128
128
129 def close(self):
129 def close(self):
130 if self._stdout:
130 if self._stdout:
131 self._stdout.close()
131 self._stdout.close()
132 self._stdout = None
132 self._stdout = None
133
133
134 # SVN conversion code stolen from bzr-svn and tailor
134 # SVN conversion code stolen from bzr-svn and tailor
135 #
135 #
136 # Subversion looks like a versioned filesystem, branches structures
136 # Subversion looks like a versioned filesystem, branches structures
137 # are defined by conventions and not enforced by the tool. First,
137 # are defined by conventions and not enforced by the tool. First,
138 # we define the potential branches (modules) as "trunk" and "branches"
138 # we define the potential branches (modules) as "trunk" and "branches"
139 # children directories. Revisions are then identified by their
139 # children directories. Revisions are then identified by their
140 # module and revision number (and a repository identifier).
140 # module and revision number (and a repository identifier).
141 #
141 #
142 # The revision graph is really a tree (or a forest). By default, a
142 # The revision graph is really a tree (or a forest). By default, a
143 # revision parent is the previous revision in the same module. If the
143 # revision parent is the previous revision in the same module. If the
144 # module directory is copied/moved from another module then the
144 # module directory is copied/moved from another module then the
145 # revision is the module root and its parent the source revision in
145 # revision is the module root and its parent the source revision in
146 # the parent module. A revision has at most one parent.
146 # the parent module. A revision has at most one parent.
147 #
147 #
148 class svn_source(converter_source):
148 class svn_source(converter_source):
149 def __init__(self, ui, url, rev=None):
149 def __init__(self, ui, url, rev=None):
150 super(svn_source, self).__init__(ui, url, rev=rev)
150 super(svn_source, self).__init__(ui, url, rev=rev)
151
151
152 try:
152 try:
153 SubversionException
153 SubversionException
154 except NameError:
154 except NameError:
155 raise NoRepo('Subversion python bindings could not be loaded')
155 raise NoRepo('Subversion python bindings could not be loaded')
156
156
157 self.encoding = locale.getpreferredencoding()
157 self.encoding = locale.getpreferredencoding()
158 self.lastrevs = {}
158 self.lastrevs = {}
159
159
160 latest = None
160 latest = None
161 try:
161 try:
162 # Support file://path@rev syntax. Useful e.g. to convert
162 # Support file://path@rev syntax. Useful e.g. to convert
163 # deleted branches.
163 # deleted branches.
164 at = url.rfind('@')
164 at = url.rfind('@')
165 if at >= 0:
165 if at >= 0:
166 latest = int(url[at+1:])
166 latest = int(url[at+1:])
167 url = url[:at]
167 url = url[:at]
168 except ValueError, e:
168 except ValueError, e:
169 pass
169 pass
170 self.url = geturl(url)
170 self.url = geturl(url)
171 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
171 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
172 try:
172 try:
173 self.transport = transport.SvnRaTransport(url=self.url)
173 self.transport = transport.SvnRaTransport(url=self.url)
174 self.ra = self.transport.ra
174 self.ra = self.transport.ra
175 self.ctx = self.transport.client
175 self.ctx = self.transport.client
176 self.baseurl = svn.ra.get_repos_root(self.ra)
176 self.baseurl = svn.ra.get_repos_root(self.ra)
177 # Module is either empty or a repository path starting with
177 # Module is either empty or a repository path starting with
178 # a slash and not ending with a slash.
178 # a slash and not ending with a slash.
179 self.module = urllib.unquote(self.url[len(self.baseurl):])
179 self.module = urllib.unquote(self.url[len(self.baseurl):])
180 self.prevmodule = None
180 self.prevmodule = None
181 self.rootmodule = self.module
181 self.rootmodule = self.module
182 self.commits = {}
182 self.commits = {}
183 self.paths = {}
183 self.paths = {}
184 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
184 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
185 except SubversionException, e:
185 except SubversionException, e:
186 ui.print_exc()
186 ui.print_exc()
187 raise NoRepo("%s does not look like a Subversion repo" % self.url)
187 raise NoRepo("%s does not look like a Subversion repo" % self.url)
188
188
189 if rev:
189 if rev:
190 try:
190 try:
191 latest = int(rev)
191 latest = int(rev)
192 except ValueError:
192 except ValueError:
193 raise util.Abort(_('svn: revision %s is not an integer') % rev)
193 raise util.Abort(_('svn: revision %s is not an integer') % rev)
194
194
195 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
195 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
196 try:
196 try:
197 self.startrev = int(self.startrev)
197 self.startrev = int(self.startrev)
198 if self.startrev < 0:
198 if self.startrev < 0:
199 self.startrev = 0
199 self.startrev = 0
200 except ValueError:
200 except ValueError:
201 raise util.Abort(_('svn: start revision %s is not an integer')
201 raise util.Abort(_('svn: start revision %s is not an integer')
202 % self.startrev)
202 % self.startrev)
203
203
204 try:
204 try:
205 self.get_blacklist()
205 self.get_blacklist()
206 except IOError, e:
206 except IOError, e:
207 pass
207 pass
208
208
209 self.head = self.latest(self.module, latest)
209 self.head = self.latest(self.module, latest)
210 if not self.head:
210 if not self.head:
211 raise util.Abort(_('no revision found in module %s') %
211 raise util.Abort(_('no revision found in module %s') %
212 self.module.encode(self.encoding))
212 self.module.encode(self.encoding))
213 self.last_changed = self.revnum(self.head)
213 self.last_changed = self.revnum(self.head)
214
214
215 self._changescache = None
215 self._changescache = None
216
216
217 if os.path.exists(os.path.join(url, '.svn/entries')):
217 if os.path.exists(os.path.join(url, '.svn/entries')):
218 self.wc = url
218 self.wc = url
219 else:
219 else:
220 self.wc = None
220 self.wc = None
221 self.convertfp = None
221 self.convertfp = None
222
222
223 def setrevmap(self, revmap):
223 def setrevmap(self, revmap):
224 lastrevs = {}
224 lastrevs = {}
225 for revid in revmap.iterkeys():
225 for revid in revmap.iterkeys():
226 uuid, module, revnum = self.revsplit(revid)
226 uuid, module, revnum = self.revsplit(revid)
227 lastrevnum = lastrevs.setdefault(module, revnum)
227 lastrevnum = lastrevs.setdefault(module, revnum)
228 if revnum > lastrevnum:
228 if revnum > lastrevnum:
229 lastrevs[module] = revnum
229 lastrevs[module] = revnum
230 self.lastrevs = lastrevs
230 self.lastrevs = lastrevs
231
231
232 def exists(self, path, optrev):
232 def exists(self, path, optrev):
233 try:
233 try:
234 svn.client.ls(self.url.rstrip('/') + '/' + urllib.quote(path),
234 svn.client.ls(self.url.rstrip('/') + '/' + urllib.quote(path),
235 optrev, False, self.ctx)
235 optrev, False, self.ctx)
236 return True
236 return True
237 except SubversionException, err:
237 except SubversionException, err:
238 return False
238 return False
239
239
240 def getheads(self):
240 def getheads(self):
241
241
242 def isdir(path, revnum):
242 def isdir(path, revnum):
243 kind = self._checkpath(path, revnum)
243 kind = self._checkpath(path, revnum)
244 return kind == svn.core.svn_node_dir
244 return kind == svn.core.svn_node_dir
245
245
246 def getcfgpath(name, rev):
246 def getcfgpath(name, rev):
247 cfgpath = self.ui.config('convert', 'svn.' + name)
247 cfgpath = self.ui.config('convert', 'svn.' + name)
248 if cfgpath is not None and cfgpath.strip() == '':
248 if cfgpath is not None and cfgpath.strip() == '':
249 return None
249 return None
250 path = (cfgpath or name).strip('/')
250 path = (cfgpath or name).strip('/')
251 if not self.exists(path, rev):
251 if not self.exists(path, rev):
252 if cfgpath:
252 if cfgpath:
253 raise util.Abort(_('expected %s to be at %r, but not found')
253 raise util.Abort(_('expected %s to be at %r, but not found')
254 % (name, path))
254 % (name, path))
255 return None
255 return None
256 self.ui.note(_('found %s at %r\n') % (name, path))
256 self.ui.note(_('found %s at %r\n') % (name, path))
257 return path
257 return path
258
258
259 rev = optrev(self.last_changed)
259 rev = optrev(self.last_changed)
260 oldmodule = ''
260 oldmodule = ''
261 trunk = getcfgpath('trunk', rev)
261 trunk = getcfgpath('trunk', rev)
262 self.tags = getcfgpath('tags', rev)
262 self.tags = getcfgpath('tags', rev)
263 branches = getcfgpath('branches', rev)
263 branches = getcfgpath('branches', rev)
264
264
265 # If the project has a trunk or branches, we will extract heads
265 # If the project has a trunk or branches, we will extract heads
266 # from them. We keep the project root otherwise.
266 # from them. We keep the project root otherwise.
267 if trunk:
267 if trunk:
268 oldmodule = self.module or ''
268 oldmodule = self.module or ''
269 self.module += '/' + trunk
269 self.module += '/' + trunk
270 self.head = self.latest(self.module, self.last_changed)
270 self.head = self.latest(self.module, self.last_changed)
271 if not self.head:
271 if not self.head:
272 raise util.Abort(_('no revision found in module %s') %
272 raise util.Abort(_('no revision found in module %s') %
273 self.module.encode(self.encoding))
273 self.module.encode(self.encoding))
274
274
275 # First head in the list is the module's head
275 # First head in the list is the module's head
276 self.heads = [self.head]
276 self.heads = [self.head]
277 if self.tags is not None:
277 if self.tags is not None:
278 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
278 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
279
279
280 # Check if branches bring a few more heads to the list
280 # Check if branches bring a few more heads to the list
281 if branches:
281 if branches:
282 rpath = self.url.strip('/')
282 rpath = self.url.strip('/')
283 branchnames = svn.client.ls(rpath + '/' + urllib.quote(branches),
283 branchnames = svn.client.ls(rpath + '/' + urllib.quote(branches),
284 rev, False, self.ctx)
284 rev, False, self.ctx)
285 for branch in branchnames.keys():
285 for branch in branchnames.keys():
286 module = '%s/%s/%s' % (oldmodule, branches, branch)
286 module = '%s/%s/%s' % (oldmodule, branches, branch)
287 if not isdir(module, self.last_changed):
287 if not isdir(module, self.last_changed):
288 continue
288 continue
289 brevid = self.latest(module, self.last_changed)
289 brevid = self.latest(module, self.last_changed)
290 if not brevid:
290 if not brevid:
291 self.ui.note(_('ignoring empty branch %s\n') %
291 self.ui.note(_('ignoring empty branch %s\n') %
292 branch.encode(self.encoding))
292 branch.encode(self.encoding))
293 continue
293 continue
294 self.ui.note(_('found branch %s at %d\n') %
294 self.ui.note(_('found branch %s at %d\n') %
295 (branch, self.revnum(brevid)))
295 (branch, self.revnum(brevid)))
296 self.heads.append(brevid)
296 self.heads.append(brevid)
297
297
298 if self.startrev and self.heads:
298 if self.startrev and self.heads:
299 if len(self.heads) > 1:
299 if len(self.heads) > 1:
300 raise util.Abort(_('svn: start revision is not supported with '
300 raise util.Abort(_('svn: start revision is not supported with '
301 'with more than one branch'))
301 'with more than one branch'))
302 revnum = self.revnum(self.heads[0])
302 revnum = self.revnum(self.heads[0])
303 if revnum < self.startrev:
303 if revnum < self.startrev:
304 raise util.Abort(_('svn: no revision found after start revision %d')
304 raise util.Abort(_('svn: no revision found after start revision %d')
305 % self.startrev)
305 % self.startrev)
306
306
307 return self.heads
307 return self.heads
308
308
309 def getfile(self, file, rev):
309 def getfile(self, file, rev):
310 data, mode = self._getfile(file, rev)
310 data, mode = self._getfile(file, rev)
311 self.modecache[(file, rev)] = mode
311 self.modecache[(file, rev)] = mode
312 return data
312 return data
313
313
314 def getmode(self, file, rev):
314 def getmode(self, file, rev):
315 return self.modecache[(file, rev)]
315 return self.modecache[(file, rev)]
316
316
317 def getchanges(self, rev):
317 def getchanges(self, rev):
318 if self._changescache and self._changescache[0] == rev:
318 if self._changescache and self._changescache[0] == rev:
319 return self._changescache[1]
319 return self._changescache[1]
320 self._changescache = None
320 self._changescache = None
321 self.modecache = {}
321 self.modecache = {}
322 (paths, parents) = self.paths[rev]
322 (paths, parents) = self.paths[rev]
323 if parents:
323 if parents:
324 files, copies = self.expandpaths(rev, paths, parents)
324 files, copies = self.expandpaths(rev, paths, parents)
325 else:
325 else:
326 # Perform a full checkout on roots
326 # Perform a full checkout on roots
327 uuid, module, revnum = self.revsplit(rev)
327 uuid, module, revnum = self.revsplit(rev)
328 entries = svn.client.ls(self.baseurl + urllib.quote(module),
328 entries = svn.client.ls(self.baseurl + urllib.quote(module),
329 optrev(revnum), True, self.ctx)
329 optrev(revnum), True, self.ctx)
330 files = [n for n,e in entries.iteritems()
330 files = [n for n,e in entries.iteritems()
331 if e.kind == svn.core.svn_node_file]
331 if e.kind == svn.core.svn_node_file]
332 copies = {}
332 copies = {}
333
333
334 files.sort()
334 files.sort()
335 files = zip(files, [rev] * len(files))
335 files = zip(files, [rev] * len(files))
336
336
337 # caller caches the result, so free it here to release memory
337 # caller caches the result, so free it here to release memory
338 del self.paths[rev]
338 del self.paths[rev]
339 return (files, copies)
339 return (files, copies)
340
340
341 def getchangedfiles(self, rev, i):
341 def getchangedfiles(self, rev, i):
342 changes = self.getchanges(rev)
342 changes = self.getchanges(rev)
343 self._changescache = (rev, changes)
343 self._changescache = (rev, changes)
344 return [f[0] for f in changes[0]]
344 return [f[0] for f in changes[0]]
345
345
346 def getcommit(self, rev):
346 def getcommit(self, rev):
347 if rev not in self.commits:
347 if rev not in self.commits:
348 uuid, module, revnum = self.revsplit(rev)
348 uuid, module, revnum = self.revsplit(rev)
349 self.module = module
349 self.module = module
350 self.reparent(module)
350 self.reparent(module)
351 # We assume that:
351 # We assume that:
352 # - requests for revisions after "stop" come from the
352 # - requests for revisions after "stop" come from the
353 # revision graph backward traversal. Cache all of them
353 # revision graph backward traversal. Cache all of them
354 # down to stop, they will be used eventually.
354 # down to stop, they will be used eventually.
355 # - requests for revisions before "stop" come to get
355 # - requests for revisions before "stop" come to get
356 # isolated branches parents. Just fetch what is needed.
356 # isolated branches parents. Just fetch what is needed.
357 stop = self.lastrevs.get(module, 0)
357 stop = self.lastrevs.get(module, 0)
358 if revnum < stop:
358 if revnum < stop:
359 stop = revnum + 1
359 stop = revnum + 1
360 self._fetch_revisions(revnum, stop)
360 self._fetch_revisions(revnum, stop)
361 commit = self.commits[rev]
361 commit = self.commits[rev]
362 # caller caches the result, so free it here to release memory
362 # caller caches the result, so free it here to release memory
363 del self.commits[rev]
363 del self.commits[rev]
364 return commit
364 return commit
365
365
366 def gettags(self):
366 def gettags(self):
367 tags = {}
367 tags = {}
368 if self.tags is None:
368 if self.tags is None:
369 return tags
369 return tags
370
370
371 # svn tags are just a convention, project branches left in a
371 # svn tags are just a convention, project branches left in a
372 # 'tags' directory. There is no other relationship than
372 # 'tags' directory. There is no other relationship than
373 # ancestry, which is expensive to discover and makes them hard
373 # ancestry, which is expensive to discover and makes them hard
374 # to update incrementally. Worse, past revisions may be
374 # to update incrementally. Worse, past revisions may be
375 # referenced by tags far away in the future, requiring a deep
375 # referenced by tags far away in the future, requiring a deep
376 # history traversal on every calculation. Current code
376 # history traversal on every calculation. Current code
377 # performs a single backward traversal, tracking moves within
377 # performs a single backward traversal, tracking moves within
378 # the tags directory (tag renaming) and recording a new tag
378 # the tags directory (tag renaming) and recording a new tag
379 # everytime a project is copied from outside the tags
379 # everytime a project is copied from outside the tags
380 # directory. It also lists deleted tags, this behaviour may
380 # directory. It also lists deleted tags, this behaviour may
381 # change in the future.
381 # change in the future.
382 pendings = []
382 pendings = []
383 tagspath = self.tags
383 tagspath = self.tags
384 start = svn.ra.get_latest_revnum(self.ra)
384 start = svn.ra.get_latest_revnum(self.ra)
385 try:
385 try:
386 for entry in self._getlog([self.tags], start, self.startrev):
386 for entry in self._getlog([self.tags], start, self.startrev):
387 origpaths, revnum, author, date, message = entry
387 origpaths, revnum, author, date, message = entry
388 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
388 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
389 in origpaths.iteritems() if e.copyfrom_path]
389 in origpaths.iteritems() if e.copyfrom_path]
390 copies.sort()
390 copies.sort()
391 # Apply moves/copies from more specific to general
391 # Apply moves/copies from more specific to general
392 copies.reverse()
392 copies.reverse()
393
393
394 srctagspath = tagspath
394 srctagspath = tagspath
395 if copies and copies[-1][2] == tagspath:
395 if copies and copies[-1][2] == tagspath:
396 # Track tags directory moves
396 # Track tags directory moves
397 srctagspath = copies.pop()[0]
397 srctagspath = copies.pop()[0]
398
398
399 for source, sourcerev, dest in copies:
399 for source, sourcerev, dest in copies:
400 if not dest.startswith(tagspath + '/'):
400 if not dest.startswith(tagspath + '/'):
401 continue
401 continue
402 for tag in pendings:
402 for tag in pendings:
403 if tag[0].startswith(dest):
403 if tag[0].startswith(dest):
404 tagpath = source + tag[0][len(dest):]
404 tagpath = source + tag[0][len(dest):]
405 tag[:2] = [tagpath, sourcerev]
405 tag[:2] = [tagpath, sourcerev]
406 break
406 break
407 else:
407 else:
408 pendings.append([source, sourcerev, dest.split('/')[-1]])
408 pendings.append([source, sourcerev, dest.split('/')[-1]])
409
409
410 # Tell tag renamings from tag creations
410 # Tell tag renamings from tag creations
411 remainings = []
411 remainings = []
412 for source, sourcerev, tagname in pendings:
412 for source, sourcerev, tagname in pendings:
413 if source.startswith(srctagspath):
413 if source.startswith(srctagspath):
414 remainings.append([source, sourcerev, tagname])
414 remainings.append([source, sourcerev, tagname])
415 continue
415 continue
416 # From revision may be fake, get one with changes
416 # From revision may be fake, get one with changes
417 tagid = self.latest(source, sourcerev)
417 tagid = self.latest(source, sourcerev)
418 if tagid:
418 if tagid:
419 tags[tagname] = tagid
419 tags[tagname] = tagid
420 pendings = remainings
420 pendings = remainings
421 tagspath = srctagspath
421 tagspath = srctagspath
422
422
423 except SubversionException, (inst, num):
423 except SubversionException, (inst, num):
424 self.ui.note(_('no tags found at revision %d\n') % start)
424 self.ui.note(_('no tags found at revision %d\n') % start)
425 return tags
425 return tags
426
426
427 def converted(self, rev, destrev):
427 def converted(self, rev, destrev):
428 if not self.wc:
428 if not self.wc:
429 return
429 return
430 if self.convertfp is None:
430 if self.convertfp is None:
431 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
431 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
432 'a')
432 'a')
433 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
433 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
434 self.convertfp.flush()
434 self.convertfp.flush()
435
435
436 # -- helper functions --
436 # -- helper functions --
437
437
438 def revid(self, revnum, module=None):
438 def revid(self, revnum, module=None):
439 if not module:
439 if not module:
440 module = self.module
440 module = self.module
441 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
441 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
442 revnum)
442 revnum)
443
443
444 def revnum(self, rev):
444 def revnum(self, rev):
445 return int(rev.split('@')[-1])
445 return int(rev.split('@')[-1])
446
446
447 def revsplit(self, rev):
447 def revsplit(self, rev):
448 url, revnum = rev.encode(self.encoding).split('@', 1)
448 url, revnum = rev.encode(self.encoding).split('@', 1)
449 revnum = int(revnum)
449 revnum = int(revnum)
450 parts = url.split('/', 1)
450 parts = url.split('/', 1)
451 uuid = parts.pop(0)[4:]
451 uuid = parts.pop(0)[4:]
452 mod = ''
452 mod = ''
453 if parts:
453 if parts:
454 mod = '/' + parts[0]
454 mod = '/' + parts[0]
455 return uuid, mod, revnum
455 return uuid, mod, revnum
456
456
457 def latest(self, path, stop=0):
457 def latest(self, path, stop=0):
458 """Find the latest revid affecting path, up to stop. It may return
458 """Find the latest revid affecting path, up to stop. It may return
459 a revision in a different module, since a branch may be moved without
459 a revision in a different module, since a branch may be moved without
460 a change being reported. Return None if computed module does not
460 a change being reported. Return None if computed module does not
461 belong to rootmodule subtree.
461 belong to rootmodule subtree.
462 """
462 """
463 if not path.startswith(self.rootmodule):
463 if not path.startswith(self.rootmodule):
464 # Requests on foreign branches may be forbidden at server level
464 # Requests on foreign branches may be forbidden at server level
465 self.ui.debug(_('ignoring foreign branch %r\n') % path)
465 self.ui.debug(_('ignoring foreign branch %r\n') % path)
466 return None
466 return None
467
467
468 if not stop:
468 if not stop:
469 stop = svn.ra.get_latest_revnum(self.ra)
469 stop = svn.ra.get_latest_revnum(self.ra)
470 try:
470 try:
471 prevmodule = self.reparent('')
471 prevmodule = self.reparent('')
472 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
472 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
473 self.reparent(prevmodule)
473 self.reparent(prevmodule)
474 except SubversionException:
474 except SubversionException:
475 dirent = None
475 dirent = None
476 if not dirent:
476 if not dirent:
477 raise util.Abort(_('%s not found up to revision %d') % (path, stop))
477 raise util.Abort(_('%s not found up to revision %d') % (path, stop))
478
478
479 # stat() gives us the previous revision on this line of development, but
479 # stat() gives us the previous revision on this line of development, but
480 # it might be in *another module*. Fetch the log and detect renames down
480 # it might be in *another module*. Fetch the log and detect renames down
481 # to the latest revision.
481 # to the latest revision.
482 stream = self._getlog([path], stop, dirent.created_rev)
482 stream = self._getlog([path], stop, dirent.created_rev)
483 try:
483 try:
484 for entry in stream:
484 for entry in stream:
485 paths, revnum, author, date, message = entry
485 paths, revnum, author, date, message = entry
486 if revnum <= dirent.created_rev:
486 if revnum <= dirent.created_rev:
487 break
487 break
488
488
489 for p in paths:
489 for p in paths:
490 if not path.startswith(p) or not paths[p].copyfrom_path:
490 if not path.startswith(p) or not paths[p].copyfrom_path:
491 continue
491 continue
492 newpath = paths[p].copyfrom_path + path[len(p):]
492 newpath = paths[p].copyfrom_path + path[len(p):]
493 self.ui.debug(_("branch renamed from %s to %s at %d\n") %
493 self.ui.debug(_("branch renamed from %s to %s at %d\n") %
494 (path, newpath, revnum))
494 (path, newpath, revnum))
495 path = newpath
495 path = newpath
496 break
496 break
497 finally:
497 finally:
498 stream.close()
498 stream.close()
499
499
500 if not path.startswith(self.rootmodule):
500 if not path.startswith(self.rootmodule):
501 self.ui.debug(_('ignoring foreign branch %r\n') % path)
501 self.ui.debug(_('ignoring foreign branch %r\n') % path)
502 return None
502 return None
503 return self.revid(dirent.created_rev, path)
503 return self.revid(dirent.created_rev, path)
504
504
505 def get_blacklist(self):
505 def get_blacklist(self):
506 """Avoid certain revision numbers.
506 """Avoid certain revision numbers.
507 It is not uncommon for two nearby revisions to cancel each other
507 It is not uncommon for two nearby revisions to cancel each other
508 out, e.g. 'I copied trunk into a subdirectory of itself instead
508 out, e.g. 'I copied trunk into a subdirectory of itself instead
509 of making a branch'. The converted repository is significantly
509 of making a branch'. The converted repository is significantly
510 smaller if we ignore such revisions."""
510 smaller if we ignore such revisions."""
511 self.blacklist = util.set()
511 self.blacklist = util.set()
512 blacklist = self.blacklist
512 blacklist = self.blacklist
513 for line in file("blacklist.txt", "r"):
513 for line in file("blacklist.txt", "r"):
514 if not line.startswith("#"):
514 if not line.startswith("#"):
515 try:
515 try:
516 svn_rev = int(line.strip())
516 svn_rev = int(line.strip())
517 blacklist.add(svn_rev)
517 blacklist.add(svn_rev)
518 except ValueError, e:
518 except ValueError, e:
519 pass # not an integer or a comment
519 pass # not an integer or a comment
520
520
521 def is_blacklisted(self, svn_rev):
521 def is_blacklisted(self, svn_rev):
522 return svn_rev in self.blacklist
522 return svn_rev in self.blacklist
523
523
524 def reparent(self, module):
524 def reparent(self, module):
525 """Reparent the svn transport and return the previous parent."""
525 """Reparent the svn transport and return the previous parent."""
526 if self.prevmodule == module:
526 if self.prevmodule == module:
527 return module
527 return module
528 svnurl = self.baseurl + urllib.quote(module)
528 svnurl = self.baseurl + urllib.quote(module)
529 prevmodule = self.prevmodule
529 prevmodule = self.prevmodule
530 if prevmodule is None:
530 if prevmodule is None:
531 prevmodule = ''
531 prevmodule = ''
532 self.ui.debug(_("reparent to %s\n") % svnurl)
532 self.ui.debug(_("reparent to %s\n") % svnurl)
533 svn.ra.reparent(self.ra, svnurl)
533 svn.ra.reparent(self.ra, svnurl)
534 self.prevmodule = module
534 self.prevmodule = module
535 return prevmodule
535 return prevmodule
536
536
537 def expandpaths(self, rev, paths, parents):
537 def expandpaths(self, rev, paths, parents):
538 entries = []
538 entries = []
539 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
539 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
540 copies = {}
540 copies = {}
541
541
542 new_module, revnum = self.revsplit(rev)[1:]
542 new_module, revnum = self.revsplit(rev)[1:]
543 if new_module != self.module:
543 if new_module != self.module:
544 self.module = new_module
544 self.module = new_module
545 self.reparent(self.module)
545 self.reparent(self.module)
546
546
547 for path, ent in paths:
547 for path, ent in paths:
548 entrypath = self.getrelpath(path)
548 entrypath = self.getrelpath(path)
549 entry = entrypath.decode(self.encoding)
549 entry = entrypath.decode(self.encoding)
550
550
551 kind = self._checkpath(entrypath, revnum)
551 kind = self._checkpath(entrypath, revnum)
552 if kind == svn.core.svn_node_file:
552 if kind == svn.core.svn_node_file:
553 entries.append(self.recode(entry))
553 entries.append(self.recode(entry))
554 if not ent.copyfrom_path or not parents:
554 if not ent.copyfrom_path or not parents:
555 continue
555 continue
556 # Copy sources not in parent revisions cannot be represented,
556 # Copy sources not in parent revisions cannot be represented,
557 # ignore their origin for now
557 # ignore their origin for now
558 pmodule, prevnum = self.revsplit(parents[0])[1:]
558 pmodule, prevnum = self.revsplit(parents[0])[1:]
559 if ent.copyfrom_rev < prevnum:
559 if ent.copyfrom_rev < prevnum:
560 continue
560 continue
561 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
561 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
562 if not copyfrom_path:
562 if not copyfrom_path:
563 continue
563 continue
564 self.ui.debug(_("copied to %s from %s@%s\n") %
564 self.ui.debug(_("copied to %s from %s@%s\n") %
565 (entrypath, copyfrom_path, ent.copyfrom_rev))
565 (entrypath, copyfrom_path, ent.copyfrom_rev))
566 copies[self.recode(entry)] = self.recode(copyfrom_path)
566 copies[self.recode(entry)] = self.recode(copyfrom_path)
567 elif kind == 0: # gone, but had better be a deleted *file*
567 elif kind == 0: # gone, but had better be a deleted *file*
568 self.ui.debug(_("gone from %s\n") % ent.copyfrom_rev)
568 self.ui.debug(_("gone from %s\n") % ent.copyfrom_rev)
569
569
570 # if a branch is created but entries are removed in the same
570 # if a branch is created but entries are removed in the same
571 # changeset, get the right fromrev
571 # changeset, get the right fromrev
572 # parents cannot be empty here, you cannot remove things from
572 # parents cannot be empty here, you cannot remove things from
573 # a root revision.
573 # a root revision.
574 uuid, old_module, fromrev = self.revsplit(parents[0])
574 uuid, old_module, fromrev = self.revsplit(parents[0])
575
575
576 basepath = old_module + "/" + self.getrelpath(path)
576 basepath = old_module + "/" + self.getrelpath(path)
577 entrypath = basepath
577 entrypath = basepath
578
578
579 def lookup_parts(p):
579 def lookup_parts(p):
580 rc = None
580 rc = None
581 parts = p.split("/")
581 parts = p.split("/")
582 for i in range(len(parts)):
582 for i in range(len(parts)):
583 part = "/".join(parts[:i])
583 part = "/".join(parts[:i])
584 info = part, copyfrom.get(part, None)
584 info = part, copyfrom.get(part, None)
585 if info[1] is not None:
585 if info[1] is not None:
586 self.ui.debug(_("Found parent directory %s\n") % info[1])
586 self.ui.debug(_("Found parent directory %s\n") % info[1])
587 rc = info
587 rc = info
588 return rc
588 return rc
589
589
590 self.ui.debug(_("base, entry %s %s\n") % (basepath, entrypath))
590 self.ui.debug(_("base, entry %s %s\n") % (basepath, entrypath))
591
591
592 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
592 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
593
593
594 # need to remove fragment from lookup_parts and replace with copyfrom_path
594 # need to remove fragment from lookup_parts and replace with copyfrom_path
595 if frompath is not None:
595 if frompath is not None:
596 self.ui.debug(_("munge-o-matic\n"))
596 self.ui.debug(_("munge-o-matic\n"))
597 self.ui.debug(entrypath + '\n')
597 self.ui.debug(entrypath + '\n')
598 self.ui.debug(entrypath[len(frompath):] + '\n')
598 self.ui.debug(entrypath[len(frompath):] + '\n')
599 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
599 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
600 fromrev = froment.copyfrom_rev
600 fromrev = froment.copyfrom_rev
601 self.ui.debug(_("Info: %s %s %s %s\n") % (frompath, froment, ent, entrypath))
601 self.ui.debug(_("Info: %s %s %s %s\n") % (frompath, froment, ent, entrypath))
602
602
603 # We can avoid the reparent calls if the module has not changed
603 # We can avoid the reparent calls if the module has not changed
604 # but it probably does not worth the pain.
604 # but it probably does not worth the pain.
605 prevmodule = self.reparent('')
605 prevmodule = self.reparent('')
606 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
606 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
607 self.reparent(prevmodule)
607 self.reparent(prevmodule)
608
608
609 if fromkind == svn.core.svn_node_file: # a deleted file
609 if fromkind == svn.core.svn_node_file: # a deleted file
610 entries.append(self.recode(entry))
610 entries.append(self.recode(entry))
611 elif fromkind == svn.core.svn_node_dir:
611 elif fromkind == svn.core.svn_node_dir:
612 # print "Deleted/moved non-file:", revnum, path, ent
612 # print "Deleted/moved non-file:", revnum, path, ent
613 # children = self._find_children(path, revnum - 1)
613 # children = self._find_children(path, revnum - 1)
614 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
614 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
615 # Sometimes this is tricky. For example: in
615 # Sometimes this is tricky. For example: in
616 # The Subversion Repository revision 6940 a dir
616 # The Subversion Repository revision 6940 a dir
617 # was copied and one of its files was deleted
617 # was copied and one of its files was deleted
618 # from the new location in the same commit. This
618 # from the new location in the same commit. This
619 # code can't deal with that yet.
619 # code can't deal with that yet.
620 if ent.action == 'C':
620 if ent.action == 'C':
621 children = self._find_children(path, fromrev)
621 children = self._find_children(path, fromrev)
622 else:
622 else:
623 oroot = entrypath.strip('/')
623 oroot = entrypath.strip('/')
624 nroot = path.strip('/')
624 nroot = path.strip('/')
625 children = self._find_children(oroot, fromrev)
625 children = self._find_children(oroot, fromrev)
626 children = [s.replace(oroot,nroot) for s in children]
626 children = [s.replace(oroot,nroot) for s in children]
627 # Mark all [files, not directories] as deleted.
627 # Mark all [files, not directories] as deleted.
628 for child in children:
628 for child in children:
629 # Can we move a child directory and its
629 # Can we move a child directory and its
630 # parent in the same commit? (probably can). Could
630 # parent in the same commit? (probably can). Could
631 # cause problems if instead of revnum -1,
631 # cause problems if instead of revnum -1,
632 # we have to look in (copyfrom_path, revnum - 1)
632 # we have to look in (copyfrom_path, revnum - 1)
633 entrypath = self.getrelpath("/" + child, module=old_module)
633 entrypath = self.getrelpath("/" + child, module=old_module)
634 if entrypath:
634 if entrypath:
635 entry = self.recode(entrypath.decode(self.encoding))
635 entry = self.recode(entrypath.decode(self.encoding))
636 if entry in copies:
636 if entry in copies:
637 # deleted file within a copy
637 # deleted file within a copy
638 del copies[entry]
638 del copies[entry]
639 else:
639 else:
640 entries.append(entry)
640 entries.append(entry)
641 else:
641 else:
642 self.ui.debug(_('unknown path in revision %d: %s\n') % \
642 self.ui.debug(_('unknown path in revision %d: %s\n') % \
643 (revnum, path))
643 (revnum, path))
644 elif kind == svn.core.svn_node_dir:
644 elif kind == svn.core.svn_node_dir:
645 # Should probably synthesize normal file entries
645 # Should probably synthesize normal file entries
646 # and handle as above to clean up copy/rename handling.
646 # and handle as above to clean up copy/rename handling.
647
647
648 # If the directory just had a prop change,
648 # If the directory just had a prop change,
649 # then we shouldn't need to look for its children.
649 # then we shouldn't need to look for its children.
650 if ent.action == 'M':
650 if ent.action == 'M':
651 continue
651 continue
652
652
653 # Also this could create duplicate entries. Not sure
653 # Also this could create duplicate entries. Not sure
654 # whether this will matter. Maybe should make entries a set.
654 # whether this will matter. Maybe should make entries a set.
655 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
655 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
656 # This will fail if a directory was copied
656 # This will fail if a directory was copied
657 # from another branch and then some of its files
657 # from another branch and then some of its files
658 # were deleted in the same transaction.
658 # were deleted in the same transaction.
659 children = util.sort(self._find_children(path, revnum))
659 children = util.sort(self._find_children(path, revnum))
660 for child in children:
660 for child in children:
661 # Can we move a child directory and its
661 # Can we move a child directory and its
662 # parent in the same commit? (probably can). Could
662 # parent in the same commit? (probably can). Could
663 # cause problems if instead of revnum -1,
663 # cause problems if instead of revnum -1,
664 # we have to look in (copyfrom_path, revnum - 1)
664 # we have to look in (copyfrom_path, revnum - 1)
665 entrypath = self.getrelpath("/" + child)
665 entrypath = self.getrelpath("/" + child)
666 # print child, self.module, entrypath
666 # print child, self.module, entrypath
667 if entrypath:
667 if entrypath:
668 # Need to filter out directories here...
668 # Need to filter out directories here...
669 kind = self._checkpath(entrypath, revnum)
669 kind = self._checkpath(entrypath, revnum)
670 if kind != svn.core.svn_node_dir:
670 if kind != svn.core.svn_node_dir:
671 entries.append(self.recode(entrypath))
671 entries.append(self.recode(entrypath))
672
672
673 # Copies here (must copy all from source)
673 # Copies here (must copy all from source)
674 # Probably not a real problem for us if
674 # Probably not a real problem for us if
675 # source does not exist
675 # source does not exist
676 if not ent.copyfrom_path or not parents:
676 if not ent.copyfrom_path or not parents:
677 continue
677 continue
678 # Copy sources not in parent revisions cannot be represented,
678 # Copy sources not in parent revisions cannot be represented,
679 # ignore their origin for now
679 # ignore their origin for now
680 pmodule, prevnum = self.revsplit(parents[0])[1:]
680 pmodule, prevnum = self.revsplit(parents[0])[1:]
681 if ent.copyfrom_rev < prevnum:
681 if ent.copyfrom_rev < prevnum:
682 continue
682 continue
683 copyfrompath = ent.copyfrom_path.decode(self.encoding)
683 copyfrompath = ent.copyfrom_path.decode(self.encoding)
684 copyfrompath = self.getrelpath(copyfrompath, pmodule)
684 copyfrompath = self.getrelpath(copyfrompath, pmodule)
685 if not copyfrompath:
685 if not copyfrompath:
686 continue
686 continue
687 copyfrom[path] = ent
687 copyfrom[path] = ent
688 self.ui.debug(_("mark %s came from %s:%d\n")
688 self.ui.debug(_("mark %s came from %s:%d\n")
689 % (path, copyfrompath, ent.copyfrom_rev))
689 % (path, copyfrompath, ent.copyfrom_rev))
690 children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev)
690 children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev)
691 children.sort()
691 children.sort()
692 for child in children:
692 for child in children:
693 entrypath = self.getrelpath("/" + child, pmodule)
693 entrypath = self.getrelpath("/" + child, pmodule)
694 if not entrypath:
694 if not entrypath:
695 continue
695 continue
696 entry = entrypath.decode(self.encoding)
696 entry = entrypath.decode(self.encoding)
697 copytopath = path + entry[len(copyfrompath):]
697 copytopath = path + entry[len(copyfrompath):]
698 copytopath = self.getrelpath(copytopath)
698 copytopath = self.getrelpath(copytopath)
699 copies[self.recode(copytopath)] = self.recode(entry, pmodule)
699 copies[self.recode(copytopath)] = self.recode(entry, pmodule)
700
700
701 return (util.unique(entries), copies)
701 return (util.unique(entries), copies)
702
702
703 def _fetch_revisions(self, from_revnum, to_revnum):
703 def _fetch_revisions(self, from_revnum, to_revnum):
704 if from_revnum < to_revnum:
704 if from_revnum < to_revnum:
705 from_revnum, to_revnum = to_revnum, from_revnum
705 from_revnum, to_revnum = to_revnum, from_revnum
706
706
707 self.child_cset = None
707 self.child_cset = None
708
708
709 def isdescendantof(parent, child):
709 def isdescendantof(parent, child):
710 if not child or not parent or not child.startswith(parent):
710 if not child or not parent or not child.startswith(parent):
711 return False
711 return False
712 subpath = child[len(parent):]
712 subpath = child[len(parent):]
713 return len(subpath) > 1 and subpath[0] == '/'
713 return len(subpath) > 1 and subpath[0] == '/'
714
714
715 def parselogentry(orig_paths, revnum, author, date, message):
715 def parselogentry(orig_paths, revnum, author, date, message):
716 """Return the parsed commit object or None, and True if
716 """Return the parsed commit object or None, and True if
717 the revision is a branch root.
717 the revision is a branch root.
718 """
718 """
719 self.ui.debug(_("parsing revision %d (%d changes)\n") %
719 self.ui.debug(_("parsing revision %d (%d changes)\n") %
720 (revnum, len(orig_paths)))
720 (revnum, len(orig_paths)))
721
721
722 branched = False
722 branched = False
723 rev = self.revid(revnum)
723 rev = self.revid(revnum)
724 # branch log might return entries for a parent we already have
724 # branch log might return entries for a parent we already have
725
725
726 if (rev in self.commits or revnum < to_revnum):
726 if (rev in self.commits or revnum < to_revnum):
727 return None, branched
727 return None, branched
728
728
729 parents = []
729 parents = []
730 # check whether this revision is the start of a branch or part
730 # check whether this revision is the start of a branch or part
731 # of a branch renaming
731 # of a branch renaming
732 orig_paths = util.sort(orig_paths.items())
732 orig_paths = util.sort(orig_paths.items())
733 root_paths = [(p,e) for p,e in orig_paths if self.module.startswith(p)]
733 root_paths = [(p,e) for p,e in orig_paths if self.module.startswith(p)]
734 if root_paths:
734 if root_paths:
735 path, ent = root_paths[-1]
735 path, ent = root_paths[-1]
736 if ent.copyfrom_path:
736 if ent.copyfrom_path:
737 # If dir was moved while one of its file was removed
737 # If dir was moved while one of its file was removed
738 # the log may look like:
738 # the log may look like:
739 # A /dir (from /dir:x)
739 # A /dir (from /dir:x)
740 # A /dir/a (from /dir/a:y)
740 # A /dir/a (from /dir/a:y)
741 # A /dir/b (from /dir/b:z)
741 # A /dir/b (from /dir/b:z)
742 # ...
742 # ...
743 # for all remaining children.
743 # for all remaining children.
744 # Let's take the highest child element from rev as source.
744 # Let's take the highest child element from rev as source.
745 copies = [(p,e) for p,e in orig_paths[:-1]
745 copies = [(p,e) for p,e in orig_paths[:-1]
746 if isdescendantof(ent.copyfrom_path, e.copyfrom_path)]
746 if isdescendantof(ent.copyfrom_path, e.copyfrom_path)]
747 fromrev = max([e.copyfrom_rev for p,e in copies] + [ent.copyfrom_rev])
747 fromrev = max([e.copyfrom_rev for p,e in copies] + [ent.copyfrom_rev])
748 branched = True
748 branched = True
749 newpath = ent.copyfrom_path + self.module[len(path):]
749 newpath = ent.copyfrom_path + self.module[len(path):]
750 # ent.copyfrom_rev may not be the actual last revision
750 # ent.copyfrom_rev may not be the actual last revision
751 previd = self.latest(newpath, fromrev)
751 previd = self.latest(newpath, fromrev)
752 if previd is not None:
752 if previd is not None:
753 prevmodule, prevnum = self.revsplit(previd)[1:]
753 prevmodule, prevnum = self.revsplit(previd)[1:]
754 if prevnum >= self.startrev:
754 if prevnum >= self.startrev:
755 parents = [previd]
755 parents = [previd]
756 self.ui.note(_('found parent of branch %s at %d: %s\n') %
756 self.ui.note(_('found parent of branch %s at %d: %s\n') %
757 (self.module, prevnum, prevmodule))
757 (self.module, prevnum, prevmodule))
758 else:
758 else:
759 self.ui.debug(_("No copyfrom path, don't know what to do.\n"))
759 self.ui.debug(_("No copyfrom path, don't know what to do.\n"))
760
760
761 paths = []
761 paths = []
762 # filter out unrelated paths
762 # filter out unrelated paths
763 for path, ent in orig_paths:
763 for path, ent in orig_paths:
764 if self.getrelpath(path) is None:
764 if self.getrelpath(path) is None:
765 continue
765 continue
766 paths.append((path, ent))
766 paths.append((path, ent))
767
767
768 # Example SVN datetime. Includes microseconds.
768 # Example SVN datetime. Includes microseconds.
769 # ISO-8601 conformant
769 # ISO-8601 conformant
770 # '2007-01-04T17:35:00.902377Z'
770 # '2007-01-04T17:35:00.902377Z'
771 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
771 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
772
772
773 log = message and self.recode(message) or ''
773 log = message and self.recode(message) or ''
774 author = author and self.recode(author) or ''
774 author = author and self.recode(author) or ''
775 try:
775 try:
776 branch = self.module.split("/")[-1]
776 branch = self.module.split("/")[-1]
777 if branch == 'trunk':
777 if branch == 'trunk':
778 branch = ''
778 branch = ''
779 except IndexError:
779 except IndexError:
780 branch = None
780 branch = None
781
781
782 cset = commit(author=author,
782 cset = commit(author=author,
783 date=util.datestr(date),
783 date=util.datestr(date),
784 desc=log,
784 desc=log,
785 parents=parents,
785 parents=parents,
786 branch=branch,
786 branch=branch,
787 rev=rev.encode('utf-8'))
787 rev=rev.encode('utf-8'))
788
788
789 self.commits[rev] = cset
789 self.commits[rev] = cset
790 # The parents list is *shared* among self.paths and the
790 # The parents list is *shared* among self.paths and the
791 # commit object. Both will be updated below.
791 # commit object. Both will be updated below.
792 self.paths[rev] = (paths, cset.parents)
792 self.paths[rev] = (paths, cset.parents)
793 if self.child_cset and not self.child_cset.parents:
793 if self.child_cset and not self.child_cset.parents:
794 self.child_cset.parents[:] = [rev]
794 self.child_cset.parents[:] = [rev]
795 self.child_cset = cset
795 self.child_cset = cset
796 return cset, branched
796 return cset, branched
797
797
798 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
798 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
799 (self.module, from_revnum, to_revnum))
799 (self.module, from_revnum, to_revnum))
800
800
801 try:
801 try:
802 firstcset = None
802 firstcset = None
803 lastonbranch = False
803 lastonbranch = False
804 stream = self._getlog([self.module], from_revnum, to_revnum)
804 stream = self._getlog([self.module], from_revnum, to_revnum)
805 try:
805 try:
806 for entry in stream:
806 for entry in stream:
807 paths, revnum, author, date, message = entry
807 paths, revnum, author, date, message = entry
808 if revnum < self.startrev:
808 if revnum < self.startrev:
809 lastonbranch = True
809 lastonbranch = True
810 break
810 break
811 if self.is_blacklisted(revnum):
811 if self.is_blacklisted(revnum):
812 self.ui.note(_('skipping blacklisted revision %d\n')
812 self.ui.note(_('skipping blacklisted revision %d\n')
813 % revnum)
813 % revnum)
814 continue
814 continue
815 if paths is None:
815 if paths is None:
816 self.ui.debug(_('revision %d has no entries\n') % revnum)
816 self.ui.debug(_('revision %d has no entries\n') % revnum)
817 continue
817 continue
818 cset, lastonbranch = parselogentry(paths, revnum, author,
818 cset, lastonbranch = parselogentry(paths, revnum, author,
819 date, message)
819 date, message)
820 if cset:
820 if cset:
821 firstcset = cset
821 firstcset = cset
822 if lastonbranch:
822 if lastonbranch:
823 break
823 break
824 finally:
824 finally:
825 stream.close()
825 stream.close()
826
826
827 if not lastonbranch and firstcset and not firstcset.parents:
827 if not lastonbranch and firstcset and not firstcset.parents:
828 # The first revision of the sequence (the last fetched one)
828 # The first revision of the sequence (the last fetched one)
829 # has invalid parents if not a branch root. Find the parent
829 # has invalid parents if not a branch root. Find the parent
830 # revision now, if any.
830 # revision now, if any.
831 try:
831 try:
832 firstrevnum = self.revnum(firstcset.rev)
832 firstrevnum = self.revnum(firstcset.rev)
833 if firstrevnum > 1:
833 if firstrevnum > 1:
834 latest = self.latest(self.module, firstrevnum - 1)
834 latest = self.latest(self.module, firstrevnum - 1)
835 if latest:
835 if latest:
836 firstcset.parents.append(latest)
836 firstcset.parents.append(latest)
837 except util.Abort:
837 except util.Abort:
838 pass
838 pass
839 except SubversionException, (inst, num):
839 except SubversionException, (inst, num):
840 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
840 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
841 raise util.Abort(_('svn: branch has no revision %s') % to_revnum)
841 raise util.Abort(_('svn: branch has no revision %s') % to_revnum)
842 raise
842 raise
843
843
844 def _getfile(self, file, rev):
844 def _getfile(self, file, rev):
845 io = StringIO()
845 io = StringIO()
846 # TODO: ra.get_file transmits the whole file instead of diffs.
846 # TODO: ra.get_file transmits the whole file instead of diffs.
847 mode = ''
847 mode = ''
848 try:
848 try:
849 new_module, revnum = self.revsplit(rev)[1:]
849 new_module, revnum = self.revsplit(rev)[1:]
850 if self.module != new_module:
850 if self.module != new_module:
851 self.module = new_module
851 self.module = new_module
852 self.reparent(self.module)
852 self.reparent(self.module)
853 info = svn.ra.get_file(self.ra, file, revnum, io)
853 info = svn.ra.get_file(self.ra, file, revnum, io)
854 if isinstance(info, list):
854 if isinstance(info, list):
855 info = info[-1]
855 info = info[-1]
856 mode = ("svn:executable" in info) and 'x' or ''
856 mode = ("svn:executable" in info) and 'x' or ''
857 mode = ("svn:special" in info) and 'l' or mode
857 mode = ("svn:special" in info) and 'l' or mode
858 except SubversionException, e:
858 except SubversionException, e:
859 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
859 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
860 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
860 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
861 if e.apr_err in notfound: # File not found
861 if e.apr_err in notfound: # File not found
862 raise IOError()
862 raise IOError()
863 raise
863 raise
864 data = io.getvalue()
864 data = io.getvalue()
865 if mode == 'l':
865 if mode == 'l':
866 link_prefix = "link "
866 link_prefix = "link "
867 if data.startswith(link_prefix):
867 if data.startswith(link_prefix):
868 data = data[len(link_prefix):]
868 data = data[len(link_prefix):]
869 return data, mode
869 return data, mode
870
870
871 def _find_children(self, path, revnum):
871 def _find_children(self, path, revnum):
872 path = path.strip('/')
872 path = path.strip('/')
873 pool = Pool()
873 pool = Pool()
874 rpath = '/'.join([self.baseurl, urllib.quote(path)]).strip('/')
874 rpath = '/'.join([self.baseurl, urllib.quote(path)]).strip('/')
875 return ['%s/%s' % (path, x) for x in
875 return ['%s/%s' % (path, x) for x in
876 svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
876 svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
877
877
878 def getrelpath(self, path, module=None):
878 def getrelpath(self, path, module=None):
879 if module is None:
879 if module is None:
880 module = self.module
880 module = self.module
881 # Given the repository url of this wc, say
881 # Given the repository url of this wc, say
882 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
882 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
883 # extract the "entry" portion (a relative path) from what
883 # extract the "entry" portion (a relative path) from what
884 # svn log --xml says, ie
884 # svn log --xml says, ie
885 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
885 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
886 # that is to say "tests/PloneTestCase.py"
886 # that is to say "tests/PloneTestCase.py"
887 if path.startswith(module):
887 if path.startswith(module):
888 relative = path.rstrip('/')[len(module):]
888 relative = path.rstrip('/')[len(module):]
889 if relative.startswith('/'):
889 if relative.startswith('/'):
890 return relative[1:]
890 return relative[1:]
891 elif relative == '':
891 elif relative == '':
892 return relative
892 return relative
893
893
894 # The path is outside our tracked tree...
894 # The path is outside our tracked tree...
895 self.ui.debug(_('%r is not under %r, ignoring\n') % (path, module))
895 self.ui.debug(_('%r is not under %r, ignoring\n') % (path, module))
896 return None
896 return None
897
897
898 def _checkpath(self, path, revnum):
898 def _checkpath(self, path, revnum):
899 # ra.check_path does not like leading slashes very much, it leads
899 # ra.check_path does not like leading slashes very much, it leads
900 # to PROPFIND subversion errors
900 # to PROPFIND subversion errors
901 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
901 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
902
902
903 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
903 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
904 strict_node_history=False):
904 strict_node_history=False):
905 # Normalize path names, svn >= 1.5 only wants paths relative to
905 # Normalize path names, svn >= 1.5 only wants paths relative to
906 # supplied URL
906 # supplied URL
907 relpaths = []
907 relpaths = []
908 for p in paths:
908 for p in paths:
909 if not p.startswith('/'):
909 if not p.startswith('/'):
910 p = self.module + '/' + p
910 p = self.module + '/' + p
911 relpaths.append(p.strip('/'))
911 relpaths.append(p.strip('/'))
912 args = [self.baseurl, relpaths, start, end, limit, discover_changed_paths,
912 args = [self.baseurl, relpaths, start, end, limit, discover_changed_paths,
913 strict_node_history]
913 strict_node_history]
914 arg = encodeargs(args)
914 arg = encodeargs(args)
915 hgexe = util.hgexecutable()
915 hgexe = util.hgexecutable()
916 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
916 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
917 stdin, stdout = util.popen2(cmd, 'b')
917 stdin, stdout = util.popen2(cmd, 'b')
918 stdin.write(arg)
918 stdin.write(arg)
919 stdin.close()
919 stdin.close()
920 return logstream(stdout)
920 return logstream(stdout)
921
921
922 pre_revprop_change = '''#!/bin/sh
922 pre_revprop_change = '''#!/bin/sh
923
923
924 REPOS="$1"
924 REPOS="$1"
925 REV="$2"
925 REV="$2"
926 USER="$3"
926 USER="$3"
927 PROPNAME="$4"
927 PROPNAME="$4"
928 ACTION="$5"
928 ACTION="$5"
929
929
930 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
930 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
931 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
931 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
932 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
932 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
933
933
934 echo "Changing prohibited revision property" >&2
934 echo "Changing prohibited revision property" >&2
935 exit 1
935 exit 1
936 '''
936 '''
937
937
938 class svn_sink(converter_sink, commandline):
938 class svn_sink(converter_sink, commandline):
939 commit_re = re.compile(r'Committed revision (\d+).', re.M)
939 commit_re = re.compile(r'Committed revision (\d+).', re.M)
940
940
941 def prerun(self):
941 def prerun(self):
942 if self.wc:
942 if self.wc:
943 os.chdir(self.wc)
943 os.chdir(self.wc)
944
944
945 def postrun(self):
945 def postrun(self):
946 if self.wc:
946 if self.wc:
947 os.chdir(self.cwd)
947 os.chdir(self.cwd)
948
948
949 def join(self, name):
949 def join(self, name):
950 return os.path.join(self.wc, '.svn', name)
950 return os.path.join(self.wc, '.svn', name)
951
951
952 def revmapfile(self):
952 def revmapfile(self):
953 return self.join('hg-shamap')
953 return self.join('hg-shamap')
954
954
955 def authorfile(self):
955 def authorfile(self):
956 return self.join('hg-authormap')
956 return self.join('hg-authormap')
957
957
958 def __init__(self, ui, path):
958 def __init__(self, ui, path):
959 converter_sink.__init__(self, ui, path)
959 converter_sink.__init__(self, ui, path)
960 commandline.__init__(self, ui, 'svn')
960 commandline.__init__(self, ui, 'svn')
961 self.delete = []
961 self.delete = []
962 self.setexec = []
962 self.setexec = []
963 self.delexec = []
963 self.delexec = []
964 self.copies = []
964 self.copies = []
965 self.wc = None
965 self.wc = None
966 self.cwd = os.getcwd()
966 self.cwd = os.getcwd()
967
967
968 path = os.path.realpath(path)
968 path = os.path.realpath(path)
969
969
970 created = False
970 created = False
971 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
971 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
972 self.wc = path
972 self.wc = path
973 self.run0('update')
973 self.run0('update')
974 else:
974 else:
975 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
975 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
976
976
977 if os.path.isdir(os.path.dirname(path)):
977 if os.path.isdir(os.path.dirname(path)):
978 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
978 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
979 ui.status(_('initializing svn repo %r\n') %
979 ui.status(_('initializing svn repo %r\n') %
980 os.path.basename(path))
980 os.path.basename(path))
981 commandline(ui, 'svnadmin').run0('create', path)
981 commandline(ui, 'svnadmin').run0('create', path)
982 created = path
982 created = path
983 path = util.normpath(path)
983 path = util.normpath(path)
984 if not path.startswith('/'):
984 if not path.startswith('/'):
985 path = '/' + path
985 path = '/' + path
986 path = 'file://' + path
986 path = 'file://' + path
987
987
988 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
988 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
989 self.run0('checkout', path, wcpath)
989 self.run0('checkout', path, wcpath)
990
990
991 self.wc = wcpath
991 self.wc = wcpath
992 self.opener = util.opener(self.wc)
992 self.opener = util.opener(self.wc)
993 self.wopener = util.opener(self.wc)
993 self.wopener = util.opener(self.wc)
994 self.childmap = mapfile(ui, self.join('hg-childmap'))
994 self.childmap = mapfile(ui, self.join('hg-childmap'))
995 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
995 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
996
996
997 if created:
997 if created:
998 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
998 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
999 fp = open(hook, 'w')
999 fp = open(hook, 'w')
1000 fp.write(pre_revprop_change)
1000 fp.write(pre_revprop_change)
1001 fp.close()
1001 fp.close()
1002 util.set_flags(hook, False, True)
1002 util.set_flags(hook, False, True)
1003
1003
1004 xport = transport.SvnRaTransport(url=geturl(path))
1004 xport = transport.SvnRaTransport(url=geturl(path))
1005 self.uuid = svn.ra.get_uuid(xport.ra)
1005 self.uuid = svn.ra.get_uuid(xport.ra)
1006
1006
1007 def wjoin(self, *names):
1007 def wjoin(self, *names):
1008 return os.path.join(self.wc, *names)
1008 return os.path.join(self.wc, *names)
1009
1009
1010 def putfile(self, filename, flags, data):
1010 def putfile(self, filename, flags, data):
1011 if 'l' in flags:
1011 if 'l' in flags:
1012 self.wopener.symlink(data, filename)
1012 self.wopener.symlink(data, filename)
1013 else:
1013 else:
1014 try:
1014 try:
1015 if os.path.islink(self.wjoin(filename)):
1015 if os.path.islink(self.wjoin(filename)):
1016 os.unlink(filename)
1016 os.unlink(filename)
1017 except OSError:
1017 except OSError:
1018 pass
1018 pass
1019 self.wopener(filename, 'w').write(data)
1019 self.wopener(filename, 'w').write(data)
1020
1020
1021 if self.is_exec:
1021 if self.is_exec:
1022 was_exec = self.is_exec(self.wjoin(filename))
1022 was_exec = self.is_exec(self.wjoin(filename))
1023 else:
1023 else:
1024 # On filesystems not supporting execute-bit, there is no way
1024 # On filesystems not supporting execute-bit, there is no way
1025 # to know if it is set but asking subversion. Setting it
1025 # to know if it is set but asking subversion. Setting it
1026 # systematically is just as expensive and much simpler.
1026 # systematically is just as expensive and much simpler.
1027 was_exec = 'x' not in flags
1027 was_exec = 'x' not in flags
1028
1028
1029 util.set_flags(self.wjoin(filename), False, 'x' in flags)
1029 util.set_flags(self.wjoin(filename), False, 'x' in flags)
1030 if was_exec:
1030 if was_exec:
1031 if 'x' not in flags:
1031 if 'x' not in flags:
1032 self.delexec.append(filename)
1032 self.delexec.append(filename)
1033 else:
1033 else:
1034 if 'x' in flags:
1034 if 'x' in flags:
1035 self.setexec.append(filename)
1035 self.setexec.append(filename)
1036
1036
1037 def _copyfile(self, source, dest):
1037 def _copyfile(self, source, dest):
1038 # SVN's copy command pukes if the destination file exists, but
1038 # SVN's copy command pukes if the destination file exists, but
1039 # our copyfile method expects to record a copy that has
1039 # our copyfile method expects to record a copy that has
1040 # already occurred. Cross the semantic gap.
1040 # already occurred. Cross the semantic gap.
1041 wdest = self.wjoin(dest)
1041 wdest = self.wjoin(dest)
1042 exists = os.path.exists(wdest)
1042 exists = os.path.exists(wdest)
1043 if exists:
1043 if exists:
1044 fd, tempname = tempfile.mkstemp(
1044 fd, tempname = tempfile.mkstemp(
1045 prefix='hg-copy-', dir=os.path.dirname(wdest))
1045 prefix='hg-copy-', dir=os.path.dirname(wdest))
1046 os.close(fd)
1046 os.close(fd)
1047 os.unlink(tempname)
1047 os.unlink(tempname)
1048 os.rename(wdest, tempname)
1048 os.rename(wdest, tempname)
1049 try:
1049 try:
1050 self.run0('copy', source, dest)
1050 self.run0('copy', source, dest)
1051 finally:
1051 finally:
1052 if exists:
1052 if exists:
1053 try:
1053 try:
1054 os.unlink(wdest)
1054 os.unlink(wdest)
1055 except OSError:
1055 except OSError:
1056 pass
1056 pass
1057 os.rename(tempname, wdest)
1057 os.rename(tempname, wdest)
1058
1058
1059 def dirs_of(self, files):
1059 def dirs_of(self, files):
1060 dirs = util.set()
1060 dirs = util.set()
1061 for f in files:
1061 for f in files:
1062 if os.path.isdir(self.wjoin(f)):
1062 if os.path.isdir(self.wjoin(f)):
1063 dirs.add(f)
1063 dirs.add(f)
1064 for i in strutil.rfindall(f, '/'):
1064 for i in strutil.rfindall(f, '/'):
1065 dirs.add(f[:i])
1065 dirs.add(f[:i])
1066 return dirs
1066 return dirs
1067
1067
1068 def add_dirs(self, files):
1068 def add_dirs(self, files):
1069 add_dirs = [d for d in util.sort(self.dirs_of(files))
1069 add_dirs = [d for d in util.sort(self.dirs_of(files))
1070 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1070 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1071 if add_dirs:
1071 if add_dirs:
1072 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1072 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1073 return add_dirs
1073 return add_dirs
1074
1074
1075 def add_files(self, files):
1075 def add_files(self, files):
1076 if files:
1076 if files:
1077 self.xargs(files, 'add', quiet=True)
1077 self.xargs(files, 'add', quiet=True)
1078 return files
1078 return files
1079
1079
1080 def tidy_dirs(self, names):
1080 def tidy_dirs(self, names):
1081 dirs = util.sort(self.dirs_of(names))
1081 dirs = util.sort(self.dirs_of(names))
1082 dirs.reverse()
1082 dirs.reverse()
1083 deleted = []
1083 deleted = []
1084 for d in dirs:
1084 for d in dirs:
1085 wd = self.wjoin(d)
1085 wd = self.wjoin(d)
1086 if os.listdir(wd) == '.svn':
1086 if os.listdir(wd) == '.svn':
1087 self.run0('delete', d)
1087 self.run0('delete', d)
1088 deleted.append(d)
1088 deleted.append(d)
1089 return deleted
1089 return deleted
1090
1090
1091 def addchild(self, parent, child):
1091 def addchild(self, parent, child):
1092 self.childmap[parent] = child
1092 self.childmap[parent] = child
1093
1093
1094 def revid(self, rev):
1094 def revid(self, rev):
1095 return u"svn:%s@%s" % (self.uuid, rev)
1095 return u"svn:%s@%s" % (self.uuid, rev)
1096
1096
1097 def putcommit(self, files, copies, parents, commit, source):
1097 def putcommit(self, files, copies, parents, commit, source):
1098 # Apply changes to working copy
1098 # Apply changes to working copy
1099 for f, v in files:
1099 for f, v in files:
1100 try:
1100 try:
1101 data = source.getfile(f, v)
1101 data = source.getfile(f, v)
1102 except IOError, inst:
1102 except IOError, inst:
1103 self.delete.append(f)
1103 self.delete.append(f)
1104 else:
1104 else:
1105 e = source.getmode(f, v)
1105 e = source.getmode(f, v)
1106 self.putfile(f, e, data)
1106 self.putfile(f, e, data)
1107 if f in copies:
1107 if f in copies:
1108 self.copies.append([copies[f], f])
1108 self.copies.append([copies[f], f])
1109 files = [f[0] for f in files]
1109 files = [f[0] for f in files]
1110
1110
1111 for parent in parents:
1111 for parent in parents:
1112 try:
1112 try:
1113 return self.revid(self.childmap[parent])
1113 return self.revid(self.childmap[parent])
1114 except KeyError:
1114 except KeyError:
1115 pass
1115 pass
1116 entries = util.set(self.delete)
1116 entries = util.set(self.delete)
1117 files = util.frozenset(files)
1117 files = util.frozenset(files)
1118 entries.update(self.add_dirs(files.difference(entries)))
1118 entries.update(self.add_dirs(files.difference(entries)))
1119 if self.copies:
1119 if self.copies:
1120 for s, d in self.copies:
1120 for s, d in self.copies:
1121 self._copyfile(s, d)
1121 self._copyfile(s, d)
1122 self.copies = []
1122 self.copies = []
1123 if self.delete:
1123 if self.delete:
1124 self.xargs(self.delete, 'delete')
1124 self.xargs(self.delete, 'delete')
1125 self.delete = []
1125 self.delete = []
1126 entries.update(self.add_files(files.difference(entries)))
1126 entries.update(self.add_files(files.difference(entries)))
1127 entries.update(self.tidy_dirs(entries))
1127 entries.update(self.tidy_dirs(entries))
1128 if self.delexec:
1128 if self.delexec:
1129 self.xargs(self.delexec, 'propdel', 'svn:executable')
1129 self.xargs(self.delexec, 'propdel', 'svn:executable')
1130 self.delexec = []
1130 self.delexec = []
1131 if self.setexec:
1131 if self.setexec:
1132 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1132 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1133 self.setexec = []
1133 self.setexec = []
1134
1134
1135 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1135 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1136 fp = os.fdopen(fd, 'w')
1136 fp = os.fdopen(fd, 'w')
1137 fp.write(commit.desc)
1137 fp.write(commit.desc)
1138 fp.close()
1138 fp.close()
1139 try:
1139 try:
1140 output = self.run0('commit',
1140 output = self.run0('commit',
1141 username=util.shortuser(commit.author),
1141 username=util.shortuser(commit.author),
1142 file=messagefile,
1142 file=messagefile,
1143 encoding='utf-8')
1143 encoding='utf-8')
1144 try:
1144 try:
1145 rev = self.commit_re.search(output).group(1)
1145 rev = self.commit_re.search(output).group(1)
1146 except AttributeError:
1146 except AttributeError:
1147 self.ui.warn(_('unexpected svn output:\n'))
1147 self.ui.warn(_('unexpected svn output:\n'))
1148 self.ui.warn(output)
1148 self.ui.warn(output)
1149 raise util.Abort(_('unable to cope with svn output'))
1149 raise util.Abort(_('unable to cope with svn output'))
1150 if commit.rev:
1150 if commit.rev:
1151 self.run('propset', 'hg:convert-rev', commit.rev,
1151 self.run('propset', 'hg:convert-rev', commit.rev,
1152 revprop=True, revision=rev)
1152 revprop=True, revision=rev)
1153 if commit.branch and commit.branch != 'default':
1153 if commit.branch and commit.branch != 'default':
1154 self.run('propset', 'hg:convert-branch', commit.branch,
1154 self.run('propset', 'hg:convert-branch', commit.branch,
1155 revprop=True, revision=rev)
1155 revprop=True, revision=rev)
1156 for parent in parents:
1156 for parent in parents:
1157 self.addchild(parent, rev)
1157 self.addchild(parent, rev)
1158 return self.revid(rev)
1158 return self.revid(rev)
1159 finally:
1159 finally:
1160 os.unlink(messagefile)
1160 os.unlink(messagefile)
1161
1161
1162 def puttags(self, tags):
1162 def puttags(self, tags):
1163 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
1163 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
@@ -1,41 +1,41 b''
1 # strutil.py - string utilities for Mercurial
1 # strutil.py - string utilities for Mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 def findall(haystack, needle, start=0, end=None):
8 def findall(haystack, needle, start=0, end=None):
9 if end is None:
9 if end is None:
10 end = len(haystack)
10 end = len(haystack)
11 if end < 0:
11 if end < 0:
12 end += len(haystack)
12 end += len(haystack)
13 if start < 0:
13 if start < 0:
14 start += len(haystack)
14 start += len(haystack)
15 while start < end:
15 while start < end:
16 c = haystack.find(needle, start, end)
16 c = haystack.find(needle, start, end)
17 if c == -1:
17 if c == -1:
18 break
18 break
19 yield c
19 yield c
20 start = c + 1
20 start = c + 1
21
21
22 def rfindall(haystack, needle, start=0, end=None):
22 def rfindall(haystack, needle, start=0, end=None):
23 if end is None:
23 if end is None:
24 end = len(haystack)
24 end = len(haystack)
25 if end < 0:
25 if end < 0:
26 end += len(haystack)
26 end += len(haystack)
27 if start < 0:
27 if start < 0:
28 start += len(haystack)
28 start += len(haystack)
29 while end >= 0:
29 while end >= 0:
30 c = haystack.rfind(needle, start, end)
30 c = haystack.rfind(needle, start, end)
31 if c == -1:
31 if c == -1:
32 break
32 break
33 yield c
33 yield c
34 end = c - 1
34 end = c - 1
35
35
36 def rsplit(s, sep=None, maxsplit=-1):
36 def rsplit(s, sep=None, maxsplit=-1):
37 try:
37 try:
38 return s.rsplit(sep, maxsplit)
38 return s.rsplit(sep, maxsplit)
39 except AttributeError:
39 except AttributeError:
40 return [chunk[::-1] for chunk in
40 return [chunk[::-1] for chunk in
41 s[::-1].split(sep, maxsplit)[::-1]]
41 s[::-1].split(sep, maxsplit)[::-1]]
@@ -1,19 +1,19 b''
1 # this is hack to make sure no escape characters are inserted into the output
1 # this is hack to make sure no escape characters are inserted into the output
2 import os;
2 import os;
3 if 'TERM' in os.environ:
3 if 'TERM' in os.environ:
4 del os.environ['TERM']
4 del os.environ['TERM']
5 import doctest
5 import doctest
6
6
7 import mercurial.changelog
7 import mercurial.changelog
8 # test doctest from changelog
8 # test doctest from changelog
9
9
10 doctest.testmod(mercurial.changelog)
10 doctest.testmod(mercurial.changelog)
11
11
12 import mercurial.httprepo
12 import mercurial.httprepo
13 doctest.testmod(mercurial.httprepo)
13 doctest.testmod(mercurial.httprepo)
14
14
15 import mercurial.util
15 import mercurial.util
16 doctest.testmod(mercurial.util)
16 doctest.testmod(mercurial.util)
17
17
18 import hgext.convert.cvsps
18 import hgext.convert.cvsps
19 doctest.testmod(hgext.convert.cvsps)
19 doctest.testmod(hgext.convert.cvsps)
General Comments 0
You need to be logged in to leave comments. Login now