Merge branch 'stable' into stable-master-merge
[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 ] + (utils.make_sign_options() + utils.make_message_options()
64 + utils.make_author_committer_options()
65 + utils.make_diff_opts_option())
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)
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()