Run commit-msg hook on new, edit, refresh -e, squash
[stgit.git] / stgit / commands / squash.py
blob93dbb39fe3727fce85bfb591038fa2d0520a781c
1 # -*- coding: utf-8 -*-
3 __copyright__ = """
4 Copyright (C) 2007, Karl Hasselström <kha@treskal.com>
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License version 2 as
8 published by the Free Software Foundation.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, see http://www.gnu.org/licenses/.
17 """
19 from stgit.argparse import opt
20 from stgit.out import *
21 from stgit import argparse, utils
22 from stgit.commands import common
23 from stgit.lib import git, transaction
25 help = 'Squash two or more patches into one'
26 kind = 'stack'
27 usage = ['[options] [--] <patches>']
28 description = """
29 Squash two or more patches, creating one big patch that contains all
30 their changes. In more detail:
32 1. Pop all the given patches, plus any other patches on top of them.
34 2. Push the given patches in the order they were given on the
35 command line.
37 3. Squash the given patches into one big patch.
39 4. Allow the user to edit the commit message of the new patch
40 interactively.
42 5. Push the other patches that were popped in step (1).
44 Conflicts can occur whenever we push a patch; that is, in step (2) and
45 (5). If there are conflicts, the command will stop so that you can
46 resolve them."""
48 args = [argparse.patch_range(argparse.applied_patches,
49 argparse.unapplied_patches)]
50 options = ([opt('-n', '--name', short = 'Name of squashed patch')] +
51 argparse.message_options(save_template = True) +
52 argparse.hook_options())
54 directory = common.DirectoryHasRepositoryLib()
56 class SaveTemplateDone(Exception):
57 pass
59 def _squash_patches(trans, patches, msg, save_template, no_verify=False):
60 cd = trans.patches[patches[0]].data
61 cd = git.CommitData(tree = cd.tree, parents = cd.parents)
62 for pn in patches[1:]:
63 c = trans.patches[pn]
64 tree = trans.stack.repository.simple_merge(
65 base = c.data.parent.data.tree,
66 ours = cd.tree, theirs = c.data.tree)
67 if not tree:
68 return None
69 cd = cd.set_tree(tree)
70 if msg == None:
71 msg = utils.append_comment(
72 trans.patches[patches[0]].data.message,
73 '\n\n'.join('%s\n\n%s' % (pn.ljust(70, '-'),
74 trans.patches[pn].data.message)
75 for pn in patches[1:]))
76 if save_template:
77 save_template(msg)
78 raise SaveTemplateDone()
79 else:
80 msg = utils.edit_string(msg, '.stgit-squash.txt')
81 msg = utils.strip_comment(msg).strip()
82 cd = cd.set_message(msg)
84 if not no_verify:
85 cd = common.run_commit_msg_hook(trans.stack.repository, cd)
87 return cd
89 def _squash(stack, iw, name, msg, save_template, patches, no_verify=False):
91 # If a name was supplied on the command line, make sure it's OK.
92 def bad_name(pn):
93 return pn not in patches and stack.patches.exists(pn)
94 def get_name(cd):
95 return name or utils.make_patch_name(cd.message, bad_name)
96 if name and bad_name(name):
97 raise common.CmdException('Patch name "%s" already taken')
99 def make_squashed_patch(trans, new_commit_data):
100 name = get_name(new_commit_data)
101 trans.patches[name] = stack.repository.commit(new_commit_data)
102 trans.unapplied.insert(0, name)
104 trans = transaction.StackTransaction(stack, 'squash',
105 allow_conflicts = True)
106 push_new_patch = bool(set(patches) & set(trans.applied))
107 try:
108 new_commit_data = _squash_patches(trans, patches, msg, save_template,
109 no_verify)
110 if new_commit_data:
111 # We were able to construct the squashed commit
112 # automatically. So just delete its constituent patches.
113 to_push = trans.delete_patches(lambda pn: pn in patches)
114 else:
115 # Automatic construction failed. So push the patches
116 # consecutively, so that a second construction attempt is
117 # guaranteed to work.
118 to_push = trans.pop_patches(lambda pn: pn in patches)
119 for pn in patches:
120 trans.push_patch(pn, iw)
121 new_commit_data = _squash_patches(trans, patches, msg,
122 save_template, no_verify)
123 assert not trans.delete_patches(lambda pn: pn in patches)
124 make_squashed_patch(trans, new_commit_data)
126 # Push the new patch if necessary, and any unrelated patches we've
127 # had to pop out of the way.
128 if push_new_patch:
129 trans.push_patch(get_name(new_commit_data), iw)
130 for pn in to_push:
131 trans.push_patch(pn, iw)
132 except SaveTemplateDone:
133 trans.abort(iw)
134 return
135 except transaction.TransactionHalted:
136 pass
137 return trans.run(iw)
139 def func(parser, options, args):
140 stack = directory.repository.current_stack
141 patches = common.parse_patches(args, list(stack.patchorder.all))
142 if len(patches) < 2:
143 raise common.CmdException('Need at least two patches')
144 return _squash(stack, stack.repository.default_iw, options.name,
145 options.message, options.save_template, patches,
146 options.no_verify)