chiark / gitweb /
Allow "branch --create" to be given a tag id
[stgit] / stgit / stack.py
index 3185d6450b933aef277d8b86c9156433941978e3..f72f83b0bd4eb3711da5a98b204ae0d5facb55bf 100644 (file)
@@ -19,14 +19,19 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os, re
 """
 
 import sys, os, re
+from email.Utils import formatdate
 
 
+from stgit.exception import *
 from stgit.utils import *
 from stgit.utils import *
+from stgit.out import *
+from stgit.run import *
 from stgit import git, basedir, templates
 from stgit.config import config
 from stgit import git, basedir, templates
 from stgit.config import config
+from shutil import copyfile
 
 
 # stack exception class
 
 
 # stack exception class
-class StackException(Exception):
+class StackException(StgException):
     pass
 
 class FilterUntil:
     pass
 
 class FilterUntil:
@@ -64,6 +69,8 @@ def __clean_comments(f):
     f.seek(0); f.truncate()
     f.writelines(lines)
 
     f.seek(0); f.truncate()
     f.writelines(lines)
 
+# TODO: move this out of the stgit.stack module, it is really for
+# higher level commands to handle the user interaction
 def edit_file(series, line, comment, show_patch = True):
     fname = '.stgitmsg.txt'
     tmpl = templates.get_template('patchdescr.tmpl')
 def edit_file(series, line, comment, show_patch = True):
     fname = '.stgitmsg.txt'
     tmpl = templates.get_template('patchdescr.tmpl')
@@ -85,25 +92,14 @@ def edit_file(series, line, comment, show_patch = True):
     if show_patch:
        print >> f, __patch_prefix
        # series.get_patch(series.get_current()).get_top()
     if show_patch:
        print >> f, __patch_prefix
        # series.get_patch(series.get_current()).get_top()
-       git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
+       diff_str = git.diff(rev1 = series.get_patch(series.get_current()).get_bottom())
+       f.write(diff_str)
 
     #Vim modeline must be near the end.
     print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
     f.close()
 
 
     #Vim modeline must be near the end.
     print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
     f.close()
 
-    # the editor
-    editor = config.get('stgit.editor')
-    if editor:
-        pass
-    elif 'EDITOR' in os.environ:
-        editor = os.environ['EDITOR']
-    else:
-        editor = 'vi'
-    editor += ' %s' % fname
-
-    print 'Invoking the editor: "%s"...' % editor,
-    sys.stdout.flush()
-    print 'done (exit code: %d)' % os.system(editor)
+    call_editor(fname)
 
     f = file(fname, 'r+')
 
 
     f = file(fname, 'r+')
 
@@ -149,18 +145,20 @@ class StgitObject:
         elif os.path.isfile(fname):
             os.remove(fname)
 
         elif os.path.isfile(fname):
             os.remove(fname)
 
-    
+
 class Patch(StgitObject):
     """Basic patch implementation
     """
 class Patch(StgitObject):
     """Basic patch implementation
     """
-    def __init__(self, name, series_dir, refs_dir):
+    def __init_refs(self):
+        self.__top_ref = self.__refs_base + '/' + self.__name
+        self.__log_ref = self.__top_ref + '.log'
+
+    def __init__(self, name, series_dir, refs_base):
         self.__series_dir = series_dir
         self.__name = name
         self._set_dir(os.path.join(self.__series_dir, self.__name))
         self.__series_dir = series_dir
         self.__name = name
         self._set_dir(os.path.join(self.__series_dir, self.__name))
-        self.__refs_dir = refs_dir
-        self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
-        self.__log_ref_file = os.path.join(self.__refs_dir,
-                                           self.__name + '.log')
+        self.__refs_base = refs_base
+        self.__init_refs()
 
     def create(self):
         os.mkdir(self._dir())
 
     def create(self):
         os.mkdir(self._dir())
@@ -168,36 +166,41 @@ class Patch(StgitObject):
         self.create_empty_field('top')
 
     def delete(self):
         self.create_empty_field('top')
 
     def delete(self):
-        for f in os.listdir(self._dir()):
-            os.remove(os.path.join(self._dir(), f))
-        os.rmdir(self._dir())
-        os.remove(self.__top_ref_file)
-        if os.path.exists(self.__log_ref_file):
-            os.remove(self.__log_ref_file)
+        if os.path.isdir(self._dir()):
+            for f in os.listdir(self._dir()):
+                os.remove(os.path.join(self._dir(), f))
+            os.rmdir(self._dir())
+        else:
+            out.warn('Patch directory "%s" does not exist' % self._dir())
+        try:
+            # the reference might not exist if the repository was corrupted
+            git.delete_ref(self.__top_ref)
+        except git.GitException, e:
+            out.warn(str(e))
+        if git.ref_exists(self.__log_ref):
+            git.delete_ref(self.__log_ref)
 
     def get_name(self):
         return self.__name
 
     def rename(self, newname):
         olddir = self._dir()
 
     def get_name(self):
         return self.__name
 
     def rename(self, newname):
         olddir = self._dir()
-        old_top_ref_file = self.__top_ref_file
-        old_log_ref_file = self.__log_ref_file
+        old_top_ref = self.__top_ref
+        old_log_ref = self.__log_ref
         self.__name = newname
         self._set_dir(os.path.join(self.__series_dir, self.__name))
         self.__name = newname
         self._set_dir(os.path.join(self.__series_dir, self.__name))
-        self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
-        self.__log_ref_file = os.path.join(self.__refs_dir,
-                                           self.__name + '.log')
+        self.__init_refs()
 
 
+        git.rename_ref(old_top_ref, self.__top_ref)
+        if git.ref_exists(old_log_ref):
+            git.rename_ref(old_log_ref, self.__log_ref)
         os.rename(olddir, self._dir())
         os.rename(olddir, self._dir())
-        os.rename(old_top_ref_file, self.__top_ref_file)
-        if os.path.exists(old_log_ref_file):
-            os.rename(old_log_ref_file, self.__log_ref_file)
 
     def __update_top_ref(self, ref):
 
     def __update_top_ref(self, ref):
-        write_string(self.__top_ref_file, ref)
+        git.set_ref(self.__top_ref, ref)
 
     def __update_log_ref(self, ref):
 
     def __update_log_ref(self, ref):
-        write_string(self.__log_ref_file, ref)
+        git.set_ref(self.__log_ref, ref)
 
     def update_top_ref(self):
         top = self.get_top()
 
     def update_top_ref(self):
         top = self.get_top()
@@ -260,7 +263,16 @@ class Patch(StgitObject):
         self._set_field('authemail', email or git.author().email)
 
     def get_authdate(self):
         self._set_field('authemail', email or git.author().email)
 
     def get_authdate(self):
-        return self._get_field('authdate')
+        date = self._get_field('authdate')
+        if not date:
+            return date
+
+        if re.match('[0-9]+\s+[+-][0-9]+', date):
+            # Unix time (seconds) + time zone
+            secs_tz = date.split()
+            date = formatdate(int(secs_tz[0]))[:-5] + secs_tz[1]
+
+        return date
 
     def set_authdate(self, date):
         self._set_field('authdate', date or git.author().date)
 
     def set_authdate(self, date):
         self._set_field('authdate', date or git.author().date)
@@ -284,49 +296,188 @@ class Patch(StgitObject):
         self._set_field('log', value)
         self.__update_log_ref(value)
 
         self._set_field('log', value)
         self.__update_log_ref(value)
 
+# The current StGIT metadata format version.
+FORMAT_VERSION = 2
 
 
-class Series(StgitObject):
-    """Class including the operations on series
-    """
+class PatchSet(StgitObject):
     def __init__(self, name = None):
     def __init__(self, name = None):
-        """Takes a series name as the parameter.
-        """
         try:
             if name:
         try:
             if name:
-                self.__name = name
+                self.set_name (name)
             else:
             else:
-                self.__name = git.get_head_file()
+                self.set_name (git.get_head_file())
             self.__base_dir = basedir.get()
         except git.GitException, ex:
             raise StackException, 'GIT tree not initialised: %s' % ex
 
             self.__base_dir = basedir.get()
         except git.GitException, ex:
             raise StackException, 'GIT tree not initialised: %s' % ex
 
-        self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
-        self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
-                                       self.__name)
-        self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
-                                        self.__name)
+        self._set_dir(os.path.join(self.__base_dir, 'patches', self.get_name()))
+
+    def get_name(self):
+        return self.__name
+    def set_name(self, name):
+        self.__name = name
+
+    def _basedir(self):
+        return self.__base_dir
+
+    def get_head(self):
+        """Return the head of the branch
+        """
+        crt = self.get_current_patch()
+        if crt:
+            return crt.get_top()
+        else:
+            return self.get_base()
+
+    def get_protected(self):
+        return os.path.isfile(os.path.join(self._dir(), 'protected'))
+
+    def protect(self):
+        protect_file = os.path.join(self._dir(), 'protected')
+        if not os.path.isfile(protect_file):
+            create_empty_file(protect_file)
+
+    def unprotect(self):
+        protect_file = os.path.join(self._dir(), 'protected')
+        if os.path.isfile(protect_file):
+            os.remove(protect_file)
+
+    def __branch_descr(self):
+        return 'branch.%s.description' % self.get_name()
+
+    def get_description(self):
+        return config.get(self.__branch_descr()) or ''
+
+    def set_description(self, line):
+        if line:
+            config.set(self.__branch_descr(), line)
+        else:
+            config.unset(self.__branch_descr())
+
+    def head_top_equal(self):
+        """Return true if the head and the top are the same
+        """
+        crt = self.get_current_patch()
+        if not crt:
+            # we don't care, no patches applied
+            return True
+        return git.get_head() == crt.get_top()
+
+    def is_initialised(self):
+        """Checks if series is already initialised
+        """
+        return bool(config.get(self.format_version_key()))
+
+
+def shortlog(patches):
+    log = ''.join(Run('git-log', '--pretty=short',
+                      p.get_top(), '^%s' % p.get_bottom()).raw_output()
+                  for p in patches)
+    return Run('git-shortlog').raw_input(log).raw_output()
+
+class Series(PatchSet):
+    """Class including the operations on series
+    """
+    def __init__(self, name = None):
+        """Takes a series name as the parameter.
+        """
+        PatchSet.__init__(self, name)
+
+        # Update the branch to the latest format version if it is
+        # initialized, but don't touch it if it isn't.
+        self.update_to_current_format_version()
+
+        self.__refs_base = 'refs/patches/%s' % self.get_name()
 
         self.__applied_file = os.path.join(self._dir(), 'applied')
         self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
         self.__hidden_file = os.path.join(self._dir(), 'hidden')
 
         self.__applied_file = os.path.join(self._dir(), 'applied')
         self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
         self.__hidden_file = os.path.join(self._dir(), 'hidden')
-        self.__current_file = os.path.join(self._dir(), 'current')
-        self.__descr_file = os.path.join(self._dir(), 'description')
 
         # where this series keeps its patches
         self.__patch_dir = os.path.join(self._dir(), 'patches')
 
         # where this series keeps its patches
         self.__patch_dir = os.path.join(self._dir(), 'patches')
-        if not os.path.isdir(self.__patch_dir):
-            self.__patch_dir = self._dir()
-
-        # if no __refs_dir, create and populate it (upgrade old repositories)
-        if self.is_initialised() and not os.path.isdir(self.__refs_dir):
-            os.makedirs(self.__refs_dir)
-            for patch in self.get_applied() + self.get_unapplied():
-                self.get_patch(patch).update_top_ref()
 
         # trash directory
         self.__trash_dir = os.path.join(self._dir(), 'trash')
 
         # trash directory
         self.__trash_dir = os.path.join(self._dir(), 'trash')
-        if self.is_initialised() and not os.path.isdir(self.__trash_dir):
-            os.makedirs(self.__trash_dir)
+
+    def format_version_key(self):
+        return 'branch.%s.stgit.stackformatversion' % self.get_name()
+
+    def update_to_current_format_version(self):
+        """Update a potentially older StGIT directory structure to the
+        latest version. Note: This function should depend as little as
+        possible on external functions that may change during a format
+        version bump, since it must remain able to process older formats."""
+
+        branch_dir = os.path.join(self._basedir(), 'patches', self.get_name())
+        def get_format_version():
+            """Return the integer format version number, or None if the
+            branch doesn't have any StGIT metadata at all, of any version."""
+            fv = config.get(self.format_version_key())
+            ofv = config.get('branch.%s.stgitformatversion' % self.get_name())
+            if fv:
+                # Great, there's an explicitly recorded format version
+                # number, which means that the branch is initialized and
+                # of that exact version.
+                return int(fv)
+            elif ofv:
+                # Old name for the version info, upgrade it
+                config.set(self.format_version_key(), ofv)
+                config.unset('branch.%s.stgitformatversion' % self.get_name())
+                return int(ofv)
+            elif os.path.isdir(os.path.join(branch_dir, 'patches')):
+                # There's a .git/patches/<branch>/patches dirctory, which
+                # means this is an initialized version 1 branch.
+                return 1
+            elif os.path.isdir(branch_dir):
+                # There's a .git/patches/<branch> directory, which means
+                # this is an initialized version 0 branch.
+                return 0
+            else:
+                # The branch doesn't seem to be initialized at all.
+                return None
+        def set_format_version(v):
+            out.info('Upgraded branch %s to format version %d' % (self.get_name(), v))
+            config.set(self.format_version_key(), '%d' % v)
+        def mkdir(d):
+            if not os.path.isdir(d):
+                os.makedirs(d)
+        def rm(f):
+            if os.path.exists(f):
+                os.remove(f)
+        def rm_ref(ref):
+            if git.ref_exists(ref):
+                git.delete_ref(ref)
+
+        # Update 0 -> 1.
+        if get_format_version() == 0:
+            mkdir(os.path.join(branch_dir, 'trash'))
+            patch_dir = os.path.join(branch_dir, 'patches')
+            mkdir(patch_dir)
+            refs_base = 'refs/patches/%s' % self.get_name()
+            for patch in (file(os.path.join(branch_dir, 'unapplied')).readlines()
+                          + file(os.path.join(branch_dir, 'applied')).readlines()):
+                patch = patch.strip()
+                os.rename(os.path.join(branch_dir, patch),
+                          os.path.join(patch_dir, patch))
+                Patch(patch, patch_dir, refs_base).update_top_ref()
+            set_format_version(1)
+
+        # Update 1 -> 2.
+        if get_format_version() == 1:
+            desc_file = os.path.join(branch_dir, 'description')
+            if os.path.isfile(desc_file):
+                desc = read_string(desc_file)
+                if desc:
+                    config.set('branch.%s.description' % self.get_name(), desc)
+                rm(desc_file)
+            rm(os.path.join(branch_dir, 'current'))
+            rm_ref('refs/bases/%s' % self.get_name())
+            set_format_version(2)
+
+        # Make sure we're at the latest version.
+        if not get_format_version() in [None, FORMAT_VERSION]:
+            raise StackException('Branch %s is at format version %d, expected %d'
+                                 % (self.get_name(), get_format_version(), FORMAT_VERSION))
 
     def __patch_name_valid(self, name):
         """Raise an exception if the patch name is not valid.
 
     def __patch_name_valid(self, name):
         """Raise an exception if the patch name is not valid.
