stgit.el: Add 'm' as alias for stgit-mark
[stgit] / stgit / stack.py
index 9c15b3f..9958e7a 100644 (file)
@@ -19,15 +19,19 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os, re
 """
 
 import sys, os, re
+from email.Utils import formatdate
 
 
+from stgit.exception import *
 from stgit.utils import *
 from stgit.utils import *
+from stgit.out import *
+from stgit.run import *
 from stgit import git, basedir, templates
 from stgit.config import config
 from shutil import copyfile
 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:
@@ -65,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')
@@ -86,7 +92,8 @@ 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:'
 
     #Vim modeline must be near the end.
     print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
@@ -138,93 +145,86 @@ class StgitObject:
         elif os.path.isfile(fname):
             os.remove(fname)
 
         elif os.path.isfile(fname):
             os.remove(fname)
 
-    
+
 class Patch(StgitObject):
     """Basic patch implementation
     """
 class Patch(StgitObject):
     """Basic patch implementation
     """
-    def __init__(self, name, series_dir, refs_dir):
+    def __init_refs(self):
+        self.__top_ref = self.__refs_base + '/' + self.__name
+        self.__log_ref = self.__top_ref + '.log'
+
+    def __init__(self, name, series_dir, refs_base):
         self.__series_dir = series_dir
         self.__name = name
         self._set_dir(os.path.join(self.__series_dir, self.__name))
         self.__series_dir = series_dir
         self.__name = name
         self._set_dir(os.path.join(self.__series_dir, self.__name))
-        self.__refs_dir = refs_dir
-        self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
-        self.__log_ref_file = os.path.join(self.__refs_dir,
-                                           self.__name + '.log')
+        self.__refs_base = refs_base
+        self.__init_refs()
 
     def create(self):
         os.mkdir(self._dir())
 
     def create(self):
         os.mkdir(self._dir())
-        self.create_empty_field('bottom')
-        self.create_empty_field('top')
 
 
-    def delete(self):
-        for f in os.listdir(self._dir()):
-            os.remove(os.path.join(self._dir(), f))
-        os.rmdir(self._dir())
-        os.remove(self.__top_ref_file)
-        if os.path.exists(self.__log_ref_file):
-            os.remove(self.__log_ref_file)
+    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()
 
     def get_name(self):
         return self.__name
 
     def rename(self, newname):
         olddir = self._dir()
-        old_top_ref_file = self.__top_ref_file
-        old_log_ref_file = self.__log_ref_file
+        old_top_ref = self.__top_ref
+        old_log_ref = self.__log_ref
         self.__name = newname
         self._set_dir(os.path.join(self.__series_dir, self.__name))
         self.__name = newname
         self._set_dir(os.path.join(self.__series_dir, self.__name))
-        self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
-        self.__log_ref_file = os.path.join(self.__refs_dir,
-                                           self.__name + '.log')
+        self.__init_refs()
 
 
+        git.rename_ref(old_top_ref, self.__top_ref)
+        if git.ref_exists(old_log_ref):
+            git.rename_ref(old_log_ref, self.__log_ref)
         os.rename(olddir, self._dir())
         os.rename(olddir, self._dir())
-        os.rename(old_top_ref_file, self.__top_ref_file)
-        if os.path.exists(old_log_ref_file):
-            os.rename(old_log_ref_file, self.__log_ref_file)
 
     def __update_top_ref(self, ref):
 
     def __update_top_ref(self, ref):
-        write_string(self.__top_ref_file, ref)
+        git.set_ref(self.__top_ref, ref)
+        self._set_field('top', ref)
+        self._set_field('bottom', git.get_commit(ref).get_parent())
 
     def __update_log_ref(self, ref):
 
     def __update_log_ref(self, ref):
-        write_string(self.__log_ref_file, ref)
-
-    def update_top_ref(self):
-        top = self.get_top()
-        if top:
-            self.__update_top_ref(top)
+        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):
         return self._get_field('top.old')
 
     def get_top(self):
 
     def get_old_top(self):
         return self._get_field('top.old')
 
     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:
             self.__update_top_ref(top)
             return True
         else:
@@ -249,7 +249,16 @@ class Patch(StgitObject):
         self._set_field('authemail', email or git.author().email)
 
     def get_authdate(self):
         self._set_field('authemail', email or git.author().email)
 
     def get_authdate(self):
-        return self._get_field('authdate')
+        date = self._get_field('authdate')
+        if not date:
+            return date
+
+        if re.match('[0-9]+\s+[+-][0-9]+', date):
+            # Unix time (seconds) + time zone
+            secs_tz = date.split()
+            date = formatdate(int(secs_tz[0]))[:-5] + secs_tz[1]
+
+        return date
 
     def set_authdate(self, date):
         self._set_field('authdate', date or git.author().date)
 
     def set_authdate(self, date):
         self._set_field('authdate', date or git.author().date)
@@ -273,9 +282,6 @@ class Patch(StgitObject):
         self._set_field('log', value)
         self.__update_log_ref(value)
 
         self._set_field('log', value)
         self.__update_log_ref(value)
 
-# The current StGIT metadata format version.
-FORMAT_VERSION = 2
-
 class PatchSet(StgitObject):
     def __init__(self, name = None):
         try:
 class PatchSet(StgitObject):
     def __init__(self, name = None):
         try:
@@ -343,8 +349,15 @@ class PatchSet(StgitObject):
     def is_initialised(self):
         """Checks if series is already initialised
         """
     def is_initialised(self):
         """Checks if series is already initialised
         """
-        return bool(config.get(self.format_version_key()))
+        return config.get(stackupgrade.format_version_key(self.get_name())
+                          ) != None
+
 
 
+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
 
 class Series(PatchSet):
     """Class including the operations on series
