2 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
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.
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.
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
18 import sys, os, re, email
19 from email.Header import decode_header, make_header
20 from optparse import OptionParser, make_option
22 from stgit.commands.common import *
23 from stgit.utils import *
24 from stgit import stack, git
27 help = 'import a GNU diff file as a new patch'
28 usage = """%prog [options] [<file>]
30 Create a new patch and apply the given GNU diff file (or the standard
31 input). By default, the file name is used as the patch name but this
32 can be overridden with the '--name' option. The patch can either be a
33 normal file with the description at the top or it can have standard
34 mail format, the Subject, From and Date headers being used for
35 generating the patch information.
37 The patch description has to be separated from the data with a '---'
40 options = [make_option('-m', '--mail',
41 help = 'import the patch from a standard e-mail file',
42 action = 'store_true'),
43 make_option('-n', '--name',
44 help = 'use NAME as the patch name'),
45 make_option('-t', '--strip',
46 help = 'strip numbering and extension from patch name',
47 action = 'store_true'),
48 make_option('-s', '--series',
49 help = 'import a series of patches',
50 action = 'store_true'),
51 make_option('-i', '--ignore',
52 help = 'ignore the applied patches in the series',
53 action = 'store_true'),
54 make_option('--replace',
55 help = 'replace the unapplied patches in the series',
56 action = 'store_true'),
57 make_option('-b', '--base',
58 help = 'use BASE instead of HEAD for file importing'),
59 make_option('-e', '--edit',
60 help = 'invoke an editor for the patch description',
61 action = 'store_true'),
62 make_option('-p', '--showpatch',
63 help = 'show the patch content in the editor buffer',
64 action = 'store_true'),
65 make_option('-a', '--author', metavar = '"NAME <EMAIL>"',
66 help = 'use "NAME <EMAIL>" as the author details'),
67 make_option('--authname',
68 help = 'use AUTHNAME as the author name'),
69 make_option('--authemail',
70 help = 'use AUTHEMAIL as the author e-mail'),
71 make_option('--authdate',
72 help = 'use AUTHDATE as the author date'),
73 make_option('--commname',
74 help = 'use COMMNAME as the committer name'),
75 make_option('--commemail',
76 help = 'use COMMEMAIL as the committer e-mail')]
79 def __end_descr(line):
80 return re.match('---\s*$', line) or re.match('diff -', line) or \
81 re.match('Index: ', line)
83 def __strip_patch_name(name):
84 stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name)
85 stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped)
89 def __replace_slashes_with_dashes(name):
90 stripped = name.replace('/', '-')
94 def __parse_description(descr):
95 """Parse the patch description and return the new description and
96 author information (if any).
99 authname = authemail = authdate = None
101 descr_lines = [line.rstrip() for line in descr.split('\n')]
103 raise CmdException, "Empty patch description"
106 end = len(descr_lines)
108 # Parse the patch header
109 for pos in range(0, end):
110 if not descr_lines[pos]:
112 # check for a "From|Author:" line
113 if re.match('\s*(?:from|author):\s+', descr_lines[pos], re.I):
114 auth = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
115 authname, authemail = name_email(auth)
118 # check for a "Date:" line
119 if re.match('\s*date:\s+', descr_lines[pos], re.I):
120 authdate = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
126 subject = descr_lines[pos]
131 body = reduce(lambda x, y: x + '\n' + y, descr_lines[lasthdr:], '')
133 return (subject + body, authname, authemail, authdate)
135 def __parse_mail(filename = None):
136 """Parse the input file in a mail format and return (description,
137 authname, authemail, authdate)
139 def __decode_header(header):
140 """Decode a qp-encoded e-mail header as per rfc2047"""
142 words_enc = decode_header(header)
143 hobj = make_header(words_enc)
144 except Exception, ex:
145 raise CmdException, 'header decoding error: %s' % str(ex)
146 return unicode(hobj).encode('utf-8')
153 msg = email.message_from_file(f)
159 if msg.has_key('from'):
160 authname, authemail = name_email(__decode_header(msg['from']))
162 authname = authemail = None
164 descr = __decode_header(msg['subject'])
165 authdate = msg['date']
167 # remove the '[*PATCH*]' expression in the subject
169 descr = re.findall('^(\[[^\s]*[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$',
173 raise CmdException, 'Subject: line not found'
175 # the rest of the message
176 if msg.is_multipart():
177 descr += msg.get_payload(0, decode = True)
178 diff = msg.get_payload(1, decode = True)
180 diff = msg.get_payload(decode = True)
182 for line in diff.split('\n'):
183 if __end_descr(line):
189 # parse the description for author information
190 descr, descr_authname, descr_authemail, descr_authdate = \
191 __parse_description(descr)
193 authname = descr_authname
195 authemail = descr_authemail
197 authdate = descr_authdate
199 return (descr, authname, authemail, authdate, diff)
201 def __parse_patch(filename = None):
202 """Parse the input file and return (description, authname,
216 if __end_descr(line):
227 descr, authname, authemail, authdate = __parse_description(descr)
229 # we don't yet have an agreed place for the creation date.
231 return (descr, authname, authemail, authdate, diff)
233 def __import_patch(patch, filename, options):
234 """Import a patch from a file or standard input
237 message = author_name = author_email = author_date = committer_name = \
238 committer_email = None
241 options.authname, options.authemail = name_email(options.author)
244 message, author_name, author_email, author_date, diff = \
245 __parse_mail(filename)
247 message, author_name, author_email, author_date, diff = \
248 __parse_patch(filename)
251 raise CmdException, 'No diff found inside the patch'
254 patch = make_patch_name(message, crt_series.patch_exists)
256 # refresh_patch() will invoke the editor in this case, with correct
261 # override the automatically parsed settings
263 author_name = options.authname
264 if options.authemail:
265 author_email = options.authemail
267 author_date = options.authdate
269 committer_name = options.commname
270 if options.commemail:
271 committer_email = options.commemail
273 if options.replace and patch in crt_series.get_unapplied():
274 crt_series.delete_patch(patch)
276 crt_series.new_patch(patch, message = message, can_edit = False,
277 author_name = author_name,
278 author_email = author_email,
279 author_date = author_date,
280 committer_name = committer_name,
281 committer_email = committer_email)
283 print 'Importing patch "%s"...' % patch,
287 git.apply_patch(diff = diff, base = git_id(options.base))
289 git.apply_patch(diff = diff)
291 crt_series.refresh_patch(edit = options.edit,
292 show_patch = options.showpatch)
296 def __import_series(filename, options):
297 """Import a series of patches
299 applied = crt_series.get_applied()
303 patchdir = os.path.dirname(filename)
309 patch = re.sub('#.*$', '', line).strip()
312 patchfile = os.path.join(patchdir, patch)
315 patch = __strip_patch_name(patch)
316 patch = __replace_slashes_with_dashes(patch);
317 if options.ignore and patch in applied:
318 print 'Ignoring already applied patch "%s"' % patch
321 __import_patch(patch, patchfile, options)
323 def func(parser, options, args):
324 """Import a GNU diff file as a new patch
327 parser.error('incorrect number of arguments')
329 check_local_changes()
331 check_head_top_equal()
339 __import_series(filename, options)
344 patch = os.path.basename(filename)
348 patch = __strip_patch_name(patch)
350 __import_patch(patch, filename, options)