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