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