chiark / gitweb /
Add a 'bottom' parameter to Series.refresh_patch and use it
[stgit] / stgit / stack.py
index 0113a1ce64405818afb78a0aa63602cb903ce7a8..e61d45be40a494313b81e6d034b3b2c6074af4e3 100644 (file)
@@ -18,11 +18,14 @@ along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
-import sys, os
+import sys, os, re
+from email.Utils import formatdate
 
 from stgit.utils import *
 
 from stgit.utils import *
+from stgit.out 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
@@ -64,6 +67,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,24 +90,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
-    if config.has_option('stgit', 'editor'):
-        editor = config.get('stgit', 'editor')
-    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+')
 
@@ -119,393 +114,510 @@ def edit_file(series, line, comment, show_patch = True):
 # Classes
 #
 
 # Classes
 #
 
-class Patch:
+class StgitObject:
+    """An object with stgit-like properties stored as files in a directory
+    """
+    def _set_dir(self, dir):
+        self.__dir = dir
+    def _dir(self):
+        return self.__dir
+
+    def create_empty_field(self, name):
+        create_empty_file(os.path.join(self.__dir, name))
+
+    def _get_field(self, name, multiline = False):
+        id_file = os.path.join(self.__dir, name)
+        if os.path.isfile(id_file):
+            line = read_string(id_file, multiline)
+            if line == '':
+                return None
+            else:
+                return line
+        else:
+            return None
+
+    def _set_field(self, name, value, multiline = False):
+        fname = os.path.join(self.__dir, name)
+        if value and value != '':
+            write_string(fname, value, multiline)
+        elif os.path.isfile(fname):
+            os.remove(fname)
+
+    
+class Patch(StgitObject):
     """Basic patch implementation
     """
     """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.__series_dir = series_dir
         self.__name = name
-        self.__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._set_dir(os.path.join(self.__series_dir, self.__name))
+        self.__refs_base = refs_base
+        self.__init_refs()
 
     def create(self):
 
     def create(self):
-        os.mkdir(self.__dir)
-        create_empty_file(os.path.join(self.__dir, 'bottom'))
-        create_empty_file(os.path.join(self.__dir, 'top'))
+        os.mkdir(self._dir())
+        self.create_empty_field('bottom')
+        self.create_empty_field('top')
 
     def delete(self):
 
     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)
+        for f in os.listdir(self._dir()):
+            os.remove(os.path.join(self._dir(), f))
+        os.rmdir(self._dir())
+        git.delete_ref(self.__top_ref)
+        if git.ref_exists(self.__log_ref):
+            git.delete_ref(self.__log_ref)
 
     def get_name(self):
         return self.__name
 
     def rename(self, newname):
 
     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
+        olddir = self._dir()
+        old_top_ref = self.__top_ref
+        old_log_ref = self.__log_ref
         self.__name = newname
         self.__name = newname
-        self.__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._set_dir(os.path.join(self.__series_dir, self.__name))
+        self.__init_refs()
 
 
-        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)
+        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())
 
     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()
         if top:
             self.__update_top_ref(top)
 
 
     def update_top_ref(self):
         top = self.get_top()
         if top:
             self.__update_top_ref(top)
 
-    def __get_field(self, name, multiline = False):
-        id_file = os.path.join(self.__dir, name)
-        if os.path.isfile(id_file):
-            line = read_string(id_file, multiline)
-            if line == '':
-                return None
-            else:
-                return line
-        else:
-            return None
-
-    def __set_field(self, name, value, multiline = False):
-        fname = os.path.join(self.__dir, name)
-        if value and value != '':
-            write_string(fname, value, multiline)
-        elif os.path.isfile(fname):
-            os.remove(fname)
-
     def get_old_bottom(self):
     def get_old_bottom(self):
-        return self.__get_field('bottom.old')
+        return self._get_field('bottom.old')
 
     def get_bottom(self):
 
     def get_bottom(self):
-        return self.__get_field('bottom')
+        return self._get_field('bottom')
 
     def set_bottom(self, value, backup = False):
         if backup:
 
     def set_bottom(self, value, backup = False):
         if backup:
-            curr = self.__get_field('bottom')
-            self.__set_field('bottom.old', curr)
-        self.__set_field('bottom', value)
+            curr = self._get_field('bottom')
+            self._set_field('bottom.old', curr)
+        self._set_field('bottom', value)
 
     def get_old_top(self):
 
     def get_old_top(self):
-        return self.__get_field('top.old')
+        return self._get_field('top.old')
 
     def get_top(self):
 
     def get_top(self):
-        return self.__get_field('top')
+        return self._get_field('top')
 
     def set_top(self, value, backup = False):
         if backup:
 
     def set_top(self, value, backup = False):
         if backup:
-            curr = self.__get_field('top')
-            self.__set_field('top.old', curr)
-        self.__set_field('top', value)
+            curr = self._get_field('top')
+            self._set_field('top.old', curr)
+        self._set_field('top', value)
         self.__update_top_ref(value)
 
     def restore_old_boundaries(self):
         self.__update_top_ref(value)
 
     def restore_old_boundaries(self):
-        bottom = self.__get_field('bottom.old')
-        top = self.__get_field('top.old')
+        bottom = self._get_field('bottom.old')
+        top = self._get_field('top.old')
 
         if top and bottom:
 
         if top and bottom:
-            self.__set_field('bottom', bottom)
-            self.__set_field('top', top)
+            self._set_field('bottom', bottom)
+            self._set_field('top', top)
             self.__update_top_ref(top)
             return True
         else:
             return False
 
     def get_description(self):
             self.__update_top_ref(top)
             return True
         else:
             return False
 
     def get_description(self):
-        return self.__get_field('description', True)
+        return self._get_field('description', True)
 
     def set_description(self, line):
 
     def set_description(self, line):
-        self.__set_field('description', line, True)
+        self._set_field('description', line, True)
 
     def get_authname(self):
 
     def get_authname(self):
-        return self.__get_field('authname')
+        return self._get_field('authname')
 
     def set_authname(self, name):
 
     def set_authname(self, name):
-        if not name:
-            if config.has_option('stgit', 'authname'):
-                name = config.get('stgit', 'authname')
-            elif 'GIT_AUTHOR_NAME' in os.environ:
-                name = os.environ['GIT_AUTHOR_NAME']
-        self.__set_field('authname', name)
+        self._set_field('authname', name or git.author().name)
 
     def get_authemail(self):
 
     def get_authemail(self):
-        return self.__get_field('authemail')
+        return self._get_field('authemail')
 
 
-    def set_authemail(self, address):
-        if not address:
-            if config.has_option('stgit', 'authemail'):
-                address = config.get('stgit', 'authemail')
-            elif 'GIT_AUTHOR_EMAIL' in os.environ:
-                address = os.environ['GIT_AUTHOR_EMAIL']
-        self.__set_field('authemail', address)
+    def set_authemail(self, email):
+        self._set_field('authemail', email or git.author().email)
 
     def get_authdate(self):
 
     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):
 
     def set_authdate(self, date):
-        if not date and 'GIT_AUTHOR_DATE' in os.environ:
-            date = os.environ['GIT_AUTHOR_DATE']
-        self.__set_field('authdate', date)
+        self._set_field('authdate', date or git.author().date)
 
     def get_commname(self):
 
     def get_commname(self):
-        return self.__get_field('commname')
+        return self._get_field('commname')
 
     def set_commname(self, name):
 
     def set_commname(self, name):
-        if not name:
-            if config.has_option('stgit', 'commname'):
-                name = config.get('stgit', 'commname')
-            elif 'GIT_COMMITTER_NAME' in os.environ:
-                name = os.environ['GIT_COMMITTER_NAME']
-        self.__set_field('commname', name)
+        self._set_field('commname', name or git.committer().name)
 
     def get_commemail(self):
 
     def get_commemail(self):
-        return self.__get_field('commemail')
+        return self._get_field('commemail')
 
 
-    def set_commemail(self, address):
-        if not address:
-            if config.has_option('stgit', 'commemail'):
-                address = config.get('stgit', 'commemail')
-            elif 'GIT_COMMITTER_EMAIL' in os.environ:
-                address = os.environ['GIT_COMMITTER_EMAIL']
-        self.__set_field('commemail', address)
+    def set_commemail(self, email):
+        self._set_field('commemail', email or git.committer().email)
 
     def get_log(self):
 
     def get_log(self):
-        return self.__get_field('log')
+        return self._get_field('log')
 
     def set_log(self, value, backup = False):
 
     def set_log(self, value, backup = False):
-        self.__set_field('log', value)
+        self._set_field('log', value)
         self.__update_log_ref(value)
 
         self.__update_log_ref(value)
 
+# The current StGIT metadata format version.
+FORMAT_VERSION = 2
 
 
-class Series:
-    """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.__series_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()))
 
 
-        self.__applied_file = os.path.join(self.__series_dir, 'applied')
-        self.__unapplied_file = os.path.join(self.__series_dir, 'unapplied')
-        self.__current_file = os.path.join(self.__series_dir, 'current')
-        self.__descr_file = os.path.join(self.__series_dir, 'description')
-
-        # where this series keeps its patches
-        self.__patch_dir = os.path.join(self.__series_dir, 'patches')
-        if not os.path.isdir(self.__patch_dir):
-            self.__patch_dir = self.__series_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()
-
-    def get_branch(self):
-        """Return the branch name for the Series object
-        """
+    def get_name(self):
         return self.__name
         return self.__name