@@ -356,10 +369,10 @@ class Series(PatchSet):
 
         # Update the branch to the latest format version if it is
         # initialized, but don't touch it if it isn't.
 
         # 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()
+        stackupgrade.update_to_current_format_version(
+            libgit.Repository.default(), self.get_name())
 
 
-        self.__refs_dir = os.path.join(self._basedir(), 'refs', 'patches',
-                                       self.get_name())
+        self.__refs_base = 'refs/patches/%s' % self.get_name()
 
         self.__applied_file = os.path.join(self._dir(), 'applied')
         self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
 
         self.__applied_file = os.path.join(self._dir(), 'applied')
         self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
@@ -371,84 +384,6 @@ class Series(PatchSet):
         # trash directory
         self.__trash_dir = os.path.join(self._dir(), 'trash')
 
         # 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)
-
-        # 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_dir = os.path.join(self._basedir(), 'refs', 'patches', self.get_name())
-            mkdir(refs_dir)
-            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_dir).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(os.path.join(self._basedir(), 'refs', 'bases', self.get_name()))
-            set_format_version(2)
-
-        # Make sure we're at the latest version.
-        if not get_format_version() in [None, FORMAT_VERSION]:
-            raise StackException('Branch %s is at format version %d, expected %d'
-                                 % (self.get_name(), get_format_version(), FORMAT_VERSION))
-
     def __patch_name_valid(self, name):
         """Raise an exception if the patch name is not valid.
         """
     def __patch_name_valid(self, name):
         """Raise an exception if the patch name is not valid.
         """
@@ -458,7 +393,7 @@ class Series(PatchSet):
     def get_patch(self, name):
         """Return a Patch object for the given name
         """
     def get_patch(self, name):
         """Return a Patch object for the given name
         """
-        return Patch(name, self.__patch_dir, self.__refs_dir)
+        return Patch(name, self.__patch_dir, self.__refs_base)
 
     def get_current_patch(self):
         """Return a Patch object representing the topmost patch, or
 
     def get_current_patch(self):
         """Return a Patch object representing the topmost patch, or
