| 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 | import sys, os |
| 21 | from optparse import OptionParser, make_option |
| 22 | |
| 23 | from stgit.commands.common import * |
| 24 | from stgit.utils import * |
| 25 | from stgit.out import * |
| 26 | from stgit import stack, git |
| 27 | |
| 28 | help = 'turn regular GIT commits into StGIT patches' |
| 29 | usage = """%prog [<patchnames>] | -n NUM [<prefix>]] | -t <committish> [-x] |
| 30 | |
| 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 exact 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 | directory = DirectoryGotoToplevel() |
| 52 | options = [make_option('-n', '--number', type = 'int', |
| 53 | help = 'uncommit the specified number of commits'), |
| 54 | make_option('-t', '--to', |
| 55 | help = 'uncommit to the specified commit'), |
| 56 | make_option('-x', '--exclusive', |
| 57 | help = 'exclude the commit specified by the --to option', |
| 58 | action = 'store_true')] |
| 59 | |
| 60 | def func(parser, options, args): |
| 61 | """Uncommit a number of patches. |
| 62 | """ |
| 63 | if options.to: |
| 64 | if options.number: |
| 65 | parser.error('cannot give both --to and --number') |
| 66 | if len(args) != 0: |
| 67 | parser.error('cannot specify patch name with --to') |
| 68 | patch_nr = patchnames = None |
| 69 | to_commit = git_id(crt_series, options.to) |
| 70 | elif options.number: |
| 71 | if options.number <= 0: |
| 72 | parser.error('invalid value passed to --number') |
| 73 | |
| 74 | patch_nr = options.number |
| 75 | |
| 76 | if len(args) == 0: |
| 77 | patchnames = None |
| 78 | elif len(args) == 1: |
| 79 | # prefix specified |
| 80 | patchnames = ['%s%d' % (args[0], i) |
| 81 | for i in xrange(patch_nr, 0, -1)] |
| 82 | else: |
| 83 | parser.error('when using --number, specify at most one patch name') |
| 84 | elif len(args) == 0: |
| 85 | patchnames = None |
| 86 | patch_nr = 1 |
| 87 | else: |
| 88 | patchnames = args |
| 89 | patch_nr = len(patchnames) |
| 90 | |
| 91 | if crt_series.get_protected(): |
| 92 | raise CmdException, \ |
| 93 | 'This branch is protected. Uncommit is not permitted' |
| 94 | |
| 95 | def get_commit(commit_id): |
| 96 | commit = git.Commit(commit_id) |
| 97 | try: |
| 98 | parent, = commit.get_parents() |
| 99 | except ValueError: |
| 100 | raise CmdException('Commit %s does not have exactly one parent' |
| 101 | % commit_id) |
| 102 | return (commit, commit_id, parent) |
| 103 | |
| 104 | commits = [] |
| 105 | next_commit = crt_series.get_base() |
| 106 | if patch_nr: |
| 107 | out.start('Uncommitting %d patches' % patch_nr) |
| 108 | for i in xrange(patch_nr): |
| 109 | commit, commit_id, parent = get_commit(next_commit) |
| 110 | commits.append((commit, commit_id, parent)) |
| 111 | next_commit = parent |
| 112 | else: |
| 113 | if options.exclusive: |
| 114 | out.start('Uncommitting to %s (exclusive)' % to_commit) |
| 115 | else: |
| 116 | out.start('Uncommitting to %s' % to_commit) |
| 117 | while True: |
| 118 | commit, commit_id, parent = get_commit(next_commit) |
| 119 | if commit_id == to_commit: |
| 120 | if not options.exclusive: |
| 121 | commits.append((commit, commit_id, parent)) |
| 122 | break |
| 123 | commits.append((commit, commit_id, parent)) |
| 124 | next_commit = parent |
| 125 | patch_nr = len(commits) |
| 126 | |
| 127 | for (commit, commit_id, parent), patchname in \ |
| 128 | zip(commits, patchnames or [None for i in xrange(len(commits))]): |
| 129 | author_name, author_email, author_date = \ |
| 130 | name_email_date(commit.get_author()) |
| 131 | crt_series.new_patch(patchname, |
| 132 | can_edit = False, before_existing = True, |
| 133 | commit = False, |
| 134 | top = commit_id, bottom = parent, |
| 135 | message = commit.get_log(), |
| 136 | author_name = author_name, |
| 137 | author_email = author_email, |
| 138 | author_date = author_date) |
| 139 | |
| 140 | out.done() |