chiark / gitweb /
Fix importing from stdin
[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
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>|<commit>]
28
29 Create a new patch and import the given GNU diff file (defaulting to
30 the standard input) or a given commit object into it. By default, the
31 file name is used as the patch name but this can be overriden with the
32 '--name' option.
33
34 The patch file can either be a normal file with the description at the
35 top or it can have standard mail format, the Subject, From and Date
36 headers being used for generating the patch information. The patch
37 description has to be separated from the data with a '---' line. For a
38 normal file, if no author information is given, the first
39 'Signed-off-by:' line is used.
40
41 When a commit object is imported, the log and author information are
42 those of the commit object. Passing the '--reverse' option will cancel
43 an existing commit object."""
44
45 options = [make_option('-m', '--mail',
46                        help = 'import the patch from a standard e-mail file',
47                        action = 'store_true'),
48            make_option('-c', '--commit',
49                        help = 'import a commit object as a patch',
50                        action = 'store_true'),
51            make_option('--reverse',
52                        help = 'reverse the commit object before importing',
53                        action = 'store_true'),
54            make_option('-n', '--name',
55                        help = 'use NAME as the patch name'),
56            make_option('--base',
57                        help = 'use BASE instead of HEAD for file importing'),
58            make_option('-e', '--edit',
59                        help = 'invoke an editor for the patch description',
60                        action = 'store_true'),
61            make_option('-s', '--showpatch',
62                        help = 'show the patch content in the editor buffer',
63                        action = 'store_true'),
64            make_option('-a', '--author', metavar = '"NAME <EMAIL>"',
65                        help = 'use "NAME <EMAIL>" as the author details'),
66            make_option('--authname',
67                        help = 'use AUTHNAME as the author name'),
68            make_option('--authemail',
69                        help = 'use AUTHEMAIL as the author e-mail'),
70            make_option('--authdate',
71                        help = 'use AUTHDATE as the author date'),
72            make_option('--commname',
73                        help = 'use COMMNAME as the committer name'),
74            make_option('--commemail',
75                        help = 'use COMMEMAIL as the committer e-mail')]
76
77
78 def __parse_mail(filename = None):
79     """Parse the input file in a mail format and return (description,
80     authname, authemail, authdate)
81     """
82     if filename:
83         f = file(filename)
84     else:
85         f = sys.stdin
86
87     descr = authname = authemail = authdate = None
88
89     # parse the headers
90     while True:
91         line = f.readline()
92         if not line:
93             break
94         line = line.strip()
95         if re.match('from:\s+', line, re.I):
96             auth = re.findall('^.*?:\s+(.*)$', line)[0]
97             authname, authemail = name_email(auth)
98         elif re.match('date:\s+', line, re.I):
99             authdate = re.findall('^.*?:\s+(.*)$', line)[0]
100         elif re.match('subject:\s+', line, re.I):
101             descr = re.findall('^.*?:\s+(.*)$', line)[0]
102         elif line == '':
103             # end of headers
104             break
105
106     # remove the '[*PATCH*]' expression in the subject
107     if descr:
108         descr = re.findall('^(\[[^\s]*[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$',
109                            descr)[0][1]
110         descr += '\n\n'
111     else:
112         raise CmdException, 'Subject: line not found'
113
114     # the rest of the patch description
115     while True:
116         line = f.readline()
117         if not line:
118             break
119         if re.match('---\s*$', line) or re.match('diff -', line) or \
120                 re.match('^Index: ', line):
121             break
122         else:
123             descr += line
124     descr.rstrip()
125
126     if filename:
127         f.close()
128
129     return (descr, authname, authemail, authdate)
130
131 def __parse_patch(filename = None):
132     """Parse the input file and return (description, authname,
133     authemail, authdate)
134     """
135     if filename:
136         f = file(filename)
137     else:
138         f = sys.stdin
139
140     authname = authemail = authdate = None
141
142     descr = ''
143     while True:
144         line = f.readline()
145         if not line:
146             break
147
148         # the first 'Signed-of-by:' is the author
149         if not authname and re.match('signed-off-by:\s+', line, re.I):
150             auth = re.findall('^.*?:\s+(.*)$', line)[0]
151             authname, authemail = name_email(auth)
152
153         if re.match('---\s*$', line) or re.match('diff -', line):
154             break
155         else:
156             descr += line
157     descr.rstrip()
158
159     if descr == '':
160         descr = None
161
162     if filename:
163         f.close()
164
165     return (descr, authname, authemail, authdate)
166
167 def import_file(parser, options, args):
168     """Import a GNU diff file as a new patch
169     """
170     if len(args) > 1:
171         parser.error('incorrect number of arguments')
172     elif len(args) == 1:
173         filename = args[0]
174     else:
175         filename = None
176
177     if options.name:
178         patch = options.name
179     elif filename:
180         patch = os.path.basename(filename)
181     else:
182         raise CmdException, 'Unkown patch name'
183
184     # the defaults
185     message = author_name = author_email = author_date = committer_name = \
186               committer_email = None
187
188     if options.author:
189         options.authname, options.authemail = name_email(options.author)
190
191     if options.mail:
192         message, author_name, author_email, author_date = \
193                  __parse_mail(filename)
194     else:
195         message, author_name, author_email, author_date = \
196                  __parse_patch(filename)
197
198     # refresh_patch() will invoke the editor in this case, with correct
199     # patch content
200     if not message:
201         can_edit = False
202
203     # override the automatically parsed settings
204     if options.authname:
205         author_name = options.authname
206     if options.authemail:
207         author_email = options.authemail
208     if options.authdate:
209         author_date = options.authdate
210     if options.commname:
211         committer_name = options.commname
212     if options.commemail:
213         committer_email = options.commemail
214
215     crt_series.new_patch(patch, message = message, can_edit = False,
216                          author_name = author_name,
217                          author_email = author_email,
218                          author_date = author_date,
219                          committer_name = committer_name,
220                          committer_email = committer_email)
221
222     print 'Importing patch %s...' % patch,
223     sys.stdout.flush()
224
225     if options.base:
226         orig_head = git.get_head()
227         git.switch(options.base)
228
229         try:
230             git.apply_patch(filename)
231         except git.GitException, ex:
232             print >> sys.stderr, '"git apply" failed'
233             git.switch(orig_head)
234             raise
235
236         top = crt_series.refresh_patch(commit_only = True)
237         git.switch(orig_head)
238         git.merge(options.base, orig_head, top)
239     else:
240         git.apply_patch(filename)
241
242     crt_series.refresh_patch(edit = options.edit,
243                              show_patch = options.showpatch)
244
245     print 'done'
246     print_crt_patch()
247
248 def import_commit(parser, options, args):
249     """Import a commit object as a new patch
250     """
251     if len(args) != 1:
252         parser.error('incorrect number of arguments')
253
254     commit_id = args[0]
255
256     if options.name:
257         patch = options.name
258     else:
259         raise CmdException, 'Unkown patch name'
260
261     commit = git.Commit(commit_id)
262
263     if not options.reverse:
264         bottom = commit.get_parent()
265         top = commit_id
266     else:
267         bottom = commit_id
268         top = commit.get_parent()
269
270     message = commit.get_log()
271     author_name, author_email, author_date = \
272                  name_email_date(commit.get_author())
273
274     print 'Importing commit %s...' % commit_id,
275     sys.stdout.flush()
276
277     crt_series.new_patch(patch, message = message, can_edit = False,
278                          unapplied = True, bottom = bottom, top = top,
279                          author_name = author_name,
280                          author_email = author_email,
281                          author_date = author_date)
282     crt_series.push_patch(patch)
283
284     print 'done'
285     print_crt_patch()
286
287 def func(parser, options, args):
288     """Import a GNU diff file or a commit object as a new patch
289     """
290     check_local_changes()
291     check_conflicts()
292     check_head_top_equal()
293
294     if options.commit:
295         import_commit(parser, options, args)
296     else:
297         import_file(parser, options, args)