.hgext/hgflow.py

changeset 253
a1accbd675a0
parent 8
ba72a71f23e6
--- a/.hgext/hgflow.py
+++ b/.hgext/hgflow.py
@@ -3,7 +3,7 @@
 # License GPL 2.0
 #
 # hgflow.py - Mercurial extension to support generalized Driessen's branching model
-# Copyright (C) 2011-2012, Yujie Wu
+# Copyright (C) 2011-2014, Yujie Wu and others
 #
 # This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
 # as published by the Free Software Foundation; either version 2 of the License or any later version.
@@ -112,7 +112,7 @@ from mercurial.i18n import _
 
 
 
-VERSION                   = "0.9.7"
+VERSION                   = "0.9.8"
 CONFIG_BASENAME           = ".hgflow"
 OLD_CONFIG_BASENAME       = ".flow"
 CONFIG_SECTION_BRANCHNAME = "branchname"
@@ -315,6 +315,10 @@ class Commands( object ) :
         self._common_opts = {}
         self._opt_mutator = {}
 
+        self.reg_option_mutator( "strip", lambda opts : dict( {"rev" : [],},                                 **opts ) )
+        self.reg_option_mutator( "graft", lambda opts : dict( {"rev" : [], "continue" : False,},             **opts ) )
+        self.reg_option_mutator( "log",   lambda opts : dict( {"date" : None, "user" : None, "rev" : None,}, **opts ) )
+
         
     
     def __getattr__( self, name ) :
@@ -331,6 +335,7 @@ class Commands( object ) :
     def __call__( self, ui, repo, *arg, **kwarg ) :
         """
         Invoke the mercurial command and save it as a string into the history.
+        
         @raise AbortFlow: Throw exception if the return code of hg command (except C{commit} and C{rebase}) is nonzero.
         """
         self.ui = ui
@@ -348,8 +353,8 @@ class Commands( object ) :
 
         kwarg = self._mutate_options( where, self._cmd, kwarg )
 
-        for key, value in kwarg.items() :
-            if (value in [None, ""]) :
+        for key, value in sorted( kwarg.items(), reverse = True ) :
+            if (value in [None, "", False]) :
                 continue
             
             # If the command is `hg commit --message <commit-hint> --force-editor [other-options...]`, we will drop the
@@ -401,8 +406,8 @@ class Commands( object ) :
 
     def _add_quotes( self, value ) :
         """
-        If value is a string that contains space, slash, double quote, and parenthesis (viz: '(' or ')'), then wrap the value
-        with double quotes and properly escape double-quotes and slashes in the string, returning the treated value.
+        If C{value} is a string that contains space, slash, double quote, and parenthesis (viz: '(' or ')'), wrap it with
+        double quotes and properly escape the double-quotes and slashes within, and finally return the modified string.
         Otherwise, return the value as is.        
         """
         if (isinstance( value, str ) and (1 in [c in value for c in " ()\\\""])) :
@@ -444,7 +449,7 @@ class Commands( object ) :
         @type  where: module
         @param where: Should be `mercurial.commands', or a Mercurial's plugin object.
         @type  cmd  : C{str}
-        @param cmd  : command name
+        @param cmd  : Mercurial command name
         """
         ret = {}
         if (self._common_opts != {}) :
@@ -461,7 +466,16 @@ class Commands( object ) :
 
     def _mutate_options( self, where, cmd, opts ) :
         """
-
+        Call the registered command option mutator for the command and return the modified command options.
+        
+        @type  where: module
+        @param where: Should be `mercurial.commands', or a Mercurial's plugin object.
+        @type  cmd  : C{str}
+        @param cmd  : Mercurial command name
+        @type  opts : C{dict}
+        @param opts : Original command options
+        
+        @rtype: C{dict}
         """
         common_opts = self._filter_common_options( where, cmd )
         common_opts.update( opts )
@@ -478,6 +492,10 @@ class Commands( object ) :
         """
         Print the history to the I{quiet} channel, where text will be displayed even when user does not specify the
         C{--verbose} option.
+
+        @type  via_quiet: C{bool}
+        @param via_quiet: To turn off using the "quiet" channel for history printing, you can call this function like:
+                          C{use_quiet_channel( False )}.
         """
         self._via_quiet = via_quiet
 
@@ -486,6 +504,10 @@ class Commands( object ) :
     def use_verbose_channel( self, via_verbose = True ) :
         """
         Print the history to the I{verbose} channel, where text will be display only when user specify the C{--verbose} option.
+
+        @type  via_verbose: C{bool}
+        @param via_verbose: To turn off using the "verbose" channel for history printing (you will be using the "quiet"
+                            channel instead), you can call this function like: C{use_verbose_channel( False )}.
         """
         self._via_quiet = not via_verbose
 
@@ -506,8 +528,12 @@ class Commands( object ) :
         """
         Register common options.
 