@@ -466,7 +401,7 @@ class Series(PatchSet):
         crt = self.get_current()
         if not crt:
             return None
         crt = self.get_current()
         if not crt:
             return None
-        return Patch(crt, self.__patch_dir, self.__refs_dir)
+        return self.get_patch(crt)
 
     def get_current(self):
         """Return the name of the topmost patch, or None if there is
 
     def get_current(self):
         """Return the name of the topmost patch, or None if there is
@@ -487,11 +422,17 @@ class Series(PatchSet):
             raise StackException, 'Branch "%s" not initialised' % self.get_name()
         return read_strings(self.__applied_file)
 
             raise StackException, 'Branch "%s" not initialised' % self.get_name()
         return read_strings(self.__applied_file)
 
+    def set_applied(self, applied):
+        write_strings(self.__applied_file, applied)
+
     def get_unapplied(self):
         if not os.path.isfile(self.__unapplied_file):
             raise StackException, 'Branch "%s" not initialised' % self.get_name()
         return read_strings(self.__unapplied_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 set_unapplied(self, unapplied):
+        write_strings(self.__unapplied_file, unapplied)
+
     def get_hidden(self):
         if not os.path.isfile(self.__hidden_file):
             return []
     def get_hidden(self):
         if not os.path.isfile(self.__hidden_file):
             return []
@@ -514,7 +455,7 @@ class Series(PatchSet):
             out.note(('No parent remote declared for stack "%s",'
                       ' defaulting to "origin".' % self.get_name()),
                      ('Consider setting "branch.%s.remote" and'
             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 repo-config".'
+                      ' "branch.%s.merge" with "git config".'
                       % (self.get_name(), self.get_name())))
             return 'origin'
         else:
                       % (self.get_name(), self.get_name())))
             return 'origin'
         else:
@@ -531,7 +472,7 @@ class Series(PatchSet):
             out.note(('No parent branch declared for stack "%s",'
                       ' defaulting to "heads/origin".' % self.get_name()),
                      ('Consider setting "branch.%s.stgit.parentbranch"'
             out.note(('No parent branch declared for stack "%s",'
                       ' defaulting to "heads/origin".' % self.get_name()),
                      ('Consider setting "branch.%s.stgit.parentbranch"'
-                      ' with "git repo-config".' % self.get_name()))
+                      ' with "git config".' % self.get_name()))
             return 'heads/origin'
         else:
             raise StackException, 'Cannot find a parent branch for "%s"' % self.get_name()
             return 'heads/origin'
         else:
             raise StackException, 'Cannot find a parent branch for "%s"' % self.get_name()
@@ -545,7 +486,8 @@ class Series(PatchSet):
 
     def set_parent(self, remote, localbranch):
         if localbranch:
 
     def set_parent(self, remote, localbranch):
         if localbranch:
-            self.__set_parent_remote(remote)
+            if remote:
+                self.__set_parent_remote(remote)
             self.__set_parent_branch(localbranch)
         # We'll enforce this later
 #         else:
             self.__set_parent_branch(localbranch)
         # We'll enforce this later
 #         else:
@@ -580,7 +522,7 @@ class Series(PatchSet):
         """
         if self.is_initialised():
             raise StackException, '%s already initialized' % self.get_name()
         """
         if self.is_initialised():
             raise StackException, '%s already initialized' % self.get_name()
-        for d in [self._dir(), self.__refs_dir]:
+        for d in [self._dir()]:
             if os.path.exists(d):
                 raise StackException, '%s already exists' % d
 
             if os.path.exists(d):
                 raise StackException, '%s already exists' % d
 
@@ -593,10 +535,9 @@ class Series(PatchSet):
 
         self.create_empty_field('applied')
         self.create_empty_field('unapplied')
 
         self.create_empty_field('applied')
         self.create_empty_field('unapplied')
