Allow 'refresh' to annotate the patch log entries
[stgit] / stgit / stack.py
index 9338f2b..3c43aa3 100644 (file)
@@ -276,101 +276,90 @@ class Patch(StgitObject):
 # The current StGIT metadata format version.
 FORMAT_VERSION = 2
 
-def format_version_key(branch):
-    return 'branch.%s.stgitformatversion' % branch
-
-def update_to_current_format_version(branch, git_dir):
-    """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(git_dir, 'patches', branch)
-    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(format_version_key(branch))
-        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 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' % (branch, v))
-        config.set(format_version_key(branch), '%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(git_dir, 'refs', 'patches', branch)
-        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' % branch, desc)
-            rm(desc_file)
-        rm(os.path.join(branch_dir, 'current'))
-        rm(os.path.join(git_dir, 'refs', 'bases', branch))
-        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'
-                             % (branch, get_format_version(), FORMAT_VERSION))
-
-class Series(StgitObject):
-    """Class including the operations on series
-    """
+class PatchSet(StgitObject):
     def __init__(self, name = None):
-        """Takes a series name as the parameter.
-        """
         try:
             if name:
-                self.__name = name
+                self.set_name (name)
             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._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
+        self._set_dir(os.path.join(self.__base_dir, 'patches', self.get_name()))
+
+    def get_name(self):
+        return self.__name
+    def set_name(self, name):
+        self.__name = name
+
+    def _basedir(self):
+        return self.__base_dir
+
+    def get_head(self):
+        """Return the head of the branch
+        """
+        crt = self.get_current_patch()
+        if crt:
+            return crt.get_top()
+        else:
+            return self.get_base()
+
+    def get_protected(self):
+        return os.path.isfile(os.path.join(self._dir(), 'protected'))
+
+    def protect(self):
+        protect_file = os.path.join(self._dir(), 'protected')
+        if not os.path.isfile(protect_file):
+            create_empty_file(protect_file)
+
+    def unprotect(self):
+        protect_file = os.path.join(self._dir(), 'protected')
+        if os.path.isfile(protect_file):
+            os.remove(protect_file)
+
+    def __branch_descr(self):
+        return 'branch.%s.description' % self.get_name()
+
+    def get_description(self):
+        return config.get(self.__branch_descr()) or ''
+
+    def set_description(self, line):
+        if line:
+            config.set(self.__branch_descr(), line)
+        else:
+            config.unset(self.__branch_descr())
+
+    def head_top_equal(self):
+        """Return true if the head and the top are the same
+        """
+        crt = self.get_current_patch()
+        if not crt:
+            # we don't care, no patches applied
+            return True
+        return git.get_head() == crt.get_top()
+
+    def is_initialised(self):
+        """Checks if series is already initialised
+        """
+        return bool(config.get(self.format_version_key()))
+
+
+class Series(PatchSet):
+    """Class including the operations on series
+    """
+    def __init__(self, name = None):
+        """Takes a series name as the parameter.
+        """
+        PatchSet.__init__(self, name)
 
         # Update the branch to the latest format version if it is
         # initialized, but don't touch it if it isn't.
-        update_to_current_format_version(self.__name, self.__base_dir)
+        self.update_to_current_format_version()
 
-        self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
-                                       self.__name)
+        self.__refs_dir = os.path.join(self._basedir(), 'refs', 'patches',
+                                       self.get_name())
 
         self.__applied_file = os.path.join(self._dir(), 'applied')
         self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
@@ -382,17 +371,90 @@ class Series(StgitObject):
         # 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.
         """
         if not name or re.search('[^\w.-]', name):
             raise StackException, 'Invalid patch name: "%s"' % name
 
-    def get_branch(self):
-        """Return the branch name for the Series object
-        """
-        return self.__name
-
     def get_patch(self, name):
         """Return a Patch object for the given name
         """
@@ -422,27 +484,18 @@ class Series(StgitObject):
 
     def get_applied(self):
         if not os.path.isfile(self.__applied_file):
