Use Patches.make_name() in various commands
[stgit.git] / stgit / commands / squash.py
blobf5de19dfdc0912b4d130fdf81077d1b2c203a582
1 from stgit import argparse, utils
2 from stgit.argparse import opt, patch_range
3 from stgit.commands.common import (
4 CmdException,
5 DirectoryHasRepository,
6 parse_patches,
7 run_commit_msg_hook,
9 from stgit.lib.transaction import StackTransaction, TransactionHalted
11 __copyright__ = """
12 Copyright (C) 2007, Karl Hasselström <kha@treskal.com>
14 This program is free software; you can redistribute it and/or modify
15 it under the terms of the GNU General Public License version 2 as
16 published by the Free Software Foundation.
18 This program is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with this program; if not, see http://www.gnu.org/licenses/.
25 """
27 help = 'Squash two or more patches into one'
28 kind = 'stack'
29 usage = ['[options] [--] <patches>']
30 description = """
31 Squash two or more patches, creating one big patch that contains all
32 their changes. In more detail:
34 1. Pop all the given patches, plus any other patches on top of them.
36 2. Push the given patches in the order they were given on the
37 command line.
39 3. Squash the given patches into one big patch.
41 4. Allow the user to edit the commit message of the new patch
42 interactively.
44 5. Push the other patches that were popped in step (1).
46 Conflicts can occur whenever we push a patch; that is, in step (2) and
47 (5). If there are conflicts, the command will stop so that you can
48 resolve them."""
50 args = [patch_range('applied_patches', 'unapplied_patches')]
51 options = (
52 [opt('-n', '--name', short='Name of squashed patch')]
53 + argparse.message_options(save_template=True)
54 + argparse.hook_options()
57 directory = DirectoryHasRepository()
60 class SaveTemplateDone(Exception):
61 pass
64 def _append_comment(message, comment):
65 return '\n'.join(
67 message,
68 '',
69 '---',
70 'Everything following the line with "---" will be ignored',
71 '',
72 comment,
77 def _strip_comment(message):
78 try:
79 return message[: message.index('\n---\n')]
80 except ValueError:
81 return message
84 def _squash_patches(trans, patches, msg, save_template, no_verify=False):
85 cd = trans.patches[patches[0]].data
86 for pn in patches[1:]:
87 c = trans.patches[pn]
88 tree = trans.stack.repository.simple_merge(
89 base=c.data.parent.data.tree,
90 ours=cd.tree,
91 theirs=c.data.tree,
93 if not tree:
94 return None
95 cd = cd.set_tree(tree)
96 if msg is None:
97 msg = _append_comment(
98 cd.message_str,
99 '\n\n'.join(
100 '%s\n\n%s' % (pn.ljust(70, '-'), trans.patches[pn].data.message_str)
101 for pn in patches[1:]
104 if save_template:
105 save_template(msg.encode(cd.encoding))
106 raise SaveTemplateDone()
107 else:
108 msg = utils.edit_string(msg, '.stgit-squash.txt')
109 msg = _strip_comment(msg).strip()
110 cd = cd.set_message(msg)
112 if not no_verify:
113 cd = run_commit_msg_hook(trans.stack.repository, cd)
115 return cd
118 def _squash(stack, iw, name, msg, save_template, patches, no_verify=False):
119 # If a name was supplied on the command line, make sure it's OK.
120 if name and name not in patches and stack.patches.exists(name):
121 raise CmdException('Patch name "%s" already taken' % name)
123 def get_name(cd):
124 return name or stack.patches.make_name(cd.message_str, allow=patches)
126 def make_squashed_patch(trans, new_commit_data):
127 name = get_name(new_commit_data)
128 trans.patches[name] = stack.repository.commit(new_commit_data)
129 trans.unapplied.insert(0, name)
131 trans = StackTransaction(stack, 'squash', allow_conflicts=True)
132 push_new_patch = bool(set(patches) & set(trans.applied))
133 try:
134 new_commit_data = _squash_patches(trans, patches, msg, save_template, no_verify)
135 if new_commit_data:
136 # We were able to construct the squashed commit
137 # automatically. So just delete its constituent patches.
138 to_push = trans.delete_patches(lambda pn: pn in patches)
139 else:
140 # Automatic construction failed. So push the patches
141 # consecutively, so that a second construction attempt is
142 # guaranteed to work.
143 to_push = trans.pop_patches(lambda pn: pn in patches)
144 for pn in patches:
145 trans.push_patch(pn, iw)
146 new_commit_data = _squash_patches(
147 trans, patches, msg, save_template, no_verify
149 popped_extra = trans.delete_patches(lambda pn: pn in patches)
150 assert not popped_extra
151 make_squashed_patch(trans, new_commit_data)
153 # Push the new patch if necessary, and any unrelated patches we've
154 # had to pop out of the way.
155 if push_new_patch:
156 trans.push_patch(get_name(new_commit_data), iw)
157 for pn in to_push:
158 trans.push_patch(pn, iw)
159 except SaveTemplateDone:
160 trans.abort(iw)
161 return
162 except TransactionHalted:
163 pass
164 return trans.run(iw)
167 def func(parser, options, args):
168 stack = directory.repository.current_stack
169 patches = parse_patches(args, list(stack.patchorder.all))
170 if len(patches) < 2:
171 raise CmdException('Need at least two patches')
172 if options.name and not stack.patches.is_name_valid(options.name):
173 raise CmdException('Patch name "%s" is invalid' % options.name)
174 return _squash(
175 stack,
176 stack.repository.default_iw,
177 options.name,
178 options.message,
179 options.save_template,
180 patches,
181 options.no_verify,