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