-            raise StackException, 'Branch "%s" not initialised' % self.__name
-        f = file(self.__applied_file)
-        names = [line.strip() for line in f.readlines()]
-        f.close()
-        return names
+            raise StackException, 'Branch "%s" not initialised' % self.get_name()
+        return read_strings(self.__applied_file)
 
     def get_unapplied(self):
         if not os.path.isfile(self.__unapplied_file):
-            raise StackException, 'Branch "%s" not initialised' % self.__name
-        f = file(self.__unapplied_file)
-        names = [line.strip() for line in f.readlines()]
-        f.close()
-        return names
+            raise StackException, 'Branch "%s" not initialised' % self.get_name()
+        return read_strings(self.__unapplied_file)
 
     def get_hidden(self):
         if not os.path.isfile(self.__hidden_file):
             return []
-        f = file(self.__hidden_file)
-        names = [line.strip() for line in f.readlines()]
-        f.close()
-        return names
+        return read_strings(self.__hidden_file)
 
     def get_base(self):
         # Return the parent of the bottommost patch, if there is one.
@@ -453,76 +506,42 @@ class Series(StgitObject):
         # No bottommost patch, so just return HEAD
         return git.get_head()
 
-    def get_head(self):
-        """Return the head of the branch
-        """
-        crt = self.get_current_patch()
-        if crt:
-            return crt.get_top()
-        else:
-            return self.get_base()
-
-    def get_protected(self):
-        return os.path.isfile(os.path.join(self._dir(), 'protected'))
-
-    def protect(self):
-        protect_file = os.path.join(self._dir(), 'protected')
-        if not os.path.isfile(protect_file):
-            create_empty_file(protect_file)
-
-    def unprotect(self):
-        protect_file = os.path.join(self._dir(), 'protected')
-        if os.path.isfile(protect_file):
-            os.remove(protect_file)
-
-    def __branch_descr(self):
-        return 'branch.%s.description' % self.get_branch()
-
-    def get_description(self):
-        return config.get(self.__branch_descr()) or ''
-
-    def set_description(self, line):
-        if line:
-            config.set(self.__branch_descr(), line)
-        else:
-            config.unset(self.__branch_descr())
-
     def get_parent_remote(self):
-        value = config.get('branch.%s.remote' % self.__name)
+        value = config.get('branch.%s.remote' % self.get_name())
         if value:
             return value
         elif 'origin' in git.remotes_list():
             out.note(('No parent remote declared for stack "%s",'
-                      ' defaulting to "origin".' % self.__name),
+                      ' defaulting to "origin".' % self.get_name()),
                      ('Consider setting "branch.%s.remote" and'
                       ' "branch.%s.merge" with "git repo-config".'
-                      % (self.__name, self.__name)))
+                      % (self.get_name(), self.get_name())))
             return 'origin'
         else:
-            raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
+            raise StackException, 'Cannot find a parent remote for "%s"' % self.get_name()
 
     def __set_parent_remote(self, remote):
-        value = config.set('branch.%s.remote' % self.__name, remote)
+        value = config.set('branch.%s.remote' % self.get_name(), remote)
 
     def get_parent_branch(self):
-        value = config.get('branch.%s.stgit.parentbranch' % self.__name)
+        value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
         if value:
             return value
         elif git.rev_parse('heads/origin'):
             out.note(('No parent branch declared for stack "%s",'
-                      ' defaulting to "heads/origin".' % self.__name),
+                      ' defaulting to "heads/origin".' % self.get_name()),
                      ('Consider setting "branch.%s.stgit.parentbranch"'
-                      ' with "git repo-config".' % self.__name))
+                      ' with "git repo-config".' % self.get_name()))
             return 'heads/origin'
         else:
-            raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
+            raise StackException, 'Cannot find a parent branch for "%s"' % self.get_name()
 
     def __set_parent_branch(self, name):
-        if config.get('branch.%s.remote' % self.__name):
+        if config.get('branch.%s.remote' % self.get_name()):
             # Never set merge if remote is not set to avoid
             # possibly-erroneous lookups into 'origin'
