##// END OF EJS Templates
convert: svn: hoist up branch creation check
Brendan Cully -
r5119:4cef0b93 default
parent child Browse files
Show More
@@ -1,632 +1,634 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 #
11 #
12 # Set these in a hgrc, or on the command line as follows:
12 # Set these in a hgrc, or on the command line as follows:
13 #
13 #
14 # hg convert --config convert.svn.trunk=wackoname [...]
14 # hg convert --config convert.svn.trunk=wackoname [...]
15
15
16 import locale
16 import locale
17 import os
17 import os
18 import cPickle as pickle
18 import cPickle as pickle
19 from mercurial import util
19 from mercurial import util
20
20
21 # Subversion stuff. Works best with very recent Python SVN bindings
21 # Subversion stuff. Works best with very recent Python SVN bindings
22 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
22 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
23 # these bindings.
23 # these bindings.
24
24
25 from cStringIO import StringIO
25 from cStringIO import StringIO
26
26
27 from common import NoRepo, commit, converter_source
27 from common import NoRepo, commit, converter_source
28
28
29 try:
29 try:
30 from svn.core import SubversionException, Pool
30 from svn.core import SubversionException, Pool
31 import svn
31 import svn
32 import svn.client
32 import svn.client
33 import svn.core
33 import svn.core
34 import svn.ra
34 import svn.ra
35 import svn.delta
35 import svn.delta
36 import transport
36 import transport
37 except ImportError:
37 except ImportError:
38 pass
38 pass
39
39
40 def geturl(path):
40 def geturl(path):
41 try:
41 try:
42 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
42 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
43 except SubversionException:
43 except SubversionException:
44 pass
44 pass
45 if os.path.isdir(path):
45 if os.path.isdir(path):
46 return 'file://%s' % os.path.normpath(os.path.abspath(path))
46 return 'file://%s' % os.path.normpath(os.path.abspath(path))
47 return path
47 return path
48
48
49 def optrev(number):
49 def optrev(number):
50 optrev = svn.core.svn_opt_revision_t()
50 optrev = svn.core.svn_opt_revision_t()
51 optrev.kind = svn.core.svn_opt_revision_number
51 optrev.kind = svn.core.svn_opt_revision_number
52 optrev.value.number = number
52 optrev.value.number = number
53 return optrev
53 return optrev
54
54
55 class changedpath(object):
55 class changedpath(object):
56 def __init__(self, p):
56 def __init__(self, p):
57 self.copyfrom_path = p.copyfrom_path
57 self.copyfrom_path = p.copyfrom_path
58 self.copyfrom_rev = p.copyfrom_rev
58 self.copyfrom_rev = p.copyfrom_rev
59 self.action = p.action
59 self.action = p.action
60
60
61 # SVN conversion code stolen from bzr-svn and tailor
61 # SVN conversion code stolen from bzr-svn and tailor
62 class convert_svn(converter_source):
62 class convert_svn(converter_source):
63 def __init__(self, ui, url, rev=None):
63 def __init__(self, ui, url, rev=None):
64 super(convert_svn, self).__init__(ui, url, rev=rev)
64 super(convert_svn, self).__init__(ui, url, rev=rev)
65
65
66 try:
66 try:
67 SubversionException
67 SubversionException
68 except NameError:
68 except NameError:
69 msg = 'subversion python bindings could not be loaded\n'
69 msg = 'subversion python bindings could not be loaded\n'
70 ui.warn(msg)
70 ui.warn(msg)
71 raise NoRepo(msg)
71 raise NoRepo(msg)
72
72
73 self.encoding = locale.getpreferredencoding()
73 self.encoding = locale.getpreferredencoding()
74 self.lastrevs = {}
74 self.lastrevs = {}
75
75
76 latest = None
76 latest = None
77 if rev:
77 if rev:
78 try:
78 try:
79 latest = int(rev)
79 latest = int(rev)
80 except ValueError:
80 except ValueError:
81 raise util.Abort('svn: revision %s is not an integer' % rev)
81 raise util.Abort('svn: revision %s is not an integer' % rev)
82 try:
82 try:
83 # Support file://path@rev syntax. Useful e.g. to convert
83 # Support file://path@rev syntax. Useful e.g. to convert
84 # deleted branches.
84 # deleted branches.
85 at = url.rfind('@')
85 at = url.rfind('@')
86 if at >= 0:
86 if at >= 0:
87 latest = int(url[at+1:])
87 latest = int(url[at+1:])
88 url = url[:at]
88 url = url[:at]
89 except ValueError, e:
89 except ValueError, e:
90 pass
90 pass
91 self.url = geturl(url)
91 self.url = geturl(url)
92 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
92 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
93 try:
93 try:
94 self.transport = transport.SvnRaTransport(url=self.url)
94 self.transport = transport.SvnRaTransport(url=self.url)
95 self.ra = self.transport.ra
95 self.ra = self.transport.ra
96 self.ctx = self.transport.client
96 self.ctx = self.transport.client
97 self.base = svn.ra.get_repos_root(self.ra)
97 self.base = svn.ra.get_repos_root(self.ra)
98 self.module = self.url[len(self.base):]
98 self.module = self.url[len(self.base):]
99 self.modulemap = {} # revision, module
99 self.modulemap = {} # revision, module
100 self.commits = {}
100 self.commits = {}
101 self.files = {}
101 self.files = {}
102 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
102 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
103 except SubversionException, e:
103 except SubversionException, e:
104 raise NoRepo("couldn't open SVN repo %s" % self.url)
104 raise NoRepo("couldn't open SVN repo %s" % self.url)
105
105
106 try:
106 try:
107 self.get_blacklist()
107 self.get_blacklist()
108 except IOError, e:
108 except IOError, e:
109 pass
109 pass
110
110
111 self.last_changed = self.latest(self.module, latest)
111 self.last_changed = self.latest(self.module, latest)
112
112
113 self.head = self.revid(self.last_changed)
113 self.head = self.revid(self.last_changed)
114
114
115 def setrevmap(self, revmap):
115 def setrevmap(self, revmap):
116 lastrevs = {}
116 lastrevs = {}
117 for revid in revmap.keys():
117 for revid in revmap.keys():
118 uuid, module, revnum = self.revsplit(revid)
118 uuid, module, revnum = self.revsplit(revid)
119 lastrevnum = lastrevs.setdefault(module, revnum)
119 lastrevnum = lastrevs.setdefault(module, revnum)
120 if revnum > lastrevnum:
120 if revnum > lastrevnum:
121 lastrevs[module] = revnum
121 lastrevs[module] = revnum
122 self.lastrevs = lastrevs
122 self.lastrevs = lastrevs
123
123
124 def exists(self, path, optrev):
124 def exists(self, path, optrev):
125 try:
125 try:
126 return svn.client.ls(self.url.rstrip('/') + '/' + path,
126 return svn.client.ls(self.url.rstrip('/') + '/' + path,
127 optrev, False, self.ctx)
127 optrev, False, self.ctx)
128 except SubversionException, err:
128 except SubversionException, err:
129 return []
129 return []
130
130
131 def getheads(self):
131 def getheads(self):
132 # detect standard /branches, /tags, /trunk layout
132 # detect standard /branches, /tags, /trunk layout
133 rev = optrev(self.last_changed)
133 rev = optrev(self.last_changed)
134 rpath = self.url.strip('/')
134 rpath = self.url.strip('/')
135 cfgtrunk = self.ui.config('convert', 'svn.trunk')
135 cfgtrunk = self.ui.config('convert', 'svn.trunk')
136 cfgbranches = self.ui.config('convert', 'svn.branches')
136 cfgbranches = self.ui.config('convert', 'svn.branches')
137 trunk = (cfgtrunk or 'trunk').strip('/')
137 trunk = (cfgtrunk or 'trunk').strip('/')
138 branches = (cfgbranches or 'branches').strip('/')
138 branches = (cfgbranches or 'branches').strip('/')
139 if self.exists(trunk, rev) and self.exists(branches, rev):
139 if self.exists(trunk, rev) and self.exists(branches, rev):
140 self.ui.note('found trunk at %r and branches at %r\n' %
140 self.ui.note('found trunk at %r and branches at %r\n' %
141 (trunk, branches))
141 (trunk, branches))
142 oldmodule = self.module
142 oldmodule = self.module
143 self.module += '/' + trunk
143 self.module += '/' + trunk
144 lt = self.latest(self.module, self.last_changed)
144 lt = self.latest(self.module, self.last_changed)
145 self.head = self.revid(lt)
145 self.head = self.revid(lt)
146 self.heads = [self.head]
146 self.heads = [self.head]
147 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
147 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
148 self.ctx)
148 self.ctx)
149 for branch in branchnames.keys():
149 for branch in branchnames.keys():
150 if oldmodule:
150 if oldmodule:
151 module = '/' + oldmodule + '/' + branches + '/' + branch
151 module = '/' + oldmodule + '/' + branches + '/' + branch
152 else:
152 else:
153 module = '/' + branches + '/' + branch
153 module = '/' + branches + '/' + branch
154 brevnum = self.latest(module, self.last_changed)
154 brevnum = self.latest(module, self.last_changed)
155 brev = self.revid(brevnum, module)
155 brev = self.revid(brevnum, module)
156 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
156 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
157 self.heads.append(brev)
157 self.heads.append(brev)
158 elif cfgtrunk or cfgbranches:
158 elif cfgtrunk or cfgbranches:
159 raise util.Abort(_('trunk/branch layout expected, '
159 raise util.Abort(_('trunk/branch layout expected, '
160 'but not found'))
160 'but not found'))
161 else:
161 else:
162 self.ui.note('working with one branch\n')
162 self.ui.note('working with one branch\n')
163 self.heads = [self.head]
163 self.heads = [self.head]
164 return self.heads
164 return self.heads
165
165
166 def getfile(self, file, rev):
166 def getfile(self, file, rev):
167 data, mode = self._getfile(file, rev)
167 data, mode = self._getfile(file, rev)
168 self.modecache[(file, rev)] = mode
168 self.modecache[(file, rev)] = mode
169 return data
169 return data
170
170
171 def getmode(self, file, rev):
171 def getmode(self, file, rev):
172 return self.modecache[(file, rev)]
172 return self.modecache[(file, rev)]
173
173
174 def getchanges(self, rev):
174 def getchanges(self, rev):
175 self.modecache = {}
175 self.modecache = {}
176 files = self.files[rev]
176 files = self.files[rev]
177 cl = files
177 cl = files
178 cl.sort()
178 cl.sort()
179 # caller caches the result, so free it here to release memory
179 # caller caches the result, so free it here to release memory
180 del self.files[rev]
180 del self.files[rev]
181 return cl
181 return cl
182
182
183 def getcommit(self, rev):
183 def getcommit(self, rev):
184 if rev not in self.commits:
184 if rev not in self.commits:
185 uuid, module, revnum = self.revsplit(rev)
185 uuid, module, revnum = self.revsplit(rev)
186 self.module = module
186 self.module = module
187 self.reparent(module)
187 self.reparent(module)
188 stop = self.lastrevs.get(module, 0)
188 stop = self.lastrevs.get(module, 0)
189 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
189 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
190 commit = self.commits[rev]
190 commit = self.commits[rev]
191 # caller caches the result, so free it here to release memory
191 # caller caches the result, so free it here to release memory
192 del self.commits[rev]
192 del self.commits[rev]
193 return commit
193 return commit
194
194
195 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
195 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
196 strict_node_history=False):
196 strict_node_history=False):
197 '''wrapper for svn.ra.get_log.
197 '''wrapper for svn.ra.get_log.
198 on a large repository, svn.ra.get_log pins huge amounts of
198 on a large repository, svn.ra.get_log pins huge amounts of
199 memory that cannot be recovered. work around it by forking
199 memory that cannot be recovered. work around it by forking
200 and writing results over a pipe.'''
200 and writing results over a pipe.'''
201
201
202 def child(fp):
202 def child(fp):
203 protocol = -1
203 protocol = -1
204 def receiver(orig_paths, revnum, author, date, message, pool):
204 def receiver(orig_paths, revnum, author, date, message, pool):
205 if orig_paths is not None:
205 if orig_paths is not None:
206 for k, v in orig_paths.iteritems():
206 for k, v in orig_paths.iteritems():
207 orig_paths[k] = changedpath(v)
207 orig_paths[k] = changedpath(v)
208 pickle.dump((orig_paths, revnum, author, date, message),
208 pickle.dump((orig_paths, revnum, author, date, message),
209 fp, protocol)
209 fp, protocol)
210
210
211 try:
211 try:
212 # Use an ra of our own so that our parent can consume
212 # Use an ra of our own so that our parent can consume
213 # our results without confusing the server.
213 # our results without confusing the server.
214 t = transport.SvnRaTransport(url=self.url)
214 t = transport.SvnRaTransport(url=self.url)
215 svn.ra.get_log(t.ra, paths, start, end, limit,
215 svn.ra.get_log(t.ra, paths, start, end, limit,
216 discover_changed_paths,
216 discover_changed_paths,
217 strict_node_history,
217 strict_node_history,
218 receiver)
218 receiver)
219 except SubversionException, (_, num):
219 except SubversionException, (_, num):
220 self.ui.print_exc()
220 self.ui.print_exc()
221 pickle.dump(num, fp, protocol)
221 pickle.dump(num, fp, protocol)
222 else:
222 else:
223 pickle.dump(None, fp, protocol)
223 pickle.dump(None, fp, protocol)
224 fp.close()
224 fp.close()
225
225
226 def parent(fp):
226 def parent(fp):
227 while True:
227 while True:
228 entry = pickle.load(fp)
228 entry = pickle.load(fp)
229 try:
229 try:
230 orig_paths, revnum, author, date, message = entry
230 orig_paths, revnum, author, date, message = entry
231 except:
231 except:
232 if entry is None:
232 if entry is None:
233 break
233 break
234 raise SubversionException("child raised exception", entry)
234 raise SubversionException("child raised exception", entry)
235 yield entry
235 yield entry
236
236
237 rfd, wfd = os.pipe()
237 rfd, wfd = os.pipe()
238 pid = os.fork()
238 pid = os.fork()
239 if pid:
239 if pid:
240 os.close(wfd)
240 os.close(wfd)
241 for p in parent(os.fdopen(rfd, 'rb')):
241 for p in parent(os.fdopen(rfd, 'rb')):
242 yield p
242 yield p
243 ret = os.waitpid(pid, 0)[1]
243 ret = os.waitpid(pid, 0)[1]
244 if ret:
244 if ret:
245 raise util.Abort(_('get_log %s') % util.explain_exit(ret))
245 raise util.Abort(_('get_log %s') % util.explain_exit(ret))
246 else:
246 else:
247 os.close(rfd)
247 os.close(rfd)
248 child(os.fdopen(wfd, 'wb'))
248 child(os.fdopen(wfd, 'wb'))
249 os._exit(0)
249 os._exit(0)
250
250
251 def gettags(self):
251 def gettags(self):
252 tags = {}
252 tags = {}
253 start = self.revnum(self.head)
253 start = self.revnum(self.head)
254 try:
254 try:
255 for entry in self.get_log(['/tags'], 0, start):
255 for entry in self.get_log(['/tags'], 0, start):
256 orig_paths, revnum, author, date, message = entry
256 orig_paths, revnum, author, date, message = entry
257 for path in orig_paths:
257 for path in orig_paths:
258 if not path.startswith('/tags/'):
258 if not path.startswith('/tags/'):
259 continue
259 continue
260 ent = orig_paths[path]
260 ent = orig_paths[path]
261 source = ent.copyfrom_path
261 source = ent.copyfrom_path
262 rev = ent.copyfrom_rev
262 rev = ent.copyfrom_rev
263 tag = path.split('/', 2)[2]
263 tag = path.split('/', 2)[2]
264 tags[tag] = self.revid(rev, module=source)
264 tags[tag] = self.revid(rev, module=source)
265 except SubversionException, (_, num):
265 except SubversionException, (_, num):
266 self.ui.note('no tags found at revision %d\n' % start)
266 self.ui.note('no tags found at revision %d\n' % start)
267 return tags
267 return tags
268
268
269 # -- helper functions --
269 # -- helper functions --
270
270
271 def revid(self, revnum, module=None):
271 def revid(self, revnum, module=None):
272 if not module:
272 if not module:
273 module = self.module
273 module = self.module
274 return (u"svn:%s%s@%s" % (self.uuid, module, revnum)).decode(self.encoding)
274 return (u"svn:%s%s@%s" % (self.uuid, module, revnum)).decode(self.encoding)
275
275
276 def revnum(self, rev):
276 def revnum(self, rev):
277 return int(rev.split('@')[-1])
277 return int(rev.split('@')[-1])
278
278
279 def revsplit(self, rev):
279 def revsplit(self, rev):
280 url, revnum = rev.encode(self.encoding).split('@', 1)
280 url, revnum = rev.encode(self.encoding).split('@', 1)
281 revnum = int(revnum)
281 revnum = int(revnum)
282 parts = url.split('/', 1)
282 parts = url.split('/', 1)
283 uuid = parts.pop(0)[4:]
283 uuid = parts.pop(0)[4:]
284 mod = ''
284 mod = ''
285 if parts:
285 if parts:
286 mod = '/' + parts[0]
286 mod = '/' + parts[0]
287 return uuid, mod, revnum
287 return uuid, mod, revnum
288
288
289 def latest(self, path, stop=0):
289 def latest(self, path, stop=0):
290 'find the latest revision affecting path, up to stop'
290 'find the latest revision affecting path, up to stop'
291 if not stop:
291 if not stop:
292 stop = svn.ra.get_latest_revnum(self.ra)
292 stop = svn.ra.get_latest_revnum(self.ra)
293 try:
293 try:
294 self.reparent('')
294 self.reparent('')
295 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
295 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
296 self.reparent(self.module)
296 self.reparent(self.module)
297 except SubversionException:
297 except SubversionException:
298 dirent = None
298 dirent = None
299 if not dirent:
299 if not dirent:
300 print self.base, path
300 print self.base, path
301 raise util.Abort('%s not found up to revision %d' % (path, stop))
301 raise util.Abort('%s not found up to revision %d' % (path, stop))
302
302
303 return dirent.created_rev
303 return dirent.created_rev
304
304
305 def get_blacklist(self):
305 def get_blacklist(self):
306 """Avoid certain revision numbers.
306 """Avoid certain revision numbers.
307 It is not uncommon for two nearby revisions to cancel each other
307 It is not uncommon for two nearby revisions to cancel each other
308 out, e.g. 'I copied trunk into a subdirectory of itself instead
308 out, e.g. 'I copied trunk into a subdirectory of itself instead
309 of making a branch'. The converted repository is significantly
309 of making a branch'. The converted repository is significantly
310 smaller if we ignore such revisions."""
310 smaller if we ignore such revisions."""
311 self.blacklist = set()
311 self.blacklist = set()
312 blacklist = self.blacklist
312 blacklist = self.blacklist
313 for line in file("blacklist.txt", "r"):
313 for line in file("blacklist.txt", "r"):
314 if not line.startswith("#"):
314 if not line.startswith("#"):
315 try:
315 try:
316 svn_rev = int(line.strip())
316 svn_rev = int(line.strip())
317 blacklist.add(svn_rev)
317 blacklist.add(svn_rev)
318 except ValueError, e:
318 except ValueError, e:
319 pass # not an integer or a comment
319 pass # not an integer or a comment
320
320
321 def is_blacklisted(self, svn_rev):
321 def is_blacklisted(self, svn_rev):
322 return svn_rev in self.blacklist
322 return svn_rev in self.blacklist
323
323
324 def reparent(self, module):
324 def reparent(self, module):
325 svn_url = self.base + module
325 svn_url = self.base + module
326 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
326 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
327 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
327 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
328
328
329 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
329 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
330 def get_entry_from_path(path, module=self.module):
330 def get_entry_from_path(path, module=self.module):
331 # Given the repository url of this wc, say
331 # Given the repository url of this wc, say
332 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
332 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
333 # extract the "entry" portion (a relative path) from what
333 # extract the "entry" portion (a relative path) from what
334 # svn log --xml says, ie
334 # svn log --xml says, ie
335 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
335 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
336 # that is to say "tests/PloneTestCase.py"
336 # that is to say "tests/PloneTestCase.py"
337
337
338 if path.startswith(module):
338 if path.startswith(module):
339 relative = path[len(module):]
339 relative = path[len(module):]
340 if relative.startswith('/'):
340 if relative.startswith('/'):
341 return relative[1:]
341 return relative[1:]
342 else:
342 else:
343 return relative
343 return relative
344
344
345 # The path is outside our tracked tree...
345 # The path is outside our tracked tree...
346 self.ui.debug('Ignoring %r since it is not under %r\n' % (path, module))
346 self.ui.debug('Ignoring %r since it is not under %r\n' % (path, module))
347 return None
347 return None
348
348
349 self.child_cset = None
349 self.child_cset = None
350 def parselogentry(orig_paths, revnum, author, date, message):
350 def parselogentry(orig_paths, revnum, author, date, message):
351 self.ui.debug("parsing revision %d (%d changes)\n" %
351 self.ui.debug("parsing revision %d (%d changes)\n" %
352 (revnum, len(orig_paths)))
352 (revnum, len(orig_paths)))
353
353
354 if revnum in self.modulemap:
354 if revnum in self.modulemap:
355 new_module = self.modulemap[revnum]
355 new_module = self.modulemap[revnum]
356 if new_module != self.module:
356 if new_module != self.module:
357 self.module = new_module
357 self.module = new_module
358 self.reparent(self.module)
358 self.reparent(self.module)
359
359
360 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
360 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
361 copies = {}
361 copies = {}
362 entries = []
362 entries = []
363 rev = self.revid(revnum)
363 rev = self.revid(revnum)
364 parents = []
364 parents = []
365
365
366 # branch log might return entries for a parent we already have
366 # branch log might return entries for a parent we already have
367 if (rev in self.commits or
367 if (rev in self.commits or
368 (revnum < self.lastrevs.get(self.module, 0))):
368 (revnum < self.lastrevs.get(self.module, 0))):
369 return
369 return
370
370
371 try:
371 try:
372 branch = self.module.split("/")[-1]
372 branch = self.module.split("/")[-1]
373 if branch == 'trunk':
373 if branch == 'trunk':
374 branch = ''
374 branch = ''
375 except IndexError:
375 except IndexError:
376 branch = None
376 branch = None
377
377
378 orig_paths = orig_paths.items()
378 orig_paths = orig_paths.items()
379 orig_paths.sort()
379 orig_paths.sort()
380
381 # check whether this revision is the start of a branch
382 path, ent = orig_paths and orig_paths[0] or (None, None)
383 if ent and path == self.module:
384 if ent.copyfrom_path:
385 # ent.copyfrom_rev may not be the actual last revision
386 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
387 self.modulemap[prev] = ent.copyfrom_path
388 parents = [self.revid(prev, ent.copyfrom_path)]
389 self.ui.note('found parent of branch %s at %d: %s\n' % \
390 (self.module, prev, ent.copyfrom_path))
391 else:
392 self.ui.debug("No copyfrom path, don't know what to do.\n")
393
380 for path, ent in orig_paths:
394 for path, ent in orig_paths:
381 # self.ui.write("path %s\n" % path)
395 # self.ui.write("path %s\n" % path)
382 if path == self.module: # Follow branching back in history
383 if ent:
384 if ent.copyfrom_path:
385 # ent.copyfrom_rev may not be the actual last revision
386 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
387 self.modulemap[prev] = ent.copyfrom_path
388 parents = [self.revid(prev, ent.copyfrom_path)]
389 self.ui.note('found parent of branch %s at %d: %s\n' % \
390 (self.module, prev, ent.copyfrom_path))
391 else:
392 self.ui.debug("No copyfrom path, don't know what to do.\n")
393 # Maybe it was added and there is no more history.
394 entrypath = get_entry_from_path(path, module=self.module)
396 entrypath = get_entry_from_path(path, module=self.module)
395 # self.ui.write("entrypath %s\n" % entrypath)
397 # self.ui.write("entrypath %s\n" % entrypath)
396 if entrypath is None:
398 if entrypath is None:
397 # Outside our area of interest
399 # Outside our area of interest
398 self.ui.debug("boring@%s: %s\n" % (revnum, path))
400 self.ui.debug("boring@%s: %s\n" % (revnum, path))
399 continue
401 continue
400 entry = entrypath.decode(self.encoding)
402 entry = entrypath.decode(self.encoding)
401
403
402 kind = svn.ra.check_path(self.ra, entrypath, revnum)
404 kind = svn.ra.check_path(self.ra, entrypath, revnum)
403 if kind == svn.core.svn_node_file:
405 if kind == svn.core.svn_node_file:
404 if ent.copyfrom_path:
406 if ent.copyfrom_path:
405 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
407 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
406 if copyfrom_path:
408 if copyfrom_path:
407 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
409 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
408 # It's probably important for hg that the source
410 # It's probably important for hg that the source
409 # exists in the revision's parent, not just the
411 # exists in the revision's parent, not just the
410 # ent.copyfrom_rev
412 # ent.copyfrom_rev
411 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
413 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
412 if fromkind != 0:
414 if fromkind != 0:
413 copies[self.recode(entry)] = self.recode(copyfrom_path)
415 copies[self.recode(entry)] = self.recode(copyfrom_path)
414 entries.append(self.recode(entry))
416 entries.append(self.recode(entry))
415 elif kind == 0: # gone, but had better be a deleted *file*
417 elif kind == 0: # gone, but had better be a deleted *file*
416 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
418 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
417
419
418 # if a branch is created but entries are removed in the same
420 # if a branch is created but entries are removed in the same
419 # changeset, get the right fromrev
421 # changeset, get the right fromrev
420 if parents:
422 if parents:
421 uuid, old_module, fromrev = self.revsplit(parents[0])
423 uuid, old_module, fromrev = self.revsplit(parents[0])
422 else:
424 else:
423 fromrev = revnum - 1
425 fromrev = revnum - 1
424 # might always need to be revnum - 1 in these 3 lines?
426 # might always need to be revnum - 1 in these 3 lines?
425 old_module = self.modulemap.get(fromrev, self.module)
427 old_module = self.modulemap.get(fromrev, self.module)
426
428
427 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
429 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
428 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
430 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
429
431
430 def lookup_parts(p):
432 def lookup_parts(p):
431 rc = None
433 rc = None
432 parts = p.split("/")
434 parts = p.split("/")
433 for i in range(len(parts)):
435 for i in range(len(parts)):
434 part = "/".join(parts[:i])
436 part = "/".join(parts[:i])
435 info = part, copyfrom.get(part, None)
437 info = part, copyfrom.get(part, None)
436 if info[1] is not None:
438 if info[1] is not None:
437 self.ui.debug("Found parent directory %s\n" % info[1])
439 self.ui.debug("Found parent directory %s\n" % info[1])
438 rc = info
440 rc = info
439 return rc
441 return rc
440
442
441 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
443 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
442
444
443 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
445 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
444
446
445 # need to remove fragment from lookup_parts and replace with copyfrom_path
447 # need to remove fragment from lookup_parts and replace with copyfrom_path
446 if frompath is not None:
448 if frompath is not None:
447 self.ui.debug("munge-o-matic\n")
449 self.ui.debug("munge-o-matic\n")
448 self.ui.debug(entrypath + '\n')
450 self.ui.debug(entrypath + '\n')
449 self.ui.debug(entrypath[len(frompath):] + '\n')
451 self.ui.debug(entrypath[len(frompath):] + '\n')
450 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
452 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
451 fromrev = froment.copyfrom_rev
453 fromrev = froment.copyfrom_rev
452 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
454 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
453
455
454 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
456 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
455 if fromkind == svn.core.svn_node_file: # a deleted file
457 if fromkind == svn.core.svn_node_file: # a deleted file
456 entries.append(self.recode(entry))
458 entries.append(self.recode(entry))
457 elif fromkind == svn.core.svn_node_dir:
459 elif fromkind == svn.core.svn_node_dir:
458 # print "Deleted/moved non-file:", revnum, path, ent
460 # print "Deleted/moved non-file:", revnum, path, ent
459 # children = self._find_children(path, revnum - 1)
461 # children = self._find_children(path, revnum - 1)
460 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
462 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
461 # Sometimes this is tricky. For example: in
463 # Sometimes this is tricky. For example: in
462 # The Subversion Repository revision 6940 a dir
464 # The Subversion Repository revision 6940 a dir
463 # was copied and one of its files was deleted
465 # was copied and one of its files was deleted
464 # from the new location in the same commit. This
466 # from the new location in the same commit. This
465 # code can't deal with that yet.
467 # code can't deal with that yet.
466 if ent.action == 'C':
468 if ent.action == 'C':
467 children = self._find_children(path, fromrev)
469 children = self._find_children(path, fromrev)
468 else:
470 else:
469 oroot = entrypath.strip('/')
471 oroot = entrypath.strip('/')
470 nroot = path.strip('/')
472 nroot = path.strip('/')
471 children = self._find_children(oroot, fromrev)
473 children = self._find_children(oroot, fromrev)
472 children = [s.replace(oroot,nroot) for s in children]
474 children = [s.replace(oroot,nroot) for s in children]
473 # Mark all [files, not directories] as deleted.
475 # Mark all [files, not directories] as deleted.
474 for child in children:
476 for child in children:
475 # Can we move a child directory and its
477 # Can we move a child directory and its
476 # parent in the same commit? (probably can). Could
478 # parent in the same commit? (probably can). Could
477 # cause problems if instead of revnum -1,
479 # cause problems if instead of revnum -1,
478 # we have to look in (copyfrom_path, revnum - 1)
480 # we have to look in (copyfrom_path, revnum - 1)
479 entrypath = get_entry_from_path("/" + child, module=old_module)
481 entrypath = get_entry_from_path("/" + child, module=old_module)
480 if entrypath:
482 if entrypath:
481 entry = self.recode(entrypath.decode(self.encoding))
483 entry = self.recode(entrypath.decode(self.encoding))
482 if entry in copies:
484 if entry in copies:
483 # deleted file within a copy
485 # deleted file within a copy
484 del copies[entry]
486 del copies[entry]
485 else:
487 else:
486 entries.append(entry)
488 entries.append(entry)
487 else:
489 else:
488 self.ui.debug('unknown path in revision %d: %s\n' % \
490 self.ui.debug('unknown path in revision %d: %s\n' % \
489 (revnum, path))
491 (revnum, path))
490 elif kind == svn.core.svn_node_dir:
492 elif kind == svn.core.svn_node_dir:
491 # Should probably synthesize normal file entries
493 # Should probably synthesize normal file entries
492 # and handle as above to clean up copy/rename handling.
494 # and handle as above to clean up copy/rename handling.
493
495
494 # If the directory just had a prop change,
496 # If the directory just had a prop change,
495 # then we shouldn't need to look for its children.
497 # then we shouldn't need to look for its children.
496 # Also this could create duplicate entries. Not sure
498 # Also this could create duplicate entries. Not sure
497 # whether this will matter. Maybe should make entries a set.
499 # whether this will matter. Maybe should make entries a set.
498 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
500 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
499 # This will fail if a directory was copied
501 # This will fail if a directory was copied
500 # from another branch and then some of its files
502 # from another branch and then some of its files
501 # were deleted in the same transaction.
503 # were deleted in the same transaction.
502 children = self._find_children(path, revnum)
504 children = self._find_children(path, revnum)
503 children.sort()
505 children.sort()
504 for child in children:
506 for child in children:
505 # Can we move a child directory and its
507 # Can we move a child directory and its
506 # parent in the same commit? (probably can). Could
508 # parent in the same commit? (probably can). Could
507 # cause problems if instead of revnum -1,
509 # cause problems if instead of revnum -1,
508 # we have to look in (copyfrom_path, revnum - 1)
510 # we have to look in (copyfrom_path, revnum - 1)
509 entrypath = get_entry_from_path("/" + child, module=self.module)
511 entrypath = get_entry_from_path("/" + child, module=self.module)
510 # print child, self.module, entrypath
512 # print child, self.module, entrypath
511 if entrypath:
513 if entrypath:
512 # Need to filter out directories here...
514 # Need to filter out directories here...
513 kind = svn.ra.check_path(self.ra, entrypath, revnum)
515 kind = svn.ra.check_path(self.ra, entrypath, revnum)
514 if kind != svn.core.svn_node_dir:
516 if kind != svn.core.svn_node_dir:
515 entries.append(self.recode(entrypath))
517 entries.append(self.recode(entrypath))
516
518
517 # Copies here (must copy all from source)
519 # Copies here (must copy all from source)
518 # Probably not a real problem for us if
520 # Probably not a real problem for us if
519 # source does not exist
521 # source does not exist
520
522
521 # Can do this with the copy command "hg copy"
523 # Can do this with the copy command "hg copy"
522 # if ent.copyfrom_path:
524 # if ent.copyfrom_path:
523 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
525 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
524 # module=self.module)
526 # module=self.module)
525 # copyto_entry = entrypath
527 # copyto_entry = entrypath
526 #
528 #
527 # print "copy directory", copyfrom_entry, 'to', copyto_entry
529 # print "copy directory", copyfrom_entry, 'to', copyto_entry
528 #
530 #
529 # copies.append((copyfrom_entry, copyto_entry))
531 # copies.append((copyfrom_entry, copyto_entry))
530
532
531 if ent.copyfrom_path:
533 if ent.copyfrom_path:
532 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
534 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
533 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
535 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
534 if copyfrom_entry:
536 if copyfrom_entry:
535 copyfrom[path] = ent
537 copyfrom[path] = ent
536 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
538 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
537
539
538 # Good, /probably/ a regular copy. Really should check
540 # Good, /probably/ a regular copy. Really should check
539 # to see whether the parent revision actually contains
541 # to see whether the parent revision actually contains
540 # the directory in question.
542 # the directory in question.
541 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
543 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
542 children.sort()
544 children.sort()
543 for child in children:
545 for child in children:
544 entrypath = get_entry_from_path("/" + child, module=self.module)
546 entrypath = get_entry_from_path("/" + child, module=self.module)
545 if entrypath:
547 if entrypath:
546 entry = entrypath.decode(self.encoding)
548 entry = entrypath.decode(self.encoding)
547 # print "COPY COPY From", copyfrom_entry, entry
549 # print "COPY COPY From", copyfrom_entry, entry
548 copyto_path = path + entry[len(copyfrom_entry):]
550 copyto_path = path + entry[len(copyfrom_entry):]
549 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
551 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
550 # print "COPY", entry, "COPY To", copyto_entry
552 # print "COPY", entry, "COPY To", copyto_entry
551 copies[self.recode(copyto_entry)] = self.recode(entry)
553 copies[self.recode(copyto_entry)] = self.recode(entry)
552 # copy from quux splort/quuxfile
554 # copy from quux splort/quuxfile
553
555
554 self.modulemap[revnum] = self.module # track backwards in time
556 self.modulemap[revnum] = self.module # track backwards in time
555 # a list of (filename, id) where id lets us retrieve the file.
557 # a list of (filename, id) where id lets us retrieve the file.
556 # eg in git, id is the object hash. for svn it'll be the
558 # eg in git, id is the object hash. for svn it'll be the
557 self.files[rev] = zip(entries, [rev] * len(entries))
559 self.files[rev] = zip(entries, [rev] * len(entries))
558 if not entries:
560 if not entries:
559 return
561 return
560
562
561 # Example SVN datetime. Includes microseconds.
563 # Example SVN datetime. Includes microseconds.
562 # ISO-8601 conformant
564 # ISO-8601 conformant
563 # '2007-01-04T17:35:00.902377Z'
565 # '2007-01-04T17:35:00.902377Z'
564 date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
566 date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
565
567
566 log = message and self.recode(message)
568 log = message and self.recode(message)
567 author = author and self.recode(author) or ''
569 author = author and self.recode(author) or ''
568
570
569 cset = commit(author=author,
571 cset = commit(author=author,
570 date=util.datestr(date),
572 date=util.datestr(date),
571 desc=log,
573 desc=log,
572 parents=parents,
574 parents=parents,
573 copies=copies,
575 copies=copies,
574 branch=branch,
576 branch=branch,
575 rev=rev.encode('utf-8'))
577 rev=rev.encode('utf-8'))
576
578
577 self.commits[rev] = cset
579 self.commits[rev] = cset
578 if self.child_cset and not self.child_cset.parents:
580 if self.child_cset and not self.child_cset.parents:
579 self.child_cset.parents = [rev]
581 self.child_cset.parents = [rev]
580 self.child_cset = cset
582 self.child_cset = cset
581
583
582 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
584 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
583 (self.module, from_revnum, to_revnum))
585 (self.module, from_revnum, to_revnum))
584
586
585 try:
587 try:
586 for entry in self.get_log([self.module], from_revnum, to_revnum):
588 for entry in self.get_log([self.module], from_revnum, to_revnum):
587 orig_paths, revnum, author, date, message = entry
589 orig_paths, revnum, author, date, message = entry
588 if self.is_blacklisted(revnum):
590 if self.is_blacklisted(revnum):
589 self.ui.note('skipping blacklisted revision %d\n' % revnum)
591 self.ui.note('skipping blacklisted revision %d\n' % revnum)
590 continue
592 continue
591 if orig_paths is None:
593 if orig_paths is None:
592 self.ui.debug('revision %d has no entries\n' % revnum)
594 self.ui.debug('revision %d has no entries\n' % revnum)
593 continue
595 continue
594 parselogentry(orig_paths, revnum, author, date, message)
596 parselogentry(orig_paths, revnum, author, date, message)
595 except SubversionException, (_, num):
597 except SubversionException, (_, num):
596 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
598 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
597 raise NoSuchRevision(branch=self,
599 raise NoSuchRevision(branch=self,
598 revision="Revision number %d" % to_revnum)
600 revision="Revision number %d" % to_revnum)
599 raise
601 raise
600
602
601 def _getfile(self, file, rev):
603 def _getfile(self, file, rev):
602 io = StringIO()
604 io = StringIO()
603 # TODO: ra.get_file transmits the whole file instead of diffs.
605 # TODO: ra.get_file transmits the whole file instead of diffs.
604 mode = ''
606 mode = ''
605 try:
607 try:
606 revnum = self.revnum(rev)
608 revnum = self.revnum(rev)
607 if self.module != self.modulemap[revnum]:
609 if self.module != self.modulemap[revnum]:
608 self.module = self.modulemap[revnum]
610 self.module = self.modulemap[revnum]
609 self.reparent(self.module)
611 self.reparent(self.module)
610 info = svn.ra.get_file(self.ra, file, revnum, io)
612 info = svn.ra.get_file(self.ra, file, revnum, io)
611 if isinstance(info, list):
613 if isinstance(info, list):
612 info = info[-1]
614 info = info[-1]
613 mode = ("svn:executable" in info) and 'x' or ''
615 mode = ("svn:executable" in info) and 'x' or ''
614 mode = ("svn:special" in info) and 'l' or mode
616 mode = ("svn:special" in info) and 'l' or mode
615 except SubversionException, e:
617 except SubversionException, e:
616 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
618 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
617 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
619 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
618 if e.apr_err in notfound: # File not found
620 if e.apr_err in notfound: # File not found
619 raise IOError()
621 raise IOError()
620 raise
622 raise
621 data = io.getvalue()
623 data = io.getvalue()
622 if mode == 'l':
624 if mode == 'l':
623 link_prefix = "link "
625 link_prefix = "link "
624 if data.startswith(link_prefix):
626 if data.startswith(link_prefix):
625 data = data[len(link_prefix):]
627 data = data[len(link_prefix):]
626 return data, mode
628 return data, mode
627
629
628 def _find_children(self, path, revnum):
630 def _find_children(self, path, revnum):
629 path = path.strip('/')
631 path = path.strip('/')
630 pool = Pool()
632 pool = Pool()
631 rpath = '/'.join([self.base, path]).strip('/')
633 rpath = '/'.join([self.base, path]).strip('/')
632 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
634 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
General Comments 0
You need to be logged in to leave comments. Login now