-        @type  opts: C{dict}
-        @param opts: Common options. Key = option's flag, value = option's value. 
+        @type  cmd    : C{str}
+        @param cmd    : Mercurial command name
+        @type  mutator: Callable object
+        @param mutator: It will take a C{dict} object as its argument and return another C{dict} object that is supposed to be
+                        a mutant of the former. The input object is supposed to be the original hg-command options in form of
+                        key-value pairs.
         """
         self._opt_mutator[cmd] = mutator
 
@@ -549,7 +575,7 @@ class Stream( object ) :
         get the object.
         
         @type  name : C{str}
-        @param name : Name of the stream
+        @param name : Name of the stream. It can be a complex stream name, e.g., "develop/spring:release".
         @type  check: C{boolean}
         @param check: If true and the stream is not a standard one, the function will check if the trunk of the stream exists
                       or not and (if exists) open or not.
@@ -557,29 +583,57 @@ class Stream( object ) :
         @raise AbortFlow     : When C{check} is true and the trunk of the stream doesn't exist or is closed
         @raise AbnormalStream: When the stream is not in any of the standard streams
         """
+        source = None
+        tokens = name.split( ':' )
+        n      = len( tokens )
+        if (n == 2) :
+            source, name = tokens[0], tokens[1]
+        if (n > 2 or not name) :
+            raise AbortFlow( "Invalid stream syntax: '%s'" % stream )
+
         for e in STREAM.values() :
             if (name == e.name()) :
-                return e
-        
-        rootstream_name = name.split( '/', 1 )[0]
-        is_normalstream = True
-        if (rootstream_name in STREAM) :
-            trunk  = name.replace( rootstream_name + '/', STREAM[rootstream_name].prefix(), 1 )
-            stream = Stream( ui, repo, name, trunk = trunk )
+                if (source) :
+                    stream = copy.copy( e )
+                    break
+                else :
+                    return e
         else :
-            stream = Stream( ui, repo, name, trunk = name )
-            is_normalstream = False
-        if (check) :
-            try :
-                trunk = stream.trunk()
-            except error.RepoLookupError :
-                misspelling = difflib.get_close_matches( stream.name(), STREAM.keys(), 3, 0.7 )
-                note        = "Did you mean: %s?" % " or ".join( misspelling ) if (misspelling) else None
-                raise AbortFlow( "Stream not found: %s" % stream, note = note )
-            if (trunk.is_closed()) :
-                raise AbortFlow( "%s has been closed." % stream )
-        if (not is_normalstream) :
-            raise AbnormalStream( stream = Stream( ui, repo, name ) )
+            rootstream_name = name.split( '/', 1 )[0]
+            is_normalstream = True
+
+            if (rootstream_name in STREAM) :
+                trunk  = name.replace( rootstream_name + '/', STREAM[rootstream_name].prefix(), 1 )
+                stream = Stream( ui, repo, name, trunk = trunk )
+            else :
+                stream = Stream( ui, repo, name, trunk = name )
+                is_normalstream = False
+                
+            if (check) :
+                try :
+                    trunk = stream.trunk()
+                except error.RepoLookupError , e :
+                    misspelling = difflib.get_close_matches( stream.name(), STREAM.keys(), 3, 0.7 )
+                    note        = "Did you mean: %s?" % " or ".join( misspelling ) if (misspelling) else None
+                    raise AbortFlow( "Stream not found: %s" % stream, note = note )
+                if (trunk.is_closed()) :
+                    raise AbortFlow( "%s has been closed." % stream )
+
+        # It seems we never really mind abnormal streams. So comment this out.
+        #if (not is_normalstream) :
+        #    raise AbnormalStream( stream = Stream( ui, repo, name ) )
+
+        if (source) :
+            source = Stream.gen( ui, repo, source, check = True )
+            stream = copy.copy( stream )
+            stream._source = source
+            for i, e in enumerate( stream.destin() ) :
+                if (source in e) :
+                    stream._destin[i] = source
+                    break
+            else :
+                stream._destin = [source]
+
         return stream
 
         
@@ -668,13 +722,13 @@ class Stream( object ) :
 
         @type  trace: C{boolean}
         @param trace: If true and this stream has no trunk, return the trunk of the source stream, and do this recursively