-            config.set('branch.%s.merge' % self.__name, name)
-        config.set('branch.%s.stgit.parentbranch' % self.__name, name)
+            config.set('branch.%s.merge' % self.get_name(), name)
+        config.set('branch.%s.stgit.parentbranch' % self.get_name(), name)
 
     def set_parent(self, remote, localbranch):
         if localbranch:
@@ -530,7 +549,7 @@ class Series(StgitObject):
             self.__set_parent_branch(localbranch)
         # We'll enforce this later
 #         else:
-#             raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
+#             raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.get_name()
 
     def __patch_is_current(self, patch):
         return patch.get_name() == self.get_current()
@@ -556,31 +575,17 @@ class Series(StgitObject):
         return self.patch_applied(name) or self.patch_unapplied(name) \
                or self.patch_hidden(name)
 
-    def head_top_equal(self):
-        """Return true if the head and the top are the same
-        """
-        crt = self.get_current_patch()
-        if not crt:
-            # we don't care, no patches applied
-            return True
-        return git.get_head() == crt.get_top()
-
-    def is_initialised(self):
-        """Checks if series is already initialised
-        """
-        return bool(config.get(format_version_key(self.get_branch())))
-
     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_branch()
+            raise StackException, '%s already initialized' % self.get_name()
         for d in [self._dir(), self.__refs_dir]:
             if os.path.exists(d):
                 raise StackException, '%s already exists' % d
 
         if (create_at!=False):
-            git.create_branch(self.__name, create_at)
+            git.create_branch(self.get_name(), create_at)
 
         os.makedirs(self.__patch_dir)
 
@@ -591,7 +596,7 @@ class Series(StgitObject):
         os.makedirs(self.__refs_dir)
         self._set_field('orig-base', git.get_head())
 
-        config.set(format_version_key(self.get_branch()), str(FORMAT_VERSION))
+        config.set(self.format_version_key(), str(FORMAT_VERSION))
 
     def rename(self, to_name):
         """Renames a series
@@ -599,19 +604,19 @@ class Series(StgitObject):
         to_stack = Series(to_name)
 
         if to_stack.is_initialised():
-            raise StackException, '"%s" already exists' % to_stack.get_branch()
+            raise StackException, '"%s" already exists' % to_stack.get_name()
 
-        git.rename_branch(self.__name, to_name)
+        git.rename_branch(self.get_name(), to_name)
 
         if os.path.isdir(self._dir()):
-            rename(os.path.join(self.__base_dir, 'patches'),
-                   self.__name, to_stack.__name)
+            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.__base_dir, 'refs', 'patches'),
-                   self.__name, to_stack.__name)
+            rename(os.path.join(self._basedir(), 'refs', 'patches'),
+                   self.get_name(), to_stack.get_name())
 
         # Rename the config section
-        config.rename_section("branch.%s" % self.__name,
+        config.rename_section("branch.%s" % self.get_name(),
                               "branch.%s" % to_name)
 
         self.__init__(to_name)
@@ -628,7 +633,7 @@ class Series(StgitObject):
         new_series = Series(target_series)
 
         # generate an artificial description file
-        new_series.set_description('clone of "%s"' % self.__name)
+        new_series.set_description('clone of "%s"' % self.get_name())
 
         # clone self's entire series as unapplied patches
         try:
@@ -658,15 +663,15 @@ class Series(StgitObject):
         new_series.forward_patches(applied)
 
         # Clone parent informations
-        value = config.get('branch.%s.remote' % self.__name)
+        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.__name)
+        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.__name)
+        value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
         if value:
             config.set('branch.%s.stgit.parentbranch' % target_series, value)
 
@@ -717,10 +722,10 @@ class Series(StgitObject):
         # 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.__name)
-        config.unset('branch.%s.merge' % self.__name)
-        config.unset('branch.%s.stgit.parentbranch' % self.__name)
-        config.unset('branch.%s.stgitformatversion' % self.__name)
+        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())
 
     def refresh_patch(self, files = None, message = None, edit = False,
                       show_patch = False,
@@ -728,7 +733,8 @@ class Series(StgitObject):
                       author_name = None, author_email = None,
                       author_date = None,
                       committer_name = None, committer_email = None,
-                      backup = False, sign_str = None, log = 'refresh'):
+                      backup = False, sign_str = None, log = 'refresh',
+                      notes = None):
         """Generates a new commit for the given patch
         """
         name = self.get_current()
@@ -791,7 +797,7 @@ class Series(StgitObject):
         patch.set_commemail(committer_email)
 
         if log:
-            self.log_patch(patch, log)
+            self.log_patch(patch, log, notes)
 
         return commit_id
 
@@ -865,10 +871,7 @@ class Series(StgitObject):
             self.log_patch(patch, 'new')
 
             patches = [patch.get_name()] + self.get_unapplied()
-
-            f = file(self.__unapplied_file, 'w+')
-            f.writelines([line + '\n' for line in patches])
-            f.close()
+            write_strings(self.__unapplied_file, patches)
         elif before_existing:
             self.log_patch(patch, 'new')
 
@@ -901,9 +904,7 @@ class Series(StgitObject):
 
         unapplied = self.get_unapplied()
         unapplied.remove(name)
-        f = file(self.__unapplied_file, 'w+')
-        f.writelines([line + '\n' for line in unapplied])
-        f.close()
+        write_strings(self.__unapplied_file, unapplied)
 
     def forward_patches(self, names):
         """Try to fast-forward an array of patches.