@@ -334,20 +485,10 @@ class Series(StgitObject):
         if not name or re.search('[^\w.-]', name):
             raise StackException, 'Invalid patch name: "%s"' % name
 
         if not name or re.search('[^\w.-]', name):
             raise StackException, 'Invalid patch name: "%s"' % name
 
-    def get_branch(self):
-        """Return the branch name for the Series object
-        """
-        return self.__name
-
-    def __set_current(self, name):
-        """Sets the topmost patch
-        """
-        self._set_field('current', name)
-
     def get_patch(self, name):
         """Return a Patch object for the given name
         """
     def get_patch(self, name):
         """Return a Patch object for the given name
         """
-        return Patch(name, self.__patch_dir, self.__refs_dir)
+        return Patch(name, self.__patch_dir, self.__refs_base)
 
     def get_current_patch(self):
         """Return a Patch object representing the topmost patch, or
 
     def get_current_patch(self):
         """Return a Patch object representing the topmost patch, or
@@ -355,112 +496,97 @@ class Series(StgitObject):
         crt = self.get_current()
         if not crt:
             return None
         crt = self.get_current()
         if not crt:
             return None
-        return Patch(crt, self.__patch_dir, self.__refs_dir)
+        return self.get_patch(crt)
 
     def get_current(self):
         """Return the name of the topmost patch, or None if there is
         no such patch."""
 
     def get_current(self):
         """Return the name of the topmost patch, or None if there is
         no such patch."""
