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