##// END OF EJS Templates
convert/svn: remove useless encoding/decoding calls (issue1676)
Patrick Mezard -
r8882:48a04961 default
parent child Browse files
Show More
@@ -0,0 +1,180 b''
1 SVN-fs-dump-format-version: 2
2
3 UUID: 64cf20ed-79ff-4f10-8aaf-041d1993b42e
4
5 Revision-number: 0
6 Prop-content-length: 56
7 Content-length: 56
8
9 K 8
10 svn:date
11 V 27
12 2009-06-21T10:05:28.031033Z
13 PROPS-END
14
15 Revision-number: 1
16 Prop-content-length: 112
17 Content-length: 112
18
19 K 7
20 svn:log
21 V 10
22 init projA
23 K 10
24 svn:author
25 V 7
26 pmezard
27 K 8
28 svn:date
29 V 27
30 2009-06-21T10:05:28.108227Z
31 PROPS-END
32
33 Node-path: branches
34 Node-kind: dir
35 Node-action: add
36 Prop-content-length: 10
37 Content-length: 10
38
39 PROPS-END
40
41
42 Node-path: tags
43 Node-kind: dir
44 Node-action: add
45 Prop-content-length: 10
46 Content-length: 10
47
48 PROPS-END
49
50
51 Node-path: trunk
52 Node-kind: dir
53 Node-action: add
54 Prop-content-length: 10
55 Content-length: 10
56
57 PROPS-END
58
59
60 Revision-number: 2
61 Prop-content-length: 106
62 Content-length: 106
63
64 K 7
65 svn:log
66 V 5
67 hello
68 K 10
69 svn:author
70 V 7
71 pmezard
72 K 8
73 svn:date
74 V 27
75 2009-06-21T10:05:29.109026Z
76 PROPS-END
77
78 Node-path: trunk/Γ 
79 Node-kind: dir
80 Node-action: add
81 Prop-content-length: 10
82 Content-length: 10
83
84 PROPS-END
85
86
87 Node-path: trunk/à/é
88 Node-kind: file
89 Node-action: add
90 Prop-content-length: 10
91 Text-content-length: 2
92 Text-content-md5: e29311f6f1bf1af907f9ef9f44b8328b
93 Text-content-sha1: e983f374794de9c64e3d1c1de1d490c0756eeeff
94 Content-length: 12
95
96 PROPS-END
97 d
98
99
100 Node-path: trunk/Γ©
101 Node-kind: file
102 Node-action: add
103 Prop-content-length: 10
104 Text-content-length: 2
105 Text-content-md5: 9ffbf43126e33be52cd2bf7e01d627f9
106 Text-content-sha1: 094e3afb2fe8dfe82f63731cdcd3b999f4856cff
107 Content-length: 12
108
109 PROPS-END
110 e
111
112
113 Revision-number: 3
114 Prop-content-length: 112
115 Content-length: 112
116
117 K 7
118 svn:log
119 V 10
120 copy files
121 K 10
122 svn:author
123 V 7
124 pmezard
125 K 8
126 svn:date
127 V 27
128 2009-06-21T10:05:32.061964Z
129 PROPS-END
130
131 Node-path: trunk/Γ¨
132 Node-kind: file
133 Node-action: add
134 Node-copyfrom-rev: 2
135 Node-copyfrom-path: trunk/Γ©
136 Text-copy-source-md5: 9ffbf43126e33be52cd2bf7e01d627f9
137 Text-copy-source-sha1: 094e3afb2fe8dfe82f63731cdcd3b999f4856cff
138
139
140 Node-path: trunk/ΓΉ
141 Node-kind: dir
142 Node-action: add
143 Node-copyfrom-rev: 2
144 Node-copyfrom-path: trunk/Γ 
145
146
147 Node-path: trunk/Γ 
148 Node-action: delete
149
150
151 Node-path: trunk/Γ©
152 Node-action: delete
153
154
155 Revision-number: 4
156 Prop-content-length: 114
157 Content-length: 114
158
159 K 7
160 svn:log
161 V 12
162 remove files
163 K 10
164 svn:author
165 V 7
166 pmezard
167 K 8
168 svn:date
169 V 27
170 2009-06-21T10:05:33.096353Z
171 PROPS-END
172
173 Node-path: trunk/Γ¨
174 Node-action: delete
175
176
177 Node-path: trunk/ΓΉ
178 Node-action: delete
179
180
@@ -0,0 +1,40 b''
1 # -*- coding: utf-8 -*-
2 #!/bin/sh
3 #
4 # Use this script to generate encoding.svndump
5 #
6
7 mkdir temp
8 cd temp
9
10 mkdir project-orig
11 cd project-orig
12 mkdir trunk
13 mkdir branches
14 mkdir tags
15 cd ..
16
17 svnadmin create svn-repo
18 svnurl=file://`pwd`/svn-repo
19 svn import project-orig $svnurl -m "init projA"
20
21 svn co $svnurl project
22 cd project
23 echo e > trunk/Γ©
24 mkdir trunk/Γ 
25 echo d > trunk/Γ /Γ©
26 svn add trunk/Γ© trunk/Γ 
27 svn ci -m hello
28
29 # Copy files and directories
30 svn mv trunk/Γ© trunk/Γ¨
31 svn mv trunk/Γ  trunk/ΓΉ
32 svn ci -m "copy files"
33
34 # Remove files
35 svn rm trunk/Γ¨
36 svn rm trunk/ΓΉ
37 svn ci -m 'remove files'
38 cd ..
39
40 svnadmin dump svn-repo > ../encoding.svndump
@@ -0,0 +1,13 b''
1 #!/bin/sh
2
3 "$TESTDIR/hghave" svn svn-bindings || exit 80
4
5 echo "[extensions]" >> $HGRCPATH
6 echo "convert = " >> $HGRCPATH
7
8 svnadmin create svn-repo
9 cat "$TESTDIR/svn/encoding.svndump" | svnadmin load svn-repo > /dev/null
10
11 echo '% convert while testing all possible outputs'
12 hg --debug convert svn-repo A-hg > /dev/null
13
@@ -0,0 +1,1 b''
1 % convert while testing all possible outputs
@@ -1,1211 +1,1209 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 import locale
5 import locale
6 import os
6 import os
7 import re
7 import re
8 import sys
8 import sys
9 import cPickle as pickle
9 import cPickle as pickle
10 import tempfile
10 import tempfile
11 import urllib
11 import urllib
12
12
13 from mercurial import strutil, util
13 from mercurial import strutil, util
14 from mercurial.i18n import _
14 from mercurial.i18n import _
15
15
16 # Subversion stuff. Works best with very recent Python SVN bindings
16 # Subversion stuff. Works best with very recent Python SVN bindings
17 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
17 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
18 # these bindings.
18 # these bindings.
19
19
20 from cStringIO import StringIO
20 from cStringIO import StringIO
21
21
22 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
22 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
23 from common import commandline, converter_source, converter_sink, mapfile
23 from common import commandline, converter_source, converter_sink, mapfile
24
24
25 try:
25 try:
26 from svn.core import SubversionException, Pool
26 from svn.core import SubversionException, Pool
27 import svn
27 import svn
28 import svn.client
28 import svn.client
29 import svn.core
29 import svn.core
30 import svn.ra
30 import svn.ra
31 import svn.delta
31 import svn.delta
32 import transport
32 import transport
33 import warnings
33 import warnings
34 warnings.filterwarnings('ignore',
34 warnings.filterwarnings('ignore',
35 module='svn.core',
35 module='svn.core',
36 category=DeprecationWarning)
36 category=DeprecationWarning)
37
37
38 except ImportError:
38 except ImportError:
39 pass
39 pass
40
40
41 class SvnPathNotFound(Exception):
41 class SvnPathNotFound(Exception):
42 pass
42 pass
43
43
44 def geturl(path):
44 def geturl(path):
45 try:
45 try:
46 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
46 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
47 except SubversionException:
47 except SubversionException:
48 pass
48 pass
49 if os.path.isdir(path):
49 if os.path.isdir(path):
50 path = os.path.normpath(os.path.abspath(path))
50 path = os.path.normpath(os.path.abspath(path))
51 if os.name == 'nt':
51 if os.name == 'nt':
52 path = '/' + util.normpath(path)
52 path = '/' + util.normpath(path)
53 return 'file://%s' % urllib.quote(path)
53 return 'file://%s' % urllib.quote(path)
54 return path
54 return path
55
55
56 def optrev(number):
56 def optrev(number):
57 optrev = svn.core.svn_opt_revision_t()
57 optrev = svn.core.svn_opt_revision_t()
58 optrev.kind = svn.core.svn_opt_revision_number
58 optrev.kind = svn.core.svn_opt_revision_number
59 optrev.value.number = number
59 optrev.value.number = number
60 return optrev
60 return optrev
61
61
62 class changedpath(object):
62 class changedpath(object):
63 def __init__(self, p):
63 def __init__(self, p):
64 self.copyfrom_path = p.copyfrom_path
64 self.copyfrom_path = p.copyfrom_path
65 self.copyfrom_rev = p.copyfrom_rev
65 self.copyfrom_rev = p.copyfrom_rev
66 self.action = p.action
66 self.action = p.action
67
67
68 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
68 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
69 strict_node_history=False):
69 strict_node_history=False):
70 protocol = -1
70 protocol = -1
71 def receiver(orig_paths, revnum, author, date, message, pool):
71 def receiver(orig_paths, revnum, author, date, message, pool):
72 if orig_paths is not None:
72 if orig_paths is not None:
73 for k, v in orig_paths.iteritems():
73 for k, v in orig_paths.iteritems():
74 orig_paths[k] = changedpath(v)
74 orig_paths[k] = changedpath(v)
75 pickle.dump((orig_paths, revnum, author, date, message),
75 pickle.dump((orig_paths, revnum, author, date, message),
76 fp, protocol)
76 fp, protocol)
77
77
78 try:
78 try:
79 # Use an ra of our own so that our parent can consume
79 # Use an ra of our own so that our parent can consume
80 # our results without confusing the server.
80 # our results without confusing the server.
81 t = transport.SvnRaTransport(url=url)
81 t = transport.SvnRaTransport(url=url)
82 svn.ra.get_log(t.ra, paths, start, end, limit,
82 svn.ra.get_log(t.ra, paths, start, end, limit,
83 discover_changed_paths,
83 discover_changed_paths,
84 strict_node_history,
84 strict_node_history,
85 receiver)
85 receiver)
86 except SubversionException, (inst, num):
86 except SubversionException, (inst, num):
87 pickle.dump(num, fp, protocol)
87 pickle.dump(num, fp, protocol)
88 except IOError:
88 except IOError:
89 # Caller may interrupt the iteration
89 # Caller may interrupt the iteration
90 pickle.dump(None, fp, protocol)
90 pickle.dump(None, fp, protocol)
91 else:
91 else:
92 pickle.dump(None, fp, protocol)
92 pickle.dump(None, fp, protocol)
93 fp.close()
93 fp.close()
94 # With large history, cleanup process goes crazy and suddenly
94 # With large history, cleanup process goes crazy and suddenly
95 # consumes *huge* amount of memory. The output file being closed,
95 # consumes *huge* amount of memory. The output file being closed,
96 # there is no need for clean termination.
96 # there is no need for clean termination.
97 os._exit(0)
97 os._exit(0)
98
98
99 def debugsvnlog(ui, **opts):
99 def debugsvnlog(ui, **opts):
100 """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
101 avoid memory collection issues.
101 avoid memory collection issues.
102 """
102 """
103 util.set_binary(sys.stdin)
103 util.set_binary(sys.stdin)
104 util.set_binary(sys.stdout)
104 util.set_binary(sys.stdout)
105 args = decodeargs(sys.stdin.read())
105 args = decodeargs(sys.stdin.read())
106 get_log_child(sys.stdout, *args)
106 get_log_child(sys.stdout, *args)
107
107
108 class logstream(object):
108 class logstream(object):
109 """Interruptible revision log iterator."""
109 """Interruptible revision log iterator."""
110 def __init__(self, stdout):
110 def __init__(self, stdout):
111 self._stdout = stdout
111 self._stdout = stdout
112
112
113 def __iter__(self):
113 def __iter__(self):
114 while True:
114 while True:
115 entry = pickle.load(self._stdout)
115 entry = pickle.load(self._stdout)
116 try:
116 try:
117 orig_paths, revnum, author, date, message = entry
117 orig_paths, revnum, author, date, message = entry
118 except:
118 except:
119 if entry is None:
119 if entry is None:
120 break
120 break
121 raise SubversionException("child raised exception", entry)
121 raise SubversionException("child raised exception", entry)
122 yield entry
122 yield entry
123
123
124 def close(self):
124 def close(self):
125 if self._stdout:
125 if self._stdout:
126 self._stdout.close()
126 self._stdout.close()
127 self._stdout = None
127 self._stdout = None
128
128
129
129
130 # Check to see if the given path is a local Subversion repo. Verify this by
130 # Check to see if the given path is a local Subversion repo. Verify this by
131 # looking for several svn-specific files and directories in the given
131 # looking for several svn-specific files and directories in the given
132 # directory.
132 # directory.
133 def filecheck(path, proto):
133 def filecheck(path, proto):
134 for x in ('locks', 'hooks', 'format', 'db', ):
134 for x in ('locks', 'hooks', 'format', 'db', ):
135 if not os.path.exists(os.path.join(path, x)):
135 if not os.path.exists(os.path.join(path, x)):
136 return False
136 return False
137 return True
137 return True
138
138
139 # Check to see if a given path is the root of an svn repo over http. We verify
139 # Check to see if a given path is the root of an svn repo over http. We verify
140 # this by requesting a version-controlled URL we know can't exist and looking
140 # this by requesting a version-controlled URL we know can't exist and looking
141 # for the svn-specific "not found" XML.
141 # for the svn-specific "not found" XML.
142 def httpcheck(path, proto):
142 def httpcheck(path, proto):
143 return ('<m:human-readable errcode="160013">' in
143 return ('<m:human-readable errcode="160013">' in
144 urllib.urlopen('%s://%s/!svn/ver/0/.svn' % (proto, path)).read())
144 urllib.urlopen('%s://%s/!svn/ver/0/.svn' % (proto, path)).read())
145
145
146 protomap = {'http': httpcheck,
146 protomap = {'http': httpcheck,
147 'https': httpcheck,
147 'https': httpcheck,
148 'file': filecheck,
148 'file': filecheck,
149 }
149 }
150 def issvnurl(url):
150 def issvnurl(url):
151 try:
151 try:
152 proto, path = url.split('://', 1)
152 proto, path = url.split('://', 1)
153 path = urllib.url2pathname(path)
153 path = urllib.url2pathname(path)
154 except ValueError:
154 except ValueError:
155 proto = 'file'
155 proto = 'file'
156 path = os.path.abspath(url)
156 path = os.path.abspath(url)
157 path = path.replace(os.sep, '/')
157 path = path.replace(os.sep, '/')
158 check = protomap.get(proto, lambda p, p2: False)
158 check = protomap.get(proto, lambda p, p2: False)
159 while '/' in path:
159 while '/' in path:
160 if check(path, proto):
160 if check(path, proto):
161 return True
161 return True
162 path = path.rsplit('/', 1)[0]
162 path = path.rsplit('/', 1)[0]
163 return False
163 return False
164
164
165 # SVN conversion code stolen from bzr-svn and tailor
165 # SVN conversion code stolen from bzr-svn and tailor
166 #
166 #
167 # Subversion looks like a versioned filesystem, branches structures
167 # Subversion looks like a versioned filesystem, branches structures
168 # are defined by conventions and not enforced by the tool. First,
168 # are defined by conventions and not enforced by the tool. First,
169 # we define the potential branches (modules) as "trunk" and "branches"
169 # we define the potential branches (modules) as "trunk" and "branches"
170 # children directories. Revisions are then identified by their
170 # children directories. Revisions are then identified by their
171 # module and revision number (and a repository identifier).
171 # module and revision number (and a repository identifier).
172 #
172 #
173 # The revision graph is really a tree (or a forest). By default, a
173 # The revision graph is really a tree (or a forest). By default, a
174 # revision parent is the previous revision in the same module. If the
174 # revision parent is the previous revision in the same module. If the
175 # module directory is copied/moved from another module then the
175 # module directory is copied/moved from another module then the
176 # revision is the module root and its parent the source revision in
176 # revision is the module root and its parent the source revision in
177 # the parent module. A revision has at most one parent.
177 # the parent module. A revision has at most one parent.
178 #
178 #
179 class svn_source(converter_source):
179 class svn_source(converter_source):
180 def __init__(self, ui, url, rev=None):
180 def __init__(self, ui, url, rev=None):
181 super(svn_source, self).__init__(ui, url, rev=rev)
181 super(svn_source, self).__init__(ui, url, rev=rev)
182
182
183 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
183 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
184 (os.path.exists(url) and
184 (os.path.exists(url) and
185 os.path.exists(os.path.join(url, '.svn'))) or
185 os.path.exists(os.path.join(url, '.svn'))) or
186 issvnurl(url)):
186 issvnurl(url)):
187 raise NoRepo("%s does not look like a Subversion repo" % url)
187 raise NoRepo("%s does not look like a Subversion repo" % url)
188
188
189 try:
189 try:
190 SubversionException
190 SubversionException
191 except NameError:
191 except NameError:
192 raise MissingTool(_('Subversion python bindings could not be loaded'))
192 raise MissingTool(_('Subversion python bindings could not be loaded'))
193
193
194 try:
194 try:
195 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
195 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
196 if version < (1, 4):
196 if version < (1, 4):
197 raise MissingTool(_('Subversion python bindings %d.%d found, '
197 raise MissingTool(_('Subversion python bindings %d.%d found, '
198 '1.4 or later required') % version)
198 '1.4 or later required') % version)
199 except AttributeError:
199 except AttributeError:
200 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
200 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
201 'or later required'))
201 'or later required'))
202
202
203 self.lastrevs = {}
203 self.lastrevs = {}
204
204
205 latest = None
205 latest = None
206 try:
206 try:
207 # Support file://path@rev syntax. Useful e.g. to convert
207 # Support file://path@rev syntax. Useful e.g. to convert
208 # deleted branches.
208 # deleted branches.
209 at = url.rfind('@')
209 at = url.rfind('@')
210 if at >= 0:
210 if at >= 0:
211 latest = int(url[at+1:])
211 latest = int(url[at+1:])
212 url = url[:at]
212 url = url[:at]
213 except ValueError:
213 except ValueError:
214 pass
214 pass
215 self.url = geturl(url)
215 self.url = geturl(url)
216 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
216 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
217 try:
217 try:
218 self.transport = transport.SvnRaTransport(url=self.url)
218 self.transport = transport.SvnRaTransport(url=self.url)
219 self.ra = self.transport.ra
219 self.ra = self.transport.ra
220 self.ctx = self.transport.client
220 self.ctx = self.transport.client
221 self.baseurl = svn.ra.get_repos_root(self.ra)
221 self.baseurl = svn.ra.get_repos_root(self.ra)
222 # Module is either empty or a repository path starting with
222 # Module is either empty or a repository path starting with
223 # a slash and not ending with a slash.
223 # a slash and not ending with a slash.
224 self.module = urllib.unquote(self.url[len(self.baseurl):])
224 self.module = urllib.unquote(self.url[len(self.baseurl):])
225 self.prevmodule = None
225 self.prevmodule = None
226 self.rootmodule = self.module
226 self.rootmodule = self.module
227 self.commits = {}
227 self.commits = {}
228 self.paths = {}
228 self.paths = {}
229 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
229 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
230 except SubversionException:
230 except SubversionException:
231 ui.traceback()
231 ui.traceback()
232 raise NoRepo("%s does not look like a Subversion repo" % self.url)
232 raise NoRepo("%s does not look like a Subversion repo" % self.url)
233
233
234 if rev:
234 if rev:
235 try:
235 try:
236 latest = int(rev)
236 latest = int(rev)
237 except ValueError:
237 except ValueError:
238 raise util.Abort(_('svn: revision %s is not an integer') % rev)
238 raise util.Abort(_('svn: revision %s is not an integer') % rev)
239
239
240 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
240 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
241 try:
241 try:
242 self.startrev = int(self.startrev)
242 self.startrev = int(self.startrev)
243 if self.startrev < 0:
243 if self.startrev < 0:
244 self.startrev = 0
244 self.startrev = 0
245 except ValueError:
245 except ValueError:
246 raise util.Abort(_('svn: start revision %s is not an integer')
246 raise util.Abort(_('svn: start revision %s is not an integer')
247 % self.startrev)
247 % self.startrev)
248
248
249 try:
249 try:
250 self.get_blacklist()
250 self.get_blacklist()
251 except IOError:
251 except IOError:
252 pass
252 pass
253
253
254 self.head = self.latest(self.module, latest)
254 self.head = self.latest(self.module, latest)
255 if not self.head:
255 if not self.head:
256 raise util.Abort(_('no revision found in module %s') %
256 raise util.Abort(_('no revision found in module %s') %
257 self.module.encode(self.encoding))
257 self.module.encode(self.encoding))
258 self.last_changed = self.revnum(self.head)
258 self.last_changed = self.revnum(self.head)
259
259
260 self._changescache = None
260 self._changescache = None
261
261
262 if os.path.exists(os.path.join(url, '.svn/entries')):
262 if os.path.exists(os.path.join(url, '.svn/entries')):
263 self.wc = url
263 self.wc = url
264 else:
264 else:
265 self.wc = None
265 self.wc = None
266 self.convertfp = None
266 self.convertfp = None
267
267
268 def setrevmap(self, revmap):
268 def setrevmap(self, revmap):
269 lastrevs = {}
269 lastrevs = {}
270 for revid in revmap.iterkeys():
270 for revid in revmap.iterkeys():
271 uuid, module, revnum = self.revsplit(revid)
271 uuid, module, revnum = self.revsplit(revid)
272 lastrevnum = lastrevs.setdefault(module, revnum)
272 lastrevnum = lastrevs.setdefault(module, revnum)
273 if revnum > lastrevnum:
273 if revnum > lastrevnum:
274 lastrevs[module] = revnum
274 lastrevs[module] = revnum
275 self.lastrevs = lastrevs
275 self.lastrevs = lastrevs
276
276
277 def exists(self, path, optrev):
277 def exists(self, path, optrev):
278 try:
278 try:
279 svn.client.ls(self.url.rstrip('/') + '/' + urllib.quote(path),
279 svn.client.ls(self.url.rstrip('/') + '/' + urllib.quote(path),
280 optrev, False, self.ctx)
280 optrev, False, self.ctx)
281 return True
281 return True
282 except SubversionException:
282 except SubversionException:
283 return False
283 return False
284
284
285 def getheads(self):
285 def getheads(self):
286
286
287 def isdir(path, revnum):
287 def isdir(path, revnum):
288 kind = self._checkpath(path, revnum)
288 kind = self._checkpath(path, revnum)
289 return kind == svn.core.svn_node_dir
289 return kind == svn.core.svn_node_dir
290
290
291 def getcfgpath(name, rev):
291 def getcfgpath(name, rev):
292 cfgpath = self.ui.config('convert', 'svn.' + name)
292 cfgpath = self.ui.config('convert', 'svn.' + name)
293 if cfgpath is not None and cfgpath.strip() == '':
293 if cfgpath is not None and cfgpath.strip() == '':
294 return None
294 return None
295 path = (cfgpath or name).strip('/')
295 path = (cfgpath or name).strip('/')
296 if not self.exists(path, rev):
296 if not self.exists(path, rev):
297 if cfgpath:
297 if cfgpath:
298 raise util.Abort(_('expected %s to be at %r, but not found')
298 raise util.Abort(_('expected %s to be at %r, but not found')
299 % (name, path))
299 % (name, path))
300 return None
300 return None
301 self.ui.note(_('found %s at %r\n') % (name, path))
301 self.ui.note(_('found %s at %r\n') % (name, path))
302 return path
302 return path
303
303
304 rev = optrev(self.last_changed)
304 rev = optrev(self.last_changed)
305 oldmodule = ''
305 oldmodule = ''
306 trunk = getcfgpath('trunk', rev)
306 trunk = getcfgpath('trunk', rev)
307 self.tags = getcfgpath('tags', rev)
307 self.tags = getcfgpath('tags', rev)
308 branches = getcfgpath('branches', rev)
308 branches = getcfgpath('branches', rev)
309
309
310 # If the project has a trunk or branches, we will extract heads
310 # If the project has a trunk or branches, we will extract heads
311 # from them. We keep the project root otherwise.
311 # from them. We keep the project root otherwise.
312 if trunk:
312 if trunk:
313 oldmodule = self.module or ''
313 oldmodule = self.module or ''
314 self.module += '/' + trunk
314 self.module += '/' + trunk
315 self.head = self.latest(self.module, self.last_changed)
315 self.head = self.latest(self.module, self.last_changed)
316 if not self.head:
316 if not self.head:
317 raise util.Abort(_('no revision found in module %s') %
317 raise util.Abort(_('no revision found in module %s') %
318 self.module.encode(self.encoding))
318 self.module.encode(self.encoding))
319
319
320 # First head in the list is the module's head
320 # First head in the list is the module's head
321 self.heads = [self.head]
321 self.heads = [self.head]
322 if self.tags is not None:
322 if self.tags is not None:
323 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
323 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
324
324
325 # Check if branches bring a few more heads to the list
325 # Check if branches bring a few more heads to the list
326 if branches:
326 if branches:
327 rpath = self.url.strip('/')
327 rpath = self.url.strip('/')
328 branchnames = svn.client.ls(rpath + '/' + urllib.quote(branches),
328 branchnames = svn.client.ls(rpath + '/' + urllib.quote(branches),
329 rev, False, self.ctx)
329 rev, False, self.ctx)
330 for branch in branchnames.keys():
330 for branch in branchnames.keys():
331 module = '%s/%s/%s' % (oldmodule, branches, branch)
331 module = '%s/%s/%s' % (oldmodule, branches, branch)
332 if not isdir(module, self.last_changed):
332 if not isdir(module, self.last_changed):
333 continue
333 continue
334 brevid = self.latest(module, self.last_changed)
334 brevid = self.latest(module, self.last_changed)
335 if not brevid:
335 if not brevid:
336 self.ui.note(_('ignoring empty branch %s\n') %
336 self.ui.note(_('ignoring empty branch %s\n') %
337 branch.encode(self.encoding))
337 branch.encode(self.encoding))
338 continue
338 continue
339 self.ui.note(_('found branch %s at %d\n') %
339 self.ui.note(_('found branch %s at %d\n') %
340 (branch, self.revnum(brevid)))
340 (branch, self.revnum(brevid)))
341 self.heads.append(brevid)
341 self.heads.append(brevid)
342
342
343 if self.startrev and self.heads:
343 if self.startrev and self.heads:
344 if len(self.heads) > 1:
344 if len(self.heads) > 1:
345 raise util.Abort(_('svn: start revision is not supported '
345 raise util.Abort(_('svn: start revision is not supported '
346 'with more than one branch'))
346 'with more than one branch'))
347 revnum = self.revnum(self.heads[0])
347 revnum = self.revnum(self.heads[0])
348 if revnum < self.startrev:
348 if revnum < self.startrev:
349 raise util.Abort(_('svn: no revision found after start revision %d')
349 raise util.Abort(_('svn: no revision found after start revision %d')
350 % self.startrev)
350 % self.startrev)
351
351
352 return self.heads
352 return self.heads
353
353
354 def getfile(self, file, rev):
354 def getfile(self, file, rev):
355 data, mode = self._getfile(file, rev)
355 data, mode = self._getfile(file, rev)
356 self.modecache[(file, rev)] = mode
356 self.modecache[(file, rev)] = mode
357 return data
357 return data
358
358
359 def getmode(self, file, rev):
359 def getmode(self, file, rev):
360 return self.modecache[(file, rev)]
360 return self.modecache[(file, rev)]
361
361
362 def getchanges(self, rev):
362 def getchanges(self, rev):
363 if self._changescache and self._changescache[0] == rev:
363 if self._changescache and self._changescache[0] == rev:
364 return self._changescache[1]
364 return self._changescache[1]
365 self._changescache = None
365 self._changescache = None
366 self.modecache = {}
366 self.modecache = {}
367 (paths, parents) = self.paths[rev]
367 (paths, parents) = self.paths[rev]
368 if parents:
368 if parents:
369 files, copies = self.expandpaths(rev, paths, parents)
369 files, copies = self.expandpaths(rev, paths, parents)
370 else:
370 else:
371 # Perform a full checkout on roots
371 # Perform a full checkout on roots
372 uuid, module, revnum = self.revsplit(rev)
372 uuid, module, revnum = self.revsplit(rev)
373 entries = svn.client.ls(self.baseurl + urllib.quote(module),
373 entries = svn.client.ls(self.baseurl + urllib.quote(module),
374 optrev(revnum), True, self.ctx)
374 optrev(revnum), True, self.ctx)
375 files = [n for n,e in entries.iteritems()
375 files = [n for n,e in entries.iteritems()
376 if e.kind == svn.core.svn_node_file]
376 if e.kind == svn.core.svn_node_file]
377 copies = {}
377 copies = {}
378
378
379 files.sort()
379 files.sort()
380 files = zip(files, [rev] * len(files))
380 files = zip(files, [rev] * len(files))
381
381
382 # caller caches the result, so free it here to release memory
382 # caller caches the result, so free it here to release memory
383 del self.paths[rev]
383 del self.paths[rev]
384 return (files, copies)
384 return (files, copies)
385
385
386 def getchangedfiles(self, rev, i):
386 def getchangedfiles(self, rev, i):
387 changes = self.getchanges(rev)
387 changes = self.getchanges(rev)
388 self._changescache = (rev, changes)
388 self._changescache = (rev, changes)
389 return [f[0] for f in changes[0]]
389 return [f[0] for f in changes[0]]
390
390
391 def getcommit(self, rev):
391 def getcommit(self, rev):
392 if rev not in self.commits:
392 if rev not in self.commits:
393 uuid, module, revnum = self.revsplit(rev)
393 uuid, module, revnum = self.revsplit(rev)
394 self.module = module
394 self.module = module
395 self.reparent(module)
395 self.reparent(module)
396 # We assume that:
396 # We assume that:
397 # - requests for revisions after "stop" come from the
397 # - requests for revisions after "stop" come from the
398 # revision graph backward traversal. Cache all of them
398 # revision graph backward traversal. Cache all of them
399 # down to stop, they will be used eventually.
399 # down to stop, they will be used eventually.
400 # - requests for revisions before "stop" come to get
400 # - requests for revisions before "stop" come to get
401 # isolated branches parents. Just fetch what is needed.
401 # isolated branches parents. Just fetch what is needed.
402 stop = self.lastrevs.get(module, 0)
402 stop = self.lastrevs.get(module, 0)
403 if revnum < stop:
403 if revnum < stop:
404 stop = revnum + 1
404 stop = revnum + 1
405 self._fetch_revisions(revnum, stop)
405 self._fetch_revisions(revnum, stop)
406 commit = self.commits[rev]
406 commit = self.commits[rev]
407 # caller caches the result, so free it here to release memory
407 # caller caches the result, so free it here to release memory
408 del self.commits[rev]
408 del self.commits[rev]
409 return commit
409 return commit
410
410
411 def gettags(self):
411 def gettags(self):
412 tags = {}
412 tags = {}
413 if self.tags is None:
413 if self.tags is None:
414 return tags
414 return tags
415
415
416 # svn tags are just a convention, project branches left in a
416 # svn tags are just a convention, project branches left in a
417 # 'tags' directory. There is no other relationship than
417 # 'tags' directory. There is no other relationship than
418 # ancestry, which is expensive to discover and makes them hard
418 # ancestry, which is expensive to discover and makes them hard
419 # to update incrementally. Worse, past revisions may be
419 # to update incrementally. Worse, past revisions may be
420 # referenced by tags far away in the future, requiring a deep
420 # referenced by tags far away in the future, requiring a deep
421 # history traversal on every calculation. Current code
421 # history traversal on every calculation. Current code
422 # performs a single backward traversal, tracking moves within
422 # performs a single backward traversal, tracking moves within
423 # the tags directory (tag renaming) and recording a new tag
423 # the tags directory (tag renaming) and recording a new tag
424 # everytime a project is copied from outside the tags
424 # everytime a project is copied from outside the tags
425 # directory. It also lists deleted tags, this behaviour may
425 # directory. It also lists deleted tags, this behaviour may
426 # change in the future.
426 # change in the future.
427 pendings = []
427 pendings = []
428 tagspath = self.tags
428 tagspath = self.tags
429 start = svn.ra.get_latest_revnum(self.ra)
429 start = svn.ra.get_latest_revnum(self.ra)
430 try:
430 try:
431 for entry in self._getlog([self.tags], start, self.startrev):
431 for entry in self._getlog([self.tags], start, self.startrev):
432 origpaths, revnum, author, date, message = entry
432 origpaths, revnum, author, date, message = entry
433 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
433 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
434 in origpaths.iteritems() if e.copyfrom_path]
434 in origpaths.iteritems() if e.copyfrom_path]
435 # Apply moves/copies from more specific to general
435 # Apply moves/copies from more specific to general
436 copies.sort(reverse=True)
436 copies.sort(reverse=True)
437
437
438 srctagspath = tagspath
438 srctagspath = tagspath
439 if copies and copies[-1][2] == tagspath:
439 if copies and copies[-1][2] == tagspath:
440 # Track tags directory moves
440 # Track tags directory moves
441 srctagspath = copies.pop()[0]
441 srctagspath = copies.pop()[0]
442
442
443 for source, sourcerev, dest in copies:
443 for source, sourcerev, dest in copies:
444 if not dest.startswith(tagspath + '/'):
444 if not dest.startswith(tagspath + '/'):
445 continue
445 continue
446 for tag in pendings:
446 for tag in pendings:
447 if tag[0].startswith(dest):
447 if tag[0].startswith(dest):
448 tagpath = source + tag[0][len(dest):]
448 tagpath = source + tag[0][len(dest):]
449 tag[:2] = [tagpath, sourcerev]
449 tag[:2] = [tagpath, sourcerev]
450 break
450 break
451 else:
451 else:
452 pendings.append([source, sourcerev, dest])
452 pendings.append([source, sourcerev, dest])
453
453
454 # Filter out tags with children coming from different
454 # Filter out tags with children coming from different
455 # parts of the repository like:
455 # parts of the repository like:
456 # /tags/tag.1 (from /trunk:10)
456 # /tags/tag.1 (from /trunk:10)
457 # /tags/tag.1/foo (from /branches/foo:12)
457 # /tags/tag.1/foo (from /branches/foo:12)
458 # Here/tags/tag.1 discarded as well as its children.
458 # Here/tags/tag.1 discarded as well as its children.
459 # It happens with tools like cvs2svn. Such tags cannot
459 # It happens with tools like cvs2svn. Such tags cannot
460 # be represented in mercurial.
460 # be represented in mercurial.
461 addeds = dict((p, e.copyfrom_path) for p, e
461 addeds = dict((p, e.copyfrom_path) for p, e
462 in origpaths.iteritems() if e.action == 'A')
462 in origpaths.iteritems() if e.action == 'A')
463 badroots = set()
463 badroots = set()
464 for destroot in addeds:
464 for destroot in addeds:
465 for source, sourcerev, dest in pendings:
465 for source, sourcerev, dest in pendings:
466 if (not dest.startswith(destroot + '/')
466 if (not dest.startswith(destroot + '/')
467 or source.startswith(addeds[destroot] + '/')):
467 or source.startswith(addeds[destroot] + '/')):
468 continue
468 continue
469 badroots.add(destroot)
469 badroots.add(destroot)
470 break
470 break
471
471
472 for badroot in badroots:
472 for badroot in badroots:
473 pendings = [p for p in pendings if p[2] != badroot
473 pendings = [p for p in pendings if p[2] != badroot
474 and not p[2].startswith(badroot + '/')]
474 and not p[2].startswith(badroot + '/')]
475
475
476 # Tell tag renamings from tag creations
476 # Tell tag renamings from tag creations
477 remainings = []
477 remainings = []
478 for source, sourcerev, dest in pendings:
478 for source, sourcerev, dest in pendings:
479 tagname = dest.split('/')[-1]
479 tagname = dest.split('/')[-1]
480 if source.startswith(srctagspath):
480 if source.startswith(srctagspath):
481 remainings.append([source, sourcerev, tagname])
481 remainings.append([source, sourcerev, tagname])
482 continue
482 continue
483 if tagname in tags:
483 if tagname in tags:
484 # Keep the latest tag value
484 # Keep the latest tag value
485 continue
485 continue
486 # From revision may be fake, get one with changes
486 # From revision may be fake, get one with changes
487 try:
487 try:
488 tagid = self.latest(source, sourcerev)
488 tagid = self.latest(source, sourcerev)
489 if tagid and tagname not in tags:
489 if tagid and tagname not in tags:
490 tags[tagname] = tagid
490 tags[tagname] = tagid
491 except SvnPathNotFound:
491 except SvnPathNotFound:
492 # It happens when we are following directories
492 # It happens when we are following directories
493 # we assumed were copied with their parents
493 # we assumed were copied with their parents
494 # but were really created in the tag
494 # but were really created in the tag
495 # directory.
495 # directory.
496 pass
496 pass
497 pendings = remainings
497 pendings = remainings
498 tagspath = srctagspath
498 tagspath = srctagspath
499
499
500 except SubversionException:
500 except SubversionException:
501 self.ui.note(_('no tags found at revision %d\n') % start)
501 self.ui.note(_('no tags found at revision %d\n') % start)
502 return tags
502 return tags
503
503
504 def converted(self, rev, destrev):
504 def converted(self, rev, destrev):
505 if not self.wc:
505 if not self.wc:
506 return
506 return
507 if self.convertfp is None:
507 if self.convertfp is None:
508 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
508 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
509 'a')
509 'a')
510 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
510 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
511 self.convertfp.flush()
511 self.convertfp.flush()
512
512
513 def revid(self, revnum, module=None):
513 def revid(self, revnum, module=None):
514 if not module:
514 if not module:
515 module = self.module
515 module = self.module
516 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
516 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
517 revnum)
517 revnum)
518
518
519 def revnum(self, rev):
519 def revnum(self, rev):
520 return int(rev.split('@')[-1])
520 return int(rev.split('@')[-1])
521
521
522 def revsplit(self, rev):
522 def revsplit(self, rev):
523 url, revnum = rev.encode(self.encoding).rsplit('@', 1)
523 url, revnum = rev.encode(self.encoding).rsplit('@', 1)
524 revnum = int(revnum)
524 revnum = int(revnum)
525 parts = url.split('/', 1)
525 parts = url.split('/', 1)
526 uuid = parts.pop(0)[4:]
526 uuid = parts.pop(0)[4:]
527 mod = ''
527 mod = ''
528 if parts:
528 if parts:
529 mod = '/' + parts[0]
529 mod = '/' + parts[0]
530 return uuid, mod, revnum
530 return uuid, mod, revnum
531
531
532 def latest(self, path, stop=0):
532 def latest(self, path, stop=0):
533 """Find the latest revid affecting path, up to stop. It may return
533 """Find the latest revid affecting path, up to stop. It may return
534 a revision in a different module, since a branch may be moved without
534 a revision in a different module, since a branch may be moved without
535 a change being reported. Return None if computed module does not
535 a change being reported. Return None if computed module does not
536 belong to rootmodule subtree.
536 belong to rootmodule subtree.
537 """
537 """
538 if not path.startswith(self.rootmodule):
538 if not path.startswith(self.rootmodule):
539 # Requests on foreign branches may be forbidden at server level
539 # Requests on foreign branches may be forbidden at server level
540 self.ui.debug(_('ignoring foreign branch %r\n') % path)
540 self.ui.debug(_('ignoring foreign branch %r\n') % path)
541 return None
541 return None
542
542
543 if not stop:
543 if not stop:
544 stop = svn.ra.get_latest_revnum(self.ra)
544 stop = svn.ra.get_latest_revnum(self.ra)
545 try:
545 try:
546 prevmodule = self.reparent('')
546 prevmodule = self.reparent('')
547 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
547 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
548 self.reparent(prevmodule)
548 self.reparent(prevmodule)
549 except SubversionException:
549 except SubversionException:
550 dirent = None
550 dirent = None
551 if not dirent:
551 if not dirent:
552 raise SvnPathNotFound(_('%s not found up to revision %d') % (path, stop))
552 raise SvnPathNotFound(_('%s not found up to revision %d') % (path, stop))
553
553
554 # stat() gives us the previous revision on this line of
554 # stat() gives us the previous revision on this line of
555 # development, but it might be in *another module*. Fetch the
555 # development, but it might be in *another module*. Fetch the
556 # log and detect renames down to the latest revision.
556 # log and detect renames down to the latest revision.
557 stream = self._getlog([path], stop, dirent.created_rev)
557 stream = self._getlog([path], stop, dirent.created_rev)
558 try:
558 try:
559 for entry in stream:
559 for entry in stream:
560 paths, revnum, author, date, message = entry
560 paths, revnum, author, date, message = entry
561 if revnum <= dirent.created_rev:
561 if revnum <= dirent.created_rev:
562 break
562 break
563
563
564 for p in paths:
564 for p in paths:
565 if not path.startswith(p) or not paths[p].copyfrom_path:
565 if not path.startswith(p) or not paths[p].copyfrom_path:
566 continue
566 continue
567 newpath = paths[p].copyfrom_path + path[len(p):]
567 newpath = paths[p].copyfrom_path + path[len(p):]
568 self.ui.debug(_("branch renamed from %s to %s at %d\n") %
568 self.ui.debug(_("branch renamed from %s to %s at %d\n") %
569 (path, newpath, revnum))
569 (path, newpath, revnum))
570 path = newpath
570 path = newpath
571 break
571 break
572 finally:
572 finally:
573 stream.close()
573 stream.close()
574
574
575 if not path.startswith(self.rootmodule):
575 if not path.startswith(self.rootmodule):
576 self.ui.debug(_('ignoring foreign branch %r\n') % path)
576 self.ui.debug(_('ignoring foreign branch %r\n') % path)
577 return None
577 return None
578 return self.revid(dirent.created_rev, path)
578 return self.revid(dirent.created_rev, path)
579
579
580 def get_blacklist(self):
580 def get_blacklist(self):
581 """Avoid certain revision numbers.
581 """Avoid certain revision numbers.
582 It is not uncommon for two nearby revisions to cancel each other
582 It is not uncommon for two nearby revisions to cancel each other
583 out, e.g. 'I copied trunk into a subdirectory of itself instead
583 out, e.g. 'I copied trunk into a subdirectory of itself instead
584 of making a branch'. The converted repository is significantly
584 of making a branch'. The converted repository is significantly
585 smaller if we ignore such revisions.
585 smaller if we ignore such revisions.
586 """
586 """
587 self.blacklist = set()
587 self.blacklist = set()
588 blacklist = self.blacklist
588 blacklist = self.blacklist
589 for line in file("blacklist.txt", "r"):
589 for line in file("blacklist.txt", "r"):
590 if not line.startswith("#"):
590 if not line.startswith("#"):
591 try:
591 try:
592 svn_rev = int(line.strip())
592 svn_rev = int(line.strip())
593 blacklist.add(svn_rev)
593 blacklist.add(svn_rev)
594 except ValueError:
594 except ValueError:
595 pass # not an integer or a comment
595 pass # not an integer or a comment
596
596
597 def is_blacklisted(self, svn_rev):
597 def is_blacklisted(self, svn_rev):
598 return svn_rev in self.blacklist
598 return svn_rev in self.blacklist
599
599
600 def reparent(self, module):
600 def reparent(self, module):
601 """Reparent the svn transport and return the previous parent."""
601 """Reparent the svn transport and return the previous parent."""
602 if self.prevmodule == module:
602 if self.prevmodule == module:
603 return module
603 return module
604 svnurl = self.baseurl + urllib.quote(module)
604 svnurl = self.baseurl + urllib.quote(module)
605 prevmodule = self.prevmodule
605 prevmodule = self.prevmodule
606 if prevmodule is None:
606 if prevmodule is None:
607 prevmodule = ''
607 prevmodule = ''
608 self.ui.debug(_("reparent to %s\n") % svnurl)
608 self.ui.debug(_("reparent to %s\n") % svnurl)
609 svn.ra.reparent(self.ra, svnurl)
609 svn.ra.reparent(self.ra, svnurl)
610 self.prevmodule = module
610 self.prevmodule = module
611 return prevmodule
611 return prevmodule
612
612
613 def expandpaths(self, rev, paths, parents):
613 def expandpaths(self, rev, paths, parents):
614 entries = []
614 entries = []
615 # Map of entrypath, revision for finding source of deleted
615 # Map of entrypath, revision for finding source of deleted
616 # revisions.
616 # revisions.
617 copyfrom = {}
617 copyfrom = {}
618 copies = {}
618 copies = {}
619
619
620 new_module, revnum = self.revsplit(rev)[1:]
620 new_module, revnum = self.revsplit(rev)[1:]
621 if new_module != self.module:
621 if new_module != self.module:
622 self.module = new_module
622 self.module = new_module
623 self.reparent(self.module)
623 self.reparent(self.module)
624
624
625 for path, ent in paths:
625 for path, ent in paths:
626 entrypath = self.getrelpath(path)
626 entrypath = self.getrelpath(path)
627 entry = entrypath.decode(self.encoding)
627 entry = entrypath.decode(self.encoding)
628
628
629 kind = self._checkpath(entrypath, revnum)
629 kind = self._checkpath(entrypath, revnum)
630 if kind == svn.core.svn_node_file:
630 if kind == svn.core.svn_node_file:
631 entries.append(self.recode(entry))
631 entries.append(self.recode(entry))
632 if not ent.copyfrom_path or not parents:
632 if not ent.copyfrom_path or not parents:
633 continue
633 continue
634 # Copy sources not in parent revisions cannot be
634 # Copy sources not in parent revisions cannot be
635 # represented, ignore their origin for now
635 # represented, ignore their origin for now
636 pmodule, prevnum = self.revsplit(parents[0])[1:]
636 pmodule, prevnum = self.revsplit(parents[0])[1:]
637 if ent.copyfrom_rev < prevnum:
637 if ent.copyfrom_rev < prevnum:
638 continue
638 continue
639 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
639 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
640 if not copyfrom_path:
640 if not copyfrom_path:
641 continue
641 continue
642 self.ui.debug(_("copied to %s from %s@%s\n") %
642 self.ui.debug(_("copied to %s from %s@%s\n") %
643 (entrypath, copyfrom_path, ent.copyfrom_rev))
643 (entrypath, copyfrom_path, ent.copyfrom_rev))
644 copies[self.recode(entry)] = self.recode(copyfrom_path)
644 copies[self.recode(entry)] = self.recode(copyfrom_path)
645 elif kind == 0: # gone, but had better be a deleted *file*
645 elif kind == 0: # gone, but had better be a deleted *file*
646 self.ui.debug(_("gone from %s\n") % ent.copyfrom_rev)
646 self.ui.debug(_("gone from %s\n") % ent.copyfrom_rev)
647
647
648 # if a branch is created but entries are removed in
648 # if a branch is created but entries are removed in
649 # the same changeset, get the right fromrev
649 # the same changeset, get the right fromrev
650 # parents cannot be empty here, you cannot remove
650 # parents cannot be empty here, you cannot remove
651 # things from a root revision.
651 # things from a root revision.
652 uuid, old_module, fromrev = self.revsplit(parents[0])
652 uuid, old_module, fromrev = self.revsplit(parents[0])
653
653
654 basepath = old_module + "/" + self.getrelpath(path)
654 basepath = old_module + "/" + self.getrelpath(path)
655 entrypath = basepath
655 entrypath = basepath
656
656
657 def lookup_parts(p):
657 def lookup_parts(p):
658 rc = None
658 rc = None
659 parts = p.split("/")
659 parts = p.split("/")
660 for i in range(len(parts)):
660 for i in range(len(parts)):
661 part = "/".join(parts[:i])
661 part = "/".join(parts[:i])
662 info = part, copyfrom.get(part, None)
662 info = part, copyfrom.get(part, None)
663 if info[1] is not None:
663 if info[1] is not None:
664 self.ui.debug(_("found parent directory %s\n") % info[1])
664 self.ui.debug(_("found parent directory %s\n") % info[1])
665 rc = info
665 rc = info
666 return rc
666 return rc
667
667
668 self.ui.debug(_("base, entry %s %s\n") % (basepath, entrypath))
668 self.ui.debug(_("base, entry %s %s\n") % (basepath, entrypath))
669
669
670 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
670 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
671
671
672 # need to remove fragment from lookup_parts and
672 # need to remove fragment from lookup_parts and
673 # replace with copyfrom_path
673 # replace with copyfrom_path
674 if frompath is not None:
674 if frompath is not None:
675 self.ui.debug(_("munge-o-matic\n"))
675 self.ui.debug(_("munge-o-matic\n"))
676 self.ui.debug(entrypath + '\n')
676 self.ui.debug(entrypath + '\n')
677 self.ui.debug(entrypath[len(frompath):] + '\n')
677 self.ui.debug(entrypath[len(frompath):] + '\n')
678 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
678 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
679 fromrev = froment.copyfrom_rev
679 fromrev = froment.copyfrom_rev
680 self.ui.debug(_("info: %s %s %s %s\n") % (frompath, froment, ent, entrypath))
680 self.ui.debug(_("info: %s %s %s %s\n") % (frompath, froment, ent, entrypath))
681
681
682 # We can avoid the reparent calls if the module has
682 # We can avoid the reparent calls if the module has
683 # not changed but it probably does not worth the pain.
683 # not changed but it probably does not worth the pain.
684 prevmodule = self.reparent('')
684 prevmodule = self.reparent('')
685 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
685 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
686 self.reparent(prevmodule)
686 self.reparent(prevmodule)
687
687
688 if fromkind == svn.core.svn_node_file:
688 if fromkind == svn.core.svn_node_file:
689 entries.append(self.recode(entry))
689 entries.append(self.recode(entry))
690 elif fromkind == svn.core.svn_node_dir:
690 elif fromkind == svn.core.svn_node_dir:
691 # Sometimes this is tricky. For example: in The
691 # Sometimes this is tricky. For example: in The
692 # Subversion Repository revision 6940 a dir was
692 # Subversion Repository revision 6940 a dir was
693 # copied and one of its files was deleted from the
693 # copied and one of its files was deleted from the
694 # new location in the same commit. This code can't
694 # new location in the same commit. This code can't
695 # deal with that yet.
695 # deal with that yet.
696 if ent.action == 'C':
696 if ent.action == 'C':
697 children = self._find_children(path, fromrev)
697 children = self._find_children(path, fromrev)
698 else:
698 else:
699 oroot = entrypath.strip('/')
699 oroot = entrypath.strip('/')
700 nroot = path.strip('/')
700 nroot = path.strip('/')
701 children = self._find_children(oroot, fromrev)
701 children = self._find_children(oroot, fromrev)
702 children = [s.replace(oroot,nroot) for s in children]
702 children = [s.replace(oroot,nroot) for s in children]
703 # Mark all [files, not directories] as deleted.
703 # Mark all [files, not directories] as deleted.
704 for child in children:
704 for child in children:
705 # Can we move a child directory and its
705 # Can we move a child directory and its
706 # parent in the same commit? (probably can). Could
706 # parent in the same commit? (probably can). Could
707 # cause problems if instead of revnum -1,
707 # cause problems if instead of revnum -1,
708 # we have to look in (copyfrom_path, revnum - 1)
708 # we have to look in (copyfrom_path, revnum - 1)
709 entrypath = self.getrelpath("/" + child, module=old_module)
709 entrypath = self.getrelpath("/" + child, module=old_module)
710 if entrypath:
710 if entrypath:
711 entry = self.recode(entrypath.decode(self.encoding))
711 entry = self.recode(entrypath.decode(self.encoding))
712 if entry in copies:
712 if entry in copies:
713 # deleted file within a copy
713 # deleted file within a copy
714 del copies[entry]
714 del copies[entry]
715 else:
715 else:
716 entries.append(entry)
716 entries.append(entry)
717 else:
717 else:
718 self.ui.debug(_('unknown path in revision %d: %s\n') % \
718 self.ui.debug(_('unknown path in revision %d: %s\n') % \
719 (revnum, path))
719 (revnum, path))
720 elif kind == svn.core.svn_node_dir:
720 elif kind == svn.core.svn_node_dir:
721 # If the directory just had a prop change,
721 # If the directory just had a prop change,
722 # then we shouldn't need to look for its children.
722 # then we shouldn't need to look for its children.
723 if ent.action == 'M':
723 if ent.action == 'M':
724 continue
724 continue
725
725
726 children = sorted(self._find_children(path, revnum))
726 children = sorted(self._find_children(path, revnum))
727 for child in children:
727 for child in children:
728 # Can we move a child directory and its
728 # Can we move a child directory and its
729 # parent in the same commit? (probably can). Could
729 # parent in the same commit? (probably can). Could
730 # cause problems if instead of revnum -1,
730 # cause problems if instead of revnum -1,
731 # we have to look in (copyfrom_path, revnum - 1)
731 # we have to look in (copyfrom_path, revnum - 1)
732 entrypath = self.getrelpath("/" + child)
732 entrypath = self.getrelpath("/" + child)
733 if entrypath:
733 if entrypath:
734 # Need to filter out directories here...
734 # Need to filter out directories here...
735 kind = self._checkpath(entrypath, revnum)
735 kind = self._checkpath(entrypath, revnum)
736 if kind != svn.core.svn_node_dir:
736 if kind != svn.core.svn_node_dir:
737 entries.append(self.recode(entrypath))
737 entries.append(self.recode(entrypath))
738
738
739 # Handle directory copies
739 # Handle directory copies
740 if not ent.copyfrom_path or not parents:
740 if not ent.copyfrom_path or not parents:
741 continue
741 continue
742 # Copy sources not in parent revisions cannot be
742 # Copy sources not in parent revisions cannot be
743 # represented, ignore their origin for now
743 # represented, ignore their origin for now
744 pmodule, prevnum = self.revsplit(parents[0])[1:]
744 pmodule, prevnum = self.revsplit(parents[0])[1:]
745 if ent.copyfrom_rev < prevnum:
745 if ent.copyfrom_rev < prevnum:
746 continue
746 continue
747 copyfrompath = ent.copyfrom_path.decode(self.encoding)
747 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
748 copyfrompath = self.getrelpath(copyfrompath, pmodule)
749 if not copyfrompath:
748 if not copyfrompath:
750 continue
749 continue
751 copyfrom[path] = ent
750 copyfrom[path] = ent
752 self.ui.debug(_("mark %s came from %s:%d\n")
751 self.ui.debug(_("mark %s came from %s:%d\n")
753 % (path, copyfrompath, ent.copyfrom_rev))
752 % (path, copyfrompath, ent.copyfrom_rev))
754 children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev)
753 children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev)
755 children.sort()
754 children.sort()
756 for child in children:
755 for child in children:
757 entrypath = self.getrelpath("/" + child, pmodule)
756 entrypath = self.getrelpath("/" + child, pmodule)
758 if not entrypath:
757 if not entrypath:
759 continue
758 continue
760 entry = entrypath.decode(self.encoding)
759 copytopath = path + entrypath[len(copyfrompath):]
761 copytopath = path + entry[len(copyfrompath):]
762 copytopath = self.getrelpath(copytopath)
760 copytopath = self.getrelpath(copytopath)
763 copies[self.recode(copytopath)] = self.recode(entry, pmodule)
761 copies[self.recode(copytopath)] = self.recode(entrypath)
764
762
765 return (list(set(entries)), copies)
763 return (list(set(entries)), copies)
766
764
767 def _fetch_revisions(self, from_revnum, to_revnum):
765 def _fetch_revisions(self, from_revnum, to_revnum):
768 if from_revnum < to_revnum:
766 if from_revnum < to_revnum:
769 from_revnum, to_revnum = to_revnum, from_revnum
767 from_revnum, to_revnum = to_revnum, from_revnum
770
768
771 self.child_cset = None
769 self.child_cset = None
772
770
773 def parselogentry(orig_paths, revnum, author, date, message):
771 def parselogentry(orig_paths, revnum, author, date, message):
774 """Return the parsed commit object or None, and True if
772 """Return the parsed commit object or None, and True if
775 the revision is a branch root.
773 the revision is a branch root.
776 """
774 """
777 self.ui.debug(_("parsing revision %d (%d changes)\n") %
775 self.ui.debug(_("parsing revision %d (%d changes)\n") %
778 (revnum, len(orig_paths)))
776 (revnum, len(orig_paths)))
779
777
780 branched = False
778 branched = False
781 rev = self.revid(revnum)
779 rev = self.revid(revnum)
782 # branch log might return entries for a parent we already have
780 # branch log might return entries for a parent we already have
783
781
784 if rev in self.commits or revnum < to_revnum:
782 if rev in self.commits or revnum < to_revnum:
785 return None, branched
783 return None, branched
786
784
787 parents = []
785 parents = []
788 # check whether this revision is the start of a branch or part
786 # check whether this revision is the start of a branch or part
789 # of a branch renaming
787 # of a branch renaming
790 orig_paths = sorted(orig_paths.iteritems())
788 orig_paths = sorted(orig_paths.iteritems())
791 root_paths = [(p,e) for p,e in orig_paths if self.module.startswith(p)]
789 root_paths = [(p,e) for p,e in orig_paths if self.module.startswith(p)]
792 if root_paths:
790 if root_paths:
793 path, ent = root_paths[-1]
791 path, ent = root_paths[-1]
794 if ent.copyfrom_path:
792 if ent.copyfrom_path:
795 branched = True
793 branched = True
796 newpath = ent.copyfrom_path + self.module[len(path):]
794 newpath = ent.copyfrom_path + self.module[len(path):]
797 # ent.copyfrom_rev may not be the actual last revision
795 # ent.copyfrom_rev may not be the actual last revision
798 previd = self.latest(newpath, ent.copyfrom_rev)
796 previd = self.latest(newpath, ent.copyfrom_rev)
799 if previd is not None:
797 if previd is not None:
800 prevmodule, prevnum = self.revsplit(previd)[1:]
798 prevmodule, prevnum = self.revsplit(previd)[1:]
801 if prevnum >= self.startrev:
799 if prevnum >= self.startrev:
802 parents = [previd]
800 parents = [previd]
803 self.ui.note(_('found parent of branch %s at %d: %s\n') %
801 self.ui.note(_('found parent of branch %s at %d: %s\n') %
804 (self.module, prevnum, prevmodule))
802 (self.module, prevnum, prevmodule))
805 else:
803 else:
806 self.ui.debug(_("no copyfrom path, don't know what to do.\n"))
804 self.ui.debug(_("no copyfrom path, don't know what to do.\n"))
807
805
808 paths = []
806 paths = []
809 # filter out unrelated paths
807 # filter out unrelated paths
810 for path, ent in orig_paths:
808 for path, ent in orig_paths:
811 if self.getrelpath(path) is None:
809 if self.getrelpath(path) is None:
812 continue
810 continue
813 paths.append((path, ent))
811 paths.append((path, ent))
814
812
815 # Example SVN datetime. Includes microseconds.
813 # Example SVN datetime. Includes microseconds.
816 # ISO-8601 conformant
814 # ISO-8601 conformant
817 # '2007-01-04T17:35:00.902377Z'
815 # '2007-01-04T17:35:00.902377Z'
818 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
816 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
819
817
820 log = message and self.recode(message) or ''
818 log = message and self.recode(message) or ''
821 author = author and self.recode(author) or ''
819 author = author and self.recode(author) or ''
822 try:
820 try:
823 branch = self.module.split("/")[-1]
821 branch = self.module.split("/")[-1]
824 if branch == 'trunk':
822 if branch == 'trunk':
825 branch = ''
823 branch = ''
826 except IndexError:
824 except IndexError:
827 branch = None
825 branch = None
828
826
829 cset = commit(author=author,
827 cset = commit(author=author,
830 date=util.datestr(date),
828 date=util.datestr(date),
831 desc=log,
829 desc=log,
832 parents=parents,
830 parents=parents,
833 branch=branch,
831 branch=branch,
834 rev=rev.encode('utf-8'))
832 rev=rev.encode('utf-8'))
835
833
836 self.commits[rev] = cset
834 self.commits[rev] = cset
837 # The parents list is *shared* among self.paths and the
835 # The parents list is *shared* among self.paths and the
838 # commit object. Both will be updated below.
836 # commit object. Both will be updated below.
839 self.paths[rev] = (paths, cset.parents)
837 self.paths[rev] = (paths, cset.parents)
840 if self.child_cset and not self.child_cset.parents:
838 if self.child_cset and not self.child_cset.parents:
841 self.child_cset.parents[:] = [rev]
839 self.child_cset.parents[:] = [rev]
842 self.child_cset = cset
840 self.child_cset = cset
843 return cset, branched
841 return cset, branched
844
842
845 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
843 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
846 (self.module, from_revnum, to_revnum))
844 (self.module, from_revnum, to_revnum))
847
845
848 try:
846 try:
849 firstcset = None
847 firstcset = None
850 lastonbranch = False
848 lastonbranch = False
851 stream = self._getlog([self.module], from_revnum, to_revnum)
849 stream = self._getlog([self.module], from_revnum, to_revnum)
852 try:
850 try:
853 for entry in stream:
851 for entry in stream:
854 paths, revnum, author, date, message = entry
852 paths, revnum, author, date, message = entry
855 if revnum < self.startrev:
853 if revnum < self.startrev:
856 lastonbranch = True
854 lastonbranch = True
857 break
855 break
858 if self.is_blacklisted(revnum):
856 if self.is_blacklisted(revnum):
859 self.ui.note(_('skipping blacklisted revision %d\n')
857 self.ui.note(_('skipping blacklisted revision %d\n')
860 % revnum)
858 % revnum)
861 continue
859 continue
862 if not paths:
860 if not paths:
863 self.ui.debug(_('revision %d has no entries\n') % revnum)
861 self.ui.debug(_('revision %d has no entries\n') % revnum)
864 continue
862 continue
865 cset, lastonbranch = parselogentry(paths, revnum, author,
863 cset, lastonbranch = parselogentry(paths, revnum, author,
866 date, message)
864 date, message)
867 if cset:
865 if cset:
868 firstcset = cset
866 firstcset = cset
869 if lastonbranch:
867 if lastonbranch:
870 break
868 break
871 finally:
869 finally:
872 stream.close()
870 stream.close()
873
871
874 if not lastonbranch and firstcset and not firstcset.parents:
872 if not lastonbranch and firstcset and not firstcset.parents:
875 # The first revision of the sequence (the last fetched one)
873 # The first revision of the sequence (the last fetched one)
876 # has invalid parents if not a branch root. Find the parent
874 # has invalid parents if not a branch root. Find the parent
877 # revision now, if any.
875 # revision now, if any.
878 try:
876 try:
879 firstrevnum = self.revnum(firstcset.rev)
877 firstrevnum = self.revnum(firstcset.rev)
880 if firstrevnum > 1:
878 if firstrevnum > 1:
881 latest = self.latest(self.module, firstrevnum - 1)
879 latest = self.latest(self.module, firstrevnum - 1)
882 if latest:
880 if latest:
883 firstcset.parents.append(latest)
881 firstcset.parents.append(latest)
884 except SvnPathNotFound:
882 except SvnPathNotFound:
885 pass
883 pass
886 except SubversionException, (inst, num):
884 except SubversionException, (inst, num):
887 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
885 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
888 raise util.Abort(_('svn: branch has no revision %s') % to_revnum)
886 raise util.Abort(_('svn: branch has no revision %s') % to_revnum)
889 raise
887 raise
890
888
891 def _getfile(self, file, rev):
889 def _getfile(self, file, rev):
892 # TODO: ra.get_file transmits the whole file instead of diffs.
890 # TODO: ra.get_file transmits the whole file instead of diffs.
893 mode = ''
891 mode = ''
894 try:
892 try:
895 new_module, revnum = self.revsplit(rev)[1:]
893 new_module, revnum = self.revsplit(rev)[1:]
896 if self.module != new_module:
894 if self.module != new_module:
897 self.module = new_module
895 self.module = new_module
898 self.reparent(self.module)
896 self.reparent(self.module)
899 io = StringIO()
897 io = StringIO()
900 info = svn.ra.get_file(self.ra, file, revnum, io)
898 info = svn.ra.get_file(self.ra, file, revnum, io)
901 data = io.getvalue()
899 data = io.getvalue()
902 # ra.get_files() seems to keep a reference on the input buffer
900 # ra.get_files() seems to keep a reference on the input buffer
903 # preventing collection. Release it explicitely.
901 # preventing collection. Release it explicitely.
904 io.close()
902 io.close()
905 if isinstance(info, list):
903 if isinstance(info, list):
906 info = info[-1]
904 info = info[-1]
907 mode = ("svn:executable" in info) and 'x' or ''
905 mode = ("svn:executable" in info) and 'x' or ''
908 mode = ("svn:special" in info) and 'l' or mode
906 mode = ("svn:special" in info) and 'l' or mode
909 except SubversionException, e:
907 except SubversionException, e:
910 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
908 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
911 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
909 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
912 if e.apr_err in notfound: # File not found
910 if e.apr_err in notfound: # File not found
913 raise IOError()
911 raise IOError()
914 raise
912 raise
915 if mode == 'l':
913 if mode == 'l':
916 link_prefix = "link "
914 link_prefix = "link "
917 if data.startswith(link_prefix):
915 if data.startswith(link_prefix):
918 data = data[len(link_prefix):]
916 data = data[len(link_prefix):]
919 return data, mode
917 return data, mode
920
918
921 def _find_children(self, path, revnum):
919 def _find_children(self, path, revnum):
922 path = path.strip('/')
920 path = path.strip('/')
923 pool = Pool()
921 pool = Pool()
924 rpath = '/'.join([self.baseurl, urllib.quote(path)]).strip('/')
922 rpath = '/'.join([self.baseurl, urllib.quote(path)]).strip('/')
925 return ['%s/%s' % (path, x) for x in
923 return ['%s/%s' % (path, x) for x in
926 svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
924 svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
927
925
928 def getrelpath(self, path, module=None):
926 def getrelpath(self, path, module=None):
929 if module is None:
927 if module is None:
930 module = self.module
928 module = self.module
931 # Given the repository url of this wc, say
929 # Given the repository url of this wc, say
932 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
930 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
933 # extract the "entry" portion (a relative path) from what
931 # extract the "entry" portion (a relative path) from what
934 # svn log --xml says, ie
932 # svn log --xml says, ie
935 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
933 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
936 # that is to say "tests/PloneTestCase.py"
934 # that is to say "tests/PloneTestCase.py"
937 if path.startswith(module):
935 if path.startswith(module):
938 relative = path.rstrip('/')[len(module):]
936 relative = path.rstrip('/')[len(module):]
939 if relative.startswith('/'):
937 if relative.startswith('/'):
940 return relative[1:]
938 return relative[1:]
941 elif relative == '':
939 elif relative == '':
942 return relative
940 return relative
943
941
944 # The path is outside our tracked tree...
942 # The path is outside our tracked tree...
945 self.ui.debug(_('%r is not under %r, ignoring\n') % (path, module))
943 self.ui.debug(_('%r is not under %r, ignoring\n') % (path, module))
946 return None
944 return None
947
945
948 def _checkpath(self, path, revnum):
946 def _checkpath(self, path, revnum):
949 # ra.check_path does not like leading slashes very much, it leads
947 # ra.check_path does not like leading slashes very much, it leads
950 # to PROPFIND subversion errors
948 # to PROPFIND subversion errors
951 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
949 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
952
950
953 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
951 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
954 strict_node_history=False):
952 strict_node_history=False):
955 # Normalize path names, svn >= 1.5 only wants paths relative to
953 # Normalize path names, svn >= 1.5 only wants paths relative to
956 # supplied URL
954 # supplied URL
957 relpaths = []
955 relpaths = []
958 for p in paths:
956 for p in paths:
959 if not p.startswith('/'):
957 if not p.startswith('/'):
960 p = self.module + '/' + p
958 p = self.module + '/' + p
961 relpaths.append(p.strip('/'))
959 relpaths.append(p.strip('/'))
962 args = [self.baseurl, relpaths, start, end, limit, discover_changed_paths,
960 args = [self.baseurl, relpaths, start, end, limit, discover_changed_paths,
963 strict_node_history]
961 strict_node_history]
964 arg = encodeargs(args)
962 arg = encodeargs(args)
965 hgexe = util.hgexecutable()
963 hgexe = util.hgexecutable()
966 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
964 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
967 stdin, stdout = util.popen2(cmd)
965 stdin, stdout = util.popen2(cmd)
968 stdin.write(arg)
966 stdin.write(arg)
969 stdin.close()
967 stdin.close()
970 return logstream(stdout)
968 return logstream(stdout)
971
969
972 pre_revprop_change = '''#!/bin/sh
970 pre_revprop_change = '''#!/bin/sh
973
971
974 REPOS="$1"
972 REPOS="$1"
975 REV="$2"
973 REV="$2"
976 USER="$3"
974 USER="$3"
977 PROPNAME="$4"
975 PROPNAME="$4"
978 ACTION="$5"
976 ACTION="$5"
979
977
980 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
978 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
981 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
979 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
982 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
980 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
983
981
984 echo "Changing prohibited revision property" >&2
982 echo "Changing prohibited revision property" >&2
985 exit 1
983 exit 1
986 '''
984 '''
987
985
988 class svn_sink(converter_sink, commandline):
986 class svn_sink(converter_sink, commandline):
989 commit_re = re.compile(r'Committed revision (\d+).', re.M)
987 commit_re = re.compile(r'Committed revision (\d+).', re.M)
990
988
991 def prerun(self):
989 def prerun(self):
992 if self.wc:
990 if self.wc:
993 os.chdir(self.wc)
991 os.chdir(self.wc)
994
992
995 def postrun(self):
993 def postrun(self):
996 if self.wc:
994 if self.wc:
997 os.chdir(self.cwd)
995 os.chdir(self.cwd)
998
996
999 def join(self, name):
997 def join(self, name):
1000 return os.path.join(self.wc, '.svn', name)
998 return os.path.join(self.wc, '.svn', name)
1001
999
1002 def revmapfile(self):
1000 def revmapfile(self):
1003 return self.join('hg-shamap')
1001 return self.join('hg-shamap')
1004
1002
1005 def authorfile(self):
1003 def authorfile(self):
1006 return self.join('hg-authormap')
1004 return self.join('hg-authormap')
1007
1005
1008 def __init__(self, ui, path):
1006 def __init__(self, ui, path):
1009 converter_sink.__init__(self, ui, path)
1007 converter_sink.__init__(self, ui, path)
1010 commandline.__init__(self, ui, 'svn')
1008 commandline.__init__(self, ui, 'svn')
1011 self.delete = []
1009 self.delete = []
1012 self.setexec = []
1010 self.setexec = []
1013 self.delexec = []
1011 self.delexec = []
1014 self.copies = []
1012 self.copies = []
1015 self.wc = None
1013 self.wc = None
1016 self.cwd = os.getcwd()
1014 self.cwd = os.getcwd()
1017
1015
1018 path = os.path.realpath(path)
1016 path = os.path.realpath(path)
1019
1017
1020 created = False
1018 created = False
1021 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1019 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1022 self.wc = path
1020 self.wc = path
1023 self.run0('update')
1021 self.run0('update')
1024 else:
1022 else:
1025 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1023 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1026
1024
1027 if os.path.isdir(os.path.dirname(path)):
1025 if os.path.isdir(os.path.dirname(path)):
1028 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1026 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1029 ui.status(_('initializing svn repo %r\n') %
1027 ui.status(_('initializing svn repo %r\n') %
1030 os.path.basename(path))
1028 os.path.basename(path))
1031 commandline(ui, 'svnadmin').run0('create', path)
1029 commandline(ui, 'svnadmin').run0('create', path)
1032 created = path
1030 created = path
1033 path = util.normpath(path)
1031 path = util.normpath(path)
1034 if not path.startswith('/'):
1032 if not path.startswith('/'):
1035 path = '/' + path
1033 path = '/' + path
1036 path = 'file://' + path
1034 path = 'file://' + path
1037
1035
1038 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
1036 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
1039 self.run0('checkout', path, wcpath)
1037 self.run0('checkout', path, wcpath)
1040
1038
1041 self.wc = wcpath
1039 self.wc = wcpath
1042 self.opener = util.opener(self.wc)
1040 self.opener = util.opener(self.wc)
1043 self.wopener = util.opener(self.wc)
1041 self.wopener = util.opener(self.wc)
1044 self.childmap = mapfile(ui, self.join('hg-childmap'))
1042 self.childmap = mapfile(ui, self.join('hg-childmap'))
1045 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
1043 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
1046
1044
1047 if created:
1045 if created:
1048 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1046 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1049 fp = open(hook, 'w')
1047 fp = open(hook, 'w')
1050 fp.write(pre_revprop_change)
1048 fp.write(pre_revprop_change)
1051 fp.close()
1049 fp.close()
1052 util.set_flags(hook, False, True)
1050 util.set_flags(hook, False, True)
1053
1051
1054 xport = transport.SvnRaTransport(url=geturl(path))
1052 xport = transport.SvnRaTransport(url=geturl(path))
1055 self.uuid = svn.ra.get_uuid(xport.ra)
1053 self.uuid = svn.ra.get_uuid(xport.ra)
1056
1054
1057 def wjoin(self, *names):
1055 def wjoin(self, *names):
1058 return os.path.join(self.wc, *names)
1056 return os.path.join(self.wc, *names)
1059
1057
1060 def putfile(self, filename, flags, data):
1058 def putfile(self, filename, flags, data):
1061 if 'l' in flags:
1059 if 'l' in flags:
1062 self.wopener.symlink(data, filename)
1060 self.wopener.symlink(data, filename)
1063 else:
1061 else:
1064 try:
1062 try:
1065 if os.path.islink(self.wjoin(filename)):
1063 if os.path.islink(self.wjoin(filename)):
1066 os.unlink(filename)
1064 os.unlink(filename)
1067 except OSError:
1065 except OSError:
1068 pass
1066 pass
1069 self.wopener(filename, 'w').write(data)
1067 self.wopener(filename, 'w').write(data)
1070
1068
1071 if self.is_exec:
1069 if self.is_exec:
1072 was_exec = self.is_exec(self.wjoin(filename))
1070 was_exec = self.is_exec(self.wjoin(filename))
1073 else:
1071 else:
1074 # On filesystems not supporting execute-bit, there is no way
1072 # On filesystems not supporting execute-bit, there is no way
1075 # to know if it is set but asking subversion. Setting it
1073 # to know if it is set but asking subversion. Setting it
1076 # systematically is just as expensive and much simpler.
1074 # systematically is just as expensive and much simpler.
1077 was_exec = 'x' not in flags
1075 was_exec = 'x' not in flags
1078
1076
1079 util.set_flags(self.wjoin(filename), False, 'x' in flags)
1077 util.set_flags(self.wjoin(filename), False, 'x' in flags)
1080 if was_exec:
1078 if was_exec:
1081 if 'x' not in flags:
1079 if 'x' not in flags:
1082 self.delexec.append(filename)
1080 self.delexec.append(filename)
1083 else:
1081 else:
1084 if 'x' in flags:
1082 if 'x' in flags:
1085 self.setexec.append(filename)
1083 self.setexec.append(filename)
1086
1084
1087 def _copyfile(self, source, dest):
1085 def _copyfile(self, source, dest):
1088 # SVN's copy command pukes if the destination file exists, but
1086 # SVN's copy command pukes if the destination file exists, but
1089 # our copyfile method expects to record a copy that has
1087 # our copyfile method expects to record a copy that has
1090 # already occurred. Cross the semantic gap.
1088 # already occurred. Cross the semantic gap.
1091 wdest = self.wjoin(dest)
1089 wdest = self.wjoin(dest)
1092 exists = os.path.exists(wdest)
1090 exists = os.path.exists(wdest)
1093 if exists:
1091 if exists:
1094 fd, tempname = tempfile.mkstemp(
1092 fd, tempname = tempfile.mkstemp(
1095 prefix='hg-copy-', dir=os.path.dirname(wdest))
1093 prefix='hg-copy-', dir=os.path.dirname(wdest))
1096 os.close(fd)
1094 os.close(fd)
1097 os.unlink(tempname)
1095 os.unlink(tempname)
1098 os.rename(wdest, tempname)
1096 os.rename(wdest, tempname)
1099 try:
1097 try:
1100 self.run0('copy', source, dest)
1098 self.run0('copy', source, dest)
1101 finally:
1099 finally:
1102 if exists:
1100 if exists:
1103 try:
1101 try:
1104 os.unlink(wdest)
1102 os.unlink(wdest)
1105 except OSError:
1103 except OSError:
1106 pass
1104 pass
1107 os.rename(tempname, wdest)
1105 os.rename(tempname, wdest)
1108
1106
1109 def dirs_of(self, files):
1107 def dirs_of(self, files):
1110 dirs = set()
1108 dirs = set()
1111 for f in files:
1109 for f in files:
1112 if os.path.isdir(self.wjoin(f)):
1110 if os.path.isdir(self.wjoin(f)):
1113 dirs.add(f)
1111 dirs.add(f)
1114 for i in strutil.rfindall(f, '/'):
1112 for i in strutil.rfindall(f, '/'):
1115 dirs.add(f[:i])
1113 dirs.add(f[:i])
1116 return dirs
1114 return dirs
1117
1115
1118 def add_dirs(self, files):
1116 def add_dirs(self, files):
1119 add_dirs = [d for d in sorted(self.dirs_of(files))
1117 add_dirs = [d for d in sorted(self.dirs_of(files))
1120 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1118 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1121 if add_dirs:
1119 if add_dirs:
1122 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1120 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1123 return add_dirs
1121 return add_dirs
1124
1122
1125 def add_files(self, files):
1123 def add_files(self, files):
1126 if files:
1124 if files:
1127 self.xargs(files, 'add', quiet=True)
1125 self.xargs(files, 'add', quiet=True)
1128 return files
1126 return files
1129
1127
1130 def tidy_dirs(self, names):
1128 def tidy_dirs(self, names):
1131 deleted = []
1129 deleted = []
1132 for d in sorted(self.dirs_of(names), reverse=True):
1130 for d in sorted(self.dirs_of(names), reverse=True):
1133 wd = self.wjoin(d)
1131 wd = self.wjoin(d)
1134 if os.listdir(wd) == '.svn':
1132 if os.listdir(wd) == '.svn':
1135 self.run0('delete', d)
1133 self.run0('delete', d)
1136 deleted.append(d)
1134 deleted.append(d)
1137 return deleted
1135 return deleted
1138
1136
1139 def addchild(self, parent, child):
1137 def addchild(self, parent, child):
1140 self.childmap[parent] = child
1138 self.childmap[parent] = child
1141
1139
1142 def revid(self, rev):
1140 def revid(self, rev):
1143 return u"svn:%s@%s" % (self.uuid, rev)
1141 return u"svn:%s@%s" % (self.uuid, rev)
1144
1142
1145 def putcommit(self, files, copies, parents, commit, source, revmap):
1143 def putcommit(self, files, copies, parents, commit, source, revmap):
1146 # Apply changes to working copy
1144 # Apply changes to working copy
1147 for f, v in files:
1145 for f, v in files:
1148 try:
1146 try:
1149 data = source.getfile(f, v)
1147 data = source.getfile(f, v)
1150 except IOError:
1148 except IOError:
1151 self.delete.append(f)
1149 self.delete.append(f)
1152 else:
1150 else:
1153 e = source.getmode(f, v)
1151 e = source.getmode(f, v)
1154 self.putfile(f, e, data)
1152 self.putfile(f, e, data)
1155 if f in copies:
1153 if f in copies:
1156 self.copies.append([copies[f], f])
1154 self.copies.append([copies[f], f])
1157 files = [f[0] for f in files]
1155 files = [f[0] for f in files]
1158
1156
1159 for parent in parents:
1157 for parent in parents:
1160 try:
1158 try:
1161 return self.revid(self.childmap[parent])
1159 return self.revid(self.childmap[parent])
1162 except KeyError:
1160 except KeyError:
1163 pass
1161 pass
1164 entries = set(self.delete)
1162 entries = set(self.delete)
1165 files = frozenset(files)
1163 files = frozenset(files)
1166 entries.update(self.add_dirs(files.difference(entries)))
1164 entries.update(self.add_dirs(files.difference(entries)))
1167 if self.copies:
1165 if self.copies:
1168 for s, d in self.copies:
1166 for s, d in self.copies:
1169 self._copyfile(s, d)
1167 self._copyfile(s, d)
1170 self.copies = []
1168 self.copies = []
1171 if self.delete:
1169 if self.delete:
1172 self.xargs(self.delete, 'delete')
1170 self.xargs(self.delete, 'delete')
1173 self.delete = []
1171 self.delete = []
1174 entries.update(self.add_files(files.difference(entries)))
1172 entries.update(self.add_files(files.difference(entries)))
1175 entries.update(self.tidy_dirs(entries))
1173 entries.update(self.tidy_dirs(entries))
1176 if self.delexec:
1174 if self.delexec:
1177 self.xargs(self.delexec, 'propdel', 'svn:executable')
1175 self.xargs(self.delexec, 'propdel', 'svn:executable')
1178 self.delexec = []
1176 self.delexec = []
1179 if self.setexec:
1177 if self.setexec:
1180 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1178 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1181 self.setexec = []
1179 self.setexec = []
1182
1180
1183 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1181 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1184 fp = os.fdopen(fd, 'w')
1182 fp = os.fdopen(fd, 'w')
1185 fp.write(commit.desc)
1183 fp.write(commit.desc)
1186 fp.close()
1184 fp.close()
1187 try:
1185 try:
1188 output = self.run0('commit',
1186 output = self.run0('commit',
1189 username=util.shortuser(commit.author),
1187 username=util.shortuser(commit.author),
1190 file=messagefile,
1188 file=messagefile,
1191 encoding='utf-8')
1189 encoding='utf-8')
1192 try:
1190 try:
1193 rev = self.commit_re.search(output).group(1)
1191 rev = self.commit_re.search(output).group(1)
1194 except AttributeError:
1192 except AttributeError:
1195 self.ui.warn(_('unexpected svn output:\n'))
1193 self.ui.warn(_('unexpected svn output:\n'))
1196 self.ui.warn(output)
1194 self.ui.warn(output)
1197 raise util.Abort(_('unable to cope with svn output'))
1195 raise util.Abort(_('unable to cope with svn output'))
1198 if commit.rev:
1196 if commit.rev:
1199 self.run('propset', 'hg:convert-rev', commit.rev,
1197 self.run('propset', 'hg:convert-rev', commit.rev,
1200 revprop=True, revision=rev)
1198 revprop=True, revision=rev)
1201 if commit.branch and commit.branch != 'default':
1199 if commit.branch and commit.branch != 'default':
1202 self.run('propset', 'hg:convert-branch', commit.branch,
1200 self.run('propset', 'hg:convert-branch', commit.branch,
1203 revprop=True, revision=rev)
1201 revprop=True, revision=rev)
1204 for parent in parents:
1202 for parent in parents:
1205 self.addchild(parent, rev)
1203 self.addchild(parent, rev)
1206 return self.revid(rev)
1204 return self.revid(rev)
1207 finally:
1205 finally:
1208 os.unlink(messagefile)
1206 os.unlink(messagefile)
1209
1207
1210 def puttags(self, tags):
1208 def puttags(self, tags):
1211 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
1209 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
General Comments 0
You need to be logged in to leave comments. Login now