Use README.md in setup.py long_description
[stgit.git] / stgit / lib / edit.py
blobaf6f60bbedf115bb61ef135a97252e3ebf7a2018
1 """This module contains utility functions for patch editing."""
3 import io
4 import re
6 from stgit import utils
7 from stgit.commands import common
8 from stgit.config import config
9 from stgit.lib import transaction
10 from stgit.lib.git import Date, Person
11 from stgit.lib.log import log_stack_state
12 from stgit.out import out
14 EDIT_MESSAGE_INSTRUCTIONS = """# Everything here is editable! You can modify the patch name, author,
15 # date, commit message, and the diff (if --diff was given).
16 # Lines starting with '#' will be ignored, and an empty message
17 # aborts the edit.
18 """
21 def update_patch_description(repo, cd, text, contains_diff):
22 """Create commit with updated description.
24 The given :class:`stgit.lib.git.CommitData` is updated with the
25 given description, which may contain author name and time
26 stamp in addition to a new commit message. If ``contains_diff`` is
27 true, it may also contain a replacement diff.
29 :returns: 3-tuple:
30 - the new :class:`CommitData<stgit.lib.git.CommitData>`
31 - the patch name if given or None otherwise
32 - the diff text if it did not apply or None otherwise
33 """
34 (message, patch_name, authname, authemail, authdate, diff) = common.parse_patch(
35 text, contains_diff
37 author = cd.author
38 if authname is not None:
39 author = author.set_name(authname)
40 if authemail is not None:
41 author = author.set_email(authemail)
42 if authdate is not None:
43 author = author.set_date(Date(authdate))
44 cd = cd.set_message(message).set_author(author)
45 failed_diff = None
46 if diff and not re.match(br'---\s*\Z', diff, re.MULTILINE):
47 tree = repo.apply(cd.parent.data.tree, diff, quiet=False)
48 if tree is None:
49 failed_diff = diff
50 else:
51 cd = cd.set_tree(tree)
52 return cd, patch_name, failed_diff
55 def patch_desc(repo, cd, patch_name, append_diff, diff_flags, replacement_diff):
56 """Return a description text for the patch.
58 The returned description is suitable for editing and/or reimporting with
59 :func:`update_patch_description()`.
61 :param cd: the :class:`stgit.lib.git.CommitData` to generate a description of
62 :param append_diff: whether to append the patch diff to the description
63 :type append_diff: bool
64 :param diff_flags: extra parameters to pass to `git diff`
65 :param replacement_diff: diff text to use; or None if it should be computed from cd
66 :type replacement_diff: str or None
68 """
69 commit_encoding = config.get('i18n.commitencoding')
70 desc = '\n'.join(
72 'Patch: %s' % patch_name,
73 'From: %s' % cd.author.name_email,
74 'Date: %s' % cd.author.date.isoformat(),
75 '',
76 cd.message_str,
77 EDIT_MESSAGE_INSTRUCTIONS,
79 ).encode(commit_encoding)
80 if append_diff:
81 parts = [desc.rstrip(), b'---', b'']
82 if replacement_diff:
83 parts.append(replacement_diff)
84 else:
85 diff = repo.diff_tree(cd.parent.data.tree, cd.tree, diff_flags)
86 if diff:
87 diffstat = repo.default_iw.diffstat(diff).encode(commit_encoding)
88 parts.extend([diffstat, diff])
89 desc = b'\n'.join(parts)
90 return desc
93 def interactive_edit_patch(repo, cd, patch_name, edit_diff, diff_flags):
94 """Edit the patch interactively.
96 If ``edit_diff`` is true, edit the diff as well. If ``replacement_diff`` is not
97 None, it contains a diff to edit instead of the patch's real diff.
99 :returns: 3-tuple:
100 - the new :class:`commitdata<stgit.lib.git.commitdata>`
101 - the patch name if given or none otherwise
102 - the diff text if it did not apply or none otherwise
104 cd, patch_name, failed_diff = update_patch_description(
105 repo,
107 utils.edit_bytes(
108 patch_desc(
109 repo, cd, patch_name, edit_diff, diff_flags, replacement_diff=None
111 '.stgit-edit.' + ['txt', 'patch'][bool(edit_diff)],
113 edit_diff,
116 # If we couldn't apply the patch, fail without even trying to
117 # effect any of the changes.
118 if failed_diff:
119 return failed(repo, cd, patch_name, edit_diff, diff_flags, failed_diff)
121 return cd, patch_name, failed_diff
124 def auto_edit_patch(repo, cd, msg, author, sign_str):
125 """Edit the patch noninteractively in a couple of ways:
127 * If ``msg`` is not None, parse it to find a replacement message, and possibly also
128 replacement author and timestamp.
130 * ``author`` is a function that takes the original :class:`stgit.lib.git.Person`
131 value as argument, and returns the new one.
133 * ``sign_str, if not None, is a trailer string to append to the message.
135 :returns: 3-tuple:
136 - the new :class:`commitdata<stgit.lib.git.commitdata>`
137 - the patch name if given or none otherwise
138 - the diff text if it did not apply or none otherwise
141 if msg is not None:
142 cd, _, failed_diff = update_patch_description(
143 repo, cd, msg, contains_diff=False
145 assert not failed_diff
146 a = author(cd.author)
147 if a != cd.author:
148 cd = cd.set_author(a)
149 if sign_str is not None:
150 cd = cd.set_message(
151 utils.add_trailer(
152 cd.message_str,
153 sign_str,
154 Person.committer().name,
155 Person.committer().email,
158 return cd
161 def failed(
162 repository,
164 patch_name,
165 edit_diff,
166 diff_flags,
167 replacement_diff,
168 reason='Edited patch did not apply.',
170 """Call when edit fails. Logs to stderr and saves a patch to filesystem."""
171 fn = '.stgit-failed.patch'
172 with io.open(fn, 'wb') as f:
173 f.write(
174 patch_desc(
175 repository,
177 patch_name,
178 edit_diff,
179 diff_flags,
180 replacement_diff=replacement_diff,
183 out.error(reason, 'The patch has been saved to "%s".' % fn)
184 return utils.STGIT_COMMAND_ERROR
187 def perform_edit(
188 stack,
190 orig_patchname,
191 new_patchname,
192 edit_diff,
193 diff_flags,
194 replacement_diff,
195 set_tree=None,
197 """Given instructions, performs required the edit.
199 :returns: 2-tuple:
200 - the result of the transaction
201 - the new patch name, whether changed or not.
203 # Refresh the committer information
204 cd = cd.set_committer(None)
206 # Rewrite the StGit patch with the given diff (and any patches on top of
207 # it).
208 iw = stack.repository.default_iw
209 trans = transaction.StackTransaction(stack, 'edit', allow_conflicts=True)
210 if orig_patchname in trans.applied:
211 popped = trans.applied[trans.applied.index(orig_patchname) + 1 :]
212 popped_extra = trans.pop_patches(lambda pn: pn in popped)
213 assert not popped_extra
214 else:
215 popped = []
216 trans.patches[orig_patchname] = stack.repository.commit(cd)
217 if new_patchname == "":
218 new_patchname = stack.patches.make_name(cd.message_str, allow=orig_patchname)
219 if new_patchname is not None and orig_patchname != new_patchname:
220 out.start('Renaming patch "%s" to "%s"' % (orig_patchname, new_patchname))
221 trans.rename_patch(orig_patchname, new_patchname)
222 out.done()
223 log_stack_state(stack, 'rename %s to %s' % (orig_patchname, new_patchname))
224 else:
225 new_patchname = orig_patchname
226 try:
227 for pn in popped:
228 if set_tree:
229 trans.push_tree(pn)
230 else:
231 trans.push_patch(pn, iw, allow_interactive=True)
232 except transaction.TransactionHalted:
233 pass
234 try:
235 # Either a complete success, or a conflict during push. But in
236 # either case, we've successfully effected the edits the user
237 # asked us for.
238 return trans.run(iw), new_patchname
239 except transaction.TransactionException:
240 # Transaction aborted -- we couldn't check out files due to
241 # dirty index/worktree. The edits were not carried out.
242 return (
243 failed(stack, cd, new_patchname, edit_diff, diff_flags, replacement_diff),
244 new_patchname,