chiark / gitweb /
ec2c4aec9dfe75304fa370db5e3c92f4196fb4d0
[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('-f', '--file',
65                        help = 'use FILE instead of invoking the editor'),
66            make_option('-O', '--diff-opts',
67                        help = 'options to pass to git-diff'),
68            make_option('--undo',
69                        help = 'revert the commit generated by the last edit',
70                        action = 'store_true'),
71            make_option('-a', '--annotate', metavar = 'NOTE',
72                        help = 'annotate the patch log entry'),
73            make_option('-m', '--message',
74                        help = 'replace the patch description with MESSAGE'),
75            make_option('--save-template', metavar = 'FILE',
76                        help = 'save the patch to FILE in the format used by -f'),
77            make_option('--author', metavar = '"NAME <EMAIL>"',
78                        help = 'replae the author details with "NAME <EMAIL>"'),
79            make_option('--authname',
80                        help = 'replace the author name with AUTHNAME'),
81            make_option('--authemail',
82                        help = 'replace the author e-mail with AUTHEMAIL'),
83            make_option('--authdate',
84                        help = 'replace the author date with AUTHDATE'),
85            make_option('--commname',
86                        help = 'replace the committer name with COMMNAME'),
87            make_option('--commemail',
88                        help = 'replace the committer e-mail with COMMEMAIL')
89            ] + make_sign_options()
90
91 def __update_patch(pname, fname, options):
92     """Update the current patch from the given file.
93     """
94     patch = crt_series.get_patch(pname)
95
96     bottom = patch.get_bottom()
97     top = patch.get_top()
98
99     f = open(fname)
100     message, author_name, author_email, author_date, diff = parse_patch(f)
101     f.close()
102
103     out.start('Updating patch "%s"' % pname)
104
105     if options.diff:
106         git.switch(bottom)
107         try:
108             git.apply_patch(fname)
109         except:
110             # avoid inconsistent repository state
111             git.switch(top)
112             raise
113
114     crt_series.refresh_patch(message = message,
115                              author_name = author_name,
116                              author_email = author_email,
117                              author_date = author_date,
118                              backup = True, log = 'edit')
119
120     if crt_series.empty_patch(pname):
121         out.done('empty patch')
122     else:
123         out.done()
124
125 def __generate_file(pname, fname, options):
126     """Generate a file containing the description to edit
127     """
128     patch = crt_series.get_patch(pname)
129
130     if options.diff_opts:
131         if not options.diff:
132             raise CmdException, '--diff-opts only available with --diff'
133         diff_flags = options.diff_opts.split()
134     else:
135         diff_flags = []
136
137     # generate the file to be edited
138     descr = patch.get_description().strip()
139     authdate = patch.get_authdate()
140
141     tmpl = 'From: %(authname)s <%(authemail)s>\n'
142     if authdate:
143         tmpl += 'Date: %(authdate)s\n'
144     tmpl += '\n%(descr)s\n'
145
146     tmpl_dict = {
147         'descr': 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     # write the file to be edited
174     f = open(fname, 'w+')
175     f.write(text)
176     f.close()
177
178 def __edit_update_patch(pname, options):
179     """Edit the given patch interactively.
180     """
181     if options.diff:
182         fname = '.stgit-edit.diff'
183     else:
184         fname = '.stgit-edit.txt'
185
186     __generate_file(pname, fname, options)
187
188     # invoke the editor
189     call_editor(fname)
190
191     __update_patch(pname, fname, options)
192
193 def func(parser, options, args):
194     """Edit the given patch or the current one.
195     """
196     crt_pname = crt_series.get_current()
197
198     if not args:
199         pname = crt_pname
200         if not pname:
201             raise CmdException, 'No patches applied'
202     elif len(args) == 1:
203         pname = args[0]
204         if crt_series.patch_unapplied(pname) or crt_series.patch_hidden(pname):
205             raise CmdException, 'Cannot edit unapplied or hidden patches'
206         elif not crt_series.patch_applied(pname):
207             raise CmdException, 'Unknown patch "%s"' % pname
208     else:
209         parser.error('incorrect number of arguments')
210
211     check_local_changes()
212     check_conflicts()
213     check_head_top_equal(crt_series)
214
215     if pname != crt_pname:
216         # Go to the patch to be edited
217         applied = crt_series.get_applied()
218         between = applied[:applied.index(pname):-1]
219         pop_patches(crt_series, between)
220
221     if options.author:
222         options.authname, options.authemail = name_email(options.author)
223
224     if options.undo:
225         out.start('Undoing the editing of "%s"' % pname)
226         crt_series.undo_refresh()
227         out.done()
228     elif options.message or options.authname or options.authemail \
229              or options.authdate or options.commname or options.commemail \
230              or options.sign_str:
231         # just refresh the patch with the given information
232         out.start('Updating patch "%s"' % pname)
233         crt_series.refresh_patch(message = options.message,
234                                  author_name = options.authname,
235                                  author_email = options.authemail,
236                                  author_date = options.authdate,
237                                  committer_name = options.commname,
238                                  committer_email = options.commemail,
239                                  backup = True, sign_str = options.sign_str,
240                                  log = 'edit',
241                                  notes = options.annotate)
242         out.done()
243     elif options.save_template:
244         __generate_file(pname, options.save_template, options)
245     elif options.file:
246         __update_patch(pname, options.file, options)
247     else:
248         __edit_update_patch(pname, options)
249
250     if pname != crt_pname:
251         # Push the patches back
252         between.reverse()
253         push_patches(crt_series, between)