Load the whole config at once and cache it for future use
[stgit] / stgit / lib / stack.py
index af1c994..a72ee22 100644 (file)
@@ -1,8 +1,16 @@
+"""A Python class hierarchy wrapping the StGit on-disk metadata."""
+
 import os.path
 from stgit import exception, utils
 from stgit.lib import git, stackupgrade
+from stgit.config import config
+
+class StackException(exception.StgException):
+    """Exception raised by L{stack} objects."""
 
 class Patch(object):
+    """Represents an StGit patch. This class is mainly concerned with
+    reading and writing the on-disk representation of a patch."""
     def __init__(self, stack, name):
         self.__stack = stack
         self.__name = name
@@ -17,6 +25,13 @@ class Patch(object):
     def commit(self):
         return self.__stack.repository.refs.get(self.__ref)
     @property
+    def old_commit(self):
+        """Return the previous commit for this patch."""
+        fn = os.path.join(self.__compat_dir, 'top.old')
+        if not os.path.isfile(fn):
+            return None
+        return self.__stack.repository.get_commit(utils.read_string(fn))
+    @property
     def __compat_dir(self):
         return os.path.join(self.__stack.directory, 'patches', self.__name)
     def __write_compat_files(self, new_commit, msg):
@@ -32,7 +47,7 @@ class Patch(object):
                 old_log = [self.__stack.repository.refs.get(self.__log_ref)]
             except KeyError:
                 old_log = []
