Some API documentation for the new infrastructure
authorKarl Hasselström <kha@treskal.com>
Tue, 3 Jun 2008 03:03:16 +0000 (05:03 +0200)
committerKarl Hasselström <kha@treskal.com>
Tue, 3 Jun 2008 03:06:53 +0000 (05:06 +0200)
Not all that comprehensive, but better than nothing.

Uses epydoc (http://epydoc.sourceforge.net/) markup.

Signed-off-by: Karl Hasselström <kha@treskal.com>
stgit/lib/git.py
stgit/lib/stack.py
stgit/lib/transaction.py

index b3181cb..c9f01e3 100644 (file)
@@ -1,26 +1,53 @@
+"""A Python class hierarchy wrapping a git repository and its
+contents."""
+
 import os, os.path, re
 from datetime import datetime, timedelta, tzinfo
 
 from stgit import exception, run, utils
 from stgit.config import config
 
 import os, os.path, re
 from datetime import datetime, timedelta, tzinfo
 
 from stgit import exception, run, utils
 from stgit.config import config
 
+class Immutable(object):
+    """I{Immutable} objects cannot be modified once created. Any
+    modification methods will return a new object, leaving the
+    original object as it was.
+
+    The reason for this is that we want to be able to represent git
+    objects, which are immutable, and want to be able to create new
+    git objects that are just slight modifications of other git
+    objects. (Such as, for example, modifying the commit message of a
+    commit object while leaving the rest of it intact. This involves
+    creating a whole new commit object that's exactly like the old one
+    except for the commit message.)
+
+    The L{Immutable} class doesn't acytually enforce immutability --
+    that is up to the individual immutable subclasses. It just serves
+    as documentation."""
+
 class RepositoryException(exception.StgException):
 class RepositoryException(exception.StgException):
-    pass
+    """Base class for all exceptions due to failed L{Repository}
+    operations."""
 
 class DateException(exception.StgException):
 
 class DateException(exception.StgException):
+    """Exception raised when a date+time string could not be parsed."""
     def __init__(self, string, type):
         exception.StgException.__init__(
             self, '"%s" is not a valid %s' % (string, type))
 
 class DetachedHeadException(RepositoryException):
     def __init__(self, string, type):
         exception.StgException.__init__(
             self, '"%s" is not a valid %s' % (string, type))
 
 class DetachedHeadException(RepositoryException):
+    """Exception raised when HEAD is detached (that is, there is no
+    current branch)."""
     def __init__(self):
         RepositoryException.__init__(self, 'Not on any branch')
 
 class Repr(object):
     def __init__(self):
         RepositoryException.__init__(self, 'Not on any branch')
 
 class Repr(object):
+    """Utility class that defines C{__reps__} in terms of C{__str__}."""
     def __repr__(self):
         return str(self)
 
 class NoValue(object):
     def __repr__(self):
         return str(self)
 
 class NoValue(object):
+    """A handy default value that is guaranteed to be distinct from any
+    real argument value."""
     pass
 
 def make_defaults(defaults):
     pass
 
 def make_defaults(defaults):
@@ -34,6 +61,9 @@ def make_defaults(defaults):
     return d
 
 class TimeZone(tzinfo, Repr):
     return d
 
 class TimeZone(tzinfo, Repr):
+    """A simple time zone class for static offsets from UTC. (We have to
+    define our own since Python's standard library doesn't define any
+    time zone classes.)"""
     def __init__(self, tzstring):
         m = re.match(r'^([+-])(\d{2}):?(\d{2})$', tzstring)
         if not m:
     def __init__(self, tzstring):
         m = re.match(r'^([+-])(\d{2}):?(\d{2})$', tzstring)
         if not m:
@@ -54,8 +84,8 @@ class TimeZone(tzinfo, Repr):
     def __str__(self):
         return self.__name
 
     def __str__(self):
         return self.__name
 
-class Date(Repr):
-    """Immutable."""
+class Date(Immutable, Repr):
+    """Represents a timestamp used in git commits."""
     def __init__(self, datestring):
         # Try git-formatted date.
         m = re.match(r'^(\d+)\s+([+-]\d\d:?\d\d)$', datestring)
     def __init__(self, datestring):
         # Try git-formatted date.
         m = re.match(r'^(\d+)\s+([+-]\d\d:?\d\d)$', datestring)
@@ -88,12 +118,15 @@ class Date(Repr):
                           self.__time.tzinfo)
     @classmethod
     def maybe(cls, datestring):
                           self.__time.tzinfo)
     @classmethod
     def maybe(cls, datestring):
