chiark / gitweb /
Merge branch 'stable'
[stgit] / stgit / stack.py
index 618182c6a5106e44b0a6e8cef4b2580446635b5c..74c2c108f3519f160d9d8d64d52e500f4284abdd 100644 (file)
@@ -18,15 +18,20 @@ 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.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
+from stgit.lib import git as libgit, stackupgrade
 
 # 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,24 +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
-    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,49 +116,18 @@ def edit_file(series, line, comment, show_patch = True):
 # Classes
 #
 
 # Classes
 #
 
-class Patch:
-    """Basic patch implementation
+class StgitObject:
+    """An object with stgit-like properties stored as files in a directory
     """
     """
-    def __init__(self, name, series_dir, refs_dir):
-        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)
-
-    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'))
+    def _set_dir(self, dir):
+        self.__dir = dir
+    def _dir(self):
+        return self.__dir
 
 
-    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)
+    def create_empty_field(self, name):
+        create_empty_file(os.path.join(self.__dir, name))
 
 
-    def get_name(self):
-        return self.__name
-
-    def rename(self, newname):
-        olddir = self.__dir
-        old_ref_file = self.__top_ref_file
-        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)
-
-        os.rename(olddir, self.__dir)
-        os.rename(old_ref_file, self.__top_ref_file)
-
-    def __update_top_ref(self, ref):
-        write_string(self.__top_ref_file, ref)
-
-    def update_top_ref(self):
-        top = self.get_top()
-        if top:
-            self.__update_top_ref(top)
-
-    def __get_field(self, name, multiline = False):
+    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)
         id_file = os.path.join(self.__dir, name)
         if os.path.isfile(id_file):
             line = read_string(id_file, multiline)
@@ -172,321 +138,406 @@ class Patch:
         else:
             return None
 
         else:
             return None
 
-    def __set_field(self, name, value, multiline = False):
+    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)
 
         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
+    """
+    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.__refs_base = refs_base
+        self.__init_refs()
+
+    def create(self):
+        os.mkdir(self._dir())
+
+    def delete(self, keep_log = False):
+        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 not keep_log and 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()
+        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.__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())
+
+    def __update_top_ref(self, ref):
+        git.set_ref(self.__top_ref, ref)
+        self._set_field('top', ref)
+        self._set_field('bottom', git.get_commit(ref).get_parent())
+
+    def __update_log_ref(self, ref):
+        git.set_ref(self.__log_ref, ref)
+
     def get_old_bottom(self):
     def get_old_bottom(self):
-        return self.__get_field('bottom.old')
+        return git.get_commit(self.get_old_top()).get_parent()
 
     def get_bottom(self):
 
     def get_bottom(self):
-        return self.__get_field('bottom')
-
-    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)
+        return git.get_commit(self.get_top()).get_parent()
 
     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 git.rev_parse(self.__top_ref)
 
     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_top = self.get_top()
+            self._set_field('top.old', curr_top)
+            self._set_field('bottom.old', git.get_commit(curr_top).get_parent())
         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')
+        top = self._get_field('top.old')
 
 
-        if top and bottom:
-            self.__set_field('bottom', bottom)
-            self.__set_field('top', top)
+        if 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):
+        return self._get_field('log')
 
 
-class Series:
-    """Class including the operations on series
-    """
+    def set_log(self, value, backup = False):
+        self._set_field('log', value)
+        self.__update_log_ref(value)
+
+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.__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')
+        self._set_dir(os.path.join(self.__base_dir, 'patches', self.get_name()))
 
 
-        # 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 _basedir(self):
+        return self.__base_dir
 
 
-    def get_patch(self, name):
-        """Return a Patch object for the given name
+    def get_head(self):
+        """Return the head of the branch
         """
         """
-        return Patch(name, self.__patch_dir, self.__refs_dir)
-
-    def get_current(self):
-        """Return a Patch object representing the topmost patch
-        """
-        if os.path.isfile(self.__current_file):
-            name = read_string(self.__current_file)
-        else:
-            return None
-        if name == '':
-            return None
+        crt = self.get_current_patch()
+        if crt:
+            return crt.get_top()
         else:
         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 config.get(stackupgrade.format_version_key(self.get_name())
+                          ) != None
 
 
-    def init(self, create_at=False):
-        """Initialises the stgit series
+
+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.
         """
         """
