chiark / gitweb /
da6727593ee341d9808f7975721c85737d57e781
[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 description and author information of the given patch (or the
34 current patch if no patch name was given). With --diff, also edit the
35 diff.
36
37 The editor is invoked with the following contents:
38
39   From: A U Thor <author@example.com>
40   Date: creation date
41
42   Patch description
43
44 If --diff was specified, the diff appears at the bottom, after a
45 separator:
46
47   ---
48
49   Diff text
50
51 Command-line options can be used to modify specific information
52 without invoking the editor.
53
54 If the patch diff is edited but the patch application fails, the
55 rejected patch is stored in the .stgit-failed.patch file (and also in
56 .stgit-edit.{diff,txt}). The edited patch can be replaced with one of
57 these files using the '--file' and '--diff' options.
58 """
59
60 directory = DirectoryGotoToplevel()
61 options = [make_option('-d', '--diff',
62                        help = 'edit the patch diff',
63                        action = 'store_true'),
64            make_option('--undo',
65                        help = 'revert the commit generated by the last edit',
66                        action = 'store_true'),
67            make_option('-a', '--annotate', metavar = 'NOTE',
68                        help = 'annotate the patch log entry'),
69            make_option('--author', metavar = '"NAME <EMAIL>"',
70                        help = 'replae the author details with "NAME <EMAIL>"'),
71            make_option('--authname',
72                        help = 'replace the author name with AUTHNAME'),
73            make_option('--authemail',
74                        help = 'replace the author e-mail with AUTHEMAIL'),
75            make_option('--authdate',
76                        help = 'replace the author date with AUTHDATE'),
77            make_option('--commname',
78                        help = 'replace the committer name with COMMNAME'),
79            make_option('--commemail',
80                        help = 'replace the committer e-mail with COMMEMAIL')
81            ] + (make_sign_options() + make_message_options()
82                 + make_diff_opts_option())
83
84 def __update_patch(pname, text, options):
85     """Update the current patch from the given text.
86     """
87     patch = crt_series.get_patch(pname)
88
89     bottom = patch.get_bottom()
90     top = patch.get_top()
91
92     if text:
93         (message, author_name, author_email, author_date, diff
94          ) = parse_patch(text)
95     else:
96         message = author_name = author_email = author_date = diff = None
97
98     out.start('Updating patch "%s"' % pname)
99
100     if options.diff:
101         git.switch(bottom)
102         try:
103             git.apply_patch(diff = diff)
104         except:
105             # avoid inconsistent repository state
106             git.switch(top)
107             raise
108
109     def c(a, b):
110         if a != None:
111             return a
112         return b
113     crt_series.refresh_patch(message = message,
114                              author_name = c(options.authname, author_name),
115                              author_email = c(options.authemail, author_email),
116                              author_date = c(options.authdate, author_date),
117                              committer_name = options.commname,
118                              committer_email = options.commemail,
119                              backup = True, sign_str = options.sign_str,
120                              log = 'edit', notes = options.annotate)
121
122     if crt_series.empty_patch(pname):
123         out.done('empty patch')
124     else:
125         out.done()
126
127 def __generate_file(pname, write_fn, options):
128     """Generate a file containing the description to edit
129     """
130     patch = crt_series.get_patch(pname)
131
132     # generate the file to be edited
133     descr = patch.get_description().strip()
134     authdate = patch.get_authdate()
135
136     tmpl = 'From: %(authname)s <%(authemail)s>\n'
137     if authdate:
138         tmpl += 'Date: %(authdate)s\n'
139     tmpl += '\n%(descr)s\n'
140
141     tmpl_dict = {
142         'descr': descr,
143         'authname': patch.get_authname(),
144         'authemail': patch.get_authemail(),
145         'authdate': patch.get_authdate()
146         }
147
148     if options.diff:
149         # add the patch diff to the edited file
150         bottom = patch.get_bottom()
151         top = patch.get_top()
152
153         tmpl += '---\n\n' \
154                 '%(diffstat)s\n' \
155                 '%(diff)s'
156
157         tmpl_dict['diffstat'] = git.diffstat(rev1 = bottom, rev2 = top)
158         tmpl_dict['diff'] = git.diff(rev1 = bottom, rev2 = top,
159                                      diff_flags = options.diff_flags)
160
161     for key in tmpl_dict:
162         # make empty strings if key is not available
163         if tmpl_dict[key] is None:
164             tmpl_dict[key] = ''
165
166     text = tmpl % tmpl_dict
167
168     # write the file to be edited
169     write_fn(text)
170
171 def __edit_update_patch(pname, options):
172     """Edit the given patch interactively.
173     """
174     if options.diff:
175         fname = '.stgit-edit.diff'
176     else:
177         fname = '.stgit-edit.txt'
178     def write_fn(text):
179         f = file(fname, 'w')
180         f.write(text)
181         f.close()
182
183     __generate_file(pname, write_fn, options)
184
185     # invoke the editor
186     call_editor(fname)
187
188     __update_patch(pname, file(fname).read(), options)
189
190 def func(parser, options, args):
191     """Edit the given patch or the current one.
192     """
193     crt_pname = crt_series.get_current()
194
195     if not args:
196         pname = crt_pname
197         if not pname:
198             raise CmdException, 'No patches applied'
199     elif len(args) == 1:
200         pname = args[0]
201         if crt_series.patch_unapplied(pname) or crt_series.patch_hidden(pname):
202             raise CmdException, 'Cannot edit unapplied or hidden patches'
203         elif not crt_series.patch_applied(pname):
204             raise CmdException, 'Unknown patch "%s"' % pname
205     else:
206         parser.error('incorrect number of arguments')
207
208     check_local_changes()
209     check_conflicts()
210     check_head_top_equal(crt_series)
211
212     if pname != crt_pname:
213         # Go to the patch to be edited
214         applied = crt_series.get_applied()
215         between = applied[:applied.index(pname):-1]
216         pop_patches(crt_series, between)
217
218     if options.author:
219         options.authname, options.authemail = name_email(options.author)
220
221     if options.undo:
222         out.start('Undoing the editing of "%s"' % pname)
223         crt_series.undo_refresh()
224         out.done()
225     elif options.save_template:
226         __generate_file(pname, options.save_template, options)
227     elif any([options.message, options.authname, options.authemail,
228               options.authdate, options.commname, options.commemail,
229               options.sign_str]):
230         out.start('Updating patch "%s"' % pname)
231         __update_patch(pname, options.message, options)
232         out.done()
233     else:
234         __edit_update_patch(pname, options)
235
236     if pname != crt_pname:
237         # Push the patches back
238         between.reverse()
239         push_patches(crt_series, between)