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