-        bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
+        PatchSet.__init__(self, name)
 
 
-        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'
+        # Update the branch to the latest format version if it is
+        # initialized, but don't touch it if it isn't.
+        stackupgrade.update_to_current_format_version(
+            libgit.Repository.default(), self.get_name())
 
 
-        if (create_at!=False):
-            git.create_branch(self.__name, create_at)
+        self.__refs_base = 'refs/patches/%s' % self.get_name()
 
 
-        os.makedirs(self.__patch_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_dirs(bases_dir)
+        # where this series keeps its patches
+        self.__patch_dir = os.path.join(self._dir(), 'patches')
 
 
-        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()
+        # trash directory
+        self.__trash_dir = os.path.join(self._dir(), 'trash')
 
 
-    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 __patch_name_valid(self, name):
+        """Raise an exception if the patch name is not valid.
         """
         """
-        if self.__patch_dir == self.__series_dir:
-            print 'Converting old-style to new-style...',
-            sys.stdout.flush()
+        if not name or re.search('[^\w.-]', name):
+            raise StackException, 'Invalid patch name: "%s"' % name
+
+    def get_patch(self, name):
+        """Return a Patch object for the given name
+        """
+        return Patch(name, self.__patch_dir, self.__refs_base)
+
+    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)
 
 
-            self.__patch_dir = os.path.join(self.__series_dir, 'patches')
-            os.makedirs(self.__patch_dir)
+    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
 
 
-            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_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)
 
 
-            print 'done'
+    def set_applied(self, applied):
+        write_strings(self.__applied_file, applied)
 
 
+    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 set_unapplied(self, unapplied):
+        write_strings(self.__unapplied_file, unapplied)
+
+    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:
+            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:
         else:
-            print 'Converting new-style to old-style...',
-            sys.stdout.flush()
+            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()
+
+    def patch_unapplied(self, name):
+        """Return true if the patch exists in the unapplied list
+        """
+        return name in self.get_unapplied()
 
 
-            self.__patch_dir = self.__series_dir
+    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')
+
+        config.set(stackupgrade.format_version_key(self.get_name()),
+                   str(stackupgrade.FORMAT_VERSION))
 
     def rename(self, to_name):
         """Renames a series
 
     def rename(self, to_name):
         """Renames a series
@@ -494,49 +545,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
@@ -547,50 +629,59 @@ 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())
 
 
-        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 git.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,
 
     def refresh_patch(self, files = None, message = None, edit = False,
+                      empty = False,
                       show_patch = False,
                       cache_update = True,
                       author_name = None, author_email = None,
                       author_date = None,
                       committer_name = None, committer_email = None,
                       show_patch = False,
                       cache_update = True,
                       author_name = None, author_email = None,
                       author_date = None,
                       committer_name = None, committer_email = None,
-                      backup = False):
-        """Generates a new commit for the given patch
+                      backup = True, 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
@@ -598,27 +689,37 @@ 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_email:
             author_email = patch.get_authemail()
 
         if not author_name:
             author_name = patch.get_authname()
         if not author_email:
             author_email = patch.get_authemail()
-        if not author_date:
-            author_date = patch.get_authdate()
         if not committer_name:
             committer_name = patch.get_commname()
         if not committer_email:
             committer_email = patch.get_commemail()
 
         if not committer_name:
             committer_name = patch.get_commname()
         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()
+
+        if empty:
+            tree_id = git.get_commit(bottom).get_tree()
+        else:
+            tree_id = None
 
         commit_id = git.commit(files = files,
                                message = descr, parents = [bottom],
                                cache_update = cache_update,
 
         commit_id = git.commit(files = files,
                                message = descr, parents = [bottom],
                                cache_update = cache_update,
+                               tree_id = tree_id,
+                               set_head = True,
                                allowempty = True,
                                author_name = author_name,
                                author_email = author_email,
                                allowempty = True,
                                author_name = author_name,
                                author_email = author_email,
@@ -626,7 +727,6 @@ class Series:
                                committer_name = committer_name,
                                committer_email = committer_email)
 
                                committer_name = committer_name,
                                committer_email = committer_email)
 
-        patch.set_bottom(bottom, backup = backup)
         patch.set_top(commit_id, backup = backup)
         patch.set_description(descr)
         patch.set_authname(author_name)
         patch.set_top(commit_id, backup = backup)
         patch.set_description(descr)
         patch.set_authname(author_name)
@@ -635,6 +735,9 @@ class Series:
         patch.set_commname(committer_name)
         patch.set_commemail(committer_email)
 
         patch.set_commname(committer_name)
         patch.set_commemail(committer_email)
 
+        if log:
+            self.log_patch(patch, log, notes)
+
         return commit_id
 
     def undo_refresh(self):
         return commit_id
 
     def undo_refresh(self):
@@ -643,7 +746,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()
 
@@ -651,45 +754,55 @@ 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)
 
         git.reset(tree_id = old_top, check_out = False)
