Infrastructure for current directory handling
[stgit] / stgit / commands / uncommit.py
index e03d207..a23ae20 100644 (file)
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
 __copyright__ = """
 Copyright (C) 2006, Karl Hasselström <kha@treskal.com>
 
@@ -20,64 +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 the patches to uncommit is determined by the
+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.
 
-The --number option specifies the number of patches to uncommit.  In
-this case, only one patch name may be specified. It is used as prefix to
-which the patch number is appended.
+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.
+
+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 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()