##// END OF EJS Templates
convert: write "repository" instead of "repo"...
Martin Geisler -
r10938:02d6149a stable
parent child Browse files
Show More
@@ -1,261 +1,261 b''
1 # bzr.py - bzr support for the convert extension
1 # bzr.py - bzr support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Marek Kubica <marek@xivilization.net> and others
3 # Copyright 2008, 2009 Marek Kubica <marek@xivilization.net> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 # This module is for handling 'bzr', that was formerly known as Bazaar-NG;
8 # This module is for handling 'bzr', that was formerly known as Bazaar-NG;
9 # it cannot access 'bar' repositories, but they were never used very much
9 # it cannot access 'bar' repositories, but they were never used very much
10
10
11 import os
11 import os
12 from mercurial import demandimport
12 from mercurial import demandimport
13 # these do not work with demandimport, blacklist
13 # these do not work with demandimport, blacklist
14 demandimport.ignore.extend([
14 demandimport.ignore.extend([
15 'bzrlib.transactions',
15 'bzrlib.transactions',
16 'bzrlib.urlutils',
16 'bzrlib.urlutils',
17 'ElementPath',
17 'ElementPath',
18 ])
18 ])
19
19
20 from mercurial.i18n import _
20 from mercurial.i18n import _
21 from mercurial import util
21 from mercurial import util
22 from common import NoRepo, commit, converter_source
22 from common import NoRepo, commit, converter_source
23
23
24 try:
24 try:
25 # bazaar imports
25 # bazaar imports
26 from bzrlib import branch, revision, errors
26 from bzrlib import branch, revision, errors
27 from bzrlib.revisionspec import RevisionSpec
27 from bzrlib.revisionspec import RevisionSpec
28 except ImportError:
28 except ImportError:
29 pass
29 pass
30
30
31 supportedkinds = ('file', 'symlink')
31 supportedkinds = ('file', 'symlink')
32
32
33 class bzr_source(converter_source):
33 class bzr_source(converter_source):
34 """Reads Bazaar repositories by using the Bazaar Python libraries"""
34 """Reads Bazaar repositories by using the Bazaar Python libraries"""
35
35
36 def __init__(self, ui, path, rev=None):
36 def __init__(self, ui, path, rev=None):
37 super(bzr_source, self).__init__(ui, path, rev=rev)
37 super(bzr_source, self).__init__(ui, path, rev=rev)
38
38
39 if not os.path.exists(os.path.join(path, '.bzr')):
39 if not os.path.exists(os.path.join(path, '.bzr')):
40 raise NoRepo('%s does not look like a Bazaar repo' % path)
40 raise NoRepo('%s does not look like a Bazaar repository' % path)
41
41
42 try:
42 try:
43 # access bzrlib stuff
43 # access bzrlib stuff
44 branch
44 branch
45 except NameError:
45 except NameError:
46 raise NoRepo('Bazaar modules could not be loaded')
46 raise NoRepo('Bazaar modules could not be loaded')
47
47
48 path = os.path.abspath(path)
48 path = os.path.abspath(path)
49 self._checkrepotype(path)
49 self._checkrepotype(path)
50 self.branch = branch.Branch.open(path)
50 self.branch = branch.Branch.open(path)
51 self.sourcerepo = self.branch.repository
51 self.sourcerepo = self.branch.repository
52 self._parentids = {}
52 self._parentids = {}
53
53
54 def _checkrepotype(self, path):
54 def _checkrepotype(self, path):
55 # Lightweight checkouts detection is informational but probably
55 # Lightweight checkouts detection is informational but probably
56 # fragile at API level. It should not terminate the conversion.
56 # fragile at API level. It should not terminate the conversion.
57 try:
57 try:
58 from bzrlib import bzrdir
58 from bzrlib import bzrdir
59 dir = bzrdir.BzrDir.open_containing(path)[0]
59 dir = bzrdir.BzrDir.open_containing(path)[0]
60 try:
60 try:
61 tree = dir.open_workingtree(recommend_upgrade=False)
61 tree = dir.open_workingtree(recommend_upgrade=False)
62 branch = tree.branch
62 branch = tree.branch
63 except (errors.NoWorkingTree, errors.NotLocalUrl), e:
63 except (errors.NoWorkingTree, errors.NotLocalUrl), e:
64 tree = None
64 tree = None
65 branch = dir.open_branch()
65 branch = dir.open_branch()
66 if (tree is not None and tree.bzrdir.root_transport.base !=
66 if (tree is not None and tree.bzrdir.root_transport.base !=
67 branch.bzrdir.root_transport.base):
67 branch.bzrdir.root_transport.base):
68 self.ui.warn(_('warning: lightweight checkouts may cause '
68 self.ui.warn(_('warning: lightweight checkouts may cause '
69 'conversion failures, try with a regular '
69 'conversion failures, try with a regular '
70 'branch instead.\n'))
70 'branch instead.\n'))
71 except:
71 except:
72 self.ui.note(_('bzr source type could not be determined\n'))
72 self.ui.note(_('bzr source type could not be determined\n'))
73
73
74 def before(self):
74 def before(self):
75 """Before the conversion begins, acquire a read lock
75 """Before the conversion begins, acquire a read lock
76 for all the operations that might need it. Fortunately
76 for all the operations that might need it. Fortunately
77 read locks don't block other reads or writes to the
77 read locks don't block other reads or writes to the
78 repository, so this shouldn't have any impact on the usage of
78 repository, so this shouldn't have any impact on the usage of
79 the source repository.
79 the source repository.
80
80
81 The alternative would be locking on every operation that
81 The alternative would be locking on every operation that
82 needs locks (there are currently two: getting the file and
82 needs locks (there are currently two: getting the file and
83 getting the parent map) and releasing immediately after,
83 getting the parent map) and releasing immediately after,
84 but this approach can take even 40% longer."""
84 but this approach can take even 40% longer."""
85 self.sourcerepo.lock_read()
85 self.sourcerepo.lock_read()
86
86
87 def after(self):
87 def after(self):
88 self.sourcerepo.unlock()
88 self.sourcerepo.unlock()
89
89
90 def getheads(self):
90 def getheads(self):
91 if not self.rev:
91 if not self.rev:
92 return [self.branch.last_revision()]
92 return [self.branch.last_revision()]
93 try:
93 try:
94 r = RevisionSpec.from_string(self.rev)
94 r = RevisionSpec.from_string(self.rev)
95 info = r.in_history(self.branch)
95 info = r.in_history(self.branch)
96 except errors.BzrError:
96 except errors.BzrError:
97 raise util.Abort(_('%s is not a valid revision in current branch')
97 raise util.Abort(_('%s is not a valid revision in current branch')
98 % self.rev)
98 % self.rev)
99 return [info.rev_id]
99 return [info.rev_id]
100
100
101 def getfile(self, name, rev):
101 def getfile(self, name, rev):
102 revtree = self.sourcerepo.revision_tree(rev)
102 revtree = self.sourcerepo.revision_tree(rev)
103 fileid = revtree.path2id(name.decode(self.encoding or 'utf-8'))
103 fileid = revtree.path2id(name.decode(self.encoding or 'utf-8'))
104 kind = None
104 kind = None
105 if fileid is not None:
105 if fileid is not None:
106 kind = revtree.kind(fileid)
106 kind = revtree.kind(fileid)
107 if kind not in supportedkinds:
107 if kind not in supportedkinds:
108 # the file is not available anymore - was deleted
108 # the file is not available anymore - was deleted
109 raise IOError(_('%s is not available in %s anymore') %
109 raise IOError(_('%s is not available in %s anymore') %
110 (name, rev))
110 (name, rev))
111 if kind == 'symlink':
111 if kind == 'symlink':
112 target = revtree.get_symlink_target(fileid)
112 target = revtree.get_symlink_target(fileid)
113 if target is None:
113 if target is None:
114 raise util.Abort(_('%s.%s symlink has no target')
114 raise util.Abort(_('%s.%s symlink has no target')
115 % (name, rev))
115 % (name, rev))
116 return target
116 return target
117 else:
117 else:
118 sio = revtree.get_file(fileid)
118 sio = revtree.get_file(fileid)
119 return sio.read()
119 return sio.read()
120
120
121 def getmode(self, name, rev):
121 def getmode(self, name, rev):
122 return self._modecache[(name, rev)]
122 return self._modecache[(name, rev)]
123
123
124 def getchanges(self, version):
124 def getchanges(self, version):
125 # set up caches: modecache and revtree
125 # set up caches: modecache and revtree
126 self._modecache = {}
126 self._modecache = {}
127 self._revtree = self.sourcerepo.revision_tree(version)
127 self._revtree = self.sourcerepo.revision_tree(version)
128 # get the parentids from the cache
128 # get the parentids from the cache
129 parentids = self._parentids.pop(version)
129 parentids = self._parentids.pop(version)
130 # only diff against first parent id
130 # only diff against first parent id
131 prevtree = self.sourcerepo.revision_tree(parentids[0])
131 prevtree = self.sourcerepo.revision_tree(parentids[0])
132 return self._gettreechanges(self._revtree, prevtree)
132 return self._gettreechanges(self._revtree, prevtree)
133
133
134 def getcommit(self, version):
134 def getcommit(self, version):
135 rev = self.sourcerepo.get_revision(version)
135 rev = self.sourcerepo.get_revision(version)
136 # populate parent id cache
136 # populate parent id cache
137 if not rev.parent_ids:
137 if not rev.parent_ids:
138 parents = []
138 parents = []
139 self._parentids[version] = (revision.NULL_REVISION,)
139 self._parentids[version] = (revision.NULL_REVISION,)
140 else:
140 else:
141 parents = self._filterghosts(rev.parent_ids)
141 parents = self._filterghosts(rev.parent_ids)
142 self._parentids[version] = parents
142 self._parentids[version] = parents
143
143
144 return commit(parents=parents,
144 return commit(parents=parents,
145 date='%d %d' % (rev.timestamp, -rev.timezone),
145 date='%d %d' % (rev.timestamp, -rev.timezone),
146 author=self.recode(rev.committer),
146 author=self.recode(rev.committer),
147 # bzr returns bytestrings or unicode, depending on the content
147 # bzr returns bytestrings or unicode, depending on the content
148 desc=self.recode(rev.message),
148 desc=self.recode(rev.message),
149 rev=version)
149 rev=version)
150
150
151 def gettags(self):
151 def gettags(self):
152 if not self.branch.supports_tags():
152 if not self.branch.supports_tags():
153 return {}
153 return {}
154 tagdict = self.branch.tags.get_tag_dict()
154 tagdict = self.branch.tags.get_tag_dict()
155 bytetags = {}
155 bytetags = {}
156 for name, rev in tagdict.iteritems():
156 for name, rev in tagdict.iteritems():
157 bytetags[self.recode(name)] = rev
157 bytetags[self.recode(name)] = rev
158 return bytetags
158 return bytetags
159
159
160 def getchangedfiles(self, rev, i):
160 def getchangedfiles(self, rev, i):
161 self._modecache = {}
161 self._modecache = {}
162 curtree = self.sourcerepo.revision_tree(rev)
162 curtree = self.sourcerepo.revision_tree(rev)
163 if i is not None:
163 if i is not None:
164 parentid = self._parentids[rev][i]
164 parentid = self._parentids[rev][i]
165 else:
165 else:
166 # no parent id, get the empty revision
166 # no parent id, get the empty revision
167 parentid = revision.NULL_REVISION
167 parentid = revision.NULL_REVISION
168
168
169 prevtree = self.sourcerepo.revision_tree(parentid)
169 prevtree = self.sourcerepo.revision_tree(parentid)
170 changes = [e[0] for e in self._gettreechanges(curtree, prevtree)[0]]
170 changes = [e[0] for e in self._gettreechanges(curtree, prevtree)[0]]
171 return changes
171 return changes
172
172
173 def _gettreechanges(self, current, origin):
173 def _gettreechanges(self, current, origin):
174 revid = current._revision_id
174 revid = current._revision_id
175 changes = []
175 changes = []
176 renames = {}
176 renames = {}
177 for (fileid, paths, changed_content, versioned, parent, name,
177 for (fileid, paths, changed_content, versioned, parent, name,
178 kind, executable) in current.iter_changes(origin):
178 kind, executable) in current.iter_changes(origin):
179
179
180 if paths[0] == u'' or paths[1] == u'':
180 if paths[0] == u'' or paths[1] == u'':
181 # ignore changes to tree root
181 # ignore changes to tree root
182 continue
182 continue
183
183
184 # bazaar tracks directories, mercurial does not, so
184 # bazaar tracks directories, mercurial does not, so
185 # we have to rename the directory contents
185 # we have to rename the directory contents
186 if kind[1] == 'directory':
186 if kind[1] == 'directory':
187 if kind[0] not in (None, 'directory'):
187 if kind[0] not in (None, 'directory'):
188 # Replacing 'something' with a directory, record it
188 # Replacing 'something' with a directory, record it
189 # so it can be removed.
189 # so it can be removed.
190 changes.append((self.recode(paths[0]), revid))
190 changes.append((self.recode(paths[0]), revid))
191
191
192 if None not in paths and paths[0] != paths[1]:
192 if None not in paths and paths[0] != paths[1]:
193 # neither an add nor an delete - a move
193 # neither an add nor an delete - a move
194 # rename all directory contents manually
194 # rename all directory contents manually
195 subdir = origin.inventory.path2id(paths[0])
195 subdir = origin.inventory.path2id(paths[0])
196 # get all child-entries of the directory
196 # get all child-entries of the directory
197 for name, entry in origin.inventory.iter_entries(subdir):
197 for name, entry in origin.inventory.iter_entries(subdir):
198 # hg does not track directory renames
198 # hg does not track directory renames
199 if entry.kind == 'directory':
199 if entry.kind == 'directory':
200 continue
200 continue
201 frompath = self.recode(paths[0] + '/' + name)
201 frompath = self.recode(paths[0] + '/' + name)
202 topath = self.recode(paths[1] + '/' + name)
202 topath = self.recode(paths[1] + '/' + name)
203 # register the files as changed
203 # register the files as changed
204 changes.append((frompath, revid))
204 changes.append((frompath, revid))
205 changes.append((topath, revid))
205 changes.append((topath, revid))
206 # add to mode cache
206 # add to mode cache
207 mode = ((entry.executable and 'x')
207 mode = ((entry.executable and 'x')
208 or (entry.kind == 'symlink' and 's')
208 or (entry.kind == 'symlink' and 's')
209 or '')
209 or '')
210 self._modecache[(topath, revid)] = mode
210 self._modecache[(topath, revid)] = mode
211 # register the change as move
211 # register the change as move
212 renames[topath] = frompath
212 renames[topath] = frompath
213
213
214 # no futher changes, go to the next change
214 # no futher changes, go to the next change
215 continue
215 continue
216
216
217 # we got unicode paths, need to convert them
217 # we got unicode paths, need to convert them
218 path, topath = [self.recode(part) for part in paths]
218 path, topath = [self.recode(part) for part in paths]
219
219
220 if topath is None:
220 if topath is None:
221 # file deleted
221 # file deleted
222 changes.append((path, revid))
222 changes.append((path, revid))
223 continue
223 continue
224
224
225 # renamed
225 # renamed
226 if path and path != topath:
226 if path and path != topath:
227 renames[topath] = path
227 renames[topath] = path
228 changes.append((path, revid))
228 changes.append((path, revid))
229
229
230 # populate the mode cache
230 # populate the mode cache
231 kind, executable = [e[1] for e in (kind, executable)]
231 kind, executable = [e[1] for e in (kind, executable)]
232 mode = ((executable and 'x') or (kind == 'symlink' and 'l')
232 mode = ((executable and 'x') or (kind == 'symlink' and 'l')
233 or '')
233 or '')
234 self._modecache[(topath, revid)] = mode
234 self._modecache[(topath, revid)] = mode
235 changes.append((topath, revid))
235 changes.append((topath, revid))
236
236
237 return changes, renames
237 return changes, renames
238
238
239 def _filterghosts(self, ids):
239 def _filterghosts(self, ids):
240 """Filters out ghost revisions which hg does not support, see
240 """Filters out ghost revisions which hg does not support, see
241 <http://bazaar-vcs.org/GhostRevision>
241 <http://bazaar-vcs.org/GhostRevision>
242 """
242 """
243 parentmap = self.sourcerepo.get_parent_map(ids)
243 parentmap = self.sourcerepo.get_parent_map(ids)
244 parents = tuple([parent for parent in ids if parent in parentmap])
244 parents = tuple([parent for parent in ids if parent in parentmap])
245 return parents
245 return parents
246
246
247 def recode(self, s, encoding=None):
247 def recode(self, s, encoding=None):
248 """This version of recode tries to encode unicode to bytecode,
248 """This version of recode tries to encode unicode to bytecode,
249 and preferably using the UTF-8 codec.
249 and preferably using the UTF-8 codec.
250 Other types than Unicode are silently returned, this is by
250 Other types than Unicode are silently returned, this is by
251 intention, e.g. the None-type is not going to be encoded but instead
251 intention, e.g. the None-type is not going to be encoded but instead
252 just passed through
252 just passed through
253 """
253 """
254 if not encoding:
254 if not encoding:
255 encoding = self.encoding or 'utf-8'
255 encoding = self.encoding or 'utf-8'
256
256
257 if isinstance(s, unicode):
257 if isinstance(s, unicode):
258 return s.encode(encoding)
258 return s.encode(encoding)
259 else:
259 else:
260 # leave it alone
260 # leave it alone
261 return s
261 return s
@@ -1,167 +1,167 b''
1 # darcs.py - darcs support for the convert extension
1 # darcs.py - darcs support for the convert extension
2 #
2 #
3 # Copyright 2007-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2007-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from common import NoRepo, checktool, commandline, commit, converter_source
8 from common import NoRepo, checktool, commandline, commit, converter_source
9 from mercurial.i18n import _
9 from mercurial.i18n import _
10 from mercurial import util
10 from mercurial import util
11 import os, shutil, tempfile
11 import os, shutil, tempfile
12
12
13 # The naming drift of ElementTree is fun!
13 # The naming drift of ElementTree is fun!
14
14
15 try:
15 try:
16 from xml.etree.cElementTree import ElementTree
16 from xml.etree.cElementTree import ElementTree
17 except ImportError:
17 except ImportError:
18 try:
18 try:
19 from xml.etree.ElementTree import ElementTree
19 from xml.etree.ElementTree import ElementTree
20 except ImportError:
20 except ImportError:
21 try:
21 try:
22 from elementtree.cElementTree import ElementTree
22 from elementtree.cElementTree import ElementTree
23 except ImportError:
23 except ImportError:
24 try:
24 try:
25 from elementtree.ElementTree import ElementTree
25 from elementtree.ElementTree import ElementTree
26 except ImportError:
26 except ImportError:
27 ElementTree = None
27 ElementTree = None
28
28
29 class darcs_source(converter_source, commandline):
29 class darcs_source(converter_source, commandline):
30 def __init__(self, ui, path, rev=None):
30 def __init__(self, ui, path, rev=None):
31 converter_source.__init__(self, ui, path, rev=rev)
31 converter_source.__init__(self, ui, path, rev=rev)
32 commandline.__init__(self, ui, 'darcs')
32 commandline.__init__(self, ui, 'darcs')
33
33
34 # check for _darcs, ElementTree, _darcs/inventory so that we can
34 # check for _darcs, ElementTree, _darcs/inventory so that we can
35 # easily skip test-convert-darcs if ElementTree is not around
35 # easily skip test-convert-darcs if ElementTree is not around
36 if not os.path.exists(os.path.join(path, '_darcs', 'inventories')):
36 if not os.path.exists(os.path.join(path, '_darcs', 'inventories')):
37 raise NoRepo("%s does not look like a darcs repo" % path)
37 raise NoRepo("%s does not look like a darcs repository" % path)
38
38
39 if not os.path.exists(os.path.join(path, '_darcs')):
39 if not os.path.exists(os.path.join(path, '_darcs')):
40 raise NoRepo("%s does not look like a darcs repo" % path)
40 raise NoRepo("%s does not look like a darcs repository" % path)
41
41
42 checktool('darcs')
42 checktool('darcs')
43 version = self.run0('--version').splitlines()[0].strip()
43 version = self.run0('--version').splitlines()[0].strip()
44 if version < '2.1':
44 if version < '2.1':
45 raise util.Abort(_('darcs version 2.1 or newer needed (found %r)') %
45 raise util.Abort(_('darcs version 2.1 or newer needed (found %r)') %
46 version)
46 version)
47
47
48 if ElementTree is None:
48 if ElementTree is None:
49 raise util.Abort(_("Python ElementTree module is not available"))
49 raise util.Abort(_("Python ElementTree module is not available"))
50
50
51 self.path = os.path.realpath(path)
51 self.path = os.path.realpath(path)
52
52
53 self.lastrev = None
53 self.lastrev = None
54 self.changes = {}
54 self.changes = {}
55 self.parents = {}
55 self.parents = {}
56 self.tags = {}
56 self.tags = {}
57
57
58 def before(self):
58 def before(self):
59 self.tmppath = tempfile.mkdtemp(
59 self.tmppath = tempfile.mkdtemp(
60 prefix='convert-' + os.path.basename(self.path) + '-')
60 prefix='convert-' + os.path.basename(self.path) + '-')
61 output, status = self.run('init', repodir=self.tmppath)
61 output, status = self.run('init', repodir=self.tmppath)
62 self.checkexit(status)
62 self.checkexit(status)
63
63
64 tree = self.xml('changes', xml_output=True, summary=True,
64 tree = self.xml('changes', xml_output=True, summary=True,
65 repodir=self.path)
65 repodir=self.path)
66 tagname = None
66 tagname = None
67 child = None
67 child = None
68 for elt in tree.findall('patch'):
68 for elt in tree.findall('patch'):
69 node = elt.get('hash')
69 node = elt.get('hash')
70 name = elt.findtext('name', '')
70 name = elt.findtext('name', '')
71 if name.startswith('TAG '):
71 if name.startswith('TAG '):
72 tagname = name[4:].strip()
72 tagname = name[4:].strip()
73 elif tagname is not None:
73 elif tagname is not None:
74 self.tags[tagname] = node
74 self.tags[tagname] = node
75 tagname = None
75 tagname = None
76 self.changes[node] = elt
76 self.changes[node] = elt
77 self.parents[child] = [node]
77 self.parents[child] = [node]
78 child = node
78 child = node
79 self.parents[child] = []
79 self.parents[child] = []
80
80
81 def after(self):
81 def after(self):
82 self.ui.debug('cleaning up %s\n' % self.tmppath)
82 self.ui.debug('cleaning up %s\n' % self.tmppath)
83 shutil.rmtree(self.tmppath, ignore_errors=True)
83 shutil.rmtree(self.tmppath, ignore_errors=True)
84
84
85 def xml(self, cmd, **kwargs):
85 def xml(self, cmd, **kwargs):
86 etree = ElementTree()
86 etree = ElementTree()
87 fp = self._run(cmd, **kwargs)
87 fp = self._run(cmd, **kwargs)
88 etree.parse(fp)
88 etree.parse(fp)
89 self.checkexit(fp.close())
89 self.checkexit(fp.close())
90 return etree.getroot()
90 return etree.getroot()
91
91
92 def manifest(self):
92 def manifest(self):
93 man = []
93 man = []
94 output, status = self.run('show', 'files', no_directories=True,
94 output, status = self.run('show', 'files', no_directories=True,
95 repodir=self.tmppath)
95 repodir=self.tmppath)
96 self.checkexit(status)
96 self.checkexit(status)
97 for line in output.split('\n'):
97 for line in output.split('\n'):
98 path = line[2:]
98 path = line[2:]
99 if path:
99 if path:
100 man.append(path)
100 man.append(path)
101 return man
101 return man
102
102
103 def getheads(self):
103 def getheads(self):
104 return self.parents[None]
104 return self.parents[None]
105
105
106 def getcommit(self, rev):
106 def getcommit(self, rev):
107 elt = self.changes[rev]
107 elt = self.changes[rev]
108 date = util.strdate(elt.get('local_date'), '%a %b %d %H:%M:%S %Z %Y')
108 date = util.strdate(elt.get('local_date'), '%a %b %d %H:%M:%S %Z %Y')
109 desc = elt.findtext('name') + '\n' + elt.findtext('comment', '')
109 desc = elt.findtext('name') + '\n' + elt.findtext('comment', '')
110 return commit(author=elt.get('author'), date=util.datestr(date),
110 return commit(author=elt.get('author'), date=util.datestr(date),
111 desc=desc.strip(), parents=self.parents[rev])
111 desc=desc.strip(), parents=self.parents[rev])
112
112
113 def pull(self, rev):
113 def pull(self, rev):
114 output, status = self.run('pull', self.path, all=True,
114 output, status = self.run('pull', self.path, all=True,
115 match='hash %s' % rev,
115 match='hash %s' % rev,
116 no_test=True, no_posthook=True,
116 no_test=True, no_posthook=True,
117 external_merge='/bin/false',
117 external_merge='/bin/false',
118 repodir=self.tmppath)
118 repodir=self.tmppath)
119 if status:
119 if status:
120 if output.find('We have conflicts in') == -1:
120 if output.find('We have conflicts in') == -1:
121 self.checkexit(status, output)
121 self.checkexit(status, output)
122 output, status = self.run('revert', all=True, repodir=self.tmppath)
122 output, status = self.run('revert', all=True, repodir=self.tmppath)
123 self.checkexit(status, output)
123 self.checkexit(status, output)
124
124
125 def getchanges(self, rev):
125 def getchanges(self, rev):
126 copies = {}
126 copies = {}
127 changes = []
127 changes = []
128 man = None
128 man = None
129 for elt in self.changes[rev].find('summary').getchildren():
129 for elt in self.changes[rev].find('summary').getchildren():
130 if elt.tag in ('add_directory', 'remove_directory'):
130 if elt.tag in ('add_directory', 'remove_directory'):
131 continue
131 continue
132 if elt.tag == 'move':
132 if elt.tag == 'move':
133 if man is None:
133 if man is None:
134 man = self.manifest()
134 man = self.manifest()
135 source, dest = elt.get('from'), elt.get('to')
135 source, dest = elt.get('from'), elt.get('to')
136 if source in man:
136 if source in man:
137 # File move
137 # File move
138 changes.append((source, rev))
138 changes.append((source, rev))
139 changes.append((dest, rev))
139 changes.append((dest, rev))
140 copies[dest] = source
140 copies[dest] = source
141 else:
141 else:
142 # Directory move, deduce file moves from manifest
142 # Directory move, deduce file moves from manifest
143 source = source + '/'
143 source = source + '/'
144 for f in man:
144 for f in man:
145 if not f.startswith(source):
145 if not f.startswith(source):
146 continue
146 continue
147 fdest = dest + '/' + f[len(source):]
147 fdest = dest + '/' + f[len(source):]
148 changes.append((f, rev))
148 changes.append((f, rev))
149 changes.append((fdest, rev))
149 changes.append((fdest, rev))
150 copies[fdest] = f
150 copies[fdest] = f
151 else:
151 else:
152 changes.append((elt.text.strip(), rev))
152 changes.append((elt.text.strip(), rev))
153 self.pull(rev)
153 self.pull(rev)
154 self.lastrev = rev
154 self.lastrev = rev
155 return sorted(changes), copies
155 return sorted(changes), copies
156
156
157 def getfile(self, name, rev):
157 def getfile(self, name, rev):
158 if rev != self.lastrev:
158 if rev != self.lastrev:
159 raise util.Abort(_('internal calling inconsistency'))
159 raise util.Abort(_('internal calling inconsistency'))
160 return open(os.path.join(self.tmppath, name), 'rb').read()
160 return open(os.path.join(self.tmppath, name), 'rb').read()
161
161
162 def getmode(self, name, rev):
162 def getmode(self, name, rev):
163 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
163 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
164 return (mode & 0111) and 'x' or ''
164 return (mode & 0111) and 'x' or ''
165
165
166 def gettags(self):
166 def gettags(self):
167 return self.tags
167 return self.tags
@@ -1,155 +1,155 b''
1 # git.py - git support for the convert extension
1 # git.py - git support for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import os
8 import os
9 from mercurial import util
9 from mercurial import util
10
10
11 from common import NoRepo, commit, converter_source, checktool
11 from common import NoRepo, commit, converter_source, checktool
12
12
13 class convert_git(converter_source):
13 class convert_git(converter_source):
14 # Windows does not support GIT_DIR= construct while other systems
14 # Windows does not support GIT_DIR= construct while other systems
15 # cannot remove environment variable. Just assume none have
15 # cannot remove environment variable. Just assume none have
16 # both issues.
16 # both issues.
17 if hasattr(os, 'unsetenv'):
17 if hasattr(os, 'unsetenv'):
18 def gitcmd(self, s):
18 def gitcmd(self, s):
19 prevgitdir = os.environ.get('GIT_DIR')
19 prevgitdir = os.environ.get('GIT_DIR')
20 os.environ['GIT_DIR'] = self.path
20 os.environ['GIT_DIR'] = self.path
21 try:
21 try:
22 return util.popen(s, 'rb')
22 return util.popen(s, 'rb')
23 finally:
23 finally:
24 if prevgitdir is None:
24 if prevgitdir is None:
25 del os.environ['GIT_DIR']
25 del os.environ['GIT_DIR']
26 else:
26 else:
27 os.environ['GIT_DIR'] = prevgitdir
27 os.environ['GIT_DIR'] = prevgitdir
28 else:
28 else:
29 def gitcmd(self, s):
29 def gitcmd(self, s):
30 return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')
30 return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')
31
31
32 def __init__(self, ui, path, rev=None):
32 def __init__(self, ui, path, rev=None):
33 super(convert_git, self).__init__(ui, path, rev=rev)
33 super(convert_git, self).__init__(ui, path, rev=rev)
34
34
35 if os.path.isdir(path + "/.git"):
35 if os.path.isdir(path + "/.git"):
36 path += "/.git"
36 path += "/.git"
37 if not os.path.exists(path + "/objects"):
37 if not os.path.exists(path + "/objects"):
38 raise NoRepo("%s does not look like a Git repo" % path)
38 raise NoRepo("%s does not look like a Git repository" % path)
39
39
40 checktool('git', 'git')
40 checktool('git', 'git')
41
41
42 self.path = path
42 self.path = path
43
43
44 def getheads(self):
44 def getheads(self):
45 if not self.rev:
45 if not self.rev:
46 fh = self.gitcmd('git rev-parse --branches --remotes')
46 fh = self.gitcmd('git rev-parse --branches --remotes')
47 return fh.read().splitlines()
47 return fh.read().splitlines()
48 else:
48 else:
49 fh = self.gitcmd("git rev-parse --verify %s" % self.rev)
49 fh = self.gitcmd("git rev-parse --verify %s" % self.rev)
50 return [fh.read()[:-1]]
50 return [fh.read()[:-1]]
51
51
52 def catfile(self, rev, type):
52 def catfile(self, rev, type):
53 if rev == "0" * 40:
53 if rev == "0" * 40:
54 raise IOError()
54 raise IOError()
55 fh = self.gitcmd("git cat-file %s %s" % (type, rev))
55 fh = self.gitcmd("git cat-file %s %s" % (type, rev))
56 return fh.read()
56 return fh.read()
57
57
58 def getfile(self, name, rev):
58 def getfile(self, name, rev):
59 return self.catfile(rev, "blob")
59 return self.catfile(rev, "blob")
60
60
61 def getmode(self, name, rev):
61 def getmode(self, name, rev):
62 return self.modecache[(name, rev)]
62 return self.modecache[(name, rev)]
63
63
64 def getchanges(self, version):
64 def getchanges(self, version):
65 self.modecache = {}
65 self.modecache = {}
66 fh = self.gitcmd("git diff-tree -z --root -m -r %s" % version)
66 fh = self.gitcmd("git diff-tree -z --root -m -r %s" % version)
67 changes = []
67 changes = []
68 seen = set()
68 seen = set()
69 entry = None
69 entry = None
70 for l in fh.read().split('\x00'):
70 for l in fh.read().split('\x00'):
71 if not entry:
71 if not entry:
72 if not l.startswith(':'):
72 if not l.startswith(':'):
73 continue
73 continue
74 entry = l
74 entry = l
75 continue
75 continue
76 f = l
76 f = l
77 if f not in seen:
77 if f not in seen:
78 seen.add(f)
78 seen.add(f)
79 entry = entry.split()
79 entry = entry.split()
80 h = entry[3]
80 h = entry[3]
81 p = (entry[1] == "100755")
81 p = (entry[1] == "100755")
82 s = (entry[1] == "120000")
82 s = (entry[1] == "120000")
83 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
83 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
84 changes.append((f, h))
84 changes.append((f, h))
85 entry = None
85 entry = None
86 return (changes, {})
86 return (changes, {})
87
87
88 def getcommit(self, version):
88 def getcommit(self, version):
89 c = self.catfile(version, "commit") # read the commit hash
89 c = self.catfile(version, "commit") # read the commit hash
90 end = c.find("\n\n")
90 end = c.find("\n\n")
91 message = c[end + 2:]
91 message = c[end + 2:]
92 message = self.recode(message)
92 message = self.recode(message)
93 l = c[:end].splitlines()
93 l = c[:end].splitlines()
94 parents = []
94 parents = []
95 author = committer = None
95 author = committer = None
96 for e in l[1:]:
96 for e in l[1:]:
97 n, v = e.split(" ", 1)
97 n, v = e.split(" ", 1)
98 if n == "author":
98 if n == "author":
99 p = v.split()
99 p = v.split()
100 tm, tz = p[-2:]
100 tm, tz = p[-2:]
101 author = " ".join(p[:-2])
101 author = " ".join(p[:-2])
102 if author[0] == "<": author = author[1:-1]
102 if author[0] == "<": author = author[1:-1]
103 author = self.recode(author)
103 author = self.recode(author)
104 if n == "committer":
104 if n == "committer":
105 p = v.split()
105 p = v.split()
106 tm, tz = p[-2:]
106 tm, tz = p[-2:]
107 committer = " ".join(p[:-2])
107 committer = " ".join(p[:-2])
108 if committer[0] == "<": committer = committer[1:-1]
108 if committer[0] == "<": committer = committer[1:-1]
109 committer = self.recode(committer)
109 committer = self.recode(committer)
110 if n == "parent":
110 if n == "parent":
111 parents.append(v)
111 parents.append(v)
112
112
113 if committer and committer != author:
113 if committer and committer != author:
114 message += "\ncommitter: %s\n" % committer
114 message += "\ncommitter: %s\n" % committer
115 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
115 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
116 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
116 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
117 date = tm + " " + str(tz)
117 date = tm + " " + str(tz)
118
118
119 c = commit(parents=parents, date=date, author=author, desc=message,
119 c = commit(parents=parents, date=date, author=author, desc=message,
120 rev=version)
120 rev=version)
121 return c
121 return c
122
122
123 def gettags(self):
123 def gettags(self):
124 tags = {}
124 tags = {}
125 fh = self.gitcmd('git ls-remote --tags "%s"' % self.path)
125 fh = self.gitcmd('git ls-remote --tags "%s"' % self.path)
126 prefix = 'refs/tags/'
126 prefix = 'refs/tags/'
127 for line in fh:
127 for line in fh:
128 line = line.strip()
128 line = line.strip()
129 if not line.endswith("^{}"):
129 if not line.endswith("^{}"):
130 continue
130 continue
131 node, tag = line.split(None, 1)
131 node, tag = line.split(None, 1)
132 if not tag.startswith(prefix):
132 if not tag.startswith(prefix):
133 continue
133 continue
134 tag = tag[len(prefix):-3]
134 tag = tag[len(prefix):-3]
135 tags[tag] = node
135 tags[tag] = node
136
136
137 return tags
137 return tags
138
138
139 def getchangedfiles(self, version, i):
139 def getchangedfiles(self, version, i):
140 changes = []
140 changes = []
141 if i is None:
141 if i is None:
142 fh = self.gitcmd("git diff-tree --root -m -r %s" % version)
142 fh = self.gitcmd("git diff-tree --root -m -r %s" % version)
143 for l in fh:
143 for l in fh:
144 if "\t" not in l:
144 if "\t" not in l:
145 continue
145 continue
146 m, f = l[:-1].split("\t")
146 m, f = l[:-1].split("\t")
147 changes.append(f)
147 changes.append(f)
148 fh.close()
148 fh.close()
149 else:
149 else:
150 fh = self.gitcmd('git diff-tree --name-only --root -r %s "%s^%s" --'
150 fh = self.gitcmd('git diff-tree --name-only --root -r %s "%s^%s" --'
151 % (version, version, i + 1))
151 % (version, version, i + 1))
152 changes = [f.rstrip('\n') for f in fh]
152 changes = [f.rstrip('\n') for f in fh]
153 fh.close()
153 fh.close()
154
154
155 return changes
155 return changes
@@ -1,345 +1,346 b''
1 # gnuarch.py - GNU Arch support for the convert extension
1 # gnuarch.py - GNU Arch support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Aleix Conchillo Flaque <aleix@member.fsf.org>
3 # Copyright 2008, 2009 Aleix Conchillo Flaque <aleix@member.fsf.org>
4 # and others
4 # and others
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from common import NoRepo, commandline, commit, converter_source
9 from common import NoRepo, commandline, commit, converter_source
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial import util
11 from mercurial import util
12 import os, shutil, tempfile, stat, locale
12 import os, shutil, tempfile, stat, locale
13 from email.Parser import Parser
13 from email.Parser import Parser
14
14
15 class gnuarch_source(converter_source, commandline):
15 class gnuarch_source(converter_source, commandline):
16
16
17 class gnuarch_rev(object):
17 class gnuarch_rev(object):
18 def __init__(self, rev):
18 def __init__(self, rev):
19 self.rev = rev
19 self.rev = rev
20 self.summary = ''
20 self.summary = ''
21 self.date = None
21 self.date = None
22 self.author = ''
22 self.author = ''
23 self.continuationof = None
23 self.continuationof = None
24 self.add_files = []
24 self.add_files = []
25 self.mod_files = []
25 self.mod_files = []
26 self.del_files = []
26 self.del_files = []
27 self.ren_files = {}
27 self.ren_files = {}
28 self.ren_dirs = {}
28 self.ren_dirs = {}
29
29
30 def __init__(self, ui, path, rev=None):
30 def __init__(self, ui, path, rev=None):
31 super(gnuarch_source, self).__init__(ui, path, rev=rev)
31 super(gnuarch_source, self).__init__(ui, path, rev=rev)
32
32
33 if not os.path.exists(os.path.join(path, '{arch}')):
33 if not os.path.exists(os.path.join(path, '{arch}')):
34 raise NoRepo(_("%s does not look like a GNU Arch repo") % path)
34 raise NoRepo(_("%s does not look like a GNU Arch repository")
35 % path)
35
36
36 # Could use checktool, but we want to check for baz or tla.
37 # Could use checktool, but we want to check for baz or tla.
37 self.execmd = None
38 self.execmd = None
38 if util.find_exe('baz'):
39 if util.find_exe('baz'):
39 self.execmd = 'baz'
40 self.execmd = 'baz'
40 else:
41 else:
41 if util.find_exe('tla'):
42 if util.find_exe('tla'):
42 self.execmd = 'tla'
43 self.execmd = 'tla'
43 else:
44 else:
44 raise util.Abort(_('cannot find a GNU Arch tool'))
45 raise util.Abort(_('cannot find a GNU Arch tool'))
45
46
46 commandline.__init__(self, ui, self.execmd)
47 commandline.__init__(self, ui, self.execmd)
47
48
48 self.path = os.path.realpath(path)
49 self.path = os.path.realpath(path)
49 self.tmppath = None
50 self.tmppath = None
50
51
51 self.treeversion = None
52 self.treeversion = None
52 self.lastrev = None
53 self.lastrev = None
53 self.changes = {}
54 self.changes = {}
54 self.parents = {}
55 self.parents = {}
55 self.tags = {}
56 self.tags = {}
56 self.modecache = {}
57 self.modecache = {}
57 self.catlogparser = Parser()
58 self.catlogparser = Parser()
58 self.locale = locale.getpreferredencoding()
59 self.locale = locale.getpreferredencoding()
59 self.archives = []
60 self.archives = []
60
61
61 def before(self):
62 def before(self):
62 # Get registered archives
63 # Get registered archives
63 self.archives = [i.rstrip('\n')
64 self.archives = [i.rstrip('\n')
64 for i in self.runlines0('archives', '-n')]
65 for i in self.runlines0('archives', '-n')]
65
66
66 if self.execmd == 'tla':
67 if self.execmd == 'tla':
67 output = self.run0('tree-version', self.path)
68 output = self.run0('tree-version', self.path)
68 else:
69 else:
69 output = self.run0('tree-version', '-d', self.path)
70 output = self.run0('tree-version', '-d', self.path)
70 self.treeversion = output.strip()
71 self.treeversion = output.strip()
71
72
72 # Get name of temporary directory
73 # Get name of temporary directory
73 version = self.treeversion.split('/')
74 version = self.treeversion.split('/')
74 self.tmppath = os.path.join(tempfile.gettempdir(),
75 self.tmppath = os.path.join(tempfile.gettempdir(),
75 'hg-%s' % version[1])
76 'hg-%s' % version[1])
76
77
77 # Generate parents dictionary
78 # Generate parents dictionary
78 self.parents[None] = []
79 self.parents[None] = []
79 treeversion = self.treeversion
80 treeversion = self.treeversion
80 child = None
81 child = None
81 while treeversion:
82 while treeversion:
82 self.ui.status(_('analyzing tree version %s...\n') % treeversion)
83 self.ui.status(_('analyzing tree version %s...\n') % treeversion)
83
84
84 archive = treeversion.split('/')[0]
85 archive = treeversion.split('/')[0]
85 if archive not in self.archives:
86 if archive not in self.archives:
86 self.ui.status(_('tree analysis stopped because it points to '
87 self.ui.status(_('tree analysis stopped because it points to '
87 'an unregistered archive %s...\n') % archive)
88 'an unregistered archive %s...\n') % archive)
88 break
89 break
89
90
90 # Get the complete list of revisions for that tree version
91 # Get the complete list of revisions for that tree version
91 output, status = self.runlines('revisions', '-r', '-f', treeversion)
92 output, status = self.runlines('revisions', '-r', '-f', treeversion)
92 self.checkexit(status, 'failed retrieveing revisions for %s'
93 self.checkexit(status, 'failed retrieveing revisions for %s'
93 % treeversion)
94 % treeversion)
94
95
95 # No new iteration unless a revision has a continuation-of header
96 # No new iteration unless a revision has a continuation-of header
96 treeversion = None
97 treeversion = None
97
98
98 for l in output:
99 for l in output:
99 rev = l.strip()
100 rev = l.strip()
100 self.changes[rev] = self.gnuarch_rev(rev)
101 self.changes[rev] = self.gnuarch_rev(rev)
101 self.parents[rev] = []
102 self.parents[rev] = []
102
103
103 # Read author, date and summary
104 # Read author, date and summary
104 catlog, status = self.run('cat-log', '-d', self.path, rev)
105 catlog, status = self.run('cat-log', '-d', self.path, rev)
105 if status:
106 if status:
106 catlog = self.run0('cat-archive-log', rev)
107 catlog = self.run0('cat-archive-log', rev)
107 self._parsecatlog(catlog, rev)
108 self._parsecatlog(catlog, rev)
108
109
109 # Populate the parents map
110 # Populate the parents map
110 self.parents[child].append(rev)
111 self.parents[child].append(rev)
111
112
112 # Keep track of the current revision as the child of the next
113 # Keep track of the current revision as the child of the next
113 # revision scanned
114 # revision scanned
114 child = rev
115 child = rev
115
116
116 # Check if we have to follow the usual incremental history
117 # Check if we have to follow the usual incremental history
117 # or if we have to 'jump' to a different treeversion given
118 # or if we have to 'jump' to a different treeversion given
118 # by the continuation-of header.
119 # by the continuation-of header.
119 if self.changes[rev].continuationof:
120 if self.changes[rev].continuationof:
120 treeversion = '--'.join(
121 treeversion = '--'.join(
121 self.changes[rev].continuationof.split('--')[:-1])
122 self.changes[rev].continuationof.split('--')[:-1])
122 break
123 break
123
124
124 # If we reached a base-0 revision w/o any continuation-of
125 # If we reached a base-0 revision w/o any continuation-of
125 # header, it means the tree history ends here.
126 # header, it means the tree history ends here.
126 if rev[-6:] == 'base-0':
127 if rev[-6:] == 'base-0':
127 break
128 break
128
129
129 def after(self):
130 def after(self):
130 self.ui.debug('cleaning up %s\n' % self.tmppath)
131 self.ui.debug('cleaning up %s\n' % self.tmppath)
131 shutil.rmtree(self.tmppath, ignore_errors=True)
132 shutil.rmtree(self.tmppath, ignore_errors=True)
132
133
133 def getheads(self):
134 def getheads(self):
134 return self.parents[None]
135 return self.parents[None]
135
136
136 def getfile(self, name, rev):
137 def getfile(self, name, rev):
137 if rev != self.lastrev:
138 if rev != self.lastrev:
138 raise util.Abort(_('internal calling inconsistency'))
139 raise util.Abort(_('internal calling inconsistency'))
139
140
140 # Raise IOError if necessary (i.e. deleted files).
141 # Raise IOError if necessary (i.e. deleted files).
141 if not os.path.exists(os.path.join(self.tmppath, name)):
142 if not os.path.exists(os.path.join(self.tmppath, name)):
142 raise IOError
143 raise IOError
143
144
144 data, mode = self._getfile(name, rev)
145 data, mode = self._getfile(name, rev)
145 self.modecache[(name, rev)] = mode
146 self.modecache[(name, rev)] = mode
146
147
147 return data
148 return data
148
149
149 def getmode(self, name, rev):
150 def getmode(self, name, rev):
150 return self.modecache[(name, rev)]
151 return self.modecache[(name, rev)]
151
152
152 def getchanges(self, rev):
153 def getchanges(self, rev):
153 self.modecache = {}
154 self.modecache = {}
154 self._update(rev)
155 self._update(rev)
155 changes = []
156 changes = []
156 copies = {}
157 copies = {}
157
158
158 for f in self.changes[rev].add_files:
159 for f in self.changes[rev].add_files:
159 changes.append((f, rev))
160 changes.append((f, rev))
160
161
161 for f in self.changes[rev].mod_files:
162 for f in self.changes[rev].mod_files:
162 changes.append((f, rev))
163 changes.append((f, rev))
163
164
164 for f in self.changes[rev].del_files:
165 for f in self.changes[rev].del_files:
165 changes.append((f, rev))
166 changes.append((f, rev))
166
167
167 for src in self.changes[rev].ren_files:
168 for src in self.changes[rev].ren_files:
168 to = self.changes[rev].ren_files[src]
169 to = self.changes[rev].ren_files[src]
169 changes.append((src, rev))
170 changes.append((src, rev))
170 changes.append((to, rev))
171 changes.append((to, rev))
171 copies[to] = src
172 copies[to] = src
172
173
173 for src in self.changes[rev].ren_dirs:
174 for src in self.changes[rev].ren_dirs:
174 to = self.changes[rev].ren_dirs[src]
175 to = self.changes[rev].ren_dirs[src]
175 chgs, cps = self._rendirchanges(src, to)
176 chgs, cps = self._rendirchanges(src, to)
176 changes += [(f, rev) for f in chgs]
177 changes += [(f, rev) for f in chgs]
177 copies.update(cps)
178 copies.update(cps)
178
179
179 self.lastrev = rev
180 self.lastrev = rev
180 return sorted(set(changes)), copies
181 return sorted(set(changes)), copies
181
182
182 def getcommit(self, rev):
183 def getcommit(self, rev):
183 changes = self.changes[rev]
184 changes = self.changes[rev]
184 return commit(author=changes.author, date=changes.date,
185 return commit(author=changes.author, date=changes.date,
185 desc=changes.summary, parents=self.parents[rev], rev=rev)
186 desc=changes.summary, parents=self.parents[rev], rev=rev)
186
187
187 def gettags(self):
188 def gettags(self):
188 return self.tags
189 return self.tags
189
190
190 def _execute(self, cmd, *args, **kwargs):
191 def _execute(self, cmd, *args, **kwargs):
191 cmdline = [self.execmd, cmd]
192 cmdline = [self.execmd, cmd]
192 cmdline += args
193 cmdline += args
193 cmdline = [util.shellquote(arg) for arg in cmdline]
194 cmdline = [util.shellquote(arg) for arg in cmdline]
194 cmdline += ['>', util.nulldev, '2>', util.nulldev]
195 cmdline += ['>', util.nulldev, '2>', util.nulldev]
195 cmdline = util.quotecommand(' '.join(cmdline))
196 cmdline = util.quotecommand(' '.join(cmdline))
196 self.ui.debug(cmdline, '\n')
197 self.ui.debug(cmdline, '\n')
197 return os.system(cmdline)
198 return os.system(cmdline)
198
199
199 def _update(self, rev):
200 def _update(self, rev):
200 self.ui.debug('applying revision %s...\n' % rev)
201 self.ui.debug('applying revision %s...\n' % rev)
201 changeset, status = self.runlines('replay', '-d', self.tmppath,
202 changeset, status = self.runlines('replay', '-d', self.tmppath,
202 rev)
203 rev)
203 if status:
204 if status:
204 # Something went wrong while merging (baz or tla
205 # Something went wrong while merging (baz or tla
205 # issue?), get latest revision and try from there
206 # issue?), get latest revision and try from there
206 shutil.rmtree(self.tmppath, ignore_errors=True)
207 shutil.rmtree(self.tmppath, ignore_errors=True)
207 self._obtainrevision(rev)
208 self._obtainrevision(rev)
208 else:
209 else:
209 old_rev = self.parents[rev][0]
210 old_rev = self.parents[rev][0]
210 self.ui.debug('computing changeset between %s and %s...\n'
211 self.ui.debug('computing changeset between %s and %s...\n'
211 % (old_rev, rev))
212 % (old_rev, rev))
212 self._parsechangeset(changeset, rev)
213 self._parsechangeset(changeset, rev)
213
214
214 def _getfile(self, name, rev):
215 def _getfile(self, name, rev):
215 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
216 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
216 if stat.S_ISLNK(mode):
217 if stat.S_ISLNK(mode):
217 data = os.readlink(os.path.join(self.tmppath, name))
218 data = os.readlink(os.path.join(self.tmppath, name))
218 mode = mode and 'l' or ''
219 mode = mode and 'l' or ''
219 else:
220 else:
220 data = open(os.path.join(self.tmppath, name), 'rb').read()
221 data = open(os.path.join(self.tmppath, name), 'rb').read()
221 mode = (mode & 0111) and 'x' or ''
222 mode = (mode & 0111) and 'x' or ''
222 return data, mode
223 return data, mode
223
224
224 def _exclude(self, name):
225 def _exclude(self, name):
225 exclude = ['{arch}', '.arch-ids', '.arch-inventory']
226 exclude = ['{arch}', '.arch-ids', '.arch-inventory']
226 for exc in exclude:
227 for exc in exclude:
227 if name.find(exc) != -1:
228 if name.find(exc) != -1:
228 return True
229 return True
229 return False
230 return False
230
231
231 def _readcontents(self, path):
232 def _readcontents(self, path):
232 files = []
233 files = []
233 contents = os.listdir(path)
234 contents = os.listdir(path)
234 while len(contents) > 0:
235 while len(contents) > 0:
235 c = contents.pop()
236 c = contents.pop()
236 p = os.path.join(path, c)
237 p = os.path.join(path, c)
237 # os.walk could be used, but here we avoid internal GNU
238 # os.walk could be used, but here we avoid internal GNU
238 # Arch files and directories, thus saving a lot time.
239 # Arch files and directories, thus saving a lot time.
239 if not self._exclude(p):
240 if not self._exclude(p):
240 if os.path.isdir(p):
241 if os.path.isdir(p):
241 contents += [os.path.join(c, f) for f in os.listdir(p)]
242 contents += [os.path.join(c, f) for f in os.listdir(p)]
242 else:
243 else:
243 files.append(c)
244 files.append(c)
244 return files
245 return files
245
246
246 def _rendirchanges(self, src, dest):
247 def _rendirchanges(self, src, dest):
247 changes = []
248 changes = []
248 copies = {}
249 copies = {}
249 files = self._readcontents(os.path.join(self.tmppath, dest))
250 files = self._readcontents(os.path.join(self.tmppath, dest))
250 for f in files:
251 for f in files:
251 s = os.path.join(src, f)
252 s = os.path.join(src, f)
252 d = os.path.join(dest, f)
253 d = os.path.join(dest, f)
253 changes.append(s)
254 changes.append(s)
254 changes.append(d)
255 changes.append(d)
255 copies[d] = s
256 copies[d] = s
256 return changes, copies
257 return changes, copies
257
258
258 def _obtainrevision(self, rev):
259 def _obtainrevision(self, rev):
259 self.ui.debug('obtaining revision %s...\n' % rev)
260 self.ui.debug('obtaining revision %s...\n' % rev)
260 output = self._execute('get', rev, self.tmppath)
261 output = self._execute('get', rev, self.tmppath)
261 self.checkexit(output)
262 self.checkexit(output)
262 self.ui.debug('analyzing revision %s...\n' % rev)
263 self.ui.debug('analyzing revision %s...\n' % rev)
263 files = self._readcontents(self.tmppath)
264 files = self._readcontents(self.tmppath)
264 self.changes[rev].add_files += files
265 self.changes[rev].add_files += files
265
266
266 def _stripbasepath(self, path):
267 def _stripbasepath(self, path):
267 if path.startswith('./'):
268 if path.startswith('./'):
268 return path[2:]
269 return path[2:]
269 return path
270 return path
270
271
271 def _parsecatlog(self, data, rev):
272 def _parsecatlog(self, data, rev):
272 try:
273 try:
273 catlog = self.catlogparser.parsestr(data)
274 catlog = self.catlogparser.parsestr(data)
274
275
275 # Commit date
276 # Commit date
276 self.changes[rev].date = util.datestr(
277 self.changes[rev].date = util.datestr(
277 util.strdate(catlog['Standard-date'],
278 util.strdate(catlog['Standard-date'],
278 '%Y-%m-%d %H:%M:%S'))
279 '%Y-%m-%d %H:%M:%S'))
279
280
280 # Commit author
281 # Commit author
281 self.changes[rev].author = self.recode(catlog['Creator'])
282 self.changes[rev].author = self.recode(catlog['Creator'])
282
283
283 # Commit description
284 # Commit description
284 self.changes[rev].summary = '\n\n'.join((catlog['Summary'],
285 self.changes[rev].summary = '\n\n'.join((catlog['Summary'],
285 catlog.get_payload()))
286 catlog.get_payload()))
286 self.changes[rev].summary = self.recode(self.changes[rev].summary)
287 self.changes[rev].summary = self.recode(self.changes[rev].summary)
287
288
288 # Commit revision origin when dealing with a branch or tag
289 # Commit revision origin when dealing with a branch or tag
289 if 'Continuation-of' in catlog:
290 if 'Continuation-of' in catlog:
290 self.changes[rev].continuationof = self.recode(
291 self.changes[rev].continuationof = self.recode(
291 catlog['Continuation-of'])
292 catlog['Continuation-of'])
292 except Exception:
293 except Exception:
293 raise util.Abort(_('could not parse cat-log of %s') % rev)
294 raise util.Abort(_('could not parse cat-log of %s') % rev)
294
295
295 def _parsechangeset(self, data, rev):
296 def _parsechangeset(self, data, rev):
296 for l in data:
297 for l in data:
297 l = l.strip()
298 l = l.strip()
298 # Added file (ignore added directory)
299 # Added file (ignore added directory)
299 if l.startswith('A') and not l.startswith('A/'):
300 if l.startswith('A') and not l.startswith('A/'):
300 file = self._stripbasepath(l[1:].strip())
301 file = self._stripbasepath(l[1:].strip())
301 if not self._exclude(file):
302 if not self._exclude(file):
302 self.changes[rev].add_files.append(file)
303 self.changes[rev].add_files.append(file)
303 # Deleted file (ignore deleted directory)
304 # Deleted file (ignore deleted directory)
304 elif l.startswith('D') and not l.startswith('D/'):
305 elif l.startswith('D') and not l.startswith('D/'):
305 file = self._stripbasepath(l[1:].strip())
306 file = self._stripbasepath(l[1:].strip())
306 if not self._exclude(file):
307 if not self._exclude(file):
307 self.changes[rev].del_files.append(file)
308 self.changes[rev].del_files.append(file)
308 # Modified binary file
309 # Modified binary file
309 elif l.startswith('Mb'):
310 elif l.startswith('Mb'):
310 file = self._stripbasepath(l[2:].strip())
311 file = self._stripbasepath(l[2:].strip())
311 if not self._exclude(file):
312 if not self._exclude(file):
312 self.changes[rev].mod_files.append(file)
313 self.changes[rev].mod_files.append(file)
313 # Modified link
314 # Modified link
314 elif l.startswith('M->'):
315 elif l.startswith('M->'):
315 file = self._stripbasepath(l[3:].strip())
316 file = self._stripbasepath(l[3:].strip())
316 if not self._exclude(file):
317 if not self._exclude(file):
317 self.changes[rev].mod_files.append(file)
318 self.changes[rev].mod_files.append(file)
318 # Modified file
319 # Modified file
319 elif l.startswith('M'):
320 elif l.startswith('M'):
320 file = self._stripbasepath(l[1:].strip())
321 file = self._stripbasepath(l[1:].strip())
321 if not self._exclude(file):
322 if not self._exclude(file):
322 self.changes[rev].mod_files.append(file)
323 self.changes[rev].mod_files.append(file)
323 # Renamed file (or link)
324 # Renamed file (or link)
324 elif l.startswith('=>'):
325 elif l.startswith('=>'):
325 files = l[2:].strip().split(' ')
326 files = l[2:].strip().split(' ')
326 if len(files) == 1:
327 if len(files) == 1:
327 files = l[2:].strip().split('\t')
328 files = l[2:].strip().split('\t')
328 src = self._stripbasepath(files[0])
329 src = self._stripbasepath(files[0])
329 dst = self._stripbasepath(files[1])
330 dst = self._stripbasepath(files[1])
330 if not self._exclude(src) and not self._exclude(dst):
331 if not self._exclude(src) and not self._exclude(dst):
331 self.changes[rev].ren_files[src] = dst
332 self.changes[rev].ren_files[src] = dst
332 # Conversion from file to link or from link to file (modified)
333 # Conversion from file to link or from link to file (modified)
333 elif l.startswith('ch'):
334 elif l.startswith('ch'):
334 file = self._stripbasepath(l[2:].strip())
335 file = self._stripbasepath(l[2:].strip())
335 if not self._exclude(file):
336 if not self._exclude(file):
336 self.changes[rev].mod_files.append(file)
337 self.changes[rev].mod_files.append(file)
337 # Renamed directory
338 # Renamed directory
338 elif l.startswith('/>'):
339 elif l.startswith('/>'):
339 dirs = l[2:].strip().split(' ')
340 dirs = l[2:].strip().split(' ')
340 if len(dirs) == 1:
341 if len(dirs) == 1:
341 dirs = l[2:].strip().split('\t')
342 dirs = l[2:].strip().split('\t')
342 src = self._stripbasepath(dirs[0])
343 src = self._stripbasepath(dirs[0])
343 dst = self._stripbasepath(dirs[1])
344 dst = self._stripbasepath(dirs[1])
344 if not self._exclude(src) and not self._exclude(dst):
345 if not self._exclude(src) and not self._exclude(dst):
345 self.changes[rev].ren_dirs[src] = dst
346 self.changes[rev].ren_dirs[src] = dst
@@ -1,374 +1,377 b''
1 # hg.py - hg backend for convert extension
1 # hg.py - hg backend for convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 # Notes for hg->hg conversion:
8 # Notes for hg->hg conversion:
9 #
9 #
10 # * Old versions of Mercurial didn't trim the whitespace from the ends
10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 # of commit messages, but new versions do. Changesets created by
11 # of commit messages, but new versions do. Changesets created by
12 # those older versions, then converted, may thus have different
12 # those older versions, then converted, may thus have different
13 # hashes for changesets that are otherwise identical.
13 # hashes for changesets that are otherwise identical.
14 #
14 #
15 # * Using "--config convert.hg.saverev=true" will make the source
15 # * Using "--config convert.hg.saverev=true" will make the source
16 # identifier to be stored in the converted revision. This will cause
16 # identifier to be stored in the converted revision. This will cause
17 # the converted revision to have a different identity than the
17 # the converted revision to have a different identity than the
18 # source.
18 # source.
19
19
20
20
21 import os, time, cStringIO
21 import os, time, cStringIO
22 from mercurial.i18n import _
22 from mercurial.i18n import _
23 from mercurial.node import bin, hex, nullid
23 from mercurial.node import bin, hex, nullid
24 from mercurial import hg, util, context, error
24 from mercurial import hg, util, context, error
25
25
26 from common import NoRepo, commit, converter_source, converter_sink
26 from common import NoRepo, commit, converter_source, converter_sink
27
27
28 class mercurial_sink(converter_sink):
28 class mercurial_sink(converter_sink):
29 def __init__(self, ui, path):
29 def __init__(self, ui, path):
30 converter_sink.__init__(self, ui, path)
30 converter_sink.__init__(self, ui, path)
31 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
31 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
32 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
32 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
33 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
33 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
34 self.lastbranch = None
34 self.lastbranch = None
35 if os.path.isdir(path) and len(os.listdir(path)) > 0:
35 if os.path.isdir(path) and len(os.listdir(path)) > 0:
36 try:
36 try:
37 self.repo = hg.repository(self.ui, path)
37 self.repo = hg.repository(self.ui, path)
38 if not self.repo.local():
38 if not self.repo.local():
39 raise NoRepo(_('%s is not a local Mercurial repo') % path)
39 raise NoRepo(_('%s is not a local Mercurial repository')
40 % path)
40 except error.RepoError, err:
41 except error.RepoError, err:
41 ui.traceback()
42 ui.traceback()
42 raise NoRepo(err.args[0])
43 raise NoRepo(err.args[0])
43 else:
44 else:
44 try:
45 try:
45 ui.status(_('initializing destination %s repository\n') % path)
46 ui.status(_('initializing destination %s repository\n') % path)
46 self.repo = hg.repository(self.ui, path, create=True)
47 self.repo = hg.repository(self.ui, path, create=True)
47 if not self.repo.local():
48 if not self.repo.local():
48 raise NoRepo(_('%s is not a local Mercurial repo') % path)
49 raise NoRepo(_('%s is not a local Mercurial repository')
50 % path)
49 self.created.append(path)
51 self.created.append(path)
50 except error.RepoError:
52 except error.RepoError:
51 ui.traceback()
53 ui.traceback()
52 raise NoRepo("could not create hg repo %s as sink" % path)
54 raise NoRepo("could not create hg repository %s as sink"
55 % path)
53 self.lock = None
56 self.lock = None
54 self.wlock = None
57 self.wlock = None
55 self.filemapmode = False
58 self.filemapmode = False
56
59
57 def before(self):
60 def before(self):
58 self.ui.debug('run hg sink pre-conversion action\n')
61 self.ui.debug('run hg sink pre-conversion action\n')
59 self.wlock = self.repo.wlock()
62 self.wlock = self.repo.wlock()
60 self.lock = self.repo.lock()
63 self.lock = self.repo.lock()
61
64
62 def after(self):
65 def after(self):
63 self.ui.debug('run hg sink post-conversion action\n')
66 self.ui.debug('run hg sink post-conversion action\n')
64 if self.lock:
67 if self.lock:
65 self.lock.release()
68 self.lock.release()
66 if self.wlock:
69 if self.wlock:
67 self.wlock.release()
70 self.wlock.release()
68
71
69 def revmapfile(self):
72 def revmapfile(self):
70 return os.path.join(self.path, ".hg", "shamap")
73 return os.path.join(self.path, ".hg", "shamap")
71
74
72 def authorfile(self):
75 def authorfile(self):
73 return os.path.join(self.path, ".hg", "authormap")
76 return os.path.join(self.path, ".hg", "authormap")
74
77
75 def getheads(self):
78 def getheads(self):
76 h = self.repo.changelog.heads()
79 h = self.repo.changelog.heads()
77 return [hex(x) for x in h]
80 return [hex(x) for x in h]
78
81
79 def setbranch(self, branch, pbranches):
82 def setbranch(self, branch, pbranches):
80 if not self.clonebranches:
83 if not self.clonebranches:
81 return
84 return
82
85
83 setbranch = (branch != self.lastbranch)
86 setbranch = (branch != self.lastbranch)
84 self.lastbranch = branch
87 self.lastbranch = branch
85 if not branch:
88 if not branch:
86 branch = 'default'
89 branch = 'default'
87 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
90 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
88 pbranch = pbranches and pbranches[0][1] or 'default'
91 pbranch = pbranches and pbranches[0][1] or 'default'
89
92
90 branchpath = os.path.join(self.path, branch)
93 branchpath = os.path.join(self.path, branch)
91 if setbranch:
94 if setbranch:
92 self.after()
95 self.after()
93 try:
96 try:
94 self.repo = hg.repository(self.ui, branchpath)
97 self.repo = hg.repository(self.ui, branchpath)
95 except:
98 except:
96 self.repo = hg.repository(self.ui, branchpath, create=True)
99 self.repo = hg.repository(self.ui, branchpath, create=True)
97 self.before()
100 self.before()
98
101
99 # pbranches may bring revisions from other branches (merge parents)
102 # pbranches may bring revisions from other branches (merge parents)
100 # Make sure we have them, or pull them.
103 # Make sure we have them, or pull them.
101 missings = {}
104 missings = {}
102 for b in pbranches:
105 for b in pbranches:
103 try:
106 try:
104 self.repo.lookup(b[0])
107 self.repo.lookup(b[0])
105 except:
108 except:
106 missings.setdefault(b[1], []).append(b[0])
109 missings.setdefault(b[1], []).append(b[0])
107
110
108 if missings:
111 if missings:
109 self.after()
112 self.after()
110 for pbranch, heads in missings.iteritems():
113 for pbranch, heads in missings.iteritems():
111 pbranchpath = os.path.join(self.path, pbranch)
114 pbranchpath = os.path.join(self.path, pbranch)
112 prepo = hg.repository(self.ui, pbranchpath)
115 prepo = hg.repository(self.ui, pbranchpath)
113 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
116 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
114 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
117 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
115 self.before()
118 self.before()
116
119
117 def _rewritetags(self, source, revmap, data):
120 def _rewritetags(self, source, revmap, data):
118 fp = cStringIO.StringIO()
121 fp = cStringIO.StringIO()
119 for line in data.splitlines():
122 for line in data.splitlines():
120 s = line.split(' ', 1)
123 s = line.split(' ', 1)
121 if len(s) != 2:
124 if len(s) != 2:
122 continue
125 continue
123 revid = revmap.get(source.lookuprev(s[0]))
126 revid = revmap.get(source.lookuprev(s[0]))
124 if not revid:
127 if not revid:
125 continue
128 continue
126 fp.write('%s %s\n' % (revid, s[1]))
129 fp.write('%s %s\n' % (revid, s[1]))
127 return fp.getvalue()
130 return fp.getvalue()
128
131
129 def putcommit(self, files, copies, parents, commit, source, revmap):
132 def putcommit(self, files, copies, parents, commit, source, revmap):
130
133
131 files = dict(files)
134 files = dict(files)
132 def getfilectx(repo, memctx, f):
135 def getfilectx(repo, memctx, f):
133 v = files[f]
136 v = files[f]
134 data = source.getfile(f, v)
137 data = source.getfile(f, v)
135 e = source.getmode(f, v)
138 e = source.getmode(f, v)
136 if f == '.hgtags':
139 if f == '.hgtags':
137 data = self._rewritetags(source, revmap, data)
140 data = self._rewritetags(source, revmap, data)
138 return context.memfilectx(f, data, 'l' in e, 'x' in e, copies.get(f))
141 return context.memfilectx(f, data, 'l' in e, 'x' in e, copies.get(f))
139
142
140 pl = []
143 pl = []
141 for p in parents:
144 for p in parents:
142 if p not in pl:
145 if p not in pl:
143 pl.append(p)
146 pl.append(p)
144 parents = pl
147 parents = pl
145 nparents = len(parents)
148 nparents = len(parents)
146 if self.filemapmode and nparents == 1:
149 if self.filemapmode and nparents == 1:
147 m1node = self.repo.changelog.read(bin(parents[0]))[0]
150 m1node = self.repo.changelog.read(bin(parents[0]))[0]
148 parent = parents[0]
151 parent = parents[0]
149
152
150 if len(parents) < 2:
153 if len(parents) < 2:
151 parents.append(nullid)
154 parents.append(nullid)
152 if len(parents) < 2:
155 if len(parents) < 2:
153 parents.append(nullid)
156 parents.append(nullid)
154 p2 = parents.pop(0)
157 p2 = parents.pop(0)
155
158
156 text = commit.desc
159 text = commit.desc
157 extra = commit.extra.copy()
160 extra = commit.extra.copy()
158 if self.branchnames and commit.branch:
161 if self.branchnames and commit.branch:
159 extra['branch'] = commit.branch
162 extra['branch'] = commit.branch
160 if commit.rev:
163 if commit.rev:
161 extra['convert_revision'] = commit.rev
164 extra['convert_revision'] = commit.rev
162
165
163 while parents:
166 while parents:
164 p1 = p2
167 p1 = p2
165 p2 = parents.pop(0)
168 p2 = parents.pop(0)
166 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(),
169 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(),
167 getfilectx, commit.author, commit.date, extra)
170 getfilectx, commit.author, commit.date, extra)
168 self.repo.commitctx(ctx)
171 self.repo.commitctx(ctx)
169 text = "(octopus merge fixup)\n"
172 text = "(octopus merge fixup)\n"
170 p2 = hex(self.repo.changelog.tip())
173 p2 = hex(self.repo.changelog.tip())
171
174
172 if self.filemapmode and nparents == 1:
175 if self.filemapmode and nparents == 1:
173 man = self.repo.manifest
176 man = self.repo.manifest
174 mnode = self.repo.changelog.read(bin(p2))[0]
177 mnode = self.repo.changelog.read(bin(p2))[0]
175 if not man.cmp(m1node, man.revision(mnode)):
178 if not man.cmp(m1node, man.revision(mnode)):
176 self.ui.status(_("filtering out empty revision\n"))
179 self.ui.status(_("filtering out empty revision\n"))
177 self.repo.rollback()
180 self.repo.rollback()
178 return parent
181 return parent
179 return p2
182 return p2
180
183
181 def puttags(self, tags):
184 def puttags(self, tags):
182 try:
185 try:
183 parentctx = self.repo[self.tagsbranch]
186 parentctx = self.repo[self.tagsbranch]
184 tagparent = parentctx.node()
187 tagparent = parentctx.node()
185 except error.RepoError:
188 except error.RepoError:
186 parentctx = None
189 parentctx = None
187 tagparent = nullid
190 tagparent = nullid
188
191
189 try:
192 try:
190 oldlines = sorted(parentctx['.hgtags'].data().splitlines(True))
193 oldlines = sorted(parentctx['.hgtags'].data().splitlines(True))
191 except:
194 except:
192 oldlines = []
195 oldlines = []
193
196
194 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
197 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
195 if newlines == oldlines:
198 if newlines == oldlines:
196 return None, None
199 return None, None
197 data = "".join(newlines)
200 data = "".join(newlines)
198 def getfilectx(repo, memctx, f):
201 def getfilectx(repo, memctx, f):
199 return context.memfilectx(f, data, False, False, None)
202 return context.memfilectx(f, data, False, False, None)
200
203
201 self.ui.status(_("updating tags\n"))
204 self.ui.status(_("updating tags\n"))
202 date = "%s 0" % int(time.mktime(time.gmtime()))
205 date = "%s 0" % int(time.mktime(time.gmtime()))
203 extra = {'branch': self.tagsbranch}
206 extra = {'branch': self.tagsbranch}
204 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
207 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
205 [".hgtags"], getfilectx, "convert-repo", date,
208 [".hgtags"], getfilectx, "convert-repo", date,
206 extra)
209 extra)
207 self.repo.commitctx(ctx)
210 self.repo.commitctx(ctx)
208 return hex(self.repo.changelog.tip()), hex(tagparent)
211 return hex(self.repo.changelog.tip()), hex(tagparent)
209
212
210 def setfilemapmode(self, active):
213 def setfilemapmode(self, active):
211 self.filemapmode = active
214 self.filemapmode = active
212
215
213 class mercurial_source(converter_source):
216 class mercurial_source(converter_source):
214 def __init__(self, ui, path, rev=None):
217 def __init__(self, ui, path, rev=None):
215 converter_source.__init__(self, ui, path, rev)
218 converter_source.__init__(self, ui, path, rev)
216 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
219 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
217 self.ignored = set()
220 self.ignored = set()
218 self.saverev = ui.configbool('convert', 'hg.saverev', False)
221 self.saverev = ui.configbool('convert', 'hg.saverev', False)
219 try:
222 try:
220 self.repo = hg.repository(self.ui, path)
223 self.repo = hg.repository(self.ui, path)
221 # try to provoke an exception if this isn't really a hg
224 # try to provoke an exception if this isn't really a hg
222 # repo, but some other bogus compatible-looking url
225 # repo, but some other bogus compatible-looking url
223 if not self.repo.local():
226 if not self.repo.local():
224 raise error.RepoError()
227 raise error.RepoError()
225 except error.RepoError:
228 except error.RepoError:
226 ui.traceback()
229 ui.traceback()
227 raise NoRepo("%s is not a local Mercurial repo" % path)
230 raise NoRepo("%s is not a local Mercurial repository" % path)
228 self.lastrev = None
231 self.lastrev = None
229 self.lastctx = None
232 self.lastctx = None
230 self._changescache = None
233 self._changescache = None
231 self.convertfp = None
234 self.convertfp = None
232 # Restrict converted revisions to startrev descendants
235 # Restrict converted revisions to startrev descendants
233 startnode = ui.config('convert', 'hg.startrev')
236 startnode = ui.config('convert', 'hg.startrev')
234 if startnode is not None:
237 if startnode is not None:
235 try:
238 try:
236 startnode = self.repo.lookup(startnode)
239 startnode = self.repo.lookup(startnode)
237 except error.RepoError:
240 except error.RepoError:
238 raise util.Abort(_('%s is not a valid start revision')
241 raise util.Abort(_('%s is not a valid start revision')
239 % startnode)
242 % startnode)
240 startrev = self.repo.changelog.rev(startnode)
243 startrev = self.repo.changelog.rev(startnode)
241 children = {startnode: 1}
244 children = {startnode: 1}
242 for rev in self.repo.changelog.descendants(startrev):
245 for rev in self.repo.changelog.descendants(startrev):
243 children[self.repo.changelog.node(rev)] = 1
246 children[self.repo.changelog.node(rev)] = 1
244 self.keep = children.__contains__
247 self.keep = children.__contains__
245 else:
248 else:
246 self.keep = util.always
249 self.keep = util.always
247
250
248 def changectx(self, rev):
251 def changectx(self, rev):
249 if self.lastrev != rev:
252 if self.lastrev != rev:
250 self.lastctx = self.repo[rev]
253 self.lastctx = self.repo[rev]
251 self.lastrev = rev
254 self.lastrev = rev
252 return self.lastctx
255 return self.lastctx
253
256
254 def parents(self, ctx):
257 def parents(self, ctx):
255 return [p for p in ctx.parents() if p and self.keep(p.node())]
258 return [p for p in ctx.parents() if p and self.keep(p.node())]
256
259
257 def getheads(self):
260 def getheads(self):
258 if self.rev:
261 if self.rev:
259 heads = [self.repo[self.rev].node()]
262 heads = [self.repo[self.rev].node()]
260 else:
263 else:
261 heads = self.repo.heads()
264 heads = self.repo.heads()
262 return [hex(h) for h in heads if self.keep(h)]
265 return [hex(h) for h in heads if self.keep(h)]
263
266
264 def getfile(self, name, rev):
267 def getfile(self, name, rev):
265 try:
268 try:
266 return self.changectx(rev)[name].data()
269 return self.changectx(rev)[name].data()
267 except error.LookupError, err:
270 except error.LookupError, err:
268 raise IOError(err)
271 raise IOError(err)
269
272
270 def getmode(self, name, rev):
273 def getmode(self, name, rev):
271 return self.changectx(rev).manifest().flags(name)
274 return self.changectx(rev).manifest().flags(name)
272
275
273 def getchanges(self, rev):
276 def getchanges(self, rev):
274 ctx = self.changectx(rev)
277 ctx = self.changectx(rev)
275 parents = self.parents(ctx)
278 parents = self.parents(ctx)
276 if not parents:
279 if not parents:
277 files = sorted(ctx.manifest())
280 files = sorted(ctx.manifest())
278 if self.ignoreerrors:
281 if self.ignoreerrors:
279 # calling getcopies() is a simple way to detect missing
282 # calling getcopies() is a simple way to detect missing
280 # revlogs and populate self.ignored
283 # revlogs and populate self.ignored
281 self.getcopies(ctx, parents, files)
284 self.getcopies(ctx, parents, files)
282 return [(f, rev) for f in files if f not in self.ignored], {}
285 return [(f, rev) for f in files if f not in self.ignored], {}
283 if self._changescache and self._changescache[0] == rev:
286 if self._changescache and self._changescache[0] == rev:
284 m, a, r = self._changescache[1]
287 m, a, r = self._changescache[1]
285 else:
288 else:
286 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
289 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
287 # getcopies() detects missing revlogs early, run it before
290 # getcopies() detects missing revlogs early, run it before
288 # filtering the changes.
291 # filtering the changes.
289 copies = self.getcopies(ctx, parents, m + a)
292 copies = self.getcopies(ctx, parents, m + a)
290 changes = [(name, rev) for name in m + a + r
293 changes = [(name, rev) for name in m + a + r
291 if name not in self.ignored]
294 if name not in self.ignored]
292 return sorted(changes), copies
295 return sorted(changes), copies
293
296
294 def getcopies(self, ctx, parents, files):
297 def getcopies(self, ctx, parents, files):
295 copies = {}
298 copies = {}
296 for name in files:
299 for name in files:
297 if name in self.ignored:
300 if name in self.ignored:
298 continue
301 continue
299 try:
302 try:
300 copysource, copynode = ctx.filectx(name).renamed()
303 copysource, copynode = ctx.filectx(name).renamed()
301 if copysource in self.ignored or not self.keep(copynode):
304 if copysource in self.ignored or not self.keep(copynode):
302 continue
305 continue
303 # Ignore copy sources not in parent revisions
306 # Ignore copy sources not in parent revisions
304 found = False
307 found = False
305 for p in parents:
308 for p in parents:
306 if copysource in p:
309 if copysource in p:
307 found = True
310 found = True
308 break
311 break
309 if not found:
312 if not found:
310 continue
313 continue
311 copies[name] = copysource
314 copies[name] = copysource
312 except TypeError:
315 except TypeError:
313 pass
316 pass
314 except error.LookupError, e:
317 except error.LookupError, e:
315 if not self.ignoreerrors:
318 if not self.ignoreerrors:
316 raise
319 raise
317 self.ignored.add(name)
320 self.ignored.add(name)
318 self.ui.warn(_('ignoring: %s\n') % e)
321 self.ui.warn(_('ignoring: %s\n') % e)
319 return copies
322 return copies
320
323
321 def getcommit(self, rev):
324 def getcommit(self, rev):
322 ctx = self.changectx(rev)
325 ctx = self.changectx(rev)
323 parents = [p.hex() for p in self.parents(ctx)]
326 parents = [p.hex() for p in self.parents(ctx)]
324 if self.saverev:
327 if self.saverev:
325 crev = rev
328 crev = rev
326 else:
329 else:
327 crev = None
330 crev = None
328 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
331 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
329 desc=ctx.description(), rev=crev, parents=parents,
332 desc=ctx.description(), rev=crev, parents=parents,
330 branch=ctx.branch(), extra=ctx.extra(),
333 branch=ctx.branch(), extra=ctx.extra(),
331 sortkey=ctx.rev())
334 sortkey=ctx.rev())
332
335
333 def gettags(self):
336 def gettags(self):
334 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
337 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
335 return dict([(name, hex(node)) for name, node in tags
338 return dict([(name, hex(node)) for name, node in tags
336 if self.keep(node)])
339 if self.keep(node)])
337
340
338 def getchangedfiles(self, rev, i):
341 def getchangedfiles(self, rev, i):
339 ctx = self.changectx(rev)
342 ctx = self.changectx(rev)
340 parents = self.parents(ctx)
343 parents = self.parents(ctx)
341 if not parents and i is None:
344 if not parents and i is None:
342 i = 0
345 i = 0
343 changes = [], ctx.manifest().keys(), []
346 changes = [], ctx.manifest().keys(), []
344 else:
347 else:
345 i = i or 0
348 i = i or 0
346 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
349 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
347 changes = [[f for f in l if f not in self.ignored] for l in changes]
350 changes = [[f for f in l if f not in self.ignored] for l in changes]
348
351
349 if i == 0:
352 if i == 0:
350 self._changescache = (rev, changes)
353 self._changescache = (rev, changes)
351
354
352 return changes[0] + changes[1] + changes[2]
355 return changes[0] + changes[1] + changes[2]
353
356
354 def converted(self, rev, destrev):
357 def converted(self, rev, destrev):
355 if self.convertfp is None:
358 if self.convertfp is None:
356 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
359 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
357 'a')
360 'a')
358 self.convertfp.write('%s %s\n' % (destrev, rev))
361 self.convertfp.write('%s %s\n' % (destrev, rev))
359 self.convertfp.flush()
362 self.convertfp.flush()
360
363
361 def before(self):
364 def before(self):
362 self.ui.debug('run hg source pre-conversion action\n')
365 self.ui.debug('run hg source pre-conversion action\n')
363
366
364 def after(self):
367 def after(self):
365 self.ui.debug('run hg source post-conversion action\n')
368 self.ui.debug('run hg source post-conversion action\n')
366
369
367 def hasnativeorder(self):
370 def hasnativeorder(self):
368 return True
371 return True
369
372
370 def lookuprev(self, rev):
373 def lookuprev(self, rev):
371 try:
374 try:
372 return hex(self.repo.lookup(rev))
375 return hex(self.repo.lookup(rev))
373 except error.RepoError:
376 except error.RepoError:
374 return None
377 return None
@@ -1,228 +1,229 b''
1 # monotone.py - monotone support for the convert extension
1 # monotone.py - monotone support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
4 # others
4 # others
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import os, re
9 import os, re
10 from mercurial import util
10 from mercurial import util
11 from common import NoRepo, commit, converter_source, checktool
11 from common import NoRepo, commit, converter_source, checktool
12 from common import commandline
12 from common import commandline
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14
14
15 class monotone_source(converter_source, commandline):
15 class monotone_source(converter_source, commandline):
16 def __init__(self, ui, path=None, rev=None):
16 def __init__(self, ui, path=None, rev=None):
17 converter_source.__init__(self, ui, path, rev)
17 converter_source.__init__(self, ui, path, rev)
18 commandline.__init__(self, ui, 'mtn')
18 commandline.__init__(self, ui, 'mtn')
19
19
20 self.ui = ui
20 self.ui = ui
21 self.path = path
21 self.path = path
22
22
23 norepo = NoRepo (_("%s does not look like a monotone repo") % path)
23 norepo = NoRepo(_("%s does not look like a monotone repository")
24 % path)
24 if not os.path.exists(os.path.join(path, '_MTN')):
25 if not os.path.exists(os.path.join(path, '_MTN')):
25 # Could be a monotone repository (SQLite db file)
26 # Could be a monotone repository (SQLite db file)
26 try:
27 try:
27 header = file(path, 'rb').read(16)
28 header = file(path, 'rb').read(16)
28 except:
29 except:
29 header = ''
30 header = ''
30 if header != 'SQLite format 3\x00':
31 if header != 'SQLite format 3\x00':
31 raise norepo
32 raise norepo
32
33
33 # regular expressions for parsing monotone output
34 # regular expressions for parsing monotone output
34 space = r'\s*'
35 space = r'\s*'
35 name = r'\s+"((?:\\"|[^"])*)"\s*'
36 name = r'\s+"((?:\\"|[^"])*)"\s*'
36 value = name
37 value = name
37 revision = r'\s+\[(\w+)\]\s*'
38 revision = r'\s+\[(\w+)\]\s*'
38 lines = r'(?:.|\n)+'
39 lines = r'(?:.|\n)+'
39
40
40 self.dir_re = re.compile(space + "dir" + name)
41 self.dir_re = re.compile(space + "dir" + name)
41 self.file_re = re.compile(space + "file" + name +
42 self.file_re = re.compile(space + "file" + name +
42 "content" + revision)
43 "content" + revision)
43 self.add_file_re = re.compile(space + "add_file" + name +
44 self.add_file_re = re.compile(space + "add_file" + name +
44 "content" + revision)
45 "content" + revision)
45 self.patch_re = re.compile(space + "patch" + name +
46 self.patch_re = re.compile(space + "patch" + name +
46 "from" + revision + "to" + revision)
47 "from" + revision + "to" + revision)
47 self.rename_re = re.compile(space + "rename" + name + "to" + name)
48 self.rename_re = re.compile(space + "rename" + name + "to" + name)
48 self.delete_re = re.compile(space + "delete" + name)
49 self.delete_re = re.compile(space + "delete" + name)
49 self.tag_re = re.compile(space + "tag" + name + "revision" +
50 self.tag_re = re.compile(space + "tag" + name + "revision" +
50 revision)
51 revision)
51 self.cert_re = re.compile(lines + space + "name" + name +
52 self.cert_re = re.compile(lines + space + "name" + name +
52 "value" + value)
53 "value" + value)
53
54
54 attr = space + "file" + lines + space + "attr" + space
55 attr = space + "file" + lines + space + "attr" + space
55 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
56 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
56 space + '"true"')
57 space + '"true"')
57
58
58 # cached data
59 # cached data
59 self.manifest_rev = None
60 self.manifest_rev = None
60 self.manifest = None
61 self.manifest = None
61 self.files = None
62 self.files = None
62 self.dirs = None
63 self.dirs = None
63
64
64 checktool('mtn', abort=False)
65 checktool('mtn', abort=False)
65
66
66 # test if there are any revisions
67 # test if there are any revisions
67 self.rev = None
68 self.rev = None
68 try:
69 try:
69 self.getheads()
70 self.getheads()
70 except:
71 except:
71 raise norepo
72 raise norepo
72 self.rev = rev
73 self.rev = rev
73
74
74 def mtnrun(self, *args, **kwargs):
75 def mtnrun(self, *args, **kwargs):
75 kwargs['d'] = self.path
76 kwargs['d'] = self.path
76 return self.run0('automate', *args, **kwargs)
77 return self.run0('automate', *args, **kwargs)
77
78
78 def mtnloadmanifest(self, rev):
79 def mtnloadmanifest(self, rev):
79 if self.manifest_rev == rev:
80 if self.manifest_rev == rev:
80 return
81 return
81 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
82 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
82 self.manifest_rev = rev
83 self.manifest_rev = rev
83 self.files = {}
84 self.files = {}
84 self.dirs = {}
85 self.dirs = {}
85
86
86 for e in self.manifest:
87 for e in self.manifest:
87 m = self.file_re.match(e)
88 m = self.file_re.match(e)
88 if m:
89 if m:
89 attr = ""
90 attr = ""
90 name = m.group(1)
91 name = m.group(1)
91 node = m.group(2)
92 node = m.group(2)
92 if self.attr_execute_re.match(e):
93 if self.attr_execute_re.match(e):
93 attr += "x"
94 attr += "x"
94 self.files[name] = (node, attr)
95 self.files[name] = (node, attr)
95 m = self.dir_re.match(e)
96 m = self.dir_re.match(e)
96 if m:
97 if m:
97 self.dirs[m.group(1)] = True
98 self.dirs[m.group(1)] = True
98
99
99 def mtnisfile(self, name, rev):
100 def mtnisfile(self, name, rev):
100 # a non-file could be a directory or a deleted or renamed file
101 # a non-file could be a directory or a deleted or renamed file
101 self.mtnloadmanifest(rev)
102 self.mtnloadmanifest(rev)
102 return name in self.files
103 return name in self.files
103
104
104 def mtnisdir(self, name, rev):
105 def mtnisdir(self, name, rev):
105 self.mtnloadmanifest(rev)
106 self.mtnloadmanifest(rev)
106 return name in self.dirs
107 return name in self.dirs
107
108
108 def mtngetcerts(self, rev):
109 def mtngetcerts(self, rev):
109 certs = {"author":"<missing>", "date":"<missing>",
110 certs = {"author":"<missing>", "date":"<missing>",
110 "changelog":"<missing>", "branch":"<missing>"}
111 "changelog":"<missing>", "branch":"<missing>"}
111 certlist = self.mtnrun("certs", rev)
112 certlist = self.mtnrun("certs", rev)
112 # mtn < 0.45:
113 # mtn < 0.45:
113 # key "test@selenic.com"
114 # key "test@selenic.com"
114 # mtn >= 0.45:
115 # mtn >= 0.45:
115 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
116 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
116 certlist = re.split('\n\n key ["\[]', certlist)
117 certlist = re.split('\n\n key ["\[]', certlist)
117 for e in certlist:
118 for e in certlist:
118 m = self.cert_re.match(e)
119 m = self.cert_re.match(e)
119 if m:
120 if m:
120 name, value = m.groups()
121 name, value = m.groups()
121 value = value.replace(r'\"', '"')
122 value = value.replace(r'\"', '"')
122 value = value.replace(r'\\', '\\')
123 value = value.replace(r'\\', '\\')
123 certs[name] = value
124 certs[name] = value
124 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
125 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
125 # and all times are stored in UTC
126 # and all times are stored in UTC
126 certs["date"] = certs["date"].split('.')[0] + " UTC"
127 certs["date"] = certs["date"].split('.')[0] + " UTC"
127 return certs
128 return certs
128
129
129 # implement the converter_source interface:
130 # implement the converter_source interface:
130
131
131 def getheads(self):
132 def getheads(self):
132 if not self.rev:
133 if not self.rev:
133 return self.mtnrun("leaves").splitlines()
134 return self.mtnrun("leaves").splitlines()
134 else:
135 else:
135 return [self.rev]
136 return [self.rev]
136
137
137 def getchanges(self, rev):
138 def getchanges(self, rev):
138 #revision = self.mtncmd("get_revision %s" % rev).split("\n\n")
139 #revision = self.mtncmd("get_revision %s" % rev).split("\n\n")
139 revision = self.mtnrun("get_revision", rev).split("\n\n")
140 revision = self.mtnrun("get_revision", rev).split("\n\n")
140 files = {}
141 files = {}
141 ignoremove = {}
142 ignoremove = {}
142 renameddirs = []
143 renameddirs = []
143 copies = {}
144 copies = {}
144 for e in revision:
145 for e in revision:
145 m = self.add_file_re.match(e)
146 m = self.add_file_re.match(e)
146 if m:
147 if m:
147 files[m.group(1)] = rev
148 files[m.group(1)] = rev
148 ignoremove[m.group(1)] = rev
149 ignoremove[m.group(1)] = rev
149 m = self.patch_re.match(e)
150 m = self.patch_re.match(e)
150 if m:
151 if m:
151 files[m.group(1)] = rev
152 files[m.group(1)] = rev
152 # Delete/rename is handled later when the convert engine
153 # Delete/rename is handled later when the convert engine
153 # discovers an IOError exception from getfile,
154 # discovers an IOError exception from getfile,
154 # but only if we add the "from" file to the list of changes.
155 # but only if we add the "from" file to the list of changes.
155 m = self.delete_re.match(e)
156 m = self.delete_re.match(e)
156 if m:
157 if m:
157 files[m.group(1)] = rev
158 files[m.group(1)] = rev
158 m = self.rename_re.match(e)
159 m = self.rename_re.match(e)
159 if m:
160 if m:
160 toname = m.group(2)
161 toname = m.group(2)
161 fromname = m.group(1)
162 fromname = m.group(1)
162 if self.mtnisfile(toname, rev):
163 if self.mtnisfile(toname, rev):
163 ignoremove[toname] = 1
164 ignoremove[toname] = 1
164 copies[toname] = fromname
165 copies[toname] = fromname
165 files[toname] = rev
166 files[toname] = rev
166 files[fromname] = rev
167 files[fromname] = rev
167 elif self.mtnisdir(toname, rev):
168 elif self.mtnisdir(toname, rev):
168 renameddirs.append((fromname, toname))
169 renameddirs.append((fromname, toname))
169
170
170 # Directory renames can be handled only once we have recorded
171 # Directory renames can be handled only once we have recorded
171 # all new files
172 # all new files
172 for fromdir, todir in renameddirs:
173 for fromdir, todir in renameddirs:
173 renamed = {}
174 renamed = {}
174 for tofile in self.files:
175 for tofile in self.files:
175 if tofile in ignoremove:
176 if tofile in ignoremove:
176 continue
177 continue
177 if tofile.startswith(todir + '/'):
178 if tofile.startswith(todir + '/'):
178 renamed[tofile] = fromdir + tofile[len(todir):]
179 renamed[tofile] = fromdir + tofile[len(todir):]
179 # Avoid chained moves like:
180 # Avoid chained moves like:
180 # d1(/a) => d3/d1(/a)
181 # d1(/a) => d3/d1(/a)
181 # d2 => d3
182 # d2 => d3
182 ignoremove[tofile] = 1
183 ignoremove[tofile] = 1
183 for tofile, fromfile in renamed.items():
184 for tofile, fromfile in renamed.items():
184 self.ui.debug (_("copying file in renamed directory "
185 self.ui.debug (_("copying file in renamed directory "
185 "from '%s' to '%s'")
186 "from '%s' to '%s'")
186 % (fromfile, tofile), '\n')
187 % (fromfile, tofile), '\n')
187 files[tofile] = rev
188 files[tofile] = rev
188 copies[tofile] = fromfile
189 copies[tofile] = fromfile
189 for fromfile in renamed.values():
190 for fromfile in renamed.values():
190 files[fromfile] = rev
191 files[fromfile] = rev
191
192
192 return (files.items(), copies)
193 return (files.items(), copies)
193
194
194 def getmode(self, name, rev):
195 def getmode(self, name, rev):
195 self.mtnloadmanifest(rev)
196 self.mtnloadmanifest(rev)
196 node, attr = self.files.get(name, (None, ""))
197 node, attr = self.files.get(name, (None, ""))
197 return attr
198 return attr
198
199
199 def getfile(self, name, rev):
200 def getfile(self, name, rev):
200 if not self.mtnisfile(name, rev):
201 if not self.mtnisfile(name, rev):
201 raise IOError() # file was deleted or renamed
202 raise IOError() # file was deleted or renamed
202 try:
203 try:
203 return self.mtnrun("get_file_of", name, r=rev)
204 return self.mtnrun("get_file_of", name, r=rev)
204 except:
205 except:
205 raise IOError() # file was deleted or renamed
206 raise IOError() # file was deleted or renamed
206
207
207 def getcommit(self, rev):
208 def getcommit(self, rev):
208 certs = self.mtngetcerts(rev)
209 certs = self.mtngetcerts(rev)
209 return commit(
210 return commit(
210 author=certs["author"],
211 author=certs["author"],
211 date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
212 date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
212 desc=certs["changelog"],
213 desc=certs["changelog"],
213 rev=rev,
214 rev=rev,
214 parents=self.mtnrun("parents", rev).splitlines(),
215 parents=self.mtnrun("parents", rev).splitlines(),
215 branch=certs["branch"])
216 branch=certs["branch"])
216
217
217 def gettags(self):
218 def gettags(self):
218 tags = {}
219 tags = {}
219 for e in self.mtnrun("tags").split("\n\n"):
220 for e in self.mtnrun("tags").split("\n\n"):
220 m = self.tag_re.match(e)
221 m = self.tag_re.match(e)
221 if m:
222 if m:
222 tags[m.group(1)] = m.group(2)
223 tags[m.group(1)] = m.group(2)
223 return tags
224 return tags
224
225
225 def getchangedfiles(self, rev, i):
226 def getchangedfiles(self, rev, i):
226 # This function is only needed to support --filemap
227 # This function is only needed to support --filemap
227 # ... and we don't support that
228 # ... and we don't support that
228 raise NotImplementedError()
229 raise NotImplementedError()
@@ -1,208 +1,208 b''
1 # Perforce source for convert extension.
1 # Perforce source for convert extension.
2 #
2 #
3 # Copyright 2009, Frank Kingswood <frank@kingswood-consulting.co.uk>
3 # Copyright 2009, Frank Kingswood <frank@kingswood-consulting.co.uk>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from mercurial import util
8 from mercurial import util
9 from mercurial.i18n import _
9 from mercurial.i18n import _
10
10
11 from common import commit, converter_source, checktool, NoRepo
11 from common import commit, converter_source, checktool, NoRepo
12 import marshal
12 import marshal
13 import re
13 import re
14
14
15 def loaditer(f):
15 def loaditer(f):
16 "Yield the dictionary objects generated by p4"
16 "Yield the dictionary objects generated by p4"
17 try:
17 try:
18 while True:
18 while True:
19 d = marshal.load(f)
19 d = marshal.load(f)
20 if not d:
20 if not d:
21 break
21 break
22 yield d
22 yield d
23 except EOFError:
23 except EOFError:
24 pass
24 pass
25
25
26 class p4_source(converter_source):
26 class p4_source(converter_source):
27 def __init__(self, ui, path, rev=None):
27 def __init__(self, ui, path, rev=None):
28 super(p4_source, self).__init__(ui, path, rev=rev)
28 super(p4_source, self).__init__(ui, path, rev=rev)
29
29
30 if "/" in path and not path.startswith('//'):
30 if "/" in path and not path.startswith('//'):
31 raise NoRepo('%s does not look like a P4 repo' % path)
31 raise NoRepo('%s does not look like a P4 repository' % path)
32
32
33 checktool('p4', abort=False)
33 checktool('p4', abort=False)
34
34
35 self.p4changes = {}
35 self.p4changes = {}
36 self.heads = {}
36 self.heads = {}
37 self.changeset = {}
37 self.changeset = {}
38 self.files = {}
38 self.files = {}
39 self.tags = {}
39 self.tags = {}
40 self.lastbranch = {}
40 self.lastbranch = {}
41 self.parent = {}
41 self.parent = {}
42 self.encoding = "latin_1"
42 self.encoding = "latin_1"
43 self.depotname = {} # mapping from local name to depot name
43 self.depotname = {} # mapping from local name to depot name
44 self.modecache = {}
44 self.modecache = {}
45 self.re_type = re.compile(
45 self.re_type = re.compile(
46 "([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)"
46 "([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)"
47 "(\+\w+)?$")
47 "(\+\w+)?$")
48 self.re_keywords = re.compile(
48 self.re_keywords = re.compile(
49 r"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)"
49 r"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)"
50 r":[^$\n]*\$")
50 r":[^$\n]*\$")
51 self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$")
51 self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$")
52
52
53 self._parse(ui, path)
53 self._parse(ui, path)
54
54
55 def _parse_view(self, path):
55 def _parse_view(self, path):
56 "Read changes affecting the path"
56 "Read changes affecting the path"
57 cmd = 'p4 -G changes -s submitted "%s"' % path
57 cmd = 'p4 -G changes -s submitted "%s"' % path
58 stdout = util.popen(cmd, mode='rb')
58 stdout = util.popen(cmd, mode='rb')
59 for d in loaditer(stdout):
59 for d in loaditer(stdout):
60 c = d.get("change", None)
60 c = d.get("change", None)
61 if c:
61 if c:
62 self.p4changes[c] = True
62 self.p4changes[c] = True
63
63
64 def _parse(self, ui, path):
64 def _parse(self, ui, path):
65 "Prepare list of P4 filenames and revisions to import"
65 "Prepare list of P4 filenames and revisions to import"
66 ui.status(_('reading p4 views\n'))
66 ui.status(_('reading p4 views\n'))
67
67
68 # read client spec or view
68 # read client spec or view
69 if "/" in path:
69 if "/" in path:
70 self._parse_view(path)
70 self._parse_view(path)
71 if path.startswith("//") and path.endswith("/..."):
71 if path.startswith("//") and path.endswith("/..."):
72 views = {path[:-3]:""}
72 views = {path[:-3]:""}
73 else:
73 else:
74 views = {"//": ""}
74 views = {"//": ""}
75 else:
75 else:
76 cmd = 'p4 -G client -o "%s"' % path
76 cmd = 'p4 -G client -o "%s"' % path
77 clientspec = marshal.load(util.popen(cmd, mode='rb'))
77 clientspec = marshal.load(util.popen(cmd, mode='rb'))
78
78
79 views = {}
79 views = {}
80 for client in clientspec:
80 for client in clientspec:
81 if client.startswith("View"):
81 if client.startswith("View"):
82 sview, cview = clientspec[client].split()
82 sview, cview = clientspec[client].split()
83 self._parse_view(sview)
83 self._parse_view(sview)
84 if sview.endswith("...") and cview.endswith("..."):
84 if sview.endswith("...") and cview.endswith("..."):
85 sview = sview[:-3]
85 sview = sview[:-3]
86 cview = cview[:-3]
86 cview = cview[:-3]
87 cview = cview[2:]
87 cview = cview[2:]
88 cview = cview[cview.find("/") + 1:]
88 cview = cview[cview.find("/") + 1:]
89 views[sview] = cview
89 views[sview] = cview
90
90
91 # list of changes that affect our source files
91 # list of changes that affect our source files
92 self.p4changes = self.p4changes.keys()
92 self.p4changes = self.p4changes.keys()
93 self.p4changes.sort(key=int)
93 self.p4changes.sort(key=int)
94
94
95 # list with depot pathnames, longest first
95 # list with depot pathnames, longest first
96 vieworder = views.keys()
96 vieworder = views.keys()
97 vieworder.sort(key=len, reverse=True)
97 vieworder.sort(key=len, reverse=True)
98
98
99 # handle revision limiting
99 # handle revision limiting
100 startrev = self.ui.config('convert', 'p4.startrev', default=0)
100 startrev = self.ui.config('convert', 'p4.startrev', default=0)
101 self.p4changes = [x for x in self.p4changes
101 self.p4changes = [x for x in self.p4changes
102 if ((not startrev or int(x) >= int(startrev)) and
102 if ((not startrev or int(x) >= int(startrev)) and
103 (not self.rev or int(x) <= int(self.rev)))]
103 (not self.rev or int(x) <= int(self.rev)))]
104
104
105 # now read the full changelists to get the list of file revisions
105 # now read the full changelists to get the list of file revisions
106 ui.status(_('collecting p4 changelists\n'))
106 ui.status(_('collecting p4 changelists\n'))
107 lastid = None
107 lastid = None
108 for change in self.p4changes:
108 for change in self.p4changes:
109 cmd = "p4 -G describe %s" % change
109 cmd = "p4 -G describe %s" % change
110 stdout = util.popen(cmd, mode='rb')
110 stdout = util.popen(cmd, mode='rb')
111 d = marshal.load(stdout)
111 d = marshal.load(stdout)
112
112
113 desc = self.recode(d["desc"])
113 desc = self.recode(d["desc"])
114 shortdesc = desc.split("\n", 1)[0]
114 shortdesc = desc.split("\n", 1)[0]
115 t = '%s %s' % (d["change"], repr(shortdesc)[1:-1])
115 t = '%s %s' % (d["change"], repr(shortdesc)[1:-1])
116 ui.status(util.ellipsis(t, 80) + '\n')
116 ui.status(util.ellipsis(t, 80) + '\n')
117
117
118 if lastid:
118 if lastid:
119 parents = [lastid]
119 parents = [lastid]
120 else:
120 else:
121 parents = []
121 parents = []
122
122
123 date = (int(d["time"]), 0) # timezone not set
123 date = (int(d["time"]), 0) # timezone not set
124 c = commit(author=self.recode(d["user"]), date=util.datestr(date),
124 c = commit(author=self.recode(d["user"]), date=util.datestr(date),
125 parents=parents, desc=desc, branch='',
125 parents=parents, desc=desc, branch='',
126 extra={"p4": change})
126 extra={"p4": change})
127
127
128 files = []
128 files = []
129 i = 0
129 i = 0
130 while ("depotFile%d" % i) in d and ("rev%d" % i) in d:
130 while ("depotFile%d" % i) in d and ("rev%d" % i) in d:
131 oldname = d["depotFile%d" % i]
131 oldname = d["depotFile%d" % i]
132 filename = None
132 filename = None
133 for v in vieworder:
133 for v in vieworder:
134 if oldname.startswith(v):
134 if oldname.startswith(v):
135 filename = views[v] + oldname[len(v):]
135 filename = views[v] + oldname[len(v):]
136 break
136 break
137 if filename:
137 if filename:
138 files.append((filename, d["rev%d" % i]))
138 files.append((filename, d["rev%d" % i]))
139 self.depotname[filename] = oldname
139 self.depotname[filename] = oldname
140 i += 1
140 i += 1
141 self.changeset[change] = c
141 self.changeset[change] = c
142 self.files[change] = files
142 self.files[change] = files
143 lastid = change
143 lastid = change
144
144
145 if lastid:
145 if lastid:
146 self.heads = [lastid]
146 self.heads = [lastid]
147
147
148 def getheads(self):
148 def getheads(self):
149 return self.heads
149 return self.heads
150
150
151 def getfile(self, name, rev):
151 def getfile(self, name, rev):
152 cmd = 'p4 -G print "%s#%s"' % (self.depotname[name], rev)
152 cmd = 'p4 -G print "%s#%s"' % (self.depotname[name], rev)
153 stdout = util.popen(cmd, mode='rb')
153 stdout = util.popen(cmd, mode='rb')
154
154
155 mode = None
155 mode = None
156 contents = ""
156 contents = ""
157 keywords = None
157 keywords = None
158
158
159 for d in loaditer(stdout):
159 for d in loaditer(stdout):
160 code = d["code"]
160 code = d["code"]
161 data = d.get("data")
161 data = d.get("data")
162
162
163 if code == "error":
163 if code == "error":
164 raise IOError(d["generic"], data)
164 raise IOError(d["generic"], data)
165
165
166 elif code == "stat":
166 elif code == "stat":
167 p4type = self.re_type.match(d["type"])
167 p4type = self.re_type.match(d["type"])
168 if p4type:
168 if p4type:
169 mode = ""
169 mode = ""
170 flags = (p4type.group(1) or "") + (p4type.group(3) or "")
170 flags = (p4type.group(1) or "") + (p4type.group(3) or "")
171 if "x" in flags:
171 if "x" in flags:
172 mode = "x"
172 mode = "x"
173 if p4type.group(2) == "symlink":
173 if p4type.group(2) == "symlink":
174 mode = "l"
174 mode = "l"
175 if "ko" in flags:
175 if "ko" in flags:
176 keywords = self.re_keywords_old
176 keywords = self.re_keywords_old
177 elif "k" in flags:
177 elif "k" in flags:
178 keywords = self.re_keywords
178 keywords = self.re_keywords
179
179
180 elif code == "text" or code == "binary":
180 elif code == "text" or code == "binary":
181 contents += data
181 contents += data
182
182
183 if mode is None:
183 if mode is None:
184 raise IOError(0, "bad stat")
184 raise IOError(0, "bad stat")
185
185
186 self.modecache[(name, rev)] = mode
186 self.modecache[(name, rev)] = mode
187
187
188 if keywords:
188 if keywords:
189 contents = keywords.sub("$\\1$", contents)
189 contents = keywords.sub("$\\1$", contents)
190 if mode == "l" and contents.endswith("\n"):
190 if mode == "l" and contents.endswith("\n"):
191 contents = contents[:-1]
191 contents = contents[:-1]
192
192
193 return contents
193 return contents
194
194
195 def getmode(self, name, rev):
195 def getmode(self, name, rev):
196 return self.modecache[(name, rev)]
196 return self.modecache[(name, rev)]
197
197
198 def getchanges(self, rev):
198 def getchanges(self, rev):
199 return self.files[rev], {}
199 return self.files[rev], {}
200
200
201 def getcommit(self, rev):
201 def getcommit(self, rev):
202 return self.changeset[rev]
202 return self.changeset[rev]
203
203
204 def gettags(self):
204 def gettags(self):
205 return self.tags
205 return self.tags
206
206
207 def getchangedfiles(self, rev, i):
207 def getchangedfiles(self, rev, i):
208 return sorted([x[0] for x in self.files[rev]])
208 return sorted([x[0] for x in self.files[rev]])
@@ -1,1171 +1,1172 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 os
5 import os
6 import re
6 import re
7 import sys
7 import sys
8 import cPickle as pickle
8 import cPickle as pickle
9 import tempfile
9 import tempfile
10 import urllib
10 import urllib
11 import urllib2
11 import urllib2
12
12
13 from mercurial import strutil, util, encoding
13 from mercurial import strutil, util, encoding
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 # Module URL is later compared with the repository URL returned
53 # Module URL is later compared with the repository URL returned
54 # by svn API, which is UTF-8.
54 # by svn API, which is UTF-8.
55 path = encoding.tolocal(path)
55 path = encoding.tolocal(path)
56 return 'file://%s' % urllib.quote(path)
56 return 'file://%s' % urllib.quote(path)
57 return path
57 return path
58
58
59 def optrev(number):
59 def optrev(number):
60 optrev = svn.core.svn_opt_revision_t()
60 optrev = svn.core.svn_opt_revision_t()
61 optrev.kind = svn.core.svn_opt_revision_number
61 optrev.kind = svn.core.svn_opt_revision_number
62 optrev.value.number = number
62 optrev.value.number = number
63 return optrev
63 return optrev
64
64
65 class changedpath(object):
65 class changedpath(object):
66 def __init__(self, p):
66 def __init__(self, p):
67 self.copyfrom_path = p.copyfrom_path
67 self.copyfrom_path = p.copyfrom_path
68 self.copyfrom_rev = p.copyfrom_rev
68 self.copyfrom_rev = p.copyfrom_rev
69 self.action = p.action
69 self.action = p.action
70
70
71 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
71 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
72 strict_node_history=False):
72 strict_node_history=False):
73 protocol = -1
73 protocol = -1
74 def receiver(orig_paths, revnum, author, date, message, pool):
74 def receiver(orig_paths, revnum, author, date, message, pool):
75 if orig_paths is not None:
75 if orig_paths is not None:
76 for k, v in orig_paths.iteritems():
76 for k, v in orig_paths.iteritems():
77 orig_paths[k] = changedpath(v)
77 orig_paths[k] = changedpath(v)
78 pickle.dump((orig_paths, revnum, author, date, message),
78 pickle.dump((orig_paths, revnum, author, date, message),
79 fp, protocol)
79 fp, protocol)
80
80
81 try:
81 try:
82 # Use an ra of our own so that our parent can consume
82 # Use an ra of our own so that our parent can consume
83 # our results without confusing the server.
83 # our results without confusing the server.
84 t = transport.SvnRaTransport(url=url)
84 t = transport.SvnRaTransport(url=url)
85 svn.ra.get_log(t.ra, paths, start, end, limit,
85 svn.ra.get_log(t.ra, paths, start, end, limit,
86 discover_changed_paths,
86 discover_changed_paths,
87 strict_node_history,
87 strict_node_history,
88 receiver)
88 receiver)
89 except SubversionException, (inst, num):
89 except SubversionException, (inst, num):
90 pickle.dump(num, fp, protocol)
90 pickle.dump(num, fp, protocol)
91 except IOError:
91 except IOError:
92 # Caller may interrupt the iteration
92 # Caller may interrupt the iteration
93 pickle.dump(None, fp, protocol)
93 pickle.dump(None, fp, protocol)
94 else:
94 else:
95 pickle.dump(None, fp, protocol)
95 pickle.dump(None, fp, protocol)
96 fp.close()
96 fp.close()
97 # With large history, cleanup process goes crazy and suddenly
97 # With large history, cleanup process goes crazy and suddenly
98 # consumes *huge* amount of memory. The output file being closed,
98 # consumes *huge* amount of memory. The output file being closed,
99 # there is no need for clean termination.
99 # there is no need for clean termination.
100 os._exit(0)
100 os._exit(0)
101
101
102 def debugsvnlog(ui, **opts):
102 def debugsvnlog(ui, **opts):
103 """Fetch SVN log in a subprocess and channel them back to parent to
103 """Fetch SVN log in a subprocess and channel them back to parent to
104 avoid memory collection issues.
104 avoid memory collection issues.
105 """
105 """
106 util.set_binary(sys.stdin)
106 util.set_binary(sys.stdin)
107 util.set_binary(sys.stdout)
107 util.set_binary(sys.stdout)
108 args = decodeargs(sys.stdin.read())
108 args = decodeargs(sys.stdin.read())
109 get_log_child(sys.stdout, *args)
109 get_log_child(sys.stdout, *args)
110
110
111 class logstream(object):
111 class logstream(object):
112 """Interruptible revision log iterator."""
112 """Interruptible revision log iterator."""
113 def __init__(self, stdout):
113 def __init__(self, stdout):
114 self._stdout = stdout
114 self._stdout = stdout
115
115
116 def __iter__(self):
116 def __iter__(self):
117 while True:
117 while True:
118 try:
118 try:
119 entry = pickle.load(self._stdout)
119 entry = pickle.load(self._stdout)
120 except EOFError:
120 except EOFError:
121 raise util.Abort(_('Mercurial failed to run itself, check'
121 raise util.Abort(_('Mercurial failed to run itself, check'
122 ' hg executable is in PATH'))
122 ' hg executable is in PATH'))
123 try:
123 try:
124 orig_paths, revnum, author, date, message = entry
124 orig_paths, revnum, author, date, message = entry
125 except:
125 except:
126 if entry is None:
126 if entry is None:
127 break
127 break
128 raise SubversionException("child raised exception", entry)
128 raise SubversionException("child raised exception", entry)
129 yield entry
129 yield entry
130
130
131 def close(self):
131 def close(self):
132 if self._stdout:
132 if self._stdout:
133 self._stdout.close()
133 self._stdout.close()
134 self._stdout = None
134 self._stdout = None
135
135
136
136
137 # Check to see if the given path is a local Subversion repo. Verify this by
137 # Check to see if the given path is a local Subversion repo. Verify this by
138 # looking for several svn-specific files and directories in the given
138 # looking for several svn-specific files and directories in the given
139 # directory.
139 # directory.
140 def filecheck(ui, path, proto):
140 def filecheck(ui, path, proto):
141 for x in ('locks', 'hooks', 'format', 'db'):
141 for x in ('locks', 'hooks', 'format', 'db'):
142 if not os.path.exists(os.path.join(path, x)):
142 if not os.path.exists(os.path.join(path, x)):
143 return False
143 return False
144 return True
144 return True
145
145
146 # Check to see if a given path is the root of an svn repo over http. We verify
146 # Check to see if a given path is the root of an svn repo over http. We verify
147 # this by requesting a version-controlled URL we know can't exist and looking
147 # this by requesting a version-controlled URL we know can't exist and looking
148 # for the svn-specific "not found" XML.
148 # for the svn-specific "not found" XML.
149 def httpcheck(ui, path, proto):
149 def httpcheck(ui, path, proto):
150 try:
150 try:
151 opener = urllib2.build_opener()
151 opener = urllib2.build_opener()
152 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
152 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
153 data = rsp.read()
153 data = rsp.read()
154 except urllib2.HTTPError, inst:
154 except urllib2.HTTPError, inst:
155 if inst.code != 404:
155 if inst.code != 404:
156 # Except for 404 we cannot know for sure this is not an svn repo
156 # Except for 404 we cannot know for sure this is not an svn repo
157 ui.warn(_('svn: cannot probe remote repository, assume it could '
157 ui.warn(_('svn: cannot probe remote repository, assume it could '
158 'be a subversion repository. Use --source-type if you '
158 'be a subversion repository. Use --source-type if you '
159 'know better.\n'))
159 'know better.\n'))
160 return True
160 return True
161 data = inst.fp.read()
161 data = inst.fp.read()
162 except:
162 except:
163 # Could be urllib2.URLError if the URL is invalid or anything else.
163 # Could be urllib2.URLError if the URL is invalid or anything else.
164 return False
164 return False
165 return '<m:human-readable errcode="160013">' in data
165 return '<m:human-readable errcode="160013">' in data
166
166
167 protomap = {'http': httpcheck,
167 protomap = {'http': httpcheck,
168 'https': httpcheck,
168 'https': httpcheck,
169 'file': filecheck,
169 'file': filecheck,
170 }
170 }
171 def issvnurl(ui, url):
171 def issvnurl(ui, url):
172 try:
172 try:
173 proto, path = url.split('://', 1)
173 proto, path = url.split('://', 1)
174 if proto == 'file':
174 if proto == 'file':
175 path = urllib.url2pathname(path)
175 path = urllib.url2pathname(path)
176 except ValueError:
176 except ValueError:
177 proto = 'file'
177 proto = 'file'
178 path = os.path.abspath(url)
178 path = os.path.abspath(url)
179 if proto == 'file':
179 if proto == 'file':
180 path = path.replace(os.sep, '/')
180 path = path.replace(os.sep, '/')
181 check = protomap.get(proto, lambda *args: False)
181 check = protomap.get(proto, lambda *args: False)
182 while '/' in path:
182 while '/' in path:
183 if check(ui, path, proto):
183 if check(ui, path, proto):
184 return True
184 return True
185 path = path.rsplit('/', 1)[0]
185 path = path.rsplit('/', 1)[0]
186 return False
186 return False
187
187
188 # SVN conversion code stolen from bzr-svn and tailor
188 # SVN conversion code stolen from bzr-svn and tailor
189 #
189 #
190 # Subversion looks like a versioned filesystem, branches structures
190 # Subversion looks like a versioned filesystem, branches structures
191 # are defined by conventions and not enforced by the tool. First,
191 # are defined by conventions and not enforced by the tool. First,
192 # we define the potential branches (modules) as "trunk" and "branches"
192 # we define the potential branches (modules) as "trunk" and "branches"
193 # children directories. Revisions are then identified by their
193 # children directories. Revisions are then identified by their
194 # module and revision number (and a repository identifier).
194 # module and revision number (and a repository identifier).
195 #
195 #
196 # The revision graph is really a tree (or a forest). By default, a
196 # The revision graph is really a tree (or a forest). By default, a
197 # revision parent is the previous revision in the same module. If the
197 # revision parent is the previous revision in the same module. If the
198 # module directory is copied/moved from another module then the
198 # module directory is copied/moved from another module then the
199 # revision is the module root and its parent the source revision in
199 # revision is the module root and its parent the source revision in
200 # the parent module. A revision has at most one parent.
200 # the parent module. A revision has at most one parent.
201 #
201 #
202 class svn_source(converter_source):
202 class svn_source(converter_source):
203 def __init__(self, ui, url, rev=None):
203 def __init__(self, ui, url, rev=None):
204 super(svn_source, self).__init__(ui, url, rev=rev)
204 super(svn_source, self).__init__(ui, url, rev=rev)
205
205
206 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
206 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
207 (os.path.exists(url) and
207 (os.path.exists(url) and
208 os.path.exists(os.path.join(url, '.svn'))) or
208 os.path.exists(os.path.join(url, '.svn'))) or
209 issvnurl(ui, url)):
209 issvnurl(ui, url)):
210 raise NoRepo("%s does not look like a Subversion repo" % url)
210 raise NoRepo("%s does not look like a Subversion repository" % url)
211
211
212 try:
212 try:
213 SubversionException
213 SubversionException
214 except NameError:
214 except NameError:
215 raise MissingTool(_('Subversion python bindings could not be loaded'))
215 raise MissingTool(_('Subversion python bindings could not be loaded'))
216
216
217 try:
217 try:
218 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
218 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
219 if version < (1, 4):
219 if version < (1, 4):
220 raise MissingTool(_('Subversion python bindings %d.%d found, '
220 raise MissingTool(_('Subversion python bindings %d.%d found, '
221 '1.4 or later required') % version)
221 '1.4 or later required') % version)
222 except AttributeError:
222 except AttributeError:
223 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
223 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
224 'or later required'))
224 'or later required'))
225
225
226 self.lastrevs = {}
226 self.lastrevs = {}
227
227
228 latest = None
228 latest = None
229 try:
229 try:
230 # Support file://path@rev syntax. Useful e.g. to convert
230 # Support file://path@rev syntax. Useful e.g. to convert
231 # deleted branches.
231 # deleted branches.
232 at = url.rfind('@')
232 at = url.rfind('@')
233 if at >= 0:
233 if at >= 0:
234 latest = int(url[at + 1:])
234 latest = int(url[at + 1:])
235 url = url[:at]
235 url = url[:at]
236 except ValueError:
236 except ValueError:
237 pass
237 pass
238 self.url = geturl(url)
238 self.url = geturl(url)
239 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
239 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
240 try:
240 try:
241 self.transport = transport.SvnRaTransport(url=self.url)
241 self.transport = transport.SvnRaTransport(url=self.url)
242 self.ra = self.transport.ra
242 self.ra = self.transport.ra
243 self.ctx = self.transport.client
243 self.ctx = self.transport.client
244 self.baseurl = svn.ra.get_repos_root(self.ra)
244 self.baseurl = svn.ra.get_repos_root(self.ra)
245 # Module is either empty or a repository path starting with
245 # Module is either empty or a repository path starting with
246 # a slash and not ending with a slash.
246 # a slash and not ending with a slash.
247 self.module = urllib.unquote(self.url[len(self.baseurl):])
247 self.module = urllib.unquote(self.url[len(self.baseurl):])
248 self.prevmodule = None
248 self.prevmodule = None
249 self.rootmodule = self.module
249 self.rootmodule = self.module
250 self.commits = {}
250 self.commits = {}
251 self.paths = {}
251 self.paths = {}
252 self.uuid = svn.ra.get_uuid(self.ra)
252 self.uuid = svn.ra.get_uuid(self.ra)
253 except SubversionException:
253 except SubversionException:
254 ui.traceback()
254 ui.traceback()
255 raise NoRepo("%s does not look like a Subversion repo" % self.url)
255 raise NoRepo("%s does not look like a Subversion repository"
256 % self.url)
256
257
257 if rev:
258 if rev:
258 try:
259 try:
259 latest = int(rev)
260 latest = int(rev)
260 except ValueError:
261 except ValueError:
261 raise util.Abort(_('svn: revision %s is not an integer') % rev)
262 raise util.Abort(_('svn: revision %s is not an integer') % rev)
262
263
263 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
264 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
264 try:
265 try:
265 self.startrev = int(self.startrev)
266 self.startrev = int(self.startrev)
266 if self.startrev < 0:
267 if self.startrev < 0:
267 self.startrev = 0
268 self.startrev = 0
268 except ValueError:
269 except ValueError:
269 raise util.Abort(_('svn: start revision %s is not an integer')
270 raise util.Abort(_('svn: start revision %s is not an integer')
270 % self.startrev)
271 % self.startrev)
271
272
272 self.head = self.latest(self.module, latest)
273 self.head = self.latest(self.module, latest)
273 if not self.head:
274 if not self.head:
274 raise util.Abort(_('no revision found in module %s')
275 raise util.Abort(_('no revision found in module %s')
275 % self.module)
276 % self.module)
276 self.last_changed = self.revnum(self.head)
277 self.last_changed = self.revnum(self.head)
277
278
278 self._changescache = None
279 self._changescache = None
279
280
280 if os.path.exists(os.path.join(url, '.svn/entries')):
281 if os.path.exists(os.path.join(url, '.svn/entries')):
281 self.wc = url
282 self.wc = url
282 else:
283 else:
283 self.wc = None
284 self.wc = None
284 self.convertfp = None
285 self.convertfp = None
285
286
286 def setrevmap(self, revmap):
287 def setrevmap(self, revmap):
287 lastrevs = {}
288 lastrevs = {}
288 for revid in revmap.iterkeys():
289 for revid in revmap.iterkeys():
289 uuid, module, revnum = self.revsplit(revid)
290 uuid, module, revnum = self.revsplit(revid)
290 lastrevnum = lastrevs.setdefault(module, revnum)
291 lastrevnum = lastrevs.setdefault(module, revnum)
291 if revnum > lastrevnum:
292 if revnum > lastrevnum:
292 lastrevs[module] = revnum
293 lastrevs[module] = revnum
293 self.lastrevs = lastrevs
294 self.lastrevs = lastrevs
294
295
295 def exists(self, path, optrev):
296 def exists(self, path, optrev):
296 try:
297 try:
297 svn.client.ls(self.url.rstrip('/') + '/' + urllib.quote(path),
298 svn.client.ls(self.url.rstrip('/') + '/' + urllib.quote(path),
298 optrev, False, self.ctx)
299 optrev, False, self.ctx)
299 return True
300 return True
300 except SubversionException:
301 except SubversionException:
301 return False
302 return False
302
303
303 def getheads(self):
304 def getheads(self):
304
305
305 def isdir(path, revnum):
306 def isdir(path, revnum):
306 kind = self._checkpath(path, revnum)
307 kind = self._checkpath(path, revnum)
307 return kind == svn.core.svn_node_dir
308 return kind == svn.core.svn_node_dir
308
309
309 def getcfgpath(name, rev):
310 def getcfgpath(name, rev):
310 cfgpath = self.ui.config('convert', 'svn.' + name)
311 cfgpath = self.ui.config('convert', 'svn.' + name)
311 if cfgpath is not None and cfgpath.strip() == '':
312 if cfgpath is not None and cfgpath.strip() == '':
312 return None
313 return None
313 path = (cfgpath or name).strip('/')
314 path = (cfgpath or name).strip('/')
314 if not self.exists(path, rev):
315 if not self.exists(path, rev):
315 if cfgpath:
316 if cfgpath:
316 raise util.Abort(_('expected %s to be at %r, but not found')
317 raise util.Abort(_('expected %s to be at %r, but not found')
317 % (name, path))
318 % (name, path))
318 return None
319 return None
319 self.ui.note(_('found %s at %r\n') % (name, path))
320 self.ui.note(_('found %s at %r\n') % (name, path))
320 return path
321 return path
321
322
322 rev = optrev(self.last_changed)
323 rev = optrev(self.last_changed)
323 oldmodule = ''
324 oldmodule = ''
324 trunk = getcfgpath('trunk', rev)
325 trunk = getcfgpath('trunk', rev)
325 self.tags = getcfgpath('tags', rev)
326 self.tags = getcfgpath('tags', rev)
326 branches = getcfgpath('branches', rev)
327 branches = getcfgpath('branches', rev)
327
328
328 # If the project has a trunk or branches, we will extract heads
329 # If the project has a trunk or branches, we will extract heads
329 # from them. We keep the project root otherwise.
330 # from them. We keep the project root otherwise.
330 if trunk:
331 if trunk:
331 oldmodule = self.module or ''
332 oldmodule = self.module or ''
332 self.module += '/' + trunk
333 self.module += '/' + trunk
333 self.head = self.latest(self.module, self.last_changed)
334 self.head = self.latest(self.module, self.last_changed)
334 if not self.head:
335 if not self.head:
335 raise util.Abort(_('no revision found in module %s')
336 raise util.Abort(_('no revision found in module %s')
336 % self.module)
337 % self.module)
337
338
338 # First head in the list is the module's head
339 # First head in the list is the module's head
339 self.heads = [self.head]
340 self.heads = [self.head]
340 if self.tags is not None:
341 if self.tags is not None:
341 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
342 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
342
343
343 # Check if branches bring a few more heads to the list
344 # Check if branches bring a few more heads to the list
344 if branches:
345 if branches:
345 rpath = self.url.strip('/')
346 rpath = self.url.strip('/')
346 branchnames = svn.client.ls(rpath + '/' + urllib.quote(branches),
347 branchnames = svn.client.ls(rpath + '/' + urllib.quote(branches),
347 rev, False, self.ctx)
348 rev, False, self.ctx)
348 for branch in branchnames.keys():
349 for branch in branchnames.keys():
349 module = '%s/%s/%s' % (oldmodule, branches, branch)
350 module = '%s/%s/%s' % (oldmodule, branches, branch)
350 if not isdir(module, self.last_changed):
351 if not isdir(module, self.last_changed):
351 continue
352 continue
352 brevid = self.latest(module, self.last_changed)
353 brevid = self.latest(module, self.last_changed)
353 if not brevid:
354 if not brevid:
354 self.ui.note(_('ignoring empty branch %s\n') % branch)
355 self.ui.note(_('ignoring empty branch %s\n') % branch)
355 continue
356 continue
356 self.ui.note(_('found branch %s at %d\n') %
357 self.ui.note(_('found branch %s at %d\n') %
357 (branch, self.revnum(brevid)))
358 (branch, self.revnum(brevid)))
358 self.heads.append(brevid)
359 self.heads.append(brevid)
359
360
360 if self.startrev and self.heads:
361 if self.startrev and self.heads:
361 if len(self.heads) > 1:
362 if len(self.heads) > 1:
362 raise util.Abort(_('svn: start revision is not supported '
363 raise util.Abort(_('svn: start revision is not supported '
363 'with more than one branch'))
364 'with more than one branch'))
364 revnum = self.revnum(self.heads[0])
365 revnum = self.revnum(self.heads[0])
365 if revnum < self.startrev:
366 if revnum < self.startrev:
366 raise util.Abort(
367 raise util.Abort(
367 _('svn: no revision found after start revision %d')
368 _('svn: no revision found after start revision %d')
368 % self.startrev)
369 % self.startrev)
369
370
370 return self.heads
371 return self.heads
371
372
372 def getfile(self, file, rev):
373 def getfile(self, file, rev):
373 data, mode = self._getfile(file, rev)
374 data, mode = self._getfile(file, rev)
374 self.modecache[(file, rev)] = mode
375 self.modecache[(file, rev)] = mode
375 return data
376 return data
376
377
377 def getmode(self, file, rev):
378 def getmode(self, file, rev):
378 return self.modecache[(file, rev)]
379 return self.modecache[(file, rev)]
379
380
380 def getchanges(self, rev):
381 def getchanges(self, rev):
381 if self._changescache and self._changescache[0] == rev:
382 if self._changescache and self._changescache[0] == rev:
382 return self._changescache[1]
383 return self._changescache[1]
383 self._changescache = None
384 self._changescache = None
384 self.modecache = {}
385 self.modecache = {}
385 (paths, parents) = self.paths[rev]
386 (paths, parents) = self.paths[rev]
386 if parents:
387 if parents:
387 files, copies = self.expandpaths(rev, paths, parents)
388 files, copies = self.expandpaths(rev, paths, parents)
388 else:
389 else:
389 # Perform a full checkout on roots
390 # Perform a full checkout on roots
390 uuid, module, revnum = self.revsplit(rev)
391 uuid, module, revnum = self.revsplit(rev)
391 entries = svn.client.ls(self.baseurl + urllib.quote(module),
392 entries = svn.client.ls(self.baseurl + urllib.quote(module),
392 optrev(revnum), True, self.ctx)
393 optrev(revnum), True, self.ctx)
393 files = [n for n, e in entries.iteritems()
394 files = [n for n, e in entries.iteritems()
394 if e.kind == svn.core.svn_node_file]
395 if e.kind == svn.core.svn_node_file]
395 copies = {}
396 copies = {}
396
397
397 files.sort()
398 files.sort()
398 files = zip(files, [rev] * len(files))
399 files = zip(files, [rev] * len(files))
399
400
400 # caller caches the result, so free it here to release memory
401 # caller caches the result, so free it here to release memory
401 del self.paths[rev]
402 del self.paths[rev]
402 return (files, copies)
403 return (files, copies)
403
404
404 def getchangedfiles(self, rev, i):
405 def getchangedfiles(self, rev, i):
405 changes = self.getchanges(rev)
406 changes = self.getchanges(rev)
406 self._changescache = (rev, changes)
407 self._changescache = (rev, changes)
407 return [f[0] for f in changes[0]]
408 return [f[0] for f in changes[0]]
408
409
409 def getcommit(self, rev):
410 def getcommit(self, rev):
410 if rev not in self.commits:
411 if rev not in self.commits:
411 uuid, module, revnum = self.revsplit(rev)
412 uuid, module, revnum = self.revsplit(rev)
412 self.module = module
413 self.module = module
413 self.reparent(module)
414 self.reparent(module)
414 # We assume that:
415 # We assume that:
415 # - requests for revisions after "stop" come from the
416 # - requests for revisions after "stop" come from the
416 # revision graph backward traversal. Cache all of them
417 # revision graph backward traversal. Cache all of them
417 # down to stop, they will be used eventually.
418 # down to stop, they will be used eventually.
418 # - requests for revisions before "stop" come to get
419 # - requests for revisions before "stop" come to get
419 # isolated branches parents. Just fetch what is needed.
420 # isolated branches parents. Just fetch what is needed.
420 stop = self.lastrevs.get(module, 0)
421 stop = self.lastrevs.get(module, 0)
421 if revnum < stop:
422 if revnum < stop:
422 stop = revnum + 1
423 stop = revnum + 1
423 self._fetch_revisions(revnum, stop)
424 self._fetch_revisions(revnum, stop)
424 commit = self.commits[rev]
425 commit = self.commits[rev]
425 # caller caches the result, so free it here to release memory
426 # caller caches the result, so free it here to release memory
426 del self.commits[rev]
427 del self.commits[rev]
427 return commit
428 return commit
428
429
429 def gettags(self):
430 def gettags(self):
430 tags = {}
431 tags = {}
431 if self.tags is None:
432 if self.tags is None:
432 return tags
433 return tags
433
434
434 # svn tags are just a convention, project branches left in a
435 # svn tags are just a convention, project branches left in a
435 # 'tags' directory. There is no other relationship than
436 # 'tags' directory. There is no other relationship than
436 # ancestry, which is expensive to discover and makes them hard
437 # ancestry, which is expensive to discover and makes them hard
437 # to update incrementally. Worse, past revisions may be
438 # to update incrementally. Worse, past revisions may be
438 # referenced by tags far away in the future, requiring a deep
439 # referenced by tags far away in the future, requiring a deep
439 # history traversal on every calculation. Current code
440 # history traversal on every calculation. Current code
440 # performs a single backward traversal, tracking moves within
441 # performs a single backward traversal, tracking moves within
441 # the tags directory (tag renaming) and recording a new tag
442 # the tags directory (tag renaming) and recording a new tag
442 # everytime a project is copied from outside the tags
443 # everytime a project is copied from outside the tags
443 # directory. It also lists deleted tags, this behaviour may
444 # directory. It also lists deleted tags, this behaviour may
444 # change in the future.
445 # change in the future.
445 pendings = []
446 pendings = []
446 tagspath = self.tags
447 tagspath = self.tags
447 start = svn.ra.get_latest_revnum(self.ra)
448 start = svn.ra.get_latest_revnum(self.ra)
448 try:
449 try:
449 for entry in self._getlog([self.tags], start, self.startrev):
450 for entry in self._getlog([self.tags], start, self.startrev):
450 origpaths, revnum, author, date, message = entry
451 origpaths, revnum, author, date, message = entry
451 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
452 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
452 in origpaths.iteritems() if e.copyfrom_path]
453 in origpaths.iteritems() if e.copyfrom_path]
453 # Apply moves/copies from more specific to general
454 # Apply moves/copies from more specific to general
454 copies.sort(reverse=True)
455 copies.sort(reverse=True)
455
456
456 srctagspath = tagspath
457 srctagspath = tagspath
457 if copies and copies[-1][2] == tagspath:
458 if copies and copies[-1][2] == tagspath:
458 # Track tags directory moves
459 # Track tags directory moves
459 srctagspath = copies.pop()[0]
460 srctagspath = copies.pop()[0]
460
461
461 for source, sourcerev, dest in copies:
462 for source, sourcerev, dest in copies:
462 if not dest.startswith(tagspath + '/'):
463 if not dest.startswith(tagspath + '/'):
463 continue
464 continue
464 for tag in pendings:
465 for tag in pendings:
465 if tag[0].startswith(dest):
466 if tag[0].startswith(dest):
466 tagpath = source + tag[0][len(dest):]
467 tagpath = source + tag[0][len(dest):]
467 tag[:2] = [tagpath, sourcerev]
468 tag[:2] = [tagpath, sourcerev]
468 break
469 break
469 else:
470 else:
470 pendings.append([source, sourcerev, dest])
471 pendings.append([source, sourcerev, dest])
471
472
472 # Filter out tags with children coming from different
473 # Filter out tags with children coming from different
473 # parts of the repository like:
474 # parts of the repository like:
474 # /tags/tag.1 (from /trunk:10)
475 # /tags/tag.1 (from /trunk:10)
475 # /tags/tag.1/foo (from /branches/foo:12)
476 # /tags/tag.1/foo (from /branches/foo:12)
476 # Here/tags/tag.1 discarded as well as its children.
477 # Here/tags/tag.1 discarded as well as its children.
477 # It happens with tools like cvs2svn. Such tags cannot
478 # It happens with tools like cvs2svn. Such tags cannot
478 # be represented in mercurial.
479 # be represented in mercurial.
479 addeds = dict((p, e.copyfrom_path) for p, e
480 addeds = dict((p, e.copyfrom_path) for p, e
480 in origpaths.iteritems()
481 in origpaths.iteritems()
481 if e.action == 'A' and e.copyfrom_path)
482 if e.action == 'A' and e.copyfrom_path)
482 badroots = set()
483 badroots = set()
483 for destroot in addeds:
484 for destroot in addeds:
484 for source, sourcerev, dest in pendings:
485 for source, sourcerev, dest in pendings:
485 if (not dest.startswith(destroot + '/')
486 if (not dest.startswith(destroot + '/')
486 or source.startswith(addeds[destroot] + '/')):
487 or source.startswith(addeds[destroot] + '/')):
487 continue
488 continue
488 badroots.add(destroot)
489 badroots.add(destroot)
489 break
490 break
490
491
491 for badroot in badroots:
492 for badroot in badroots:
492 pendings = [p for p in pendings if p[2] != badroot
493 pendings = [p for p in pendings if p[2] != badroot
493 and not p[2].startswith(badroot + '/')]
494 and not p[2].startswith(badroot + '/')]
494
495
495 # Tell tag renamings from tag creations
496 # Tell tag renamings from tag creations
496 remainings = []
497 remainings = []
497 for source, sourcerev, dest in pendings:
498 for source, sourcerev, dest in pendings:
498 tagname = dest.split('/')[-1]
499 tagname = dest.split('/')[-1]
499 if source.startswith(srctagspath):
500 if source.startswith(srctagspath):
500 remainings.append([source, sourcerev, tagname])
501 remainings.append([source, sourcerev, tagname])
501 continue
502 continue
502 if tagname in tags:
503 if tagname in tags:
503 # Keep the latest tag value
504 # Keep the latest tag value
504 continue
505 continue
505 # From revision may be fake, get one with changes
506 # From revision may be fake, get one with changes
506 try:
507 try:
507 tagid = self.latest(source, sourcerev)
508 tagid = self.latest(source, sourcerev)
508 if tagid and tagname not in tags:
509 if tagid and tagname not in tags:
509 tags[tagname] = tagid
510 tags[tagname] = tagid
510 except SvnPathNotFound:
511 except SvnPathNotFound:
511 # It happens when we are following directories
512 # It happens when we are following directories
512 # we assumed were copied with their parents
513 # we assumed were copied with their parents
513 # but were really created in the tag
514 # but were really created in the tag
514 # directory.
515 # directory.
515 pass
516 pass
516 pendings = remainings
517 pendings = remainings
517 tagspath = srctagspath
518 tagspath = srctagspath
518
519
519 except SubversionException:
520 except SubversionException:
520 self.ui.note(_('no tags found at revision %d\n') % start)
521 self.ui.note(_('no tags found at revision %d\n') % start)
521 return tags
522 return tags
522
523
523 def converted(self, rev, destrev):
524 def converted(self, rev, destrev):
524 if not self.wc:
525 if not self.wc:
525 return
526 return
526 if self.convertfp is None:
527 if self.convertfp is None:
527 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
528 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
528 'a')
529 'a')
529 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
530 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
530 self.convertfp.flush()
531 self.convertfp.flush()
531
532
532 def revid(self, revnum, module=None):
533 def revid(self, revnum, module=None):
533 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
534 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
534
535
535 def revnum(self, rev):
536 def revnum(self, rev):
536 return int(rev.split('@')[-1])
537 return int(rev.split('@')[-1])
537
538
538 def revsplit(self, rev):
539 def revsplit(self, rev):
539 url, revnum = rev.rsplit('@', 1)
540 url, revnum = rev.rsplit('@', 1)
540 revnum = int(revnum)
541 revnum = int(revnum)
541 parts = url.split('/', 1)
542 parts = url.split('/', 1)
542 uuid = parts.pop(0)[4:]
543 uuid = parts.pop(0)[4:]
543 mod = ''
544 mod = ''
544 if parts:
545 if parts:
545 mod = '/' + parts[0]
546 mod = '/' + parts[0]
546 return uuid, mod, revnum
547 return uuid, mod, revnum
547
548
548 def latest(self, path, stop=0):
549 def latest(self, path, stop=0):
549 """Find the latest revid affecting path, up to stop. It may return
550 """Find the latest revid affecting path, up to stop. It may return
550 a revision in a different module, since a branch may be moved without
551 a revision in a different module, since a branch may be moved without
551 a change being reported. Return None if computed module does not
552 a change being reported. Return None if computed module does not
552 belong to rootmodule subtree.
553 belong to rootmodule subtree.
553 """
554 """
554 if not path.startswith(self.rootmodule):
555 if not path.startswith(self.rootmodule):
555 # Requests on foreign branches may be forbidden at server level
556 # Requests on foreign branches may be forbidden at server level
556 self.ui.debug('ignoring foreign branch %r\n' % path)
557 self.ui.debug('ignoring foreign branch %r\n' % path)
557 return None
558 return None
558
559
559 if not stop:
560 if not stop:
560 stop = svn.ra.get_latest_revnum(self.ra)
561 stop = svn.ra.get_latest_revnum(self.ra)
561 try:
562 try:
562 prevmodule = self.reparent('')
563 prevmodule = self.reparent('')
563 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
564 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
564 self.reparent(prevmodule)
565 self.reparent(prevmodule)
565 except SubversionException:
566 except SubversionException:
566 dirent = None
567 dirent = None
567 if not dirent:
568 if not dirent:
568 raise SvnPathNotFound(_('%s not found up to revision %d')
569 raise SvnPathNotFound(_('%s not found up to revision %d')
569 % (path, stop))
570 % (path, stop))
570
571
571 # stat() gives us the previous revision on this line of
572 # stat() gives us the previous revision on this line of
572 # development, but it might be in *another module*. Fetch the
573 # development, but it might be in *another module*. Fetch the
573 # log and detect renames down to the latest revision.
574 # log and detect renames down to the latest revision.
574 stream = self._getlog([path], stop, dirent.created_rev)
575 stream = self._getlog([path], stop, dirent.created_rev)
575 try:
576 try:
576 for entry in stream:
577 for entry in stream:
577 paths, revnum, author, date, message = entry
578 paths, revnum, author, date, message = entry
578 if revnum <= dirent.created_rev:
579 if revnum <= dirent.created_rev:
579 break
580 break
580
581
581 for p in paths:
582 for p in paths:
582 if not path.startswith(p) or not paths[p].copyfrom_path:
583 if not path.startswith(p) or not paths[p].copyfrom_path:
583 continue
584 continue
584 newpath = paths[p].copyfrom_path + path[len(p):]
585 newpath = paths[p].copyfrom_path + path[len(p):]
585 self.ui.debug("branch renamed from %s to %s at %d\n" %
586 self.ui.debug("branch renamed from %s to %s at %d\n" %
586 (path, newpath, revnum))
587 (path, newpath, revnum))
587 path = newpath
588 path = newpath
588 break
589 break
589 finally:
590 finally:
590 stream.close()
591 stream.close()
591
592
592 if not path.startswith(self.rootmodule):
593 if not path.startswith(self.rootmodule):
593 self.ui.debug('ignoring foreign branch %r\n' % path)
594 self.ui.debug('ignoring foreign branch %r\n' % path)
594 return None
595 return None
595 return self.revid(dirent.created_rev, path)
596 return self.revid(dirent.created_rev, path)
596
597
597 def reparent(self, module):
598 def reparent(self, module):
598 """Reparent the svn transport and return the previous parent."""
599 """Reparent the svn transport and return the previous parent."""
599 if self.prevmodule == module:
600 if self.prevmodule == module:
600 return module
601 return module
601 svnurl = self.baseurl + urllib.quote(module)
602 svnurl = self.baseurl + urllib.quote(module)
602 prevmodule = self.prevmodule
603 prevmodule = self.prevmodule
603 if prevmodule is None:
604 if prevmodule is None:
604 prevmodule = ''
605 prevmodule = ''
605 self.ui.debug("reparent to %s\n" % svnurl)
606 self.ui.debug("reparent to %s\n" % svnurl)
606 svn.ra.reparent(self.ra, svnurl)
607 svn.ra.reparent(self.ra, svnurl)
607 self.prevmodule = module
608 self.prevmodule = module
608 return prevmodule
609 return prevmodule
609
610
610 def expandpaths(self, rev, paths, parents):
611 def expandpaths(self, rev, paths, parents):
611 entries = []
612 entries = []
612 # Map of entrypath, revision for finding source of deleted
613 # Map of entrypath, revision for finding source of deleted
613 # revisions.
614 # revisions.
614 copyfrom = {}
615 copyfrom = {}
615 copies = {}
616 copies = {}
616
617
617 new_module, revnum = self.revsplit(rev)[1:]
618 new_module, revnum = self.revsplit(rev)[1:]
618 if new_module != self.module:
619 if new_module != self.module:
619 self.module = new_module
620 self.module = new_module
620 self.reparent(self.module)
621 self.reparent(self.module)
621
622
622 for path, ent in paths:
623 for path, ent in paths:
623 entrypath = self.getrelpath(path)
624 entrypath = self.getrelpath(path)
624
625
625 kind = self._checkpath(entrypath, revnum)
626 kind = self._checkpath(entrypath, revnum)
626 if kind == svn.core.svn_node_file:
627 if kind == svn.core.svn_node_file:
627 entries.append(self.recode(entrypath))
628 entries.append(self.recode(entrypath))
628 if not ent.copyfrom_path or not parents:
629 if not ent.copyfrom_path or not parents:
629 continue
630 continue
630 # Copy sources not in parent revisions cannot be
631 # Copy sources not in parent revisions cannot be
631 # represented, ignore their origin for now
632 # represented, ignore their origin for now
632 pmodule, prevnum = self.revsplit(parents[0])[1:]
633 pmodule, prevnum = self.revsplit(parents[0])[1:]
633 if ent.copyfrom_rev < prevnum:
634 if ent.copyfrom_rev < prevnum:
634 continue
635 continue
635 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
636 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
636 if not copyfrom_path:
637 if not copyfrom_path:
637 continue
638 continue
638 self.ui.debug("copied to %s from %s@%s\n" %
639 self.ui.debug("copied to %s from %s@%s\n" %
639 (entrypath, copyfrom_path, ent.copyfrom_rev))
640 (entrypath, copyfrom_path, ent.copyfrom_rev))
640 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
641 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
641 elif kind == 0: # gone, but had better be a deleted *file*
642 elif kind == 0: # gone, but had better be a deleted *file*
642 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
643 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
643 pmodule, prevnum = self.revsplit(parents[0])[1:]
644 pmodule, prevnum = self.revsplit(parents[0])[1:]
644 parentpath = pmodule + "/" + entrypath
645 parentpath = pmodule + "/" + entrypath
645 self.ui.debug("entry %s\n" % parentpath)
646 self.ui.debug("entry %s\n" % parentpath)
646
647
647 # We can avoid the reparent calls if the module has
648 # We can avoid the reparent calls if the module has
648 # not changed but it probably does not worth the pain.
649 # not changed but it probably does not worth the pain.
649 prevmodule = self.reparent('')
650 prevmodule = self.reparent('')
650 fromkind = svn.ra.check_path(self.ra, parentpath.strip('/'),
651 fromkind = svn.ra.check_path(self.ra, parentpath.strip('/'),
651 prevnum)
652 prevnum)
652 self.reparent(prevmodule)
653 self.reparent(prevmodule)
653
654
654 if fromkind == svn.core.svn_node_file:
655 if fromkind == svn.core.svn_node_file:
655 entries.append(self.recode(entrypath))
656 entries.append(self.recode(entrypath))
656 elif fromkind == svn.core.svn_node_dir:
657 elif fromkind == svn.core.svn_node_dir:
657 if ent.action == 'C':
658 if ent.action == 'C':
658 children = self._find_children(path, prevnum)
659 children = self._find_children(path, prevnum)
659 else:
660 else:
660 oroot = parentpath.strip('/')
661 oroot = parentpath.strip('/')
661 nroot = path.strip('/')
662 nroot = path.strip('/')
662 children = self._find_children(oroot, prevnum)
663 children = self._find_children(oroot, prevnum)
663 children = [s.replace(oroot, nroot) for s in children]
664 children = [s.replace(oroot, nroot) for s in children]
664
665
665 for child in children:
666 for child in children:
666 childpath = self.getrelpath("/" + child, pmodule)
667 childpath = self.getrelpath("/" + child, pmodule)
667 if not childpath:
668 if not childpath:
668 continue
669 continue
669 if childpath in copies:
670 if childpath in copies:
670 del copies[childpath]
671 del copies[childpath]
671 entries.append(childpath)
672 entries.append(childpath)
672 else:
673 else:
673 self.ui.debug('unknown path in revision %d: %s\n' % \
674 self.ui.debug('unknown path in revision %d: %s\n' % \
674 (revnum, path))
675 (revnum, path))
675 elif kind == svn.core.svn_node_dir:
676 elif kind == svn.core.svn_node_dir:
676 # If the directory just had a prop change,
677 # If the directory just had a prop change,
677 # then we shouldn't need to look for its children.
678 # then we shouldn't need to look for its children.
678 if ent.action == 'M':
679 if ent.action == 'M':
679 continue
680 continue
680
681
681 children = sorted(self._find_children(path, revnum))
682 children = sorted(self._find_children(path, revnum))
682 for child in children:
683 for child in children:
683 # Can we move a child directory and its
684 # Can we move a child directory and its
684 # parent in the same commit? (probably can). Could
685 # parent in the same commit? (probably can). Could
685 # cause problems if instead of revnum -1,
686 # cause problems if instead of revnum -1,
686 # we have to look in (copyfrom_path, revnum - 1)
687 # we have to look in (copyfrom_path, revnum - 1)
687 entrypath = self.getrelpath("/" + child)
688 entrypath = self.getrelpath("/" + child)
688 if entrypath:
689 if entrypath:
689 # Need to filter out directories here...
690 # Need to filter out directories here...
690 kind = self._checkpath(entrypath, revnum)
691 kind = self._checkpath(entrypath, revnum)
691 if kind != svn.core.svn_node_dir:
692 if kind != svn.core.svn_node_dir:
692 entries.append(self.recode(entrypath))
693 entries.append(self.recode(entrypath))
693
694
694 # Handle directory copies
695 # Handle directory copies
695 if not ent.copyfrom_path or not parents:
696 if not ent.copyfrom_path or not parents:
696 continue
697 continue
697 # Copy sources not in parent revisions cannot be
698 # Copy sources not in parent revisions cannot be
698 # represented, ignore their origin for now
699 # represented, ignore their origin for now
699 pmodule, prevnum = self.revsplit(parents[0])[1:]
700 pmodule, prevnum = self.revsplit(parents[0])[1:]
700 if ent.copyfrom_rev < prevnum:
701 if ent.copyfrom_rev < prevnum:
701 continue
702 continue
702 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
703 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
703 if not copyfrompath:
704 if not copyfrompath:
704 continue
705 continue
705 copyfrom[path] = ent
706 copyfrom[path] = ent
706 self.ui.debug("mark %s came from %s:%d\n"
707 self.ui.debug("mark %s came from %s:%d\n"
707 % (path, copyfrompath, ent.copyfrom_rev))
708 % (path, copyfrompath, ent.copyfrom_rev))
708 children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev)
709 children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev)
709 children.sort()
710 children.sort()
710 for child in children:
711 for child in children:
711 entrypath = self.getrelpath("/" + child, pmodule)
712 entrypath = self.getrelpath("/" + child, pmodule)
712 if not entrypath:
713 if not entrypath:
713 continue
714 continue
714 copytopath = path + entrypath[len(copyfrompath):]
715 copytopath = path + entrypath[len(copyfrompath):]
715 copytopath = self.getrelpath(copytopath)
716 copytopath = self.getrelpath(copytopath)
716 copies[self.recode(copytopath)] = self.recode(entrypath)
717 copies[self.recode(copytopath)] = self.recode(entrypath)
717
718
718 return (list(set(entries)), copies)
719 return (list(set(entries)), copies)
719
720
720 def _fetch_revisions(self, from_revnum, to_revnum):
721 def _fetch_revisions(self, from_revnum, to_revnum):
721 if from_revnum < to_revnum:
722 if from_revnum < to_revnum:
722 from_revnum, to_revnum = to_revnum, from_revnum
723 from_revnum, to_revnum = to_revnum, from_revnum
723
724
724 self.child_cset = None
725 self.child_cset = None
725
726
726 def parselogentry(orig_paths, revnum, author, date, message):
727 def parselogentry(orig_paths, revnum, author, date, message):
727 """Return the parsed commit object or None, and True if
728 """Return the parsed commit object or None, and True if
728 the revision is a branch root.
729 the revision is a branch root.
729 """
730 """
730 self.ui.debug("parsing revision %d (%d changes)\n" %
731 self.ui.debug("parsing revision %d (%d changes)\n" %
731 (revnum, len(orig_paths)))
732 (revnum, len(orig_paths)))
732
733
733 branched = False
734 branched = False
734 rev = self.revid(revnum)
735 rev = self.revid(revnum)
735 # branch log might return entries for a parent we already have
736 # branch log might return entries for a parent we already have
736
737
737 if rev in self.commits or revnum < to_revnum:
738 if rev in self.commits or revnum < to_revnum:
738 return None, branched
739 return None, branched
739
740
740 parents = []
741 parents = []
741 # check whether this revision is the start of a branch or part
742 # check whether this revision is the start of a branch or part
742 # of a branch renaming
743 # of a branch renaming
743 orig_paths = sorted(orig_paths.iteritems())
744 orig_paths = sorted(orig_paths.iteritems())
744 root_paths = [(p, e) for p, e in orig_paths
745 root_paths = [(p, e) for p, e in orig_paths
745 if self.module.startswith(p)]
746 if self.module.startswith(p)]
746 if root_paths:
747 if root_paths:
747 path, ent = root_paths[-1]
748 path, ent = root_paths[-1]
748 if ent.copyfrom_path:
749 if ent.copyfrom_path:
749 branched = True
750 branched = True
750 newpath = ent.copyfrom_path + self.module[len(path):]
751 newpath = ent.copyfrom_path + self.module[len(path):]
751 # ent.copyfrom_rev may not be the actual last revision
752 # ent.copyfrom_rev may not be the actual last revision
752 previd = self.latest(newpath, ent.copyfrom_rev)
753 previd = self.latest(newpath, ent.copyfrom_rev)
753 if previd is not None:
754 if previd is not None:
754 prevmodule, prevnum = self.revsplit(previd)[1:]
755 prevmodule, prevnum = self.revsplit(previd)[1:]
755 if prevnum >= self.startrev:
756 if prevnum >= self.startrev:
756 parents = [previd]
757 parents = [previd]
757 self.ui.note(
758 self.ui.note(
758 _('found parent of branch %s at %d: %s\n') %
759 _('found parent of branch %s at %d: %s\n') %
759 (self.module, prevnum, prevmodule))
760 (self.module, prevnum, prevmodule))
760 else:
761 else:
761 self.ui.debug("no copyfrom path, don't know what to do.\n")
762 self.ui.debug("no copyfrom path, don't know what to do.\n")
762
763
763 paths = []
764 paths = []
764 # filter out unrelated paths
765 # filter out unrelated paths
765 for path, ent in orig_paths:
766 for path, ent in orig_paths:
766 if self.getrelpath(path) is None:
767 if self.getrelpath(path) is None:
767 continue
768 continue
768 paths.append((path, ent))
769 paths.append((path, ent))
769
770
770 # Example SVN datetime. Includes microseconds.
771 # Example SVN datetime. Includes microseconds.
771 # ISO-8601 conformant
772 # ISO-8601 conformant
772 # '2007-01-04T17:35:00.902377Z'
773 # '2007-01-04T17:35:00.902377Z'
773 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
774 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
774
775
775 log = message and self.recode(message) or ''
776 log = message and self.recode(message) or ''
776 author = author and self.recode(author) or ''
777 author = author and self.recode(author) or ''
777 try:
778 try:
778 branch = self.module.split("/")[-1]
779 branch = self.module.split("/")[-1]
779 if branch == 'trunk':
780 if branch == 'trunk':
780 branch = ''
781 branch = ''
781 except IndexError:
782 except IndexError:
782 branch = None
783 branch = None
783
784
784 cset = commit(author=author,
785 cset = commit(author=author,
785 date=util.datestr(date),
786 date=util.datestr(date),
786 desc=log,
787 desc=log,
787 parents=parents,
788 parents=parents,
788 branch=branch,
789 branch=branch,
789 rev=rev)
790 rev=rev)
790
791
791 self.commits[rev] = cset
792 self.commits[rev] = cset
792 # The parents list is *shared* among self.paths and the
793 # The parents list is *shared* among self.paths and the
793 # commit object. Both will be updated below.
794 # commit object. Both will be updated below.
794 self.paths[rev] = (paths, cset.parents)
795 self.paths[rev] = (paths, cset.parents)
795 if self.child_cset and not self.child_cset.parents:
796 if self.child_cset and not self.child_cset.parents:
796 self.child_cset.parents[:] = [rev]
797 self.child_cset.parents[:] = [rev]
797 self.child_cset = cset
798 self.child_cset = cset
798 return cset, branched
799 return cset, branched
799
800
800 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
801 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
801 (self.module, from_revnum, to_revnum))
802 (self.module, from_revnum, to_revnum))
802
803
803 try:
804 try:
804 firstcset = None
805 firstcset = None
805 lastonbranch = False
806 lastonbranch = False
806 stream = self._getlog([self.module], from_revnum, to_revnum)
807 stream = self._getlog([self.module], from_revnum, to_revnum)
807 try:
808 try:
808 for entry in stream:
809 for entry in stream:
809 paths, revnum, author, date, message = entry
810 paths, revnum, author, date, message = entry
810 if revnum < self.startrev:
811 if revnum < self.startrev:
811 lastonbranch = True
812 lastonbranch = True
812 break
813 break
813 if not paths:
814 if not paths:
814 self.ui.debug('revision %d has no entries\n' % revnum)
815 self.ui.debug('revision %d has no entries\n' % revnum)
815 # If we ever leave the loop on an empty
816 # If we ever leave the loop on an empty
816 # revision, do not try to get a parent branch
817 # revision, do not try to get a parent branch
817 lastonbranch = lastonbranch or revnum == 0
818 lastonbranch = lastonbranch or revnum == 0
818 continue
819 continue
819 cset, lastonbranch = parselogentry(paths, revnum, author,
820 cset, lastonbranch = parselogentry(paths, revnum, author,
820 date, message)
821 date, message)
821 if cset:
822 if cset:
822 firstcset = cset
823 firstcset = cset
823 if lastonbranch:
824 if lastonbranch:
824 break
825 break
825 finally:
826 finally:
826 stream.close()
827 stream.close()
827
828
828 if not lastonbranch and firstcset and not firstcset.parents:
829 if not lastonbranch and firstcset and not firstcset.parents:
829 # The first revision of the sequence (the last fetched one)
830 # The first revision of the sequence (the last fetched one)
830 # has invalid parents if not a branch root. Find the parent
831 # has invalid parents if not a branch root. Find the parent
831 # revision now, if any.
832 # revision now, if any.
832 try:
833 try:
833 firstrevnum = self.revnum(firstcset.rev)
834 firstrevnum = self.revnum(firstcset.rev)
834 if firstrevnum > 1:
835 if firstrevnum > 1:
835 latest = self.latest(self.module, firstrevnum - 1)
836 latest = self.latest(self.module, firstrevnum - 1)
836 if latest:
837 if latest:
837 firstcset.parents.append(latest)
838 firstcset.parents.append(latest)
838 except SvnPathNotFound:
839 except SvnPathNotFound:
839 pass
840 pass
840 except SubversionException, (inst, num):
841 except SubversionException, (inst, num):
841 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
842 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
842 raise util.Abort(_('svn: branch has no revision %s') % to_revnum)
843 raise util.Abort(_('svn: branch has no revision %s') % to_revnum)
843 raise
844 raise
844
845
845 def _getfile(self, file, rev):
846 def _getfile(self, file, rev):
846 # TODO: ra.get_file transmits the whole file instead of diffs.
847 # TODO: ra.get_file transmits the whole file instead of diffs.
847 mode = ''
848 mode = ''
848 try:
849 try:
849 new_module, revnum = self.revsplit(rev)[1:]
850 new_module, revnum = self.revsplit(rev)[1:]
850 if self.module != new_module:
851 if self.module != new_module:
851 self.module = new_module
852 self.module = new_module
852 self.reparent(self.module)
853 self.reparent(self.module)
853 io = StringIO()
854 io = StringIO()
854 info = svn.ra.get_file(self.ra, file, revnum, io)
855 info = svn.ra.get_file(self.ra, file, revnum, io)
855 data = io.getvalue()
856 data = io.getvalue()
856 # ra.get_files() seems to keep a reference on the input buffer
857 # ra.get_files() seems to keep a reference on the input buffer
857 # preventing collection. Release it explicitely.
858 # preventing collection. Release it explicitely.
858 io.close()
859 io.close()
859 if isinstance(info, list):
860 if isinstance(info, list):
860 info = info[-1]
861 info = info[-1]
861 mode = ("svn:executable" in info) and 'x' or ''
862 mode = ("svn:executable" in info) and 'x' or ''
862 mode = ("svn:special" in info) and 'l' or mode
863 mode = ("svn:special" in info) and 'l' or mode
863 except SubversionException, e:
864 except SubversionException, e:
864 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
865 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
865 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
866 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
866 if e.apr_err in notfound: # File not found
867 if e.apr_err in notfound: # File not found
867 raise IOError()
868 raise IOError()
868 raise
869 raise
869 if mode == 'l':
870 if mode == 'l':
870 link_prefix = "link "
871 link_prefix = "link "
871 if data.startswith(link_prefix):
872 if data.startswith(link_prefix):
872 data = data[len(link_prefix):]
873 data = data[len(link_prefix):]
873 return data, mode
874 return data, mode
874
875
875 def _find_children(self, path, revnum):
876 def _find_children(self, path, revnum):
876 path = path.strip('/')
877 path = path.strip('/')
877 pool = Pool()
878 pool = Pool()
878 rpath = '/'.join([self.baseurl, urllib.quote(path)]).strip('/')
879 rpath = '/'.join([self.baseurl, urllib.quote(path)]).strip('/')
879 return ['%s/%s' % (path, x) for x in
880 return ['%s/%s' % (path, x) for x in
880 svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
881 svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
881
882
882 def getrelpath(self, path, module=None):
883 def getrelpath(self, path, module=None):
883 if module is None:
884 if module is None:
884 module = self.module
885 module = self.module
885 # Given the repository url of this wc, say
886 # Given the repository url of this wc, say
886 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
887 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
887 # extract the "entry" portion (a relative path) from what
888 # extract the "entry" portion (a relative path) from what
888 # svn log --xml says, ie
889 # svn log --xml says, ie
889 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
890 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
890 # that is to say "tests/PloneTestCase.py"
891 # that is to say "tests/PloneTestCase.py"
891 if path.startswith(module):
892 if path.startswith(module):
892 relative = path.rstrip('/')[len(module):]
893 relative = path.rstrip('/')[len(module):]
893 if relative.startswith('/'):
894 if relative.startswith('/'):
894 return relative[1:]
895 return relative[1:]
895 elif relative == '':
896 elif relative == '':
896 return relative
897 return relative
897
898
898 # The path is outside our tracked tree...
899 # The path is outside our tracked tree...
899 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
900 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
900 return None
901 return None
901
902
902 def _checkpath(self, path, revnum):
903 def _checkpath(self, path, revnum):
903 # ra.check_path does not like leading slashes very much, it leads
904 # ra.check_path does not like leading slashes very much, it leads
904 # to PROPFIND subversion errors
905 # to PROPFIND subversion errors
905 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
906 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
906
907
907 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
908 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
908 strict_node_history=False):
909 strict_node_history=False):
909 # Normalize path names, svn >= 1.5 only wants paths relative to
910 # Normalize path names, svn >= 1.5 only wants paths relative to
910 # supplied URL
911 # supplied URL
911 relpaths = []
912 relpaths = []
912 for p in paths:
913 for p in paths:
913 if not p.startswith('/'):
914 if not p.startswith('/'):
914 p = self.module + '/' + p
915 p = self.module + '/' + p
915 relpaths.append(p.strip('/'))
916 relpaths.append(p.strip('/'))
916 args = [self.baseurl, relpaths, start, end, limit, discover_changed_paths,
917 args = [self.baseurl, relpaths, start, end, limit, discover_changed_paths,
917 strict_node_history]
918 strict_node_history]
918 arg = encodeargs(args)
919 arg = encodeargs(args)
919 hgexe = util.hgexecutable()
920 hgexe = util.hgexecutable()
920 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
921 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
921 stdin, stdout = util.popen2(cmd)
922 stdin, stdout = util.popen2(cmd)
922 stdin.write(arg)
923 stdin.write(arg)
923 try:
924 try:
924 stdin.close()
925 stdin.close()
925 except IOError:
926 except IOError:
926 raise util.Abort(_('Mercurial failed to run itself, check'
927 raise util.Abort(_('Mercurial failed to run itself, check'
927 ' hg executable is in PATH'))
928 ' hg executable is in PATH'))
928 return logstream(stdout)
929 return logstream(stdout)
929
930
930 pre_revprop_change = '''#!/bin/sh
931 pre_revprop_change = '''#!/bin/sh
931
932
932 REPOS="$1"
933 REPOS="$1"
933 REV="$2"
934 REV="$2"
934 USER="$3"
935 USER="$3"
935 PROPNAME="$4"
936 PROPNAME="$4"
936 ACTION="$5"
937 ACTION="$5"
937
938
938 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
939 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
939 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
940 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
940 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
941 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
941
942
942 echo "Changing prohibited revision property" >&2
943 echo "Changing prohibited revision property" >&2
943 exit 1
944 exit 1
944 '''
945 '''
945
946
946 class svn_sink(converter_sink, commandline):
947 class svn_sink(converter_sink, commandline):
947 commit_re = re.compile(r'Committed revision (\d+).', re.M)
948 commit_re = re.compile(r'Committed revision (\d+).', re.M)
948
949
949 def prerun(self):
950 def prerun(self):
950 if self.wc:
951 if self.wc:
951 os.chdir(self.wc)
952 os.chdir(self.wc)
952
953
953 def postrun(self):
954 def postrun(self):
954 if self.wc:
955 if self.wc:
955 os.chdir(self.cwd)
956 os.chdir(self.cwd)
956
957
957 def join(self, name):
958 def join(self, name):
958 return os.path.join(self.wc, '.svn', name)
959 return os.path.join(self.wc, '.svn', name)
959
960
960 def revmapfile(self):
961 def revmapfile(self):
961 return self.join('hg-shamap')
962 return self.join('hg-shamap')
962
963
963 def authorfile(self):
964 def authorfile(self):
964 return self.join('hg-authormap')
965 return self.join('hg-authormap')
965
966
966 def __init__(self, ui, path):
967 def __init__(self, ui, path):
967 converter_sink.__init__(self, ui, path)
968 converter_sink.__init__(self, ui, path)
968 commandline.__init__(self, ui, 'svn')
969 commandline.__init__(self, ui, 'svn')
969 self.delete = []
970 self.delete = []
970 self.setexec = []
971 self.setexec = []
971 self.delexec = []
972 self.delexec = []
972 self.copies = []
973 self.copies = []
973 self.wc = None
974 self.wc = None
974 self.cwd = os.getcwd()
975 self.cwd = os.getcwd()
975
976
976 path = os.path.realpath(path)
977 path = os.path.realpath(path)
977
978
978 created = False
979 created = False
979 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
980 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
980 self.wc = path
981 self.wc = path
981 self.run0('update')
982 self.run0('update')
982 else:
983 else:
983 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
984 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
984
985
985 if os.path.isdir(os.path.dirname(path)):
986 if os.path.isdir(os.path.dirname(path)):
986 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
987 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
987 ui.status(_('initializing svn repo %r\n') %
988 ui.status(_('initializing svn repository %r\n') %
988 os.path.basename(path))
989 os.path.basename(path))
989 commandline(ui, 'svnadmin').run0('create', path)
990 commandline(ui, 'svnadmin').run0('create', path)
990 created = path
991 created = path
991 path = util.normpath(path)
992 path = util.normpath(path)
992 if not path.startswith('/'):
993 if not path.startswith('/'):
993 path = '/' + path
994 path = '/' + path
994 path = 'file://' + path
995 path = 'file://' + path
995
996
996 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
997 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
997 self.run0('checkout', path, wcpath)
998 self.run0('checkout', path, wcpath)
998
999
999 self.wc = wcpath
1000 self.wc = wcpath
1000 self.opener = util.opener(self.wc)
1001 self.opener = util.opener(self.wc)
1001 self.wopener = util.opener(self.wc)
1002 self.wopener = util.opener(self.wc)
1002 self.childmap = mapfile(ui, self.join('hg-childmap'))
1003 self.childmap = mapfile(ui, self.join('hg-childmap'))
1003 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
1004 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
1004
1005
1005 if created:
1006 if created:
1006 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1007 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1007 fp = open(hook, 'w')
1008 fp = open(hook, 'w')
1008 fp.write(pre_revprop_change)
1009 fp.write(pre_revprop_change)
1009 fp.close()
1010 fp.close()
1010 util.set_flags(hook, False, True)
1011 util.set_flags(hook, False, True)
1011
1012
1012 xport = transport.SvnRaTransport(url=geturl(path))
1013 xport = transport.SvnRaTransport(url=geturl(path))
1013 self.uuid = svn.ra.get_uuid(xport.ra)
1014 self.uuid = svn.ra.get_uuid(xport.ra)
1014
1015
1015 def wjoin(self, *names):
1016 def wjoin(self, *names):
1016 return os.path.join(self.wc, *names)
1017 return os.path.join(self.wc, *names)
1017
1018
1018 def putfile(self, filename, flags, data):
1019 def putfile(self, filename, flags, data):
1019 if 'l' in flags:
1020 if 'l' in flags:
1020 self.wopener.symlink(data, filename)
1021 self.wopener.symlink(data, filename)
1021 else:
1022 else:
1022 try:
1023 try:
1023 if os.path.islink(self.wjoin(filename)):
1024 if os.path.islink(self.wjoin(filename)):
1024 os.unlink(filename)
1025 os.unlink(filename)
1025 except OSError:
1026 except OSError:
1026 pass
1027 pass
1027 self.wopener(filename, 'w').write(data)
1028 self.wopener(filename, 'w').write(data)
1028
1029
1029 if self.is_exec:
1030 if self.is_exec:
1030 was_exec = self.is_exec(self.wjoin(filename))
1031 was_exec = self.is_exec(self.wjoin(filename))
1031 else:
1032 else:
1032 # On filesystems not supporting execute-bit, there is no way
1033 # On filesystems not supporting execute-bit, there is no way
1033 # to know if it is set but asking subversion. Setting it
1034 # to know if it is set but asking subversion. Setting it
1034 # systematically is just as expensive and much simpler.
1035 # systematically is just as expensive and much simpler.
1035 was_exec = 'x' not in flags
1036 was_exec = 'x' not in flags
1036
1037
1037 util.set_flags(self.wjoin(filename), False, 'x' in flags)
1038 util.set_flags(self.wjoin(filename), False, 'x' in flags)
1038 if was_exec:
1039 if was_exec:
1039 if 'x' not in flags:
1040 if 'x' not in flags:
1040 self.delexec.append(filename)
1041 self.delexec.append(filename)
1041 else:
1042 else:
1042 if 'x' in flags:
1043 if 'x' in flags:
1043 self.setexec.append(filename)
1044 self.setexec.append(filename)
1044
1045
1045 def _copyfile(self, source, dest):
1046 def _copyfile(self, source, dest):
1046 # SVN's copy command pukes if the destination file exists, but
1047 # SVN's copy command pukes if the destination file exists, but
1047 # our copyfile method expects to record a copy that has
1048 # our copyfile method expects to record a copy that has
1048 # already occurred. Cross the semantic gap.
1049 # already occurred. Cross the semantic gap.
1049 wdest = self.wjoin(dest)
1050 wdest = self.wjoin(dest)
1050 exists = os.path.exists(wdest)
1051 exists = os.path.exists(wdest)
1051 if exists:
1052 if exists:
1052 fd, tempname = tempfile.mkstemp(
1053 fd, tempname = tempfile.mkstemp(
1053 prefix='hg-copy-', dir=os.path.dirname(wdest))
1054 prefix='hg-copy-', dir=os.path.dirname(wdest))
1054 os.close(fd)
1055 os.close(fd)
1055 os.unlink(tempname)
1056 os.unlink(tempname)
1056 os.rename(wdest, tempname)
1057 os.rename(wdest, tempname)
1057 try:
1058 try:
1058 self.run0('copy', source, dest)
1059 self.run0('copy', source, dest)
1059 finally:
1060 finally:
1060 if exists:
1061 if exists:
1061 try:
1062 try:
1062 os.unlink(wdest)
1063 os.unlink(wdest)
1063 except OSError:
1064 except OSError:
1064 pass
1065 pass
1065 os.rename(tempname, wdest)
1066 os.rename(tempname, wdest)
1066
1067
1067 def dirs_of(self, files):
1068 def dirs_of(self, files):
1068 dirs = set()
1069 dirs = set()
1069 for f in files:
1070 for f in files:
1070 if os.path.isdir(self.wjoin(f)):
1071 if os.path.isdir(self.wjoin(f)):
1071 dirs.add(f)
1072 dirs.add(f)
1072 for i in strutil.rfindall(f, '/'):
1073 for i in strutil.rfindall(f, '/'):
1073 dirs.add(f[:i])
1074 dirs.add(f[:i])
1074 return dirs
1075 return dirs
1075
1076
1076 def add_dirs(self, files):
1077 def add_dirs(self, files):
1077 add_dirs = [d for d in sorted(self.dirs_of(files))
1078 add_dirs = [d for d in sorted(self.dirs_of(files))
1078 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1079 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1079 if add_dirs:
1080 if add_dirs:
1080 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1081 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1081 return add_dirs
1082 return add_dirs
1082
1083
1083 def add_files(self, files):
1084 def add_files(self, files):
1084 if files:
1085 if files:
1085 self.xargs(files, 'add', quiet=True)
1086 self.xargs(files, 'add', quiet=True)
1086 return files
1087 return files
1087
1088
1088 def tidy_dirs(self, names):
1089 def tidy_dirs(self, names):
1089 deleted = []
1090 deleted = []
1090 for d in sorted(self.dirs_of(names), reverse=True):
1091 for d in sorted(self.dirs_of(names), reverse=True):
1091 wd = self.wjoin(d)
1092 wd = self.wjoin(d)
1092 if os.listdir(wd) == '.svn':
1093 if os.listdir(wd) == '.svn':
1093 self.run0('delete', d)
1094 self.run0('delete', d)
1094 deleted.append(d)
1095 deleted.append(d)
1095 return deleted
1096 return deleted
1096
1097
1097 def addchild(self, parent, child):
1098 def addchild(self, parent, child):
1098 self.childmap[parent] = child
1099 self.childmap[parent] = child
1099
1100
1100 def revid(self, rev):
1101 def revid(self, rev):
1101 return u"svn:%s@%s" % (self.uuid, rev)
1102 return u"svn:%s@%s" % (self.uuid, rev)
1102
1103
1103 def putcommit(self, files, copies, parents, commit, source, revmap):
1104 def putcommit(self, files, copies, parents, commit, source, revmap):
1104 # Apply changes to working copy
1105 # Apply changes to working copy
1105 for f, v in files:
1106 for f, v in files:
1106 try:
1107 try:
1107 data = source.getfile(f, v)
1108 data = source.getfile(f, v)
1108 except IOError:
1109 except IOError:
1109 self.delete.append(f)
1110 self.delete.append(f)
1110 else:
1111 else:
1111 e = source.getmode(f, v)
1112 e = source.getmode(f, v)
1112 self.putfile(f, e, data)
1113 self.putfile(f, e, data)
1113 if f in copies:
1114 if f in copies:
1114 self.copies.append([copies[f], f])
1115 self.copies.append([copies[f], f])
1115 files = [f[0] for f in files]
1116 files = [f[0] for f in files]
1116
1117
1117 for parent in parents:
1118 for parent in parents:
1118 try:
1119 try:
1119 return self.revid(self.childmap[parent])
1120 return self.revid(self.childmap[parent])
1120 except KeyError:
1121 except KeyError:
1121 pass
1122 pass
1122 entries = set(self.delete)
1123 entries = set(self.delete)
1123 files = frozenset(files)
1124 files = frozenset(files)
1124 entries.update(self.add_dirs(files.difference(entries)))
1125 entries.update(self.add_dirs(files.difference(entries)))
1125 if self.copies:
1126 if self.copies:
1126 for s, d in self.copies:
1127 for s, d in self.copies:
1127 self._copyfile(s, d)
1128 self._copyfile(s, d)
1128 self.copies = []
1129 self.copies = []
1129 if self.delete:
1130 if self.delete:
1130 self.xargs(self.delete, 'delete')
1131 self.xargs(self.delete, 'delete')
1131 self.delete = []
1132 self.delete = []
1132 entries.update(self.add_files(files.difference(entries)))
1133 entries.update(self.add_files(files.difference(entries)))
1133 entries.update(self.tidy_dirs(entries))
1134 entries.update(self.tidy_dirs(entries))
1134 if self.delexec:
1135 if self.delexec:
1135 self.xargs(self.delexec, 'propdel', 'svn:executable')
1136 self.xargs(self.delexec, 'propdel', 'svn:executable')
1136 self.delexec = []
1137 self.delexec = []
1137 if self.setexec:
1138 if self.setexec:
1138 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1139 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1139 self.setexec = []
1140 self.setexec = []
1140
1141
1141 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1142 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1142 fp = os.fdopen(fd, 'w')
1143 fp = os.fdopen(fd, 'w')
1143 fp.write(commit.desc)
1144 fp.write(commit.desc)
1144 fp.close()
1145 fp.close()
1145 try:
1146 try:
1146 output = self.run0('commit',
1147 output = self.run0('commit',
1147 username=util.shortuser(commit.author),
1148 username=util.shortuser(commit.author),
1148 file=messagefile,
1149 file=messagefile,
1149 encoding='utf-8')
1150 encoding='utf-8')
1150 try:
1151 try:
1151 rev = self.commit_re.search(output).group(1)
1152 rev = self.commit_re.search(output).group(1)
1152 except AttributeError:
1153 except AttributeError:
1153 if not files:
1154 if not files:
1154 return parents[0]
1155 return parents[0]
1155 self.ui.warn(_('unexpected svn output:\n'))
1156 self.ui.warn(_('unexpected svn output:\n'))
1156 self.ui.warn(output)
1157 self.ui.warn(output)
1157 raise util.Abort(_('unable to cope with svn output'))
1158 raise util.Abort(_('unable to cope with svn output'))
1158 if commit.rev:
1159 if commit.rev:
1159 self.run('propset', 'hg:convert-rev', commit.rev,
1160 self.run('propset', 'hg:convert-rev', commit.rev,
1160 revprop=True, revision=rev)
1161 revprop=True, revision=rev)
1161 if commit.branch and commit.branch != 'default':
1162 if commit.branch and commit.branch != 'default':
1162 self.run('propset', 'hg:convert-branch', commit.branch,
1163 self.run('propset', 'hg:convert-branch', commit.branch,
1163 revprop=True, revision=rev)
1164 revprop=True, revision=rev)
1164 for parent in parents:
1165 for parent in parents:
1165 self.addchild(parent, rev)
1166 self.addchild(parent, rev)
1166 return self.revid(rev)
1167 return self.revid(rev)
1167 finally:
1168 finally:
1168 os.unlink(messagefile)
1169 os.unlink(messagefile)
1169
1170
1170 def puttags(self, tags):
1171 def puttags(self, tags):
1171 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
1172 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
@@ -1,337 +1,337 b''
1 % add
1 % add
2 adding a
2 adding a
3 adding d1/d2/b
3 adding d1/d2/b
4 % modify
4 % modify
5 1:e0e2b8a9156b
5 1:e0e2b8a9156b
6 assuming destination a-hg
6 assuming destination a-hg
7 initializing svn repo 'a-hg'
7 initializing svn repository 'a-hg'
8 initializing svn wc 'a-hg-wc'
8 initializing svn wc 'a-hg-wc'
9 scanning source...
9 scanning source...
10 sorting...
10 sorting...
11 converting...
11 converting...
12 1 add a file
12 1 add a file
13 0 modify a file
13 0 modify a file
14 At revision 2.
14 At revision 2.
15 2 2 test .
15 2 2 test .
16 2 2 test a
16 2 2 test a
17 2 1 test d1
17 2 1 test d1
18 2 1 test d1/d2
18 2 1 test d1/d2
19 2 1 test d1/d2/b
19 2 1 test d1/d2/b
20 <?xml version="1.0"?>
20 <?xml version="1.0"?>
21 <log>
21 <log>
22 <logentry
22 <logentry
23 revision="2">
23 revision="2">
24 <author>test</author>
24 <author>test</author>
25 <date/>
25 <date/>
26 <paths>
26 <paths>
27 <path
27 <path
28 action="M">/a</path>
28 action="M">/a</path>
29 </paths>
29 </paths>
30 <msg>modify a file</msg>
30 <msg>modify a file</msg>
31 </logentry>
31 </logentry>
32 <logentry
32 <logentry
33 revision="1">
33 revision="1">
34 <author>test</author>
34 <author>test</author>
35 <date/>
35 <date/>
36 <paths>
36 <paths>
37 <path
37 <path
38 action="A">/a</path>
38 action="A">/a</path>
39 <path
39 <path
40 action="A">/d1</path>
40 action="A">/d1</path>
41 <path
41 <path
42 action="A">/d1/d2</path>
42 action="A">/d1/d2</path>
43 <path
43 <path
44 action="A">/d1/d2/b</path>
44 action="A">/d1/d2/b</path>
45 </paths>
45 </paths>
46 <msg>add a file</msg>
46 <msg>add a file</msg>
47 </logentry>
47 </logentry>
48 </log>
48 </log>
49 a:
49 a:
50 a
50 a
51 d1
51 d1
52
52
53 a-hg-wc:
53 a-hg-wc:
54 a
54 a
55 d1
55 d1
56 same
56 same
57 % rename
57 % rename
58 2:eb5169441d43
58 2:eb5169441d43
59 assuming destination a-hg
59 assuming destination a-hg
60 initializing svn wc 'a-hg-wc'
60 initializing svn wc 'a-hg-wc'
61 scanning source...
61 scanning source...
62 sorting...
62 sorting...
63 converting...
63 converting...
64 0 rename a file
64 0 rename a file
65 At revision 3.
65 At revision 3.
66 3 3 test .
66 3 3 test .
67 3 3 test b
67 3 3 test b
68 3 1 test d1
68 3 1 test d1
69 3 1 test d1/d2
69 3 1 test d1/d2
70 3 1 test d1/d2/b
70 3 1 test d1/d2/b
71 <?xml version="1.0"?>
71 <?xml version="1.0"?>
72 <log>
72 <log>
73 <logentry
73 <logentry
74 revision="3">
74 revision="3">
75 <author>test</author>
75 <author>test</author>
76 <date/>
76 <date/>
77 <paths>
77 <paths>
78 <path
78 <path
79 action="D">/a</path>
79 action="D">/a</path>
80 <path
80 <path
81 copyfrom-path="/a"
81 copyfrom-path="/a"
82 copyfrom-rev="2"
82 copyfrom-rev="2"
83 action="A">/b</path>
83 action="A">/b</path>
84 </paths>
84 </paths>
85 <msg>rename a file</msg>
85 <msg>rename a file</msg>
86 </logentry>
86 </logentry>
87 </log>
87 </log>
88 a:
88 a:
89 b
89 b
90 d1
90 d1
91
91
92 a-hg-wc:
92 a-hg-wc:
93 b
93 b
94 d1
94 d1
95 % copy
95 % copy
96 3:60effef6ab48
96 3:60effef6ab48
97 assuming destination a-hg
97 assuming destination a-hg
98 initializing svn wc 'a-hg-wc'
98 initializing svn wc 'a-hg-wc'
99 scanning source...
99 scanning source...
100 sorting...
100 sorting...
101 converting...
101 converting...
102 0 copy a file
102 0 copy a file
103 At revision 4.
103 At revision 4.
104 4 4 test .
104 4 4 test .
105 4 3 test b
105 4 3 test b
106 4 4 test c
106 4 4 test c
107 4 1 test d1
107 4 1 test d1
108 4 1 test d1/d2
108 4 1 test d1/d2
109 4 1 test d1/d2/b
109 4 1 test d1/d2/b
110 <?xml version="1.0"?>
110 <?xml version="1.0"?>
111 <log>
111 <log>
112 <logentry
112 <logentry
113 revision="4">
113 revision="4">
114 <author>test</author>
114 <author>test</author>
115 <date/>
115 <date/>
116 <paths>
116 <paths>
117 <path
117 <path
118 copyfrom-path="/b"
118 copyfrom-path="/b"
119 copyfrom-rev="3"
119 copyfrom-rev="3"
120 action="A">/c</path>
120 action="A">/c</path>
121 </paths>
121 </paths>
122 <msg>copy a file</msg>
122 <msg>copy a file</msg>
123 </logentry>
123 </logentry>
124 </log>
124 </log>
125 a:
125 a:
126 b
126 b
127 c
127 c
128 d1
128 d1
129
129
130 a-hg-wc:
130 a-hg-wc:
131 b
131 b
132 c
132 c
133 d1
133 d1
134 % remove
134 % remove
135 4:87bbe3013fb6
135 4:87bbe3013fb6
136 assuming destination a-hg
136 assuming destination a-hg
137 initializing svn wc 'a-hg-wc'
137 initializing svn wc 'a-hg-wc'
138 scanning source...
138 scanning source...
139 sorting...
139 sorting...
140 converting...
140 converting...
141 0 remove a file
141 0 remove a file
142 At revision 5.
142 At revision 5.
143 5 5 test .
143 5 5 test .
144 5 4 test c
144 5 4 test c
145 5 1 test d1
145 5 1 test d1
146 5 1 test d1/d2
146 5 1 test d1/d2
147 5 1 test d1/d2/b
147 5 1 test d1/d2/b
148 <?xml version="1.0"?>
148 <?xml version="1.0"?>
149 <log>
149 <log>
150 <logentry
150 <logentry
151 revision="5">
151 revision="5">
152 <author>test</author>
152 <author>test</author>
153 <date/>
153 <date/>
154 <paths>
154 <paths>
155 <path
155 <path
156 action="D">/b</path>
156 action="D">/b</path>
157 </paths>
157 </paths>
158 <msg>remove a file</msg>
158 <msg>remove a file</msg>
159 </logentry>
159 </logentry>
160 </log>
160 </log>
161 a:
161 a:
162 c
162 c
163 d1
163 d1
164
164
165 a-hg-wc:
165 a-hg-wc:
166 c
166 c
167 d1
167 d1
168 % executable
168 % executable
169 5:ff42e473c340
169 5:ff42e473c340
170 assuming destination a-hg
170 assuming destination a-hg
171 initializing svn wc 'a-hg-wc'
171 initializing svn wc 'a-hg-wc'
172 scanning source...
172 scanning source...
173 sorting...
173 sorting...
174 converting...
174 converting...
175 0 make a file executable
175 0 make a file executable
176 At revision 6.
176 At revision 6.
177 6 6 test .
177 6 6 test .
178 6 6 test c
178 6 6 test c
179 6 1 test d1
179 6 1 test d1
180 6 1 test d1/d2
180 6 1 test d1/d2
181 6 1 test d1/d2/b
181 6 1 test d1/d2/b
182 <?xml version="1.0"?>
182 <?xml version="1.0"?>
183 <log>
183 <log>
184 <logentry
184 <logentry
185 revision="6">
185 revision="6">
186 <author>test</author>
186 <author>test</author>
187 <date/>
187 <date/>
188 <paths>
188 <paths>
189 <path
189 <path
190 action="M">/c</path>
190 action="M">/c</path>
191 </paths>
191 </paths>
192 <msg>make a file executable</msg>
192 <msg>make a file executable</msg>
193 </logentry>
193 </logentry>
194 </log>
194 </log>
195 executable
195 executable
196 % executable in new directory
196 % executable in new directory
197 adding d1/a
197 adding d1/a
198 assuming destination a-hg
198 assuming destination a-hg
199 initializing svn repo 'a-hg'
199 initializing svn repository 'a-hg'
200 initializing svn wc 'a-hg-wc'
200 initializing svn wc 'a-hg-wc'
201 scanning source...
201 scanning source...
202 sorting...
202 sorting...
203 converting...
203 converting...
204 0 add executable file in new directory
204 0 add executable file in new directory
205 At revision 1.
205 At revision 1.
206 1 1 test .
206 1 1 test .
207 1 1 test d1
207 1 1 test d1
208 1 1 test d1/a
208 1 1 test d1/a
209 <?xml version="1.0"?>
209 <?xml version="1.0"?>
210 <log>
210 <log>
211 <logentry
211 <logentry
212 revision="1">
212 revision="1">
213 <author>test</author>
213 <author>test</author>
214 <date/>
214 <date/>
215 <paths>
215 <paths>
216 <path
216 <path
217 action="A">/d1</path>
217 action="A">/d1</path>
218 <path
218 <path
219 action="A">/d1/a</path>
219 action="A">/d1/a</path>
220 </paths>
220 </paths>
221 <msg>add executable file in new directory</msg>
221 <msg>add executable file in new directory</msg>
222 </logentry>
222 </logentry>
223 </log>
223 </log>
224 executable
224 executable
225 % copy to new directory
225 % copy to new directory
226 assuming destination a-hg
226 assuming destination a-hg
227 initializing svn wc 'a-hg-wc'
227 initializing svn wc 'a-hg-wc'
228 scanning source...
228 scanning source...
229 sorting...
229 sorting...
230 converting...
230 converting...
231 0 copy file to new directory
231 0 copy file to new directory
232 At revision 2.
232 At revision 2.
233 2 2 test .
233 2 2 test .
234 2 1 test d1
234 2 1 test d1
235 2 1 test d1/a
235 2 1 test d1/a
236 2 2 test d2
236 2 2 test d2
237 2 2 test d2/a
237 2 2 test d2/a
238 <?xml version="1.0"?>
238 <?xml version="1.0"?>
239 <log>
239 <log>
240 <logentry
240 <logentry
241 revision="2">
241 revision="2">
242 <author>test</author>
242 <author>test</author>
243 <date/>
243 <date/>
244 <paths>
244 <paths>
245 <path
245 <path
246 action="A">/d2</path>
246 action="A">/d2</path>
247 <path
247 <path
248 copyfrom-path="/d1/a"
248 copyfrom-path="/d1/a"
249 copyfrom-rev="1"
249 copyfrom-rev="1"
250 action="A">/d2/a</path>
250 action="A">/d2/a</path>
251 </paths>
251 </paths>
252 <msg>copy file to new directory</msg>
252 <msg>copy file to new directory</msg>
253 </logentry>
253 </logentry>
254 </log>
254 </log>
255 % branchy history
255 % branchy history
256 adding b
256 adding b
257 adding left-1
257 adding left-1
258 adding left-2
258 adding left-2
259 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
259 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
260 adding right-1
260 adding right-1
261 created new head
261 created new head
262 adding right-2
262 adding right-2
263 3 files updated, 0 files merged, 2 files removed, 0 files unresolved
263 3 files updated, 0 files merged, 2 files removed, 0 files unresolved
264 merging b
264 merging b
265 warning: conflicts during merge.
265 warning: conflicts during merge.
266 merging b failed!
266 merging b failed!
267 2 files updated, 0 files merged, 0 files removed, 1 files unresolved
267 2 files updated, 0 files merged, 0 files removed, 1 files unresolved
268 use 'hg resolve' to retry unresolved file merges or 'hg update -C' to abandon
268 use 'hg resolve' to retry unresolved file merges or 'hg update -C' to abandon
269 assuming destination b-hg
269 assuming destination b-hg
270 initializing svn repo 'b-hg'
270 initializing svn repository 'b-hg'
271 initializing svn wc 'b-hg-wc'
271 initializing svn wc 'b-hg-wc'
272 scanning source...
272 scanning source...
273 sorting...
273 sorting...
274 converting...
274 converting...
275 5 base
275 5 base
276 4 left-1
276 4 left-1
277 3 left-2
277 3 left-2
278 2 right-1
278 2 right-1
279 1 right-2
279 1 right-2
280 0 merge
280 0 merge
281 % expect 4 changes
281 % expect 4 changes
282 At revision 4.
282 At revision 4.
283 4 4 test .
283 4 4 test .
284 4 3 test b
284 4 3 test b
285 4 2 test left-1
285 4 2 test left-1
286 4 3 test left-2
286 4 3 test left-2
287 4 4 test right-1
287 4 4 test right-1
288 4 4 test right-2
288 4 4 test right-2
289 <?xml version="1.0"?>
289 <?xml version="1.0"?>
290 <log>
290 <log>
291 <logentry
291 <logentry
292 revision="4">
292 revision="4">
293 <author>test</author>
293 <author>test</author>
294 <date/>
294 <date/>
295 <paths>
295 <paths>
296 <path
296 <path
297 action="A">/right-1</path>
297 action="A">/right-1</path>
298 <path
298 <path
299 action="A">/right-2</path>
299 action="A">/right-2</path>
300 </paths>
300 </paths>
301 <msg>merge</msg>
301 <msg>merge</msg>
302 </logentry>
302 </logentry>
303 <logentry
303 <logentry
304 revision="3">
304 revision="3">
305 <author>test</author>
305 <author>test</author>
306 <date/>
306 <date/>
307 <paths>
307 <paths>
308 <path
308 <path
309 action="M">/b</path>
309 action="M">/b</path>
310 <path
310 <path
311 action="A">/left-2</path>
311 action="A">/left-2</path>
312 </paths>
312 </paths>
313 <msg>left-2</msg>
313 <msg>left-2</msg>
314 </logentry>
314 </logentry>
315 <logentry
315 <logentry
316 revision="2">
316 revision="2">
317 <author>test</author>
317 <author>test</author>
318 <date/>
318 <date/>
319 <paths>
319 <paths>
320 <path
320 <path
321 action="M">/b</path>
321 action="M">/b</path>
322 <path
322 <path
323 action="A">/left-1</path>
323 action="A">/left-1</path>
324 </paths>
324 </paths>
325 <msg>left-1</msg>
325 <msg>left-1</msg>
326 </logentry>
326 </logentry>
327 <logentry
327 <logentry
328 revision="1">
328 revision="1">
329 <author>test</author>
329 <author>test</author>
330 <date/>
330 <date/>
331 <paths>
331 <paths>
332 <path
332 <path
333 action="A">/b</path>
333 action="A">/b</path>
334 </paths>
334 </paths>
335 <msg>base</msg>
335 <msg>base</msg>
336 </logentry>
336 </logentry>
337 </log>
337 </log>
@@ -1,291 +1,291 b''
1 hg convert [OPTION]... SOURCE [DEST [REVMAP]]
1 hg convert [OPTION]... SOURCE [DEST [REVMAP]]
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 [identifiers]:
5 Accepted source formats [identifiers]:
6
6
7 - Mercurial [hg]
7 - Mercurial [hg]
8 - CVS [cvs]
8 - CVS [cvs]
9 - Darcs [darcs]
9 - Darcs [darcs]
10 - git [git]
10 - git [git]
11 - Subversion [svn]
11 - Subversion [svn]
12 - Monotone [mtn]
12 - Monotone [mtn]
13 - GNU Arch [gnuarch]
13 - GNU Arch [gnuarch]
14 - Bazaar [bzr]
14 - Bazaar [bzr]
15 - Perforce [p4]
15 - Perforce [p4]
16
16
17 Accepted destination formats [identifiers]:
17 Accepted destination formats [identifiers]:
18
18
19 - Mercurial [hg]
19 - Mercurial [hg]
20 - Subversion [svn] (history on branches is not preserved)
20 - Subversion [svn] (history on branches is not preserved)
21
21
22 If no revision is given, all revisions will be converted. Otherwise,
22 If no revision is given, all revisions will be converted. Otherwise,
23 convert will only import up to the named revision (given in a format
23 convert will only import up to the named revision (given in a format
24 understood by the source).
24 understood by the source).
25
25
26 If no destination directory name is specified, it defaults to the basename
26 If no destination directory name is specified, it defaults to the basename
27 of the source with '-hg' appended. If the destination repository doesn't
27 of the source with '-hg' appended. If the destination repository doesn't
28 exist, it will be created.
28 exist, it will be created.
29
29
30 By default, all sources except Mercurial will use --branchsort. Mercurial
30 By default, all sources except Mercurial will use --branchsort. Mercurial
31 uses --sourcesort to preserve original revision numbers order. Sort modes
31 uses --sourcesort to preserve original revision numbers order. Sort modes
32 have the following effects:
32 have the following effects:
33
33
34 --branchsort convert from parent to child revision when possible, which
34 --branchsort convert from parent to child revision when possible, which
35 means branches are usually converted one after the other. It
35 means branches are usually converted one after the other. It
36 generates more compact repositories.
36 generates more compact repositories.
37 --datesort sort revisions by date. Converted repositories have good-
37 --datesort sort revisions by date. Converted repositories have good-
38 looking changelogs but are often an order of magnitude
38 looking changelogs but are often an order of magnitude
39 larger than the same ones generated by --branchsort.
39 larger than the same ones generated by --branchsort.
40 --sourcesort try to preserve source revisions order, only supported by
40 --sourcesort try to preserve source revisions order, only supported by
41 Mercurial sources.
41 Mercurial sources.
42
42
43 If <REVMAP> isn't given, it will be put in a default location
43 If <REVMAP> isn't given, it will be put in a default location
44 (<dest>/.hg/shamap by default). The <REVMAP> is a simple text file that
44 (<dest>/.hg/shamap by default). The <REVMAP> is a simple text file that
45 maps each source commit ID to the destination ID for that revision, like
45 maps each source commit ID to the destination ID for that revision, like
46 so:
46 so:
47
47
48 <source ID> <destination ID>
48 <source ID> <destination ID>
49
49
50 If the file doesn't exist, it's automatically created. It's updated on
50 If the file doesn't exist, it's automatically created. It's updated on
51 each commit copied, so convert-repo can be interrupted and can be run
51 each commit copied, so convert-repo can be interrupted and can be run
52 repeatedly to copy new commits.
52 repeatedly to copy new commits.
53
53
54 The [username mapping] file is a simple text file that maps each source
54 The [username mapping] file is a simple text file that maps each source
55 commit author to a destination commit author. It is handy for source SCMs
55 commit author to a destination commit author. It is handy for source SCMs
56 that use unix logins to identify authors (eg: CVS). One line per author
56 that use unix logins to identify authors (eg: CVS). One line per author
57 mapping and the line format is: srcauthor=whatever string you want
57 mapping and the line format is: srcauthor=whatever string you want
58
58
59 The filemap is a file that allows filtering and remapping of files and
59 The filemap is a file that allows filtering and remapping of files and
60 directories. Comment lines start with '#'. Each line can contain one of
60 directories. Comment lines start with '#'. Each line can contain one of
61 the following directives:
61 the following directives:
62
62
63 include path/to/file
63 include path/to/file
64
64
65 exclude path/to/file
65 exclude path/to/file
66
66
67 rename from/file to/file
67 rename from/file to/file
68
68
69 The 'include' directive causes a file, or all files under a directory, to
69 The 'include' directive causes a file, or all files under a directory, to
70 be included in the destination repository, and the exclusion of all other
70 be included in the destination repository, and the exclusion of all other
71 files and directories not explicitly included. The 'exclude' directive
71 files and directories not explicitly included. The 'exclude' directive
72 causes files or directories to be omitted. The 'rename' directive renames
72 causes files or directories to be omitted. The 'rename' directive renames
73 a file or directory. To rename from a subdirectory into the root of the
73 a file or directory. To rename from a subdirectory into the root of the
74 repository, use '.' as the path to rename to.
74 repository, use '.' as the path to rename to.
75
75
76 The splicemap is a file that allows insertion of synthetic history,
76 The splicemap is a file that allows insertion of synthetic history,
77 letting you specify the parents of a revision. This is useful if you want
77 letting you specify the parents of a revision. This is useful if you want
78 to e.g. give a Subversion merge two parents, or graft two disconnected
78 to e.g. give a Subversion merge two parents, or graft two disconnected
79 series of history together. Each entry contains a key, followed by a
79 series of history together. Each entry contains a key, followed by a
80 space, followed by one or two comma-separated values. The key is the
80 space, followed by one or two comma-separated values. The key is the
81 revision ID in the source revision control system whose parents should be
81 revision ID in the source revision control system whose parents should be
82 modified (same format as a key in .hg/shamap). The values are the revision
82 modified (same format as a key in .hg/shamap). The values are the revision
83 IDs (in either the source or destination revision control system) that
83 IDs (in either the source or destination revision control system) that
84 should be used as the new parents for that node. For example, if you have
84 should be used as the new parents for that node. For example, if you have
85 merged "release-1.0" into "trunk", then you should specify the revision on
85 merged "release-1.0" into "trunk", then you should specify the revision on
86 "trunk" as the first parent and the one on the "release-1.0" branch as the
86 "trunk" as the first parent and the one on the "release-1.0" branch as the
87 second.
87 second.
88
88
89 The branchmap is a file that allows you to rename a branch when it is
89 The branchmap is a file that allows you to rename a branch when it is
90 being brought in from whatever external repository. When used in
90 being brought in from whatever external repository. When used in
91 conjunction with a splicemap, it allows for a powerful combination to help
91 conjunction with a splicemap, it allows for a powerful combination to help
92 fix even the most badly mismanaged repositories and turn them into nicely
92 fix even the most badly mismanaged repositories and turn them into nicely
93 structured Mercurial repositories. The branchmap contains lines of the
93 structured Mercurial repositories. The branchmap contains lines of the
94 form "original_branch_name new_branch_name". "original_branch_name" is the
94 form "original_branch_name new_branch_name". "original_branch_name" is the
95 name of the branch in the source repository, and "new_branch_name" is the
95 name of the branch in the source repository, and "new_branch_name" is the
96 name of the branch is the destination repository. This can be used to (for
96 name of the branch is the destination repository. This can be used to (for
97 instance) move code in one repository from "default" to a named branch.
97 instance) move code in one repository from "default" to a named branch.
98
98
99 Mercurial Source
99 Mercurial Source
100 ----------------
100 ----------------
101
101
102 --config convert.hg.ignoreerrors=False (boolean)
102 --config convert.hg.ignoreerrors=False (boolean)
103 ignore integrity errors when reading. Use it to fix Mercurial
103 ignore integrity errors when reading. Use it to fix Mercurial
104 repositories with missing revlogs, by converting from and to
104 repositories with missing revlogs, by converting from and to
105 Mercurial.
105 Mercurial.
106 --config convert.hg.saverev=False (boolean)
106 --config convert.hg.saverev=False (boolean)
107 store original revision ID in changeset (forces target IDs to change)
107 store original revision ID in changeset (forces target IDs to change)
108 --config convert.hg.startrev=0 (hg revision identifier)
108 --config convert.hg.startrev=0 (hg revision identifier)
109 convert start revision and its descendants
109 convert start revision and its descendants
110
110
111 CVS Source
111 CVS Source
112 ----------
112 ----------
113
113
114 CVS source will use a sandbox (i.e. a checked-out copy) from CVS to
114 CVS source will use a sandbox (i.e. a checked-out copy) from CVS to
115 indicate the starting point of what will be converted. Direct access to
115 indicate the starting point of what will be converted. Direct access to
116 the repository files is not needed, unless of course the repository is
116 the repository files is not needed, unless of course the repository is
117 :local:. The conversion uses the top level directory in the sandbox to
117 :local:. The conversion uses the top level directory in the sandbox to
118 find the CVS repository, and then uses CVS rlog commands to find files to
118 find the CVS repository, and then uses CVS rlog commands to find files to
119 convert. This means that unless a filemap is given, all files under the
119 convert. This means that unless a filemap is given, all files under the
120 starting directory will be converted, and that any directory
120 starting directory will be converted, and that any directory
121 reorganization in the CVS sandbox is ignored.
121 reorganization in the CVS sandbox is ignored.
122
122
123 The options shown are the defaults.
123 The options shown are the defaults.
124
124
125 --config convert.cvsps.cache=True (boolean)
125 --config convert.cvsps.cache=True (boolean)
126 Set to False to disable remote log caching, for testing and debugging
126 Set to False to disable remote log caching, for testing and debugging
127 purposes.
127 purposes.
128 --config convert.cvsps.fuzz=60 (integer)
128 --config convert.cvsps.fuzz=60 (integer)
129 Specify the maximum time (in seconds) that is allowed between commits
129 Specify the maximum time (in seconds) that is allowed between commits
130 with identical user and log message in a single changeset. When very
130 with identical user and log message in a single changeset. When very
131 large files were checked in as part of a changeset then the default
131 large files were checked in as part of a changeset then the default
132 may not be long enough.
132 may not be long enough.
133 --config convert.cvsps.mergeto='{{mergetobranch ([-\w]+)}}'
133 --config convert.cvsps.mergeto='{{mergetobranch ([-\w]+)}}'
134 Specify a regular expression to which commit log messages are matched.
134 Specify a regular expression to which commit log messages are matched.
135 If a match occurs, then the conversion process will insert a dummy
135 If a match occurs, then the conversion process will insert a dummy
136 revision merging the branch on which this log message occurs to the
136 revision merging the branch on which this log message occurs to the
137 branch indicated in the regex.
137 branch indicated in the regex.
138 --config convert.cvsps.mergefrom='{{mergefrombranch ([-\w]+)}}'
138 --config convert.cvsps.mergefrom='{{mergefrombranch ([-\w]+)}}'
139 Specify a regular expression to which commit log messages are matched.
139 Specify a regular expression to which commit log messages are matched.
140 If a match occurs, then the conversion process will add the most
140 If a match occurs, then the conversion process will add the most
141 recent revision on the branch indicated in the regex as the second
141 recent revision on the branch indicated in the regex as the second
142 parent of the changeset.
142 parent of the changeset.
143 --config hook.cvslog
143 --config hook.cvslog
144 Specify a Python function to be called at the end of gathering the CVS
144 Specify a Python function to be called at the end of gathering the CVS
145 log. The function is passed a list with the log entries, and can
145 log. The function is passed a list with the log entries, and can
146 modify the entries in-place, or add or delete them.
146 modify the entries in-place, or add or delete them.
147 --config hook.cvschangesets
147 --config hook.cvschangesets
148 Specify a Python function to be called after the changesets are
148 Specify a Python function to be called after the changesets are
149 calculated from the the CVS log. The function is passed a list with
149 calculated from the the CVS log. The function is passed a list with
150 the changeset entries, and can modify the changesets in-place, or add
150 the changeset entries, and can modify the changesets in-place, or add
151 or delete them.
151 or delete them.
152
152
153 An additional "debugcvsps" Mercurial command allows the builtin changeset
153 An additional "debugcvsps" Mercurial command allows the builtin changeset
154 merging code to be run without doing a conversion. Its parameters and
154 merging code to be run without doing a conversion. Its parameters and
155 output are similar to that of cvsps 2.1. Please see the command help for
155 output are similar to that of cvsps 2.1. Please see the command help for
156 more details.
156 more details.
157
157
158 Subversion Source
158 Subversion Source
159 -----------------
159 -----------------
160
160
161 Subversion source detects classical trunk/branches/tags layouts. By
161 Subversion source detects classical trunk/branches/tags layouts. By
162 default, the supplied "svn://repo/path/" source URL is converted as a
162 default, the supplied "svn://repo/path/" source URL is converted as a
163 single branch. If "svn://repo/path/trunk" exists it replaces the default
163 single branch. If "svn://repo/path/trunk" exists it replaces the default
164 branch. If "svn://repo/path/branches" exists, its subdirectories are
164 branch. If "svn://repo/path/branches" exists, its subdirectories are
165 listed as possible branches. If "svn://repo/path/tags" exists, it is
165 listed as possible branches. If "svn://repo/path/tags" exists, it is
166 looked for tags referencing converted branches. Default "trunk",
166 looked for tags referencing converted branches. Default "trunk",
167 "branches" and "tags" values can be overridden with following options. Set
167 "branches" and "tags" values can be overridden with following options. Set
168 them to paths relative to the source URL, or leave them blank to disable
168 them to paths relative to the source URL, or leave them blank to disable
169 auto detection.
169 auto detection.
170
170
171 --config convert.svn.branches=branches (directory name)
171 --config convert.svn.branches=branches (directory name)
172 specify the directory containing branches
172 specify the directory containing branches
173 --config convert.svn.tags=tags (directory name)
173 --config convert.svn.tags=tags (directory name)
174 specify the directory containing tags
174 specify the directory containing tags
175 --config convert.svn.trunk=trunk (directory name)
175 --config convert.svn.trunk=trunk (directory name)
176 specify the name of the trunk branch
176 specify the name of the trunk branch
177
177
178 Source history can be retrieved starting at a specific revision, instead
178 Source history can be retrieved starting at a specific revision, instead
179 of being integrally converted. Only single branch conversions are
179 of being integrally converted. Only single branch conversions are
180 supported.
180 supported.
181
181
182 --config convert.svn.startrev=0 (svn revision number)
182 --config convert.svn.startrev=0 (svn revision number)
183 specify start Subversion revision.
183 specify start Subversion revision.
184
184
185 Perforce Source
185 Perforce Source
186 ---------------
186 ---------------
187
187
188 The Perforce (P4) importer can be given a p4 depot path or a client
188 The Perforce (P4) importer can be given a p4 depot path or a client
189 specification as source. It will convert all files in the source to a flat
189 specification as source. It will convert all files in the source to a flat
190 Mercurial repository, ignoring labels, branches and integrations. Note
190 Mercurial repository, ignoring labels, branches and integrations. Note
191 that when a depot path is given you then usually should specify a target
191 that when a depot path is given you then usually should specify a target
192 directory, because otherwise the target may be named ...-hg.
192 directory, because otherwise the target may be named ...-hg.
193
193
194 It is possible to limit the amount of source history to be converted by
194 It is possible to limit the amount of source history to be converted by
195 specifying an initial Perforce revision.
195 specifying an initial Perforce revision.
196
196
197 --config convert.p4.startrev=0 (perforce changelist number)
197 --config convert.p4.startrev=0 (perforce changelist number)
198 specify initial Perforce revision.
198 specify initial Perforce revision.
199
199
200 Mercurial Destination
200 Mercurial Destination
201 ---------------------
201 ---------------------
202
202
203 --config convert.hg.clonebranches=False (boolean)
203 --config convert.hg.clonebranches=False (boolean)
204 dispatch source branches in separate clones.
204 dispatch source branches in separate clones.
205 --config convert.hg.tagsbranch=default (branch name)
205 --config convert.hg.tagsbranch=default (branch name)
206 tag revisions branch name
206 tag revisions branch name
207 --config convert.hg.usebranchnames=True (boolean)
207 --config convert.hg.usebranchnames=True (boolean)
208 preserve branch names
208 preserve branch names
209
209
210 options:
210 options:
211
211
212 -A --authors username mapping filename
212 -A --authors username mapping filename
213 -d --dest-type destination repository type
213 -d --dest-type destination repository type
214 --filemap remap file names using contents of file
214 --filemap remap file names using contents of file
215 -r --rev import up to target revision REV
215 -r --rev import up to target revision REV
216 -s --source-type source repository type
216 -s --source-type source repository type
217 --splicemap splice synthesized history into place
217 --splicemap splice synthesized history into place
218 --branchmap change branch names while converting
218 --branchmap change branch names while converting
219 --branchsort try to sort changesets by branches
219 --branchsort try to sort changesets by branches
220 --datesort try to sort changesets by date
220 --datesort try to sort changesets by date
221 --sourcesort preserve source changesets order
221 --sourcesort preserve source changesets order
222
222
223 use "hg -v help convert" to show global options
223 use "hg -v help convert" to show global options
224 adding a
224 adding a
225 assuming destination a-hg
225 assuming destination a-hg
226 initializing destination a-hg repository
226 initializing destination a-hg repository
227 scanning source...
227 scanning source...
228 sorting...
228 sorting...
229 converting...
229 converting...
230 4 a
230 4 a
231 3 b
231 3 b
232 2 c
232 2 c
233 1 d
233 1 d
234 0 e
234 0 e
235 pulling from ../a
235 pulling from ../a
236 searching for changes
236 searching for changes
237 no changes found
237 no changes found
238 % should fail
238 % should fail
239 initializing destination bogusfile repository
239 initializing destination bogusfile repository
240 abort: cannot create new bundle repository
240 abort: cannot create new bundle repository
241 % should fail
241 % should fail
242 abort: Permission denied: bogusdir
242 abort: Permission denied: bogusdir
243 % should succeed
243 % should succeed
244 initializing destination bogusdir repository
244 initializing destination bogusdir repository
245 scanning source...
245 scanning source...
246 sorting...
246 sorting...
247 converting...
247 converting...
248 4 a
248 4 a
249 3 b
249 3 b
250 2 c
250 2 c
251 1 d
251 1 d
252 0 e
252 0 e
253 % test pre and post conversion actions
253 % test pre and post conversion actions
254 run hg source pre-conversion action
254 run hg source pre-conversion action
255 run hg sink pre-conversion action
255 run hg sink pre-conversion action
256 run hg sink post-conversion action
256 run hg sink post-conversion action
257 run hg source post-conversion action
257 run hg source post-conversion action
258 % converting empty dir should fail nicely
258 % converting empty dir should fail nicely
259 assuming destination emptydir-hg
259 assuming destination emptydir-hg
260 initializing destination emptydir-hg repository
260 initializing destination emptydir-hg repository
261 emptydir does not look like a CVS checkout
261 emptydir does not look like a CVS checkout
262 emptydir does not look like a Git repo
262 emptydir does not look like a Git repository
263 emptydir does not look like a Subversion repo
263 emptydir does not look like a Subversion repository
264 emptydir is not a local Mercurial repo
264 emptydir is not a local Mercurial repository
265 emptydir does not look like a darcs repo
265 emptydir does not look like a darcs repository
266 emptydir does not look like a monotone repo
266 emptydir does not look like a monotone repository
267 emptydir does not look like a GNU Arch repo
267 emptydir does not look like a GNU Arch repository
268 emptydir does not look like a Bazaar repo
268 emptydir does not look like a Bazaar repository
269 cannot find required "p4" tool
269 cannot find required "p4" tool
270 abort: emptydir: missing or unsupported repository
270 abort: emptydir: missing or unsupported repository
271 % convert with imaginary source type
271 % convert with imaginary source type
272 initializing destination a-foo repository
272 initializing destination a-foo repository
273 abort: foo: invalid source repository type
273 abort: foo: invalid source repository type
274 % convert with imaginary sink type
274 % convert with imaginary sink type
275 abort: foo: invalid destination repository type
275 abort: foo: invalid destination repository type
276
276
277 % testing: convert must not produce duplicate entries in fncache
277 % testing: convert must not produce duplicate entries in fncache
278 initializing destination b repository
278 initializing destination b repository
279 scanning source...
279 scanning source...
280 sorting...
280 sorting...
281 converting...
281 converting...
282 4 a
282 4 a
283 3 b
283 3 b
284 2 c
284 2 c
285 1 d
285 1 d
286 0 e
286 0 e
287 % contents of fncache file:
287 % contents of fncache file:
288 data/a.i
288 data/a.i
289 data/b.i
289 data/b.i
290 % test bogus URL
290 % test bogus URL
291 abort: bzr+ssh://foobar@selenic.com/baz: missing or unsupported repository
291 abort: bzr+ssh://foobar@selenic.com/baz: missing or unsupported repository
General Comments 0
You need to be logged in to leave comments. Login now