From bb623c2f346923414280eb6b36bc3df27c41f2ce Mon Sep 17 00:00:00 2001 From: Austin King Date: Wed, 26 Jan 2011 22:22:59 -0800 Subject: [PATCH] Adding back GitPython 0.1.7 --- lib/python/git/__init__.py | 27 ++ lib/python/git/actor.py | 42 +++ lib/python/git/blob.py | 161 ++++++++++++ lib/python/git/cmd.py | 221 ++++++++++++++++ lib/python/git/commit.py | 289 +++++++++++++++++++++ lib/python/git/diff.py | 71 +++++ lib/python/git/errors.py | 32 +++ lib/python/git/head.py | 118 +++++++++ lib/python/git/lazy.py | 32 +++ lib/python/git/repo.py | 517 +++++++++++++++++++++++++++++++++++++ lib/python/git/stats.py | 59 +++++ lib/python/git/tag.py | 92 +++++++ lib/python/git/tree.py | 108 ++++++++ lib/python/git/utils.py | 27 ++ 14 files changed, 1796 insertions(+) create mode 100644 lib/python/git/__init__.py create mode 100644 lib/python/git/actor.py create mode 100644 lib/python/git/blob.py create mode 100644 lib/python/git/cmd.py create mode 100644 lib/python/git/commit.py create mode 100644 lib/python/git/diff.py create mode 100644 lib/python/git/errors.py create mode 100644 lib/python/git/head.py create mode 100644 lib/python/git/lazy.py create mode 100644 lib/python/git/repo.py create mode 100644 lib/python/git/stats.py create mode 100644 lib/python/git/tag.py create mode 100644 lib/python/git/tree.py create mode 100644 lib/python/git/utils.py diff --git a/lib/python/git/__init__.py b/lib/python/git/__init__.py new file mode 100644 index 0000000..7c31bcb --- /dev/null +++ b/lib/python/git/__init__.py @@ -0,0 +1,27 @@ +# __init__.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import os +import inspect + +__version__ = '0.1.7' + +from git.actor import Actor +from git.blob import Blob +from git.commit import Commit +from git.diff import Diff +from git.errors import InvalidGitRepositoryError, NoSuchPathError, GitCommandError +from git.cmd import Git +from git.head import Head +from git.repo import Repo +from git.stats import Stats +from git.tag import Tag +from git.tree import Tree +from git.utils import dashify +from git.utils import touch + +__all__ = [ name for name, obj in locals().items() + if not (name.startswith('_') or inspect.ismodule(obj)) ] diff --git a/lib/python/git/actor.py b/lib/python/git/actor.py new file mode 100644 index 0000000..de85a20 --- /dev/null +++ b/lib/python/git/actor.py @@ -0,0 +1,42 @@ +# actor.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import re + +class Actor(object): + """Actors hold information about a person acting on the repository. They + can be committers and authors or anything with a name and an email as + mentioned in the git log entries.""" + def __init__(self, name, email): + self.name = name + self.email = email + + def __str__(self): + return self.name + + def __repr__(self): + return '">' % (self.name, self.email) + + @classmethod + def from_string(cls, string): + """ + Create an Actor from a string. + + ``str`` + is the string, which is expected to be in regular git format + + Format + John Doe + + Returns + Actor + """ + if re.search(r'<.+>', string): + m = re.search(r'(.*) <(.+?)>', string) + name, email = m.groups() + return Actor(name, email) + else: + return Actor(string, None) diff --git a/lib/python/git/blob.py b/lib/python/git/blob.py new file mode 100644 index 0000000..e63037d --- /dev/null +++ b/lib/python/git/blob.py @@ -0,0 +1,161 @@ +# blob.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import mimetypes +import os +import re +import time +from actor import Actor +from commit import Commit + +class Blob(object): + """A Blob encapsulates a git blob object""" + DEFAULT_MIME_TYPE = "text/plain" + + def __init__(self, repo, id, mode=None, name=None): + """ + Create an unbaked Blob containing just the specified attributes + + ``repo`` + is the Repo + + ``id`` + is the git object id + + ``mode`` + is the file mode + + ``name`` + is the file name + + Returns + git.Blob + """ + self.repo = repo + self.id = id + self.mode = mode + self.name = name + + self._size = None + self.data_stored = None + + @property + def size(self): + """ + The size of this blob in bytes + + Returns + int + + NOTE + The size will be cached after the first access + """ + if self._size is None: + self._size = int(self.repo.git.cat_file(self.id, s=True).rstrip()) + return self._size + + @property + def data(self): + """ + The binary contents of this blob. + + Returns + str + + NOTE + The data will be cached after the first access. + """ + self.data_stored = self.data_stored or self.repo.git.cat_file(self.id, p=True, with_raw_output=True) + return self.data_stored + + @property + def mime_type(self): + """ + The mime type of this file (based on the filename) + + Returns + str + + NOTE + Defaults to 'text/plain' in case the actual file type is unknown. + """ + guesses = None + if self.name: + guesses = mimetypes.guess_type(self.name) + return guesses and guesses[0] or self.DEFAULT_MIME_TYPE + + @property + def basename(self): + """ + Returns + The basename of the Blobs file name + """ + return os.path.basename(self.name) + + @classmethod + def blame(cls, repo, commit, file): + """ + The blame information for the given file at the given commit + + Returns + list: [git.Commit, list: []] + A list of tuples associating a Commit object with a list of lines that + changed within the given commit. The Commit objects will be given in order + of appearance. + """ + data = repo.git.blame(commit, '--', file, p=True) + commits = {} + blames = [] + info = None + + for line in data.splitlines(): + parts = re.split(r'\s+', line, 1) + if re.search(r'^[0-9A-Fa-f]{40}$', parts[0]): + if re.search(r'^([0-9A-Fa-f]{40}) (\d+) (\d+) (\d+)$', line): + m = re.search(r'^([0-9A-Fa-f]{40}) (\d+) (\d+) (\d+)$', line) + id, origin_line, final_line, group_lines = m.groups() + info = {'id': id} + blames.append([None, []]) + elif re.search(r'^([0-9A-Fa-f]{40}) (\d+) (\d+)$', line): + m = re.search(r'^([0-9A-Fa-f]{40}) (\d+) (\d+)$', line) + id, origin_line, final_line = m.groups() + info = {'id': id} + elif re.search(r'^(author|committer)', parts[0]): + if re.search(r'^(.+)-mail$', parts[0]): + m = re.search(r'^(.+)-mail$', parts[0]) + info["%s_email" % m.groups()[0]] = parts[-1] + elif re.search(r'^(.+)-time$', parts[0]): + m = re.search(r'^(.+)-time$', parts[0]) + info["%s_date" % m.groups()[0]] = time.gmtime(int(parts[-1])) + elif re.search(r'^(author|committer)$', parts[0]): + m = re.search(r'^(author|committer)$', parts[0]) + info[m.groups()[0]] = parts[-1] + elif re.search(r'^filename', parts[0]): + info['filename'] = parts[-1] + elif re.search(r'^summary', parts[0]): + info['summary'] = parts[-1] + elif parts[0] == '': + if info: + c = commits.has_key(info['id']) and commits[info['id']] + if not c: + c = Commit(repo, id=info['id'], + author=Actor.from_string(info['author'] + ' ' + info['author_email']), + authored_date=info['author_date'], + committer=Actor.from_string(info['committer'] + ' ' + info['committer_email']), + committed_date=info['committer_date'], + message=info['summary']) + commits[info['id']] = c + + m = re.search(r'^\t(.*)$', line) + text, = m.groups() + blames[-1][0] = c + blames[-1][1].append( text ) + info = None + + return blames + + def __repr__(self): + return '' % self.id diff --git a/lib/python/git/cmd.py b/lib/python/git/cmd.py new file mode 100644 index 0000000..9831419 --- /dev/null +++ b/lib/python/git/cmd.py @@ -0,0 +1,221 @@ +# cmd.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import os, sys +import subprocess +import re +from utils import * +from errors import GitCommandError + +# Enables debugging of GitPython's git commands +GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False) + +execute_kwargs = ('istream', 'with_keep_cwd', 'with_extended_output', + 'with_exceptions', 'with_raw_output') + +extra = {} +if sys.platform == 'win32': + extra = {'shell': True} + +class Git(object): + """ + The Git class manages communication with the Git binary. + + It provides a convenient interface to calling the Git binary, such as in:: + + g = Git( git_dir ) + g.init() # calls 'git init' program + rval = g.ls_files() # calls 'git ls-files' program + + ``Debugging`` + Set the GIT_PYTHON_TRACE environment variable print each invocation + of the command to stdout. + Set its value to 'full' to see details about the returned values. + """ + def __init__(self, git_dir=None): + """ + Initialize this instance with: + + ``git_dir`` + Git directory we should work in. If None, we always work in the current + directory as returned by os.getcwd() + """ + super(Git, self).__init__() + self.git_dir = git_dir + + def __getattr__(self, name): + """ + A convenience method as it allows to call the command as if it was + an object. + Returns + Callable object that will execute call _call_process with your arguments. + """ + if name[:1] == '_': + raise AttributeError(name) + return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) + + @property + def get_dir(self): + """ + Returns + Git directory we are working on + """ + return self.git_dir + + def execute(self, command, + istream=None, + with_keep_cwd=False, + with_extended_output=False, + with_exceptions=True, + with_raw_output=False, + ): + """ + Handles executing the command on the shell and consumes and returns + the returned information (stdout) + + ``command`` + The command argument list to execute. + It should be a string, or a sequence of program arguments. The + program to execute is the first item in the args sequence or string. + + ``istream`` + Standard input filehandle passed to subprocess.Popen. + + ``with_keep_cwd`` + Whether to use the current working directory from os.getcwd(). + GitPython uses get_work_tree() as its working directory by + default and get_git_dir() for bare repositories. + + ``with_extended_output`` + Whether to return a (status, stdout, stderr) tuple. + + ``with_exceptions`` + Whether to raise an exception when git returns a non-zero status. + + ``with_raw_output`` + Whether to avoid stripping off trailing whitespace. + + Returns:: + + str(output) # extended_output = False (Default) + tuple(int(status), str(stdout), str(stderr)) # extended_output = True + + Raise + GitCommandError + + NOTE + If you add additional keyword arguments to the signature of this method, + you must update the execute_kwargs tuple housed in this module. + """ + if GIT_PYTHON_TRACE and not GIT_PYTHON_TRACE == 'full': + print ' '.join(command) + + # Allow the user to have the command executed in their working dir. + if with_keep_cwd or self.git_dir is None: + cwd = os.getcwd() + else: + cwd=self.git_dir + + # Start the process + proc = subprocess.Popen(command, + cwd=cwd, + stdin=istream, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + **extra + ) + + # Wait for the process to return + try: + stdout_value = proc.stdout.read() + stderr_value = proc.stderr.read() + status = proc.wait() + finally: + proc.stdout.close() + proc.stderr.close() + + # Strip off trailing whitespace by default + if not with_raw_output: + stdout_value = stdout_value.rstrip() + stderr_value = stderr_value.rstrip() + + if with_exceptions and status != 0: + raise GitCommandError(command, status, stderr_value) + + if GIT_PYTHON_TRACE == 'full': + if stderr_value: + print "%s -> %d: '%s' !! '%s'" % (command, status, stdout_value, stderr_value) + elif stdout_value: + print "%s -> %d: '%s'" % (command, status, stdout_value) + else: + print "%s -> %d" % (command, status) + + # Allow access to the command's status code + if with_extended_output: + return (status, stdout_value, stderr_value) + else: + return stdout_value + + def transform_kwargs(self, **kwargs): + """ + Transforms Python style kwargs into git command line options. + """ + args = [] + for k, v in kwargs.items(): + if len(k) == 1: + if v is True: + args.append("-%s" % k) + elif type(v) is not bool: + args.append("-%s%s" % (k, v)) + else: + if v is True: + args.append("--%s" % dashify(k)) + elif type(v) is not bool: + args.append("--%s=%s" % (dashify(k), v)) + return args + + def _call_process(self, method, *args, **kwargs): + """ + Run the given git command with the specified arguments and return + the result as a String + + ``method`` + is the command. Contained "_" characters will be converted to dashes, + such as in 'ls_files' to call 'ls-files'. + + ``args`` + is the list of arguments + + ``kwargs`` + is a dict of keyword arguments. + This function accepts the same optional keyword arguments + as execute(). + + Examples:: + git.rev_list('master', max_count=10, header=True) + + Returns + Same as execute() + """ + + # Handle optional arguments prior to calling transform_kwargs + # otherwise these'll end up in args, which is bad. + _kwargs = {} + for kwarg in execute_kwargs: + try: + _kwargs[kwarg] = kwargs.pop(kwarg) + except KeyError: + pass + + # Prepare the argument list + opt_args = self.transform_kwargs(**kwargs) + ext_args = map(str, args) + args = opt_args + ext_args + + call = ["git", dashify(method)] + call.extend(args) + + return self.execute(call, **_kwargs) diff --git a/lib/python/git/commit.py b/lib/python/git/commit.py new file mode 100644 index 0000000..b074d3a --- /dev/null +++ b/lib/python/git/commit.py @@ -0,0 +1,289 @@ +# commit.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import re +import time + +from actor import Actor +from lazy import LazyMixin +from tree import Tree +import diff +import stats + +class Commit(LazyMixin): + """ + Wraps a git Commit object. + + This class will act lazily on some of its attributes and will query the + value on demand only if it involves calling the git binary. + """ + def __init__(self, repo, id, tree=None, author=None, authored_date=None, + committer=None, committed_date=None, message=None, parents=None): + """ + Instantiate a new Commit. All keyword arguments taking None as default will + be implicitly set if id names a valid sha. + + The parameter documentation indicates the type of the argument after a colon ':'. + + ``id`` + is the sha id of the commit + + ``parents`` : list( Commit, ... ) + is a list of commit ids + + ``tree`` : Tree + is the corresponding tree id + + ``author`` : Actor + is the author string ( will be implicitly converted into an Actor object ) + + ``authored_date`` : (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst ) + is the authored DateTime + + ``committer`` : Actor + is the committer string + + ``committed_date`` : (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst) + is the committed DateTime + + ``message`` : string + is the commit message + + Returns + git.Commit + """ + LazyMixin.__init__(self) + + self.repo = repo + self.id = id + self.parents = None + self.tree = None + self.author = author + self.authored_date = authored_date + self.committer = committer + self.committed_date = committed_date + self.message = message + + if self.id: + if parents is not None: + self.parents = [Commit(repo, p) for p in parents] + if tree is not None: + self.tree = Tree(repo, id=tree) + + def __bake__(self): + """ + Called by LazyMixin superclass when the first uninitialized member needs + to be set as it is queried. + """ + temp = Commit.find_all(self.repo, self.id, max_count=1)[0] + self.parents = temp.parents + self.tree = temp.tree + self.author = temp.author + self.authored_date = temp.authored_date + self.committer = temp.committer + self.committed_date = temp.committed_date + self.message = temp.message + + @property + def id_abbrev(self): + """ + Returns + First 7 bytes of the commit's sha id as an abbreviation of the full string. + """ + return self.id[0:7] + + @property + def summary(self): + """ + Returns + First line of the commit message. + """ + return self.message.split('\n', 1)[0] + + @classmethod + def count(cls, repo, ref, path=''): + """ + Count the number of commits reachable from this ref + + ``repo`` + is the Repo + + ``ref`` + is the ref from which to begin (SHA1 or name) + + ``path`` + is an optional path + + Returns + int + """ + return len(repo.git.rev_list(ref, '--', path).strip().splitlines()) + + @classmethod + def find_all(cls, repo, ref, path='', **kwargs): + """ + Find all commits matching the given criteria. + ``repo`` + is the Repo + + ``ref`` + is the ref from which to begin (SHA1 or name) + + ``path`` + is an optinal path, if set only Commits that include the path + will be considered + + ``kwargs`` + optional keyword arguments to git where + ``max_count`` is the maximum number of commits to fetch + ``skip`` is the number of commits to skip + + Returns + git.Commit[] + """ + options = {'pretty': 'raw'} + options.update(kwargs) + + output = repo.git.rev_list(ref, '--', path, **options) + return cls.list_from_string(repo, output) + + @classmethod + def list_from_string(cls, repo, text): + """ + Parse out commit information into a list of Commit objects + + ``repo`` + is the Repo + + ``text`` + is the text output from the git-rev-list command (raw format) + + Returns + git.Commit[] + """ + lines = [l for l in text.splitlines() if l.strip()] + + commits = [] + + while lines: + id = lines.pop(0).split()[1] + tree = lines.pop(0).split()[1] + + parents = [] + while lines and lines[0].startswith('parent'): + parents.append(lines.pop(0).split()[-1]) + author, authored_date = cls.actor(lines.pop(0)) + committer, committed_date = cls.actor(lines.pop(0)) + + messages = [] + while lines and lines[0].startswith(' '): + messages.append(lines.pop(0).strip()) + + message = '\n'.join(messages) + + commits.append(Commit(repo, id=id, parents=parents, tree=tree, author=author, authored_date=authored_date, + committer=committer, committed_date=committed_date, message=message)) + + return commits + + @classmethod + def diff(cls, repo, a, b=None, paths=None): + """ + Creates diffs between a tree and the index or between two trees: + + ``repo`` + is the Repo + + ``a`` + is a named commit + + ``b`` + is an optional named commit. Passing a list assumes you + wish to omit the second named commit and limit the diff to the + given paths. + + ``paths`` + is a list of paths to limit the diff to. + + Returns + git.Diff[]:: + + between tree and the index if only a is given + between two trees if a and b are given and are commits + """ + paths = paths or [] + + if isinstance(b, list): + paths = b + b = None + + if paths: + paths.insert(0, "--") + + if b: + paths.insert(0, b) + paths.insert(0, a) + text = repo.git.diff('-M', full_index=True, *paths) + return diff.Diff.list_from_string(repo, text) + + @property + def diffs(self): + """ + Returns + git.Diff[] + Diffs between this commit and its first parent or all changes if this + commit is the first commit and has no parent. + """ + if not self.parents: + d = self.repo.git.show(self.id, '-M', full_index=True, pretty='raw') + if re.search(r'diff --git a', d): + if not re.search(r'^diff --git a', d): + p = re.compile(r'.+?(diff --git a)', re.MULTILINE | re.DOTALL) + d = p.sub(r'diff --git a', d, 1) + else: + d = '' + return diff.Diff.list_from_string(self.repo, d) + else: + return self.diff(self.repo, self.parents[0].id, self.id) + + @property + def stats(self): + """ + Create a git stat from changes between this commit and its first parent + or from all changes done if this is the very first commit. + + Return + git.Stats + """ + if not self.parents: + text = self.repo.git.diff_tree(self.id, '--', numstat=True, root=True) + text2 = "" + for line in text.splitlines()[1:]: + (insertions, deletions, filename) = line.split("\t") + text2 += "%s\t%s\t%s\n" % (insertions, deletions, filename) + text = text2 + else: + text = self.repo.git.diff(self.parents[0].id, self.id, '--', numstat=True) + return stats.Stats.list_from_string(self.repo, text) + + def __str__(self): + """ Convert commit to string which is SHA1 """ + return self.id + + def __repr__(self): + return '' % self.id + + @classmethod + def actor(cls, line): + """ + Parse out the actor (author or committer) info + + Returns + [Actor, gmtime(acted at time)] + """ + m = re.search(r'^.+? (.*) (\d+) .*$', line) + actor, epoch = m.groups() + return [Actor.from_string(actor), time.gmtime(int(epoch))] diff --git a/lib/python/git/diff.py b/lib/python/git/diff.py new file mode 100644 index 0000000..6c4ba65 --- /dev/null +++ b/lib/python/git/diff.py @@ -0,0 +1,71 @@ +# diff.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import re +import commit + +class Diff(object): + """ + A Diff contains diff information between two commits. + """ + + def __init__(self, repo, a_path, b_path, a_commit, b_commit, a_mode, + b_mode, new_file, deleted_file, rename_from, + rename_to, diff): + self.repo = repo + self.a_path = a_path + self.b_path = b_path + + if not a_commit or re.search(r'^0{40}$', a_commit): + self.a_commit = None + else: + self.a_commit = commit.Commit(repo, id=a_commit) + if not b_commit or re.search(r'^0{40}$', b_commit): + self.b_commit = None + else: + self.b_commit = commit.Commit(repo, id=b_commit) + + self.a_mode = a_mode + self.b_mode = b_mode + self.new_file = new_file + self.deleted_file = deleted_file + self.rename_from = rename_from + self.rename_to = rename_to + self.renamed = rename_from != rename_to + self.diff = diff + + @classmethod + def list_from_string(cls, repo, text): + diffs = [] + + diff_header = re.compile(r""" + #^diff[ ]--git + [ ]a/(?P\S+)[ ]b/(?P\S+)\n + (?:^similarity[ ]index[ ](?P\d+)%\n + ^rename[ ]from[ ](?P\S+)\n + ^rename[ ]to[ ](?P\S+)(?:\n|$))? + (?:^old[ ]mode[ ](?P\d+)\n + ^new[ ]mode[ ](?P\d+)(?:\n|$))? + (?:^new[ ]file[ ]mode[ ](?P.+)(?:\n|$))? + (?:^deleted[ ]file[ ]mode[ ](?P.+)(?:\n|$))? + (?:^index[ ](?P[0-9A-Fa-f]+) + \.\.(?P[0-9A-Fa-f]+)[ ]?(?P.+)?(?:\n|$))? + """, re.VERBOSE | re.MULTILINE).match + + for diff in ('\n' + text).split('\ndiff --git')[1:]: + header = diff_header(diff) + + a_path, b_path, similarity_index, rename_from, rename_to, \ + old_mode, new_mode, new_file_mode, deleted_file_mode, \ + a_commit, b_commit, b_mode = header.groups() + new_file, deleted_file = bool(new_file_mode), bool(deleted_file_mode) + + diffs.append(Diff(repo, a_path, b_path, a_commit, b_commit, + old_mode or deleted_file_mode, new_mode or new_file_mode or b_mode, + new_file, deleted_file, rename_from, rename_to, diff[header.end():])) + + return diffs + diff --git a/lib/python/git/errors.py b/lib/python/git/errors.py new file mode 100644 index 0000000..86debaa --- /dev/null +++ b/lib/python/git/errors.py @@ -0,0 +1,32 @@ +# errors.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php +""" +Module containing all exceptions thrown througout the git package, +""" + +class InvalidGitRepositoryError(Exception): + """ + Thrown if the given repository appears to have an invalid format. + """ + +class NoSuchPathError(Exception): + """ + Thrown if a path could not be access by the system. + """ + +class GitCommandError(Exception): + """ + Thrown if execution of the git command fails with non-zero status code. + """ + def __init__(self, command, status, stderr=None): + self.stderr = stderr + self.status = status + self.command = command + + def __str__(self): + return repr("%s returned exit status %d" % + (str(self.command), self.status)) + diff --git a/lib/python/git/head.py b/lib/python/git/head.py new file mode 100644 index 0000000..f2e9e3f --- /dev/null +++ b/lib/python/git/head.py @@ -0,0 +1,118 @@ +# head.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import commit + +class Head(object): + """ + A Head is a named reference to a Commit. Every Head instance contains a name + and a Commit object. + + Examples:: + + >>> repo = Repo("/path/to/repo") + >>> head = repo.heads[0] + + >>> head.name + 'master' + + >>> head.commit + + + >>> head.commit.id + '1c09f116cbc2cb4100fb6935bb162daa4723f455' + """ + + def __init__(self, name, commit): + """ + Initialize a newly instanced Head + + `name` + is the name of the head + + `commit` + is the Commit object that the head points to + """ + self.name = name + self.commit = commit + + @classmethod + def find_all(cls, repo, **kwargs): + """ + Find all Heads in the repository + + `repo` + is the Repo + + `kwargs` + Additional options given as keyword arguments, will be passed + to git-for-each-ref + + Returns + git.Head[] + + List is sorted by committerdate + """ + + options = {'sort': "committerdate", + 'format': "%(refname)%00%(objectname)"} + options.update(kwargs) + + output = repo.git.for_each_ref("refs/heads", **options) + return cls.list_from_string(repo, output) + + @classmethod + def list_from_string(cls, repo, text): + """ + Parse out head information into a list of head objects + + ``repo`` + is the Repo + ``text`` + is the text output from the git-for-each-ref command + + Returns + git.Head[] + """ + heads = [] + + for line in text.splitlines(): + heads.append(cls.from_string(repo, line)) + + return heads + + @classmethod + def from_string(cls, repo, line): + """ + Create a new Head instance from the given string. + + ``repo`` + is the Repo + + ``line`` + is the formatted head information + + Format:: + + name: [a-zA-Z_/]+ + + id: [0-9A-Fa-f]{40} + + Returns + git.Head + """ + full_name, ids = line.split("\x00") + + if full_name.startswith('refs/heads/'): + name = full_name[len('refs/heads/'):] + else: + name = full_name + + c = commit.Commit(repo, id=ids) + return Head(name, c) + + def __repr__(self): + return '' % self.name diff --git a/lib/python/git/lazy.py b/lib/python/git/lazy.py new file mode 100644 index 0000000..59c5f06 --- /dev/null +++ b/lib/python/git/lazy.py @@ -0,0 +1,32 @@ +# lazy.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +class LazyMixin(object): + lazy_properties = [] + + def __init__(self): + self.__baked__ = False + + def __getattribute__(self, attr): + val = object.__getattribute__(self, attr) + if val is not None: + return val + else: + self.__prebake__() + return object.__getattribute__(self, attr) + + def __bake__(self): + """ This method should be overridden in the derived class. """ + raise NotImplementedError(" '__bake__' method has not been implemented.") + + def __prebake__(self): + if self.__baked__: + return + self.__bake__() + self.__baked__ = True + + def __bake_it__(self): + self.__baked__ = True diff --git a/lib/python/git/repo.py b/lib/python/git/repo.py new file mode 100644 index 0000000..cfc0913 --- /dev/null +++ b/lib/python/git/repo.py @@ -0,0 +1,517 @@ +# repo.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import os +import re +import gzip +import StringIO +from errors import InvalidGitRepositoryError, NoSuchPathError +from utils import touch, is_git_dir +from cmd import Git +from head import Head +from blob import Blob +from tag import Tag +from commit import Commit +from tree import Tree + +class Repo(object): + """ + Represents a git repository and allows you to query references, + gather commit information, generate diffs, create and clone repositories query + the log. + """ + DAEMON_EXPORT_FILE = 'git-daemon-export-ok' + + def __init__(self, path=None): + """ + Create a new Repo instance + + ``path`` + is the path to either the root git directory or the bare git repo + + Examples:: + + repo = Repo("/Users/mtrier/Development/git-python") + repo = Repo("/Users/mtrier/Development/git-python.git") + + Raises + InvalidGitRepositoryError or NoSuchPathError + + Returns + ``git.Repo`` + """ + + epath = os.path.abspath(os.path.expanduser(path or os.getcwd())) + + if not os.path.exists(epath): + raise NoSuchPathError(epath) + + self.path = None + curpath = epath + while curpath: + if is_git_dir(curpath): + self.bare = True + self.path = curpath + self.wd = curpath + break + gitpath = os.path.join(curpath, '.git') + if is_git_dir(gitpath): + self.bare = False + self.path = gitpath + self.wd = curpath + break + curpath, dummy = os.path.split(curpath) + if not dummy: + break + + if self.path is None: + raise InvalidGitRepositoryError(epath) + + self.git = Git(self.wd) + + # Description property + def _get_description(self): + filename = os.path.join(self.path, 'description') + return file(filename).read().rstrip() + + def _set_description(self, descr): + filename = os.path.join(self.path, 'description') + file(filename, 'w').write(descr+'\n') + + description = property(_get_description, _set_description, + doc="the project's description") + del _get_description + del _set_description + + @property + def heads(self): + """ + A list of ``Head`` objects representing the branch heads in + this repo + + Returns + ``git.Head[]`` + """ + return Head.find_all(self) + + # alias heads + branches = heads + + @property + def tags(self): + """ + A list of ``Tag`` objects that are available in this repo + + Returns + ``git.Tag[]`` + """ + return Tag.find_all(self) + + def commits(self, start='master', path='', max_count=10, skip=0): + """ + A list of Commit objects representing the history of a given ref/commit + + ``start`` + is the branch/commit name (default 'master') + + ``path`` + is an optional path to limit the returned commits to + Commits that do not contain that path will not be returned. + + ``max_count`` + is the maximum number of commits to return (default 10) + + ``skip`` + is the number of commits to skip (default 0) which will effectively + move your commit-window by the given number. + + Returns + ``git.Commit[]`` + """ + options = {'max_count': max_count, + 'skip': skip} + + return Commit.find_all(self, start, path, **options) + + def commits_between(self, frm, to): + """ + The Commits objects that are reachable via ``to`` but not via ``frm`` + Commits are returned in chronological order. + + ``from`` + is the branch/commit name of the younger item + + ``to`` + is the branch/commit name of the older item + + Returns + ``git.Commit[]`` + """ + return reversed(Commit.find_all(self, "%s..%s" % (frm, to))) + + def commits_since(self, start='master', path='', since='1970-01-01'): + """ + The Commits objects that are newer than the specified date. + Commits are returned in chronological order. + + ``start`` + is the branch/commit name (default 'master') + + ``path`` + is an optional path to limit the returned commits to. + + + ``since`` + is a string representing a date/time + + Returns + ``git.Commit[]`` + """ + options = {'since': since} + + return Commit.find_all(self, start, path, **options) + + def commit_count(self, start='master', path=''): + """ + The number of commits reachable by the given branch/commit + + ``start`` + is the branch/commit name (default 'master') + + ``path`` + is an optional path + Commits that do not contain the path will not contribute to the count. + + Returns + ``int`` + """ + return Commit.count(self, start, path) + + def commit(self, id, path = ''): + """ + The Commit object for the specified id + + ``id`` + is the SHA1 identifier of the commit + + ``path`` + is an optional path, if set the returned commit must contain the path. + + Returns + ``git.Commit`` + """ + options = {'max_count': 1} + + commits = Commit.find_all(self, id, path, **options) + + if not commits: + raise ValueError, "Invalid identifier %s, or given path '%s' too restrictive" % ( id, path ) + return commits[0] + + def commit_deltas_from(self, other_repo, ref='master', other_ref='master'): + """ + Returns a list of commits that is in ``other_repo`` but not in self + + Returns + git.Commit[] + """ + repo_refs = self.git.rev_list(ref, '--').strip().splitlines() + other_repo_refs = other_repo.git.rev_list(other_ref, '--').strip().splitlines() + + diff_refs = list(set(other_repo_refs) - set(repo_refs)) + return map(lambda ref: Commit.find_all(other_repo, ref, max_count=1)[0], diff_refs) + + def tree(self, treeish='master'): + """ + The Tree object for the given treeish reference + + ``treeish`` + is the reference (default 'master') + + Examples:: + + repo.tree('master') + + + Returns + ``git.Tree`` + """ + return Tree(self, id=treeish) + + def blob(self, id): + """ + The Blob object for the given id + + ``id`` + is the SHA1 id of the blob + + Returns + ``git.Blob`` + """ + return Blob(self, id=id) + + def log(self, commit='master', path=None, **kwargs): + """ + The Commit for a treeish, and all commits leading to it. + + ``kwargs`` + keyword arguments specifying flags to be used in git-log command, + i.e.: max_count=1 to limit the amount of commits returned + + Returns + ``git.Commit[]`` + """ + options = {'pretty': 'raw'} + options.update(kwargs) + arg = [commit, '--'] + if path: + arg.append(path) + commits = self.git.log(*arg, **options) + return Commit.list_from_string(self, commits) + + def diff(self, a, b, *paths): + """ + The diff from commit ``a`` to commit ``b``, optionally restricted to the given file(s) + + ``a`` + is the base commit + ``b`` + is the other commit + + ``paths`` + is an optional list of file paths on which to restrict the diff + + Returns + ``str`` + """ + return self.git.diff(a, b, '--', *paths) + + def commit_diff(self, commit): + """ + The commit diff for the given commit + ``commit`` is the commit name/id + + Returns + ``git.Diff[]`` + """ + return Commit.diff(self, commit) + + @classmethod + def init_bare(self, path, mkdir=True, **kwargs): + """ + Initialize a bare git repository at the given path + + ``path`` + is the full path to the repo (traditionally ends with /.git) + + ``mkdir`` + if specified will create the repository directory if it doesn't + already exists. Creates the directory with a mode=0755. + + ``kwargs`` + keyword arguments serving as additional options to the git init command + + Examples:: + + git.Repo.init_bare('/var/git/myrepo.git') + + Returns + ``git.Repo`` (the newly created repo) + """ + + if mkdir and not os.path.exists(path): + os.makedirs(path, 0755) + + git = Git(path) + output = git.init('--bare', **kwargs) + return Repo(path) + create = init_bare + + def fork_bare(self, path, **kwargs): + """ + Fork a bare git repository from this repo + + ``path`` + is the full path of the new repo (traditionally ends with /.git) + + ``kwargs`` + keyword arguments to be given to the git clone command + + Returns + ``git.Repo`` (the newly forked repo) + """ + options = {'bare': True} + options.update(kwargs) + self.git.clone(self.path, path, **options) + return Repo(path) + + def archive_tar(self, treeish='master', prefix=None): + """ + Archive the given treeish + + ``treeish`` + is the treeish name/id (default 'master') + + ``prefix`` + is the optional prefix to prepend to each filename in the archive + + Examples:: + + >>> repo.archive_tar + + + >>> repo.archive_tar('a87ff14') + + + >>> repo.archive_tar('master', 'myproject/') + + + Returns + str (containing bytes of tar archive) + """ + options = {} + if prefix: + options['prefix'] = prefix + return self.git.archive(treeish, **options) + + def archive_tar_gz(self, treeish='master', prefix=None): + """ + Archive and gzip the given treeish + + ``treeish`` + is the treeish name/id (default 'master') + + ``prefix`` + is the optional prefix to prepend to each filename in the archive + + Examples:: + + >>> repo.archive_tar_gz + + + >>> repo.archive_tar_gz('a87ff14') + + + >>> repo.archive_tar_gz('master', 'myproject/') + + + Returns + str (containing the bytes of tar.gz archive) + """ + kwargs = {} + if prefix: + kwargs['prefix'] = prefix + resultstr = self.git.archive(treeish, **kwargs) + sio = StringIO.StringIO() + gf = gzip.GzipFile(fileobj=sio, mode ='wb') + gf.write(resultstr) + gf.close() + return sio.getvalue() + + def _get_daemon_export(self): + filename = os.path.join(self.path, self.DAEMON_EXPORT_FILE) + return os.path.exists(filename) + + def _set_daemon_export(self, value): + filename = os.path.join(self.path, self.DAEMON_EXPORT_FILE) + fileexists = os.path.exists(filename) + if value and not fileexists: + touch(filename) + elif not value and fileexists: + os.unlink(filename) + + daemon_export = property(_get_daemon_export, _set_daemon_export, + doc="If True, git-daemon may export this repository") + del _get_daemon_export + del _set_daemon_export + + def _get_alternates(self): + """ + The list of alternates for this repo from which objects can be retrieved + + Returns + list of strings being pathnames of alternates + """ + alternates_path = os.path.join(self.path, 'objects', 'info', 'alternates') + + if os.path.exists(alternates_path): + try: + f = open(alternates_path) + alts = f.read() + finally: + f.close() + return alts.strip().splitlines() + else: + return [] + + def _set_alternates(self, alts): + """ + Sets the alternates + + ``alts`` + is the array of string paths representing the alternates at which + git should look for objects, i.e. /home/user/repo/.git/objects + + Raises + NoSuchPathError + + Note + The method does not check for the existance of the paths in alts + as the caller is responsible. + + Returns + None + """ + alternates_path = os.path.join(self.path, 'objects', 'info', 'alternates') + if not alts: + if os.path.isfile(alternates_path): + os.remove(alternates_path) + else: + try: + f = open(alternates_path, 'w') + f.write("\n".join(alts)) + finally: + f.close() + # END file handling + # END alts handling + + alternates = property(_get_alternates, _set_alternates, doc="Retrieve a list of alternates paths or set a list paths to be used as alternates") + + @property + def is_dirty(self): + """ + Return the status of the index. + + Returns + ``True``, if the index has any uncommitted changes, + otherwise ``False`` + + NOTE + Working tree changes that have not been staged will not be detected ! + """ + if self.bare: + # Bare repositories with no associated working directory are + # always considered to be clean. + return False + + return len(self.git.diff('HEAD', '--').strip()) > 0 + + @property + def active_branch(self): + """ + The name of the currently active branch. + + Returns + str (the branch name) + """ + branch = self.git.symbolic_ref('HEAD').strip() + if branch.startswith('refs/heads/'): + branch = branch[len('refs/heads/'):] + + return branch + + def __repr__(self): + return '' % self.path diff --git a/lib/python/git/stats.py b/lib/python/git/stats.py new file mode 100644 index 0000000..74f0aed --- /dev/null +++ b/lib/python/git/stats.py @@ -0,0 +1,59 @@ +# stats.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +class Stats(object): + """ + Represents stat information as presented by git at the end of a merge. It is + created from the output of a diff operation. + + ``Example``:: + + c = Commit( sha1 ) + s = c.stats + s.total # full-stat-dict + s.files # dict( filepath : stat-dict ) + + ``stat-dict`` + + A dictionary with the following keys and values:: + + deletions = number of deleted lines as int + insertions = number of inserted lines as int + lines = total number of lines changed as int, or deletions + insertions + + ``full-stat-dict`` + + In addition to the items in the stat-dict, it features additional information:: + + files = number of changed files as int + + """ + def __init__(self, repo, total, files): + self.repo = repo + self.total = total + self.files = files + + @classmethod + def list_from_string(cls, repo, text): + """ + Create a Stat object from output retrieved by git-diff. + + Returns + git.Stat + """ + hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, 'files': {}} + for line in text.splitlines(): + (raw_insertions, raw_deletions, filename) = line.split("\t") + insertions = raw_insertions != '-' and int(raw_insertions) or 0 + deletions = raw_deletions != '-' and int(raw_deletions) or 0 + hsh['total']['insertions'] += insertions + hsh['total']['deletions'] += deletions + hsh['total']['lines'] += insertions + deletions + hsh['total']['files'] += 1 + hsh['files'][filename.strip()] = {'insertions': insertions, + 'deletions': deletions, + 'lines': insertions + deletions} + return Stats(repo, hsh['total'], hsh['files']) diff --git a/lib/python/git/tag.py b/lib/python/git/tag.py new file mode 100644 index 0000000..8ec2f8a --- /dev/null +++ b/lib/python/git/tag.py @@ -0,0 +1,92 @@ +# tag.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +from commit import Commit + +class Tag(object): + def __init__(self, name, commit): + """ + Initialize a newly instantiated Tag + + ``name`` + is the name of the head + + ``commit`` + is the Commit that the head points to + """ + self.name = name + self.commit = commit + + @classmethod + def find_all(cls, repo, **kwargs): + """ + Find all Tags in the repository + + ``repo`` + is the Repo + + ``kwargs`` + Additional options given as keyword arguments, will be passed + to git-for-each-ref + + Returns + ``git.Tag[]`` + + List is sorted by committerdate + """ + options = {'sort': "committerdate", + 'format': "%(refname)%00%(objectname)"} + options.update(**kwargs) + + output = repo.git.for_each_ref("refs/tags", **options) + return cls.list_from_string(repo, output) + + @classmethod + def list_from_string(cls, repo, text): + """ + Parse out tag information into an array of Tag objects + + ``repo`` + is the Repo + + ``text`` + is the text output from the git-for-each command + + Returns + git.Tag[] + """ + tags = [] + for line in text.splitlines(): + tags.append(cls.from_string(repo, line)) + return tags + + @classmethod + def from_string(cls, repo, line): + """ + Create a new Tag instance from the given string. + + ``repo`` + is the Repo + + ``line`` + is the formatted tag information + + Format:: + + name: [a-zA-Z_/]+ + + id: [0-9A-Fa-f]{40} + + Returns + git.Tag + """ + full_name, ids = line.split("\x00") + name = full_name.split("/")[-1] + commit = Commit(repo, id=ids) + return Tag(name, commit) + + def __repr__(self): + return '' % self.name diff --git a/lib/python/git/tree.py b/lib/python/git/tree.py new file mode 100644 index 0000000..02266dc --- /dev/null +++ b/lib/python/git/tree.py @@ -0,0 +1,108 @@ +# tree.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import os +from lazy import LazyMixin +import blob + +class Tree(LazyMixin): + def __init__(self, repo, id, mode=None, name=None): + LazyMixin.__init__(self) + self.repo = repo + self.id = id + self.mode = mode + self.name = name + self._contents = None + + def __bake__(self): + # Ensure the treeish references directly a tree + treeish = self.id + if not treeish.endswith(':'): + treeish = treeish + ':' + + # Read the tree contents. + self._contents = {} + for line in self.repo.git.ls_tree(self.id).splitlines(): + obj = self.content_from_string(self.repo, line) + if obj is not None: + self._contents[obj.name] = obj + + @staticmethod + def content_from_string(repo, text): + """ + Parse a content item and create the appropriate object + + ``repo`` + is the Repo + + ``text`` + is the single line containing the items data in `git ls-tree` format + + Returns + ``git.Blob`` or ``git.Tree`` + """ + try: + mode, typ, id, name = text.expandtabs(1).split(" ", 3) + except: + return None + + if typ == "tree": + return Tree(repo, id=id, mode=mode, name=name) + elif typ == "blob": + return blob.Blob(repo, id=id, mode=mode, name=name) + elif typ == "commit": + return None + else: + raise(TypeError, "Invalid type: %s" % typ) + + def __div__(self, file): + """ + Find the named object in this tree's contents + + Examples:: + + >>> Repo('/path/to/python-git').tree()/'lib' + + >>> Repo('/path/to/python-git').tree()/'README' + + + Returns + ``git.Blob`` or ``git.Tree`` or ``None`` if not found + """ + return self.get(file) + + @property + def basename(self): + os.path.basename(self.name) + + def __repr__(self): + return '' % self.id + + # Implement the basics of the dict protocol: + # directories/trees can be seen as object dicts. + def __getitem__(self, key): + return self._contents[key] + + def __iter__(self): + return iter(self._contents) + + def __len__(self): + return len(self._contents) + + def __contains__(self, key): + return key in self._contents + + def get(self, key): + return self._contents.get(key) + + def items(self): + return self._contents.items() + + def keys(self): + return self._contents.keys() + + def values(self): + return self._contents.values() diff --git a/lib/python/git/utils.py b/lib/python/git/utils.py new file mode 100644 index 0000000..a7d7543 --- /dev/null +++ b/lib/python/git/utils.py @@ -0,0 +1,27 @@ +# utils.py +# Copyright (C) 2008-2010 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import os + +def dashify(string): + return string.replace('_', '-') + +def touch(filename): + fp = open(filename, 'a') + fp.close() + +def is_git_dir(d): + """ This is taken from the git setup.c:is_git_directory + function.""" + + if os.path.isdir(d) and \ + os.path.isdir(os.path.join(d, 'objects')) and \ + os.path.isdir(os.path.join(d, 'refs')): + headref = os.path.join(d, 'HEAD') + return os.path.isfile(headref) or \ + (os.path.islink(headref) and + os.readlink(headref).startswith('refs')) + return False