-        name = self._get_field('current')
-        if name == '':
+        try:
+            applied = self.get_applied()
+        except StackException:
+            # No "applied" file: branch is not initialized.
+            return None
+        try:
+            return applied[-1]
+        except IndexError:
+            # No patches applied.
             return None
             return None
-        else:
-            return name
 
     def get_applied(self):
         if not os.path.isfile(self.__applied_file):
 
     def get_applied(self):
         if not os.path.isfile(self.__applied_file):
-            raise StackException, 'Branch "%s" not initialised' % self.__name
-        f = file(self.__applied_file)
-        names = [line.strip() for line in f.readlines()]
-        f.close()
-        return names
+            raise StackException, 'Branch "%s" not initialised' % self.get_name()
+        return read_strings(self.__applied_file)
+
+    def set_applied(self, applied):
+        write_strings(self.__applied_file, applied)
 
     def get_unapplied(self):
         if not os.path.isfile(self.__unapplied_file):
 
     def get_unapplied(self):
         if not os.path.isfile(self.__unapplied_file):
-            raise StackException, 'Branch "%s" not initialised' % self.__name
-        f = file(self.__unapplied_file)
-        names = [line.strip() for line in f.readlines()]
-        f.close()
-        return names
+            raise StackException, 'Branch "%s" not initialised' % self.get_name()
+        return read_strings(self.__unapplied_file)
+
+    def set_unapplied(self, unapplied):
+        write_strings(self.__unapplied_file, unapplied)
 
     def get_hidden(self):
         if not os.path.isfile(self.__hidden_file):
             return []
 
     def get_hidden(self):
         if not os.path.isfile(self.__hidden_file):
             return []
-        f = file(self.__hidden_file)
-        names = [line.strip() for line in f.readlines()]
-        f.close()
-        return names
-
-    def get_base_file(self):
-        self.__begin_stack_check()
-        return self.__base_file
+        return read_strings(self.__hidden_file)
 
     def get_base(self):
 
     def get_base(self):
-        return read_string(self.get_base_file())
-
-    def get_protected(self):
-        return os.path.isfile(os.path.join(self._dir(), 'protected'))
-
-    def protect(self):
-        protect_file = os.path.join(self._dir(), 'protected')
-        if not os.path.isfile(protect_file):
-            create_empty_file(protect_file)
-
-    def unprotect(self):
-        protect_file = os.path.join(self._dir(), 'protected')
-        if os.path.isfile(protect_file):
-            os.remove(protect_file)
-
-    def get_description(self):
-        return self._get_field('description') or ''
-
-    def set_description(self, line):
-        self._set_field('description', line)
+        # Return the parent of the bottommost patch, if there is one.
+        if os.path.isfile(self.__applied_file):
+            bottommost = file(self.__applied_file).readline().strip()
+            if bottommost:
+                return self.get_patch(bottommost).get_bottom()
+        # No bottommost patch, so just return HEAD
+        return git.get_head()
 
     def get_parent_remote(self):
 
     def get_parent_remote(self):
-        value = config.get('branch.%s.remote' % self.__name)
+        value = config.get('branch.%s.remote' % self.get_name())
         if value:
             return value
         elif 'origin' in git.remotes_list():
         if value:
             return value
         elif 'origin' in git.remotes_list():
-            print 'Notice: no parent remote declared for stack "%s", ' \
-                  'defaulting to "origin". Consider setting "branch.%s.remote" ' \
-                  'and "branch.%s.merge" with "git repo-config".' \
-                  % (self.__name, self.__name, self.__name)
+            out.note(('No parent remote declared for stack "%s",'
+                      ' defaulting to "origin".' % self.get_name()),
+                     ('Consider setting "branch.%s.remote" and'
+                      ' "branch.%s.merge" with "git config".'
+                      % (self.get_name(), self.get_name())))
             return 'origin'
         else:
             return 'origin'
         else:
-            raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
+            raise StackException, 'Cannot find a parent remote for "%s"' % self.get_name()
 
     def __set_parent_remote(self, remote):
 
     def __set_parent_remote(self, remote):
-        value = config.set('branch.%s.remote' % self.__name, remote)
+        value = config.set('branch.%s.remote' % self.get_name(), remote)
 
     def get_parent_branch(self):
 
     def get_parent_branch(self):
-        value = config.get('branch.%s.stgit.parentbranch' % self.__name)
+        value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
         if value:
             return value
         elif git.rev_parse('heads/origin'):
         if value:
             return value
         elif git.rev_parse('heads/origin'):
-            print 'Notice: no parent branch declared for stack "%s", ' \
-                  'defaulting to "heads/origin". Consider setting ' \
-                  '"branch.%s.stgit.parentbranch" with "git repo-config".' \
-                  % (self.__name, self.__name)
+            out.note(('No parent branch declared for stack "%s",'
+                      ' defaulting to "heads/origin".' % self.get_name()),
+                     ('Consider setting "branch.%s.stgit.parentbranch"'
+                      ' with "git config".' % self.get_name()))
             return 'heads/origin'
         else:
             return 'heads/origin'
         else:
-            raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
+            raise StackException, 'Cannot find a parent branch for "%s"' % self.get_name()
 
     def __set_parent_branch(self, name):
 
     def __set_parent_branch(self, name):
-        if config.get('branch.%s.remote' % self.__name):
+        if config.get('branch.%s.remote' % self.get_name()):
             # Never set merge if remote is not set to avoid
             # possibly-erroneous lookups into 'origin'
             # Never set merge if remote is not set to avoid
             # possibly-erroneous lookups into 'origin'
-            config.set('branch.%s.merge' % self.__name, name)
-        config.set('branch.%s.stgit.parentbranch' % self.__name, name)
+            config.set('branch.%s.merge' % self.get_name(), name)
+        config.set('branch.%s.stgit.parentbranch' % self.get_name(), name)
 
     def set_parent(self, remote, localbranch):
 
     def set_parent(self, remote, localbranch):
-        # policy: record local branches as remote='.'
-        recordremote = remote or '.'
         if localbranch:
         if localbranch:
-            self.__set_parent_remote(recordremote)
+            if remote:
+                self.__set_parent_remote(remote)
             self.__set_parent_branch(localbranch)
         # We'll enforce this later
 #         else:
             self.__set_parent_branch(localbranch)
         # We'll enforce this later
 #         else:
-#             raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
+#             raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.get_name()
 
     def __patch_is_current(self, patch):
         return patch.get_name() == self.get_current()
 
     def __patch_is_current(self, patch):
         return patch.get_name() == self.get_current()
@@ -483,99 +609,29 @@ class Series(StgitObject):
     def patch_exists(self, name):
         """Return true if there is a patch with the given name, false
         otherwise."""
     def patch_exists(self, name):
         """Return true if there is a patch with the given name, false
         otherwise."""
-        return self.patch_applied(name) or self.patch_unapplied(name)
-
-    def __begin_stack_check(self):
-        """Save the current HEAD into .git/refs/heads/base if the stack
-        is empty
-        """
-        if len(self.get_applied()) == 0:
-            head = git.get_head()
-            write_string(self.__base_file, head)
-
-    def __end_stack_check(self):
-        """Remove .git/refs/heads/base if the stack is empty.
-        This warning should never happen
-        """
-        if len(self.get_applied()) == 0 \
-           and read_string(self.__base_file) != git.get_head():
-            print 'Warning: stack empty but the HEAD and base are different'
-
-    def head_top_equal(self):
-        """Return true if the head and the top are the same
-        """
-        crt = self.get_current_patch()
-        if not crt:
-            # we don't care, no patches applied
-            return True
-        return git.get_head() == crt.get_top()
-
-    def is_initialised(self):
-        """Checks if series is already initialised
-        """
-        return os.path.isdir(self.__patch_dir)
+        return self.patch_applied(name) or self.patch_unapplied(name) \
+               or self.patch_hidden(name)
 
     def init(self, create_at=False, parent_remote=None, parent_branch=None):
         """Initialises the stgit series
         """
 
     def init(self, create_at=False, parent_remote=None, parent_branch=None):
         """Initialises the stgit series
         """
-        if os.path.exists(self.__patch_dir):
-            raise StackException, self.__patch_dir + ' already exists'
-        if os.path.exists(self.__refs_dir):
-            raise StackException, self.__refs_dir + ' already exists'
-        if os.path.exists(self.__base_file):
-            raise StackException, self.__base_file + ' already exists'
+        if self.is_initialised():
+            raise StackException, '%s already initialized' % self.get_name()
+        for d in [self._dir()]:
+            if os.path.exists(d):
+                raise StackException, '%s already exists' % d
 
         if (create_at!=False):
 
         if (create_at!=False):
-            git.create_branch(self.__name, create_at)
+            git.create_branch(self.get_name(), create_at)
 
         os.makedirs(self.__patch_dir)
 
         self.set_parent(parent_remote, parent_branch)
 
         os.makedirs(self.__patch_dir)
 
         self.set_parent(parent_remote, parent_branch)
-        
-        create_dirs(os.path.join(self.__base_dir, 'refs', 'bases'))
 
         self.create_empty_field('applied')
         self.create_empty_field('unapplied')
 
         self.create_empty_field('applied')
         self.create_empty_field('unapplied')
-        self.create_empty_field('description')
-        os.makedirs(os.path.join(self._dir(), 'patches'))
-        os.makedirs(self.__refs_dir)
-        self.__begin_stack_check()
-
-    def convert(self):
-        """Either convert to use a separate patch directory, or
-        unconvert to place the patches in the same directory with
-        series control files
-        """
-        if self.__patch_dir == self._dir():
-            print 'Converting old-style to new-style...',
-            sys.stdout.flush()
-
-            self.__patch_dir = os.path.join(self._dir(), 'patches')
-            os.makedirs(self.__patch_dir)
-
-            for p in self.get_applied() + self.get_unapplied():
-                src = os.path.join(self._dir(), p)
-                dest = os.path.join(self.__patch_dir, p)
-                os.rename(src, dest)
-
-            print 'done'
-
-        else:
-            print 'Converting new-style to old-style...',
-            sys.stdout.flush()
-
-            for p in self.get_applied() + self.get_unapplied():
-                src = os.path.join(self.__patch_dir, p)
-                dest = os.path.join(self._dir(), p)
-                os.rename(src, dest)
-
-            if not os.listdir(self.__patch_dir):
-                os.rmdir(self.__patch_dir)
-                print 'done'
-            else:
-                print 'Patch directory %s is not empty.' % self.__name
 
 
-            self.__patch_dir = self._dir()
+        config.set(self.format_version_key(), str(FORMAT_VERSION))
 
     def rename(self, to_name):
         """Renames a series
 
     def rename(self, to_name):
         """Renames a series
@@ -583,25 +639,24 @@ class Series(StgitObject):
         to_stack = Series(to_name)
 
         if to_stack.is_initialised():
         to_stack = Series(to_name)
 
         if to_stack.is_initialised():
-            raise StackException, '"%s" already exists' % to_stack.get_branch()
-        if os.path.exists(to_stack.__base_file):
-            os.remove(to_stack.__base_file)
+            raise StackException, '"%s" already exists' % to_stack.get_name()
+
+        patches = self.get_applied() + self.get_unapplied()
 
 
-        git.rename_branch(self.__name, to_name)
+        git.rename_branch(self.get_name(), to_name)
 
 
+        for patch in patches:
+            git.rename_ref('refs/patches/%s/%s' % (self.get_name(), patch),
+                           'refs/patches/%s/%s' % (to_name, patch))
+            git.rename_ref('refs/patches/%s/%s.log' % (self.get_name(), patch),
+                           'refs/patches/%s/%s.log' % (to_name, patch))
         if os.path.isdir(self._dir()):
         if os.path.isdir(self._dir()):
-            rename(os.path.join(self.__base_dir, 'patches'),
-                   self.__name, to_stack.__name)
-        if os.path.exists(self.__base_file):
-            rename(os.path.join(self.__base_dir, 'refs', 'bases'),
-                   self.__name, to_stack.__name)
-        if os.path.exists(self.__refs_dir):
-            rename(os.path.join(self.__base_dir, 'refs', 'patches'),
-                   self.__name, to_stack.__name)
+            rename(os.path.join(self._basedir(), 'patches'),
+                   self.get_name(), to_stack.get_name())
 
         # Rename the config section
 
         # Rename the config section
