chiark / gitweb /
squash: Make commit message editing more convenient
[stgit] / stgit / commands / squash.py
CommitLineData
48b209cd
KH
1# -*- coding: utf-8 -*-
2
3__copyright__ = """
4Copyright (C) 2007, Karl Hasselström <kha@treskal.com>
5
6This program is free software; you can redistribute it and/or modify
7it under the terms of the GNU General Public License version 2 as
8published by the Free Software Foundation.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13GNU General Public License for more details.
14
15You should have received a copy of the GNU General Public License
16along with this program; if not, write to the Free Software
17Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18"""
19
575bbdae 20from stgit.argparse import opt
48b209cd 21from stgit.out import *
20a52e06 22from stgit import argparse, utils
48b209cd
KH
23from stgit.commands import common
24from stgit.lib import git, transaction
25
594aa463 26help = 'Squash two or more patches into one'
33ff9cdd 27kind = 'stack'
575bbdae
KH
28usage = ['[options] <patches>']
29description = """
594aa463 30Squash two or more patches, creating one big patch that contains all
0d1d5574 31their changes. In more detail:
dcd32afa 32
0d1d5574
KH
33 1. Pop all the given patches, plus any other patches on top of them.
34
35 2. Push the given patches in the order they were given on the
36 command line.
37
38 3. Squash the given patches into one big patch.
39
40 4. Allow the user to edit the commit message of the new patch
41 interactively.
42
43 5. Push the other patches that were popped in step (1).
44
45Conflicts can occur whenever we push a patch; that is, in step (2) and
46(5). If there are conflicts, the command will stop so that you can
47resolve them."""
48b209cd 48
6c8a90e1
KH
49args = [argparse.patch_range(argparse.applied_patches,
50 argparse.unapplied_patches)]
594aa463 51options = [opt('-n', '--name', short = 'Name of squashed patch')
f9d69fc4 52 ] + argparse.message_options(save_template = True)
48b209cd 53
575bbdae
KH
54directory = common.DirectoryHasRepositoryLib()
55
d568af72
KH
56class SaveTemplateDone(Exception):
57 pass
58
594aa463 59def _squash_patches(trans, patches, msg, save_template):
dcd32afa 60 cd = trans.patches[patches[0]].data
f5f22afe 61 cd = git.CommitData(tree = cd.tree, parents = cd.parents)
dcd32afa
KH
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:
4c47af78
KW
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:]))
d568af72
KH
76 if save_template:
77 save_template(msg)
78 raise SaveTemplateDone()
79 else:
4c47af78
KW
80 msg = utils.edit_string(msg, '.stgit-squash.txt')
81 msg = utils.strip_comment(msg).strip()
dcd32afa
KH
82 cd = cd.set_message(msg)
83
84 return cd
48b209cd 85
594aa463 86def _squash(stack, iw, name, msg, save_template, patches):
48b209cd 87
dcd32afa 88 # If a name was supplied on the command line, make sure it's OK.
48b209cd
KH
89 def bad_name(pn):
90 return pn not in patches and stack.patches.exists(pn)
dcd32afa
KH
91 def get_name(cd):
92 return name or utils.make_patch_name(cd.message, bad_name)
48b209cd
KH
93 if name and bad_name(name):
94 raise common.CmdException('Patch name "%s" already taken')
48b209cd 95
594aa463 96 def make_squashed_patch(trans, new_commit_data):
dcd32afa
KH
97 name = get_name(new_commit_data)
98 trans.patches[name] = stack.repository.commit(new_commit_data)
99 trans.unapplied.insert(0, name)
100
594aa463 101 trans = transaction.StackTransaction(stack, 'squash',
781e549a 102 allow_conflicts = True)
dcd32afa 103 push_new_patch = bool(set(patches) & set(trans.applied))
dcd32afa 104 try:
594aa463 105 new_commit_data = _squash_patches(trans, patches, msg, save_template)
dcd32afa 106 if new_commit_data:
594aa463 107 # We were able to construct the squashed commit
dcd32afa
KH
108 # automatically. So just delete its constituent patches.
109 to_push = trans.delete_patches(lambda pn: pn in patches)
110 else:
111 # Automatic construction failed. So push the patches
112 # consecutively, so that a second construction attempt is
113 # guaranteed to work.
114 to_push = trans.pop_patches(lambda pn: pn in patches)
115 for pn in patches:
116 trans.push_patch(pn, iw)
594aa463 117 new_commit_data = _squash_patches(trans, patches, msg,
d568af72 118 save_template)
dcd32afa 119 assert not trans.delete_patches(lambda pn: pn in patches)
594aa463 120 make_squashed_patch(trans, new_commit_data)
dcd32afa
KH
121
122 # Push the new patch if necessary, and any unrelated patches we've
123 # had to pop out of the way.
124 if push_new_patch:
125 trans.push_patch(get_name(new_commit_data), iw)
126 for pn in to_push:
127 trans.push_patch(pn, iw)
d568af72
KH
128 except SaveTemplateDone:
129 trans.abort(iw)
130 return
dcd32afa
KH
131 except transaction.TransactionHalted:
132 pass
f9cc5e69 133 return trans.run(iw)
48b209cd
KH
134
135def func(parser, options, args):
136 stack = directory.repository.current_stack
a5920051 137 patches = common.parse_patches(args, list(stack.patchorder.all))
48b209cd
KH
138 if len(patches) < 2:
139 raise common.CmdException('Need at least two patches')
594aa463
KH
140 return _squash(stack, stack.repository.default_iw, options.name,
141 options.message, options.save_template, patches)