chiark / gitweb /
63c710be9e1da238a6da8fa0bebef1bad1638c6c
[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     out.start('Updating patch "%s"' % pname)
97
98     if options.diff:
99         git.switch(bottom)
100         try:
101             git.apply_patch(fname)
102         except:
103             # avoid inconsistent repository state
104             git.switch(top)
105             raise
106
107     crt_series.refresh_patch(message = message,
108                              author_name = author_name,
109                              author_email = author_email,
110                              author_date = author_date,
111                              backup = True, log = 'edit')
112
113     if crt_series.empty_patch(pname):
114         out.done('empty patch')
115     else:
116         out.done()
117
118 def __edit_update_patch(pname, options):
119     """Edit the given patch interactively.
120     """
121     patch = crt_series.get_patch(pname)
122
123     if options.diff_opts:
124         if not options.diff:
125             raise CmdException, '--diff-opts only available with --diff'
126         diff_flags = options.diff_opts.split()
127     else:
128         diff_flags = []
129
130     # generate the file to be edited
131     descr = patch.get_description().strip()
132     descr_lines = descr.split('\n')
133     authdate = patch.get_authdate()
134
135     short_descr = descr_lines[0].rstrip()
136     long_descr = reduce(lambda x, y: x + '\n' + y,
137                         descr_lines[1:], '').strip()
138
139     tmpl = '%(shortdescr)s\n\n' \
140            'From: %(authname)s <%(authemail)s>\n'
141     if authdate:
142         tmpl += 'Date: %(authdate)s\n'
143     tmpl += '\n%(longdescr)s\n'
144
145     tmpl_dict = {
146         'shortdescr': short_descr,
147         'longdescr': long_descr,
148         'authname': patch.get_authname(),
149         'authemail': patch.get_authemail(),
150         'authdate': patch.get_authdate()
151         }
152
153     if options.diff:
154         # add the patch diff to the edited file
155         bottom = patch.get_bottom()
156         top = patch.get_top()
157
158         tmpl += '---\n\n' \
159                 '%(diffstat)s\n' \
160                 '%(diff)s'
161
162         tmpl_dict['diffstat'] = git.diffstat(rev1 = bottom, rev2 = top)
163         tmpl_dict['diff'] = git.diff(rev1 = bottom, rev2 = top,
164                                      diff_flags = diff_flags)
165
166     for key in tmpl_dict:
167         # make empty strings if key is not available
168         if tmpl_dict[key] is None:
169             tmpl_dict[key] = ''
170
171     text = tmpl % tmpl_dict
172
173     if options.diff:
174         fname = '.stgit-edit.diff'
175     else:
176         fname = '.stgit-edit.txt'
177
178     # write the file to be edited
179     f = open(fname, 'w+')
180     f.write(text)
181     f.close()
182
183     # invoke the editor
184     call_editor(fname)
185
186     __update_patch(pname, fname, options)
187
188 def func(parser, options, args):
189     """Edit the given patch or the current one.
190     """
191     crt_pname = crt_series.get_current()
192
193     if not args:
194         pname = crt_pname
195         if not pname:
196             raise CmdException, 'No patches applied'
197     elif len(args) == 1:
198         pname = args[0]
199         if crt_series.patch_unapplied(pname) or crt_series.patch_hidden(pname):
200             raise CmdException, 'Cannot edit unapplied or hidden patches'
201         elif not crt_series.patch_applied(pname):
202             raise CmdException, 'Unknown patch "%s"' % pname
203     else:
204         parser.error('incorrect number of arguments')
205
206     check_local_changes()
207     check_conflicts()
208     check_head_top_equal()
209
210     if pname != crt_pname:
211         # Go to the patch to be edited
212         applied = crt_series.get_applied()
213         between = applied[:applied.index(pname):-1]
214         pop_patches(between)
215
216     if options.author:
217         options.authname, options.authemail = name_email(options.author)
218
219     if options.undo:
220         out.start('Undoing the editing of "%s"' % pname)
221         crt_series.undo_refresh()
222         out.done()
223     elif options.message or options.authname or options.authemail \
224              or options.authdate or options.commname or options.commemail \
225              or options.sign_str:
226         # just refresh the patch with the given information
227         out.start('Updating patch "%s"' % pname)
228         crt_series.refresh_patch(message = options.message,
229                                  author_name = options.authname,
230                                  author_email = options.authemail,
231                                  author_date = options.authdate,
232                                  committer_name = options.commname,
233                                  committer_email = options.commemail,
234                                  backup = True, sign_str = options.sign_str,
235                                  log = 'edit',
236                                  notes = options.annotate)
237         out.done()
238     elif options.file:
239         __update_patch(pname, options.file, options)
240     else:
241         __edit_update_patch(pname, options)
242
243     if pname != crt_pname:
244         # Push the patches back
245         between.reverse()
246         push_patches(between)