-                      until a trunk is found. If false, this function will return C{None}.
+                      until a trunk is found. If false and this stream has no trunk, this function will return C{None}.
                       
         @return: A C{Branch} object or C{None}
         """
         if (self._tcache) :
             return self._tcache
-        
+
         trunk = Branch( self.ui, self.repo, self._trunk ) if (self._trunk) else None
         if (not trunk and trace) :
             return self.source().trunk( True )
@@ -751,14 +805,18 @@ class Stream( object ) :
         if (openclosed not in ["open", "closed", "all",]) :
             raise ValueError( "Invalid value for `openclosed` parameter: %s" % openclosed )
 
+        all_branches = []
         if (openclosed == "open") :
-            all_branches = [Branch( self.ui, self.repo, head[0] ) for branch_fullname, head in self.repo.branchmap().items()
-                            if (not self.repo[head[0]].extra().get( "close", False ) )]
+            for branch_fullname, heads in self.repo.branchmap().items() :
+                all_branches += [Branch( self.ui, self.repo, head ) for head in heads
+                                 if (not self.repo[head].extra().get( "close", False ))]
         elif (openclosed == "closed") :
-            all_branches = [Branch( self.ui, self.repo, head[0] ) for branch_fullname, head in self.repo.branchmap().items()
-                            if (self.repo[head[0]].extra().get( "close", False ) )]
+            for branch_fullname, heads in self.repo.branchmap().items() :
+                all_branches += [Branch( self.ui, self.repo, head ) for head in heads
+                                 if (self.repo[head].extra().get( "close", False ))]
         else :
-            all_branches = [Branch( self.ui, self.repo, head[0] ) for branch_fullname, head in self.repo.branchmap().items()]
+            for branch_fullname, heads in self.repo.branchmap().items() :
+                all_branches += [Branch( self.ui, self.repo, head ) for head in heads]
 
         return sorted( [e for e in all_branches if (e in self)] )
         
@@ -811,8 +869,10 @@ class Branch( object ) :
         not contain any '/'s).
         Return the string "trunk" if this branch is the trunk of the C{stream}.
 
-        @type  stream: C{Stream} or C{None}
-        @param stream: Stream to which the basename is relative
+        @type        stream: C{Stream} or C{None}
+        @param       stream: Stream to which the basename is relative
+        @type  should_quote: C{bool}
+        @param should_quote: The returned string will be wrapped with single quotes ' if this parameter's value is true.
         """
         if (stream) :
             if (self._fullname == stream._trunk) :
@@ -823,7 +883,16 @@ class Branch( object ) :
         if (should_quote) :
             ret = "'%s'" % ret
         return ret
+
+
     
+    def rev_node( self ) :
+        """
+        Return a string showing this branch's head's revision number and short node ID in the format of "<rev>:<node-ID>",
+        e.g., "9:db14bf692069".
+        """
+        return "%s:%s" % (self.ctx.rev(), short( self.ctx.node() ),)
+
 
 
     def is_closed( self ) :
@@ -891,6 +960,9 @@ STREAM   = {}           # key = stream n
 
 
 class Flow( object ) :
+
+    ACTION_NAME = ["start", "finish", "push", "publish", "pull", "list", "log", "abort", "promote", "rebase", "rename",]
+
     def __init__( self, ui, repo, init = False ) :
         """
         Construct a C{Flow} instance that will execute the workflow.
@@ -946,7 +1018,8 @@ class Flow( object ) :
                 except Exception, e :
                     self._error( str( e ) )
                     self._error( "Flow has not been initialized properly for this repository." )
-                    self._note ( "You can use command `hg flow init -f` to reinitialize for this repository.", via_quiet = True )
+                    self._note ( "You can use command `hg flow init -f` to reinitialize for this repository.",
+                                 via_quiet = True )
                     sys.exit( 1 )
             else :
                 self._error( "Flow has not been initialized for this repository: %s file is missing." % CONFIG_BASENAME )
@@ -1142,7 +1215,7 @@ class Flow( object ) :
         where <patchname> follows the pattern: flow/<branch_fullname>.pch, which was previously created by flow's shelving.
 
         @type  basename: C{str}
-        @param basename: Base name of the shelved patch file. Default is the name of current workspace branch.
+        @param basename: Basename of the path of the shelved patch file. Default is the name of current workspace branch.
         """
         if (self.autoshelve or kwarg.get( "force" )) :
             basename    = basename if (basename) else self.curr_workspace.fullname()
@@ -1175,30 +1248,37 @@ class Flow( object ) :
         """
         if (openclosed not in ["open", "closed", "all",]) :
             raise ValueError( "Invalid value for openclosed parameter: %s" % openclosed )
+
+        all_branches = []
         if (openclosed == "open") :
-            all_branches = [Branch( self.ui, self.repo, head[0] ) for branch_fullname, head in self.repo.branchmap().items()
-                            if (not self.repo[head[0]].extra().get( "close", False ) )]
+            for branch_fullname, heads in self.repo.branchmap().items() :
+                all_branches += [Branch( self.ui, self.repo, head ) for head in heads
+                                 if (not self.repo[head].extra().get( "close", False ))]
         elif (openclosed == "closed") :
-            all_branches = [Branch( self.ui, self.repo, head[0] ) for branch_fullname, head in self.repo.branchmap().items()
-                            if (self.repo[head[0]].extra().get( "close", False ))]
+            for branch_fullname, heads in self.repo.branchmap().items() :
+                all_branches += [Branch( self.ui, self.repo, head ) for head in heads
+                                 if (self.repo[head].extra().get( "close", False ))]
         else :
-            all_branches = [Branch( self.ui, self.repo, head[0] ) for branch_fullname, head in self.repo.branchmap().items()]
+            for branch_fullname, heads in self.repo.branchmap().items() :
+                all_branches += [Branch( self.ui, self.repo, head ) for head in heads]
+
         return all_branches
 
 
 
     def _find_branch( self, fullname ) :
         """
-        Return true if a branch C{fullname} is open.
+        Try to find a branch of name: C{fullname}. If it exists, return a C{Branch} object of this branch and a boolean value
+        indicating if it's open (C{True}) or closed (C{False}). If it does not exists, return C{(None, None)}.
 
         @type  fullname: C{str}