+        """Return a new object initialized with the argument if it contains a
+        value (otherwise, just return the argument)."""
         if datestring in [None, NoValue]:
             return datestring
         return cls(datestring)
 
         if datestring in [None, NoValue]:
             return datestring
         return cls(datestring)
 
-class Person(Repr):
-    """Immutable."""
+class Person(Immutable, Repr):
+    """Represents an author or committer in a git commit object. Contains
+    name, email and timestamp."""
     def __init__(self, name = NoValue, email = NoValue,
                  date = NoValue, defaults = NoValue):
         d = make_defaults(defaults)
     def __init__(self, name = NoValue, email = NoValue,
                  date = NoValue, defaults = NoValue):
         d = make_defaults(defaults)
@@ -146,16 +179,16 @@ class Person(Repr):
                 defaults = cls.user())
         return cls.__committer
 
                 defaults = cls.user())
         return cls.__committer
 
-class Tree(Repr):
-    """Immutable."""
+class Tree(Immutable, Repr):
+    """Represents a git tree object."""
     def __init__(self, sha1):
         self.__sha1 = sha1
     sha1 = property(lambda self: self.__sha1)
     def __str__(self):
         return 'Tree<%s>' % self.sha1
 
     def __init__(self, sha1):
         self.__sha1 = sha1
     sha1 = property(lambda self: self.__sha1)
     def __str__(self):
         return 'Tree<%s>' % self.sha1
 
-class CommitData(Repr):
-    """Immutable."""
+class CommitData(Immutable, Repr):
+    """Represents the actual data contents of a git commit object."""
     def __init__(self, tree = NoValue, parents = NoValue, author = NoValue,
                  committer = NoValue, message = NoValue, defaults = NoValue):
         d = make_defaults(defaults)
     def __init__(self, tree = NoValue, parents = NoValue, author = NoValue,
                  committer = NoValue, message = NoValue, defaults = NoValue):
         d = make_defaults(defaults)
@@ -223,8 +256,10 @@ class CommitData(Repr):
                 assert False
         assert False
 
                 assert False
         assert False
 
-class Commit(Repr):
-    """Immutable."""
+class Commit(Immutable, Repr):
+    """Represents a git commit object. All the actual data contents of the
+    commit object is stored in the L{data} member, which is a
+    L{CommitData} object."""
     def __init__(self, repository, sha1):
         self.__sha1 = sha1
         self.__repository = repository
     def __init__(self, repository, sha1):
         self.__sha1 = sha1
         self.__repository = repository
@@ -241,21 +276,26 @@ class Commit(Repr):
         return 'Commit<sha1: %s, data: %s>' % (self.sha1, self.__data)
 
 class Refs(object):
         return 'Commit<sha1: %s, data: %s>' % (self.sha1, self.__data)
 
 class Refs(object):
+    """Accessor for the refs stored in a git repository. Will
+    transparently cache the values of all refs."""
     def __init__(self, repository):
         self.__repository = repository
         self.__refs = None
     def __cache_refs(self):
     def __init__(self, repository):
         self.__repository = repository
         self.__refs = None
     def __cache_refs(self):
+        """(Re-)Build the cache of all refs in the repository."""
         self.__refs = {}
         for line in self.__repository.run(['git', 'show-ref']).output_lines():
             m = re.match(r'^([0-9a-f]{40})\s+(\S+)$', line)
             sha1, ref = m.groups()
             self.__refs[ref] = sha1
     def get(self, ref):
         self.__refs = {}
         for line in self.__repository.run(['git', 'show-ref']).output_lines():
             m = re.match(r'^([0-9a-f]{40})\s+(\S+)$', line)
             sha1, ref = m.groups()
             self.__refs[ref] = sha1
     def get(self, ref):