-        patch.restore_old_boundaries()
+        if patch.restore_old_boundaries():
+            self.log_patch(patch, 'undo')
 
     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):
-        """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.
         """
         """
-        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)
@@ -697,44 +810,66 @@ class Series:
         patch.set_commname(committer_name)
         patch.set_commemail(committer_email)
 
         patch.set_commname(committer_name)
         patch.set_commemail(committer_email)
 
-        if unapplied:
+        if before_existing:
+            insert_string(self.__applied_file, patch.get_name())
+        elif unapplied:
             patches = [patch.get_name()] + self.get_unapplied()
             patches = [patch.get_name()] + self.get_unapplied()
-
-            f = file(self.__unapplied_file, 'w+')
-            f.writelines([line + '\n' for line in patches])
-            f.close()
+            write_strings(self.__unapplied_file, patches)
+            set_head = False
         else:
         else:
-            if before_existing:
-                insert_string(self.__applied_file, patch.get_name())
-                if not self.get_current():
-                    self.__set_current(name)
+            append_string(self.__applied_file, patch.get_name())
+            set_head = True
+
+        if commit:
+            if top:
+                top_commit = git.get_commit(top)
             else:
             else:
-                append_string(self.__applied_file, patch.get_name())
-                self.__set_current(name)
+                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_top(commit_id)
+        else:
+            patch.set_top(top)
+
+        self.log_patch(patch, 'new')
 
 
-                self.refresh_patch(cache_update = False)
+        return patch
 
 
-    def delete_patch(self, name):
+    def delete_patch(self, name, keep_log = False):
         """Deletes a patch
         """
         """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
 
-        patch.delete()
+        # save the commit id to a trash file
+        write_string(os.path.join(self.__trash_dir, name), patch.get_top())
+
+        patch.delete(keep_log = keep_log)
 
         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()
-        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.
@@ -743,7 +878,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()
@@ -751,7 +885,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()
@@ -759,8 +893,8 @@ class Series:
 
             # top != bottom always since we have a commit for each patch
             if head == bottom:
 
             # top != bottom always since we have a commit for each patch
             if head == bottom:
-                # reset the backup information
-                patch.set_bottom(head, backup = True)
+                # reset the backup information. No logging since the
+                # patch hasn't changed
                 patch.set_top(top, backup = True)
 
             else:
                 patch.set_top(top, backup = True)
 
             else:
@@ -788,8 +922,9 @@ class Series:
                                      committer_name = committer_name,
                                      committer_email = committer_email)
 
                                      committer_name = committer_name,
                                      committer_email = committer_email)
 
-                    patch.set_bottom(head, backup = True)
                     patch.set_top(top, backup = True)
                     patch.set_top(top, backup = True)
+
+                    self.log_patch(patch, 'push(f)')
                 else:
                     top = head
                     # stop the fast-forwarding, must do a real merge
                 else:
                     top = head
                     # stop the fast-forwarding, must do a real merge
@@ -804,12 +939,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
 
@@ -819,8 +949,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 = []
@@ -833,76 +962,84 @@ 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)
+
+        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 = Patch(name, self.__patch_dir, self.__refs_dir)
+        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_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
-            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_recursive(bottom, head, top)
+            except git.GitException, ex:
+                out.error('The merge failed during "push".',
+                          '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
-                self.refresh_patch(cache_update = False)
+        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 make the patch empty, with the merged state in the
+            # working tree.
+            self.refresh_patch(bottom = head, cache_update = False,
+                               empty = True, log = 'push(c)')
+            raise StackException, str(ex)
 
         return modified
 
 
         return modified
 
@@ -910,7 +1047,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()
 
@@ -919,22 +1056,33 @@ 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)
-        return patch.restore_old_boundaries()
+        ret = patch.restore_old_boundaries()
+        if ret:
+            self.log_patch(patch, 'undo')
+
+        return ret
 
 
-    def pop_patch(self, name):
+    def pop_patch(self, name, keep = False):
         """Pops the top patch from the stack
         """
         applied = self.get_applied()
         applied.reverse()
         assert(name in applied)
 
         """Pops the top patch from the stack
         """
         applied = self.get_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())
+        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
@@ -942,29 +1090,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()
 
@@ -977,6 +1113,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()
 
@@ -987,21 +1125,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, notes = None):
+        """Generate a log commit for a patch
+        """
+        top = git.get_commit(patch.get_top())
+        old_log = patch.get_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:
+            # 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)
+
+    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)