-        @param fullname: Fullname of branch that you want to know whether it is open
+        @param fullname: Fullname of the branch to find
         """
         try :
-            Branch( self.ui, self.repo, fullname )
-            return True
+            branch = Branch( self.ui, self.repo, fullname )
+            return branch, branch.is_open()
         except error.RepoLookupError :
-            return False
+            return None, None
 
     
     
@@ -1263,12 +1343,15 @@ class Flow( object ) :
         except IndexError :
             raise AbortFlow( "You must specify a name for the new branch to start." )
             
-        rev      = kwarg.pop( "rev",     None )
-        msg      = kwarg.pop( "message", ""   )
-        dirty    = kwarg.pop( "dirty",   None )
-        fullname = stream.get_fullname( basename )
-        if (self._find_branch( fullname )) :
-            self._warn( "An open branch named '%s' already exists in %s." % (basename, stream,) )
+        rev         = kwarg.pop( "rev",     None )
+        msg         = kwarg.pop( "message", ""   )
+        dirty       = kwarg.pop( "dirty",   None )
+        fullname    = stream.get_fullname( basename )
+        br, is_open = self._find_branch( fullname )
+        if (br) :
+            self._error( "A branch named '%s' already exists in %s: '%s'." % (basename, stream, fullname,) )
+            if (not is_open) :
+                self._note( "Branch '%s' is currently closed." % fullname, via_quiet = True )
         else :
             shelvedpatch_basename = self.curr_workspace.fullname()
             if (rev is None) :
@@ -1343,8 +1426,9 @@ class Flow( object ) :
         if (open_branches) :
             self._print( "Open %s branches:" % stream )
             for e in open_branches :
-                marker  = "#" if (self._is_shelved( e )  ) else ""
+                marker  = "#" if (self._is_shelved( e )   ) else ""
                 marker += "*" if (e == self.curr_workspace) else ""
+                marker += "  %s" % e.rev_node()
                 self._print( str( e ) + marker, prefix = "  " )
         else :
             self._print( "No open %s branches" % stream )
@@ -1354,8 +1438,8 @@ class Flow( object ) :
                 self._print( "Closed %s branches:" % stream )
                 closed_branches.sort( lambda x, y : y.ctx.rev() - x.ctx.rev() )
                 for e in closed_branches :
-                    self.ui.write( "%-31s"   % e.basename( stream ),                  label = "branches.closed" )
-                    self.ui.write( " %5s:%s" % (e.ctx.rev(), short( e.ctx.node() ),), label = "log.changeset"   )
+                    self.ui.write( "%-31s" % e.basename( stream ), label = "branches.closed" )
+                    self.ui.write( " %18s" % e.rev_node(),         label = "log.changeset"   )
                     self.ui.write( "  %s\n"  % util.datestr( e.ctx.date(), format = "%Y-%m-%d %a %H:%M %1" ),
                                    label = "log.date" )
                     bn = str( e )
@@ -1450,8 +1534,7 @@ class Flow( object ) :
             # At this point, `branch` must be existent.
             branches = [branch,]
 
-        # OK. We have to explicitly specify the date, rev, and user arguments to prevent mercurial python APIs from crashing.
-        opts = {"date" : None, "rev" : None, "user" : None, "branch" : branches,}
+        opts = {"branch" : branches,}
         opts.update( kwarg )
         self._log( *filenames, **opts )
 
@@ -1464,6 +1547,7 @@ class Flow( object ) :
         @type  stream: C{Stream}
         @param stream: Stream where the branch which you want to abort is
         """
+        arg            = arg[1:]
         msg            = kwarg.pop( "message",  ""    )
         should_erase   = kwarg.pop( "erase",    False )
         onstream       = kwarg.pop( "onstream", False )
@@ -1472,6 +1556,9 @@ class Flow( object ) :
             msg = "%s\n" % msg
         if (curr_workspace.is_develop_trunk()) :
             raise AbortFlow( "You cannot abort the <develop> trunk." )
+        if (arg) :
+            if (len( arg ) > 1 or curr_workspace.basename() != arg[0]) :
+                raise AbortFlow( "hgflow intentionally forbids aborting a non-workspace branch." )
         if (onstream) :
             branches = stream.branches()
             if (stream == STREAM["develop"]) :
@@ -1480,7 +1567,7 @@ class Flow( object ) :
                 branches.append( stream.trunk() )
             for branch in branches :
                 if (should_erase) :