-        os.makedirs(self.__refs_dir)
-        self._set_field('orig-base', git.get_head())
 
 
-        config.set(self.format_version_key(), str(FORMAT_VERSION))
+        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
@@ -606,14 +547,18 @@ class Series(PatchSet):
         if to_stack.is_initialised():
             raise StackException, '"%s" already exists' % to_stack.get_name()
 
         if to_stack.is_initialised():
             raise StackException, '"%s" already exists' % to_stack.get_name()
 
+        patches = self.get_applied() + self.get_unapplied()
+
         git.rename_branch(self.get_name(), to_name)
 
         git.rename_branch(self.get_name(), to_name)
 
+        for patch in patches:
+            git.rename_ref('refs/patches/%s/%s' % (self.get_name(), patch),
+                           'refs/patches/%s/%s' % (to_name, patch))
+            git.rename_ref('refs/patches/%s/%s.log' % (self.get_name(), patch),
+                           'refs/patches/%s/%s.log' % (to_name, patch))
         if os.path.isdir(self._dir()):
             rename(os.path.join(self._basedir(), 'patches'),
                    self.get_name(), to_stack.get_name())
         if os.path.isdir(self._dir()):
             rename(os.path.join(self._basedir(), 'patches'),
                    self.get_name(), to_stack.get_name())
-        if os.path.exists(self.__refs_dir):
-            rename(os.path.join(self._basedir(), 'refs', 'patches'),
-                   self.get_name(), to_stack.get_name())
 
         # Rename the config section
         for k in ['branch.%s', 'branch.%s.stgit']:
 
         # Rename the config section
         for k in ['branch.%s', 'branch.%s.stgit']:
@@ -684,7 +629,7 @@ class Series(PatchSet):
                 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):
 
             # remove the trash directory if any
             if os.path.exists(self.__trash_dir):
@@ -714,35 +659,29 @@ class Series(PatchSet):
                 raise StackException('Series directory %s is not empty'
                                      % self._dir())
 
                 raise StackException('Series directory %s is not empty'
                                      % self._dir())
 
-            try:
-                os.removedirs(self.__refs_dir)
-            except OSError:
-                out.warn('Refs directory %s is not empty' % self.__refs_dir)
+        try:
+            git.delete_branch(self.get_name())
+        except git.GitException:
+            out.warn('Could not delete branch "%s"' % self.get_name())
 
 
-        # Cleanup parent informations
-        # FIXME: should one day make use of git-config --section-remove,
-        # scheduled for 1.5.1
-        config.unset('branch.%s.remote' % self.get_name())
-        config.unset('branch.%s.merge' % self.get_name())
-        config.unset('branch.%s.stgit.parentbranch' % self.get_name())
-        config.unset(self.format_version_key())
+        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, sign_str = None, log = 'refresh',
-                      notes = None):
-        """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
@@ -750,36 +689,37 @@ class Series(PatchSet):
         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()
 
-        if sign_str:
-            descr = descr.rstrip()
-            if descr.find("\nSigned-off-by:") < 0 \
-               and descr.find("\nAcked-by:") < 0:
-                descr = descr + "\n"
+        descr = add_sign_line(descr, sign_str, committer_name, committer_email)
 
 
-            descr = '%s\n%s: %s <%s>\n' % (descr, sign_str,
-                                           committer_name, committer_email)
+        if not bottom:
+            bottom = patch.get_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,
@@ -787,7 +727,6 @@ class Series(PatchSet):
                                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)
@@ -801,63 +740,49 @@ class Series(PatchSet):
 
         return commit_id
 
 
         return commit_id
 
-    def undo_refresh(self):
-        """Undo the patch boundaries changes caused by 'refresh'
-        """
-        name = self.get_current()
-        assert(name)
-
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
-        old_bottom = patch.get_old_bottom()
-        old_top = patch.get_old_top()
-
-        # the bottom of the patch is not changed by refresh. If the
-        # 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 undo information available'
-
-        git.reset(tree_id = old_top, check_out = False)
-        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,
                   top = None, bottom = None, commit = True,
                   author_name = None, author_email = None, author_date = None,
                   committer_name = None, committer_email = None,
     def new_patch(self, name, message = None, can_edit = True,
                   unapplied = False, show_patch = False,
                   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
+                  before_existing = False, sign_str = None):
+        """Creates a new patch, either pointing to an existing commit object,
+        or by creating a new commit object.
         """
 
         """
 
