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