-                    self._strip( rev = ["branch('%s')" % branch,] )
+                    self._strip( branch, self.repo.revs( "min(branch('%s'))" % branch )[0] )
                 else :
                     self._update( branch )
                     self._commit( close_branch = True, message = "%s%sAborted %s %s." %
@@ -1497,7 +1584,7 @@ class Flow( object ) :
                 raise AbortFlow( "Your workspace is '%s' branch, which is not in %s." % (curr_workspace, stream,),
                                  "To abort a %s branch, you must first update to it." % stream )
             if (should_erase) :
-                self._strip( rev = ["branch('%s')" % curr_workspace,] )
+                self._strip( curr_workspace, self.repo.revs( "min(branch('%s'))" % curr_workspace )[0] )
             else :
                 self._commit( close_branch = True, message = "%s%sAborted %s '%s'." %
                               (msg, self.msg_prefix, stream, curr_workspace.basename( stream ),) )
@@ -1513,8 +1600,8 @@ class Flow( object ) :
 
         @type  stream: C{Stream}
         @param stream: Stream where the branch which you want to rebase is
-        @type     rev: C{str}
-        @param    rev: If provided, promote this revision instead of the head. The specified revision must be in the workspace
+        @type  rev   : C{str}
+        @param rev   : If provided, promote this revision instead of the head. The specified revision must be in the workspace
                        branch.
         """
         rev            = kwarg.pop( "rev",     None )
@@ -1590,8 +1677,8 @@ class Flow( object ) :
 
         @type  stream: C{Stream}
         @param stream: Stream where the branch which you want to rebase is
-        @type  dest:   C{str}
-        @param dest:   If provided, use its value as the destination of rebasing. The value must be a changeset of the parent
+        @type  dest  : C{str}
+        @param dest  : If provided, use its value as the destination of rebasing. The value must be a changeset of the parent
                        branch, otherwise it will trigger an error. If not provided, use the tip of the parent branch as the
                        destination of rebasing.
         """
@@ -1627,6 +1714,46 @@ class Flow( object ) :
                 self._shelve()
                 self._rebase( base = curr_workspace, dest = dest, keepbranches = True )
                 self._unshelve()
+
+
+
+    def _action_rename( self, stream, *arg, **kwarg ) :
+        """
+        Rename the workspace branch to a new basename. If there are uncommitted changes in the current branch, they will be
+        automatically shelved before renaming and unshelved afterwards.
+        Under the hood this action will create a new branch and copy (or graft) all commits in the workspace branch to the new
+        branch and then erase the workspace branch.
+
+        @type  stream: C{Stream}
+        @param stream: Stream where the branch which you want to rename is
+        @type  to    : C{str}
+        @param to    : Its value should be the new basename of the workspace branch.
+        """
+        new_branch_name = kwarg.pop( "to", None )
+        curr_workspace  = self.curr_workspace
+        if (not new_branch_name) :
+            raise AbortFlow( "Please specify the new base name of this branch via the `-t` option." )
+        if (curr_workspace not in stream) :
+            raise AbortFlow( "Your workspace is '%s' branch, which is not in %s." % (curr_workspace, stream,),
+                             "To rename a %s branch, you must first update to it." % stream )
+        if (curr_workspace.is_trunk( stream )) :
+            raise AbortFlow( "You cannot rename the trunk of %s." % stream )
+        if (new_branch_name == curr_workspace.basename( stream )) :
+            self._warn( "No effects because the supposed new basename turns out to be the same as the current one." )
+        else :
+            cfn = curr_workspace.fullname()
+            brn = "branch(%s)" % cfn
+            rev = "min(%s)" % brn
+            ctx = self.repo[self.repo.revs( rev )[0]]
+            nfn = stream.get_fullname( new_branch_name )
+            msg = ctx.description()
+            msg = msg.replace( cfn, nfn )
+            self._shelve()
+            self._update( self.repo.revs( "%s^" % rev )[0] )
+            self._create_branch( nfn, msg, user = ctx.user(), date = util.datestr( ctx.date() ) )
+            self._graft( curr_workspace, **kwarg )
+            self._unshelve( cfn )
+            self._strip( curr_workspace, int( ctx ) )
     
     
     
@@ -1650,6 +1777,7 @@ class Flow( object ) :
             self._shelve()
             self._update( branch )
             self._unshelve()
+        self._print( "Parent of working directory: %s" % branch.rev_node() )
 
 
     
@@ -1668,12 +1796,12 @@ class Flow( object ) :
                 self._warn( "%s '%s' has been closed." % (stream, name,) )
             self._update_workspace( stream, branch )
         except error.RepoLookupError :
-            misspelling = difflib.get_close_matches( name, ["start", "finish", "push", "publish", "pull",
-                                                            "list", "log", "abort", "promote", "rebase",], 3, 0.7 )
+            misspelling = difflib.get_close_matches( name, Flow.ACTION_NAME, 3, 0.7 )
             note = ("Did you mean: %s?" % " or ".join( misspelling )) if (misspelling    ) else None
             note = ("Did you mean: finish or abort?")                 if ("close" == name) else note
-            note = ("If you meant to create a new branch called '%s' in %s" % (name, stream,),
-                    "try command:", "  hg flow %s start %s" % (stream.name(), name,),) if (not note) else note
+            if (stream != STREAM["master"]) :
+                note = ("If you meant to create a new branch called '%s' in %s" % (name, stream,),
+                        "try command:", "  hg flow %s start %s" % (stream.name(), name,),) if (not note) else note
             raise AbortFlow( "Invalid action or unknown branch in %s: '%s'" % (stream, name,), note = note )
 
             
@@ -1740,6 +1868,7 @@ class Flow( object ) :
         curr_workspace = self.curr_workspace
         curr_stream    = curr_workspace.stream()
         name           = curr_workspace.basename( stream, should_quote = True )
+        tag_name_orig  = tag_name
         tag_name       = tag_name if (tag_name) else (self.version_prefix + name[1:-1])
         develop_stream = STREAM["develop"]
 
@@ -1797,14 +1926,14 @@ class Flow( object ) :
             raise AbortFlow( "Cannot finish '%s' branch because it has uncommitted changes." % curr_workspace )
         
         # For destin streams without trunks, we need to create a branch in each of these destin streams.
-        # Each newly created branch will be named after the pattern: <stream-prefix>/<current-branch-basename> and will be from
-        # the current branch. The current branch will be closed. Note that there is no need to merge the current branch because
-        # a new branch has been created from it.
+        # Each newly created branch will be from the current branch and named after the pattern:
+        # <stream-prefix>/<current-branch-basename>. Afterwards, the current branch will be closed.
+        # Note that there is no need to merge the current branch because the new branch is created from it.
         for s in destin_without_trunk :
             trunk    = s.trunk()
             so_name  = "" if ("trunk" == curr_workspace.basename( stream )) else ("/" + curr_workspace.basename( stream ))
-            so       = Stream( self.ui, self.repo, stream.name() + so_name, trunk = curr_workspace.fullname() )
-            so       = "%s:%s" % (so.name(), s.name(),)
+            so       = Stream    ( self.ui, self.repo, stream.name() + so_name, trunk = curr_workspace.fullname() )
+            so       = Stream.gen( self.ui, self.repo, "%s:%s" % (so.name(), s.name(),), check = True )
             basename = curr_workspace.basename()
             self.action( so, "start", basename )
             final_branch = s.get_fullname( basename )
@@ -1821,13 +1950,17 @@ class Flow( object ) :
                 final_branch = STREAM["master"].trunk()
             else :
                 self._print( "All open branches in %s are finished and merged to its trunk." % stream )
+
+        if (tag_name_orig and (STREAM["master"] not in destin_with_trunk)) :
+            self._warn( "You specified a tag name, but it has effect only when the workspace branch is merged to <master>." )
+                
         for s in destin_with_trunk :
             trunk = s.trunk()
             self._update( trunk          )
             self._merge ( curr_workspace )
             self._commit( message = "%sMerged %s %s to %s ('%s')." % (self.msg_prefix, stream, name, s, trunk,), **kwarg )
             if (s == STREAM["master"]) :
-                self._tag( tag_name )
+                self._tag( tag_name, force = True )
             elif (s in develop_stream and s is not develop_stream) :
                 tr_stream = trunk.stream()
                 for ss in tr_stream.destin() :
@@ -1846,7 +1979,7 @@ class Flow( object ) :
             self._update( "tip" )
             self._update( rev   )
             self._revert( rev = "-1", all = True )
-            self._strip ( rev = ["branch('%s')" % curr_workspace,] )
+            self._strip ( curr_workspace, self.repo.revs( "min(branch('%s'))" % curr_workspace )[0] )
             self._commit( message = message, **kwarg )
         self._unshelve()
             
@@ -1880,6 +2013,7 @@ class Flow( object ) :
             "abort"   : self._action_abort,
             "promote" : self._action_promote,
             "rebase"  : self._action_rebase,
+            "rename"  : self._action_rename,
             "other"   : self._action_other,
         }
 