+        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
 
         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:
             descr = edit_file(
         if not message and can_edit:
             descr = edit_file(
-                self, None,
+                self, sign(''),
                 'Please enter the description for the patch above.',
                 show_patch)
         else:
                 'Please enter the description for the patch above.',
                 show_patch)
         else:
-            descr = message
+            descr = sign(message)
 
         head = git.get_head()
 
         if name == None:
             name = make_patch_name(descr, self.patch_exists)
 
 
         head = git.get_head()
 
         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 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)
@@ -867,9 +792,6 @@ class Series(PatchSet):
 
         if before_existing:
             insert_string(self.__applied_file, patch.get_name())
 
         if before_existing:
             insert_string(self.__applied_file, patch.get_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)
         elif unapplied:
             patches = [patch.get_name()] + self.get_unapplied()
             write_strings(self.__unapplied_file, patches)
@@ -879,10 +801,15 @@ class Series(PatchSet):
             set_head = True
 
         if commit:
             set_head = True
 
         if commit:
+            if top:
+                top_commit = git.get_commit(top)
+            else:
+                bottom = head
+                top_commit = git.get_commit(head)
+
             # create a commit for the patch (may be empty if top == bottom);
             # only commit on top of the current branch
             assert(unapplied or bottom == head)
             # 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(),
             commit_id = git.commit(message = descr, parents = [bottom],
                                    cache_update = False,
                                    tree_id = top_commit.get_tree(),
@@ -894,16 +821,18 @@ class Series(PatchSet):
                                    committer_email = committer_email)
             # set the patch top to the new commit
             patch.set_top(commit_id)
                                    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')
 
         return patch
 
 
         self.log_patch(patch, 'new')
 
         return patch
 
-    def delete_patch(self, name):
+    def delete_patch(self, name, keep_log = False):
         """Deletes a patch
         """
         self.__patch_name_valid(name)
         """Deletes a patch
         """
         self.__patch_name_valid(name)
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
 
         if self.__patch_is_current(patch):
             self.pop_patch(name)
 
         if self.__patch_is_current(patch):
             self.pop_patch(name)
@@ -916,7 +845,7 @@ class Series(PatchSet):
         # save the commit id to a trash file
         write_string(os.path.join(self.__trash_dir, name), patch.get_top())
 
         # save the commit id to a trash file
         write_string(os.path.join(self.__trash_dir, name), patch.get_top())
 
-        patch.delete()
+        patch.delete(keep_log = keep_log)
 
         unapplied = self.get_unapplied()
         unapplied.remove(name)
 
         unapplied = self.get_unapplied()
         unapplied.remove(name)
@@ -936,7 +865,7 @@ class Series(PatchSet):
         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()
@@ -946,7 +875,6 @@ class Series(PatchSet):
             if head == bottom:
                 # reset the backup information. No logging since the
                 # patch hasn't changed
             if head == bottom:
                 # reset the backup information. No logging since the
                 # patch hasn't changed
-                patch.set_bottom(head, backup = True)
                 patch.set_top(top, backup = True)
 
             else:
                 patch.set_top(top, backup = True)
 
             else:
@@ -974,7 +902,6 @@ class Series(PatchSet):
                                      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)
 
                     self.log_patch(patch, 'push(f)')
                     patch.set_top(top, backup = True)
 
                     self.log_patch(patch, 'push(f)')
@@ -1002,8 +929,7 @@ class Series(PatchSet):
         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 = []
@@ -1016,105 +942,87 @@ class Series(PatchSet):
 
         return merged
 
 
         return merged
 