+    def set_name(self, name):
+        self.__name = name
 
 
-    def __set_current(self, name):
-        """Sets the topmost patch
-        """
-        if name:
-            write_string(self.__current_file, name)
-        else:
-            create_empty_file(self.__current_file)
-
-    def get_patch(self, name):
-        """Return a Patch object for the given name
-        """
-        return Patch(name, self.__patch_dir, self.__refs_dir)
+    def _basedir(self):
+        return self.__base_dir
 
 
-    def get_current(self):
-        """Return a Patch object representing the topmost patch
+    def get_head(self):
+        """Return the head of the branch
         """
         """
-        if os.path.isfile(self.__current_file):
-            name = read_string(self.__current_file)
+        crt = self.get_current_patch()
+        if crt:
+            return crt.get_top()
         else:
         else:
-            return None
-        if name == '':
-            return None
-        else:
-            return name
-
-    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
-
-    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
-
-    def get_base_file(self):
-        self.__begin_stack_check()
-        return self.__base_file
+            return self.get_base()
 
     def get_protected(self):
 
     def get_protected(self):
-        return os.path.isfile(os.path.join(self.__series_dir, 'protected'))
+        return os.path.isfile(os.path.join(self._dir(), 'protected'))
 
     def protect(self):
 
     def protect(self):
