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