fix timezones in darcs-fast-export, take 2
[bzr-fastimport/rorcz.git] / exporters / __init__.py
blobdfdf4f56027803b1e24250351c5d07f82d5faaa7
1 # Copyright (C) 2009 Canonical Ltd
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 """Simplified and unified access to the various xxx-fast-export tools."""
20 import gzip, os, subprocess, sys
22 from bzrlib import errors
23 from bzrlib.trace import note, warning
26 class MissingDependency(Exception):
28 def __init__(self, tool, minimum_version, missing):
29 self.tool = tool
30 self.minimum_version = minimum_version
31 self.missing = missing
33 def get_message(self):
34 return "%s missing. Please install %s %s or later and try again." % \
35 (self.missing, self.tool, self.minimum_version)
38 class _Exporter(object):
40 def check_install(self, tool_name, minimum_version, required_commands=None,
41 required_libraries=None):
42 """Check dependencies are correctly installed.
44 :param tool_name: name of the tool
45 :param minimum_version: minimum version required
46 :param required_commands: list of commands that must be on the path
47 :param required_libraries: list of Python libraries that must be
48 available
49 :raises MissingDependency: if a required dependency is not found
50 """
51 self.tool_name = tool_name
52 self.minimum_version = minimum_version
53 if required_commands:
54 for cmd in required_commands:
55 self._check_cmd_available(cmd)
56 if required_libraries:
57 for lib in required_libraries:
58 self._check_lib_available(lib)
60 def _check_cmd_available(self, cmd):
61 try:
62 if isinstance(cmd, str):
63 args = [cmd]
64 else:
65 args = cmd
66 retcode = subprocess.call(args, stdout=subprocess.PIPE,
67 stderr=subprocess.PIPE)
68 except OSError:
69 raise MissingDependency(self.tool_name, self.minimum_version, cmd)
71 def _check_lib_available(self, lib):
72 try:
73 __import__(lib)
74 except ImportError:
75 raise MissingDependency(self.tool_name, self.minimum_version, lib)
77 def generate(self, source, destination, verbose=False, custom=None):
78 """Generate a fast import stream.
80 :param source: the source filename or URL
81 :param destination: filename or '-' for standard output
82 :param verbose: if True, output additional diagnostics
83 :param custom: a list of custom options to be added to the
84 command line of the underlying scripts used. If an option
85 and its argument are to be separated by a space, pass them
86 as consecutive items.
87 """
88 raise NotImplementedError(self.generate)
90 def get_output_info(self, dest):
91 """Get the output streams/filenames given a destination filename.
93 :return: outf, basename, marks where
94 outf is a file-like object for storing the output,
95 basename is the name without the .fi and .gz prefixes
96 marks is the name of the marks file to use, if any
97 """
98 if dest == '-':
99 return sys.stdout, None, None
100 else:
101 #if dest.endswith('.gz'):
102 # outf = gzip.open(dest, 'wb')
103 # base = dest[:-3]
104 #else:
105 outf = open(dest, 'w')
106 base = dest
107 if base.endswith(".fi"):
108 base = dest[:-3]
109 marks = "%s.marks" % (base,)
110 return outf, base, marks
112 def execute(self, args, outf, cwd=None):
113 """Execute a command, capture the output and close files.
115 :param args: list of arguments making up the command
116 :param outf: a file-like object for storing the output,
117 :param cwd: current working directory to use
118 :return: the return code
120 if cwd is not None:
121 note("Executing %s in directory %s ..." % (" ".join(args), cwd))
122 else:
123 note("Executing %s ..." % (" ".join(args),))
124 try:
125 p = subprocess.Popen(args, stdout=outf, cwd=cwd)
126 p.wait()
127 finally:
128 if outf != sys.stdout:
129 outf.close()
130 return p.returncode
132 def report_results(self, retcode, destination):
133 """Report whether the export succeeded or otherwise."""
134 if retcode == 0:
135 note("Export to %s completed successfully." % (destination,))
136 else:
137 warning("Export to %s exited with error code %d."
138 % (destination, retcode))
140 def execute_exporter_script(self, args, outf):
141 """Execute an exporter script, capturing the output.
143 The script must be a Python script under the exporters directory.
145 :param args: list of arguments making up the script, the first of
146 which is the script name relative to the exporters directory.
147 :param outf: a file-like object for storing the output,
148 :return: the return code
150 # Note: currently assume Python is on the path. We could work around
151 # this later (for Windows users say) by packaging the scripts as Python
152 # modules and calling their internals directly.
153 exporters_dir = os.path.dirname(__file__)
154 script_abspath = os.path.join(exporters_dir, args[0])
155 actual_args = ['python', script_abspath] + args[1:]
156 return self.execute(actual_args, outf)
159 class CvsExporter(_Exporter):
161 def __init__(self):
162 self.check_install('cvs2svn', '2.30', ['cvs2bzr'])
163 self.check_install('CVS', '1.11', ['cvs'])
165 def generate(self, source, destination, verbose=False, custom=None):
166 """Generate a fast import stream. See _Exporter.generate() for details."""
167 # TODO: pass a custom cvs2bzr-default.options file as soon as
168 # cvs2bzr handles --options along with others.
169 args = ["cvs2bzr", "--dumpfile", destination]
170 outf, base, marks = self.get_output_info(destination)
171 # Marks aren't supported by cvs2bzr so no need to set that option
172 if custom:
173 args.extend(custom)
174 args.append(source)
175 retcode = self.execute(args, outf)
176 self.report_results(retcode, destination)
179 class DarcsExporter(_Exporter):
181 def __init__(self):
182 self.check_install('Darcs', '2.2', [('darcs', '--version')])
184 def generate(self, source, destination, verbose=False, custom=None):
185 """Generate a fast import stream. See _Exporter.generate() for details."""
186 args = ["darcs/darcs-fast-export"]
187 outf, base, marks = self.get_output_info(destination)
188 if marks:
189 args.append('--export-marks=%s' % marks)
190 if custom:
191 args.extend(custom)
192 args.append(source)
193 retcode = self.execute_exporter_script(args, outf)
194 self.report_results(retcode, destination)
197 class MercurialExporter(_Exporter):
199 def __init__(self):
200 self.check_install('Mercurial', '1.2', None, ['mercurial'])
202 def generate(self, source, destination, verbose=False, custom=None):
203 """Generate a fast import stream. See _Exporter.generate() for details."""
204 # XXX: Should we add --force here?
205 args = ["hg-fast-export.py", "-r", source, "-s"]
206 outf, base, marks = self.get_output_info(destination)
207 if base:
208 args.append('--marks=%s.marks' % (base,))
209 args.append('--mapping=%s.mapping' % (base,))
210 args.append('--heads=%s.heads' % (base,))
211 args.append('--status=%s.status' % (base,))
212 if custom:
213 args.extend(custom)
214 retcode = self.execute_exporter_script(args, outf)
215 self.report_results(retcode, destination)
218 class GitExporter(_Exporter):
220 def __init__(self):
221 self.cmd_name = "git"
222 if sys.platform == 'win32':
223 self.cmd_name = "git.cmd"
224 self.check_install('Git', '1.6', [self.cmd_name])
226 def generate(self, source, destination, verbose=False, custom=None):
227 """Generate a fast import stream. See _Exporter.generate() for details."""
228 args = [self.cmd_name, "fast-export", "--all", "--signed-tags=warn"]
229 outf, base, marks = self.get_output_info(destination)
230 if marks:
231 marks = os.path.abspath(marks)
232 # Note: we don't pass import-marks because that creates
233 # a stream of incremental changes, not the full thing.
234 # We may support incremental output later ...
235 #if os.path.exists(marks):
236 # args.append('--import-marks=%s' % marks)
237 args.append('--export-marks=%s' % marks)
238 if custom:
239 args.extend(custom)
240 retcode = self.execute(args, outf, cwd=source)
241 self.report_results(retcode, destination)
244 class MonotoneExporter(_Exporter):
246 def __init__(self):
247 self.check_install('Monotone', '0.43', ['mnt'])
249 def generate(self, source, destination, verbose=False, custom=None):
250 """Generate a fast import stream. See _Exporter.generate() for details."""
251 args = ["mnt", "git_export"]
252 outf, base, marks = self.get_output_info(destination)
253 if marks:
254 marks = os.path.abspath(marks)
255 if os.path.exists(marks):
256 args.append('--import-marks=%s' % marks)
257 args.append('--export-marks=%s' % marks)
258 if custom:
259 args.extend(custom)
260 retcode = self.execute(args, outf, cwd=source)
261 self.report_results(retcode, destination)
264 class PerforceExporter(_Exporter):
266 def __init__(self):
267 self.check_install('p4', '2009.1', ['p4'])
268 self.check_install('Perforce Python API', '2009.1', None, ['P4'])
269 self.check_install('bzrp4', '', None, ['bzrlib.plugins.bzrp4'])
271 def generate(self, source, destination, verbose=False, custom=None):
272 """Generate a fast import stream. See _Exporter.generate() for details."""
273 from bzrlib.plugins.bzrp4 import p4_fast_export
274 outf, base, marks = self.get_output_info(destination)
275 # Marks aren't supported by p4_fast_export so no need to set that
276 # option
277 original_stdout = sys.stdout
278 sys.stdout = outf
279 try:
280 retcode = p4_fast_export.main([source])
281 finally:
282 sys.stdout = original_stdout
283 self.report_results(retcode, destination)
286 class SubversionExporter(_Exporter):
288 def __init__(self):
289 self.check_install('Python Subversion', '1.4', None,
290 ['svn.fs', 'svn.core', 'svn.repos'])
292 def generate(self, source, destination, verbose=False, custom=None):
293 """Generate a fast import stream. See _Exporter.generate() for details."""
294 args = ["svn-fast-export.py"]
295 outf, base, marks = self.get_output_info(destination)
296 # Marks aren't supported by svn-fast-export so no need to set that option
297 if custom:
298 args.extend(custom)
299 args.append(source)
300 retcode = self.execute_exporter_script(args, outf)
301 self.report_results(retcode, destination)
304 def fast_export_from(source, destination, tool, verbose=False, custom=None):
305 # Get the exporter
306 if tool == 'cvs':
307 factory = CvsExporter
308 elif tool == 'darcs':
309 factory = DarcsExporter
310 elif tool == 'hg':
311 factory = MercurialExporter
312 elif tool == 'git':
313 factory = GitExporter
314 elif tool == 'mnt':
315 factory = MonotoneExporter
316 elif tool == 'p4':
317 factory = PerforceExporter
318 elif tool == 'svn':
319 factory = SubversionExporter
320 try:
321 exporter = factory()
322 except MissingDependency, ex:
323 raise errors.BzrError(ex.get_message())
325 # Do the export
326 exporter.generate(source, destination, verbose=verbose,
327 custom=custom)