##// END OF EJS Templates
convert: added GNU Arch source converter
Aleix Conchillo Flaque -
r6035:df659eb2 default
parent child Browse files
Show More
@@ -0,0 +1,263
1 # GNU Arch support for the convert extension
2
3 from common import NoRepo, checktool, commandline, commit, converter_source
4 from mercurial.i18n import _
5 from mercurial import util
6 import os, shutil, tempfile, stat
7
8 class gnuarch_source(converter_source, commandline):
9
10 class gnuarch_rev:
11 def __init__(self, rev):
12 self.rev = rev
13 self.summary = ''
14 self.date = None
15 self.author = ''
16 self.add_files = []
17 self.mod_files = []
18 self.del_files = []
19 self.ren_files = {}
20 self.ren_dirs = {}
21
22 def __init__(self, ui, path, rev=None):
23 super(gnuarch_source, self).__init__(ui, path, rev=rev)
24
25 if not os.path.exists(os.path.join(path, '{arch}')):
26 raise NoRepo(_("couldn't open GNU Arch repo %s" % path))
27
28 # Could use checktool, but we want to check for baz or tla.
29 self.execmd = None
30 if util.find_exe('tla'):
31 self.execmd = 'tla'
32 else:
33 if util.find_exe('baz'):
34 self.execmd = 'baz'
35 else:
36 raise util.Abort(_('cannot find a GNU Arch tool'))
37
38 commandline.__init__(self, ui, self.execmd)
39
40 self.path = os.path.realpath(path)
41 self.tmppath = None
42
43 self.treeversion = None
44 self.lastrev = None
45 self.changes = {}
46 self.parents = {}
47 self.tags = {}
48 self.modecache = {}
49
50 def before(self):
51 if self.execmd == 'tla':
52 output = self.run0('tree-version', self.path)
53 else:
54 output = self.run0('tree-version', '-d', self.path)
55 self.treeversion = output.strip()
56
57 self.ui.status(_('analyzing tree version %s...\n' % self.treeversion))
58
59 # Get name of temporary directory
60 version = self.treeversion.split('/')
61 self.tmppath = os.path.join(tempfile.gettempdir(),
62 'hg-%s' % version[1])
63
64 # Generate parents dictionary
65 child = []
66 output, status = self.runlines('revisions', self.treeversion)
67 self.checkexit(status, 'archive registered?')
68 for l in output:
69 rev = l.strip()
70 self.changes[rev] = self.gnuarch_rev(rev)
71
72 # Read author, date and summary
73 catlog = self.runlines0('cat-log', '-d', self.path, rev)
74 self._parsecatlog(catlog, rev)
75
76 self.parents[rev] = child
77 child = [rev]
78 if rev == self.rev:
79 break
80 self.parents[None] = child
81
82 def after(self):
83 self.ui.debug(_('cleaning up %s\n' % self.tmppath))
84 shutil.rmtree(self.tmppath, ignore_errors=True)
85
86 def getheads(self):
87 return self.parents[None]
88
89 def getfile(self, name, rev):
90 if rev != self.lastrev:
91 raise util.Abort(_('internal calling inconsistency'))
92
93 # Raise IOError if necessary (i.e. deleted files).
94 if not os.path.exists(os.path.join(self.tmppath, name)):
95 raise IOError
96
97 data, mode = self._getfile(name, rev)
98 self.modecache[(name, rev)] = mode
99
100 return data
101
102 def getmode(self, name, rev):
103 return self.modecache[(name, rev)]
104
105 def getchanges(self, rev):
106 self.modecache = {}
107 self._update(rev)
108 changes = []
109 copies = {}
110
111 for f in self.changes[rev].add_files:
112 changes.append((f, rev))
113
114 for f in self.changes[rev].mod_files:
115 changes.append((f, rev))
116
117 for f in self.changes[rev].del_files:
118 changes.append((f, rev))
119
120 for src in self.changes[rev].ren_files:
121 to = self.changes[rev].ren_files[src]
122 changes.append((src, rev))
123 changes.append((to, rev))
124 copies[src] = to
125
126 for src in self.changes[rev].ren_dirs:
127 to = self.changes[rev].ren_dirs[src]
128 chgs, cps = self._rendirchanges(src, to);
129 changes += [(f, rev) for f in chgs]
130 for c in cps:
131 copies[c] = cps[c]
132
133 changes.sort()
134 self.lastrev = rev
135
136 return changes, copies
137
138 def getcommit(self, rev):
139 changes = self.changes[rev]
140 return commit(author = changes.author, date = changes.date,
141 desc = changes.summary, parents = self.parents[rev])
142
143 def gettags(self):
144 return self.tags
145
146 def _execute(self, cmd, *args, **kwargs):
147 cmdline = [self.execmd, cmd]
148 cmdline += args
149 cmdline = [util.shellquote(arg) for arg in cmdline]
150 cmdline += ['>', util.nulldev, '2>', util.nulldev]
151 cmdline = util.quotecommand(' '.join(cmdline))
152 self.ui.debug(cmdline, '\n')
153 return os.system(cmdline)
154
155 def _update(self, rev):
156 if rev == 'base-0':
157 # Initialise 'base-0' revision
158 self.ui.debug(_('obtaining revision %s...\n' % rev))
159 revision = '%s--%s' % (self.treeversion, rev)
160 output = self._execute('get', revision, self.tmppath)
161 self.ui.debug(_('analysing revision %s...\n' % rev))
162 files = self._readcontents(self.tmppath)
163 self.changes[rev].add_files += files
164 else:
165 self.ui.debug(_('applying revision %s...\n' % rev))
166 revision = '%s--%s' % (self.treeversion, rev)
167 output = self._execute('replay', '-d', self.tmppath, revision)
168
169 old_rev = self.parents[rev][0]
170 self.ui.debug(_('computing changeset between %s and %s...\n' \
171 % (old_rev, rev)))
172 rev_a = '%s--%s' % (self.treeversion, old_rev)
173 rev_b = '%s--%s' % (self.treeversion, rev)
174 delta = self.runlines0('delta', '-n', rev_a, rev_b)
175 self._parsedelta(delta, rev)
176
177 def _getfile(self, name, rev):
178 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
179 if stat.S_ISLNK(mode):
180 data = os.readlink(os.path.join(self.tmppath, name))
181 mode = mode and 'l' or ''
182 else:
183 data = open(os.path.join(self.tmppath, name), 'rb').read()
184 mode = (mode & 0111) and 'x' or ''
185 return data, mode
186
187 def _exclude(self, name):
188 exclude = [ '{arch}', '.arch-ids', '.arch-inventory' ]
189 for exc in exclude:
190 if name.find(exc) != -1:
191 return True
192 return False
193
194 def _readcontents(self, path):
195 files = []
196 contents = os.listdir(path)
197 while len(contents) > 0:
198 c = contents.pop()
199 p = os.path.join(path, c)
200 if not self._exclude(p):
201 if os.path.isdir(p):
202 contents += [os.path.join(c, f) for f in os.listdir(p)]
203 else:
204 files.append(c)
205 return files
206
207 def _rendirchanges(self, src, dest):
208 changes = []
209 copies = {}
210 files = self._readcontents(os.path.join(self.tmppath, dest))
211 for f in files:
212 s = os.path.join(src, f)
213 d = os.path.join(dest, f)
214 changes.append(s)
215 changes.append(d)
216 copies[s] = d
217 return changes, copies
218
219 def _parsecatlog(self, data, rev):
220 for l in data:
221 l = l.strip()
222 if l.startswith('Summary:'):
223 self.changes[rev].summary = l[len('Summary: '):]
224
225 if l.startswith('Standard-date:'):
226 date = l[len('Standard-date: '):]
227 strdate = util.strdate(date, '%Y-%m-%d %H:%M:%S')
228 self.changes[rev].date = util.datestr(strdate)
229
230 if l.startswith('Creator:'):
231 self.changes[rev].author = l[len('Creator: '):]
232
233 def _parsedelta(self, data, rev):
234 for l in data:
235 l = l.strip()
236 if l.startswith('A') and not l.startswith('A/'):
237 file = l[1:].strip()
238 if not self._exclude(file):
239 self.changes[rev].add_files.append(file)
240 elif l.startswith('/>'):
241 dirs = l[2:].strip().split(' ')
242 if len(dirs) == 1:
243 dirs = l[2:].strip().split('\t')
244 if not self._exclude(dirs[0]) and not self._exclude(dirs[1]):
245 self.changes[rev].ren_dirs[dirs[0]] = dirs[1]
246 elif l.startswith('M'):
247 file = l[1:].strip()
248 if not self._exclude(file):
249 self.changes[rev].mod_files.append(file)
250 elif l.startswith('->'):
251 file = l[2:].strip()
252 if not self._exclude(file):
253 self.changes[rev].mod_files.append(file)
254 elif l.startswith('D') and not l.startswith('D/'):
255 file = l[1:].strip()
256 if not self._exclude(file):
257 self.changes[rev].del_files.append(file)
258 elif l.startswith('=>'):
259 files = l[2:].strip().split(' ')
260 if len(files) == 1:
261 files = l[2:].strip().split('\t')
262 if not self._exclude(files[0]) and not self._exclude(files[1]):
263 self.changes[rev].ren_files[files[0]] = files[1]
@@ -1,108 +1,109
1 1 # convert.py Foreign SCM converter
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 import convcmd
9 9 from mercurial import commands
10 10
11 11 # Commands definition was moved elsewhere to ease demandload job.
12 12
13 13 def convert(ui, src, dest=None, revmapfile=None, **opts):
14 14 """Convert a foreign SCM repository to a Mercurial one.
15 15
16 16 Accepted source formats:
17 17 - Mercurial
18 18 - CVS
19 19 - Darcs
20 20 - git
21 21 - Subversion
22 - GNU Arch
22 23
23 24 Accepted destination formats:
24 25 - Mercurial
25 26 - Subversion (history on branches is not preserved)
26 27
27 28 If no revision is given, all revisions will be converted. Otherwise,
28 29 convert will only import up to the named revision (given in a format
29 30 understood by the source).
30 31
31 32 If no destination directory name is specified, it defaults to the
32 33 basename of the source with '-hg' appended. If the destination
33 34 repository doesn't exist, it will be created.
34 35
35 36 If <MAPFILE> isn't given, it will be put in a default location
36 37 (<dest>/.hg/shamap by default). The <MAPFILE> is a simple text
37 38 file that maps each source commit ID to the destination ID for
38 39 that revision, like so:
39 40 <source ID> <destination ID>
40 41
41 42 If the file doesn't exist, it's automatically created. It's updated
42 43 on each commit copied, so convert-repo can be interrupted and can
43 44 be run repeatedly to copy new commits.
44 45
45 46 The [username mapping] file is a simple text file that maps each source
46 47 commit author to a destination commit author. It is handy for source SCMs
47 48 that use unix logins to identify authors (eg: CVS). One line per author
48 49 mapping and the line format is:
49 50 srcauthor=whatever string you want
50 51
51 52 The filemap is a file that allows filtering and remapping of files
52 53 and directories. Comment lines start with '#'. Each line can
53 54 contain one of the following directives:
54 55
55 56 include path/to/file
56 57
57 58 exclude path/to/file
58 59
59 60 rename from/file to/file
60 61
61 62 The 'include' directive causes a file, or all files under a
62 63 directory, to be included in the destination repository, and the
63 64 exclusion of all other files and dirs not explicitely included.
64 65 The 'exclude' directive causes files or directories to be omitted.
65 66 The 'rename' directive renames a file or directory. To rename from a
66 67 subdirectory into the root of the repository, use '.' as the path to
67 68 rename to.
68 69
69 70 Back end options:
70 71
71 72 --config convert.hg.clonebranches=False (boolean)
72 73 hg target: XXX not documented
73 74 --config convert.hg.saverev=True (boolean)
74 75 hg source: allow target to preserve source revision ID
75 76 --config convert.hg.tagsbranch=default (branch name)
76 77 hg target: XXX not documented
77 78 --config convert.hg.usebranchnames=True (boolean)
78 79 hg target: preserve branch names
79 80
80 81 --config convert.svn.branches=branches (directory name)
81 82 svn source: specify the directory containing branches
82 83 --config convert.svn.tags=tags (directory name)
83 84 svn source: specify the directory containing tags
84 85 --config convert.svn.trunk=trunk (directory name)
85 86 svn source: specify the name of the trunk branch
86 87 """
87 88 return convcmd.convert(ui, src, dest, revmapfile, **opts)
88 89
89 90 def debugsvnlog(ui, **opts):
90 91 return convcmd.debugsvnlog(ui, **opts)
91 92
92 93 commands.norepo += " convert debugsvnlog"
93 94
94 95 cmdtable = {
95 96 "convert":
96 97 (convert,
97 98 [('A', 'authors', '', 'username mapping filename'),
98 99 ('d', 'dest-type', '', 'destination repository type'),
99 100 ('', 'filemap', '', 'remap file names using contents of file'),
100 101 ('r', 'rev', '', 'import up to target revision REV'),
101 102 ('s', 'source-type', '', 'source repository type'),
102 103 ('', 'datesort', None, 'try to sort changesets by date')],
103 104 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
104 105 "debugsvnlog":
105 106 (debugsvnlog,
106 107 [],
107 108 'hg debugsvnlog'),
108 109 }
@@ -1,343 +1,354
1 1 # common code for the convert extension
2 2 import base64, errno
3 3 import os
4 4 import cPickle as pickle
5 5 from mercurial import util
6 6 from mercurial.i18n import _
7 7
8 8 def encodeargs(args):
9 9 def encodearg(s):
10 10 lines = base64.encodestring(s)
11 11 lines = [l.splitlines()[0] for l in lines]
12 12 return ''.join(lines)
13 13
14 14 s = pickle.dumps(args)
15 15 return encodearg(s)
16 16
17 17 def decodeargs(s):
18 18 s = base64.decodestring(s)
19 19 return pickle.loads(s)
20 20
21 21 def checktool(exe, name=None):
22 22 name = name or exe
23 23 if not util.find_exe(exe):
24 24 raise util.Abort('cannot find required "%s" tool' % name)
25 25
26 26 class NoRepo(Exception): pass
27 27
28 28 SKIPREV = 'SKIP'
29 29
30 30 class commit(object):
31 31 def __init__(self, author, date, desc, parents, branch=None, rev=None,
32 32 extra={}):
33 33 self.author = author or 'unknown'
34 34 self.date = date or '0 0'
35 35 self.desc = desc
36 36 self.parents = parents
37 37 self.branch = branch
38 38 self.rev = rev
39 39 self.extra = extra
40 40
41 41 class converter_source(object):
42 42 """Conversion source interface"""
43 43
44 44 def __init__(self, ui, path=None, rev=None):
45 45 """Initialize conversion source (or raise NoRepo("message")
46 46 exception if path is not a valid repository)"""
47 47 self.ui = ui
48 48 self.path = path
49 49 self.rev = rev
50 50
51 51 self.encoding = 'utf-8'
52 52
53 53 def before(self):
54 54 pass
55 55
56 56 def after(self):
57 57 pass
58 58
59 59 def setrevmap(self, revmap):
60 60 """set the map of already-converted revisions"""
61 61 pass
62 62
63 63 def getheads(self):
64 64 """Return a list of this repository's heads"""
65 65 raise NotImplementedError()
66 66
67 67 def getfile(self, name, rev):
68 68 """Return file contents as a string"""
69 69 raise NotImplementedError()
70 70
71 71 def getmode(self, name, rev):
72 72 """Return file mode, eg. '', 'x', or 'l'"""
73 73 raise NotImplementedError()
74 74
75 75 def getchanges(self, version):
76 76 """Returns a tuple of (files, copies)
77 77 Files is a sorted list of (filename, id) tuples for all files changed
78 78 in version, where id is the source revision id of the file.
79 79
80 80 copies is a dictionary of dest: source
81 81 """
82 82 raise NotImplementedError()
83 83
84 84 def getcommit(self, version):
85 85 """Return the commit object for version"""
86 86 raise NotImplementedError()
87 87
88 88 def gettags(self):
89 89 """Return the tags as a dictionary of name: revision"""
90 90 raise NotImplementedError()
91 91
92 92 def recode(self, s, encoding=None):
93 93 if not encoding:
94 94 encoding = self.encoding or 'utf-8'
95 95
96 96 if isinstance(s, unicode):
97 97 return s.encode("utf-8")
98 98 try:
99 99 return s.decode(encoding).encode("utf-8")
100 100 except:
101 101 try:
102 102 return s.decode("latin-1").encode("utf-8")
103 103 except:
104 104 return s.decode(encoding, "replace").encode("utf-8")
105 105
106 106 def getchangedfiles(self, rev, i):
107 107 """Return the files changed by rev compared to parent[i].
108 108
109 109 i is an index selecting one of the parents of rev. The return
110 110 value should be the list of files that are different in rev and
111 111 this parent.
112 112
113 113 If rev has no parents, i is None.
114 114
115 115 This function is only needed to support --filemap
116 116 """
117 117 raise NotImplementedError()
118 118
119 119 def converted(self, rev, sinkrev):
120 120 '''Notify the source that a revision has been converted.'''
121 121 pass
122 122
123 123
124 124 class converter_sink(object):
125 125 """Conversion sink (target) interface"""
126 126
127 127 def __init__(self, ui, path):
128 128 """Initialize conversion sink (or raise NoRepo("message")
129 129 exception if path is not a valid repository)
130 130
131 131 created is a list of paths to remove if a fatal error occurs
132 132 later"""
133 133 self.ui = ui
134 134 self.path = path
135 135 self.created = []
136 136
137 137 def getheads(self):
138 138 """Return a list of this repository's heads"""
139 139 raise NotImplementedError()
140 140
141 141 def revmapfile(self):
142 142 """Path to a file that will contain lines
143 143 source_rev_id sink_rev_id
144 144 mapping equivalent revision identifiers for each system."""
145 145 raise NotImplementedError()
146 146
147 147 def authorfile(self):
148 148 """Path to a file that will contain lines
149 149 srcauthor=dstauthor
150 150 mapping equivalent authors identifiers for each system."""
151 151 return None
152 152
153 153 def putfile(self, f, e, data):
154 154 """Put file for next putcommit().
155 155 f: path to file
156 156 e: '', 'x', or 'l' (regular file, executable, or symlink)
157 157 data: file contents"""
158 158 raise NotImplementedError()
159 159
160 160 def delfile(self, f):
161 161 """Delete file for next putcommit().
162 162 f: path to file"""
163 163 raise NotImplementedError()
164 164
165 165 def putcommit(self, files, parents, commit):
166 166 """Create a revision with all changed files listed in 'files'
167 167 and having listed parents. 'commit' is a commit object containing
168 168 at a minimum the author, date, and message for this changeset.
169 169 Called after putfile() and delfile() calls. Note that the sink
170 170 repository is not told to update itself to a particular revision
171 171 (or even what that revision would be) before it receives the
172 172 file data."""
173 173 raise NotImplementedError()
174 174
175 175 def puttags(self, tags):
176 176 """Put tags into sink.
177 177 tags: {tagname: sink_rev_id, ...}"""
178 178 raise NotImplementedError()
179 179
180 180 def setbranch(self, branch, pbranches):
181 181 """Set the current branch name. Called before the first putfile
182 182 on the branch.
183 183 branch: branch name for subsequent commits
184 184 pbranches: (converted parent revision, parent branch) tuples"""
185 185 pass
186 186
187 187 def setfilemapmode(self, active):
188 188 """Tell the destination that we're using a filemap
189 189
190 190 Some converter_sources (svn in particular) can claim that a file
191 191 was changed in a revision, even if there was no change. This method
192 192 tells the destination that we're using a filemap and that it should
193 193 filter empty revisions.
194 194 """
195 195 pass
196 196
197 197 def before(self):
198 198 pass
199 199
200 200 def after(self):
201 201 pass
202 202
203 203
204 204 class commandline(object):
205 205 def __init__(self, ui, command):
206 206 self.ui = ui
207 207 self.command = command
208 208
209 209 def prerun(self):
210 210 pass
211 211
212 212 def postrun(self):
213 213 pass
214 214
215 215 def _cmdline(self, cmd, *args, **kwargs):
216 216 cmdline = [self.command, cmd] + list(args)
217 217 for k, v in kwargs.iteritems():
218 218 if len(k) == 1:
219 219 cmdline.append('-' + k)
220 220 else:
221 221 cmdline.append('--' + k.replace('_', '-'))
222 222 try:
223 223 if len(k) == 1:
224 224 cmdline.append('' + v)
225 225 else:
226 226 cmdline[-1] += '=' + v
227 227 except TypeError:
228 228 pass
229 229 cmdline = [util.shellquote(arg) for arg in cmdline]
230 cmdline += ['<', util.nulldev]
230 cmdline += ['2>', util.nulldev, '<', util.nulldev]
231 231 cmdline = ' '.join(cmdline)
232 232 self.ui.debug(cmdline, '\n')
233 233 return cmdline
234 234
235 235 def _run(self, cmd, *args, **kwargs):
236 236 cmdline = self._cmdline(cmd, *args, **kwargs)
237 237 self.prerun()
238 238 try:
239 239 return util.popen(cmdline)
240 240 finally:
241 241 self.postrun()
242 242
243 243 def run(self, cmd, *args, **kwargs):
244 244 fp = self._run(cmd, *args, **kwargs)
245 245 output = fp.read()
246 246 self.ui.debug(output)
247 247 return output, fp.close()
248 248
249 def runlines(self, cmd, *args, **kwargs):
250 fp = self._run(cmd, *args, **kwargs)
251 output = fp.readlines()
252 self.ui.debug(output)
253 return output, fp.close()
254
249 255 def checkexit(self, status, output=''):
250 256 if status:
251 257 if output:
252 258 self.ui.warn(_('%s error:\n') % self.command)
253 259 self.ui.warn(output)
254 260 msg = util.explain_exit(status)[0]
255 261 raise util.Abort(_('%s %s') % (self.command, msg))
256 262
257 263 def run0(self, cmd, *args, **kwargs):
258 264 output, status = self.run(cmd, *args, **kwargs)
259 265 self.checkexit(status, output)
260 266 return output
261 267
268 def runlines0(self, cmd, *args, **kwargs):
269 output, status = self.runlines(cmd, *args, **kwargs)
270 self.checkexit(status, output)
271 return output
272
262 273 def getargmax(self):
263 274 if '_argmax' in self.__dict__:
264 275 return self._argmax
265 276
266 277 # POSIX requires at least 4096 bytes for ARG_MAX
267 278 self._argmax = 4096
268 279 try:
269 280 self._argmax = os.sysconf("SC_ARG_MAX")
270 281 except:
271 282 pass
272 283
273 284 # Windows shells impose their own limits on command line length,
274 285 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
275 286 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
276 287 # details about cmd.exe limitations.
277 288
278 289 # Since ARG_MAX is for command line _and_ environment, lower our limit
279 290 # (and make happy Windows shells while doing this).
280 291
281 292 self._argmax = self._argmax/2 - 1
282 293 return self._argmax
283 294
284 295 def limit_arglist(self, arglist, cmd, *args, **kwargs):
285 296 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
286 297 bytes = 0
287 298 fl = []
288 299 for fn in arglist:
289 300 b = len(fn) + 3
290 301 if bytes + b < limit or len(fl) == 0:
291 302 fl.append(fn)
292 303 bytes += b
293 304 else:
294 305 yield fl
295 306 fl = [fn]
296 307 bytes = b
297 308 if fl:
298 309 yield fl
299 310
300 311 def xargs(self, arglist, cmd, *args, **kwargs):
301 312 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
302 313 self.run0(cmd, *(list(args) + l), **kwargs)
303 314
304 315 class mapfile(dict):
305 316 def __init__(self, ui, path):
306 317 super(mapfile, self).__init__()
307 318 self.ui = ui
308 319 self.path = path
309 320 self.fp = None
310 321 self.order = []
311 322 self._read()
312 323
313 324 def _read(self):
314 325 if self.path is None:
315 326 return
316 327 try:
317 328 fp = open(self.path, 'r')
318 329 except IOError, err:
319 330 if err.errno != errno.ENOENT:
320 331 raise
321 332 return
322 333 for line in fp:
323 334 key, value = line[:-1].split(' ', 1)
324 335 if key not in self:
325 336 self.order.append(key)
326 337 super(mapfile, self).__setitem__(key, value)
327 338 fp.close()
328 339
329 340 def __setitem__(self, key, value):
330 341 if self.fp is None:
331 342 try:
332 343 self.fp = open(self.path, 'a')
333 344 except IOError, err:
334 345 raise util.Abort(_('could not open map file %r: %s') %
335 346 (self.path, err.strerror))
336 347 self.fp.write('%s %s\n' % (key, value))
337 348 self.fp.flush()
338 349 super(mapfile, self).__setitem__(key, value)
339 350
340 351 def close(self):
341 352 if self.fp:
342 353 self.fp.close()
343 354 self.fp = None
@@ -1,328 +1,330
1 1 # convcmd - convert extension commands definition
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from common import NoRepo, SKIPREV, converter_source, converter_sink, mapfile
9 9 from cvs import convert_cvs
10 10 from darcs import darcs_source
11 11 from git import convert_git
12 12 from hg import mercurial_source, mercurial_sink
13 13 from subversion import debugsvnlog, svn_source, svn_sink
14 from gnuarch import gnuarch_source
14 15 import filemap
15 16
16 17 import os, shutil
17 18 from mercurial import hg, util
18 19 from mercurial.i18n import _
19 20
20 21 source_converters = [
21 22 ('cvs', convert_cvs),
22 23 ('git', convert_git),
23 24 ('svn', svn_source),
24 25 ('hg', mercurial_source),
25 26 ('darcs', darcs_source),
27 ('gnuarch', gnuarch_source),
26 28 ]
27 29
28 30 sink_converters = [
29 31 ('hg', mercurial_sink),
30 32 ('svn', svn_sink),
31 33 ]
32 34
33 35 def convertsource(ui, path, type, rev):
34 36 exceptions = []
35 37 for name, source in source_converters:
36 38 try:
37 39 if not type or name == type:
38 40 return source(ui, path, rev)
39 41 except NoRepo, inst:
40 42 exceptions.append(inst)
41 43 if not ui.quiet:
42 44 for inst in exceptions:
43 45 ui.write(_("%s\n") % inst)
44 46 raise util.Abort('%s: unknown repository type' % path)
45 47
46 48 def convertsink(ui, path, type):
47 49 for name, sink in sink_converters:
48 50 try:
49 51 if not type or name == type:
50 52 return sink(ui, path)
51 53 except NoRepo, inst:
52 54 ui.note(_("convert: %s\n") % inst)
53 55 raise util.Abort('%s: unknown repository type' % path)
54 56
55 57 class converter(object):
56 58 def __init__(self, ui, source, dest, revmapfile, opts):
57 59
58 60 self.source = source
59 61 self.dest = dest
60 62 self.ui = ui
61 63 self.opts = opts
62 64 self.commitcache = {}
63 65 self.authors = {}
64 66 self.authorfile = None
65 67
66 68 self.map = mapfile(ui, revmapfile)
67 69
68 70 # Read first the dst author map if any
69 71 authorfile = self.dest.authorfile()
70 72 if authorfile and os.path.exists(authorfile):
71 73 self.readauthormap(authorfile)
72 74 # Extend/Override with new author map if necessary
73 75 if opts.get('authors'):
74 76 self.readauthormap(opts.get('authors'))
75 77 self.authorfile = self.dest.authorfile()
76 78
77 79 self.splicemap = mapfile(ui, ui.config('convert', 'splicemap'))
78 80
79 81 def walktree(self, heads):
80 82 '''Return a mapping that identifies the uncommitted parents of every
81 83 uncommitted changeset.'''
82 84 visit = heads
83 85 known = {}
84 86 parents = {}
85 87 while visit:
86 88 n = visit.pop(0)
87 89 if n in known or n in self.map: continue
88 90 known[n] = 1
89 91 commit = self.cachecommit(n)
90 92 parents[n] = []
91 93 for p in commit.parents:
92 94 parents[n].append(p)
93 95 visit.append(p)
94 96
95 97 return parents
96 98
97 99 def toposort(self, parents):
98 100 '''Return an ordering such that every uncommitted changeset is
99 101 preceeded by all its uncommitted ancestors.'''
100 102 visit = parents.keys()
101 103 seen = {}
102 104 children = {}
103 105
104 106 while visit:
105 107 n = visit.pop(0)
106 108 if n in seen: continue
107 109 seen[n] = 1
108 110 # Ensure that nodes without parents are present in the 'children'
109 111 # mapping.
110 112 children.setdefault(n, [])
111 113 for p in parents[n]:
112 114 if not p in self.map:
113 115 visit.append(p)
114 116 children.setdefault(p, []).append(n)
115 117
116 118 s = []
117 119 removed = {}
118 120 visit = children.keys()
119 121 while visit:
120 122 n = visit.pop(0)
121 123 if n in removed: continue
122 124 dep = 0
123 125 if n in parents:
124 126 for p in parents[n]:
125 127 if p in self.map: continue
126 128 if p not in removed:
127 129 # we're still dependent
128 130 visit.append(n)
129 131 dep = 1
130 132 break
131 133
132 134 if not dep:
133 135 # all n's parents are in the list
134 136 removed[n] = 1
135 137 if n not in self.map:
136 138 s.append(n)
137 139 if n in children:
138 140 for c in children[n]:
139 141 visit.insert(0, c)
140 142
141 143 if self.opts.get('datesort'):
142 144 depth = {}
143 145 for n in s:
144 146 depth[n] = 0
145 147 pl = [p for p in self.commitcache[n].parents
146 148 if p not in self.map]
147 149 if pl:
148 150 depth[n] = max([depth[p] for p in pl]) + 1
149 151
150 152 s = [(depth[n], util.parsedate(self.commitcache[n].date), n)
151 153 for n in s]
152 154 s.sort()
153 155 s = [e[2] for e in s]
154 156
155 157 return s
156 158
157 159 def writeauthormap(self):
158 160 authorfile = self.authorfile
159 161 if authorfile:
160 162 self.ui.status('Writing author map file %s\n' % authorfile)
161 163 ofile = open(authorfile, 'w+')
162 164 for author in self.authors:
163 165 ofile.write("%s=%s\n" % (author, self.authors[author]))
164 166 ofile.close()
165 167
166 168 def readauthormap(self, authorfile):
167 169 afile = open(authorfile, 'r')
168 170 for line in afile:
169 171 try:
170 172 srcauthor = line.split('=')[0].strip()
171 173 dstauthor = line.split('=')[1].strip()
172 174 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
173 175 self.ui.status(
174 176 'Overriding mapping for author %s, was %s, will be %s\n'
175 177 % (srcauthor, self.authors[srcauthor], dstauthor))
176 178 else:
177 179 self.ui.debug('Mapping author %s to %s\n'
178 180 % (srcauthor, dstauthor))
179 181 self.authors[srcauthor] = dstauthor
180 182 except IndexError:
181 183 self.ui.warn(
182 184 'Ignoring bad line in author file map %s: %s\n'
183 185 % (authorfile, line))
184 186 afile.close()
185 187
186 188 def cachecommit(self, rev):
187 189 commit = self.source.getcommit(rev)
188 190 commit.author = self.authors.get(commit.author, commit.author)
189 191 self.commitcache[rev] = commit
190 192 return commit
191 193
192 194 def copy(self, rev):
193 195 commit = self.commitcache[rev]
194 196 do_copies = hasattr(self.dest, 'copyfile')
195 197 filenames = []
196 198
197 199 changes = self.source.getchanges(rev)
198 200 if isinstance(changes, basestring):
199 201 if changes == SKIPREV:
200 202 dest = SKIPREV
201 203 else:
202 204 dest = self.map[changes]
203 205 self.map[rev] = dest
204 206 return
205 207 files, copies = changes
206 208 pbranches = []
207 209 if commit.parents:
208 210 for prev in commit.parents:
209 211 if prev not in self.commitcache:
210 212 self.cachecommit(prev)
211 213 pbranches.append((self.map[prev],
212 214 self.commitcache[prev].branch))
213 215 self.dest.setbranch(commit.branch, pbranches)
214 216 for f, v in files:
215 217 filenames.append(f)
216 218 try:
217 219 data = self.source.getfile(f, v)
218 220 except IOError, inst:
219 221 self.dest.delfile(f)
220 222 else:
221 223 e = self.source.getmode(f, v)
222 224 self.dest.putfile(f, e, data)
223 225 if do_copies:
224 226 if f in copies:
225 227 copyf = copies[f]
226 228 # Merely marks that a copy happened.
227 229 self.dest.copyfile(copyf, f)
228 230
229 231 try:
230 232 parents = [self.splicemap[rev]]
231 233 self.ui.debug('spliced in %s as parents of %s\n' %
232 234 (parents, rev))
233 235 except KeyError:
234 236 parents = [b[0] for b in pbranches]
235 237 newnode = self.dest.putcommit(filenames, parents, commit)
236 238 self.source.converted(rev, newnode)
237 239 self.map[rev] = newnode
238 240
239 241 def convert(self):
240 242
241 243 def recode(s):
242 244 return s.decode('utf-8').encode(orig_encoding, 'replace')
243 245
244 246 try:
245 247 self.source.before()
246 248 self.dest.before()
247 249 self.source.setrevmap(self.map)
248 250 self.ui.status("scanning source...\n")
249 251 heads = self.source.getheads()
250 252 parents = self.walktree(heads)
251 253 self.ui.status("sorting...\n")
252 254 t = self.toposort(parents)
253 255 num = len(t)
254 256 c = None
255 257
256 258 self.ui.status("converting...\n")
257 259 for c in t:
258 260 num -= 1
259 261 desc = self.commitcache[c].desc
260 262 if "\n" in desc:
261 263 desc = desc.splitlines()[0]
262 264 # convert log message to local encoding without using
263 265 # tolocal() because util._encoding conver() use it as
264 266 # 'utf-8'
265 267 self.ui.status("%d %s\n" % (num, recode(desc)))
266 268 self.ui.note(_("source: %s\n" % recode(c)))
267 269 self.copy(c)
268 270
269 271 tags = self.source.gettags()
270 272 ctags = {}
271 273 for k in tags:
272 274 v = tags[k]
273 275 if self.map.get(v, SKIPREV) != SKIPREV:
274 276 ctags[k] = self.map[v]
275 277
276 278 if c and ctags:
277 279 nrev = self.dest.puttags(ctags)
278 280 # write another hash correspondence to override the previous
279 281 # one so we don't end up with extra tag heads
280 282 if nrev:
281 283 self.map[c] = nrev
282 284
283 285 self.writeauthormap()
284 286 finally:
285 287 self.cleanup()
286 288
287 289 def cleanup(self):
288 290 try:
289 291 self.dest.after()
290 292 finally:
291 293 self.source.after()
292 294 self.map.close()
293 295
294 296 orig_encoding = 'ascii'
295 297
296 298 def convert(ui, src, dest=None, revmapfile=None, **opts):
297 299 global orig_encoding
298 300 orig_encoding = util._encoding
299 301 util._encoding = 'UTF-8'
300 302
301 303 if not dest:
302 304 dest = hg.defaultdest(src) + "-hg"
303 305 ui.status("assuming destination %s\n" % dest)
304 306
305 307 destc = convertsink(ui, dest, opts.get('dest_type'))
306 308
307 309 try:
308 310 srcc = convertsource(ui, src, opts.get('source_type'),
309 311 opts.get('rev'))
310 312 except Exception:
311 313 for path in destc.created:
312 314 shutil.rmtree(path, True)
313 315 raise
314 316
315 317 fmap = opts.get('filemap')
316 318 if fmap:
317 319 srcc = filemap.filemap_source(ui, srcc, fmap)
318 320 destc.setfilemapmode(True)
319 321
320 322 if not revmapfile:
321 323 try:
322 324 revmapfile = destc.revmapfile()
323 325 except:
324 326 revmapfile = os.path.join(destc, "map")
325 327
326 328 c = converter(ui, srcc, destc, revmapfile, opts)
327 329 c.convert()
328 330
General Comments 0
You need to be logged in to leave comments. Login now