-        """Throws KeyError if ref doesn't exist."""
+        """Get the Commit the given ref points to. Throws KeyError if ref
+        doesn't exist."""
         if self.__refs == None:
             self.__cache_refs()
         return self.__repository.get_commit(self.__refs[ref])
     def exists(self, ref):
         if self.__refs == None:
             self.__cache_refs()
         return self.__repository.get_commit(self.__refs[ref])
     def exists(self, ref):
+        """Check if the given ref exists."""
         try:
             self.get(ref)
         except KeyError:
         try:
             self.get(ref)
         except KeyError:
@@ -263,6 +303,8 @@ class Refs(object):
         else:
             return True
     def set(self, ref, commit, msg):
         else:
             return True
     def set(self, ref, commit, msg):
+        """Write the sha1 of the given Commit to the ref. The ref may or may
+        not already exist."""
         if self.__refs == None:
             self.__cache_refs()
         old_sha1 = self.__refs.get(ref, '0'*40)
         if self.__refs == None:
             self.__cache_refs()
         old_sha1 = self.__refs.get(ref, '0'*40)
@@ -272,6 +314,7 @@ class Refs(object):
                                    ref, new_sha1, old_sha1]).no_output()
             self.__refs[ref] = new_sha1
     def delete(self, ref):
                                    ref, new_sha1, old_sha1]).no_output()
             self.__refs[ref] = new_sha1
     def delete(self, ref):
