chiark / gitweb /
7daf156ffe402442485048329cca627d72ddb283
[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 make_option
22
23 from stgit import git, utils
24 from stgit.commands import common
25 from stgit.lib import git as gitlib, transaction
26 from stgit.out import *
27
28 help = 'edit a patch description or diff'
29 usage = """%prog [options] [<patch>]
30
31 Edit the description and author information of the given patch (or the
32 current patch if no patch name was given). With --diff, also edit the
33 diff.
34
35 The editor is invoked with the following contents:
36
37   From: A U Thor <author@example.com>
38   Date: creation date
39
40   Patch description
41
42 If --diff was specified, the diff appears at the bottom, after a
43 separator:
44
45   ---
46
47   Diff text
48
49 Command-line options can be used to modify specific information
50 without invoking the editor. (With the --edit option, the editor is
51 invoked even if such command-line options are given.)
52
53 If the patch diff is edited but does not apply, no changes are made to
54 the patch at all. The edited patch is saved to a file which you can
55 feed to "stg edit --file", once you have made sure it does apply."""
56
57 directory = common.DirectoryHasRepositoryLib()
58 options = [make_option('-d', '--diff',
59                        help = 'edit the patch diff',
60                        action = 'store_true'),
61            make_option('-e', '--edit', action = 'store_true',
62                        help = 'invoke interactive editor'),
63            make_option('--author', metavar = '"NAME <EMAIL>"',
64                        help = 'replae the author details with "NAME <EMAIL>"'),
65            make_option('--authname',
66                        help = 'replace the author name with AUTHNAME'),
67            make_option('--authemail',
68                        help = 'replace the author e-mail with AUTHEMAIL'),
69            make_option('--authdate',
70                        help = 'replace the author date with AUTHDATE'),
71            make_option('--commname',
72                        help = 'replace the committer name with COMMNAME'),
73            make_option('--commemail',
74                        help = 'replace the committer e-mail with COMMEMAIL')
75            ] + (utils.make_sign_options() + utils.make_message_options()
76                 + utils.make_diff_opts_option())
77
78 def patch_diff(repository, cd, diff, diff_flags):
79     if diff:
80         diff = repository.diff_tree(cd.parent.data.tree, cd.tree, diff_flags)
81         return '\n'.join([git.diffstat(diff), diff])
82     else:
83         return None
84
85 def patch_description(cd, diff):
86     """Generate a string containing the description to edit."""
87
88     desc = ['From: %s <%s>' % (cd.author.name, cd.author.email),
89             'Date: %s' % cd.author.date.isoformat(),
90             '',
91             cd.message]
92     if diff:
93         desc += ['---',
94                  '',
95                 diff]
96     return '\n'.join(desc)
97
98 def patch_desc(repository, cd, failed_diff, diff, diff_flags):
99     return patch_description(cd, failed_diff or patch_diff(
100             repository, cd, diff, diff_flags))
101
102 def update_patch_description(repository, cd, text):
103     message, authname, authemail, authdate, diff = common.parse_patch(text)
104     cd = (cd.set_message(message)
105             .set_author(cd.author.set_name(authname)
106                                  .set_email(authemail)
107                                  .set_date(gitlib.Date.maybe(authdate))))
108     failed_diff = None
109     if diff:
110         tree = repository.apply(cd.parent.data.tree, diff)
111         if tree == None:
112             failed_diff = diff
113         else:
114             cd = cd.set_tree(tree)
115     return cd, failed_diff
116
117 def func(parser, options, args):
118     """Edit the given patch or the current one.
119     """
120     stack = directory.repository.current_stack
121
122     if len(args) == 0:
123         if not stack.patchorder.applied:
124             raise common.CmdException(
125                 'Cannot edit top patch, because no patches are applied')
126         patchname = stack.patchorder.applied[-1]
127     elif len(args) == 1:
128         [patchname] = args
129         if not stack.patches.exists(patchname):
130             raise common.CmdException('%s: no such patch' % patchname)
131     else:
132         parser.error('Cannot edit more than one patch')
133
134     cd = orig_cd = stack.patches.get(patchname).commit.data
135
136     # Read patch from user-provided description.
137     if options.message == None:
138         failed_diff = None
139     else:
140         cd, failed_diff = update_patch_description(stack.repository, cd,
141                                                    options.message)
142
143     # Modify author and committer data.
144     if options.author != None:
145         options.authname, options.authemail = common.name_email(options.author)
146     for p, f, val in [('author', 'name', options.authname),
147                       ('author', 'email', options.authemail),
148                       ('author', 'date', gitlib.Date.maybe(options.authdate)),
149                       ('committer', 'name', options.commname),
150                       ('committer', 'email', options.commemail)]:
151         if val != None:
152             cd = getattr(cd, 'set_' + p)(
153                 getattr(getattr(cd, p), 'set_' + f)(val))
154
155     # Add Signed-off-by: or similar.
156     if options.sign_str != None:
157         cd = cd.set_message(utils.add_sign_line(
158                 cd.message, options.sign_str, gitlib.Person.committer().name,
159                 gitlib.Person.committer().email))
160
161     if options.save_template:
162         options.save_template(
163             patch_desc(stack.repository, cd, failed_diff,
164                        options.diff, options.diff_flags))
165         return utils.STGIT_SUCCESS
166
167     # Let user edit the patch manually.
168     if cd == orig_cd or options.edit:
169         fn = '.stgit-edit.' + ['txt', 'patch'][bool(options.diff)]
170         cd, failed_diff = update_patch_description(
171             stack.repository, cd, utils.edit_string(
172                 patch_desc(stack.repository, cd, failed_diff,
173                            options.diff, options.diff_flags),
174                 fn))
175
176     def failed():
177         fn = '.stgit-failed.patch'
178         f = file(fn, 'w')
179         f.write(patch_desc(stack.repository, cd, failed_diff,
180                            options.diff, options.diff_flags))
181         f.close()
182         out.error('Edited patch did not apply.',
183                   'It has been saved to "%s".' % fn)
184         return utils.STGIT_COMMAND_ERROR
185
186     # If we couldn't apply the patch, fail without even trying to
187     # effect any of the changes.
188     if failed_diff:
189         return failed()
190
191     # The patch applied, so now we have to rewrite the StGit patch
192     # (and any patches on top of it).
193     iw = stack.repository.default_iw
194     trans = transaction.StackTransaction(stack, 'stg edit')
195     if patchname in trans.applied:
196         popped = trans.applied[trans.applied.index(patchname)+1:]
197         assert not trans.pop_patches(lambda pn: pn in popped)
198     else:
199         popped = []
200     trans.patches[patchname] = stack.repository.commit(cd)
201     try:
202         for pn in popped:
203             trans.push_patch(pn, iw)
204     except transaction.TransactionHalted:
205         pass
206     try:
207         # Either a complete success, or a conflict during push. But in
208         # either case, we've successfully effected the edits the user
209         # asked us for.
210         return trans.run(iw)
211     except transaction.TransactionException:
212         # Transaction aborted -- we couldn't check out files due to
213         # dirty index/worktree. The edits were not carried out.
214         return failed()