chiark / gitweb /
97a6a09d78d55bad5a22867602efc00055427c4a
[stgit] / stgit / commands / imprt.py
1 __copyright__ = """
2 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License version 2 as
6 published by the Free Software Foundation.
7
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.
12
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
16 """
17
18 import sys, os, re, email, tarfile
19 from mailbox import UnixMailbox
20 from StringIO import StringIO
21 from stgit.argparse import opt
22 from stgit.commands.common import *
23 from stgit.utils import *
24 from stgit.out import *
25 from stgit import argparse, stack, git
26
27 name = 'import'
28 help = 'Import a GNU diff file as a new patch'
29 kind = 'patch'
30 usage = ['[options] [<file>|<url>]']
31 description = """
32 Create a new patch and apply the given GNU diff file (or the standard
33 input). By default, the file name is used as the patch name but this
34 can be overridden with the '--name' option. The patch can either be a
35 normal file with the description at the top or it can have standard
36 mail format, the Subject, From and Date headers being used for
37 generating the patch information. The command can also read series and
38 mbox files.
39
40 If a patch does not apply cleanly, the failed diff is written to the
41 .stgit-failed.patch file and an empty StGIT patch is added to the
42 stack.
43
44 The patch description has to be separated from the data with a '---'
45 line."""
46
47 args = [argparse.files]
48 options = [
49     opt('-m', '--mail', action = 'store_true',
50         short = 'Import the patch from a standard e-mail file'),
51     opt('-M', '--mbox', action = 'store_true',
52         short = 'Import a series of patches from an mbox file'),
53     opt('-s', '--series', action = 'store_true',
54         short = 'Import a series of patches', long = """
55         Import a series of patches from a series file or a tar archive."""),
56     opt('-u', '--url', action = 'store_true',
57         short = 'Import a patch from a URL'),
58     opt('-n', '--name',
59         short = 'Use NAME as the patch name'),
60     opt('-t', '--strip', action = 'store_true',
61         short = 'Strip numbering and extension from patch name'),
62     opt('-i', '--ignore', action = 'store_true',
63         short = 'Ignore the applied patches in the series'),
64     opt('--replace', action = 'store_true',
65         short = 'Replace the unapplied patches in the series'),
66     opt('-b', '--base', args = [argparse.commit],
67         short = 'Use BASE instead of HEAD for file importing'),
68     opt('-e', '--edit', action = 'store_true',
69         short = 'Invoke an editor for the patch description'),
70     opt('-p', '--showpatch', action = 'store_true',
71         short = 'Show the patch content in the editor buffer'),
72     opt('-a', '--author', metavar = '"NAME <EMAIL>"',
73         short = 'Use "NAME <EMAIL>" as the author details'),
74     opt('--authname',
75         short = 'Use AUTHNAME as the author name'),
76     opt('--authemail',
77         short = 'Use AUTHEMAIL as the author e-mail'),
78     opt('--authdate',
79         short = 'Use AUTHDATE as the author date'),
80     ] + argparse.sign_options()
81
82 directory = DirectoryHasRepository(log = True)
83
84 def __strip_patch_name(name):
85     stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name)
86     stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped)
87
88     return stripped
89
90 def __replace_slashes_with_dashes(name):
91     stripped = name.replace('/', '-')
92
93     return stripped
94
95 def __create_patch(filename, message, author_name, author_email,
96                    author_date, diff, options):
97     """Create a new patch on the stack
98     """
99     if options.name:
100         patch = options.name
101     elif filename:
102         patch = os.path.basename(filename)
103     else:
104         patch = ''
105     if options.strip:
106         patch = __strip_patch_name(patch)
107
108     if not patch:
109         if options.ignore or options.replace:
110             unacceptable_name = lambda name: False
111         else:
112             unacceptable_name = crt_series.patch_exists
113         patch = make_patch_name(message, unacceptable_name)
114     else:
115         # fix possible invalid characters in the patch name
116         patch = re.sub('[^\w.]+', '-', patch).strip('-')
117
118     if options.ignore and patch in crt_series.get_applied():
119         out.info('Ignoring already applied patch "%s"' % patch)
120         return
121     if options.replace and patch in crt_series.get_unapplied():
122         crt_series.delete_patch(patch, keep_log = True)
123
124     # refresh_patch() will invoke the editor in this case, with correct
125     # patch content
126     if not message:
127         can_edit = False
128
129     if options.author:
130         options.authname, options.authemail = name_email(options.author)
131
132     # override the automatically parsed settings
133     if options.authname:
134         author_name = options.authname
135     if options.authemail:
136         author_email = options.authemail
137     if options.authdate:
138         author_date = options.authdate
139
140     crt_series.new_patch(patch, message = message, can_edit = False,
141                          author_name = author_name,
142                          author_email = author_email,
143                          author_date = author_date)
144
145     if not diff:
146         out.warn('No diff found, creating empty patch')
147     else:
148         out.start('Importing patch "%s"' % patch)
149         if options.base:
150             git.apply_patch(diff = diff,
151                             base = git_id(crt_series, options.base))
152         else:
153             git.apply_patch(diff = diff)
154         crt_series.refresh_patch(edit = options.edit,
155                                  show_patch = options.showpatch,
156                                  sign_str = options.sign_str,
157                                  backup = False)
158         out.done()
159
160 def __mkpatchname(name, suffix):
161     if name.lower().endswith(suffix.lower()):
162         return name[:-len(suffix)]
163     return name
164
165 def __get_handle_and_name(filename):
166     """Return a file object and a patch name derived from filename
167     """
168     # see if it's a gzip'ed or bzip2'ed patch
169     import bz2, gzip
170     for copen, ext in [(gzip.open, '.gz'), (bz2.BZ2File, '.bz2')]:
171         try:
172             f = copen(filename)
173             f.read(1)
174             f.seek(0)
175             return (f, __mkpatchname(filename, ext))
176         except IOError, e:
177             pass
178
179     # plain old file...
180     return (open(filename), filename)
181
182 def __import_file(filename, options, patch = None):
183     """Import a patch from a file or standard input
184     """
185     pname = None
186     if filename:
187         (f, pname) = __get_handle_and_name(filename)
188     else:
189         f = sys.stdin
190
191     if patch:
192         pname = patch
193     elif not pname:
194         pname = filename
195
196     if options.mail:
197         try:
198             msg = email.message_from_file(f)
199         except Exception, ex:
200             raise CmdException, 'error parsing the e-mail file: %s' % str(ex)
201         message, author_name, author_email, author_date, diff = \
202                  parse_mail(msg)
203     else:
204         message, author_name, author_email, author_date, diff = \
205                  parse_patch(f.read(), contains_diff = True)
206
207     if filename:
208         f.close()
209
210     __create_patch(pname, message, author_name, author_email,
211                    author_date, diff, options)
212
213 def __import_series(filename, options):
214     """Import a series of patches
215     """
216     applied = crt_series.get_applied()
217
218     if filename:
219         if tarfile.is_tarfile(filename):
220             __import_tarfile(filename, options)
221             return
222         f = file(filename)
223         patchdir = os.path.dirname(filename)
224     else:
225         f = sys.stdin
226         patchdir = ''
227
228     for line in f:
229         patch = re.sub('#.*$', '', line).strip()
230         if not patch:
231             continue
232         patchfile = os.path.join(patchdir, patch)
233         patch = __replace_slashes_with_dashes(patch);
234
235         __import_file(patchfile, options, patch)
236
237     if filename:
238         f.close()
239
240 def __import_mbox(filename, options):
241     """Import a series from an mbox file
242     """
243     if filename:
244         f = file(filename, 'rb')
245     else:
246         f = StringIO(sys.stdin.read())
247
248     try:
249         mbox = UnixMailbox(f, email.message_from_file)
250     except Exception, ex:
251         raise CmdException, 'error parsing the mbox file: %s' % str(ex)
252
253     for msg in mbox:
254         message, author_name, author_email, author_date, diff = \
255                  parse_mail(msg)
256         __create_patch(None, message, author_name, author_email,
257                        author_date, diff, options)
258
259     f.close()
260
261 def __import_url(url, options):
262     """Import a patch from a URL
263     """
264     import urllib
265     import tempfile
266
267     if not url:
268         parser.error('URL argument required')
269
270     patch = os.path.basename(urllib.unquote(url))
271     filename = os.path.join(tempfile.gettempdir(), patch)
272     urllib.urlretrieve(url, filename)
273     __import_file(filename, options)
274
275 def __import_tarfile(tar, options):
276     """Import patch series from a tar archive
277     """
278     import tempfile
279     import shutil
280
281     if not tarfile.is_tarfile(tar):
282         raise CmdException, "%s is not a tarfile!" % tar
283
284     t = tarfile.open(tar, 'r')
285     names = t.getnames()
286
287     # verify paths in the tarfile are safe
288     for n in names:
289         if n.startswith('/'):
290             raise CmdException, "Absolute path found in %s" % tar
291         if n.find("..") > -1:
292             raise CmdException, "Relative path found in %s" % tar
293
294     # find the series file
295     seriesfile = '';
296     for m in names:
297         if m.endswith('/series') or m == 'series':
298             seriesfile = m
299             break
300     if seriesfile == '':
301         raise CmdException, "no 'series' file found in %s" % tar
302
303     # unpack into a tmp dir
304     tmpdir = tempfile.mkdtemp('.stg')
305     t.extractall(tmpdir)
306
307     # apply the series
308     __import_series(os.path.join(tmpdir, seriesfile), options)
309
310     # cleanup the tmpdir
311     shutil.rmtree(tmpdir)
312
313 def func(parser, options, args):
314     """Import a GNU diff file as a new patch
315     """
316     if len(args) > 1:
317         parser.error('incorrect number of arguments')
318
319     check_local_changes()
320     check_conflicts()
321     check_head_top_equal(crt_series)
322
323     if len(args) == 1:
324         filename = args[0]
325     else:
326         filename = None
327
328     if filename:
329         filename = os.path.abspath(filename)
330     directory.cd_to_topdir()
331
332     if options.series:
333         __import_series(filename, options)
334     elif options.mbox:
335         __import_mbox(filename, options)
336     elif options.url:
337         __import_url(filename, options)
338     else:
339         __import_file(filename, options)
340
341     print_crt_patch(crt_series)