+        """Delete the given ref. Throws KeyError if ref doesn't exist."""
         if self.__refs == None:
             self.__cache_refs()
         self.__repository.run(['git', 'update-ref',
         if self.__refs == None:
             self.__cache_refs()
         self.__repository.run(['git', 'update-ref',
@@ -280,7 +323,8 @@ class Refs(object):
 
 class ObjectCache(object):
     """Cache for Python objects, for making sure that we create only one
 
 class ObjectCache(object):
     """Cache for Python objects, for making sure that we create only one
-    Python object per git object."""
+    Python object per git object. This reduces memory consumption and
+    makes object comparison very cheap."""
     def __init__(self, create):
         self.__objects = {}
         self.__create = create
     def __init__(self, create):
         self.__objects = {}
         self.__create = create
@@ -296,13 +340,27 @@ class ObjectCache(object):
 
 class RunWithEnv(object):
     def run(self, args, env = {}):
 
 class RunWithEnv(object):
     def run(self, args, env = {}):
+        """Run the given command with an environment given by self.env.
+
+        @type args: list of strings
+        @param args: Command and argument vector
+        @type env: dict
+        @param env: Extra environment"""
         return run.Run(*args).env(utils.add_dict(self.env, env))
 
 class RunWithEnvCwd(RunWithEnv):
     def run(self, args, env = {}):
         return run.Run(*args).env(utils.add_dict(self.env, env))
 
 class RunWithEnvCwd(RunWithEnv):
     def run(self, args, env = {}):
+        """Run the given command with an environment given by self.env, and
+        current working directory given by self.cwd.
+
+        @type args: list of strings
+        @param args: Command and argument vector
+        @type env: dict
+        @param env: Extra environment"""
         return RunWithEnv.run(self, args, env).cwd(self.cwd)
 
 class Repository(RunWithEnv):
         return RunWithEnv.run(self, args, env).cwd(self.cwd)
 
 class Repository(RunWithEnv):
+    """Represents a git repository."""
     def __init__(self, directory):
         self.__git_dir = directory
         self.__refs = Refs(self)
     def __init__(self, directory):
         self.__git_dir = directory
         self.__refs = Refs(self)
@@ -322,15 +380,20 @@ class Repository(RunWithEnv):
             raise RepositoryException('Cannot find git repository')
     @property
     def default_index(self):
             raise RepositoryException('Cannot find git repository')
     @property
     def default_index(self):
+        """An L{Index} object representing the default index file for the
+        repository."""
         if self.__default_index == None:
             self.__default_index = Index(
                 self, (os.environ.get('GIT_INDEX_FILE', None)
                        or os.path.join(self.__git_dir, 'index')))
         return self.__default_index
     def temp_index(self):
         if self.__default_index == None:
             self.__default_index = Index(
                 self, (os.environ.get('GIT_INDEX_FILE', None)
                        or os.path.join(self.__git_dir, 'index')))
         return self.__default_index
     def temp_index(self):
+        """Return an L{Index} object representing a new temporary index file
+        for the repository."""
         return Index(self, self.__git_dir)
     @property
     def default_worktree(self):
         return Index(self, self.__git_dir)
     @property
     def default_worktree(self):
+        """A L{Worktree} object representing the default work tree."""
         if self.__default_worktree == None:
             path = os.environ.get('GIT_WORK_TREE', None)
             if not path:
         if self.__default_worktree == None:
             path = os.environ.get('GIT_WORK_TREE', None)
             if not path:
@@ -342,6 +405,8 @@ class Repository(RunWithEnv):
         return self.__default_worktree
     @property
     def default_iw(self):
         return self.__default_worktree
     @property
     def default_iw(self):
+        """An L{IndexAndWorktree} object representing the default index and
+        work tree for this repository."""
         if self.__default_iw == None:
             self.__default_iw = IndexAndWorktree(self.default_index,
                                                  self.default_worktree)
         if self.__default_iw == None:
             self.__default_iw = IndexAndWorktree(self.default_index,
                                                  self.default_worktree)
@@ -387,9 +452,9 @@ class Repository(RunWithEnv):
     def set_head(self, ref, msg):
         self.run(['git', 'symbolic-ref', '-m', msg, 'HEAD', ref]).no_output()
     def simple_merge(self, base, ours, theirs):
     def set_head(self, ref, msg):
         self.run(['git', 'symbolic-ref', '-m', msg, 'HEAD', ref]).no_output()
     def simple_merge(self, base, ours, theirs):
-        """Given three trees, tries to do an in-index merge in a temporary
-        index with a temporary index. Returns the result tree, or None if
-        the merge failed (due to conflicts)."""
+        """Given three L{Tree}s, tries to do an in-index merge with a
+        temporary index. Returns the result L{Tree}, or None if the
+        merge failed (due to conflicts)."""
         assert isinstance(base, Tree)
         assert isinstance(ours, Tree)
         assert isinstance(theirs, Tree)
         assert isinstance(base, Tree)
         assert isinstance(ours, Tree)
         assert isinstance(theirs, Tree)
@@ -412,8 +477,8 @@ class Repository(RunWithEnv):
         finally:
             index.delete()
     def apply(self, tree, patch_text):
         finally:
             index.delete()
     def apply(self, tree, patch_text):
-        """Given a tree and a patch, will either return the new tree that
-        results when the patch is applied, or None if the patch
+        """Given a L{Tree} and a patch, will either return the new L{Tree}
+        that results when the patch is applied, or None if the patch
         couldn't be applied."""
         assert isinstance(tree, Tree)
         if not patch_text:
         couldn't be applied."""
         assert isinstance(tree, Tree)
         if not patch_text:
@@ -429,18 +494,26 @@ class Repository(RunWithEnv):
         finally:
             index.delete()
     def diff_tree(self, t1, t2, diff_opts):
         finally:
             index.delete()
     def diff_tree(self, t1, t2, diff_opts):
+        """Given two L{Tree}s C{t1} and C{t2}, return the patch that takes
+        C{t1} to C{t2}.
+
+        @type diff_opts: list of strings
+        @param diff_opts: Extra diff options
+        @rtype: String
+        @return: Patch text"""
         assert isinstance(t1, Tree)
         assert isinstance(t2, Tree)
         return self.run(['git', 'diff-tree', '-p'] + list(diff_opts)
                         + [t1.sha1, t2.sha1]).raw_output()
 
 class MergeException(exception.StgException):
         assert isinstance(t1, Tree)
         assert isinstance(t2, Tree)
         return self.run(['git', 'diff-tree', '-p'] + list(diff_opts)
                         + [t1.sha1, t2.sha1]).raw_output()
 
 class MergeException(exception.StgException):
-    pass
+    """Exception raised when a merge fails for some reason."""
 
 class MergeConflictException(MergeException):
 
 class MergeConflictException(MergeException):
-    pass
+    """Exception raised when a merge fails due to conflicts."""
 
 class Index(RunWithEnv):
 
 class Index(RunWithEnv):
+    """Represents a git index file."""
     def __init__(self, repository, filename):
         self.__repository = repository
         if os.path.isdir(filename):
     def __init__(self, repository, filename):
         self.__repository = repository
         if os.path.isdir(filename):
@@ -492,15 +565,20 @@ class Index(RunWithEnv):
         return paths
 
 class Worktree(object):
         return paths
 
 class Worktree(object):
+    """Represents a git worktree (that is, a checked-out file tree)."""
     def __init__(self, directory):
         self.__directory = directory
     env = property(lambda self: { 'GIT_WORK_TREE': '.' })
     directory = property(lambda self: self.__directory)
 
 class CheckoutException(exception.StgException):
     def __init__(self, directory):
         self.__directory = directory
     env = property(lambda self: { 'GIT_WORK_TREE': '.' })
     directory = property(lambda self: self.__directory)
 
 class CheckoutException(exception.StgException):
-    pass
+    """Exception raised when a checkout fails."""
 
 class IndexAndWorktree(RunWithEnvCwd):
 
 class IndexAndWorktree(RunWithEnvCwd):
+    """Represents a git index and a worktree. Anything that an index or
+    worktree can do on their own are handled by the L{Index} and
+    L{Worktree} classes; this class concerns itself with the
+    operations that require both."""
     def __init__(self, index, worktree):
         self.__index = index
         self.__worktree = worktree
     def __init__(self, index, worktree):
         self.__index = index
         self.__worktree = worktree
index 6a2b40d..f9e750e 100644 (file)
@@ -1,8 +1,12 @@
+"""A Python class hierarchy wrapping the StGit on-disk metadata."""
+
 import os.path
 from stgit import exception, utils
 from stgit.lib import git, stackupgrade
 
 class Patch(object):
 import os.path
 from stgit import exception, utils
 from stgit.lib import git, stackupgrade
 
 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
     def __init__(self, stack, name):
         self.__stack = stack
         self.__name = name
@@ -102,7 +106,8 @@ class PatchOrder(object):
     all = property(lambda self: self.applied + self.unapplied)
 
 class Patches(object):
     all = property(lambda self: self.applied + self.unapplied)
 
 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):
     def __init__(self, stack):
         self.__stack = stack
         def create_patch(name):
@@ -126,6 +131,8 @@ class Patches(object):
         return p
 
 class Stack(object):
         return p
 
 class Stack(object):
+    """Represents an StGit stack (that is, a git branch with some extra
+    metadata)."""
     def __init__(self, repository, name):
         self.__repository = repository
         self.__name = name
     def __init__(self, repository, name):
         self.__repository = repository
         self.__name = name
@@ -164,6 +171,8 @@ class Stack(object):
         return self.head == self.patches.get(self.patchorder.applied[-1]).commit
 
 class Repository(git.Repository):
         return self.head == self.patches.get(self.patchorder.applied[-1]).commit
 
 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
     def __init__(self, *args, **kwargs):
         git.Repository.__init__(self, *args, **kwargs)
         self.__stacks = {} # name -> Stack
index 874f81b..e47997e 100644 (file)
@@ -1,13 +1,19 @@
+"""The L{StackTransaction} class makes it possible to make complex
+updates to an StGit stack in a safe and convenient way."""
+
 from stgit import exception, utils
 from stgit.utils import any, all
 from stgit.out import *
 from stgit.lib import git
 
 class TransactionException(exception.StgException):
 from stgit import exception, utils
 from stgit.utils import any, all
 from stgit.out import *
 from stgit.lib import git
 
 class TransactionException(exception.StgException):
-    pass
+    """Exception raised when something goes wrong with a
+    L{StackTransaction}."""
 
 class TransactionHalted(TransactionException):
 
 class TransactionHalted(TransactionException):
-    pass
+    """Exception raised when a L{StackTransaction} stops part-way through.
+    Used to make a non-local jump from the transaction setup to the
+    part of the transaction code where the transaction is run."""
 
 def _print_current_patch(old_applied, new_applied):
     def now_at(pn):
 
 def _print_current_patch(old_applied, new_applied):
     def now_at(pn):
@@ -24,6 +30,7 @@ def _print_current_patch(old_applied, new_applied):
         now_at(new_applied[-1])
 
 class _TransPatchMap(dict):
         now_at(new_applied[-1])
 
 class _TransPatchMap(dict):
+    """Maps patch names to sha1 strings."""
     def __init__(self, stack):
         dict.__init__(self)
         self.__stack = stack
     def __init__(self, stack):
         dict.__init__(self)
         self.__stack = stack
@@ -34,6 +41,36 @@ class _TransPatchMap(dict):
             return self.__stack.patches.get(pn).commit
 
 class StackTransaction(object):
             return self.__stack.patches.get(pn).commit
 
 class StackTransaction(object):
+    """A stack transaction, used for making complex updates to an StGit
+    stack in one single operation that will either succeed or fail
+    cleanly.
+
+    The basic theory of operation is the following:
+
+      1. Create a transaction object.
+
+      2. Inside a::
+
+         try
+           ...
+         except TransactionHalted:
+           pass
+
+      block, update the transaction with e.g. methods like
+      L{pop_patches} and L{push_patch}. This may create new git
+      objects such as commits, but will not write any refs; this means
+      that in case of a fatal error we can just walk away, no clean-up
+      required.
+
+      (Some operations may need to touch your index and working tree,
+      though. But they are cleaned up when needed.)
+
+      3. After the C{try} block -- wheher or not the setup ran to
+      completion or halted part-way through by raising a
+      L{TransactionHalted} exception -- call the transaction's L{run}
+      method. This will either succeed in writing the updated state to
+      your refs and index+worktree, or fail without having done
+      anything."""
     def __init__(self, stack, msg, allow_conflicts = False):
         self.__stack = stack
         self.__msg = msg
     def __init__(self, stack, msg, allow_conflicts = False):
         self.__stack = stack
         self.__msg = msg
@@ -102,6 +139,8 @@ class StackTransaction(object):
         if iw:
             self.__checkout(self.__stack.head.data.tree, iw)
     def run(self, iw = None):
         if iw:
             self.__checkout(self.__stack.head.data.tree, iw)
     def run(self, iw = None):
+        """Execute the transaction. Will either succeed, or fail (with an
+        exception) and do nothing."""
         self.__check_consistency()
         new_head = self.__head
 
         self.__check_consistency()
         new_head = self.__head
 
@@ -152,7 +191,8 @@ class StackTransaction(object):
 
     def pop_patches(self, p):
         """Pop all patches pn for which p(pn) is true. Return the list of
 
     def pop_patches(self, p):
         """Pop all patches pn for which p(pn) is true. Return the list of
-        other patches that had to be popped to accomplish this."""
+        other patches that had to be popped to accomplish this. Always
+        succeeds."""
         popped = []
         for i in xrange(len(self.applied)):
             if p(self.applied[i]):
         popped = []
         for i in xrange(len(self.applied)):
             if p(self.applied[i]):
@@ -167,7 +207,8 @@ class StackTransaction(object):
 
     def delete_patches(self, p):
         """Delete all patches pn for which p(pn) is true. Return the list of
 
     def delete_patches(self, p):
         """Delete all patches pn for which p(pn) is true. Return the list of
-        other patches that had to be popped to accomplish this."""
+        other patches that had to be popped to accomplish this. Always
+        succeeds."""
         popped = []
         all_patches = self.applied + self.unapplied
         for i in xrange(len(self.applied)):
         popped = []
         all_patches = self.applied + self.unapplied
         for i in xrange(len(self.applied)):