-        config.rename_section("branch.%s" % self.__name,
-                              "branch.%s" % to_name)
+        for k in ['branch.%s', 'branch.%s.stgit']:
+            config.rename_section(k % self.get_name(), k % to_name)
 
         self.__init__(to_name)
 
 
         self.__init__(to_name)
 
@@ -617,7 +672,7 @@ class Series(StgitObject):
         new_series = Series(target_series)
 
         # generate an artificial description file
         new_series = Series(target_series)
 
         # generate an artificial description file
-        new_series.set_description('clone of "%s"' % self.__name)
+        new_series.set_description('clone of "%s"' % self.get_name())
 
         # clone self's entire series as unapplied patches
         try:
 
         # clone self's entire series as unapplied patches
         try:
@@ -630,26 +685,35 @@ class Series(StgitObject):
             patches = applied = unapplied = []
         for p in patches:
             patch = self.get_patch(p)
             patches = applied = unapplied = []
         for p in patches:
             patch = self.get_patch(p)
-            new_series.new_patch(p, message = patch.get_description(),
-                                 can_edit = False, unapplied = True,
-                                 bottom = patch.get_bottom(),
-                                 top = patch.get_top(),
-                                 author_name = patch.get_authname(),
-                                 author_email = patch.get_authemail(),
-                                 author_date = patch.get_authdate())
+            newpatch = new_series.new_patch(p, message = patch.get_description(),
+                                            can_edit = False, unapplied = True,
+                                            bottom = patch.get_bottom(),
+                                            top = patch.get_top(),
+                                            author_name = patch.get_authname(),
+                                            author_email = patch.get_authemail(),
+                                            author_date = patch.get_authdate())
+            if patch.get_log():
+                out.info('Setting log to %s' %  patch.get_log())
+                newpatch.set_log(patch.get_log())
+            else:
+                out.info('No log for %s' % p)
 
         # fast forward the cloned series to self's top
         new_series.forward_patches(applied)
 
 
         # fast forward the cloned series to self's top
         new_series.forward_patches(applied)
 
-        # Clone remote and merge settings
-        value = config.get('branch.%s.remote' % self.__name)
+        # Clone parent informations
+        value = config.get('branch.%s.remote' % self.get_name())
         if value:
             config.set('branch.%s.remote' % target_series, value)
 
         if value:
             config.set('branch.%s.remote' % target_series, value)
 
-        value = config.get('branch.%s.merge' % self.__name)
+        value = config.get('branch.%s.merge' % self.get_name())
         if value:
             config.set('branch.%s.merge' % target_series, value)
 
         if value:
             config.set('branch.%s.merge' % target_series, value)
 
+        value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
+        if value:
+            config.set('branch.%s.stgit.parentbranch' % target_series, value)
+
     def delete(self, force = False):
         """Deletes an stgit series
         """
     def delete(self, force = False):
         """Deletes an stgit series
         """
@@ -659,12 +723,13 @@ class Series(StgitObject):
                 raise StackException, \
                       'Cannot delete: the series still contains patches'
             for p in patches:
                 raise StackException, \
                       'Cannot delete: the series still contains patches'
             for p in patches:
-                Patch(p, self.__patch_dir, self.__refs_dir).delete()
+                self.get_patch(p).delete()
 
 
-            # remove the trash directory
-            for fname in os.listdir(self.__trash_dir):
-                os.remove(fname)
-            os.rmdir(self.__trash_dir)
+            # remove the trash directory if any
+            if os.path.exists(self.__trash_dir):
+                for fname in os.listdir(self.__trash_dir):
+                    os.remove(os.path.join(self.__trash_dir, fname))
+                os.rmdir(self.__trash_dir)
 
             # FIXME: find a way to get rid of those manual removals
             # (move functionality to StgitObject ?)
 
             # FIXME: find a way to get rid of those manual removals
             # (move functionality to StgitObject ?)
@@ -674,28 +739,27 @@ class Series(StgitObject):
                 os.remove(self.__unapplied_file)
             if os.path.exists(self.__hidden_file):
                 os.remove(self.__hidden_file)
                 os.remove(self.__unapplied_file)
             if os.path.exists(self.__hidden_file):
                 os.remove(self.__hidden_file)
-            if os.path.exists(self.__current_file):
-                os.remove(self.__current_file)
-            if os.path.exists(self.__descr_file):
-                os.remove(self.__descr_file)
+            if os.path.exists(self._dir()+'/orig-base'):
+                os.remove(self._dir()+'/orig-base')
+
             if not os.listdir(self.__patch_dir):
                 os.rmdir(self.__patch_dir)
             else:
             if not os.listdir(self.__patch_dir):
                 os.rmdir(self.__patch_dir)
             else:
-                print 'Patch directory %s is not empty.' % self.__name
-            if not os.listdir(self._dir()):
-                remove_dirs(os.path.join(self.__base_dir, 'patches'),
-                            self.__name)
-            else:
-                print 'Series directory %s is not empty.' % self.__name
-            if not os.listdir(self.__refs_dir):
-                remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
-                            self.__name)
-            else:
-                print 'Refs directory %s is not empty.' % self.__refs_dir
+                out.warn('Patch directory %s is not empty' % self.__patch_dir)
+
+            try:
+                os.removedirs(self._dir())
+            except OSError:
+                raise StackException('Series directory %s is not empty'
+                                     % self._dir())
 
 
-        if os.path.exists(self.__base_file):
-            remove_file_and_dirs(
-                os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
+            try:
+                git.delete_branch(self.get_name())
+            except GitException:
+                out.warn('Could not delete branch "%s"' % self.get_name())
+
+        config.remove_section('branch.%s' % self.get_name())
+        config.remove_section('branch.%s.stgit' % self.get_name())
 
     def refresh_patch(self, files = None, message = None, edit = False,
                       show_patch = False,
 
     def refresh_patch(self, files = None, message = None, edit = False,
                       show_patch = False,
@@ -703,15 +767,14 @@ class Series(StgitObject):
                       author_name = None, author_email = None,
                       author_date = None,
                       committer_name = None, committer_email = None,
                       author_name = None, author_email = None,
                       author_date = None,
                       committer_name = None, committer_email = None,
-                      backup = False, sign_str = None, log = 'refresh'):
-        """Generates a new commit for the given patch
+                      backup = False, sign_str = None, log = 'refresh',
+                      notes = None, bottom = None):
+        """Generates a new commit for the topmost patch
         """
         """
-        name = self.get_current()
-        if not name:
+        patch = self.get_current_patch()
+        if not patch:
             raise StackException, 'No patches applied'
 
             raise StackException, 'No patches applied'
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
-
         descr = patch.get_description()
         if not (message or descr):
             edit = True
         descr = patch.get_description()
         if not (message or descr):
             edit = True
@@ -719,10 +782,12 @@ class Series(StgitObject):
         elif message:
             descr = message
 
         elif message:
             descr = message
 
+        # TODO: move this out of the stgit.stack module, it is really
+        # for higher level commands to handle the user interaction
         if not message and edit:
             descr = edit_file(self, descr.rstrip(), \
                               'Please edit the description for patch "%s" ' \
         if not message and edit:
             descr = edit_file(self, descr.rstrip(), \
                               'Please edit the description for patch "%s" ' \
-                              'above.' % name, show_patch)
+                              'above.' % patch.get_name(), show_patch)
 
         if not author_name:
             author_name = patch.get_authname()
 
         if not author_name:
             author_name = patch.get_authname()
@@ -735,11 +800,10 @@ class Series(StgitObject):
         if not committer_email:
             committer_email = patch.get_commemail()
 
         if not committer_email:
             committer_email = patch.get_commemail()
 
-        if sign_str:
-            descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
-                                           committer_name, committer_email)
+        descr = add_sign_line(descr, sign_str, committer_name, committer_email)
 
 
-        bottom = patch.get_bottom()
+        if not bottom:
+            bottom = patch.get_bottom()
 
         commit_id = git.commit(files = files,
                                message = descr, parents = [bottom],
 
         commit_id = git.commit(files = files,
                                message = descr, parents = [bottom],
@@ -761,7 +825,7 @@ class Series(StgitObject):
         patch.set_commemail(committer_email)
 
         if log:
         patch.set_commemail(committer_email)
 
         if log:
-            self.log_patch(patch, log)
+            self.log_patch(patch, log, notes)
 
         return commit_id
 
 
         return commit_id
 
@@ -771,7 +835,7 @@ class Series(StgitObject):
         name = self.get_current()
         assert(name)
 
         name = self.get_current()
         assert(name)
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
         old_bottom = patch.get_old_bottom()
         old_top = patch.get_old_top()
 
         old_bottom = patch.get_old_bottom()
         old_top = patch.get_old_top()
 
@@ -787,40 +851,47 @@ class Series(StgitObject):
 
     def new_patch(self, name, message = None, can_edit = True,
                   unapplied = False, show_patch = False,
 
     def new_patch(self, name, message = None, can_edit = True,
                   unapplied = False, show_patch = False,
-                  top = None, bottom = None,
+                  top = None, bottom = None, commit = True,
                   author_name = None, author_email = None, author_date = None,
                   committer_name = None, committer_email = None,
                   author_name = None, author_email = None, author_date = None,
                   committer_name = None, committer_email = None,
-                  before_existing = False, refresh = True):
-        """Creates a new patch
+                  before_existing = False, sign_str = None):
+        """Creates a new patch, either pointing to an existing commit object,
+        or by creating a new commit object.
         """
         """
-        self.__patch_name_valid(name)
-
-        if self.patch_applied(name) or self.patch_unapplied(name):
-            raise StackException, 'Patch "%s" already exists' % name
 
 
+        assert commit or (top and bottom)
+        assert not before_existing or (top and bottom)
+        assert not (commit and before_existing)
+        assert (top and bottom) or (not top and not bottom)
+        assert commit or (not top or (bottom == git.get_commit(top).get_parent()))
+
+        if name != None:
+            self.__patch_name_valid(name)
+            if self.patch_exists(name):
+                raise StackException, 'Patch "%s" already exists' % name
+
+        # TODO: move this out of the stgit.stack module, it is really
+        # for higher level commands to handle the user interaction
+        def sign(msg):
+            return add_sign_line(msg, sign_str,
+                                 committer_name or git.committer().name,
+                                 committer_email or git.committer().email)
         if not message and can_edit:
         if not message and can_edit:
-            descr = edit_file(self, None, \
-                              'Please enter the description for patch "%s" ' \
-                              'above.' % name, show_patch)
+            descr = edit_file(
+                self, sign(''),
+                'Please enter the description for the patch above.',
+                show_patch)
         else:
         else:
-            descr = message
+            descr = sign(message)
 
         head = git.get_head()
 
 
         head = git.get_head()
 
-        self.__begin_stack_check()
+        if name == None:
+            name = make_patch_name(descr, self.patch_exists)
 
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
         patch.create()
 
         patch.create()
 
-        if bottom:
-            patch.set_bottom(bottom)
-        else:
-            patch.set_bottom(head)
-        if top:
-            patch.set_top(top)
-        else:
-            patch.set_top(head)
-
         patch.set_description(descr)
         patch.set_authname(author_name)
         patch.set_authemail(author_email)
         patch.set_description(descr)
         patch.set_authname(author_name)
         patch.set_authemail(author_email)
@@ -828,31 +899,52 @@ class Series(StgitObject):
         patch.set_commname(committer_name)
         patch.set_commemail(committer_email)
 
         patch.set_commname(committer_name)
         patch.set_commemail(committer_email)
 
-        if unapplied:
-            self.log_patch(patch, 'new')
-
-            patches = [patch.get_name()] + self.get_unapplied()
-
-            f = file(self.__unapplied_file, 'w+')
-            f.writelines([line + '\n' for line in patches])
-            f.close()
-        elif before_existing:
-            self.log_patch(patch, 'new')
-
+        if before_existing:
             insert_string(self.__applied_file, patch.get_name())
             insert_string(self.__applied_file, patch.get_name())
-            if not self.get_current():
-                self.__set_current(name)
+        elif unapplied:
+            patches = [patch.get_name()] + self.get_unapplied()
+            write_strings(self.__unapplied_file, patches)
+            set_head = False
         else:
             append_string(self.__applied_file, patch.get_name())
         else:
             append_string(self.__applied_file, patch.get_name())
-            self.__set_current(name)
-            if refresh:
-                self.refresh_patch(cache_update = False, log = 'new')
+            set_head = True
+
+        if commit:
+            if top:
+                top_commit = git.get_commit(top)
+            else:
+                bottom = head
+                top_commit = git.get_commit(head)
+
+            # create a commit for the patch (may be empty if top == bottom);
+            # only commit on top of the current branch
+            assert(unapplied or bottom == head)
+            commit_id = git.commit(message = descr, parents = [bottom],
+                                   cache_update = False,
+                                   tree_id = top_commit.get_tree(),
+                                   allowempty = True, set_head = set_head,
+                                   author_name = author_name,
+                                   author_email = author_email,
+                                   author_date = author_date,
+                                   committer_name = committer_name,
+                                   committer_email = committer_email)
+            # set the patch top to the new commit
+            patch.set_bottom(bottom)
+            patch.set_top(commit_id)
+        else:
+            assert top != bottom
+            patch.set_bottom(bottom)
+            patch.set_top(top)
+
+        self.log_patch(patch, 'new')
+
+        return patch
 
     def delete_patch(self, name):
         """Deletes a patch
         """
         self.__patch_name_valid(name)
 
     def delete_patch(self, name):
         """Deletes a patch
         """
         self.__patch_name_valid(name)
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
 
         if self.__patch_is_current(patch):
             self.pop_patch(name)
 
         if self.__patch_is_current(patch):
             self.pop_patch(name)
@@ -869,14 +961,7 @@ class Series(StgitObject):
 
         unapplied = self.get_unapplied()
         unapplied.remove(name)
 
         unapplied = self.get_unapplied()
         unapplied.remove(name)
-        f = file(self.__unapplied_file, 'w+')
-        f.writelines([line + '\n' for line in unapplied])
-        f.close()
-
-        if self.patch_hidden(name):
-            self.unhide_patch(name)
-
-        self.__begin_stack_check()
+        write_strings(self.__unapplied_file, unapplied)
 
     def forward_patches(self, names):
         """Try to fast-forward an array of patches.
 
     def forward_patches(self, names):
         """Try to fast-forward an array of patches.
@@ -885,7 +970,6 @@ class Series(StgitObject):
         stack. Apply the rest with push_patch
         """
         unapplied = self.get_unapplied()
         stack. Apply the rest with push_patch
         """
         unapplied = self.get_unapplied()
-        self.__begin_stack_check()
 
         forwarded = 0
         top = git.get_head()
 
         forwarded = 0
         top = git.get_head()
@@ -893,7 +977,7 @@ class Series(StgitObject):
         for name in names:
             assert(name in unapplied)
 
         for name in names:
             assert(name in unapplied)
 
-            patch = Patch(name, self.__patch_dir, self.__refs_dir)
+            patch = self.get_patch(name)
 
             head = top
             bottom = patch.get_bottom()
 
             head = top
             bottom = patch.get_bottom()
@@ -949,12 +1033,7 @@ class Series(StgitObject):
         git.switch(top)
 
         append_strings(self.__applied_file, names[0:forwarded])
         git.switch(top)
 
         append_strings(self.__applied_file, names[0:forwarded])
-
-        f = file(self.__unapplied_file, 'w+')
-        f.writelines([line + '\n' for line in unapplied])
-        f.close()
-
-        self.__set_current(name)
+        write_strings(self.__unapplied_file, unapplied)
 
         return forwarded
 
 
         return forwarded
 
@@ -964,8 +1043,7 @@ class Series(StgitObject):
         patches detected to have been applied. The state of the tree
         is restored to the original one
         """
         patches detected to have been applied. The state of the tree
         is restored to the original one
         """
-        patches = [Patch(name, self.__patch_dir, self.__refs_dir)
-                   for name in names]
+        patches = [self.get_patch(name) for name in names]
         patches.reverse()
 
         merged = []
         patches.reverse()
 
         merged = []
@@ -978,85 +1056,88 @@ class Series(StgitObject):
 
         return merged
 
 
         return merged
 
-    def push_patch(self, name, empty = False):
-        """Pushes a patch on the stack
+    def push_empty_patch(self, name):
+        """Pushes an empty patch on the stack
         """
         unapplied = self.get_unapplied()
         assert(name in unapplied)
 
         """
         unapplied = self.get_unapplied()
         assert(name in unapplied)
 
-        self.__begin_stack_check()
+        # patch = self.get_patch(name)
+        head = git.get_head()
+
+        append_string(self.__applied_file, name)
+
+        unapplied.remove(name)
+        write_strings(self.__unapplied_file, unapplied)
+
+        self.refresh_patch(bottom = head, cache_update = False, log = 'push(m)')
 
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+    def push_patch(self, name):
+        """Pushes a patch on the stack
+        """
+        unapplied = self.get_unapplied()
+        assert(name in unapplied)
+
+        patch = self.get_patch(name)
 
         head = git.get_head()
         bottom = patch.get_bottom()
         top = patch.get_top()
 
         head = git.get_head()
         bottom = patch.get_bottom()
         top = patch.get_top()
+        # top != bottom always since we have a commit for each patch
 
 
+        if head == bottom:
+            # A fast-forward push. Just reset the backup
+            # information. No need for logging
+            patch.set_bottom(bottom, backup = True)
+            patch.set_top(top, backup = True)
+
+            git.switch(top)
+            append_string(self.__applied_file, name)
+
+            unapplied.remove(name)
+            write_strings(self.__unapplied_file, unapplied)
+            return False
+
+        # Need to create a new commit an merge in the old patch
         ex = None
         modified = False
 
         ex = None
         modified = False
 
-        # top != bottom always since we have a commit for each patch
-        if empty:
-            # just make an empty patch (top = bottom = HEAD). This
-            # option is useful to allow undoing already merged
-            # patches. The top is updated by refresh_patch since we
-            # need an empty commit
-            patch.set_bottom(head, backup = True)
-            patch.set_top(head, backup = True)
+        # Try the fast applying first. If this fails, fall back to the
+        # three-way merge
+        if not git.apply_diff(bottom, top):
+            # if git.apply_diff() fails, the patch requires a diff3
+            # merge and can be reported as modified
             modified = True
             modified = True
-        elif head == bottom:
-            # reset the backup information. No need for logging
-            patch.set_bottom(bottom, backup = True)
-            patch.set_top(top, backup = True)
 
 
-            git.switch(top)
-        else:
-            # new patch needs to be refreshed.
-            # The current patch is empty after merge.
-            patch.set_bottom(head, backup = True)
-            patch.set_top(head, backup = True)
-
-            # Try the fast applying first. If this fails, fall back to the
-            # three-way merge
-            if not git.apply_diff(bottom, top):
-                # if git.apply_diff() fails, the patch requires a diff3
-                # merge and can be reported as modified
-                modified = True
-
-                # merge can fail but the patch needs to be pushed
-                try:
-                    git.merge(bottom, head, top, recursive = True)
-                except git.GitException, ex:
-                    print >> sys.stderr, \
-                          'The merge failed during "push". ' \
-                          'Use "refresh" after fixing the conflicts'
+            # merge can fail but the patch needs to be pushed
+            try:
+                git.merge(bottom, head, top, recursive = True)
+            except git.GitException, ex:
+                out.error('The merge failed during "push".',
+                          'Use "refresh" after fixing the conflicts or'
+                          ' revert the operation with "push --undo".')
 
         append_string(self.__applied_file, name)
 
         unapplied.remove(name)
 
         append_string(self.__applied_file, name)
 
         unapplied.remove(name)
-        f = file(self.__unapplied_file, 'w+')
-        f.writelines([line + '\n' for line in unapplied])
-        f.close()
-
-        self.__set_current(name)
-
-        # head == bottom case doesn't need to refresh the patch
-        if empty or head != bottom:
-            if not ex:
-                # if the merge was OK and no conflicts, just refresh the patch
-                # The GIT cache was already updated by the merge operation
-                if modified:
-                    log = 'push(m)'
-                else:
-                    log = 'push'
-                self.refresh_patch(cache_update = False, log = log)
+        write_strings(self.__unapplied_file, unapplied)
+
+        if not ex:
+            # if the merge was OK and no conflicts, just refresh the patch
+            # The GIT cache was already updated by the merge operation
+            if modified:
+                log = 'push(m)'
             else:
             else:
-                # we store the correctly merged files only for
-                # tracking the conflict history. Note that the
-                # git.merge() operations should always leave the index
-                # in a valid state (i.e. only stage 0 files)
-                self.refresh_patch(cache_update = False, log = 'push(c)')
-                raise StackException, str(ex)
+                log = 'push'
+            self.refresh_patch(bottom = head, cache_update = False, log = log)
+        else:
+            # we store the correctly merged files only for
+            # tracking the conflict history. Note that the
+            # git.merge() operations should always leave the index
+            # in a valid state (i.e. only stage 0 files)
+            self.refresh_patch(bottom = head, cache_update = False,
+                               log = 'push(c)')
+            raise StackException, str(ex)
 
         return modified
 
 
         return modified
 
@@ -1064,7 +1145,7 @@ class Series(StgitObject):
         name = self.get_current()
         assert(name)
 
         name = self.get_current()
         assert(name)
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
         old_bottom = patch.get_old_bottom()
         old_top = patch.get_old_top()
 
         old_bottom = patch.get_old_bottom()
         old_top = patch.get_old_top()
 
@@ -1090,14 +1171,16 @@ class Series(StgitObject):
         applied.reverse()
         assert(name in applied)
 
         applied.reverse()
         assert(name in applied)
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
-
-        # only keep the local changes
-        if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
-            raise StackException, \
-                  'Failed to pop patches while preserving the local changes'
+        patch = self.get_patch(name)
 
 
-        git.switch(patch.get_bottom(), keep)
+        if git.get_head_file() == self.get_name():
+            if keep and not git.apply_diff(git.get_head(), patch.get_bottom(),
+                                           check_index = False):
+                raise StackException(
+                    'Failed to pop patches while preserving the local changes')
+            git.switch(patch.get_bottom(), keep)
+        else:
+            git.set_branch(self.get_name(), patch.get_bottom())
 
         # save the new applied list
         idx = applied.index(name) + 1
 
         # save the new applied list
         idx = applied.index(name) + 1
@@ -1105,30 +1188,17 @@ class Series(StgitObject):
         popped = applied[:idx]
         popped.reverse()
         unapplied = popped + self.get_unapplied()
         popped = applied[:idx]
         popped.reverse()
         unapplied = popped + self.get_unapplied()
-
-        f = file(self.__unapplied_file, 'w+')
-        f.writelines([line + '\n' for line in unapplied])
-        f.close()
+        write_strings(self.__unapplied_file, unapplied)
 
         del applied[:idx]
         applied.reverse()
 
         del applied[:idx]
         applied.reverse()
-
-        f = file(self.__applied_file, 'w+')
-        f.writelines([line + '\n' for line in applied])
-        f.close()
-
-        if applied == []:
-            self.__set_current(None)
-        else:
-            self.__set_current(applied[-1])
-
-        self.__end_stack_check()
+        write_strings(self.__applied_file, applied)
 
     def empty_patch(self, name):
         """Returns True if the patch is empty
         """
         self.__patch_name_valid(name)
 
     def empty_patch(self, name):
         """Returns True if the patch is empty
         """
         self.__patch_name_valid(name)
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
         bottom = patch.get_bottom()
         top = patch.get_top()
 
         bottom = patch.get_bottom()
         top = patch.get_top()
 
@@ -1152,41 +1222,49 @@ class Series(StgitObject):
         if newname in applied or newname in unapplied:
             raise StackException, 'Patch "%s" already exists' % newname
 
         if newname in applied or newname in unapplied:
             raise StackException, 'Patch "%s" already exists' % newname
 
-        if self.patch_hidden(oldname):
-            self.unhide_patch(oldname)
-            self.hide_patch(newname)
-
         if oldname in unapplied:
         if oldname in unapplied:
-            Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
+            self.get_patch(oldname).rename(newname)
             unapplied[unapplied.index(oldname)] = newname
             unapplied[unapplied.index(oldname)] = newname
-
-            f = file(self.__unapplied_file, 'w+')
-            f.writelines([line + '\n' for line in unapplied])
-            f.close()
+            write_strings(self.__unapplied_file, unapplied)
         elif oldname in applied:
         elif oldname in applied:
-            Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
-            if oldname == self.get_current():
-                self.__set_current(newname)
+            self.get_patch(oldname).rename(newname)
 
             applied[applied.index(oldname)] = newname
 
             applied[applied.index(oldname)] = newname
-
-            f = file(self.__applied_file, 'w+')
-            f.writelines([line + '\n' for line in applied])
-            f.close()
+            write_strings(self.__applied_file, applied)
         else:
             raise StackException, 'Unknown patch "%s"' % oldname
 
         else:
             raise StackException, 'Unknown patch "%s"' % oldname
 
-    def log_patch(self, patch, message):
+    def log_patch(self, patch, message, notes = None):
         """Generate a log commit for a patch
         """
         top = git.get_commit(patch.get_top())
         """Generate a log commit for a patch
         """
         top = git.get_commit(patch.get_top())
-        msg = '%s\t%s' % (message, top.get_id_hash())
-
         old_log = patch.get_log()
         old_log = patch.get_log()
-        if old_log:
-            parents = [old_log]
+
+        if message is None:
+            # replace the current log entry
+            if not old_log:
+                raise StackException, \
+                      'No log entry to annotate for patch "%s"' \
+                      % patch.get_name()
+            replace = True
+            log_commit = git.get_commit(old_log)
+            msg = log_commit.get_log().split('\n')[0]
+            log_parent = log_commit.get_parent()
+            if log_parent:
+                parents = [log_parent]
+            else:
+                parents = []
         else:
         else:
-            parents = []
+            # generate a new log entry
+            replace = False
+            msg = '%s\t%s' % (message, top.get_id_hash())
+            if old_log:
+                parents = [old_log]
+            else:
+                parents = []
+
+        if notes:
+            msg += '\n\n' + notes
 
         log = git.commit(message = msg, parents = parents,
                          cache_update = False, tree_id = top.get_tree(),
 
         log = git.commit(message = msg, parents = parents,
                          cache_update = False, tree_id = top.get_tree(),
@@ -1196,24 +1274,39 @@ class Series(StgitObject):
     def hide_patch(self, name):
         """Add the patch to the hidden list.
         """
     def hide_patch(self, name):
         """Add the patch to the hidden list.
         """
-        if not self.patch_exists(name):
-            raise StackException, 'Unknown patch "%s"' % name
-        elif self.patch_hidden(name):
-            raise StackException, 'Patch "%s" already hidden' % name
+        unapplied = self.get_unapplied()
+        if name not in unapplied:
+            # keep the checking order for backward compatibility with
+            # the old hidden patches functionality
+            if self.patch_applied(name):
+                raise StackException, 'Cannot hide applied patch "%s"' % name
+            elif self.patch_hidden(name):
+                raise StackException, 'Patch "%s" already hidden' % name
+            else:
+                raise StackException, 'Unknown patch "%s"' % name
+
+        if not self.patch_hidden(name):
+            # check needed for backward compatibility with the old
+            # hidden patches functionality
+            append_string(self.__hidden_file, name)
 
 
-        append_string(self.__hidden_file, name)
+        unapplied.remove(name)
+        write_strings(self.__unapplied_file, unapplied)
 
     def unhide_patch(self, name):
 
     def unhide_patch(self, name):
-        """Add the patch to the hidden list.
+        """Remove the patch from the hidden list.
         """
         """
-        if not self.patch_exists(name):
-            raise StackException, 'Unknown patch "%s"' % name
         hidden = self.get_hidden()
         if not name in hidden:
         hidden = self.get_hidden()
         if not name in hidden:
-            raise StackException, 'Patch "%s" not hidden' % name
+            if self.patch_applied(name) or self.patch_unapplied(name):
+                raise StackException, 'Patch "%s" not hidden' % name
+            else:
+                raise StackException, 'Unknown patch "%s"' % name
 
         hidden.remove(name)
 
         hidden.remove(name)
+        write_strings(self.__hidden_file, hidden)
 
 
-        f = file(self.__hidden_file, 'w+')
-        f.writelines([line + '\n' for line in hidden])
-        f.close()
+        if not self.patch_applied(name) and not self.patch_unapplied(name):
+            # check needed for backward compatibility with the old
+            # hidden patches functionality
+            append_string(self.__unapplied_file, name)