@@ -1893,6 +2027,9 @@ class Flow( object ) :
     def action( self, stream, *arg, **kwarg ) :
         """
         Execute action on the stream.
+
+        @type  stream: C{Stream}
+        @param stream: Stream where we will execute the action
         """
         if (len( arg ) > 0) :
             action = arg[0]
@@ -1900,7 +2037,8 @@ class Flow( object ) :
                 if (action in ["start", "finish", "abort", "rebase",]) :
                     raise AbortFlow( "Invalid action for <master>" )
         else :
-            self._update_workspace( stream, stream.trunk(), verbose = False )
+            trunk = stream.trunk()
+            self._update_workspace( stream, trunk, verbose = False )
         self._execute_action( stream, *arg, **kwarg )
         
     
@@ -1933,17 +2071,18 @@ class Flow( object ) :
         curr_workspace = self.curr_workspace
         stream_names   = ["master", "develop", "feature", "release", "hotfix", "support",]
         all_branches   = self._branches()
+        name_branches  = {}
+        for branch in all_branches :
+            name_branches.setdefault( branch.fullname(), [] ).append( branch )
+        name_branches = sorted( name_branches.items() )
         for sn in stream_names :
             stream = STREAM[sn]
             trunk  = stream.trunk()
             open_branches_in_stream = []
-            remaining_branches      = []
-            for e in all_branches :
+            for name, heads in name_branches :
+                e = heads[0]
                 if (e in stream) :
                     open_branches_in_stream.append( e )
-                else :
-                    remaining_branches.append( e )
-            all_branches = remaining_branches
             if (trunk is None and not open_branches_in_stream) :
                 continue
             self._print( "%-9s: " % stream, newline = False )
