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