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