Use get/set_name for a stack's name.
[stgit] / stgit / commands / branch.py
index 56a5fe6..c22e143 100644 (file)
@@ -18,31 +18,38 @@ along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
-import sys, os
+import sys, os, time
 from optparse import OptionParser, make_option
 
 from stgit.commands.common import *
 from stgit.utils import *
 from optparse import OptionParser, make_option
 
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
+from stgit import stack, git, basedir
 
 
 
 
-help = 'manage development branches'
+help = 'manage patch stacks'
 usage = """%prog [options] branch-name [commit-id]
 
 usage = """%prog [options] branch-name [commit-id]
 
-Create, list, switch between, rename, or delete development branches
+Create, clone, switch between, rename, or delete development branches
 within a git repository.  By default, a single branch called 'master'
 is always created in a new repository.  This subcommand allows you to
 within a git repository.  By default, a single branch called 'master'
 is always created in a new repository.  This subcommand allows you to
-manage several patch series in the same repository.
+manage several patch series in the same repository via GIT branches.
 
 When displaying the branches, the names can be prefixed with
 
 When displaying the branches, the names can be prefixed with
-'s' (StGIT managed) or 'p' (protected)."""
+'s' (StGIT managed) or 'p' (protected).
+
+If not given any options, switch to the named branch."""
 
 options = [make_option('-c', '--create',
                        help = 'create a new development branch',
                        action = 'store_true'),
 
 options = [make_option('-c', '--create',
                        help = 'create a new development branch',
                        action = 'store_true'),
+           make_option('--clone',
+                       help = 'clone the contents of the current branch',
+                       action = 'store_true'),
            make_option('--delete',
                        help = 'delete an existing development branch',
                        action = 'store_true'),
            make_option('--delete',
                        help = 'delete an existing development branch',
                        action = 'store_true'),
+           make_option('-d', '--description',
+                       help = 'set the branch description'),
            make_option('--force',
                        help = 'force a delete when the series is not empty',
                        action = 'store_true'),
            make_option('--force',
                        help = 'force a delete when the series is not empty',
                        action = 'store_true'),
@@ -50,89 +57,139 @@ options = [make_option('-c', '--create',
                        help = 'list branches contained in this repository',
                        action = 'store_true'),
            make_option('-p', '--protect',
                        help = 'list branches contained in this repository',
                        action = 'store_true'),
            make_option('-p', '--protect',
-                       help = 'prevent "stg pull" from modifying this branch',
+                       help = 'prevent StGIT from modifying this branch',
                        action = 'store_true'),
            make_option('-r', '--rename',
                        help = 'rename an existing development branch',
                        action = 'store_true'),
            make_option('-u', '--unprotect',
                        action = 'store_true'),
            make_option('-r', '--rename',
                        help = 'rename an existing development branch',
                        action = 'store_true'),
            make_option('-u', '--unprotect',
-                       help = 'allow "stg pull" to modify this branch',
+                       help = 'allow StGIT to modify this branch',
                        action = 'store_true')]
 
 
                        action = 'store_true')]
 
 
-def is_current_branch(branch_name):
-    return git.get_head_file() == branch_name
+def __is_current_branch(branch_name):
+    return crt_series.get_name() == branch_name
 
 
-def print_branch(branch_name):
+def __print_branch(branch_name, length):
     initialized = ' '
     current = ' '
     protected = ' '
     initialized = ' '
     current = ' '
     protected = ' '
-    if os.path.isdir(os.path.join(git.base_dir, 'patches', branch_name)):
+
+    branch = stack.Series(branch_name)
+
+    if branch.is_initialised():
         initialized = 's'
         initialized = 's'
-    if is_current_branch(branch_name):
+    if __is_current_branch(branch_name):
         current = '>'
         current = '>'
-    if stack.Series(branch_name).get_protected():
+    if branch.get_protected():
         protected = 'p'
         protected = 'p'
-    print '%s %s%s\t%s\t%s' % (current, initialized, protected, branch_name, \
-                               stack.Series(branch_name).get_description())
+    out.stdout(current + ' ' + initialized + protected + '\t'
+               + branch_name.ljust(length) + '  | ' + branch.get_description())
+
+def __delete_branch(doomed_name, force = False):
+    doomed = stack.Series(doomed_name)
 
 
-def delete_branch(doomed_name, force = False):
-    if stack.Series(doomed_name).get_protected():
+    if doomed.get_protected():
         raise CmdException, 'This branch is protected. Delete is not permitted'
 
         raise CmdException, 'This branch is protected. Delete is not permitted'
 
