uncommit: Prevent stack trace for uncommit --to a merge
[stgit/kha.git] / stgit / commands / uncommit.py
blob0dcabffda0e9461afe14638a10c2da6b0422f119
1 # -*- coding: utf-8 -*-
3 __copyright__ = """
4 Copyright (C) 2006, 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, write to the Free Software
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 """
20 from stgit.argparse import opt
21 from stgit.commands import common
22 from stgit.lib import transaction
23 from stgit.out import *
24 from stgit import argparse, utils
26 help = 'Turn regular git commits into StGit patches'
27 kind = 'stack'
28 usage = ['[--] <patch-name-1> [<patch-name-2> ...]',
29 '-n NUM [--] [<prefix>]',
30 '-t <committish> [-x]']
31 description = """
32 Take one or more git commits at the base of the current stack and turn
33 them into StGIT patches. The new patches are created as applied patches
34 at the bottom of the stack. This is the opposite of 'stg commit'.
36 By default, the number of patches to uncommit is determined by the
37 number of patch names provided on the command line. First name is used
38 for the first patch to uncommit, i.e. for the newest patch.
40 The -n/--number option specifies the number of patches to uncommit. In
41 this case, at most one patch name may be specified. It is used as
42 prefix to which the patch number is appended. If no patch names are
43 provided on the command line, StGIT automatically generates them based
44 on the first line of the patch description.
46 The -t/--to option specifies that all commits up to and including the
47 given commit should be uncommitted.
49 Only commits with exactly one parent can be uncommitted; in other
50 words, you can't uncommit a merge."""
52 args = []
53 options = [
54 opt('-n', '--number', type = 'int',
55 short = 'Uncommit the specified number of commits'),
56 opt('-t', '--to', args = [argparse.commit],
57 short = 'Uncommit to the specified commit'),
58 opt('-x', '--exclusive', action = 'store_true',
59 short = 'Exclude the commit specified by the --to option')]
61 directory = common.DirectoryHasRepositoryLib()
63 def func(parser, options, args):
64 """Uncommit a number of patches.
65 """
66 stack = directory.repository.current_stack
67 if options.to:
68 if options.number:
69 parser.error('cannot give both --to and --number')
70 if len(args) != 0:
71 parser.error('cannot specify patch name with --to')
72 patch_nr = patchnames = None
73 to_commit = stack.repository.rev_parse(options.to)
74 # check whether the --to commit is on a different branch
75 merge_bases = directory.repository.get_merge_bases(to_commit, stack.base)
76 if not to_commit in merge_bases:
77 to_commit = merge_bases[0]
78 options.exclusive = True
79 elif options.number:
80 if options.number <= 0:
81 parser.error('invalid value passed to --number')
82 patch_nr = options.number
83 if len(args) == 0:
84 patchnames = None
85 elif len(args) == 1:
86 # prefix specified
87 patchnames = ['%s%d' % (args[0], i)
88 for i in xrange(patch_nr, 0, -1)]
89 else:
90 parser.error('when using --number, specify at most one patch name')
91 elif len(args) == 0:
92 patchnames = None
93 patch_nr = 1
94 else:
95 patchnames = args
96 patch_nr = len(patchnames)
98 def check_and_append(c, n):
99 next = n.data.parents;
100 try:
101 [next] = next
102 except ValueError:
103 out.done()
104 raise common.CmdException(
105 'Trying to uncommit %s, which does not have exactly one parent'
106 % n.sha1)
107 return c.append(n)
109 commits = []
110 next_commit = stack.base
111 if patch_nr:
112 out.start('Uncommitting %d patches' % patch_nr)
113 for i in xrange(patch_nr):
114 check_and_append(commits, next_commit)
115 next_commit = next_commit.data.parent
116 else:
117 if options.exclusive:
118 out.start('Uncommitting to %s (exclusive)' % to_commit.sha1)
119 else:
120 out.start('Uncommitting to %s' % to_commit.sha1)
121 while True:
122 if next_commit == to_commit:
123 if not options.exclusive:
124 check_and_append(commits, next_commit)
125 break
126 check_and_append(commits, next_commit)
127 next_commit = next_commit.data.parent
128 patch_nr = len(commits)
130 taken_names = set(stack.patchorder.all)
131 if patchnames:
132 for pn in patchnames:
133 if pn in taken_names:
134 raise common.CmdException('Patch name "%s" already taken' % pn)
135 taken_names.add(pn)
136 else:
137 patchnames = []
138 for c in reversed(commits):
139 pn = utils.make_patch_name(c.data.message,
140 lambda pn: pn in taken_names)
141 patchnames.append(pn)
142 taken_names.add(pn)
143 patchnames.reverse()
145 trans = transaction.StackTransaction(stack, 'uncommit',
146 allow_conflicts = True,
147 allow_bad_head = True)
148 for commit, pn in zip(commits, patchnames):
149 trans.patches[pn] = commit
150 trans.applied = list(reversed(patchnames)) + trans.applied
151 trans.run(set_head = False)
152 out.done()