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