Commit | Line | Data |
---|---|---|
0d2cd1e4 CM |
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 | ||
9417ece4 | 18 | import sys, os, re |
2ac5a14c | 19 | from email.Header import decode_header, make_header |
0d2cd1e4 CM |
20 | from optparse import OptionParser, make_option |
21 | ||
22 | from stgit.commands.common import * | |
23 | from stgit.utils import * | |
24 | from stgit import stack, git | |
25 | ||
26 | ||
27 | help = 'import a GNU diff file as a new patch' | |
b8a0986f | 28 | usage = """%prog [options] [<file>] |
0d2cd1e4 | 29 | |
b8a0986f CM |
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 | |
388f63b6 | 32 | can be overridden with the '--name' option. The patch can either be a |
b8a0986f CM |
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. | |
0d2cd1e4 | 36 | |
b8a0986f | 37 | The patch description has to be separated from the data with a '---' |
99e73103 | 38 | line.""" |
0d2cd1e4 CM |
39 | |
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'), | |
b0cdad5e CM |
45 | make_option('-t', '--strip', |
46 | help = 'strip numbering and extension from patch name', | |
47 | action = 'store_true'), | |
9417ece4 CM |
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'), | |
034db15c CM |
54 | make_option('--replace', |
55 | help = 'replace the unapplied patches in the series', | |
56 | action = 'store_true'), | |
b21bc8d1 | 57 | make_option('-b', '--base', |
35344f86 | 58 | help = 'use BASE instead of HEAD for file importing'), |
33e580e0 CM |
59 | make_option('-e', '--edit', |
60 | help = 'invoke an editor for the patch description', | |
61 | action = 'store_true'), | |
9417ece4 | 62 | make_option('-p', '--showpatch', |
6ad48e48 PBG |
63 | help = 'show the patch content in the editor buffer', |
64 | action = 'store_true'), | |
0d2cd1e4 CM |
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')] | |
77 | ||
78 | ||
d4c43e19 PBG |
79 | def __end_descr(line): |
80 | return re.match('---\s*$', line) or re.match('diff -', line) or \ | |
81 | re.match('Index: ', line) | |
99e73103 | 82 | |
b0cdad5e | 83 | def __strip_patch_name(name): |
bcb6d890 CM |
84 | stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name) |
85 | stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped) | |
86 | ||
87 | return stripped | |
b0cdad5e | 88 | |
613a2f16 PBG |
89 | def __replace_slashes_with_dashes(name): |
90 | stripped = name.replace('/', '-') | |
91 | ||
92 | return stripped | |
93 | ||
99e73103 CM |
94 | def __parse_description(descr): |
95 | """Parse the patch description and return the new description and | |
96 | author information (if any). | |
97 | """ | |
98 | subject = body = '' | |
0543bc5f | 99 | authname = authemail = authdate = None |
99e73103 | 100 | |
0543bc5f | 101 | descr_lines = [line.rstrip() for line in descr.split('\n')] |
99e73103 CM |
102 | if not descr_lines: |
103 | raise CmdException, "Empty patch description" | |
104 | ||
0543bc5f | 105 | lasthdr = 0 |
99e73103 CM |
106 | end = len(descr_lines) |
107 | ||
0543bc5f | 108 | # Parse the patch header |
61dabd0e | 109 | for pos in range(0, end): |
0543bc5f TM |
110 | if not descr_lines[pos]: |
111 | continue | |
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) | |
116 | lasthdr = pos + 1 | |
117 | continue | |
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] | |
121 | lasthdr = pos + 1 | |
122 | continue | |
123 | if subject: | |
124 | break | |
125 | # get the subject | |
126 | subject = descr_lines[pos] | |
127 | lasthdr = pos + 1 | |
99e73103 CM |
128 | |
129 | # get the body | |
0543bc5f TM |
130 | if lasthdr < end: |
131 | body = reduce(lambda x, y: x + '\n' + y, descr_lines[lasthdr:], '') | |
99e73103 | 132 | |
0543bc5f | 133 | return (subject + body, authname, authemail, authdate) |
99e73103 | 134 | |
0d2cd1e4 CM |
135 | def __parse_mail(filename = None): |
136 | """Parse the input file in a mail format and return (description, | |
137 | authname, authemail, authdate) | |
138 | """ | |
2ac5a14c CM |
139 | def __decode_header(header): |
140 | """Decode a qp-encoded e-mail header as per rfc2047""" | |
141 | try: | |
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') | |
147 | ||
0d2cd1e4 CM |
148 | if filename: |
149 | f = file(filename) | |
150 | else: | |
151 | f = sys.stdin | |
152 | ||
153 | descr = authname = authemail = authdate = None | |
154 | ||
155 | # parse the headers | |
6fe6b1bd CM |
156 | while True: |
157 | line = f.readline() | |
158 | if not line: | |
159 | break | |
0d2cd1e4 CM |
160 | line = line.strip() |
161 | if re.match('from:\s+', line, re.I): | |
2ac5a14c | 162 | auth = __decode_header(re.findall('^.*?:\s+(.*)$', line)[0]) |
0d2cd1e4 CM |
163 | authname, authemail = name_email(auth) |
164 | elif re.match('date:\s+', line, re.I): | |
165 | authdate = re.findall('^.*?:\s+(.*)$', line)[0] | |
166 | elif re.match('subject:\s+', line, re.I): | |
2ac5a14c | 167 | descr = __decode_header(re.findall('^.*?:\s+(.*)$', line)[0]) |
0d2cd1e4 CM |
168 | elif line == '': |
169 | # end of headers | |
170 | break | |
171 | ||
186e6b6b | 172 | # remove the '[*PATCH*]' expression in the subject |
0d2cd1e4 | 173 | if descr: |
7c02f338 CM |
174 | descr = re.findall('^(\[[^\s]*[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$', |
175 | descr)[0][1] | |
0d2cd1e4 CM |
176 | descr += '\n\n' |
177 | else: | |
178 | raise CmdException, 'Subject: line not found' | |
179 | ||
180 | # the rest of the patch description | |
6fe6b1bd CM |
181 | while True: |
182 | line = f.readline() | |
183 | if not line: | |
184 | break | |
d4c43e19 | 185 | if __end_descr(line): |
0d2cd1e4 CM |
186 | break |
187 | else: | |
188 | descr += line | |
189 | descr.rstrip() | |
190 | ||
191 | if filename: | |
192 | f.close() | |
193 | ||
99e73103 | 194 | # parse the description for author information |
0543bc5f | 195 | descr, descr_authname, descr_authemail, descr_authdate = __parse_description(descr) |
99e73103 CM |
196 | if descr_authname: |
197 | authname = descr_authname | |
198 | if descr_authemail: | |
199 | authemail = descr_authemail | |
0543bc5f TM |
200 | if descr_authdate: |
201 | authdate = descr_authdate | |
99e73103 | 202 | |
0d2cd1e4 CM |
203 | return (descr, authname, authemail, authdate) |
204 | ||
205 | def __parse_patch(filename = None): | |
206 | """Parse the input file and return (description, authname, | |
207 | authemail, authdate) | |
208 | """ | |
209 | if filename: | |
210 | f = file(filename) | |
211 | else: | |
212 | f = sys.stdin | |
213 | ||
0d2cd1e4 | 214 | descr = '' |
6fe6b1bd CM |
215 | while True: |
216 | line = f.readline() | |
217 | if not line: | |
218 | break | |
219 | ||
d4c43e19 | 220 | if __end_descr(line): |
0d2cd1e4 CM |
221 | break |
222 | else: | |
223 | descr += line | |
224 | descr.rstrip() | |
225 | ||
0d2cd1e4 CM |
226 | if filename: |
227 | f.close() | |
228 | ||
0543bc5f | 229 | descr, authname, authemail, authdate = __parse_description(descr) |
99e73103 CM |
230 | |
231 | # we don't yet have an agreed place for the creation date. | |
232 | # Just return None | |
0543bc5f | 233 | return (descr, authname, authemail, authdate) |
0d2cd1e4 | 234 | |
9417ece4 CM |
235 | def __import_patch(patch, filename, options): |
236 | """Import a patch from a file or standard input | |
0d2cd1e4 | 237 | """ |
0d2cd1e4 CM |
238 | # the defaults |
239 | message = author_name = author_email = author_date = committer_name = \ | |
240 | committer_email = None | |
241 | ||
242 | if options.author: | |
243 | options.authname, options.authemail = name_email(options.author) | |
244 | ||
245 | if options.mail: | |
246 | message, author_name, author_email, author_date = \ | |
247 | __parse_mail(filename) | |
248 | else: | |
249 | message, author_name, author_email, author_date = \ | |
250 | __parse_patch(filename) | |
251 | ||
fff9bce5 | 252 | if not patch: |
b839b1cf | 253 | patch = make_patch_name(message, crt_series.patch_exists) |
fff9bce5 | 254 | |
95742cfc PBG |
255 | # refresh_patch() will invoke the editor in this case, with correct |
256 | # patch content | |
9d15ccd8 | 257 | if not message: |
95742cfc | 258 | can_edit = False |
9d15ccd8 | 259 | |
0d2cd1e4 CM |
260 | # override the automatically parsed settings |
261 | if options.authname: | |
262 | author_name = options.authname | |
263 | if options.authemail: | |
264 | author_email = options.authemail | |
265 | if options.authdate: | |
266 | author_date = options.authdate | |
267 | if options.commname: | |
268 | committer_name = options.commname | |
269 | if options.commemail: | |
270 | committer_email = options.commemail | |
271 | ||
034db15c CM |
272 | if options.replace and patch in crt_series.get_unapplied(): |
273 | crt_series.delete_patch(patch) | |
274 | ||
95742cfc | 275 | crt_series.new_patch(patch, message = message, can_edit = False, |
0d2cd1e4 CM |
276 | author_name = author_name, |
277 | author_email = author_email, | |
278 | author_date = author_date, | |
279 | committer_name = committer_name, | |
280 | committer_email = committer_email) | |
281 | ||
9417ece4 | 282 | print 'Importing patch "%s"...' % patch, |
0d2cd1e4 CM |
283 | sys.stdout.flush() |
284 | ||
35344f86 | 285 | if options.base: |
be3e6bd9 | 286 | git.apply_patch(filename, git_id(options.base)) |
35344f86 CM |
287 | else: |
288 | git.apply_patch(filename) | |
289 | ||
6ad48e48 PBG |
290 | crt_series.refresh_patch(edit = options.edit, |
291 | show_patch = options.showpatch) | |
0d2cd1e4 CM |
292 | |
293 | print 'done' | |
9417ece4 CM |
294 | |
295 | def __import_series(filename, options): | |
296 | """Import a series of patches | |
297 | """ | |
298 | applied = crt_series.get_applied() | |
299 | ||
300 | if filename: | |
301 | f = file(filename) | |
302 | patchdir = os.path.dirname(filename) | |
303 | else: | |
304 | f = sys.stdin | |
305 | patchdir = '' | |
306 | ||
307 | for line in f: | |
308 | patch = re.sub('#.*$', '', line).strip() | |
309 | if not patch: | |
310 | continue | |
bcb6d890 CM |
311 | patchfile = os.path.join(patchdir, patch) |
312 | ||
b0cdad5e CM |
313 | if options.strip: |
314 | patch = __strip_patch_name(patch) | |
613a2f16 | 315 | patch = __replace_slashes_with_dashes(patch); |
9417ece4 CM |
316 | if options.ignore and patch in applied: |
317 | print 'Ignoring already applied patch "%s"' % patch | |
318 | continue | |
319 | ||
9417ece4 CM |
320 | __import_patch(patch, patchfile, options) |
321 | ||
322 | def func(parser, options, args): | |
323 | """Import a GNU diff file as a new patch | |
324 | """ | |
325 | if len(args) > 1: | |
326 | parser.error('incorrect number of arguments') | |
327 | ||
328 | check_local_changes() | |
329 | check_conflicts() | |
330 | check_head_top_equal() | |
331 | ||
332 | if len(args) == 1: | |
333 | filename = args[0] | |
334 | else: | |
335 | filename = None | |
336 | ||
337 | if options.series: | |
338 | __import_series(filename, options) | |
339 | else: | |
340 | if options.name: | |
341 | patch = options.name | |
342 | elif filename: | |
343 | patch = os.path.basename(filename) | |
344 | else: | |
fff9bce5 | 345 | patch = '' |
b0cdad5e CM |
346 | if options.strip: |
347 | patch = __strip_patch_name(patch) | |
9417ece4 CM |
348 | |
349 | __import_patch(patch, filename, options) | |
350 | ||
0d2cd1e4 | 351 | print_crt_patch() |