@@ -1960,7 +2099,15 @@ class Flow( object ) :
                     marker += "*" if (e == curr_workspace  ) else ""
                     self.ui.write( "%s%s " % (e, marker,) )
             self.ui.write( "\n" )
-
+        if (sum( [len( heads ) - 1 for name, heads in name_branches] )) :
+            self._print( "\n", newline = False )
+            self._print( "Multihead branches:" )
+            for name, heads in name_branches :
+                if (len( heads ) > 1) :
+                    self._print( "  %s" % name )
+                    for head in heads :
+                        self._print( "    %s" % head.rev_node() )
+            
     
      
     def init( self, *arg, **kwarg ) :
@@ -2037,7 +2184,7 @@ class Flow( object ) :
                 else :
                     self._warn( "  %s" % branch.fullname() )
             print
-            
+
         # 'status' method returns a 7-member tuple:
         # 0 modified, 1 added, 2 removed, 3 deleted, 4 unknown(?), 5 ignored, and 6 clean
         orig_repo_status = self.repo.status()[:4]
@@ -2100,8 +2247,9 @@ Here is what you need to do:
                         "support = %s" % support_stream,]
         def write_config() :
             # Writes the configuration in the current branch.
-            with open( config_fname, "w" ) as fh :
-                print >> fh, "\n".join( cfg_contents )
+            if (not commands.dryrun()) :
+                with open( config_fname, "w" ) as fh :
+                    print >> fh, "\n".join( cfg_contents )
             repo_status = self.repo.status( unknown = True )
             if (CONFIG_BASENAME in repo_status[0]) :
                 self._commit( config_fname, message = "flow initialization: Modified configuration file." )
@@ -2110,7 +2258,19 @@ Here is what you need to do:
                 self._commit( config_fname, message = "flow initialization: Added configuration file." )
 
         write_config()
-        
+
+        master_trunk, is_open = self._find_branch( master_stream )
+        if (master_trunk and not is_open) :
+            self._warn( "Branch \"%s\" is currently closed." % master_stream )
+            self._warn( "Will reopen and use it as <master> trunk."          )
+            branches.append( master_trunk )
+            
+        develop_trunk, is_open = self._find_branch( develop_stream )
+        if (develop_trunk and not is_open) :
+            self._warn( "Branch \"%s\" is currently closed." % develop_stream )
+            self._warn( "Will reopen and use it as <develop> trunk."          )
+            branches.append( develop_trunk )
+            
         # Writes the configuration in all the other branches.
         self.autoshelve = True
         self._shelve()
@@ -2123,14 +2283,15 @@ Here is what you need to do:
             self._update( workspace )
     
         # Creates 'master' and 'develop' streams if they don't yet exist.
-        if (not self._find_branch( master_stream )) :
+        if (master_trunk is None) :
             self._create_branch( master_stream, "flow initialization: Created <master> trunk: %s." % master_stream )
-        if (not self._find_branch( develop_stream )) :
+        if (develop_trunk is None) :
             self._create_branch( develop_stream, "flow initialization: Created <develop> trunk: %s." % develop_stream )
+            
         self._update( workspace )
         self._unshelve()
 
-    
+
 
     def upgrade( self, *arg, **kwarg ) :
         """
@@ -2167,6 +2328,7 @@ actions:
 - log      Show revision history of branch.
 - promote  Merge workspace to other branches. (not closing any branches.)
 - rebase   Rebase workspace branch to its parent branch.
+- rename   Rename workspace branch to a new basename.
 - abort    Abort branch. Close branch without merging.
 
 If no action is specified by user, the action will default to `list`. If a
@@ -2220,31 +2382,8 @@ commands:
         # Constructs a `Stream' objects.
         # This will also check the validity of the part of user's input that is supposed to specify a stream.
         if (isinstance( stream, str )) :
-            source = None
-            tokens = stream.split( ':' )
-            n      = len( tokens )
-            if (n == 2) :
-                source, stream = tokens[0], tokens[1]
-            if (n > 2 or not stream) :
-                raise AbortFlow( "Invalid stream syntax: '%s'" % stream )
-            try :
-                stream = Stream.gen( ui, repo, stream, check = True )
-            except AbnormalStream, e :
-                stream = e.stream()
-            if (source) :
-                try :
-                    source = Stream.gen( ui, repo, source, check = True )
-                except AbnormalStream, e :
-                    source = e.stream()
-                stream = copy.copy( stream )
-                stream._source = source
-                for i, e in enumerate( stream.destin() ) :
-                    if (source in e ) :
-                        stream._destin[i] = source
-                        break
-                else :
-                    stream._destin = [source]
-    
+            stream = Stream.gen( ui, repo, stream, check = True )
+            
         # Checks the options for all commands and actions.
         kwarg = _getopt( ui, cmd, kwarg )
         stamp = kwarg.pop( "stamp", None )
@@ -2264,6 +2403,14 @@ commands:
         _error( ui, *errmsg )
         if (getattr( e, "note", None )) :
             _note( ui, *((e.note,) if (isinstance( e.note, str )) else e.note), via_quiet = True )
+        elif (errmsg[0].startswith( "Stream not found" )) :
+            misspelling = difflib.get_close_matches( stream, ["init", "upgrade", "unshelve", "help", "version",], 3, 0.7 )
+            note = ("Did you mean the command: %s?" % " or ".join( misspelling )) if (misspelling        ) else None
+            note = ("Did you mean the command: init?")                            if ("install" == stream) else note
+            note = ("Did you mean the command: upgrade?")                         if ("update"  == stream) else note
+            if (note) :
+                _note( ui, note, via_quiet = True )
+            
         if (ui.tracebackflag) :
             if (hasattr( e, "traceback" )) :
                 ei = e.traceback
@@ -2310,6 +2457,10 @@ The following item has been deprecated i
 the future:
   * [hgflow]   The '[hgflow]' section name in hg's configuration file has been
                renamed to '[flow]'.
+  * Syntax: hg flow <stream> finish {{{<tag-name>}}}
+               Replacement syntax: hg flow <stream> finish {{{-t <tag-name>}}}
+               Any positional arguments for `finish` will be considered as
+               error.
 """,
 
 "@examples" : """
@@ -2620,6 +2771,34 @@ Show version of the flow extension.
 syntax:
 {{{hg flow version}}}
 """,
