From: "Karl Hasselström" <kha@treskal.com>
To: Catalin Marinas <catalin.marinas@gmail.com>
Cc: git@vger.kernel.org
Subject: [StGit PATCH] Some API documentation for the new infrastructure
Date: Tue, 03 Jun 2008 07:41:03 +0200 [thread overview]
Message-ID: <20080603053946.15327.29723.stgit@yoghurt> (raw)
Not all that comprehensive, but better than nothing.
Uses epydoc (http://epydoc.sourceforge.net/) markup.
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
I'm not 100% sure that epydoc is the way to go -- there seems to be a
few constructs, such as properties, that it really doesn't handle.
stgit/lib/git.py | 120 ++++++++++++++++++++++++++++++++++++++--------
stgit/lib/stack.py | 11 ++++
stgit/lib/transaction.py | 49 +++++++++++++++++--
3 files changed, 154 insertions(+), 26 deletions(-)
diff --git a/stgit/lib/git.py b/stgit/lib/git.py
index b3181cb..c9f01e3 100644
--- a/stgit/lib/git.py
+++ b/stgit/lib/git.py
@@ -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
+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):
- pass
+ """Base class for all exceptions due to failed L{Repository}
+ operations."""
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):
+ """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):
+ """Utility class that defines C{__reps__} in terms of C{__str__}."""
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):
@@ -34,6 +61,9 @@ def make_defaults(defaults):
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:
@@ -54,8 +84,8 @@ class TimeZone(tzinfo, Repr):
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)
@@ -88,12 +118,15 @@ class Date(Repr):
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)
-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)
@@ -146,16 +179,16 @@ class Person(Repr):
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
-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)
@@ -223,8 +256,10 @@ class CommitData(Repr):
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
@@ -241,21 +276,26 @@ class Commit(Repr):
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):
+ """(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):
- """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):
+ """Check if the given ref exists."""
try:
self.get(ref)
except KeyError:
@@ -263,6 +303,8 @@ class Refs(object):
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)
@@ -272,6 +314,7 @@ class Refs(object):
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',
@@ -280,7 +323,8 @@ class Refs(object):
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
@@ -296,13 +340,27 @@ class ObjectCache(object):
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 = {}):
+ """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):
+ """Represents a git repository."""
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):
+ """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):
+ """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):
+ """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:
@@ -342,6 +405,8 @@ class Repository(RunWithEnv):
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)
@@ -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):
- """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)
@@ -412,8 +477,8 @@ class Repository(RunWithEnv):
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:
@@ -429,18 +494,26 @@ class Repository(RunWithEnv):
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):
- pass
+ """Exception raised when a merge fails for some reason."""
class MergeConflictException(MergeException):
- pass
+ """Exception raised when a merge fails due to conflicts."""
class Index(RunWithEnv):
+ """Represents a git index file."""
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):
+ """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):
- pass
+ """Exception raised when a checkout fails."""
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
diff --git a/stgit/lib/stack.py b/stgit/lib/stack.py
index 6a2b40d..f9e750e 100644
--- a/stgit/lib/stack.py
+++ b/stgit/lib/stack.py
@@ -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):
+ """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
@@ -102,7 +106,8 @@ class PatchOrder(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):
@@ -126,6 +131,8 @@ class Patches(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
@@ -164,6 +171,8 @@ class Stack(object):
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
diff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py
index 874f81b..e47997e 100644
--- a/stgit/lib/transaction.py
+++ b/stgit/lib/transaction.py
@@ -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):
- pass
+ """Exception raised when something goes wrong with a
+ L{StackTransaction}."""
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):
@@ -24,6 +30,7 @@ def _print_current_patch(old_applied, new_applied):
now_at(new_applied[-1])
class _TransPatchMap(dict):
+ """Maps patch names to sha1 strings."""
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):
+ """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
@@ -102,6 +139,8 @@ class StackTransaction(object):
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
@@ -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
- 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]):
@@ -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
- 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)):
reply other threads:[~2008-06-03 5:42 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20080603053946.15327.29723.stgit@yoghurt \
--to=kha@treskal.com \
--cc=catalin.marinas@gmail.com \
--cc=git@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).