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