+        
+"@rename" : """
+Rename the workspace branch to a new basename. Under the hood, this action
+will create a new branch with the new basename and copy/graft the commits in
+the workspace branch to the new branch, and then erase the workspace branch.
+All the user, date, and commit-message information will be copied to the
+new branch.
+
+N.B.: The workspace branch should be a simple linear branch. This means:
+(1) It has not merged to other branches;
+(2) It has no subbranches;
+(3) No other branches has merged to this branch.
+
+syntax:
+{{{hg flow <stream> rename [-t <basename>]}}}
+
+option:
+ -t --to NAME  Rename the basename of the workspace branch to NAME.
+
+The workspace branch must be in <stream>.
+""",
+        
+"@version" : """
+Show version of the flow extension.
+
+syntax:
+{{{hg flow version}}}
+""",
 
 "@init" : """
 Initialize the flow extension for the repository. The configuration file:
@@ -2803,7 +2982,7 @@ available for the following topics:
   @<stream>   - Show help about a particular stream, e.g., @feature, @master.
   @<action>   - Show help about an action, e.g., @finish, @log.
   @<command>  - Show help about a command, e.g., @help, @unshelve.
-  @<option>   - Show help about an option, e.g., @-F, @--history.
+  @terms      - Show explanations of terminologies used in hgflow.
   @examples   - Show a few command examples.
   @deprecated - Show a list of deprecated features.
 """ )
@@ -2820,6 +2999,7 @@ OPT_FILTER = {
 "abort"   : ("erase", "message", "stamp", "onstream",),
 "promote" : ("rev", "message", "stamp", "tag", "date", "user", "onstream",),
 "rebase"  : ("dest", "onstream", "stamp",),
+"rename"  : ("to",),
 }
 
 OPT_CONFLICT = {
@@ -2830,6 +3010,8 @@ OPT_CONFLICT = {
 "commit"  : ("-c", False,),
 "stamp"   : ("-p", ''    ),
 "patch"   : ("-p", False ),
+"tag"     : ("-t", ''    ),
+"to"      : ("-t", ''    ),
 }
 
 def _getopt( ui, key, opt ) :
@@ -2884,10 +3066,13 @@ def _getopt( ui, key, opt ) :
             raise AbortFlow( "Unrecognized option%s for `hg flow`: %s." %
                              ("" if (len( bad_opt ) == 1) else "s", "--" + (", --".join( bad_opt )),),
                              note = "`hg flow` should take no options." )
-        else :
+        elif (key in Flow.ACTION_NAME) :
             raise AbortFlow( "Unrecognized option%s for `%s`: %s." %
                              ("" if (len( bad_opt ) == 1) else "s", key, "--" + (", --".join( bad_opt )),),
                              note = "Execute `hg flow help @%s` to see available options for `%s`." % (key, key,) )
+        else :
+            raise AbortFlow( "Unrecognized option%s: %s." %
+                             ("" if (len( bad_opt ) == 1) else "s", "--" + (", --".join( bad_opt )),) )
             
     return ret
 
@@ -2921,6 +3106,7 @@ cmdtable = {
       ("s", "onstream",  False, _("Act on stream. [finish, rebase, log, abort]"),                                  ),
       ("t", "tag",       '',    _("Tag the merging changeset with NAME. [promote]"),                     _('NAME'),),
       ("t", "tag",       '',    _("Tag the <master> trunk with NAME after merging. [finish]"),           _('NAME'),),
+      ("t", "to",        '',    _("Rename the branch to NAME. [rename]"),                                _('NAME'),),
       ("u", "user",      '',    _("Use specified user as committer. [init, upgrade, start, finish,"
                                   " promote]"),                                                          _('USER'),),
       ("u", "user",      '',    _("Show revisions committed by specified user. [log]"),                  _('USER'),),

mercurial