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