-    if is_current_branch(doomed_name) and doomed_name != 'master':
-        git.switch_branch('master')
+    out.start('Deleting branch "%s"' % doomed_name)
+
+    if __is_current_branch(doomed_name):
+        check_local_changes()
+        check_conflicts()
+        check_head_top_equal()
 
 
-    stack.Series(doomed_name).delete(force)
+        if doomed_name != 'master':
+            git.switch_branch('master')
+
+    doomed.delete(force)
 
     if doomed_name != 'master':
         git.delete_branch(doomed_name)
 
 
     if doomed_name != 'master':
         git.delete_branch(doomed_name)
 
-    print 'Branch "%s" has been deleted.' % doomed_name
+    out.done()
 
 
-def rename_branch(from_name, to_name):
-    if from_name == 'master':
-        raise CmdException, 'Renaming the master branch is not allowed'
+def func(parser, options, args):
 
 
-    to_patchdir = os.path.join(git.base_dir, 'patches', to_name)
-    if os.path.isdir(to_patchdir):
-        raise CmdException, '"%s" already exists' % to_patchdir
-    to_base = os.path.join(git.base_dir, 'refs', 'bases', to_name)
-    if os.path.isfile(to_base):
-        raise CmdException, '"%s" already exists' % to_base
+    if options.create:
+
+        if len(args) == 0 or len(args) > 2:
+            parser.error('incorrect number of arguments')
 
 
-    git.rename_branch(from_name, to_name)
+        check_local_changes()
+        check_conflicts()
+        check_head_top_equal()
 
 
-    from_patchdir = os.path.join(git.base_dir, 'patches', from_name)
-    if os.path.isdir(from_patchdir):
-        os.rename(from_patchdir, to_patchdir)
-    from_base = os.path.join(git.base_dir, 'refs', 'bases', from_name)
-    if os.path.isfile(from_base):
-        os.rename(from_base, to_base)
+        tree_id = None
+        if len(args) >= 2:
+            try:
+                if git.rev_parse(args[1]) == git.rev_parse('refs/heads/' + args[1]):
+                    # we are for sure referring to a branch
+                    parentbranch = 'refs/heads/' + args[1]
+                    out.info('Recording "%s" as parent branch' % parentbranch)
+                elif git.rev_parse(args[1]) and re.search('/', args[1]):
+                    # FIXME: should the test be more strict ?
+                    parentbranch = args[1]
+                else:
+                    # Note: this includes refs to StGIT patches
+                    out.info('Don\'t know how to determine parent branch'
+                             ' from "%s"' % args[1])
+                    parentbranch = None
+            except git.GitException:
+                # should use a more specific exception to catch only
+                # non-git refs ?
+                out.info('Don\'t know how to determine parent branch'
+                         ' from "%s"' % args[1])
+                parentbranch = None
+
+            tree_id = git_id(args[1])
+        else:
+            # branch stack off current branch
+            parentbranch = git.get_head_file()
+
+        if parentbranch:
+            parentremote = git.identify_remote(parentbranch)
+            if parentremote:
+                out.info('Using remote "%s" to pull parent from'
+                         % parentremote)
+            else:
+                out.info('Recording as a local branch')
+        else:
+            # no known parent branch, can't guess the remote
+            parentremote = None
 
 
-    print 'Renamed branch "%s" as "%s".' % (from_name, to_name)
+        stack.Series(args[0]).init(create_at = tree_id,
+                                   parent_remote = parentremote,
+                                   parent_branch = parentbranch)
 
 
-def func(parser, options, args):
+        out.info('Branch "%s" created' % args[0])
+        return
 
 
-    if options.create:
+    elif options.clone:
 
 
-        if len(args) == 0 or len(args) > 2:
+        if len(args) == 0:
+            clone = crt_series.get_name() + \
+                    time.strftime('-%C%y%m%d-%H%M%S')
+        elif len(args) == 1:
+            clone = args[0]
+        else:
             parser.error('incorrect number of arguments')
             parser.error('incorrect number of arguments')
-        tree_id = None
-        if len(args) == 2:
-            tree_id = args[1]
 
 
-        git.create_branch(args[0], tree_id)
-        stack.Series(args[0]).init()
+        check_local_changes()
+        check_conflicts()
+        check_head_top_equal()
+
+        out.start('Cloning current branch to "%s"' % clone)
+        crt_series.clone(clone)
+        out.done()
 
 
-        print 'Branch "%s" created.' % args[0]
         return
 
     elif options.delete:
 
         if len(args) != 1:
             parser.error('incorrect number of arguments')
         return
 
     elif options.delete:
 
         if len(args) != 1:
             parser.error('incorrect number of arguments')
-        delete_branch(args[0], options.force)
+        __delete_branch(args[0], options.force)
         return
 
     elif options.list:
         return
 
     elif options.list:
@@ -140,67 +197,110 @@ def func(parser, options, args):
         if len(args) != 0:
             parser.error('incorrect number of arguments')
 
         if len(args) != 0:
             parser.error('incorrect number of arguments')
 
-        branches = os.listdir(os.path.join(git.base_dir, 'refs', 'heads'))
+        branches = []
+        basepath = os.path.join(basedir.get(), 'refs', 'heads')
+        for path, files, dirs in walk_tree(basepath):
+            branches += [os.path.join(path, f) for f in files]
         branches.sort()
 
         branches.sort()
 
-        print 'Available branches:'
-        for i in branches:
-            print_branch(i)
+        if branches:
+            out.info('Available branches:')
+            max_len = max([len(i) for i in branches])
+            for i in branches:
+                __print_branch(i, max_len)
+        else:
+            out.info('No branches')
         return
 
     elif options.protect:
 
         if len(args) == 0:
         return
 
     elif options.protect:
 
         if len(args) == 0:
-            branch = git.get_head_file()
+            branch_name = crt_series.get_name()
         elif len(args) == 1:
         elif len(args) == 1:
-            branch = args[0]
+            branch_name = args[0]
         else:
             parser.error('incorrect number of arguments')
         else:
             parser.error('incorrect number of arguments')
+        branch = stack.Series(branch_name)
 
 
-        base = os.path.join(git.base_dir, 'refs', 'bases', branch)
-        if not os.path.isfile(base):
-            raise CmdException, 'Branch "%s" is not controlled by StGit' % branch
+        if not branch.is_initialised():
+            raise CmdException, 'Branch "%s" is not controlled by StGIT' \
+                  % branch_name
+
+        out.start('Protecting branch "%s"' % branch_name)
+        branch.protect()
+        out.done()
 
 
-        print 'Protecting branch "%s"...' % branch
-        stack.Series(branch).protect()
         return
 
     elif options.rename:
 
         if len(args) != 2:
             parser.error('incorrect number of arguments')
         return
 
     elif options.rename:
 
         if len(args) != 2:
             parser.error('incorrect number of arguments')
-        rename_branch(args[0], args[1])
+
+        if __is_current_branch(args[0]):
+            raise CmdException, 'Renaming the current branch is not supported'
+
+        stack.Series(args[0]).rename(args[1])
+
+        out.info('Renamed branch "%s" to "%s"' % (args[0], args[1]))
+
         return
 
     elif options.unprotect:
 
         if len(args) == 0:
         return
 
     elif options.unprotect:
 
         if len(args) == 0:
-            branch = git.get_head_file()
+            branch_name = crt_series.get_name()
         elif len(args) == 1:
         elif len(args) == 1:
-            branch = args[0]
+            branch_name = args[0]
         else:
             parser.error('incorrect number of arguments')
         else:
             parser.error('incorrect number of arguments')
+        branch = stack.Series(branch_name)
+
+        if not branch.is_initialised():
+            raise CmdException, 'Branch "%s" is not controlled by StGIT' \
+                  % branch_name
 
 
-        base = os.path.join(git.base_dir, 'refs', 'bases', branch)
-        if not os.path.isfile(base):
-            raise CmdException, 'Branch "%s" is not controlled by StGit' % branch
+        out.info('Unprotecting branch "%s"' % branch_name)
+        branch.unprotect()
+        out.done()
+
+        return
+
+    elif options.description is not None:
+
+        if len(args) == 0:
+            branch_name = crt_series.get_name()
+        elif len(args) == 1:
+            branch_name = args[0]
+        else:
+            parser.error('incorrect number of arguments')
+        branch = stack.Series(branch_name)
+
+        if not branch.is_initialised():
+            raise CmdException, 'Branch "%s" is not controlled by StGIT' \
+                  % branch_name
+
+        branch.set_description(options.description)
 
 
-        print 'Unprotecting branch "%s"...' % branch
-        stack.Series(branch).unprotect()
         return
 
     elif len(args) == 1:
 
         return
 
     elif len(args) == 1:
 
-        print 'Switching to branch "%s"...' % args[0],
-        sys.stdout.flush()
+        if __is_current_branch(args[0]):
+            raise CmdException, 'Branch "%s" is already the current branch' \
+                  % args[0]
 
 
-        git.switch_branch(args[0])
+        check_local_changes()
+        check_conflicts()
+        check_head_top_equal()
 
 
-        print 'done'
+        out.start('Switching to branch "%s"' % args[0])
+        git.switch_branch(args[0])
+        out.done()
         return
 
     # default action: print the current branch
     if len(args) != 0:
         parser.error('incorrect number of arguments')
 
         return
 
     # default action: print the current branch
     if len(args) != 0:
         parser.error('incorrect number of arguments')
 
-    print git.get_head_file()
+    print crt_series.get_name()