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 | ||
972de1db | 18 | import sys, os, re, email, tarfile |
99c52915 | 19 | from mailbox import UnixMailbox |
457c3093 | 20 | from StringIO import StringIO |
575bbdae | 21 | from stgit.argparse import opt |
0d2cd1e4 CM |
22 | from stgit.commands.common import * |
23 | from stgit.utils import * | |
5e888f30 | 24 | from stgit.out import * |
20a52e06 | 25 | from stgit import argparse, stack, git |
0d2cd1e4 | 26 | |
575bbdae KH |
27 | name = 'import' |
28 | help = 'Import a GNU diff file as a new patch' | |
33ff9cdd | 29 | kind = 'patch' |
575bbdae KH |
30 | usage = ['[options] [<file>|<url>]'] |
31 | description = """ | |
b8a0986f CM |
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 | |
388f63b6 | 34 | can be overridden with the '--name' option. The patch can either be a |
b8a0986f CM |
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 | |
99c52915 CM |
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. | |
0d2cd1e4 | 43 | |
b8a0986f | 44 | The patch description has to be separated from the data with a '---' |
99e73103 | 45 | line.""" |
0d2cd1e4 | 46 | |
6c8a90e1 | 47 | args = [argparse.files] |
575bbdae KH |
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', | |
972de1db CW |
54 | short = 'Import a series of patches', long = """ |
55 | Import a series of patches from a series file or a tar archive."""), | |
575bbdae KH |
56 | opt('-u', '--url', action = 'store_true', |
57 | short = 'Import a patch from a URL'), | |
58 | opt('-n', '--name', | |
59 | short = 'Use NAME as the patch name'), | |
c18842cc CM |
60 | opt('-p', '--strip', type = 'int', metavar = 'N', |
61 | short = 'Remove N leading slashes from diff paths (default 1)'), | |
62 | opt('-t', '--stripname', action = 'store_true', | |
575bbdae KH |
63 | short = 'Strip numbering and extension from patch name'), |
64 | opt('-i', '--ignore', action = 'store_true', | |
65 | short = 'Ignore the applied patches in the series'), | |
66 | opt('--replace', action = 'store_true', | |
67 | short = 'Replace the unapplied patches in the series'), | |
6c8a90e1 | 68 | opt('-b', '--base', args = [argparse.commit], |
575bbdae | 69 | short = 'Use BASE instead of HEAD for file importing'), |
fc8dcca7 CM |
70 | opt('--reject', action = 'store_true', |
71 | short = 'leave the rejected hunks in corresponding *.rej files'), | |
575bbdae KH |
72 | opt('-e', '--edit', action = 'store_true', |
73 | short = 'Invoke an editor for the patch description'), | |
c18842cc | 74 | opt('-d', '--showdiff', action = 'store_true', |
575bbdae KH |
75 | short = 'Show the patch content in the editor buffer'), |
76 | opt('-a', '--author', metavar = '"NAME <EMAIL>"', | |
77 | short = 'Use "NAME <EMAIL>" as the author details'), | |
78 | opt('--authname', | |
79 | short = 'Use AUTHNAME as the author name'), | |
80 | opt('--authemail', | |
81 | short = 'Use AUTHEMAIL as the author e-mail'), | |
82 | opt('--authdate', | |
83 | short = 'Use AUTHDATE as the author date'), | |
575bbdae | 84 | ] + argparse.sign_options() |
0d2cd1e4 | 85 | |
117ed129 | 86 | directory = DirectoryHasRepository(log = True) |
0d2cd1e4 | 87 | |
b0cdad5e | 88 | def __strip_patch_name(name): |
bcb6d890 CM |
89 | stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name) |
90 | stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped) | |
91 | ||
92 | return stripped | |
b0cdad5e | 93 | |
613a2f16 PBG |
94 | def __replace_slashes_with_dashes(name): |
95 | stripped = name.replace('/', '-') | |
96 | ||
97 | return stripped | |
98 | ||
fd1c0cfc | 99 | def __create_patch(filename, message, author_name, author_email, |
99c52915 CM |
100 | author_date, diff, options): |
101 | """Create a new patch on the stack | |
0d2cd1e4 | 102 | """ |
fd1c0cfc CM |
103 | if options.name: |
104 | patch = options.name | |
105 | elif filename: | |
106 | patch = os.path.basename(filename) | |
107 | else: | |
108 | patch = '' | |
c18842cc | 109 | if options.stripname: |
fd1c0cfc | 110 | patch = __strip_patch_name(patch) |
6ef533bc | 111 | |
fff9bce5 | 112 | if not patch: |
c4f99b6c KH |
113 | if options.ignore or options.replace: |
114 | unacceptable_name = lambda name: False | |
115 | else: | |
116 | unacceptable_name = crt_series.patch_exists | |
117 | patch = make_patch_name(message, unacceptable_name) | |
fd1c0cfc CM |
118 | else: |
119 | # fix possible invalid characters in the patch name | |
120 | patch = re.sub('[^\w.]+', '-', patch).strip('-') | |
121 | ||
99c52915 | 122 | if options.ignore and patch in crt_series.get_applied(): |
27ac2b7e | 123 | out.info('Ignoring already applied patch "%s"' % patch) |
99c52915 CM |
124 | return |
125 | if options.replace and patch in crt_series.get_unapplied(): | |
c26ca1b2 | 126 | crt_series.delete_patch(patch, keep_log = True) |
fff9bce5 | 127 | |
95742cfc PBG |
128 | # refresh_patch() will invoke the editor in this case, with correct |
129 | # patch content | |
9d15ccd8 | 130 | if not message: |
95742cfc | 131 | can_edit = False |
9d15ccd8 | 132 | |
99c52915 CM |
133 | if options.author: |
134 | options.authname, options.authemail = name_email(options.author) | |
135 | ||
0d2cd1e4 CM |
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 | |
0d2cd1e4 | 143 | |
95742cfc | 144 | crt_series.new_patch(patch, message = message, can_edit = False, |
0d2cd1e4 CM |
145 | author_name = author_name, |
146 | author_email = author_email, | |
53388a71 | 147 | author_date = author_date) |
0d2cd1e4 | 148 | |
5f1629be CM |
149 | if not diff: |
150 | out.warn('No diff found, creating empty patch') | |
35344f86 | 151 | else: |
5f1629be CM |
152 | out.start('Importing patch "%s"' % patch) |
153 | if options.base: | |
fc8dcca7 | 154 | base = git_id(crt_series, options.base) |
5f1629be | 155 | else: |
fc8dcca7 | 156 | base = None |
c18842cc CM |
157 | git.apply_patch(diff = diff, base = base, reject = options.reject, |
158 | strip = options.strip) | |
5f1629be | 159 | crt_series.refresh_patch(edit = options.edit, |
c18842cc | 160 | show_patch = options.showdiff, |
cb688601 CM |
161 | sign_str = options.sign_str, |
162 | backup = False) | |
5f1629be | 163 | out.done() |
99c52915 | 164 | |
354d2031 CW |
165 | def __mkpatchname(name, suffix): |
166 | if name.lower().endswith(suffix.lower()): | |
167 | return name[:-len(suffix)] | |
168 | return name | |
169 | ||
170 | def __get_handle_and_name(filename): | |
171 | """Return a file object and a patch name derived from filename | |
172 | """ | |
173 | # see if it's a gzip'ed or bzip2'ed patch | |
174 | import bz2, gzip | |
175 | for copen, ext in [(gzip.open, '.gz'), (bz2.BZ2File, '.bz2')]: | |
176 | try: | |
177 | f = copen(filename) | |
178 | f.read(1) | |
179 | f.seek(0) | |
180 | return (f, __mkpatchname(filename, ext)) | |
181 | except IOError, e: | |
182 | pass | |
183 | ||
184 | # plain old file... | |
185 | return (open(filename), filename) | |
186 | ||
fd1c0cfc | 187 | def __import_file(filename, options, patch = None): |
99c52915 CM |
188 | """Import a patch from a file or standard input |
189 | """ | |
354d2031 | 190 | pname = None |
99c52915 | 191 | if filename: |
354d2031 | 192 | (f, pname) = __get_handle_and_name(filename) |
99c52915 CM |
193 | else: |
194 | f = sys.stdin | |
195 | ||
354d2031 CW |
196 | if patch: |
197 | pname = patch | |
198 | elif not pname: | |
199 | pname = filename | |
200 | ||
99c52915 CM |
201 | if options.mail: |
202 | try: | |
203 | msg = email.message_from_file(f) | |
204 | except Exception, ex: | |
205 | raise CmdException, 'error parsing the e-mail file: %s' % str(ex) | |
206 | message, author_name, author_email, author_date, diff = \ | |
ed60fdae | 207 | parse_mail(msg) |
99c52915 CM |
208 | else: |
209 | message, author_name, author_email, author_date, diff = \ | |
ef954fe6 | 210 | parse_patch(f.read(), contains_diff = True) |
99c52915 CM |
211 | |
212 | if filename: | |
213 | f.close() | |
214 | ||
fd1c0cfc | 215 | __create_patch(pname, message, author_name, author_email, |
99c52915 | 216 | author_date, diff, options) |
9417ece4 CM |
217 | |
218 | def __import_series(filename, options): | |
219 | """Import a series of patches | |
220 | """ | |
221 | applied = crt_series.get_applied() | |
222 | ||
223 | if filename: | |
972de1db CW |
224 | if tarfile.is_tarfile(filename): |
225 | __import_tarfile(filename, options) | |
226 | return | |
9417ece4 CM |
227 | f = file(filename) |
228 | patchdir = os.path.dirname(filename) | |
229 | else: | |
230 | f = sys.stdin | |
231 | patchdir = '' | |
232 | ||
233 | for line in f: | |
234 | patch = re.sub('#.*$', '', line).strip() | |
235 | if not patch: | |
236 | continue | |
bcb6d890 | 237 | patchfile = os.path.join(patchdir, patch) |
613a2f16 | 238 | patch = __replace_slashes_with_dashes(patch); |
9417ece4 | 239 | |
fd1c0cfc | 240 | __import_file(patchfile, options, patch) |
99c52915 CM |
241 | |
242 | if filename: | |
243 | f.close() | |
244 | ||
245 | def __import_mbox(filename, options): | |
246 | """Import a series from an mbox file | |
247 | """ | |
248 | if filename: | |
249 | f = file(filename, 'rb') | |
250 | else: | |
457c3093 | 251 | f = StringIO(sys.stdin.read()) |
99c52915 CM |
252 | |
253 | try: | |
254 | mbox = UnixMailbox(f, email.message_from_file) | |
255 | except Exception, ex: | |
256 | raise CmdException, 'error parsing the mbox file: %s' % str(ex) | |
257 | ||
258 | for msg in mbox: | |
259 | message, author_name, author_email, author_date, diff = \ | |
ed60fdae | 260 | parse_mail(msg) |
99c52915 CM |
261 | __create_patch(None, message, author_name, author_email, |
262 | author_date, diff, options) | |
263 | ||
457c3093 | 264 | f.close() |
9417ece4 | 265 | |
575c575e CW |
266 | def __import_url(url, options): |
267 | """Import a patch from a URL | |
268 | """ | |
269 | import urllib | |
270 | import tempfile | |
271 | ||
272 | if not url: | |
4a8e79dc | 273 | raise CmdException('URL argument required') |
575c575e | 274 | |
fd1c0cfc CM |
275 | patch = os.path.basename(urllib.unquote(url)) |
276 | filename = os.path.join(tempfile.gettempdir(), patch) | |
277 | urllib.urlretrieve(url, filename) | |
278 | __import_file(filename, options) | |
575c575e | 279 | |
972de1db CW |
280 | def __import_tarfile(tar, options): |
281 | """Import patch series from a tar archive | |
282 | """ | |
283 | import tempfile | |
284 | import shutil | |
285 | ||
286 | if not tarfile.is_tarfile(tar): | |
287 | raise CmdException, "%s is not a tarfile!" % tar | |
288 | ||
289 | t = tarfile.open(tar, 'r') | |
290 | names = t.getnames() | |
291 | ||
292 | # verify paths in the tarfile are safe | |
293 | for n in names: | |
294 | if n.startswith('/'): | |
295 | raise CmdException, "Absolute path found in %s" % tar | |
296 | if n.find("..") > -1: | |
297 | raise CmdException, "Relative path found in %s" % tar | |
298 | ||
299 | # find the series file | |
300 | seriesfile = ''; | |
301 | for m in names: | |
302 | if m.endswith('/series') or m == 'series': | |
303 | seriesfile = m | |
304 | break | |
305 | if seriesfile == '': | |
306 | raise CmdException, "no 'series' file found in %s" % tar | |
307 | ||
308 | # unpack into a tmp dir | |
309 | tmpdir = tempfile.mkdtemp('.stg') | |
310 | t.extractall(tmpdir) | |
311 | ||
312 | # apply the series | |
313 | __import_series(os.path.join(tmpdir, seriesfile), options) | |
314 | ||
315 | # cleanup the tmpdir | |
316 | shutil.rmtree(tmpdir) | |
317 | ||
9417ece4 CM |
318 | def func(parser, options, args): |
319 | """Import a GNU diff file as a new patch | |
320 | """ | |
321 | if len(args) > 1: | |
322 | parser.error('incorrect number of arguments') | |
323 | ||
324 | check_local_changes() | |
325 | check_conflicts() | |
6972fd6b | 326 | check_head_top_equal(crt_series) |
9417ece4 CM |
327 | |
328 | if len(args) == 1: | |
329 | filename = args[0] | |
330 | else: | |
331 | filename = None | |
332 | ||
4a8e79dc | 333 | if not options.url and filename: |
47d51d91 CM |
334 | filename = os.path.abspath(filename) |
335 | directory.cd_to_topdir() | |
336 | ||
9417ece4 CM |
337 | if options.series: |
338 | __import_series(filename, options) | |
99c52915 CM |
339 | elif options.mbox: |
340 | __import_mbox(filename, options) | |
575c575e CW |
341 | elif options.url: |
342 | __import_url(filename, options) | |
9417ece4 | 343 | else: |
fd1c0cfc | 344 | __import_file(filename, options) |
9417ece4 | 345 | |
6972fd6b | 346 | print_crt_patch(crt_series) |