stg import now extracts Message-ID header
[stgit.git] / stgit / lib / edit.py
blob05a9dbe30432aeef076550f8336af650db7f112d
1 """This module contains utility functions for patch editing."""
3 import re
5 from stgit import utils
6 from stgit.commands import common
7 from stgit.config import config
8 from stgit.lib import transaction
9 from stgit.lib.git import Date, Person
10 from stgit.lib.log import log_stack_state
11 from stgit.out import out
13 EDIT_MESSAGE_INSTRUCTIONS = """# Everything here is editable! You can modify the patch name, author,
14 # date, commit message, and the diff (if --diff was given).
15 # Lines starting with '#' will be ignored, and an empty message
16 # aborts the edit.
17 """
20 def _update_patch_description(repo, cd, text, contains_diff):
21 """Create commit with updated description.
23 The given :class:`stgit.lib.git.CommitData` is updated with the
24 given description, which may contain author name and time
25 stamp in addition to a new commit message. If ``contains_diff`` is
26 true, it may also contain a replacement diff.
28 :returns: 3-tuple:
29 - the new :class:`CommitData<stgit.lib.git.CommitData>`
30 - the patch name if given or None otherwise
31 - the diff text if it did not apply or None otherwise
32 """
33 (message, patch_name, authname, authemail, authdate, diff) = common.parse_patch(
34 text, contains_diff
36 author = cd.author
37 if authname is not None:
38 author = author.set_name(authname)
39 if authemail is not None:
40 author = author.set_email(authemail)
41 if authdate is not None:
42 author = author.set_date(Date(authdate))
43 cd = cd.set_message(message).set_author(author)
44 failed_diff = None
45 if diff and not re.match(br'---\s*\Z', diff, re.MULTILINE):
46 tree = repo.apply(cd.parent.data.tree, diff, quiet=False)
47 if tree is None:
48 failed_diff = diff
49 else:
50 cd = cd.set_tree(tree)
51 return cd, patch_name, failed_diff
54 def get_patch_description(repo, cd, patch_name, append_diff, diff_flags):
55 """Return a description text for the patch.
57 The returned description is suitable for editing and/or reimporting with
58 :func:`_update_patch_description()`.
60 :param cd: the :class:`stgit.lib.git.CommitData` to generate a description of
61 :param append_diff: whether to append the patch diff to the description
62 :type append_diff: bool
63 :param diff_flags: extra parameters to pass to `git diff`
65 """
66 commit_encoding = config.get('i18n.commitencoding')
67 desc = '\n'.join(
69 'Patch: %s' % patch_name,
70 'From: %s' % cd.author.name_email,
71 'Date: %s' % cd.author.date.isoformat(),
72 '',
73 cd.message_str,
74 EDIT_MESSAGE_INSTRUCTIONS,
76 ).encode(commit_encoding)
77 if append_diff:
78 parts = [desc.rstrip(), b'---', b'']
79 diff = repo.diff_tree(
80 cd.parent.data.tree, cd.tree, diff_flags, binary=False, full_index=True
82 if diff:
83 diffstat = repo.default_iw.diffstat(diff).encode(commit_encoding)
84 parts.extend([diffstat, diff])
85 desc = b'\n'.join(parts)
86 return desc
89 def interactive_edit_patch(repo, cd, patch_name, edit_diff, diff_flags):
90 """Edit the patch interactively.
92 If ``edit_diff`` is true, edit the diff as well.
94 :returns: 3-tuple:
95 - the new :class:`commitdata<stgit.lib.git.commitdata>`
96 - the patch name if given or none otherwise
97 - the diff text if it did not apply or none otherwise
98 """
99 patch_desc = get_patch_description(repo, cd, patch_name, edit_diff, diff_flags)
100 cd, patch_name, failed_diff = _update_patch_description(
101 repo,
103 utils.edit_bytes(
104 patch_desc, '.stgit-edit.' + ['txt', 'patch'][bool(edit_diff)]
106 edit_diff,
109 if failed_diff:
110 note_patch_application_failure(patch_desc)
112 return cd, patch_name, failed_diff
115 def auto_edit_patch(repo, cd, msg, author, trailers):
116 """Edit the patch noninteractively in a couple of ways:
118 * If ``msg`` is not None, parse it to find a replacement message, and possibly also
119 replacement author and timestamp.
121 * ``author`` is a function that takes the original :class:`stgit.lib.git.Person`
122 value as argument, and returns the new one.
124 * ``trailers`` is a list of trailer strings to append to the message.
126 :returns: 3-tuple:
127 - the new :class:`commitdata<stgit.lib.git.commitdata>`
128 - the patch name if given or none otherwise
129 - the diff text if it did not apply or none otherwise
132 if msg is not None:
133 cd, _, failed_diff = _update_patch_description(
134 repo, cd, msg, contains_diff=False
136 assert not failed_diff
137 a = author(cd.author)
138 if a != cd.author:
139 cd = cd.set_author(a)
140 if trailers:
141 cd = cd.set_message(
142 utils.add_trailers(
143 cd.message_str,
144 trailers,
145 Person.committer().name,
146 Person.committer().email,
149 return cd
152 def note_patch_application_failure(patch_desc, reason='Edited patch did not apply.'):
153 """Call when edit fails. Logs to stderr and saves a patch to filesystem."""
154 fn = '.stgit-failed.patch'
155 with open(fn, 'wb') as f:
156 f.write(patch_desc)
157 out.error(reason, 'The patch has been saved to "%s".' % fn)
160 def perform_edit(
161 stack,
163 orig_patchname,
164 new_patchname,
165 edit_diff,
166 diff_flags,
167 set_tree=None,
169 """Given instructions, performs required the edit.
171 :returns: 2-tuple:
172 - the result of the transaction
173 - the new patch name, whether changed or not.
175 # Refresh the committer information
176 cd = cd.set_committer(None)
178 # Rewrite the StGit patch with the given diff (and any patches on top of
179 # it).
180 iw = stack.repository.default_iw
181 trans = transaction.StackTransaction(stack, 'edit', allow_conflicts=True)
182 if orig_patchname in trans.applied:
183 popped = trans.applied[trans.applied.index(orig_patchname) + 1 :]
184 popped_extra = trans.pop_patches(lambda pn: pn in popped)
185 assert not popped_extra
186 else:
187 popped = []
188 trans.patches[orig_patchname] = stack.repository.commit(cd)
189 if new_patchname == "":
190 new_patchname = stack.patches.make_name(cd.message_str, allow=orig_patchname)
191 if new_patchname is not None and orig_patchname != new_patchname:
192 out.start('Renaming patch "%s" to "%s"' % (orig_patchname, new_patchname))
193 trans.rename_patch(orig_patchname, new_patchname)
194 out.done()
195 log_stack_state(stack, 'rename %s to %s' % (orig_patchname, new_patchname))
196 else:
197 new_patchname = orig_patchname
198 try:
199 for pn in popped:
200 if set_tree:
201 trans.push_tree(pn)
202 else:
203 trans.push_patch(pn, iw, allow_interactive=True)
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), new_patchname
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 note_patch_application_failure(
215 get_patch_description(
216 stack.repository, cd, orig_patchname, edit_diff, diff_flags
219 return utils.STGIT_COMMAND_ERROR, new_patchname