chiark / gitweb /
ad61b88412cbd4a61f564fc251b608dea63c9a1b
[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
19 from optparse import OptionParser, make_option
20
21 from stgit.commands.common import *
22 from stgit.utils import *
23 from stgit import stack, git
24
25
26 help = 'import a GNU diff file as a new patch'
27 usage = """%prog [options] [<file>]
28
29 Create a new patch and apply the given GNU diff file (or the standard
30 input). By default, the file name is used as the patch name but this
31 can be overridden with the '--name' option. The patch can either be a
32 normal file with the description at the top or it can have standard
33 mail format, the Subject, From and Date headers being used for
34 generating the patch information.
35
36 The patch description has to be separated from the data with a '---'
37 line."""
38
39 options = [make_option('-m', '--mail',
40                        help = 'import the patch from a standard e-mail file',
41                        action = 'store_true'),
42            make_option('-n', '--name',
43                        help = 'use NAME as the patch name'),
44            make_option('-s', '--series',
45                        help = 'import a series of patches',
46                        action = 'store_true'),
47            make_option('-i', '--ignore',
48                        help = 'ignore the applied patches in the series',
49                        action = 'store_true'),
50            make_option('-b', '--base',
51                        help = 'use BASE instead of HEAD for file importing'),
52            make_option('-e', '--edit',
53                        help = 'invoke an editor for the patch description',
54                        action = 'store_true'),
55            make_option('-p', '--showpatch',
56                        help = 'show the patch content in the editor buffer',
57                        action = 'store_true'),
58            make_option('-a', '--author', metavar = '"NAME <EMAIL>"',
59                        help = 'use "NAME <EMAIL>" as the author details'),
60            make_option('--authname',
61                        help = 'use AUTHNAME as the author name'),
62            make_option('--authemail',
63                        help = 'use AUTHEMAIL as the author e-mail'),
64            make_option('--authdate',
65                        help = 'use AUTHDATE as the author date'),
66            make_option('--commname',
67                        help = 'use COMMNAME as the committer name'),
68            make_option('--commemail',
69                        help = 'use COMMEMAIL as the committer e-mail')]
70
71
72 def __end_descr(line):
73     return re.match('---\s*$', line) or re.match('diff -', line) or \
74             re.match('Index: ', line)
75
76 def __parse_description(descr):
77     """Parse the patch description and return the new description and
78     author information (if any).
79     """
80     subject = body = ''
81     authname = authemail = authdate = None
82
83     descr_lines = [line.rstrip() for line in  descr.split('\n')]
84     if not descr_lines:
85         raise CmdException, "Empty patch description"
86
87     lasthdr = 0
88     end = len(descr_lines)
89
90     # Parse the patch header
91     for pos in range(0, end):
92         if not descr_lines[pos]:
93            continue
94         # check for a "From|Author:" line
95         if re.match('\s*(?:from|author):\s+', descr_lines[pos], re.I):
96             auth = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
97             authname, authemail = name_email(auth)
98             lasthdr = pos + 1
99             continue
100         # check for a "Date:" line
101         if re.match('\s*date:\s+', descr_lines[pos], re.I):
102             authdate = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
103             lasthdr = pos + 1
104             continue
105         if subject:
106             break
107         # get the subject
108         subject = descr_lines[pos]
109         lasthdr = pos + 1
110
111     # get the body
112     if lasthdr < end:
113         body = reduce(lambda x, y: x + '\n' + y, descr_lines[lasthdr:], '')
114
115     return (subject + body, authname, authemail, authdate)
116
117 def __parse_mail(filename = None):
118     """Parse the input file in a mail format and return (description,
119     authname, authemail, authdate)
120     """
121     if filename:
122         f = file(filename)
123     else:
124         f = sys.stdin
125
126     descr = authname = authemail = authdate = None
127
128     # parse the headers
129     while True:
130         line = f.readline()
131         if not line:
132             break
133         line = line.strip()
134         if re.match('from:\s+', line, re.I):
135             auth = re.findall('^.*?:\s+(.*)$', line)[0]
136             authname, authemail = name_email(auth)
137         elif re.match('date:\s+', line, re.I):
138             authdate = re.findall('^.*?:\s+(.*)$', line)[0]
139         elif re.match('subject:\s+', line, re.I):
140             descr = re.findall('^.*?:\s+(.*)$', line)[0]
141         elif line == '':
142             # end of headers
143             break
144
145     # remove the '[*PATCH*]' expression in the subject
146     if descr:
147         descr = re.findall('^(\[[^\s]*[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$',
148                            descr)[0][1]
149         descr += '\n\n'
150     else:
151         raise CmdException, 'Subject: line not found'
152
153     # the rest of the patch description
154     while True:
155         line = f.readline()
156         if not line:
157             break
158         if __end_descr(line):
159             break
160         else:
161             descr += line
162     descr.rstrip()
163
164     if filename:
165         f.close()
166
167     # parse the description for author information
168     descr, descr_authname, descr_authemail, descr_authdate = __parse_description(descr)
169     if descr_authname:
170         authname = descr_authname
171     if descr_authemail:
172         authemail = descr_authemail
173     if descr_authdate:
174        authdate = descr_authdate
175
176     return (descr, authname, authemail, authdate)
177
178 def __parse_patch(filename = None):
179     """Parse the input file and return (description, authname,
180     authemail, authdate)
181     """
182     if filename:
183         f = file(filename)
184     else:
185         f = sys.stdin
186
187     descr = ''
188     while True:
189         line = f.readline()
190         if not line:
191             break
192
193         if __end_descr(line):
194             break
195         else:
196             descr += line
197     descr.rstrip()
198
199     if filename:
200         f.close()
201
202     descr, authname, authemail, authdate = __parse_description(descr)
203
204     # we don't yet have an agreed place for the creation date.
205     # Just return None
206     return (descr, authname, authemail, authdate)
207
208 def __import_patch(patch, filename, options):
209     """Import a patch from a file or standard input
210     """
211     # the defaults
212     message = author_name = author_email = author_date = committer_name = \
213               committer_email = None
214
215     if options.author:
216         options.authname, options.authemail = name_email(options.author)
217
218     if options.mail:
219         message, author_name, author_email, author_date = \
220                  __parse_mail(filename)
221     else:
222         message, author_name, author_email, author_date = \
223                  __parse_patch(filename)
224
225     # refresh_patch() will invoke the editor in this case, with correct
226     # patch content
227     if not message:
228         can_edit = False
229
230     # override the automatically parsed settings
231     if options.authname:
232         author_name = options.authname
233     if options.authemail:
234         author_email = options.authemail
235     if options.authdate:
236         author_date = options.authdate
237     if options.commname:
238         committer_name = options.commname
239     if options.commemail:
240         committer_email = options.commemail
241
242     crt_series.new_patch(patch, message = message, can_edit = False,
243                          author_name = author_name,
244                          author_email = author_email,
245                          author_date = author_date,
246                          committer_name = committer_name,
247                          committer_email = committer_email)
248
249     print 'Importing patch "%s"...' % patch,
250     sys.stdout.flush()
251
252     if options.base:
253         git.apply_patch(filename, git_id(options.base))
254     else:
255         git.apply_patch(filename)
256
257     crt_series.refresh_patch(edit = options.edit,
258                              show_patch = options.showpatch)
259
260     print 'done'
261
262 def __import_series(filename, options):
263     """Import a series of patches
264     """
265     applied = crt_series.get_applied()
266
267     if filename:
268         f = file(filename)
269         patchdir = os.path.dirname(filename)
270     else:
271         f = sys.stdin
272         patchdir = ''
273
274     for line in f:
275         patch = re.sub('#.*$', '', line).strip()
276         if not patch:
277             continue
278         if options.ignore and patch in applied:
279             print 'Ignoring already applied patch "%s"' % patch
280             continue
281
282         patchfile = os.path.join(patchdir, patch)
283         __import_patch(patch, patchfile, options)
284
285 def func(parser, options, args):
286     """Import a GNU diff file as a new patch
287     """
288     if len(args) > 1:
289         parser.error('incorrect number of arguments')
290
291     check_local_changes()
292     check_conflicts()
293     check_head_top_equal()
294
295     if len(args) == 1:
296         filename = args[0]
297     else:
298         filename = None
299
300     if options.series:
301         __import_series(filename, options)
302     else:
303         if options.name:
304             patch = options.name
305         elif filename:
306             patch = os.path.basename(filename)
307         else:
308             raise CmdException, 'Unknown patch name'
309
310         __import_patch(patch, filename, options)
311
312     print_crt_patch()