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