chiark / gitweb /
b6ef6c7c272cc3b3f2bbd31fa9845ad76f114375
[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 stgit.argparse import opt
22 from stgit import argparse, git, utils
23 from stgit.commands import common
24 from stgit.lib import git as gitlib, transaction
25 from stgit.out import *
26
27 help = 'edit a patch description or diff'
28 kind = 'patch'
29 usage = ['[options] [<patch>]']
30 description = """
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 options = [
58     opt('-d', '--diff', action = 'store_true',
59         short = 'Edit the patch diff'),
60     opt('-e', '--edit', action = 'store_true',
61         short = 'Invoke interactive editor'),
62     ] + (argparse.sign_options() + argparse.message_options() +
63          argparse.author_committer_options() + argparse.diff_opts_option())
64
65 directory = common.DirectoryHasRepositoryLib()
66
67 def patch_diff(repository, cd, diff, diff_flags):
68     if diff:
69         diff = repository.diff_tree(cd.parent.data.tree, cd.tree, diff_flags)
70         return '\n'.join([git.diffstat(diff), diff])
71     else:
72         return None
73
74 def patch_description(cd, diff):
75     """Generate a string containing the description to edit."""
76
77     desc = ['From: %s <%s>' % (cd.author.name, cd.author.email),
78             'Date: %s' % cd.author.date.isoformat(),
79             '',
80             cd.message]
81     if diff:
82         desc += ['---',
83                  '',
84                 diff]
85     return '\n'.join(desc)
86
87 def patch_desc(repository, cd, failed_diff, diff, diff_flags):
88     return patch_description(cd, failed_diff or patch_diff(
89             repository, cd, diff, diff_flags))
90
91 def update_patch_description(repository, cd, text):
92     message, authname, authemail, authdate, diff = common.parse_patch(text)
93     cd = (cd.set_message(message)
94             .set_author(cd.author.set_name(authname)
95                                  .set_email(authemail)
96                                  .set_date(gitlib.Date.maybe(authdate))))
97     failed_diff = None
98     if diff:
99         tree = repository.apply(cd.parent.data.tree, diff, quiet = False)
100         if tree == None:
101             failed_diff = diff
102         else:
103             cd = cd.set_tree(tree)
104     return cd, failed_diff
105
106 def func(parser, options, args):
107     """Edit the given patch or the current one.
108     """
109     stack = directory.repository.current_stack
110
111     if len(args) == 0:
112         if not stack.patchorder.applied:
113             raise common.CmdException(
114                 'Cannot edit top patch, because no patches are applied')
115         patchname = stack.patchorder.applied[-1]
116     elif len(args) == 1:
117         [patchname] = args
118         if not stack.patches.exists(patchname):
119             raise common.CmdException('%s: no such patch' % patchname)
120     else:
121         parser.error('Cannot edit more than one patch')
122
123     cd = orig_cd = stack.patches.get(patchname).commit.data
124
125     # Read patch from user-provided description.
126     if options.message == None:
127         failed_diff = None
128     else:
129         cd, failed_diff = update_patch_description(stack.repository, cd,
130                                                    options.message)
131
132     # Modify author and committer data.
133     a, c = options.author(cd.author), options.committer(cd.committer)
134     if (a, c) != (cd.author, cd.committer):
135         cd = cd.set_author(a).set_committer(c)
136
137     # Add Signed-off-by: or similar.
138     if options.sign_str != None:
139         cd = cd.set_message(utils.add_sign_line(
140                 cd.message, options.sign_str, gitlib.Person.committer().name,
141                 gitlib.Person.committer().email))
142
143     if options.save_template:
144         options.save_template(
145             patch_desc(stack.repository, cd, failed_diff,
146                        options.diff, options.diff_flags))
147         return utils.STGIT_SUCCESS
148
149     # Let user edit the patch manually.
150     if cd == orig_cd or options.edit:
151         fn = '.stgit-edit.' + ['txt', 'patch'][bool(options.diff)]
152         cd, failed_diff = update_patch_description(
153             stack.repository, cd, utils.edit_string(
154                 patch_desc(stack.repository, cd, failed_diff,
155                            options.diff, options.diff_flags),
156                 fn))
157
158     def failed():
159         fn = '.stgit-failed.patch'
160         f = file(fn, 'w')
161         f.write(patch_desc(stack.repository, cd, failed_diff,
162                            options.diff, options.diff_flags))
163         f.close()
164         out.error('Edited patch did not apply.',
165                   'It has been saved to "%s".' % fn)
166         return utils.STGIT_COMMAND_ERROR
167
168     # If we couldn't apply the patch, fail without even trying to
169     # effect any of the changes.
170     if failed_diff:
171         return failed()
172
173     # The patch applied, so now we have to rewrite the StGit patch
174     # (and any patches on top of it).
175     iw = stack.repository.default_iw
176     trans = transaction.StackTransaction(stack, 'edit', allow_conflicts = True)
177     if patchname in trans.applied:
178         popped = trans.applied[trans.applied.index(patchname)+1:]
179         assert not trans.pop_patches(lambda pn: pn in popped)
180     else:
181         popped = []
182     trans.patches[patchname] = stack.repository.commit(cd)
183     try:
184         for pn in popped:
185             trans.push_patch(pn, iw)
186     except transaction.TransactionHalted:
187         pass
188     try:
189         # Either a complete success, or a conflict during push. But in
190         # either case, we've successfully effected the edits the user
191         # asked us for.
192         return trans.run(iw)
193     except transaction.TransactionException:
194         # Transaction aborted -- we couldn't check out files due to
195         # dirty index/worktree. The edits were not carried out.
196         return failed()