Infrastructure for current directory handling
[stgit] / stgit / commands / uncommit.py
index 7011ed6..a23ae20 100644 (file)
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
 __copyright__ = """
 Copyright (C) 2006, Karl Hasselström <kha@treskal.com>
 
@@ -20,61 +22,119 @@ from optparse import OptionParser, make_option
 
 from stgit.commands.common import *
 from stgit.utils import *
+from stgit.out import *
 from stgit import stack, git
 
-help = 'turn regular git commits into StGIT patches'
-usage = """%prog [options] <patchname1> [<patchname2> ... ]
+help = 'turn regular GIT commits into StGIT patches'
+usage = """%prog [<patchnames>] | -n NUM [<prefix>]] | -t <committish>
+
+Take one or more git commits at the base of the current stack and turn
+them into StGIT patches. The new patches are created as applied patches
+at the bottom of the stack. This is the exact opposite of 'stg commit'.
+
+By default, the number of patches to uncommit is determined by the
+number of patch names provided on the command line. First name is used
+for the first patch to uncommit, i.e. for the newest patch.
 
-Takes one or more git commits at the base of the current stack, and
-turns them into StGIT patches. These new patches are alreay applied,
-at the bottom of the stack. This is the exact opposite of 'stg
-commit'.
+The -n/--number option specifies the number of patches to uncommit. In
+this case, at most one patch name may be specified. It is used as
+prefix to which the patch number is appended. If no patch names are
+provided on the command line, StGIT automatically generates them based
+on the first line of the patch description.
 
-You can either give one patch name for each commit you wish to
-uncommit, or use the --number option and exactly one patch name; StGIT
-will then create numbered patches with the given patch name as prefix.
+The -t/--to option specifies that all commits up to and including the
+given commit should be uncommitted.
 
 Only commits with exactly one parent can be uncommitted; in other
-words, you can't uncommmit a merge."""
+words, you can't uncommit a merge."""
 
+directory = DirectoryHasRepository()
 options = [make_option('-n', '--number', type = 'int',
-                       help = 'uncommit the specified number of commits')]
+                       help = 'uncommit the specified number of commits'),
+           make_option('-t', '--to',
+                       help = 'uncommit to the specified commit'),
+           make_option('-x', '--exclusive',
+                       help = 'exclude the commit specified by the --to option',
+                       action = 'store_true')]
 
 def func(parser, options, args):
-    if len(args) == 0:
-        parser.error('you must specify at least one patch name')
-    if options.number:
-        if len(args) != 1:
-            parser.error('when using --number, specify exactly one patch name')
-        patchnames = ['%s%d' % (args[0], i)
-                      for i in xrange(options.number - 1, -1, -1)]
+    """Uncommit a number of patches.
+    """
+    if options.to:
+        if options.number:
+            parser.error('cannot give both --to and --number')
+        if len(args) != 0:
+            parser.error('cannot specify patch name with --to')
+        patch_nr = patchnames = None
+        to_commit = git_id(options.to)
+    elif options.number:
+        if options.number <= 0:
+            parser.error('invalid value passed to --number')
+
+        patch_nr = options.number
+
+        if len(args) == 0:
+            patchnames = None
+        elif len(args) == 1:
+            # prefix specified
+            patchnames = ['%s%d' % (args[0], i)
+                          for i in xrange(patch_nr, 0, -1)]
+        else:
+            parser.error('when using --number, specify at most one patch name')
+    elif len(args) == 0:
+        patchnames = None
+        patch_nr = 1
     else:
         patchnames = args
+        patch_nr = len(patchnames)
 
     if crt_series.get_protected():
-        raise CmdException, 'This branch is protected. Uncommit is not permitted'
+        raise CmdException, \
+              'This branch is protected. Uncommit is not permitted'
 
-    print 'Uncommitting %d patches...' % len(patchnames),
-    sys.stdout.flush()
-
-    for patchname in patchnames:
-        base_file = crt_series.get_base_file()
-        commit_id = read_string(base_file)
+    def get_commit(commit_id):
         commit = git.Commit(commit_id)
         try:
             parent, = commit.get_parents()
         except ValueError:
-            raise CmdException, ('Commit %s does not have exactly one parent'
-                                 % commit_id)
-        author_name, author_email, author_date = name_email_date(
-            commit.get_author())
+            raise CmdException('Commit %s does not have exactly one parent'
+                               % commit_id)
+        return (commit, commit_id, parent)
+
+    commits = []
+    next_commit = crt_series.get_base()
+    if patch_nr:
+        out.start('Uncommitting %d patches' % patch_nr)
+        for i in xrange(patch_nr):
+            commit, commit_id, parent = get_commit(next_commit)
+            commits.append((commit, commit_id, parent))
+            next_commit = parent
+    else:
+        if options.exclusive:
+            out.start('Uncommitting to %s (exclusive)' % to_commit)
+        else:
+            out.start('Uncommitting to %s' % to_commit)
+        while True:
+            commit, commit_id, parent = get_commit(next_commit)
+            if commit_id == to_commit:
+                if not options.exclusive:
+                    commits.append((commit, commit_id, parent))
+                break
+            commits.append((commit, commit_id, parent))
+            next_commit = parent
+        patch_nr = len(commits)
+
+    for (commit, commit_id, parent), patchname in \
+        zip(commits, patchnames or [None for i in xrange(len(commits))]):
+        author_name, author_email, author_date = \
+                     name_email_date(commit.get_author())
         crt_series.new_patch(patchname,
                              can_edit = False, before_existing = True,
+                             commit = False,
                              top = commit_id, bottom = parent,
                              message = commit.get_log(),
                              author_name = author_name,
                              author_email = author_email,
                              author_date = author_date)
-        write_string(base_file, parent)
 
-    print 'done'
+    out.done()