##// END OF EJS Templates
convert: don't scan directories on property changes
Patrick Mezard -
r5870:0d583ade default
parent child Browse files
Show More
@@ -1,930 +1,933 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
24
25 from mercurial import strutil, util
25 from mercurial import strutil, util
26 from mercurial.i18n import _
26 from mercurial.i18n import _
27
27
28 # Subversion stuff. Works best with very recent Python SVN bindings
28 # Subversion stuff. Works best with very recent Python SVN bindings
29 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
29 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
30 # these bindings.
30 # these bindings.
31
31
32 from cStringIO import StringIO
32 from cStringIO import StringIO
33
33
34 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
34 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
35 from common import commandline, converter_sink, mapfile
35 from common import commandline, converter_sink, mapfile
36
36
37 try:
37 try:
38 from svn.core import SubversionException, Pool
38 from svn.core import SubversionException, Pool
39 import svn
39 import svn
40 import svn.client
40 import svn.client
41 import svn.core
41 import svn.core
42 import svn.ra
42 import svn.ra
43 import svn.delta
43 import svn.delta
44 import transport
44 import transport
45 except ImportError:
45 except ImportError:
46 pass
46 pass
47
47
48 def geturl(path):
48 def geturl(path):
49 try:
49 try:
50 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
50 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
51 except SubversionException:
51 except SubversionException:
52 pass
52 pass
53 if os.path.isdir(path):
53 if os.path.isdir(path):
54 path = os.path.normpath(os.path.abspath(path))
54 path = os.path.normpath(os.path.abspath(path))
55 if os.name == 'nt':
55 if os.name == 'nt':
56 path = '/' + util.normpath(path)
56 path = '/' + util.normpath(path)
57 return 'file://%s' % path
57 return 'file://%s' % path
58 return path
58 return path
59
59
60 def optrev(number):
60 def optrev(number):
61 optrev = svn.core.svn_opt_revision_t()
61 optrev = svn.core.svn_opt_revision_t()
62 optrev.kind = svn.core.svn_opt_revision_number
62 optrev.kind = svn.core.svn_opt_revision_number
63 optrev.value.number = number
63 optrev.value.number = number
64 return optrev
64 return optrev
65
65
66 class changedpath(object):
66 class changedpath(object):
67 def __init__(self, p):
67 def __init__(self, p):
68 self.copyfrom_path = p.copyfrom_path
68 self.copyfrom_path = p.copyfrom_path
69 self.copyfrom_rev = p.copyfrom_rev
69 self.copyfrom_rev = p.copyfrom_rev
70 self.action = p.action
70 self.action = p.action
71
71
72 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
72 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
73 strict_node_history=False):
73 strict_node_history=False):
74 protocol = -1
74 protocol = -1
75 def receiver(orig_paths, revnum, author, date, message, pool):
75 def receiver(orig_paths, revnum, author, date, message, pool):
76 if orig_paths is not None:
76 if orig_paths is not None:
77 for k, v in orig_paths.iteritems():
77 for k, v in orig_paths.iteritems():
78 orig_paths[k] = changedpath(v)
78 orig_paths[k] = changedpath(v)
79 pickle.dump((orig_paths, revnum, author, date, message),
79 pickle.dump((orig_paths, revnum, author, date, message),
80 fp, protocol)
80 fp, protocol)
81
81
82 try:
82 try:
83 # Use an ra of our own so that our parent can consume
83 # Use an ra of our own so that our parent can consume
84 # our results without confusing the server.
84 # our results without confusing the server.
85 t = transport.SvnRaTransport(url=url)
85 t = transport.SvnRaTransport(url=url)
86 svn.ra.get_log(t.ra, paths, start, end, limit,
86 svn.ra.get_log(t.ra, paths, start, end, limit,
87 discover_changed_paths,
87 discover_changed_paths,
88 strict_node_history,
88 strict_node_history,
89 receiver)
89 receiver)
90 except SubversionException, (inst, num):
90 except SubversionException, (inst, num):
91 pickle.dump(num, fp, protocol)
91 pickle.dump(num, fp, protocol)
92 else:
92 else:
93 pickle.dump(None, fp, protocol)
93 pickle.dump(None, fp, protocol)
94 fp.close()
94 fp.close()
95
95
96 def debugsvnlog(ui, **opts):
96 def debugsvnlog(ui, **opts):
97 """Fetch SVN log in a subprocess and channel them back to parent to
97 """Fetch SVN log in a subprocess and channel them back to parent to
98 avoid memory collection issues.
98 avoid memory collection issues.
99 """
99 """
100 util.set_binary(sys.stdin)
100 util.set_binary(sys.stdin)
101 util.set_binary(sys.stdout)
101 util.set_binary(sys.stdout)
102 args = decodeargs(sys.stdin.read())
102 args = decodeargs(sys.stdin.read())
103 get_log_child(sys.stdout, *args)
103 get_log_child(sys.stdout, *args)
104
104
105 # SVN conversion code stolen from bzr-svn and tailor
105 # SVN conversion code stolen from bzr-svn and tailor
106 class svn_source(converter_source):
106 class svn_source(converter_source):
107 def __init__(self, ui, url, rev=None):
107 def __init__(self, ui, url, rev=None):
108 super(svn_source, self).__init__(ui, url, rev=rev)
108 super(svn_source, self).__init__(ui, url, rev=rev)
109
109
110 try:
110 try:
111 SubversionException
111 SubversionException
112 except NameError:
112 except NameError:
113 raise NoRepo('Subversion python bindings could not be loaded')
113 raise NoRepo('Subversion python bindings could not be loaded')
114
114
115 self.encoding = locale.getpreferredencoding()
115 self.encoding = locale.getpreferredencoding()
116 self.lastrevs = {}
116 self.lastrevs = {}
117
117
118 latest = None
118 latest = None
119 try:
119 try:
120 # Support file://path@rev syntax. Useful e.g. to convert
120 # Support file://path@rev syntax. Useful e.g. to convert
121 # deleted branches.
121 # deleted branches.
122 at = url.rfind('@')
122 at = url.rfind('@')
123 if at >= 0:
123 if at >= 0:
124 latest = int(url[at+1:])
124 latest = int(url[at+1:])
125 url = url[:at]
125 url = url[:at]
126 except ValueError, e:
126 except ValueError, e:
127 pass
127 pass
128 self.url = geturl(url)
128 self.url = geturl(url)
129 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
129 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
130 try:
130 try:
131 self.transport = transport.SvnRaTransport(url=self.url)
131 self.transport = transport.SvnRaTransport(url=self.url)
132 self.ra = self.transport.ra
132 self.ra = self.transport.ra
133 self.ctx = self.transport.client
133 self.ctx = self.transport.client
134 self.base = svn.ra.get_repos_root(self.ra)
134 self.base = svn.ra.get_repos_root(self.ra)
135 self.module = self.url[len(self.base):]
135 self.module = self.url[len(self.base):]
136 self.modulemap = {} # revision, module
136 self.modulemap = {} # revision, module
137 self.commits = {}
137 self.commits = {}
138 self.paths = {}
138 self.paths = {}
139 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
139 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
140 except SubversionException, e:
140 except SubversionException, e:
141 ui.print_exc()
141 ui.print_exc()
142 raise NoRepo("%s does not look like a Subversion repo" % self.url)
142 raise NoRepo("%s does not look like a Subversion repo" % self.url)
143
143
144 if rev:
144 if rev:
145 try:
145 try:
146 latest = int(rev)
146 latest = int(rev)
147 except ValueError:
147 except ValueError:
148 raise util.Abort('svn: revision %s is not an integer' % rev)
148 raise util.Abort('svn: revision %s is not an integer' % rev)
149
149
150 try:
150 try:
151 self.get_blacklist()
151 self.get_blacklist()
152 except IOError, e:
152 except IOError, e:
153 pass
153 pass
154
154
155 self.last_changed = self.latest(self.module, latest)
155 self.last_changed = self.latest(self.module, latest)
156
156
157 self.head = self.revid(self.last_changed)
157 self.head = self.revid(self.last_changed)
158 self._changescache = None
158 self._changescache = None
159
159
160 if os.path.exists(os.path.join(url, '.svn/entries')):
160 if os.path.exists(os.path.join(url, '.svn/entries')):
161 self.wc = url
161 self.wc = url
162 else:
162 else:
163 self.wc = None
163 self.wc = None
164 self.convertfp = None
164 self.convertfp = None
165
165
166 def setrevmap(self, revmap):
166 def setrevmap(self, revmap):
167 lastrevs = {}
167 lastrevs = {}
168 for revid in revmap.iterkeys():
168 for revid in revmap.iterkeys():
169 uuid, module, revnum = self.revsplit(revid)
169 uuid, module, revnum = self.revsplit(revid)
170 lastrevnum = lastrevs.setdefault(module, revnum)
170 lastrevnum = lastrevs.setdefault(module, revnum)
171 if revnum > lastrevnum:
171 if revnum > lastrevnum:
172 lastrevs[module] = revnum
172 lastrevs[module] = revnum
173 self.lastrevs = lastrevs
173 self.lastrevs = lastrevs
174
174
175 def exists(self, path, optrev):
175 def exists(self, path, optrev):
176 try:
176 try:
177 svn.client.ls(self.url.rstrip('/') + '/' + path,
177 svn.client.ls(self.url.rstrip('/') + '/' + path,
178 optrev, False, self.ctx)
178 optrev, False, self.ctx)
179 return True
179 return True
180 except SubversionException, err:
180 except SubversionException, err:
181 return False
181 return False
182
182
183 def getheads(self):
183 def getheads(self):
184
184
185 def getcfgpath(name, rev):
185 def getcfgpath(name, rev):
186 cfgpath = self.ui.config('convert', 'svn.' + name)
186 cfgpath = self.ui.config('convert', 'svn.' + name)
187 path = (cfgpath or name).strip('/')
187 path = (cfgpath or name).strip('/')
188 if not self.exists(path, rev):
188 if not self.exists(path, rev):
189 if cfgpath:
189 if cfgpath:
190 raise util.Abort(_('expected %s to be at %r, but not found')
190 raise util.Abort(_('expected %s to be at %r, but not found')
191 % (name, path))
191 % (name, path))
192 return None
192 return None
193 self.ui.note(_('found %s at %r\n') % (name, path))
193 self.ui.note(_('found %s at %r\n') % (name, path))
194 return path
194 return path
195
195
196 rev = optrev(self.last_changed)
196 rev = optrev(self.last_changed)
197 oldmodule = ''
197 oldmodule = ''
198 trunk = getcfgpath('trunk', rev)
198 trunk = getcfgpath('trunk', rev)
199 tags = getcfgpath('tags', rev)
199 tags = getcfgpath('tags', rev)
200 branches = getcfgpath('branches', rev)
200 branches = getcfgpath('branches', rev)
201
201
202 # If the project has a trunk or branches, we will extract heads
202 # If the project has a trunk or branches, we will extract heads
203 # from them. We keep the project root otherwise.
203 # from them. We keep the project root otherwise.
204 if trunk:
204 if trunk:
205 oldmodule = self.module or ''
205 oldmodule = self.module or ''
206 self.module += '/' + trunk
206 self.module += '/' + trunk
207 lt = self.latest(self.module, self.last_changed)
207 lt = self.latest(self.module, self.last_changed)
208 self.head = self.revid(lt)
208 self.head = self.revid(lt)
209
209
210 # First head in the list is the module's head
210 # First head in the list is the module's head
211 self.heads = [self.head]
211 self.heads = [self.head]
212 self.tags = '%s/%s' % (oldmodule , (tags or 'tags'))
212 self.tags = '%s/%s' % (oldmodule , (tags or 'tags'))
213
213
214 # Check if branches bring a few more heads to the list
214 # Check if branches bring a few more heads to the list
215 if branches:
215 if branches:
216 rpath = self.url.strip('/')
216 rpath = self.url.strip('/')
217 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
217 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
218 self.ctx)
218 self.ctx)
219 for branch in branchnames.keys():
219 for branch in branchnames.keys():
220 module = '%s/%s/%s' % (oldmodule, branches, branch)
220 module = '%s/%s/%s' % (oldmodule, branches, branch)
221 brevnum = self.latest(module, self.last_changed)
221 brevnum = self.latest(module, self.last_changed)
222 brev = self.revid(brevnum, module)
222 brev = self.revid(brevnum, module)
223 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
223 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
224 self.heads.append(brev)
224 self.heads.append(brev)
225
225
226 return self.heads
226 return self.heads
227
227
228 def getfile(self, file, rev):
228 def getfile(self, file, rev):
229 data, mode = self._getfile(file, rev)
229 data, mode = self._getfile(file, rev)
230 self.modecache[(file, rev)] = mode
230 self.modecache[(file, rev)] = mode
231 return data
231 return data
232
232
233 def getmode(self, file, rev):
233 def getmode(self, file, rev):
234 return self.modecache[(file, rev)]
234 return self.modecache[(file, rev)]
235
235
236 def getchanges(self, rev):
236 def getchanges(self, rev):
237 if self._changescache and self._changescache[0] == rev:
237 if self._changescache and self._changescache[0] == rev:
238 return self._changescache[1]
238 return self._changescache[1]
239 self._changescache = None
239 self._changescache = None
240 self.modecache = {}
240 self.modecache = {}
241 (paths, parents) = self.paths[rev]
241 (paths, parents) = self.paths[rev]
242 files, copies = self.expandpaths(rev, paths, parents)
242 files, copies = self.expandpaths(rev, paths, parents)
243 files.sort()
243 files.sort()
244 files = zip(files, [rev] * len(files))
244 files = zip(files, [rev] * len(files))
245
245
246 # caller caches the result, so free it here to release memory
246 # caller caches the result, so free it here to release memory
247 del self.paths[rev]
247 del self.paths[rev]
248 return (files, copies)
248 return (files, copies)
249
249
250 def getchangedfiles(self, rev, i):
250 def getchangedfiles(self, rev, i):
251 changes = self.getchanges(rev)
251 changes = self.getchanges(rev)
252 self._changescache = (rev, changes)
252 self._changescache = (rev, changes)
253 return [f[0] for f in changes[0]]
253 return [f[0] for f in changes[0]]
254
254
255 def getcommit(self, rev):
255 def getcommit(self, rev):
256 if rev not in self.commits:
256 if rev not in self.commits:
257 uuid, module, revnum = self.revsplit(rev)
257 uuid, module, revnum = self.revsplit(rev)
258 self.module = module
258 self.module = module
259 self.reparent(module)
259 self.reparent(module)
260 stop = self.lastrevs.get(module, 0)
260 stop = self.lastrevs.get(module, 0)
261 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
261 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
262 commit = self.commits[rev]
262 commit = self.commits[rev]
263 # caller caches the result, so free it here to release memory
263 # caller caches the result, so free it here to release memory
264 del self.commits[rev]
264 del self.commits[rev]
265 return commit
265 return commit
266
266
267 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
267 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
268 strict_node_history=False):
268 strict_node_history=False):
269
269
270 def parent(fp):
270 def parent(fp):
271 while True:
271 while True:
272 entry = pickle.load(fp)
272 entry = pickle.load(fp)
273 try:
273 try:
274 orig_paths, revnum, author, date, message = entry
274 orig_paths, revnum, author, date, message = entry
275 except:
275 except:
276 if entry is None:
276 if entry is None:
277 break
277 break
278 raise SubversionException("child raised exception", entry)
278 raise SubversionException("child raised exception", entry)
279 yield entry
279 yield entry
280
280
281 args = [self.url, paths, start, end, limit, discover_changed_paths,
281 args = [self.url, paths, start, end, limit, discover_changed_paths,
282 strict_node_history]
282 strict_node_history]
283 arg = encodeargs(args)
283 arg = encodeargs(args)
284 hgexe = util.hgexecutable()
284 hgexe = util.hgexecutable()
285 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
285 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
286 stdin, stdout = os.popen2(cmd, 'b')
286 stdin, stdout = os.popen2(cmd, 'b')
287
287
288 stdin.write(arg)
288 stdin.write(arg)
289 stdin.close()
289 stdin.close()
290
290
291 for p in parent(stdout):
291 for p in parent(stdout):
292 yield p
292 yield p
293
293
294 def gettags(self):
294 def gettags(self):
295 tags = {}
295 tags = {}
296 start = self.revnum(self.head)
296 start = self.revnum(self.head)
297 try:
297 try:
298 for entry in self.get_log([self.tags], 0, start):
298 for entry in self.get_log([self.tags], 0, start):
299 orig_paths, revnum, author, date, message = entry
299 orig_paths, revnum, author, date, message = entry
300 for path in orig_paths:
300 for path in orig_paths:
301 if not path.startswith(self.tags+'/'):
301 if not path.startswith(self.tags+'/'):
302 continue
302 continue
303 ent = orig_paths[path]
303 ent = orig_paths[path]
304 source = ent.copyfrom_path
304 source = ent.copyfrom_path
305 rev = ent.copyfrom_rev
305 rev = ent.copyfrom_rev
306 tag = path.split('/')[-1]
306 tag = path.split('/')[-1]
307 tags[tag] = self.revid(rev, module=source)
307 tags[tag] = self.revid(rev, module=source)
308 except SubversionException, (inst, num):
308 except SubversionException, (inst, num):
309 self.ui.note('no tags found at revision %d\n' % start)
309 self.ui.note('no tags found at revision %d\n' % start)
310 return tags
310 return tags
311
311
312 def converted(self, rev, destrev):
312 def converted(self, rev, destrev):
313 if not self.wc:
313 if not self.wc:
314 return
314 return
315 if self.convertfp is None:
315 if self.convertfp is None:
316 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
316 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
317 'a')
317 'a')
318 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
318 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
319 self.convertfp.flush()
319 self.convertfp.flush()
320
320
321 # -- helper functions --
321 # -- helper functions --
322
322
323 def revid(self, revnum, module=None):
323 def revid(self, revnum, module=None):
324 if not module:
324 if not module:
325 module = self.module
325 module = self.module
326 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
326 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
327 revnum)
327 revnum)
328
328
329 def revnum(self, rev):
329 def revnum(self, rev):
330 return int(rev.split('@')[-1])
330 return int(rev.split('@')[-1])
331
331
332 def revsplit(self, rev):
332 def revsplit(self, rev):
333 url, revnum = rev.encode(self.encoding).split('@', 1)
333 url, revnum = rev.encode(self.encoding).split('@', 1)
334 revnum = int(revnum)
334 revnum = int(revnum)
335 parts = url.split('/', 1)
335 parts = url.split('/', 1)
336 uuid = parts.pop(0)[4:]
336 uuid = parts.pop(0)[4:]
337 mod = ''
337 mod = ''
338 if parts:
338 if parts:
339 mod = '/' + parts[0]
339 mod = '/' + parts[0]
340 return uuid, mod, revnum
340 return uuid, mod, revnum
341
341
342 def latest(self, path, stop=0):
342 def latest(self, path, stop=0):
343 'find the latest revision affecting path, up to stop'
343 'find the latest revision affecting path, up to stop'
344 if not stop:
344 if not stop:
345 stop = svn.ra.get_latest_revnum(self.ra)
345 stop = svn.ra.get_latest_revnum(self.ra)
346 try:
346 try:
347 self.reparent('')
347 self.reparent('')
348 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
348 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
349 self.reparent(self.module)
349 self.reparent(self.module)
350 except SubversionException:
350 except SubversionException:
351 dirent = None
351 dirent = None
352 if not dirent:
352 if not dirent:
353 raise util.Abort('%s not found up to revision %d' % (path, stop))
353 raise util.Abort('%s not found up to revision %d' % (path, stop))
354
354
355 return dirent.created_rev
355 return dirent.created_rev
356
356
357 def get_blacklist(self):
357 def get_blacklist(self):
358 """Avoid certain revision numbers.
358 """Avoid certain revision numbers.
359 It is not uncommon for two nearby revisions to cancel each other
359 It is not uncommon for two nearby revisions to cancel each other
360 out, e.g. 'I copied trunk into a subdirectory of itself instead
360 out, e.g. 'I copied trunk into a subdirectory of itself instead
361 of making a branch'. The converted repository is significantly
361 of making a branch'. The converted repository is significantly
362 smaller if we ignore such revisions."""
362 smaller if we ignore such revisions."""
363 self.blacklist = util.set()
363 self.blacklist = util.set()
364 blacklist = self.blacklist
364 blacklist = self.blacklist
365 for line in file("blacklist.txt", "r"):
365 for line in file("blacklist.txt", "r"):
366 if not line.startswith("#"):
366 if not line.startswith("#"):
367 try:
367 try:
368 svn_rev = int(line.strip())
368 svn_rev = int(line.strip())
369 blacklist.add(svn_rev)
369 blacklist.add(svn_rev)
370 except ValueError, e:
370 except ValueError, e:
371 pass # not an integer or a comment
371 pass # not an integer or a comment
372
372
373 def is_blacklisted(self, svn_rev):
373 def is_blacklisted(self, svn_rev):
374 return svn_rev in self.blacklist
374 return svn_rev in self.blacklist
375
375
376 def reparent(self, module):
376 def reparent(self, module):
377 svn_url = self.base + module
377 svn_url = self.base + module
378 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
378 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
379 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
379 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
380
380
381 def expandpaths(self, rev, paths, parents):
381 def expandpaths(self, rev, paths, parents):
382 def get_entry_from_path(path, module=self.module):
382 def get_entry_from_path(path, module=self.module):
383 # Given the repository url of this wc, say
383 # Given the repository url of this wc, say
384 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
384 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
385 # extract the "entry" portion (a relative path) from what
385 # extract the "entry" portion (a relative path) from what
386 # svn log --xml says, ie
386 # svn log --xml says, ie
387 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
387 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
388 # that is to say "tests/PloneTestCase.py"
388 # that is to say "tests/PloneTestCase.py"
389 if path.startswith(module):
389 if path.startswith(module):
390 relative = path[len(module):]
390 relative = path[len(module):]
391 if relative.startswith('/'):
391 if relative.startswith('/'):
392 return relative[1:]
392 return relative[1:]
393 else:
393 else:
394 return relative
394 return relative
395
395
396 # The path is outside our tracked tree...
396 # The path is outside our tracked tree...
397 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
397 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
398 return None
398 return None
399
399
400 entries = []
400 entries = []
401 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
401 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
402 copies = {}
402 copies = {}
403 revnum = self.revnum(rev)
403 revnum = self.revnum(rev)
404
404
405 if revnum in self.modulemap:
405 if revnum in self.modulemap:
406 new_module = self.modulemap[revnum]
406 new_module = self.modulemap[revnum]
407 if new_module != self.module:
407 if new_module != self.module:
408 self.module = new_module
408 self.module = new_module
409 self.reparent(self.module)
409 self.reparent(self.module)
410
410
411 for path, ent in paths:
411 for path, ent in paths:
412 entrypath = get_entry_from_path(path, module=self.module)
412 entrypath = get_entry_from_path(path, module=self.module)
413 entry = entrypath.decode(self.encoding)
413 entry = entrypath.decode(self.encoding)
414
414
415 kind = svn.ra.check_path(self.ra, entrypath, revnum)
415 kind = svn.ra.check_path(self.ra, entrypath, revnum)
416 if kind == svn.core.svn_node_file:
416 if kind == svn.core.svn_node_file:
417 if ent.copyfrom_path:
417 if ent.copyfrom_path:
418 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
418 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
419 if copyfrom_path:
419 if copyfrom_path:
420 self.ui.debug("Copied to %s from %s@%s\n" %
420 self.ui.debug("Copied to %s from %s@%s\n" %
421 (entrypath, copyfrom_path,
421 (entrypath, copyfrom_path,
422 ent.copyfrom_rev))
422 ent.copyfrom_rev))
423 # It's probably important for hg that the source
423 # It's probably important for hg that the source
424 # exists in the revision's parent, not just the
424 # exists in the revision's parent, not just the
425 # ent.copyfrom_rev
425 # ent.copyfrom_rev
426 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
426 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
427 if fromkind != 0:
427 if fromkind != 0:
428 copies[self.recode(entry)] = self.recode(copyfrom_path)
428 copies[self.recode(entry)] = self.recode(copyfrom_path)
429 entries.append(self.recode(entry))
429 entries.append(self.recode(entry))
430 elif kind == 0: # gone, but had better be a deleted *file*
430 elif kind == 0: # gone, but had better be a deleted *file*
431 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
431 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
432
432
433 # if a branch is created but entries are removed in the same
433 # if a branch is created but entries are removed in the same
434 # changeset, get the right fromrev
434 # changeset, get the right fromrev
435 if parents:
435 if parents:
436 uuid, old_module, fromrev = self.revsplit(parents[0])
436 uuid, old_module, fromrev = self.revsplit(parents[0])
437 else:
437 else:
438 fromrev = revnum - 1
438 fromrev = revnum - 1
439 # might always need to be revnum - 1 in these 3 lines?
439 # might always need to be revnum - 1 in these 3 lines?
440 old_module = self.modulemap.get(fromrev, self.module)
440 old_module = self.modulemap.get(fromrev, self.module)
441
441
442 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
442 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
443 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
443 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
444
444
445 def lookup_parts(p):
445 def lookup_parts(p):
446 rc = None
446 rc = None
447 parts = p.split("/")
447 parts = p.split("/")
448 for i in range(len(parts)):
448 for i in range(len(parts)):
449 part = "/".join(parts[:i])
449 part = "/".join(parts[:i])
450 info = part, copyfrom.get(part, None)
450 info = part, copyfrom.get(part, None)
451 if info[1] is not None:
451 if info[1] is not None:
452 self.ui.debug("Found parent directory %s\n" % info[1])
452 self.ui.debug("Found parent directory %s\n" % info[1])
453 rc = info
453 rc = info
454 return rc
454 return rc
455
455
456 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
456 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
457
457
458 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
458 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
459
459
460 # need to remove fragment from lookup_parts and replace with copyfrom_path
460 # need to remove fragment from lookup_parts and replace with copyfrom_path
461 if frompath is not None:
461 if frompath is not None:
462 self.ui.debug("munge-o-matic\n")
462 self.ui.debug("munge-o-matic\n")
463 self.ui.debug(entrypath + '\n')
463 self.ui.debug(entrypath + '\n')
464 self.ui.debug(entrypath[len(frompath):] + '\n')
464 self.ui.debug(entrypath[len(frompath):] + '\n')
465 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
465 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
466 fromrev = froment.copyfrom_rev
466 fromrev = froment.copyfrom_rev
467 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
467 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
468
468
469 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
469 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
470 if fromkind == svn.core.svn_node_file: # a deleted file
470 if fromkind == svn.core.svn_node_file: # a deleted file
471 entries.append(self.recode(entry))
471 entries.append(self.recode(entry))
472 elif fromkind == svn.core.svn_node_dir:
472 elif fromkind == svn.core.svn_node_dir:
473 # print "Deleted/moved non-file:", revnum, path, ent
473 # print "Deleted/moved non-file:", revnum, path, ent
474 # children = self._find_children(path, revnum - 1)
474 # children = self._find_children(path, revnum - 1)
475 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
475 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
476 # Sometimes this is tricky. For example: in
476 # Sometimes this is tricky. For example: in
477 # The Subversion Repository revision 6940 a dir
477 # The Subversion Repository revision 6940 a dir
478 # was copied and one of its files was deleted
478 # was copied and one of its files was deleted
479 # from the new location in the same commit. This
479 # from the new location in the same commit. This
480 # code can't deal with that yet.
480 # code can't deal with that yet.
481 if ent.action == 'C':
481 if ent.action == 'C':
482 children = self._find_children(path, fromrev)
482 children = self._find_children(path, fromrev)
483 else:
483 else:
484 oroot = entrypath.strip('/')
484 oroot = entrypath.strip('/')
485 nroot = path.strip('/')
485 nroot = path.strip('/')
486 children = self._find_children(oroot, fromrev)
486 children = self._find_children(oroot, fromrev)
487 children = [s.replace(oroot,nroot) for s in children]
487 children = [s.replace(oroot,nroot) for s in children]
488 # Mark all [files, not directories] as deleted.
488 # Mark all [files, not directories] as deleted.
489 for child in children:
489 for child in children:
490 # Can we move a child directory and its
490 # Can we move a child directory and its
491 # parent in the same commit? (probably can). Could
491 # parent in the same commit? (probably can). Could
492 # cause problems if instead of revnum -1,
492 # cause problems if instead of revnum -1,
493 # we have to look in (copyfrom_path, revnum - 1)
493 # we have to look in (copyfrom_path, revnum - 1)
494 entrypath = get_entry_from_path("/" + child, module=old_module)
494 entrypath = get_entry_from_path("/" + child, module=old_module)
495 if entrypath:
495 if entrypath:
496 entry = self.recode(entrypath.decode(self.encoding))
496 entry = self.recode(entrypath.decode(self.encoding))
497 if entry in copies:
497 if entry in copies:
498 # deleted file within a copy
498 # deleted file within a copy
499 del copies[entry]
499 del copies[entry]
500 else:
500 else:
501 entries.append(entry)
501 entries.append(entry)
502 else:
502 else:
503 self.ui.debug('unknown path in revision %d: %s\n' % \
503 self.ui.debug('unknown path in revision %d: %s\n' % \
504 (revnum, path))
504 (revnum, path))
505 elif kind == svn.core.svn_node_dir:
505 elif kind == svn.core.svn_node_dir:
506 # Should probably synthesize normal file entries
506 # Should probably synthesize normal file entries
507 # and handle as above to clean up copy/rename handling.
507 # and handle as above to clean up copy/rename handling.
508
508
509 # If the directory just had a prop change,
509 # If the directory just had a prop change,
510 # then we shouldn't need to look for its children.
510 # then we shouldn't need to look for its children.
511 if ent.action == 'M':
512 continue
513
511 # Also this could create duplicate entries. Not sure
514 # Also this could create duplicate entries. Not sure
512 # whether this will matter. Maybe should make entries a set.
515 # whether this will matter. Maybe should make entries a set.
513 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
516 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
514 # This will fail if a directory was copied
517 # This will fail if a directory was copied
515 # from another branch and then some of its files
518 # from another branch and then some of its files
516 # were deleted in the same transaction.
519 # were deleted in the same transaction.
517 children = self._find_children(path, revnum)
520 children = self._find_children(path, revnum)
518 children.sort()
521 children.sort()
519 for child in children:
522 for child in children:
520 # Can we move a child directory and its
523 # Can we move a child directory and its
521 # parent in the same commit? (probably can). Could
524 # parent in the same commit? (probably can). Could
522 # cause problems if instead of revnum -1,
525 # cause problems if instead of revnum -1,
523 # we have to look in (copyfrom_path, revnum - 1)
526 # we have to look in (copyfrom_path, revnum - 1)
524 entrypath = get_entry_from_path("/" + child, module=self.module)
527 entrypath = get_entry_from_path("/" + child, module=self.module)
525 # print child, self.module, entrypath
528 # print child, self.module, entrypath
526 if entrypath:
529 if entrypath:
527 # Need to filter out directories here...
530 # Need to filter out directories here...
528 kind = svn.ra.check_path(self.ra, entrypath, revnum)
531 kind = svn.ra.check_path(self.ra, entrypath, revnum)
529 if kind != svn.core.svn_node_dir:
532 if kind != svn.core.svn_node_dir:
530 entries.append(self.recode(entrypath))
533 entries.append(self.recode(entrypath))
531
534
532 # Copies here (must copy all from source)
535 # Copies here (must copy all from source)
533 # Probably not a real problem for us if
536 # Probably not a real problem for us if
534 # source does not exist
537 # source does not exist
535
538
536 # Can do this with the copy command "hg copy"
539 # Can do this with the copy command "hg copy"
537 # if ent.copyfrom_path:
540 # if ent.copyfrom_path:
538 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
541 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
539 # module=self.module)
542 # module=self.module)
540 # copyto_entry = entrypath
543 # copyto_entry = entrypath
541 #
544 #
542 # print "copy directory", copyfrom_entry, 'to', copyto_entry
545 # print "copy directory", copyfrom_entry, 'to', copyto_entry
543 #
546 #
544 # copies.append((copyfrom_entry, copyto_entry))
547 # copies.append((copyfrom_entry, copyto_entry))
545
548
546 if ent.copyfrom_path:
549 if ent.copyfrom_path:
547 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
550 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
548 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
551 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
549 if copyfrom_entry:
552 if copyfrom_entry:
550 copyfrom[path] = ent
553 copyfrom[path] = ent
551 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
554 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
552
555
553 # Good, /probably/ a regular copy. Really should check
556 # Good, /probably/ a regular copy. Really should check
554 # to see whether the parent revision actually contains
557 # to see whether the parent revision actually contains
555 # the directory in question.
558 # the directory in question.
556 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
559 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
557 children.sort()
560 children.sort()
558 for child in children:
561 for child in children:
559 entrypath = get_entry_from_path("/" + child, module=self.module)
562 entrypath = get_entry_from_path("/" + child, module=self.module)
560 if entrypath:
563 if entrypath:
561 entry = entrypath.decode(self.encoding)
564 entry = entrypath.decode(self.encoding)
562 # print "COPY COPY From", copyfrom_entry, entry
565 # print "COPY COPY From", copyfrom_entry, entry
563 copyto_path = path + entry[len(copyfrom_entry):]
566 copyto_path = path + entry[len(copyfrom_entry):]
564 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
567 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
565 # print "COPY", entry, "COPY To", copyto_entry
568 # print "COPY", entry, "COPY To", copyto_entry
566 copies[self.recode(copyto_entry)] = self.recode(entry)
569 copies[self.recode(copyto_entry)] = self.recode(entry)
567 # copy from quux splort/quuxfile
570 # copy from quux splort/quuxfile
568
571
569 return (entries, copies)
572 return (entries, copies)
570
573
571 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
574 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
572 self.child_cset = None
575 self.child_cset = None
573 def parselogentry(orig_paths, revnum, author, date, message):
576 def parselogentry(orig_paths, revnum, author, date, message):
574 self.ui.debug("parsing revision %d (%d changes)\n" %
577 self.ui.debug("parsing revision %d (%d changes)\n" %
575 (revnum, len(orig_paths)))
578 (revnum, len(orig_paths)))
576
579
577 if revnum in self.modulemap:
580 if revnum in self.modulemap:
578 new_module = self.modulemap[revnum]
581 new_module = self.modulemap[revnum]
579 if new_module != self.module:
582 if new_module != self.module:
580 self.module = new_module
583 self.module = new_module
581 self.reparent(self.module)
584 self.reparent(self.module)
582
585
583 rev = self.revid(revnum)
586 rev = self.revid(revnum)
584 # branch log might return entries for a parent we already have
587 # branch log might return entries for a parent we already have
585 if (rev in self.commits or
588 if (rev in self.commits or
586 (revnum < self.lastrevs.get(self.module, 0))):
589 (revnum < self.lastrevs.get(self.module, 0))):
587 return
590 return
588
591
589 parents = []
592 parents = []
590 # check whether this revision is the start of a branch
593 # check whether this revision is the start of a branch
591 if self.module in orig_paths:
594 if self.module in orig_paths:
592 ent = orig_paths[self.module]
595 ent = orig_paths[self.module]
593 if ent.copyfrom_path:
596 if ent.copyfrom_path:
594 # ent.copyfrom_rev may not be the actual last revision
597 # ent.copyfrom_rev may not be the actual last revision
595 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
598 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
596 self.modulemap[prev] = ent.copyfrom_path
599 self.modulemap[prev] = ent.copyfrom_path
597 parents = [self.revid(prev, ent.copyfrom_path)]
600 parents = [self.revid(prev, ent.copyfrom_path)]
598 self.ui.note('found parent of branch %s at %d: %s\n' % \
601 self.ui.note('found parent of branch %s at %d: %s\n' % \
599 (self.module, prev, ent.copyfrom_path))
602 (self.module, prev, ent.copyfrom_path))
600 else:
603 else:
601 self.ui.debug("No copyfrom path, don't know what to do.\n")
604 self.ui.debug("No copyfrom path, don't know what to do.\n")
602
605
603 self.modulemap[revnum] = self.module # track backwards in time
606 self.modulemap[revnum] = self.module # track backwards in time
604
607
605 orig_paths = orig_paths.items()
608 orig_paths = orig_paths.items()
606 orig_paths.sort()
609 orig_paths.sort()
607 paths = []
610 paths = []
608 # filter out unrelated paths
611 # filter out unrelated paths
609 for path, ent in orig_paths:
612 for path, ent in orig_paths:
610 if not path.startswith(self.module):
613 if not path.startswith(self.module):
611 self.ui.debug("boring@%s: %s\n" % (revnum, path))
614 self.ui.debug("boring@%s: %s\n" % (revnum, path))
612 continue
615 continue
613 paths.append((path, ent))
616 paths.append((path, ent))
614
617
615 self.paths[rev] = (paths, parents)
618 self.paths[rev] = (paths, parents)
616
619
617 # Example SVN datetime. Includes microseconds.
620 # Example SVN datetime. Includes microseconds.
618 # ISO-8601 conformant
621 # ISO-8601 conformant
619 # '2007-01-04T17:35:00.902377Z'
622 # '2007-01-04T17:35:00.902377Z'
620 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
623 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
621
624
622 log = message and self.recode(message)
625 log = message and self.recode(message)
623 author = author and self.recode(author) or ''
626 author = author and self.recode(author) or ''
624 try:
627 try:
625 branch = self.module.split("/")[-1]
628 branch = self.module.split("/")[-1]
626 if branch == 'trunk':
629 if branch == 'trunk':
627 branch = ''
630 branch = ''
628 except IndexError:
631 except IndexError:
629 branch = None
632 branch = None
630
633
631 cset = commit(author=author,
634 cset = commit(author=author,
632 date=util.datestr(date),
635 date=util.datestr(date),
633 desc=log,
636 desc=log,
634 parents=parents,
637 parents=parents,
635 branch=branch,
638 branch=branch,
636 rev=rev.encode('utf-8'))
639 rev=rev.encode('utf-8'))
637
640
638 self.commits[rev] = cset
641 self.commits[rev] = cset
639 if self.child_cset and not self.child_cset.parents:
642 if self.child_cset and not self.child_cset.parents:
640 self.child_cset.parents = [rev]
643 self.child_cset.parents = [rev]
641 self.child_cset = cset
644 self.child_cset = cset
642
645
643 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
646 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
644 (self.module, from_revnum, to_revnum))
647 (self.module, from_revnum, to_revnum))
645
648
646 try:
649 try:
647 for entry in self.get_log([self.module], from_revnum, to_revnum):
650 for entry in self.get_log([self.module], from_revnum, to_revnum):
648 orig_paths, revnum, author, date, message = entry
651 orig_paths, revnum, author, date, message = entry
649 if self.is_blacklisted(revnum):
652 if self.is_blacklisted(revnum):
650 self.ui.note('skipping blacklisted revision %d\n' % revnum)
653 self.ui.note('skipping blacklisted revision %d\n' % revnum)
651 continue
654 continue
652 if orig_paths is None:
655 if orig_paths is None:
653 self.ui.debug('revision %d has no entries\n' % revnum)
656 self.ui.debug('revision %d has no entries\n' % revnum)
654 continue
657 continue
655 parselogentry(orig_paths, revnum, author, date, message)
658 parselogentry(orig_paths, revnum, author, date, message)
656 except SubversionException, (inst, num):
659 except SubversionException, (inst, num):
657 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
660 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
658 raise NoSuchRevision(branch=self,
661 raise NoSuchRevision(branch=self,
659 revision="Revision number %d" % to_revnum)
662 revision="Revision number %d" % to_revnum)
660 raise
663 raise
661
664
662 def _getfile(self, file, rev):
665 def _getfile(self, file, rev):
663 io = StringIO()
666 io = StringIO()
664 # TODO: ra.get_file transmits the whole file instead of diffs.
667 # TODO: ra.get_file transmits the whole file instead of diffs.
665 mode = ''
668 mode = ''
666 try:
669 try:
667 revnum = self.revnum(rev)
670 revnum = self.revnum(rev)
668 if self.module != self.modulemap[revnum]:
671 if self.module != self.modulemap[revnum]:
669 self.module = self.modulemap[revnum]
672 self.module = self.modulemap[revnum]
670 self.reparent(self.module)
673 self.reparent(self.module)
671 info = svn.ra.get_file(self.ra, file, revnum, io)
674 info = svn.ra.get_file(self.ra, file, revnum, io)
672 if isinstance(info, list):
675 if isinstance(info, list):
673 info = info[-1]
676 info = info[-1]
674 mode = ("svn:executable" in info) and 'x' or ''
677 mode = ("svn:executable" in info) and 'x' or ''
675 mode = ("svn:special" in info) and 'l' or mode
678 mode = ("svn:special" in info) and 'l' or mode
676 except SubversionException, e:
679 except SubversionException, e:
677 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
680 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
678 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
681 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
679 if e.apr_err in notfound: # File not found
682 if e.apr_err in notfound: # File not found
680 raise IOError()
683 raise IOError()
681 raise
684 raise
682 data = io.getvalue()
685 data = io.getvalue()
683 if mode == 'l':
686 if mode == 'l':
684 link_prefix = "link "
687 link_prefix = "link "
685 if data.startswith(link_prefix):
688 if data.startswith(link_prefix):
686 data = data[len(link_prefix):]
689 data = data[len(link_prefix):]
687 return data, mode
690 return data, mode
688
691
689 def _find_children(self, path, revnum):
692 def _find_children(self, path, revnum):
690 path = path.strip('/')
693 path = path.strip('/')
691 pool = Pool()
694 pool = Pool()
692 rpath = '/'.join([self.base, path]).strip('/')
695 rpath = '/'.join([self.base, path]).strip('/')
693 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
696 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
694
697
695 pre_revprop_change = '''#!/bin/sh
698 pre_revprop_change = '''#!/bin/sh
696
699
697 REPOS="$1"
700 REPOS="$1"
698 REV="$2"
701 REV="$2"
699 USER="$3"
702 USER="$3"
700 PROPNAME="$4"
703 PROPNAME="$4"
701 ACTION="$5"
704 ACTION="$5"
702
705
703 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
706 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
704 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
707 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
705 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
708 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
706
709
707 echo "Changing prohibited revision property" >&2
710 echo "Changing prohibited revision property" >&2
708 exit 1
711 exit 1
709 '''
712 '''
710
713
711 class svn_sink(converter_sink, commandline):
714 class svn_sink(converter_sink, commandline):
712 commit_re = re.compile(r'Committed revision (\d+).', re.M)
715 commit_re = re.compile(r'Committed revision (\d+).', re.M)
713
716
714 def prerun(self):
717 def prerun(self):
715 if self.wc:
718 if self.wc:
716 os.chdir(self.wc)
719 os.chdir(self.wc)
717
720
718 def postrun(self):
721 def postrun(self):
719 if self.wc:
722 if self.wc:
720 os.chdir(self.cwd)
723 os.chdir(self.cwd)
721
724
722 def join(self, name):
725 def join(self, name):
723 return os.path.join(self.wc, '.svn', name)
726 return os.path.join(self.wc, '.svn', name)
724
727
725 def revmapfile(self):
728 def revmapfile(self):
726 return self.join('hg-shamap')
729 return self.join('hg-shamap')
727
730
728 def authorfile(self):
731 def authorfile(self):
729 return self.join('hg-authormap')
732 return self.join('hg-authormap')
730
733
731 def __init__(self, ui, path):
734 def __init__(self, ui, path):
732 converter_sink.__init__(self, ui, path)
735 converter_sink.__init__(self, ui, path)
733 commandline.__init__(self, ui, 'svn')
736 commandline.__init__(self, ui, 'svn')
734 self.delete = []
737 self.delete = []
735 self.setexec = []
738 self.setexec = []
736 self.delexec = []
739 self.delexec = []
737 self.copies = []
740 self.copies = []
738 self.wc = None
741 self.wc = None
739 self.cwd = os.getcwd()
742 self.cwd = os.getcwd()
740
743
741 path = os.path.realpath(path)
744 path = os.path.realpath(path)
742
745
743 created = False
746 created = False
744 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
747 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
745 self.wc = path
748 self.wc = path
746 self.run0('update')
749 self.run0('update')
747 else:
750 else:
748 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
751 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
749
752
750 if os.path.isdir(os.path.dirname(path)):
753 if os.path.isdir(os.path.dirname(path)):
751 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
754 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
752 ui.status(_('initializing svn repo %r\n') %
755 ui.status(_('initializing svn repo %r\n') %
753 os.path.basename(path))
756 os.path.basename(path))
754 commandline(ui, 'svnadmin').run0('create', path)
757 commandline(ui, 'svnadmin').run0('create', path)
755 created = path
758 created = path
756 path = util.normpath(path)
759 path = util.normpath(path)
757 if not path.startswith('/'):
760 if not path.startswith('/'):
758 path = '/' + path
761 path = '/' + path
759 path = 'file://' + path
762 path = 'file://' + path
760
763
761 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
764 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
762 self.run0('checkout', path, wcpath)
765 self.run0('checkout', path, wcpath)
763
766
764 self.wc = wcpath
767 self.wc = wcpath
765 self.opener = util.opener(self.wc)
768 self.opener = util.opener(self.wc)
766 self.wopener = util.opener(self.wc)
769 self.wopener = util.opener(self.wc)
767 self.childmap = mapfile(ui, self.join('hg-childmap'))
770 self.childmap = mapfile(ui, self.join('hg-childmap'))
768 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
771 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
769
772
770 if created:
773 if created:
771 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
774 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
772 fp = open(hook, 'w')
775 fp = open(hook, 'w')
773 fp.write(pre_revprop_change)
776 fp.write(pre_revprop_change)
774 fp.close()
777 fp.close()
775 util.set_flags(hook, "x")
778 util.set_flags(hook, "x")
776
779
777 xport = transport.SvnRaTransport(url=geturl(path))
780 xport = transport.SvnRaTransport(url=geturl(path))
778 self.uuid = svn.ra.get_uuid(xport.ra)
781 self.uuid = svn.ra.get_uuid(xport.ra)
779
782
780 def wjoin(self, *names):
783 def wjoin(self, *names):
781 return os.path.join(self.wc, *names)
784 return os.path.join(self.wc, *names)
782
785
783 def putfile(self, filename, flags, data):
786 def putfile(self, filename, flags, data):
784 if 'l' in flags:
787 if 'l' in flags:
785 self.wopener.symlink(data, filename)
788 self.wopener.symlink(data, filename)
786 else:
789 else:
787 try:
790 try:
788 if os.path.islink(self.wjoin(filename)):
791 if os.path.islink(self.wjoin(filename)):
789 os.unlink(filename)
792 os.unlink(filename)
790 except OSError:
793 except OSError:
791 pass
794 pass
792 self.wopener(filename, 'w').write(data)
795 self.wopener(filename, 'w').write(data)
793
796
794 if self.is_exec:
797 if self.is_exec:
795 was_exec = self.is_exec(self.wjoin(filename))
798 was_exec = self.is_exec(self.wjoin(filename))
796 else:
799 else:
797 # On filesystems not supporting execute-bit, there is no way
800 # On filesystems not supporting execute-bit, there is no way
798 # to know if it is set but asking subversion. Setting it
801 # to know if it is set but asking subversion. Setting it
799 # systematically is just as expensive and much simpler.
802 # systematically is just as expensive and much simpler.
800 was_exec = 'x' not in flags
803 was_exec = 'x' not in flags
801
804
802 util.set_flags(self.wjoin(filename), flags)
805 util.set_flags(self.wjoin(filename), flags)
803 if was_exec:
806 if was_exec:
804 if 'x' not in flags:
807 if 'x' not in flags:
805 self.delexec.append(filename)
808 self.delexec.append(filename)
806 else:
809 else:
807 if 'x' in flags:
810 if 'x' in flags:
808 self.setexec.append(filename)
811 self.setexec.append(filename)
809
812
810 def delfile(self, name):
813 def delfile(self, name):
811 self.delete.append(name)
814 self.delete.append(name)
812
815
813 def copyfile(self, source, dest):
816 def copyfile(self, source, dest):
814 self.copies.append([source, dest])
817 self.copies.append([source, dest])
815
818
816 def _copyfile(self, source, dest):
819 def _copyfile(self, source, dest):
817 # SVN's copy command pukes if the destination file exists, but
820 # SVN's copy command pukes if the destination file exists, but
818 # our copyfile method expects to record a copy that has
821 # our copyfile method expects to record a copy that has
819 # already occurred. Cross the semantic gap.
822 # already occurred. Cross the semantic gap.
820 wdest = self.wjoin(dest)
823 wdest = self.wjoin(dest)
821 exists = os.path.exists(wdest)
824 exists = os.path.exists(wdest)
822 if exists:
825 if exists:
823 fd, tempname = tempfile.mkstemp(
826 fd, tempname = tempfile.mkstemp(
824 prefix='hg-copy-', dir=os.path.dirname(wdest))
827 prefix='hg-copy-', dir=os.path.dirname(wdest))
825 os.close(fd)
828 os.close(fd)
826 os.unlink(tempname)
829 os.unlink(tempname)
827 os.rename(wdest, tempname)
830 os.rename(wdest, tempname)
828 try:
831 try:
829 self.run0('copy', source, dest)
832 self.run0('copy', source, dest)
830 finally:
833 finally:
831 if exists:
834 if exists:
832 try:
835 try:
833 os.unlink(wdest)
836 os.unlink(wdest)
834 except OSError:
837 except OSError:
835 pass
838 pass
836 os.rename(tempname, wdest)
839 os.rename(tempname, wdest)
837
840
838 def dirs_of(self, files):
841 def dirs_of(self, files):
839 dirs = set()
842 dirs = set()
840 for f in files:
843 for f in files:
841 if os.path.isdir(self.wjoin(f)):
844 if os.path.isdir(self.wjoin(f)):
842 dirs.add(f)
845 dirs.add(f)
843 for i in strutil.rfindall(f, '/'):
846 for i in strutil.rfindall(f, '/'):
844 dirs.add(f[:i])
847 dirs.add(f[:i])
845 return dirs
848 return dirs
846
849
847 def add_dirs(self, files):
850 def add_dirs(self, files):
848 add_dirs = [d for d in self.dirs_of(files)
851 add_dirs = [d for d in self.dirs_of(files)
849 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
852 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
850 if add_dirs:
853 if add_dirs:
851 add_dirs.sort()
854 add_dirs.sort()
852 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
855 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
853 return add_dirs
856 return add_dirs
854
857
855 def add_files(self, files):
858 def add_files(self, files):
856 if files:
859 if files:
857 self.xargs(files, 'add', quiet=True)
860 self.xargs(files, 'add', quiet=True)
858 return files
861 return files
859
862
860 def tidy_dirs(self, names):
863 def tidy_dirs(self, names):
861 dirs = list(self.dirs_of(names))
864 dirs = list(self.dirs_of(names))
862 dirs.sort(reverse=True)
865 dirs.sort(reverse=True)
863 deleted = []
866 deleted = []
864 for d in dirs:
867 for d in dirs:
865 wd = self.wjoin(d)
868 wd = self.wjoin(d)
866 if os.listdir(wd) == '.svn':
869 if os.listdir(wd) == '.svn':
867 self.run0('delete', d)
870 self.run0('delete', d)
868 deleted.append(d)
871 deleted.append(d)
869 return deleted
872 return deleted
870
873
871 def addchild(self, parent, child):
874 def addchild(self, parent, child):
872 self.childmap[parent] = child
875 self.childmap[parent] = child
873
876
874 def revid(self, rev):
877 def revid(self, rev):
875 return u"svn:%s@%s" % (self.uuid, rev)
878 return u"svn:%s@%s" % (self.uuid, rev)
876
879
877 def putcommit(self, files, parents, commit):
880 def putcommit(self, files, parents, commit):
878 for parent in parents:
881 for parent in parents:
879 try:
882 try:
880 return self.revid(self.childmap[parent])
883 return self.revid(self.childmap[parent])
881 except KeyError:
884 except KeyError:
882 pass
885 pass
883 entries = set(self.delete)
886 entries = set(self.delete)
884 files = util.frozenset(files)
887 files = util.frozenset(files)
885 entries.update(self.add_dirs(files.difference(entries)))
888 entries.update(self.add_dirs(files.difference(entries)))
886 if self.copies:
889 if self.copies:
887 for s, d in self.copies:
890 for s, d in self.copies:
888 self._copyfile(s, d)
891 self._copyfile(s, d)
889 self.copies = []
892 self.copies = []
890 if self.delete:
893 if self.delete:
891 self.xargs(self.delete, 'delete')
894 self.xargs(self.delete, 'delete')
892 self.delete = []
895 self.delete = []
893 entries.update(self.add_files(files.difference(entries)))
896 entries.update(self.add_files(files.difference(entries)))
894 entries.update(self.tidy_dirs(entries))
897 entries.update(self.tidy_dirs(entries))
895 if self.delexec:
898 if self.delexec:
896 self.xargs(self.delexec, 'propdel', 'svn:executable')
899 self.xargs(self.delexec, 'propdel', 'svn:executable')
897 self.delexec = []
900 self.delexec = []
898 if self.setexec:
901 if self.setexec:
899 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
902 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
900 self.setexec = []
903 self.setexec = []
901
904
902 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
905 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
903 fp = os.fdopen(fd, 'w')
906 fp = os.fdopen(fd, 'w')
904 fp.write(commit.desc)
907 fp.write(commit.desc)
905 fp.close()
908 fp.close()
906 try:
909 try:
907 output = self.run0('commit',
910 output = self.run0('commit',
908 username=util.shortuser(commit.author),
911 username=util.shortuser(commit.author),
909 file=messagefile,
912 file=messagefile,
910 encoding='utf-8')
913 encoding='utf-8')
911 try:
914 try:
912 rev = self.commit_re.search(output).group(1)
915 rev = self.commit_re.search(output).group(1)
913 except AttributeError:
916 except AttributeError:
914 self.ui.warn(_('unexpected svn output:\n'))
917 self.ui.warn(_('unexpected svn output:\n'))
915 self.ui.warn(output)
918 self.ui.warn(output)
916 raise util.Abort(_('unable to cope with svn output'))
919 raise util.Abort(_('unable to cope with svn output'))
917 if commit.rev:
920 if commit.rev:
918 self.run('propset', 'hg:convert-rev', commit.rev,
921 self.run('propset', 'hg:convert-rev', commit.rev,
919 revprop=True, revision=rev)
922 revprop=True, revision=rev)
920 if commit.branch and commit.branch != 'default':
923 if commit.branch and commit.branch != 'default':
921 self.run('propset', 'hg:convert-branch', commit.branch,
924 self.run('propset', 'hg:convert-branch', commit.branch,
922 revprop=True, revision=rev)
925 revprop=True, revision=rev)
923 for parent in parents:
926 for parent in parents:
924 self.addchild(parent, rev)
927 self.addchild(parent, rev)
925 return self.revid(rev)
928 return self.revid(rev)
926 finally:
929 finally:
927 os.unlink(messagefile)
930 os.unlink(messagefile)
928
931
929 def puttags(self, tags):
932 def puttags(self, tags):
930 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
933 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
General Comments 0
You need to be logged in to leave comments. Login now