chiark / gitweb /
1a7b1b36c2f11463ef99df3a0c2afb4e6914fef9
[stgit] / stgit / commands / edit.py
1 """Patch editing command
2 """
3
4 __copyright__ = """
5 Copyright (C) 2007, Catalin Marinas <catalin.marinas@gmail.com>
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 """
20
21 from optparse import OptionParser, make_option
22 from email.Utils import formatdate
23
24 from stgit.commands.common import *
25 from stgit.utils import *
26 from stgit.out import *
27 from stgit import stack, git
28
29
30 help = 'edit a patch description or diff'
31 usage = """%prog [options] [<patch>]
32
33 Edit the given patch (defaulting to the current one) description,
34 author information or its diff (if the '--diff' option is
35 passed). Without any other option, the command invokes the editor with
36 the patch description and diff in the form below:
37
38   Subject line
39
40   From: author information
41   Date: creation date
42
43   Patch description
44
45   Signed-off-by: author
46
47 Command-line options can be used to modify specific information
48 without invoking the editor.
49
50 If the patch diff is edited but the patch application fails, the
51 rejected patch is stored in the .stgit-failed.patch file (and also in
52 .stgit-edit.{diff,txt}). The edited patch can be replaced with one of
53 these files using the '--file' and '--diff' options.
54 """
55
56 options = [make_option('-d', '--diff',
57                        help = 'allow the editing of the patch diff',
58                        action = 'store_true'),
59            make_option('-f', '--file',
60                        help = 'use FILE instead of invoking the editor'),
61            make_option('-O', '--diff-opts',
62                        help = 'options to pass to git-diff'),
63            make_option('--undo',
64                        help = 'revert the commit generated by the last edit',
65                        action = 'store_true'),
66            make_option('-a', '--annotate', metavar = 'NOTE',
67                        help = 'annotate the patch log entry'),
68            make_option('-m', '--message',
69                        help = 'replace the patch description with MESSAGE'),
70            make_option('--author', metavar = '"NAME <EMAIL>"',
71                        help = 'replae the author details with "NAME <EMAIL>"'),
72            make_option('--authname',
73                        help = 'replace the author name with AUTHNAME'),
74            make_option('--authemail',
75                        help = 'replace the author e-mail with AUTHEMAIL'),
76            make_option('--authdate',
77                        help = 'replace the author date with AUTHDATE'),
78            make_option('--commname',
79                        help = 'replace the committer name with COMMNAME'),
80            make_option('--commemail',
81                        help = 'replace the committer e-mail with COMMEMAIL')
82            ] + make_sign_options()
83
84 def __update_patch(pname, fname, options):
85     """Update the current patch from the given file.
86     """
87     patch = crt_series.get_patch(pname)
88
89     bottom = patch.get_bottom()
90     top = patch.get_top()
91
92     f = open(fname)
93     message, author_name, author_email, author_date, diff = parse_patch(f)
94     f.close()
95
96     if options.diff:
97         git.switch(bottom)
98         try:
99             git.apply_patch(fname)
100         except:
101             # avoid inconsistent repository state
102             git.switch(top)
103             raise
104
105     out.start('Updating patch "%s"' % pname)
106     crt_series.refresh_patch(message = message,
107                              author_name = author_name,
108                              author_email = author_email,
109                              author_date = author_date,
110                              backup = True, log = 'edit')
111     if crt_series.empty_patch(pname):
112         out.done('empty patch')
113     else:
114         out.done()
115
116 def __edit_update_patch(pname, options):
117     """Edit the given patch interactively.
118     """
119     patch = crt_series.get_patch(pname)
120
121     if options.diff_opts:
122         if not options.diff:
123             raise CmdException, '--diff-opts only available with --diff'
124         diff_flags = options.diff_opts.split()
125     else:
126         diff_flags = []
127
128     # generate the file to be edited
129     descr = patch.get_description().strip()
130     descr_lines = descr.split('\n')
131     authdate = patch.get_authdate()
132
133     short_descr = descr_lines[0].rstrip()
134     long_descr = reduce(lambda x, y: x + '\n' + y,
135                         descr_lines[1:], '').strip()
136
137     tmpl = '%(shortdescr)s\n\n' \
138            'From: %(authname)s <%(authemail)s>\n'
139     if authdate:
140         tmpl += 'Date: %(authdate)s\n'
141     tmpl += '\n%(longdescr)s\n'
142
143     tmpl_dict = {
144         'shortdescr': short_descr,
145         'longdescr': long_descr,
146         'authname': patch.get_authname(),
147         'authemail': patch.get_authemail(),
148         'authdate': patch.get_authdate()
149         }
150
151     if options.diff:
152         # add the patch diff to the edited file
153         bottom = patch.get_bottom()
154         top = patch.get_top()
155
156         tmpl += '---\n\n' \
157                 '%(diffstat)s\n' \
158                 '%(diff)s'
159
160         tmpl_dict['diffstat'] = git.diffstat(rev1 = bottom, rev2 = top)
161         tmpl_dict['diff'] = git.diff(rev1 = bottom, rev2 = top,
162                                      diff_flags = diff_flags)
163
164     for key in tmpl_dict:
165         # make empty strings if key is not available
166         if tmpl_dict[key] is None:
167             tmpl_dict[key] = ''
168
169     text = tmpl % tmpl_dict
170
171     if options.diff:
172         fname = '.stgit-edit.diff'
173     else:
174         fname = '.stgit-edit.txt'
175
176     # write the file to be edited
177     f = open(fname, 'w+')
178     f.write(text)
179     f.close()
180
181     # invoke the editor
182     call_editor(fname)
183
184     __update_patch(pname, fname, options)
185
186 def func(parser, options, args):
187     """Edit the given patch or the current one.
188     """
189     crt_pname = crt_series.get_current()
190
191     if not args:
192         pname = crt_pname
193         if not pname:
194             raise CmdException, 'No patches applied'
195     elif len(args) == 1:
196         pname = args[0]
197         if crt_series.patch_unapplied(pname) or crt_series.patch_hidden(pname):
198             raise CmdException, 'Cannot edit unapplied or hidden patches'
199         elif not crt_series.patch_applied(pname):
200             raise CmdException, 'Unknown patch "%s"' % pname
201     else:
202         parser.error('incorrect number of arguments')
203
204     check_local_changes()
205     check_conflicts()
206     check_head_top_equal()
207
208     if pname != crt_pname:
209         # Go to the patch to be edited
210         applied = crt_series.get_applied()
211         between = applied[:applied.index(pname):-1]
212         pop_patches(between)
213
214     if options.author:
215         options.authname, options.authemail = name_email(options.author)
216
217     if options.undo:
218         out.start('Undoing the editing of "%s"' % pname)
219         crt_series.undo_refresh()
220         out.done()
221     elif options.message or options.authname or options.authemail \
222              or options.authdate or options.commname or options.commemail \
223              or options.sign_str:
224         # just refresh the patch with the given information
225         out.start('Updating patch "%s"' % pname)
226         crt_series.refresh_patch(message = options.message,
227                                  author_name = options.authname,
228                                  author_email = options.authemail,
229                                  author_date = options.authdate,
230                                  committer_name = options.commname,
231                                  committer_email = options.commemail,
232                                  backup = True, sign_str = options.sign_str,
233                                  log = 'edit',
234                                  notes = options.annotate)
235         out.done()
236     elif options.file:
237         __update_patch(pname, options.file, options)
238     else:
239         __edit_update_patch(pname, options)
240
241     if pname != crt_pname:
242         # Push the patches back
243         between.reverse()
244         push_patches(between)