chiark / gitweb /
squash: Make commit message editing more convenient
[stgit] / stgit / commands / squash.py
1 # -*- coding: utf-8 -*-
2
3 __copyright__ = """
4 Copyright (C) 2007, Karl Hasselström <kha@treskal.com>
5
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.
9
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.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 """
19
20 from stgit.argparse import opt
21 from stgit.out import *
22 from stgit import argparse, utils
23 from stgit.commands import common
24 from stgit.lib import git, transaction
25
26 help = 'Squash two or more patches into one'
27 kind = 'stack'
28 usage = ['[options] <patches>']
29 description = """
30 Squash two or more patches, creating one big patch that contains all
31 their changes. In more detail:
32
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
45 Conflicts 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
47 resolve them."""
48
49 args = [argparse.patch_range(argparse.applied_patches,
50                              argparse.unapplied_patches)]
51 options = [opt('-n', '--name', short = 'Name of squashed patch')
52            ] + argparse.message_options(save_template = True)
53
54 directory = common.DirectoryHasRepositoryLib()
55
56 class SaveTemplateDone(Exception):
57     pass
58
59 def _squash_patches(trans, patches, msg, save_template):
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)
83
84     return cd
85
86 def _squash(stack, iw, name, msg, save_template, patches):
87
88     # If a name was supplied on the command line, make sure it's OK.
89     def bad_name(pn):
90         return pn not in patches and stack.patches.exists(pn)
91     def get_name(cd):
92         return name or utils.make_patch_name(cd.message, bad_name)
93     if name and bad_name(name):
94         raise common.CmdException('Patch name "%s" already taken')
95
96     def make_squashed_patch(trans, new_commit_data):
97         name = get_name(new_commit_data)
98         trans.patches[name] = stack.repository.commit(new_commit_data)
99         trans.unapplied.insert(0, name)
100
101     trans = transaction.StackTransaction(stack, 'squash',
102                                          allow_conflicts = True)
103     push_new_patch = bool(set(patches) & set(trans.applied))
104     try:
105         new_commit_data = _squash_patches(trans, patches, msg, save_template)
106         if new_commit_data:
107             # We were able to construct the squashed commit
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)
117             new_commit_data = _squash_patches(trans, patches, msg,
118                                                 save_template)
119             assert not trans.delete_patches(lambda pn: pn in patches)
120         make_squashed_patch(trans, new_commit_data)
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)
128     except SaveTemplateDone:
129         trans.abort(iw)
130         return
131     except transaction.TransactionHalted:
132         pass
133     return trans.run(iw)
134
135 def func(parser, options, args):
136     stack = directory.repository.current_stack
137     patches = common.parse_patches(args, list(stack.patchorder.all))
138     if len(patches) < 2:
139         raise common.CmdException('Need at least two patches')
140     return _squash(stack, stack.repository.default_iw, options.name,
141                    options.message, options.save_template, patches)