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