chiark / gitweb /
Fix "stg branch --delete" on a nonexistent branch
[stgit] / stgit / stack.py
index e33fe62fcd518fdbb8caf4f5ccf3f9e543b1a5bc..802a382ad0f7609c136d660daee6c05e28c68c1a 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
 
 
 # stack exception class
 from stgit import git, basedir, templates
 from stgit.config import config
 from shutil import copyfile
 
 
 # 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,55 +145,62 @@ 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())
         self.create_empty_field('bottom')
         self.create_empty_field('top')
 
 
     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)
 
     def __update_log_ref(self, ref):
 
     def __update_log_ref(self, ref):
-        write_string(self.__log_ref_file, ref)
+        git.set_ref(self.__log_ref, ref)
 
     def update_top_ref(self):
         top = self.get_top()
 
     def update_top_ref(self):
         top = self.get_top()
@@ -249,7 +263,16 @@ class Patch(StgitObject):
         self._set_field('authemail', email or git.author().email)
 
     def get_authdate(self):
         self._set_field('authemail', email or git.author().email)
 
     def get_authdate(self):
-        return self._get_field('authdate')
+        date = self._get_field('authdate')
+        if not date:
+            return date
+
+        if re.match('[0-9]+\s+[+-][0-9]+', date):
+            # Unix time (seconds) + time zone
+            secs_tz = date.split()
+            date = formatdate(int(secs_tz[0]))[:-5] + secs_tz[1]
+
+        return date
 
     def set_authdate(self, date):
         self._set_field('authdate', date or git.author().date)
 
     def set_authdate(self, date):
         self._set_field('authdate', date or git.author().date)
@@ -346,6 +369,12 @@ class PatchSet(StgitObject):
         return bool(config.get(self.format_version_key()))
 
 
         return bool(config.get(self.format_version_key()))
 
 
+def shortlog(patches):
+    log = ''.join(Run('git', 'log', '--pretty=short',
+                      p.get_top(), '^%s' % p.get_bottom()).raw_output()
+                  for p in patches)
+    return Run('git', 'shortlog').raw_input(log).raw_output()
+
 class Series(PatchSet):
     """Class including the operations on series
     """
 class Series(PatchSet):
     """Class including the operations on series
     """
@@ -358,8 +387,7 @@ class Series(PatchSet):
         # initialized, but don't touch it if it isn't.
         self.update_to_current_format_version()
 
         # initialized, but don't touch it if it isn't.
         self.update_to_current_format_version()
 
-        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')
@@ -416,20 +444,22 @@ class Series(PatchSet):
         def rm(f):
             if os.path.exists(f):
                 os.remove(f)
         def rm(f):
             if os.path.exists(f):
                 os.remove(f)
+        def rm_ref(ref):
+            if git.ref_exists(ref):
+                git.delete_ref(ref)
 
         # Update 0 -> 1.
         if get_format_version() == 0:
             mkdir(os.path.join(branch_dir, 'trash'))
             patch_dir = os.path.join(branch_dir, 'patches')
             mkdir(patch_dir)
 
         # 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)
+            refs_base = 'refs/patches/%s' % self.get_name()
             for patch in (file(os.path.join(branch_dir, 'unapplied')).readlines()
                           + file(os.path.join(branch_dir, 'applied')).readlines()):
                 patch = patch.strip()
                 os.rename(os.path.join(branch_dir, patch),
                           os.path.join(patch_dir, patch))
             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()
+                Patch(patch, patch_dir, refs_base).update_top_ref()
             set_format_version(1)
 
         # Update 1 -> 2.
             set_format_version(1)
 
         # Update 1 -> 2.
@@ -441,7 +471,7 @@ class Series(PatchSet):
                     config.set('branch.%s.description' % self.get_name(), desc)
                 rm(desc_file)
             rm(os.path.join(branch_dir, 'current'))
                     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()))
+            rm_ref('refs/bases/%s' % self.get_name())
             set_format_version(2)
 
         # Make sure we're at the latest version.
             set_format_version(2)
 
         # Make sure we're at the latest version.
@@ -458,7 +488,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 +496,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 +517,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 +550,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 +567,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 +581,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 +617,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,8 +630,6 @@ 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(self.format_version_key(), str(FORMAT_VERSION))
 
@@ -606,18 +641,22 @@ 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
 
         # Rename the config section