@@ -975,10 +976,7 @@ class Series(StgitObject):
         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()
+        write_strings(self.__unapplied_file, unapplied)
 
         return forwarded
 
@@ -1056,9 +1054,7 @@ class Series(StgitObject):
         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()
+        write_strings(self.__unapplied_file, unapplied)
 
         # head == bottom case doesn't need to refresh the patch
         if empty or head != bottom:
@@ -1112,13 +1108,13 @@ class Series(StgitObject):
 
         patch = Patch(name, self.__patch_dir, self.__refs_dir)
 
-        if git.get_head_file() == self.get_branch():
+        if git.get_head_file() == self.get_name():
             if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
                 raise StackException(
                     'Failed to pop patches while preserving the local changes')
             git.switch(patch.get_bottom(), keep)
         else:
-            git.set_branch(self.get_branch(), patch.get_bottom())
+            git.set_branch(self.get_name(), patch.get_bottom())
 
         # save the new applied list
         idx = applied.index(name) + 1
@@ -1126,17 +1122,11 @@ class Series(StgitObject):
         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()
-
-        f = file(self.__applied_file, 'w+')
-        f.writelines([line + '\n' for line in applied])
-        f.close()
+        write_strings(self.__applied_file, applied)
 
     def empty_patch(self, name):
         """Returns True if the patch is empty
@@ -1169,26 +1159,22 @@ class Series(StgitObject):
         if oldname in unapplied:
             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(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:
             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(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
 
-    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())
         msg = '%s\t%s' % (message, top.get_id_hash())
+        if notes:
+            msg += '\n\n' + notes
 
         old_log = patch.get_log()
         if old_log:
@@ -1221,9 +1207,7 @@ class Series(StgitObject):
             append_string(self.__hidden_file, name)
 
         unapplied.remove(name)
-        f = file(self.__unapplied_file, 'w+')
-        f.writelines([line + '\n' for line in unapplied])
-        f.close()
+        write_strings(self.__unapplied_file, unapplied)
 
     def unhide_patch(self, name):
         """Remove the patch from the hidden list.
@@ -1236,9 +1220,7 @@ class Series(StgitObject):
                 raise StackException, 'Unknown patch "%s"' % name
 
         hidden.remove(name)
-        f = file(self.__hidden_file, 'w+')
-        f.writelines([line + '\n' for line in hidden])
-        f.close()
+        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