chiark / gitweb /
8067beb11adfaf0ebec52aa5c097eac08b2e8f25
[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('--reject', action = 'store_true',
69         short = 'leave the rejected hunks in corresponding *.rej files'),
70     opt('-e', '--edit', action = 'store_true',
71         short = 'Invoke an editor for the patch description'),
72     opt('-p', '--showpatch', action = 'store_true',
73         short = 'Show the patch content in the editor buffer'),
74     opt('-a', '--author', metavar = '"NAME <EMAIL>"',
75         short = 'Use "NAME <EMAIL>" as the author details'),
76     opt('--authname',
77         short = 'Use AUTHNAME as the author name'),
78     opt('--authemail',
79         short = 'Use AUTHEMAIL as the author e-mail'),
80     opt('--authdate',
81         short = 'Use AUTHDATE as the author date'),
82     ] + argparse.sign_options()
83
84 directory = DirectoryHasRepository(log = True)
85
86 def __strip_patch_name(name):
87     stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name)
88     stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped)
89
90     return stripped
91
92 def __replace_slashes_with_dashes(name):
93     stripped = name.replace('/', '-')
94
95     return stripped
96
97 def __create_patch(filename, message, author_name, author_email,
98                    author_date, diff, options):
99     """Create a new patch on the stack
100     """
101     if options.name:
102         patch = options.name
103     elif filename:
104         patch = os.path.basename(filename)
105     else:
106         patch = ''
107     if options.strip:
108         patch = __strip_patch_name(patch)
109
110     if not patch:
111         if options.ignore or options.replace:
112             unacceptable_name = lambda name: False
113         else:
114             unacceptable_name = crt_series.patch_exists
115         patch = make_patch_name(message, unacceptable_name)
116     else:
117         # fix possible invalid characters in the patch name
118         patch = re.sub('[^\w.]+', '-', patch).strip('-')
119
120     if options.ignore and patch in crt_series.get_applied():
121         out.info('Ignoring already applied patch "%s"' % patch)
122         return
123     if options.replace and patch in crt_series.get_unapplied():
124         crt_series.delete_patch(patch, keep_log = True)
125
126     # refresh_patch() will invoke the editor in this case, with correct
127     # patch content
128     if not message:
129         can_edit = False
130
131     if options.author:
132         options.authname, options.authemail = name_email(options.author)
133
134     # override the automatically parsed settings
135     if options.authname:
136         author_name = options.authname
137     if options.authemail:
138         author_email = options.authemail
139     if options.authdate:
140         author_date = options.authdate
141
142     crt_series.new_patch(patch, message = message, can_edit = False,
143                          author_name = author_name,
144                          author_email = author_email,
145                          author_date = author_date)
146
147     if not diff:
148         out.warn('No diff found, creating empty patch')
149     else:
150         out.start('Importing patch "%s"' % patch)
151         if options.base:
152             base = git_id(crt_series, options.base)
153         else:
154             base = None
155         git.apply_patch(diff = diff, base = base, reject = options.reject)
156         crt_series.refresh_patch(edit = options.edit,
157                                  show_patch = options.showpatch,
158                                  sign_str = options.sign_str,
159                                  backup = False)
160         out.done()
161
162 def __mkpatchname(name, suffix):
163     if name.lower().endswith(suffix.lower()):
164         return name[:-len(suffix)]
165     return name
166
167 def __get_handle_and_name(filename):
168     """Return a file object and a patch name derived from filename
169     """
170     # see if it's a gzip'ed or bzip2'ed patch
171     import bz2, gzip
172     for copen, ext in [(gzip.open, '.gz'), (bz2.BZ2File, '.bz2')]:
173         try:
174             f = copen(filename)
175             f.read(1)
176             f.seek(0)
177             return (f, __mkpatchname(filename, ext))
178         except IOError, e:
179             pass
180
181     # plain old file...
182     return (open(filename), filename)
183
184 def __import_file(filename, options, patch = None):
185     """Import a patch from a file or standard input
186     """
187     pname = None
188     if filename:
189         (f, pname) = __get_handle_and_name(filename)
190     else:
191         f = sys.stdin
192
193     if patch:
194         pname = patch
195     elif not pname:
196         pname = filename
197
198     if options.mail:
199         try:
200             msg = email.message_from_file(f)
201         except Exception, ex:
202             raise CmdException, 'error parsing the e-mail file: %s' % str(ex)
203         message, author_name, author_email, author_date, diff = \
204                  parse_mail(msg)
205     else:
206         message, author_name, author_email, author_date, diff = \
207                  parse_patch(f.read(), contains_diff = True)
208
209     if filename:
210         f.close()
211
212     __create_patch(pname, message, author_name, author_email,
213                    author_date, diff, options)
214
215 def __import_series(filename, options):
216     """Import a series of patches
217     """
218     applied = crt_series.get_applied()
219
220     if filename:
221         if tarfile.is_tarfile(filename):
222             __import_tarfile(filename, options)
223             return
224         f = file(filename)
225         patchdir = os.path.dirname(filename)
226     else:
227         f = sys.stdin
228         patchdir = ''
229
230     for line in f:
231         patch = re.sub('#.*$', '', line).strip()
232         if not patch:
233             continue
234         patchfile = os.path.join(patchdir, patch)
235         patch = __replace_slashes_with_dashes(patch);
236
237         __import_file(patchfile, options, patch)
238
239     if filename:
240         f.close()
241
242 def __import_mbox(filename, options):
243     """Import a series from an mbox file
244     """
245     if filename:
246         f = file(filename, 'rb')
247     else:
248         f = StringIO(sys.stdin.read())
249
250     try:
251         mbox = UnixMailbox(f, email.message_from_file)
252     except Exception, ex:
253         raise CmdException, 'error parsing the mbox file: %s' % str(ex)
254
255     for msg in mbox:
256         message, author_name, author_email, author_date, diff = \
257                  parse_mail(msg)
258         __create_patch(None, message, author_name, author_email,
259                        author_date, diff, options)
260
261     f.close()
262
263 def __import_url(url, options):
264     """Import a patch from a URL
265     """
266     import urllib
267     import tempfile
268
269     if not url:
270         raise CmdException('URL argument required')
271
272     patch = os.path.basename(urllib.unquote(url))
273     filename = os.path.join(tempfile.gettempdir(), patch)
274     urllib.urlretrieve(url, filename)
275     __import_file(filename, options)
276
277 def __import_tarfile(tar, options):
278     """Import patch series from a tar archive
279     """
280     import tempfile
281     import shutil
282
283     if not tarfile.is_tarfile(tar):
284         raise CmdException, "%s is not a tarfile!" % tar
285
286     t = tarfile.open(tar, 'r')
287     names = t.getnames()
288
289     # verify paths in the tarfile are safe
290     for n in names:
291         if n.startswith('/'):
292             raise CmdException, "Absolute path found in %s" % tar
293         if n.find("..") > -1:
294             raise CmdException, "Relative path found in %s" % tar
295
296     # find the series file
297     seriesfile = '';
298     for m in names:
299         if m.endswith('/series') or m == 'series':
300             seriesfile = m
301             break
302     if seriesfile == '':
303         raise CmdException, "no 'series' file found in %s" % tar
304
305     # unpack into a tmp dir
306     tmpdir = tempfile.mkdtemp('.stg')
307     t.extractall(tmpdir)
308
309     # apply the series
310     __import_series(os.path.join(tmpdir, seriesfile), options)
311
312     # cleanup the tmpdir
313     shutil.rmtree(tmpdir)
314
315 def func(parser, options, args):
316     """Import a GNU diff file as a new patch
317     """
318     if len(args) > 1:
319         parser.error('incorrect number of arguments')
320
321     check_local_changes()
322     check_conflicts()
323     check_head_top_equal(crt_series)
324
325     if len(args) == 1:
326         filename = args[0]
327     else:
328         filename = None
329
330     if not options.url and filename:
331         filename = os.path.abspath(filename)
332     directory.cd_to_topdir()
333
334     if options.series:
335         __import_series(filename, options)
336     elif options.mbox:
337         __import_mbox(filename, options)
338     elif options.url:
339         __import_url(filename, options)
340     else:
341         __import_file(filename, options)
342
343     print_crt_patch(crt_series)