-        protect_file = os.path.join(self.__series_dir, 'protected')
+        protect_file = os.path.join(self._dir(), 'protected')
         if not os.path.isfile(protect_file):
             create_empty_file(protect_file)
 
     def unprotect(self):
         if not os.path.isfile(protect_file):
             create_empty_file(protect_file)
 
     def unprotect(self):
-        protect_file = os.path.join(self.__series_dir, 'protected')
+        protect_file = os.path.join(self._dir(), 'protected')
         if os.path.isfile(protect_file):
             os.remove(protect_file)
 
         if os.path.isfile(protect_file):
             os.remove(protect_file)
 
-    def get_description(self):
-        if os.path.isfile(self.__descr_file):
-            return read_string(self.__descr_file)
-        else:
-            return ''
-
-    def __patch_is_current(self, patch):
-        return patch.get_name() == read_string(self.__current_file)
+    def __branch_descr(self):
+        return 'branch.%s.description' % self.get_name()
 
 
-    def __patch_applied(self, name):
-        """Return true if the patch exists in the applied list
-        """
-        return name in self.get_applied()
-
-    def __patch_unapplied(self, name):
-        """Return true if the patch exists in the unapplied list
-        """
-        return name in self.get_unapplied()
-
-    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 get_description(self):
+        return config.get(self.__branch_descr()) or ''
 
 
-    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 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
         """
 
     def head_top_equal(self):
         """Return true if the head and the top are the same
         """
-        crt = self.get_current()
+        crt = self.get_current_patch()
         if not crt:
             # we don't care, no patches applied
             return True
         if not crt:
             # we don't care, no patches applied
             return True
-        return git.get_head() == Patch(crt, self.__patch_dir,
-                                       self.__refs_dir).get_top()
+        return git.get_head() == crt.get_top()
 
     def is_initialised(self):
         """Checks if series is already initialised
         """
 
     def is_initialised(self):
         """Checks if series is already initialised
         """
-        return os.path.isdir(self.__patch_dir)
+        return bool(config.get(self.format_version_key()))
 
 
-    def init(self, create_at=False):
-        """Initialises the stgit series
-        """
-        bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
 
 
-        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'
+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()
 
 
-        if (create_at!=False):
-            git.create_branch(self.__name, create_at)
+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)
 
 
-        os.makedirs(self.__patch_dir)
+        # 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()
 
 
-        create_dirs(bases_dir)
+        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')
 
 
-        create_empty_file(self.__applied_file)
-        create_empty_file(self.__unapplied_file)
-        create_empty_file(self.__descr_file)
-        os.makedirs(os.path.join(self.__series_dir, 'patches'))
-        os.makedirs(self.__refs_dir)
-        self.__begin_stack_check()
+        # where this series keeps its patches
+        self.__patch_dir = os.path.join(self._dir(), 'patches')
+
+        # trash directory
+        self.__trash_dir = os.path.join(self._dir(), 'trash')
+
+    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.
+        """
+        if not name or re.search('[^\w.-]', name):
+            raise StackException, 'Invalid patch name: "%s"' % name
 
 
-    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
+    def get_patch(self, name):
+        """Return a Patch object for the given name
         """
         """
-        if self.__patch_dir == self.__series_dir:
-            print 'Converting old-style to new-style...',
-            sys.stdout.flush()
+        return Patch(name, self.__patch_dir, self.__refs_base)
 
 
-            self.__patch_dir = os.path.join(self.__series_dir, 'patches')
-            os.makedirs(self.__patch_dir)
+    def get_current_patch(self):
+        """Return a Patch object representing the topmost patch, or
+        None if there is no such patch."""
+        crt = self.get_current()
+        if not crt:
+            return None
+        return self.get_patch(crt)
 
 
-            for p in self.get_applied() + self.get_unapplied():
-                src = os.path.join(self.__series_dir, p)
-                dest = os.path.join(self.__patch_dir, p)
-                os.rename(src, dest)
+    def get_current(self):
+        """Return the name of the topmost patch, or None if there is
+        no such patch."""
+        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
 
 
-            print 'done'
+    def get_applied(self):
+        if not os.path.isfile(self.__applied_file):
+            raise StackException, 'Branch "%s" not initialised' % self.get_name()
+        return read_strings(self.__applied_file)
 
 
+    def get_unapplied(self):
+        if not os.path.isfile(self.__unapplied_file):
+            raise StackException, 'Branch "%s" not initialised' % self.get_name()
+        return read_strings(self.__unapplied_file)
+
+    def get_hidden(self):
+        if not os.path.isfile(self.__hidden_file):
+            return []
+        return read_strings(self.__hidden_file)
+
+    def get_base(self):
+        # 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):
+        value = config.get('branch.%s.remote' % self.get_name())
+        if value:
+            return value
+        elif 'origin' in git.remotes_list():
+            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:
         else:
-            print 'Converting new-style to old-style...',
-            sys.stdout.flush()
+            raise StackException, 'Cannot find a parent remote for "%s"' % self.get_name()
+
+    def __set_parent_remote(self, remote):
+        value = config.set('branch.%s.remote' % self.get_name(), remote)
+
+    def get_parent_branch(self):
+        value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
+        if value:
+            return value
+        elif git.rev_parse('heads/origin'):
+            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:
+            raise StackException, 'Cannot find a parent branch for "%s"' % self.get_name()
+
+    def __set_parent_branch(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'
+            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):
+        if localbranch:
+            if remote:
+                self.__set_parent_remote(remote)
+            self.__set_parent_branch(localbranch)
+        # We'll enforce this later
+#         else:
+#             raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.get_name()
 
 
-            for p in self.get_applied() + self.get_unapplied():
-                src = os.path.join(self.__patch_dir, p)
-                dest = os.path.join(self.__series_dir, p)
-                os.rename(src, dest)
+    def __patch_is_current(self, patch):
+        return patch.get_name() == self.get_current()
 
 
-            if not os.listdir(self.__patch_dir):
-                os.rmdir(self.__patch_dir)
-                print 'done'
-            else:
-                print 'Patch directory %s is not empty.' % self.__name
+    def patch_applied(self, name):
+        """Return true if the patch exists in the applied list
+        """
+        return name in self.get_applied()
 
 
-            self.__patch_dir = self.__series_dir
+    def patch_unapplied(self, name):
+        """Return true if the patch exists in the unapplied list
+        """
+        return name in self.get_unapplied()
+
+    def patch_hidden(self, name):
+        """Return true if the patch is hidden.
+        """
+        return name in self.get_hidden()
+
+    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) \
+               or self.patch_hidden(name)
+
+    def init(self, create_at=False, parent_remote=None, parent_branch=None):
+        """Initialises the stgit series
+        """
+        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):
+            git.create_branch(self.get_name(), create_at)
+
+        os.makedirs(self.__patch_dir)
+
+        self.set_parent(parent_remote, parent_branch)
+
+        self.create_empty_field('applied')
+        self.create_empty_field('unapplied')
+        self._set_field('orig-base', git.get_head())
+
+        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
@@ -513,49 +625,80 @@ class Series:
         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)
-
-        git.rename_branch(self.__name, to_name)
-
-        if os.path.isdir(self.__series_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)
+            raise StackException, '"%s" already exists' % to_stack.get_name()
+
+        patches = self.get_applied() + self.get_unapplied()
+
+        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()):
+            rename(os.path.join(self._basedir(), 'patches'),
+                   self.get_name(), to_stack.get_name())
+
+        # Rename the config section
+        for k in ['branch.%s', 'branch.%s.stgit']:
+            config.rename_section(k % self.get_name(), k % to_name)
 
         self.__init__(to_name)
 
     def clone(self, target_series):
         """Clones a series
         """
 
         self.__init__(to_name)
 
     def clone(self, target_series):
         """Clones a series
         """
-        base = read_string(self.get_base_file())
+        try:
+            # allow cloning of branches not under StGIT control
+            base = self.get_base()
+        except:
+            base = git.get_head()
         Series(target_series).init(create_at = base)
         new_series = Series(target_series)
 
         # generate an artificial description file
         Series(target_series).init(create_at = base)
         new_series = Series(target_series)
 
         # generate an artificial description file
-        write_string(new_series.__descr_file, 'clone of "%s"' % self.__name)
+        new_series.set_description('clone of "%s"' % self.get_name())
 
         # clone self's entire series as unapplied patches
 
         # clone self's entire series as unapplied patches
-        patches = self.get_applied() + self.get_unapplied()
-        patches.reverse()
+        try:
+            # allow cloning of branches not under StGIT control
+            applied = self.get_applied()
+            unapplied = self.get_unapplied()
+            patches = applied + unapplied
+            patches.reverse()
+        except:
+            patches = applied = unapplied = []
         for p in patches:
             patch = self.get_patch(p)
         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
 
         # fast forward the cloned series to self's top
-        new_series.forward_patches(self.get_applied())
+        new_series.forward_patches(applied)
+
+        # Clone parent informations
+        value = config.get('branch.%s.remote' % self.get_name())
+        if value:
+            config.set('branch.%s.remote' % target_series, value)
+
+        value = config.get('branch.%s.merge' % self.get_name())
+        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
@@ -566,34 +709,43 @@ class Series:
                 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 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 ?)
             if os.path.exists(self.__applied_file):
                 os.remove(self.__applied_file)
             if os.path.exists(self.__unapplied_file):
                 os.remove(self.__unapplied_file)
             if os.path.exists(self.__applied_file):
                 os.remove(self.__applied_file)
             if os.path.exists(self.__unapplied_file):
                 os.remove(self.__unapplied_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.__hidden_file):
+                os.remove(self.__hidden_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.__series_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())
+
+            try:
+                git.delete_branch(self.get_name())
+            except GitException:
+                out.warn('Could not delete branch "%s"' % self.get_name())
 
 
-        if os.path.exists(self.__base_file):
-            remove_file_and_dirs(
-                os.path.join(self.__base_dir, 'refs', 'bases'), self.__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,
@@ -601,15 +753,14 @@ class Series:
                       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, 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
@@ -617,10 +768,12 @@ class Series:
         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()
@@ -633,7 +786,10 @@ class Series:
         if not committer_email:
             committer_email = patch.get_commemail()
 
         if not committer_email:
             committer_email = patch.get_commemail()
 
-        bottom = patch.get_bottom()
+        descr = add_sign_line(descr, sign_str, committer_name, committer_email)
+
+        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],
@@ -655,7 +811,7 @@ class Series:
         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
 
@@ -665,7 +821,7 @@ class Series:
         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()
 
@@ -673,7 +829,7 @@ class Series:
         # old_bottom is different, there wasn't any previous 'refresh'
         # command (probably only a 'push')
         if old_bottom != patch.get_bottom() or old_top == patch.get_top():
         # old_bottom is different, there wasn't any previous 'refresh'
         # command (probably only a 'push')
         if old_bottom != patch.get_bottom() or old_top == patch.get_top():
-            raise StackException, 'No refresh undo information available'
+            raise StackException, 'No undo information available'
 
         git.reset(tree_id = old_top, check_out = False)
         if patch.restore_old_boundaries():
 
         git.reset(tree_id = old_top, check_out = False)
         if patch.restore_old_boundaries():
@@ -681,38 +837,43 @@ class Series:
 
     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,
                   before_existing = False):
         """Creates a new patch
         """
                   author_name = None, author_email = None, author_date = None,
                   committer_name = None, committer_email = None,
                   before_existing = False):
         """Creates a new patch
         """
-        if self.__patch_applied(name) or self.__patch_unapplied(name):
-            raise StackException, 'Patch "%s" already exists' % name
 
 
+        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
         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, None,
+                'Please enter the description for the patch above.',
+                show_patch)
         else:
             descr = message
 
         head = git.get_head()
 
         else:
             descr = message
 
         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)
+        if not bottom:
+            bottom = head
+        if not top:
+            top = head
 
 
+        patch.set_bottom(bottom)
+        patch.set_top(top)
         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)
@@ -720,47 +881,62 @@ class Series:
         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)
+            # no need to commit anything as the object is already
+            # present (mainly used by 'uncommit')
+            commit = False
+        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)
-
-            self.refresh_patch(cache_update = False, log = 'new')
+            set_head = True
+
+        if commit:
+            # 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)
+            top_commit = git.get_commit(top)
+            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_top(commit_id)
+
+        self.log_patch(patch, 'new')
+
+        return patch
 
     def delete_patch(self, name):
         """Deletes a patch
         """
 
     def delete_patch(self, name):
         """Deletes a patch
         """
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        self.__patch_name_valid(name)
+        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)
-        elif self.__patch_applied(name):
+        elif self.patch_applied(name):
             raise StackException, 'Cannot remove an applied patch, "%s", ' \
                   'which is not current' % name
         elif not name in self.get_unapplied():
             raise StackException, 'Unknown patch "%s"' % name
 
             raise StackException, 'Cannot remove an applied patch, "%s", ' \
                   'which is not current' % name
         elif not name in self.get_unapplied():
             raise StackException, 'Unknown patch "%s"' % name
 
+        # save the commit id to a trash file
+        write_string(os.path.join(self.__trash_dir, name), patch.get_top())
+
         patch.delete()
 
         unapplied = self.get_unapplied()
         unapplied.remove(name)
         patch.delete()
 
         unapplied = self.get_unapplied()
         unapplied.remove(name)
-        f = file(self.__unapplied_file, 'w+')
-        f.writelines([line + '\n' for line in unapplied])
-        f.close()
-        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.
@@ -769,7 +945,6 @@ class Series:
         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()
@@ -777,7 +952,7 @@ class Series:
         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()
@@ -833,12 +1008,7 @@ class Series:
         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
 
@@ -848,8 +1018,7 @@ class Series:
         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 = []
@@ -862,80 +1031,88 @@ class Series:
 
         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)
 
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        self.refresh_patch(bottom = head, cache_update = False, log = 'push(m)')
+
+    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)
-                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:
-                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
 
@@ -943,7 +1120,7 @@ class Series:
         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()
 
@@ -952,7 +1129,7 @@ class Series:
         # modified by 'refresh'). If they are both unchanged, there
         # was a fast forward
         if old_bottom == patch.get_bottom() and old_top != patch.get_top():
         # modified by 'refresh'). If they are both unchanged, there
         # was a fast forward
         if old_bottom == patch.get_bottom() and old_top != patch.get_top():
-            raise StackException, 'No push undo information available'
+            raise StackException, 'No undo information available'
 
         git.reset()
         self.pop_patch(name)
 
         git.reset()
         self.pop_patch(name)
@@ -969,9 +1146,15 @@ class Series:
         applied.reverse()
         assert(name in applied)
 
         applied.reverse()
         assert(name in applied)
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        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()):
+                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
@@ -979,29 +1162,17 @@ class Series:
         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
         """
 
     def empty_patch(self, name):
         """Returns True if the patch is empty
         """
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        self.__patch_name_valid(name)
+        patch = self.get_patch(name)
         bottom = patch.get_bottom()
         top = patch.get_top()
 
         bottom = patch.get_bottom()
         top = patch.get_top()
 
@@ -1014,6 +1185,8 @@ class Series:
         return False
 
     def rename_patch(self, oldname, newname):
         return False
 
     def rename_patch(self, oldname, newname):
+        self.__patch_name_valid(newname)
+
         applied = self.get_applied()
         unapplied = self.get_unapplied()
 
         applied = self.get_applied()
         unapplied = self.get_unapplied()
 
@@ -1024,38 +1197,90 @@ class Series:
             raise StackException, 'Patch "%s" already exists' % newname
 
         if oldname in unapplied:
             raise StackException, 'Patch "%s" already exists' % newname
 
         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(),
                          allowempty = True)
         patch.set_log(log)
 
         log = git.commit(message = msg, parents = parents,
                          cache_update = False, tree_id = top.get_tree(),
                          allowempty = True)
         patch.set_log(log)
+
+    def hide_patch(self, name):
+        """Add the patch to the hidden list.
+        """
+        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)
+
+        unapplied.remove(name)
+        write_strings(self.__unapplied_file, unapplied)
+
+    def unhide_patch(self, name):
+        """Remove the patch from the hidden list.
+        """
+        hidden = self.get_hidden()
+        if not name in hidden:
+            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)
+        write_strings(self.__hidden_file, hidden)
+
+        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)