-        config.rename_section("branch.%s" % self.get_name(),
-                              "branch.%s" % to_name)
+        for k in ['branch.%s', 'branch.%s.stgit']:
+            config.rename_section(k % self.get_name(), k % to_name)
 
         self.__init__(to_name)
 
 
         self.__init__(to_name)
 
@@ -684,7 +723,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,18 +753,13 @@ 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,
                       show_patch = False,
 
     def refresh_patch(self, files = None, message = None, edit = False,
                       show_patch = False,
@@ -733,15 +767,14 @@ class Series(PatchSet):
                       author_name = None, author_email = None,
                       author_date = None,
                       committer_name = None, committer_email = None,
                       author_name = None, author_email = None,
                       author_date = None,
                       committer_name = None, committer_email = None,
-                      backup = False, sign_str = None, log = 'refresh'):
-        """Generates a new commit for the given patch
+                      backup = 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
@@ -749,32 +782,26 @@ 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 = '%s\n%s: %s <%s>\n' % (descr, sign_str,
-                                           committer_name, committer_email)
+        descr = add_sign_line(descr, sign_str, committer_name, committer_email)
 
 
-        bottom = patch.get_bottom()
+        if not bottom:
+            bottom = patch.get_bottom()
 
         commit_id = git.commit(files = files,
                                message = descr, parents = [bottom],
 
         commit_id = git.commit(files = files,
                                message = descr, parents = [bottom],
@@ -796,7 +823,7 @@ class Series(PatchSet):
         patch.set_commemail(committer_email)
 
         if log:
         patch.set_commemail(committer_email)
 
         if log:
-            self.log_patch(patch, log)
+            self.log_patch(patch, log, notes)
 
         return commit_id
 
 
         return commit_id
 
@@ -806,7 +833,7 @@ class Series(PatchSet):
         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()
 
@@ -822,43 +849,47 @@ class Series(PatchSet):
 
     def new_patch(self, name, message = None, can_edit = True,
                   unapplied = False, show_patch = False,
 
     def new_patch(self, name, message = None, can_edit = True,
                   unapplied = False, show_patch = False,
-                  top = None, bottom = None,
+                  top = None, bottom = None, commit = True,
                   author_name = None, author_email = None, author_date = None,
                   committer_name = None, committer_email = None,
                   author_name = None, author_email = None, author_date = None,
                   committer_name = None, committer_email = None,
-                  before_existing = False, refresh = True):
-        """Creates a new patch
+                  before_existing = False, sign_str = None):
+        """Creates a new patch, either pointing to an existing commit object,
+        or by creating a new commit object.
         """
 
         """
 
+        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 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)
@@ -866,27 +897,52 @@ class Series(PatchSet):
         patch.set_commname(committer_name)
         patch.set_commemail(committer_email)
 
         patch.set_commname(committer_name)
         patch.set_commemail(committer_email)
 
-        if unapplied:
-            self.log_patch(patch, 'new')
-
+        if before_existing:
+            insert_string(self.__applied_file, patch.get_name())
+        elif unapplied:
             patches = [patch.get_name()] + self.get_unapplied()
             write_strings(self.__unapplied_file, patches)
             patches = [patch.get_name()] + self.get_unapplied()
             write_strings(self.__unapplied_file, patches)
-        elif before_existing:
-            self.log_patch(patch, 'new')
-
-            insert_string(self.__applied_file, patch.get_name())
+            set_head = False
         else:
             append_string(self.__applied_file, patch.get_name())
         else:
             append_string(self.__applied_file, patch.get_name())
-            if refresh:
-                self.refresh_patch(cache_update = False, log = 'new')
+            set_head = True
+
+        if commit:
+            if top:
+                top_commit = git.get_commit(top)
+            else:
+                bottom = head
+                top_commit = git.get_commit(head)
+
+            # create a commit for the patch (may be empty if top == bottom);
+            # only commit on top of the current branch
+            assert(unapplied or bottom == head)
+            commit_id = git.commit(message = descr, parents = [bottom],
+                                   cache_update = False,
+                                   tree_id = top_commit.get_tree(),
+                                   allowempty = True, set_head = set_head,
+                                   author_name = author_name,
+                                   author_email = author_email,
+                                   author_date = author_date,
+                                   committer_name = committer_name,
+                                   committer_email = committer_email)
+            # set the patch top to the new commit
+            patch.set_bottom(bottom)
+            patch.set_top(commit_id)
+        else:
+            assert top != bottom
+            patch.set_bottom(bottom)
+            patch.set_top(top)
+
+        self.log_patch(patch, 'new')
 
         return patch
 
 
         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)
@@ -899,7 +955,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)
@@ -919,7 +975,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()
@@ -985,8 +1041,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 = []
@@ -999,79 +1054,88 @@ 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_bottom(bottom, backup = True)
+            patch.set_top(top, backup = True)
+
+            git.switch(top)
+            append_string(self.__applied_file, name)
+
+            unapplied.remove(name)
+            write_strings(self.__unapplied_file, unapplied)
+            return False
 
 
+        # Need to create a new commit an merge in the old patch
         ex = None
         modified = False
 
         ex = None
         modified = False
 
-        # top != bottom always since we have a commit for each patch
-        if empty:
-            # just make an empty patch (top = bottom = HEAD). This
-            # option is useful to allow undoing already merged
-            # patches. The top is updated by refresh_patch since we
-            # need an empty commit
-            patch.set_bottom(head, backup = True)
-            patch.set_top(head, backup = True)
+        # Try the fast applying first. If this fails, fall back to the
+        # three-way merge
+        if not git.apply_diff(bottom, top):
+            # if git.apply_diff() fails, the patch requires a diff3
+            # merge and can be reported as modified
             modified = True
             modified = True
-        elif head == bottom:
-            # reset the backup information. No need for logging
-            patch.set_bottom(bottom, backup = True)
-            patch.set_top(top, backup = True)
 
 
-            git.switch(top)
-        else:
-            # new patch needs to be refreshed.
-            # The current patch is empty after merge.
-            patch.set_bottom(head, backup = True)
-            patch.set_top(head, backup = True)
-
-            # Try the fast applying first. If this fails, fall back to the
-            # three-way merge
-            if not git.apply_diff(bottom, top):
-                # if git.apply_diff() fails, the patch requires a diff3
-                # merge and can be reported as modified
-                modified = True
-
-                # merge can fail but the patch needs to be pushed
-                try:
-                    git.merge(bottom, head, top, recursive = True)
-                except git.GitException, ex:
-                    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(bottom, head, top, recursive = True)
+            except git.GitException, ex:
+                out.error('The merge failed during "push".',
+                          'Use "refresh" after fixing the conflicts or'
+                          ' revert the operation with "push --undo".')
 
         append_string(self.__applied_file, name)
 
         unapplied.remove(name)
         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 store the correctly merged files only for
+            # tracking the conflict history. Note that the
+            # git.merge() operations should always leave the index
+            # in a valid state (i.e. only stage 0 files)
+            self.refresh_patch(bottom = head, cache_update = False,
+                               log = 'push(c)')
+            raise StackException, str(ex)
 
         return modified
 
 
         return modified
 
@@ -1079,7 +1143,7 @@ class Series(PatchSet):
         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()
 
@@ -1105,10 +1169,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)
@@ -1131,7 +1196,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()
 
@@ -1156,28 +1221,48 @@ 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)
         else:
             raise StackException, 'Unknown patch "%s"' % oldname
 
 
             applied[applied.index(oldname)] = newname
             write_strings(self.__applied_file, applied)
         else:
             raise StackException, 'Unknown patch "%s"' % oldname
 
-    def log_patch(self, patch, message):
+    def log_patch(self, patch, message, notes = None):
         """Generate a log commit for a patch
         """
         top = git.get_commit(patch.get_top())
         """Generate a log commit for a patch
         """
         top = git.get_commit(patch.get_top())
-        msg = '%s\t%s' % (message, top.get_id_hash())
-
         old_log = patch.get_log()
         old_log = patch.get_log()
-        if old_log:
-            parents = [old_log]
+
+        if message is None:
+            # replace the current log entry
+            if not old_log:
+                raise StackException, \
+                      'No log entry to annotate for patch "%s"' \
+                      % patch.get_name()
+            replace = True
+            log_commit = git.get_commit(old_log)
+            msg = log_commit.get_log().split('\n')[0]
+            log_parent = log_commit.get_parent()
+            if log_parent:
+                parents = [log_parent]
+            else:
+                parents = []
         else:
         else:
-            parents = []
+            # generate a new log entry
+            replace = False
+            msg = '%s\t%s' % (message, top.get_id_hash())
+            if old_log:
+                parents = [old_log]
+            else:
+                parents = []
+
+        if notes:
+            msg += '\n\n' + notes
 
         log = git.commit(message = msg, parents = parents,
                          cache_update = False, tree_id = top.get_tree(),
 
         log = git.commit(message = msg, parents = parents,
                          cache_update = False, tree_id = top.get_tree(),