From 652b2e670f7666e4055fbcd5c2dd9e4275efee2a Mon Sep 17 00:00:00 2001 From: =?utf8?q?Karl=20Hasselstr=C3=B6m?= Date: Tue, 3 Jun 2008 05:03:16 +0200 Subject: [PATCH 1/1] Some API documentation for the new infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Not all that comprehensive, but better than nothing. Uses epydoc (http://epydoc.sourceforge.net/) markup. Signed-off-by: Karl Hasselström --- 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' % (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} 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)): -- 2.11.0