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