-            cd = git.Commitdata(tree = new_commit.data.tree, parents = old_log,
+            cd = git.CommitData(tree = new_commit.data.tree, parents = old_log,
                                 message = '%s\t%s' % (msg, new_commit.sha1))
             c = self.__stack.repository.commit(cd)
             self.__stack.repository.refs.set(self.__log_ref, c, msg)
@@ -43,7 +58,7 @@ class Patch(object):
         write('authdate', d.author.date)
         write('commname', d.committer.name)
         write('commemail', d.committer.email)
-        write('description', d.message)
+        write('description', d.message, multiline = True)
         write('log', write_patchlog().sha1)
         write('top', new_commit.sha1)
         write('bottom', d.parent.sha1)
@@ -60,7 +75,11 @@ class Patch(object):
             for f in os.listdir(self.__compat_dir):
                 os.remove(os.path.join(self.__compat_dir, f))
             os.rmdir(self.__compat_dir)
-        self.__stack.repository.refs.delete(self.__log_ref)
+        try:
+            # this compatibility log ref might not exist
+            self.__stack.repository.refs.delete(self.__log_ref)
+        except KeyError:
+            pass
     def set_commit(self, commit, msg):
         self.__write_compat_files(commit, msg)
         self.__stack.repository.refs.set(self.__ref, commit, msg)
@@ -71,6 +90,15 @@ class Patch(object):
         return self.name in self.__stack.patchorder.applied
     def is_empty(self):
         return self.commit.data.is_nochange()
+    def files(self):
+        """Return the set of files this patch touches."""
+        fs = set()
+        for (_, _, _, _, _, oldname, newname
+             ) in self.__stack.repository.diff_tree_files(
+            self.commit.data.tree, self.commit.data.parent.data.tree):
+            fs.add(oldname)
+            fs.add(newname)
+        return fs
 
 class PatchOrder(object):
     """Keeps track of patch order, and which patches are applied.
@@ -96,10 +124,22 @@ class PatchOrder(object):
                        lambda self, val: self.__set_list('applied', val))
     unapplied = property(lambda self: self.__get_list('unapplied'),
                          lambda self, val: self.__set_list('unapplied', val))
-    all = property(lambda self: self.applied + self.unapplied)
+    hidden = property(lambda self: self.__get_list('hidden'),
+                      lambda self, val: self.__set_list('hidden', val))
+    all = property(lambda self: self.applied + self.unapplied + self.hidden)
+    all_visible = property(lambda self: self.applied + self.unapplied)
+
+    @staticmethod
+    def create(stackdir):
+        """Create the PatchOrder specific files
+        """
+        utils.create_empty_file(os.path.join(stackdir, 'applied'))
+        utils.create_empty_file(os.path.join(stackdir, 'unapplied'))
+        utils.create_empty_file(os.path.join(stackdir, 'hidden'))
 
 class Patches(object):
-    """Creates Patch objects."""
+    """Creates L{Patch} objects. Makes sure there is only one such object
+    per patch."""
     def __init__(self, stack):
         self.__stack = stack
         def create_patch(name):
@@ -122,32 +162,22 @@ class Patches(object):
         self.__patches[name] = p
         return p
 
-class Stack(object):
+class Stack(git.Branch):
+    """Represents an StGit stack (that is, a git branch with some extra
+    metadata)."""
+    __repo_subdir = 'patches'
+
     def __init__(self, repository, name):
-        self.__repository = repository
-        self.__name = name
-        try:
-            self.head
-        except KeyError:
-            raise exception.StgException('%s: no such branch' % name)
+        git.Branch.__init__(self, repository, name)
         self.__patchorder = PatchOrder(self)
         self.__patches = Patches(self)
         if not stackupgrade.update_to_current_format_version(repository, name):
-            raise exception.StgException('%s: branch not initialized' % name)
-    name = property(lambda self: self.__name)
-    repository = property(lambda self: self.__repository)
+            raise StackException('%s: branch not initialized' % name)
     patchorder = property(lambda self: self.__patchorder)
     patches = property(lambda self: self.__patches)
     @property
     def directory(self):
-        return os.path.join(self.__repository.directory, 'patches', self.__name)
-    def __ref(self):
-        return 'refs/heads/%s' % self.__name
-    @property
-    def head(self):
-        return self.__repository.refs.get(self.__ref())
-    def set_head(self, commit, msg):
-        self.__repository.refs.set(self.__ref(), commit, msg)
+        return os.path.join(self.repository.directory, self.__repo_subdir, self.name)
     @property
     def base(self):
         if self.patchorder.applied:
@@ -155,22 +185,81 @@ class Stack(object):
                                     ).commit.data.parent
         else:
             return self.head
+    @property
+    def top(self):
+        """Commit of the topmost patch, or the stack base if no patches are
+        applied."""
+        if self.patchorder.applied:
+            return self.patches.get(self.patchorder.applied[-1]).commit
+        else:
+            # When no patches are applied, base == head.
+            return self.head
     def head_top_equal(self):
         if not self.patchorder.applied:
             return True
         return self.head == self.patches.get(self.patchorder.applied[-1]).commit
 
+    def set_parents(self, remote, branch):
+        if remote:
+            self.set_parent_remote(remote)
+        if branch:
+            self.set_parent_branch(branch)
+
+    @classmethod
+    def initialise(cls, repository, name = None):
+        """Initialise a Git branch to handle patch series.
+
+        @param repository: The L{Repository} where the L{Stack} will be created
+        @param name: The name of the L{Stack}
+        """
+        if not name:
+            name = repository.current_branch_name
+        # make sure that the corresponding Git branch exists
+        git.Branch(repository, name)
+
+        dir = os.path.join(repository.directory, cls.__repo_subdir, name)
+        compat_dir = os.path.join(dir, 'patches')
+        if os.path.exists(dir):
+            raise StackException('%s: branch already initialized' % name)
+
+        # create the stack directory and files
+        utils.create_dirs(dir)
+        utils.create_dirs(compat_dir)
+        PatchOrder.create(dir)
+        config.set(stackupgrade.format_version_key(name),
+                   str(stackupgrade.FORMAT_VERSION))
+
+        return repository.get_stack(name)
+
+    @classmethod
+    def create(cls, repository, name,
+               create_at = None, parent_remote = None, parent_branch = None):
+        """Create and initialise a Git branch returning the L{Stack} object.
+
+        @param repository: The L{Repository} where the L{Stack} will be created
+        @param name: The name of the L{Stack}
+        @param create_at: The Git id used as the base for the newly created
+            Git branch
+        @param parent_remote: The name of the remote Git branch
+        @param parent_branch: The name of the parent Git branch
+        """
+        git.Branch.create(repository, name, create_at = create_at)
+        stack = cls.initialise(repository, name)
+        stack.set_parents(parent_remote, parent_branch)
+        return stack
+
 class Repository(git.Repository):
+    """A git L{Repository<git.Repository>} with some added StGit-specific
+    operations."""
     def __init__(self, *args, **kwargs):
         git.Repository.__init__(self, *args, **kwargs)
         self.__stacks = {} # name -> Stack
     @property
-    def current_branch(self):
-        return utils.strip_leading('refs/heads/', self.head)
-    @property
     def current_stack(self):
-        return self.get_stack(self.current_branch)
-    def get_stack(self, name):
+        return self.get_stack()
+    def get_stack(self, name = None):
+        if not name:
+            name = self.current_branch_name
         if not name in self.__stacks:
             self.__stacks[name] = Stack(self, name)
         return self.__stacks[name]