-    def push_patch(self, name, empty = False):
+    def push_empty_patch(self, name):
+        """Pushes an empty patch on the stack
+        """
+        unapplied = self.get_unapplied()
+        assert(name in unapplied)
+
+        # 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)
 
         """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. No need for logging
-            patch.set_bottom(bottom, backup = True)
-            patch.set_top(top, backup = True)
 
 
-            git.switch(top)
-        else:
-            # new patch needs to be refreshed.
-            # The current patch is empty after merge.
-            patch.set_bottom(head, backup = True)
-            patch.set_top(head, backup = True)
-
-            # Try the fast applying first. If this fails, fall back to the
-            # three-way merge
-            if not git.apply_diff(bottom, top):
-                # if git.apply_diff() fails, the patch requires a diff3
-                # merge and can be reported as modified
-                modified = True
-
-                # merge can fail but the patch needs to be pushed
-                try:
-                    git.merge(bottom, head, top, recursive = True)
-                except git.GitException, ex:
-                    out.error('The merge failed during "push".',
-                              'Use "refresh" after fixing the conflicts or'
-                              ' revert the operation with "push --undo".')
+            # 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 "stg undo".')
 
         append_string(self.__applied_file, name)
 
         unapplied.remove(name)
         write_strings(self.__unapplied_file, unapplied)
 
 
         append_string(self.__applied_file, name)
 
         unapplied.remove(name)
         write_strings(self.__unapplied_file, unapplied)
 
-        # 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)
+        if not ex:
+            # if the merge was OK and no conflicts, just refresh the patch
+            # The GIT cache was already updated by the merge operation
+            if modified:
+                log = 'push(m)'
             else:
             else:
-                # we store the correctly merged files only for
-                # tracking the conflict history. Note that the
-                # git.merge() operations should always leave the index
-                # in a valid state (i.e. only stage 0 files)
-                self.refresh_patch(cache_update = False, log = 'push(c)')
-                raise StackException, str(ex)
+                log = 'push'
+            self.refresh_patch(bottom = head, cache_update = False, log = log)
+        else:
+            # we 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
 
-    def undo_push(self):
-        name = self.get_current()
-        assert(name)
-
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
-        old_bottom = patch.get_old_bottom()
-        old_top = patch.get_old_top()
-
-        # the top of the patch is changed by a push operation only
-        # together with the bottom (otherwise the top was probably
-        # 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 undo information available'
-
-        git.reset()
-        self.pop_patch(name)
-        ret = patch.restore_old_boundaries()
-        if ret:
-            self.log_patch(patch, 'undo')
-
-        return ret
-
     def pop_patch(self, name, keep = False):
         """Pops the top patch from the stack
         """
     def pop_patch(self, name, keep = False):
         """Pops the top patch from the stack
         """
@@ -1122,10 +1030,11 @@ class Series(PatchSet):
         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)
 
         if git.get_head_file() == self.get_name():
 
         if git.get_head_file() == self.get_name():
-            if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
+            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)
                 raise StackException(
                     'Failed to pop patches while preserving the local changes')
             git.switch(patch.get_bottom(), keep)
@@ -1148,7 +1057,7 @@ class Series(PatchSet):
         """Returns True if the patch is empty
         """
         self.__patch_name_valid(name)
         """Returns True if the patch is empty
         """
         self.__patch_name_valid(name)
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
         bottom = patch.get_bottom()
         top = patch.get_top()
 
         bottom = patch.get_bottom()
         top = patch.get_top()
 
@@ -1173,11 +1082,11 @@ class Series(PatchSet):
             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
             write_strings(self.__unapplied_file, unapplied)
         elif oldname in applied:
             unapplied[unapplied.index(oldname)] = newname
             write_strings(self.__unapplied_file, unapplied)
         elif oldname in applied:
-            Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
+            self.get_patch(oldname).rename(newname)
 
             applied[applied.index(oldname)] = newname
             write_strings(self.__applied_file, applied)
 
             applied[applied.index(oldname)] = newname
             write_strings(self.__applied_file, applied)