From f963881e53a9f0a2746a11cb9cdfa82eb1f90d8c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 6 Jul 2010 00:35:30 +0200 Subject: Initial version of the rev-parse routine, which doesn't work too bad, but its still rather slow and many tests are not yet implemented --- lib/git/exc.py | 4 +- lib/git/ext/gitdb | 2 +- lib/git/objects/base.py | 11 ++++ lib/git/refs.py | 19 +++++- lib/git/repo.py | 165 +++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 197 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/git/exc.py b/lib/git/exc.py index 93919d5e..d2cb8d7e 100644 --- a/lib/git/exc.py +++ b/lib/git/exc.py @@ -1,10 +1,12 @@ -# errors.py +# exc.py # Copyright (C) 2008, 2009 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, """ +from gitdb.exc import * + class InvalidGitRepositoryError(Exception): """ Thrown if the given repository appears to have an invalid format. """ diff --git a/lib/git/ext/gitdb b/lib/git/ext/gitdb index 6c8721a7..46bf4710 160000 --- a/lib/git/ext/gitdb +++ b/lib/git/ext/gitdb @@ -1 +1 @@ -Subproject commit 6c8721a7d5d32e54bb4ffd3725ed23ac5d76a593 +Subproject commit 46bf4710e0f7184ac4875e8037de30b5081bfda2 diff --git a/lib/git/objects/base.py b/lib/git/objects/base.py index d4a46788..21b9b1ea 100644 --- a/lib/git/objects/base.py +++ b/lib/git/objects/base.py @@ -53,6 +53,17 @@ class Object(LazyMixin): inst = get_object_type_by_name(typename)(repo, hex_to_bin(hexsha)) inst.size = size return inst + + @classmethod + def new_from_sha(cls, repo, sha1): + """ + :return: new object instance of a type appropriate to represent the given + binary sha1 + :param sha1: 20 byte binary sha1""" + oinfo = repo.odb.info(sha1) + inst = get_object_type_by_name(oinfo.type)(repo, oinfo.binsha) + inst.size = oinfo.size + return inst def _set_self_from_args_(self, args_dict): """Initialize attributes on self from the given dict that was retrieved diff --git a/lib/git/refs.py b/lib/git/refs.py index 343a0afb..a466e419 100644 --- a/lib/git/refs.py +++ b/lib/git/refs.py @@ -68,7 +68,7 @@ class SymbolicReference(object): :return: In case of symbolic references, the shortest assumable name is the path itself.""" - return self.path + return self.path def _abs_path(self): return join_path_native(self.repo.git_dir, self.path) @@ -109,6 +109,19 @@ class SymbolicReference(object): # I believe files are closing themselves on destruction, so it is # alright. + @classmethod + def dereference_recursive(cls, repo, ref_path): + """ + :return: hexsha stored in the reference at the given ref_path, recursively dereferencing all + intermediate references as required + :param repo: the repository containing the reference at ref_path""" + while True: + ref = cls(repo, ref_path) + hexsha, ref_path = ref._get_ref_info() + if hexsha is not None: + return hexsha + # END recursive dereferencing + def _get_ref_info(self): """Return: (sha, target_ref_path) if available, the sha the file at rela_path points to, or None. target_ref_path is the reference we @@ -794,6 +807,10 @@ class TagReference(Reference): else: raise ValueError( "Tag %s points to a Blob or Tree - have never seen that before" % self ) + @property + def tree(self): + return self.commit.tree + @property def tag(self): """ diff --git a/lib/git/repo.py b/lib/git/repo.py index 62202364..8e97adee 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -12,12 +12,13 @@ from index import IndexFile from objects import * from config import GitConfigParser from remote import Remote - +from string import digits from db import ( GitCmdObjectDB, GitDB ) +from gitdb.exc import BadObject from gitdb.util import ( join, isdir, @@ -70,6 +71,7 @@ class Repo(object): # precompiled regex re_whitespace = re.compile(r'\s+') re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$') + re_hexsha_shortened = re.compile('^[0-9A-Fa-f]{7:40}$') re_author_committer_start = re.compile(r'^(author|committer)') re_tab_full_line = re.compile(r'^\t(.*)$') @@ -698,6 +700,167 @@ class Repo(object): self.git.archive(treeish, **kwargs) return self + + def rev_parse(self, rev): + """ + :return: Object at the given revision, either Commit, Tag, Tree or Blob + :param rev: git-rev-parse compatible revision specification, please see + http://www.kernel.org/pub/software/scm/git/docs/git-rev-parse.html + for details + :note: Currently there is no access to the rev-log, rev-specs may only contain + topological tokens such ~ and ^. + :raise BadObject: if the given revision could not be found""" + if '@' in rev: + raise ValueError("There is no rev-log support yet") + + + # colon search mode ? + if rev.startswith(':/'): + # colon search mode + raise NotImplementedError("commit by message search ( regex )") + # END handle search + + # return object specified by the given name + def name_to_object(name): + hexsha = None + + # is it a hexsha ? + if self.re_hexsha_shortened.match(name): + if len(name) != 40: + # find long sha for short sha + raise NotImplementedError("short sha parsing") + else: + hexsha = name + # END handle short shas + else: + for base in ('%s', 'refs/%s', 'refs/tags/%s', 'refs/heads/%s', 'refs/remotes/%s', 'refs/remotes/%s/HEAD'): + try: + hexsha = SymbolicReference.dereference_recursive(self, base % name) + break + except ValueError: + pass + # END for each base + # END handle hexsha + + # tried everything ? fail + if hexsha is None: + raise BadObject(name) + # END assert hexsha was found + + return Object.new_from_sha(self, hex_to_bin(hexsha)) + # END object by name + + obj = None + output_type = "commit" + start = 0 + parsed_to = 0 + lr = len(rev) + while start < lr and start != -1: + if rev[start] not in "^~:": + start += 1 + continue + # END handle start + + if obj is None: + # token is a rev name + obj = name_to_object(rev[:start]) + # END initialize obj on first token + + token = rev[start] + start += 1 + + # try to parse {type} + if start < lr and rev[start] == '{': + end = rev.find('}', start) + if end == -1: + raise ValueError("Missing closing brace to define type in %s" % rev) + output_type = rev[start+1:end] # exclude brace + + # handle type + if output_type == 'commit': + pass # default + elif output_type == 'tree': + try: + obj = obj.tree + except AttributeError: + pass # error raised later + # END exception handling + elif output_type in ('', 'blob'): + while True: + try: + obj = obj.object + except AttributeError: + break + # END dereference tag + else: + raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev)) + # END handle output type + + if obj.type != output_type: + raise ValueError("Could not accomodate requested object type %s, got %s" % (output_type, obj.type)) + # END verify ouput type + + start = end+1 # skip brace + parsed_to = start + continue + # END parse type + + # try to parse a number + num = 0 + if token != ":": + while start < lr: + if rev[start] in digits: + num = num * 10 + int(rev[start]) + start += 1 + else: + break + # END handle number + # END number parse loop + + # no explicit number given, 1 is the default + if num == 0: + num = 1 + # END set default num + # END number parsing only if non-blob mode + + + parsed_to = start + # handle hiererarchy walk + try: + if token == "~": + for item in xrange(num): + obj = obj.parents[0] + # END for each history item to walk + elif token == "^": + # must be n'th parent + obj = obj.parents[num-1] + elif token == ":": + if obj.type != "tree": + obj = obj.tree + # END get tree type + obj = obj[rev[start:]] + parsed_to = lr + else: + raise "Invalid token: %r" % token + # END end handle tag + except (IndexError, AttributeError): + raise BadObject("Invalid Revision") + # END exception handling + # END parse loop + + # still no obj ? Its probably a simple name + if obj is None: + obj = name_to_object(rev) + parsed_to = lr + # END handle simple name + + if obj is None: + raise ValueError("Revision specifier could not be parsed: %s" % rev) + + if parsed_to != lr: + raise ValueError("Didn't consume complete rev spec %s, consumed part: %s" % (rev, rev[:parsed_to])) + + return obj def __repr__(self): return '' % self.git_dir -- cgit v1.2.3 From 1c6d7830d9b87f47a0bfe82b3b5424a32e3164ad Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 6 Jul 2010 10:46:02 +0200 Subject: RevParse now generally works, but there are still some more specialized tests missing --- lib/git/refs.py | 4 ---- lib/git/repo.py | 50 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 37 insertions(+), 17 deletions(-) (limited to 'lib') diff --git a/lib/git/refs.py b/lib/git/refs.py index a466e419..23d45ed0 100644 --- a/lib/git/refs.py +++ b/lib/git/refs.py @@ -807,10 +807,6 @@ class TagReference(Reference): else: raise ValueError( "Tag %s points to a Blob or Tree - have never seen that before" % self ) - @property - def tree(self): - return self.commit.tree - @property def tag(self): """ diff --git a/lib/git/repo.py b/lib/git/repo.py index 8e97adee..5a1af920 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -750,12 +750,31 @@ class Repo(object): return Object.new_from_sha(self, hex_to_bin(hexsha)) # END object by name + def deref_tag(tag): + while True: + try: + tag = tag.object + except AttributeError: + break + # END dereference tag + return tag + + def to_commit(obj): + if obj.type == 'tag': + obj = deref_tag(obj) + + if obj.type != "commit": + raise ValueError("Cannot convert object %r to type commit" % obj) + # END verify type + return obj + # END commit converter + obj = None output_type = "commit" start = 0 parsed_to = 0 lr = len(rev) - while start < lr and start != -1: + while start < lr: if rev[start] not in "^~:": start += 1 continue @@ -781,17 +800,17 @@ class Repo(object): pass # default elif output_type == 'tree': try: - obj = obj.tree - except AttributeError: + obj = to_commit(obj).tree + except (AttributeError, ValueError): pass # error raised later # END exception handling elif output_type in ('', 'blob'): - while True: - try: - obj = obj.object - except AttributeError: - break - # END dereference tag + if obj.type == 'tag': + obj = deref_tag(tag) + else: + # cannot do anything for non-tags + pass + # END handle tag else: raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev)) # END handle output type @@ -808,17 +827,20 @@ class Repo(object): # try to parse a number num = 0 if token != ":": + found_digit = False while start < lr: if rev[start] in digits: num = num * 10 + int(rev[start]) start += 1 + found_digit = True else: break # END handle number # END number parse loop # no explicit number given, 1 is the default - if num == 0: + # It could be 0 though + if not found_digit: num = 1 # END set default num # END number parsing only if non-blob mode @@ -827,13 +849,15 @@ class Repo(object): parsed_to = start # handle hiererarchy walk try: + obj = to_commit(obj) if token == "~": for item in xrange(num): obj = obj.parents[0] # END for each history item to walk elif token == "^": # must be n'th parent - obj = obj.parents[num-1] + if num: + obj = obj.parents[num-1] elif token == ":": if obj.type != "tree": obj = obj.tree @@ -841,10 +865,10 @@ class Repo(object): obj = obj[rev[start:]] parsed_to = lr else: - raise "Invalid token: %r" % token + raise ValueError("Invalid token: %r" % token) # END end handle tag except (IndexError, AttributeError): - raise BadObject("Invalid Revision") + raise BadObject("Invalid Revision in %s" % rev) # END exception handling # END parse loop -- cgit v1.2.3 From a32a6bcd784fca9cb2b17365591c29d15c2f638e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 6 Jul 2010 11:16:49 +0200 Subject: Refs now use object.new_from_sha where possible, preventing git-batch-check to be started up for sha resolution --- lib/git/refs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/git/refs.py b/lib/git/refs.py index 23d45ed0..8b773ae7 100644 --- a/lib/git/refs.py +++ b/lib/git/refs.py @@ -210,7 +210,7 @@ class SymbolicReference(object): except AttributeError: sha = str(ref) try: - obj = Object.new(self.repo, sha) + obj = Object.new_from_sha(self.repo, hex_to_bin(sha)) if obj.type != "commit": raise TypeError("Invalid object type behind sha: %s" % sha) write_value = obj.hexsha @@ -536,7 +536,7 @@ class Reference(SymbolicReference, LazyMixin, Iterable): always point to the actual object as it gets re-created on each query""" # have to be dynamic here as we may be a tag which can point to anything # Our path will be resolved to the hexsha which will be used accordingly - return Object.new(self.repo, self.path) + return Object.new_from_sha(self.repo, hex_to_bin(self.dereference_recursive(self.repo, self.path))) def _set_object(self, ref): """ -- cgit v1.2.3 From 73959f3a2d4f224fbda03c8a8850f66f53d8cb3b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 6 Jul 2010 12:09:11 +0200 Subject: Implemented main rev-parsing, including long hexshas, tags and refs. Short Shas still to be done --- lib/git/repo.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/git/repo.py b/lib/git/repo.py index 5a1af920..e9dfabcd 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -71,7 +71,8 @@ class Repo(object): # precompiled regex re_whitespace = re.compile(r'\s+') re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$') - re_hexsha_shortened = re.compile('^[0-9A-Fa-f]{7:40}$') + re_hexsha_shortened = re.compile('^[0-9A-Fa-f]{7,40}$') + re_hexsha_domain = re.compile('^[0-9A-Fa-f]{1,40}$') re_author_committer_start = re.compile(r'^(author|committer)') re_tab_full_line = re.compile(r'^\t(.*)$') @@ -724,7 +725,7 @@ class Repo(object): def name_to_object(name): hexsha = None - # is it a hexsha ? + # is it a hexsha ? Try the most common ones, which is 7 to 40 if self.re_hexsha_shortened.match(name): if len(name) != 40: # find long sha for short sha @@ -744,6 +745,11 @@ class Repo(object): # tried everything ? fail if hexsha is None: + # it could also be a very short ( less than 7 ) hexsha, which + # wasnt tested in the first run + if len(name) < 7 and self.re_hexsha_domain.match(name): + raise NotImplementedError() + # END try short name raise BadObject(name) # END assert hexsha was found @@ -806,7 +812,7 @@ class Repo(object): # END exception handling elif output_type in ('', 'blob'): if obj.type == 'tag': - obj = deref_tag(tag) + obj = deref_tag(obj) else: # cannot do anything for non-tags pass @@ -815,8 +821,9 @@ class Repo(object): raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev)) # END handle output type - if obj.type != output_type: - raise ValueError("Could not accomodate requested object type %s, got %s" % (output_type, obj.type)) + # empty output types don't require any specific type, its just about dereferencing tags + if output_type and obj.type != output_type: + raise ValueError("Could not accomodate requested object type %r, got %s" % (output_type, obj.type)) # END verify ouput type start = end+1 # skip brace @@ -849,12 +856,13 @@ class Repo(object): parsed_to = start # handle hiererarchy walk try: - obj = to_commit(obj) if token == "~": + obj = to_commit(obj) for item in xrange(num): obj = obj.parents[0] # END for each history item to walk elif token == "^": + obj = to_commit(obj) # must be n'th parent if num: obj = obj.parents[num-1] -- cgit v1.2.3 From 9059525a75b91e6eb6a425f1edcc608739727168 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 6 Jul 2010 20:21:52 +0200 Subject: Made repo.py a package to allow better localization of functions and utilities - the repo module got rather large --- lib/git/index/__init__.py | 2 +- lib/git/refs.py | 3 +- lib/git/repo.py | 898 ---------------------------------------------- lib/git/repo/__init__.py | 3 + lib/git/repo/base.py | 695 +++++++++++++++++++++++++++++++++++ lib/git/repo/fun.py | 224 ++++++++++++ 6 files changed, 924 insertions(+), 901 deletions(-) delete mode 100644 lib/git/repo.py create mode 100644 lib/git/repo/__init__.py create mode 100644 lib/git/repo/base.py create mode 100644 lib/git/repo/fun.py (limited to 'lib') diff --git a/lib/git/index/__init__.py b/lib/git/index/__init__.py index 13f874b0..fe4a7f59 100644 --- a/lib/git/index/__init__.py +++ b/lib/git/index/__init__.py @@ -1,4 +1,4 @@ -"""Initialize the index module""" +"""Initialize the index package""" from base import * from typ import * \ No newline at end of file diff --git a/lib/git/refs.py b/lib/git/refs.py index 8b773ae7..be094d01 100644 --- a/lib/git/refs.py +++ b/lib/git/refs.py @@ -208,9 +208,8 @@ class SymbolicReference(object): try: write_value = ref.commit.hexsha except AttributeError: - sha = str(ref) try: - obj = Object.new_from_sha(self.repo, hex_to_bin(sha)) + obj = self.repo.rev_parse(ref+"^{}") # optionally deref tags if obj.type != "commit": raise TypeError("Invalid object type behind sha: %s" % sha) write_value = obj.hexsha diff --git a/lib/git/repo.py b/lib/git/repo.py deleted file mode 100644 index e9dfabcd..00000000 --- a/lib/git/repo.py +++ /dev/null @@ -1,898 +0,0 @@ -# repo.py -# Copyright (C) 2008, 2009 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 exc import InvalidGitRepositoryError, NoSuchPathError -from cmd import Git -from objects import Actor -from refs import * -from index import IndexFile -from objects import * -from config import GitConfigParser -from remote import Remote -from string import digits -from db import ( - GitCmdObjectDB, - GitDB - ) - -from gitdb.exc import BadObject -from gitdb.util import ( - join, - isdir, - isfile, - join, - hex_to_bin - ) -import os -import sys -import re - - -__all__ = ('Repo', ) - -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 isdir(d) and \ - isdir(join(d, 'objects')) and \ - isdir(join(d, 'refs')): - headref = join(d, 'HEAD') - return isfile(headref) or \ - (os.path.islink(headref) and - os.readlink(headref).startswith('refs')) - return False - - -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. - - The following attributes are worth using: - - 'working_dir' is the working directory of the git command, wich is the working tree - directory if available or the .git directory in case of bare repositories - - 'working_tree_dir' is the working tree directory, but will raise AssertionError - if we are a bare repository. - - 'git_dir' is the .git repository directoy, which is always set.""" - DAEMON_EXPORT_FILE = 'git-daemon-export-ok' - __slots__ = ( "working_dir", "_working_tree_dir", "git_dir", "_bare", "git", "odb" ) - - # precompiled regex - re_whitespace = re.compile(r'\s+') - re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$') - re_hexsha_shortened = re.compile('^[0-9A-Fa-f]{7,40}$') - re_hexsha_domain = re.compile('^[0-9A-Fa-f]{1,40}$') - re_author_committer_start = re.compile(r'^(author|committer)') - re_tab_full_line = re.compile(r'^\t(.*)$') - - # invariants - # represents the configuration level of a configuration file - config_level = ("system", "global", "repository") - - def __init__(self, path=None, odbt = GitDB): - """Create a new Repo instance - - :param path: is the path to either the root git directory or the bare git repo:: - - repo = Repo("/Users/mtrier/Development/git-python") - repo = Repo("/Users/mtrier/Development/git-python.git") - repo = Repo("~/Development/git-python.git") - repo = Repo("$REPOSITORIES/Development/git-python.git") - - :param odbt: Object DataBase type - a type which is constructed by providing - the directory containing the database objects, i.e. .git/objects. It will - be used to access all object data - :raise InvalidGitRepositoryError: - :raise NoSuchPathError: - :return: git.Repo """ - epath = os.path.abspath(os.path.expandvars(os.path.expanduser(path or os.getcwd()))) - - if not os.path.exists(epath): - raise NoSuchPathError(epath) - - self.working_dir = None - self._working_tree_dir = None - self.git_dir = None - curpath = epath - - # walk up the path to find the .git dir - while curpath: - if is_git_dir(curpath): - self.git_dir = curpath - self._working_tree_dir = os.path.dirname(curpath) - break - gitpath = os.path.join(curpath, '.git') - if is_git_dir(gitpath): - self.git_dir = gitpath - self._working_tree_dir = curpath - break - curpath, dummy = os.path.split(curpath) - if not dummy: - break - # END while curpath - - if self.git_dir is None: - raise InvalidGitRepositoryError(epath) - - self._bare = False - try: - self._bare = self.config_reader("repository").getboolean('core','bare') - except Exception: - # lets not assume the option exists, although it should - pass - - # adjust the wd in case we are actually bare - we didn't know that - # in the first place - if self._bare: - self._working_tree_dir = None - # END working dir handling - - self.working_dir = self._working_tree_dir or self.git_dir - self.git = Git(self.working_dir) - - # special handling, in special times - args = [os.path.join(self.git_dir, 'objects')] - if issubclass(odbt, GitCmdObjectDB): - args.append(self.git) - self.odb = odbt(*args) - - def __eq__(self, rhs): - if isinstance(rhs, Repo): - return self.git_dir == rhs.git_dir - return False - - def __ne__(self, rhs): - return not self.__eq__(rhs) - - def __hash__(self): - return hash(self.git_dir) - - def __repr__(self): - return "%s(%r)" % (type(self).__name__, self.git_dir) - - # Description property - def _get_description(self): - filename = os.path.join(self.git_dir, 'description') - return file(filename).read().rstrip() - - def _set_description(self, descr): - filename = os.path.join(self.git_dir, '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 working_tree_dir(self): - """:return: The working tree directory of our git repository - :raise AssertionError: If we are a bare repository""" - if self._working_tree_dir is None: - raise AssertionError( "Repository at %r is bare and does not have a working tree directory" % self.git_dir ) - return self._working_tree_dir - - @property - def bare(self): - """:return: True if the repository is bare""" - return self._bare - - @property - def heads(self): - """A list of ``Head`` objects representing the branch heads in - this repo - - :return: ``git.IterableList(Head, ...)``""" - return Head.list_items(self) - - @property - def references(self): - """A list of Reference objects representing tags, heads and remote references. - - :return: IterableList(Reference, ...)""" - return Reference.list_items(self) - - # alias for references - refs = references - - # alias for heads - branches = heads - - @property - def index(self): - """:return: IndexFile representing this repository's index.""" - return IndexFile(self) - - @property - def head(self): - """:return: HEAD Object pointing to the current head reference""" - return HEAD(self,'HEAD') - - @property - def remotes(self): - """A list of Remote objects allowing to access and manipulate remotes - :return: ``git.IterableList(Remote, ...)``""" - return Remote.list_items(self) - - def remote(self, name='origin'): - """:return: Remote with the specified name - :raise ValueError: if no remote with such a name exists""" - return Remote(self, name) - - @property - def tags(self): - """A list of ``Tag`` objects that are available in this repo - :return: ``git.IterableList(TagReference, ...)`` """ - return TagReference.list_items(self) - - def tag(self,path): - """:return: TagReference Object, reference pointing to a Commit or Tag - :param path: path to the tag reference, i.e. 0.1.5 or tags/0.1.5 """ - return TagReference(self, path) - - def create_head(self, path, commit='HEAD', force=False, **kwargs ): - """Create a new head within the repository. - For more documentation, please see the Head.create method. - - :return: newly created Head Reference""" - return Head.create(self, path, commit, force, **kwargs) - - def delete_head(self, *heads, **kwargs): - """Delete the given heads - - :param kwargs: Additional keyword arguments to be passed to git-branch""" - return Head.delete(self, *heads, **kwargs) - - def create_tag(self, path, ref='HEAD', message=None, force=False, **kwargs): - """Create a new tag reference. - For more documentation, please see the TagReference.create method. - - :return: TagReference object """ - return TagReference.create(self, path, ref, message, force, **kwargs) - - def delete_tag(self, *tags): - """Delete the given tag references""" - return TagReference.delete(self, *tags) - - def create_remote(self, name, url, **kwargs): - """Create a new remote. - - For more information, please see the documentation of the Remote.create - methods - - :return: Remote reference""" - return Remote.create(self, name, url, **kwargs) - - def delete_remote(self, remote): - """Delete the given remote.""" - return Remote.remove(self, remote) - - def _get_config_path(self, config_level ): - # we do not support an absolute path of the gitconfig on windows , - # use the global config instead - if sys.platform == "win32" and config_level == "system": - config_level = "global" - - if config_level == "system": - return "/etc/gitconfig" - elif config_level == "global": - return os.path.normpath(os.path.expanduser("~/.gitconfig")) - elif config_level == "repository": - return join(self.git_dir, "config") - - raise ValueError( "Invalid configuration level: %r" % config_level ) - - def config_reader(self, config_level=None): - """ - :return: - GitConfigParser allowing to read the full git configuration, but not to write it - - The configuration will include values from the system, user and repository - configuration files. - - :param config_level: - For possible values, see config_writer method - If None, all applicable levels will be used. Specify a level in case - you know which exact file you whish to read to prevent reading multiple files for - instance - :note: On windows, system configuration cannot currently be read as the path is - unknown, instead the global path will be used.""" - files = None - if config_level is None: - files = [ self._get_config_path(f) for f in self.config_level ] - else: - files = [ self._get_config_path(config_level) ] - return GitConfigParser(files, read_only=True) - - def config_writer(self, config_level="repository"): - """ - :return: - GitConfigParser allowing to write values of the specified configuration file level. - Config writers should be retrieved, used to change the configuration ,and written - right away as they will lock the configuration file in question and prevent other's - to write it. - - :param config_level: - One of the following values - system = sytem wide configuration file - global = user level configuration file - repository = configuration file for this repostory only""" - return GitConfigParser(self._get_config_path(config_level), read_only = False) - - def commit(self, rev=None): - """The Commit object for the specified revision - :param rev: revision specifier, see git-rev-parse for viable options. - :return: ``git.Commit``""" - if rev is None: - rev = self.active_branch - - c = Object.new(self, rev) - assert c.type == "commit", "Revision %s did not point to a commit, but to %s" % (rev, c) - return c - - def iter_trees(self, *args, **kwargs): - """:return: Iterator yielding Tree objects - :note: Takes all arguments known to iter_commits method""" - return ( c.tree for c in self.iter_commits(*args, **kwargs) ) - - def tree(self, rev=None): - """The Tree object for the given treeish revision - Examples:: - - repo.tree(repo.heads[0]) - - :param rev: is a revision pointing to a Treeish ( being a commit or tree ) - :return: ``git.Tree`` - - :note: - If you need a non-root level tree, find it by iterating the root tree. Otherwise - it cannot know about its path relative to the repository root and subsequent - operations might have unexpected results.""" - if rev is None: - rev = self.active_branch - - c = Object.new(self, rev) - if c.type == "commit": - return c.tree - elif c.type == "tree": - return c - raise ValueError( "Revision %s did not point to a treeish, but to %s" % (rev, c)) - - def iter_commits(self, rev=None, paths='', **kwargs): - """A list of Commit objects representing the history of a given ref/commit - - :parm rev: - revision specifier, see git-rev-parse for viable options. - If None, the active branch will be used. - - :parm paths: - is an optional path or a list of paths to limit the returned commits to - Commits that do not contain that path or the paths will not be returned. - - :parm kwargs: - Arguments to be passed to git-rev-list - common ones are - max_count and skip - - :note: to receive only commits between two named revisions, use the - "revA..revB" revision specifier - - :return ``git.Commit[]``""" - if rev is None: - rev = self.active_branch - - return Commit.iter_items(self, rev, paths, **kwargs) - - def _get_daemon_export(self): - filename = os.path.join(self.git_dir, self.DAEMON_EXPORT_FILE) - return os.path.exists(filename) - - def _set_daemon_export(self, value): - filename = os.path.join(self.git_dir, 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 - - :return: list of strings being pathnames of alternates""" - alternates_path = os.path.join(self.git_dir, '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 list() - - def _set_alternates(self, alts): - """Sets the alternates - - :parm alts: - is the array of string paths representing the alternates at which - git should look for objects, i.e. /home/user/repo/.git/objects - - :raise NoSuchPathError: - :note: - The method does not check for the existance of the paths in alts - as the caller is responsible.""" - alternates_path = os.path.join(self.git_dir, '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") - - def is_dirty(self, index=True, working_tree=True, untracked_files=False): - """ - :return: - ``True``, the repository is considered dirty. By default it will react - like a git-status without untracked files, hence it is dirty if the - index or the working copy have changes.""" - if self._bare: - # Bare repositories with no associated working directory are - # always consired to be clean. - return False - - # start from the one which is fastest to evaluate - default_args = ('--abbrev=40', '--full-index', '--raw') - if index: - # diff index against HEAD - if os.path.isfile(self.index.path) and self.head.is_valid() and \ - len(self.git.diff('HEAD', '--cached', *default_args)): - return True - # END index handling - if working_tree: - # diff index against working tree - if len(self.git.diff(*default_args)): - return True - # END working tree handling - if untracked_files: - if len(self.untracked_files): - return True - # END untracked files - return False - - @property - def untracked_files(self): - """ - :return: - list(str,...) - - Files currently untracked as they have not been staged yet. Paths - are relative to the current working directory of the git command. - - :note: - ignored files will not appear here, i.e. files mentioned in .gitignore""" - # make sure we get all files, no only untracked directores - proc = self.git.status(untracked_files=True, as_process=True) - stream = iter(proc.stdout) - untracked_files = list() - for line in stream: - if not line.startswith("# Untracked files:"): - continue - # skip two lines - stream.next() - stream.next() - - for untracked_info in stream: - if not untracked_info.startswith("#\t"): - break - untracked_files.append(untracked_info.replace("#\t", "").rstrip()) - # END for each utracked info line - # END for each line - return untracked_files - - @property - def active_branch(self): - """The name of the currently active branch. - - :return: Head to the active branch""" - return self.head.reference - - def blame(self, rev, file): - """The blame information for the given file at the given revision. - - :parm rev: revision specifier, see git-rev-parse for viable options. - :return: - 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 = self.git.blame(rev, '--', file, p=True) - commits = dict() - blames = list() - info = None - - for line in data.splitlines(False): - parts = self.re_whitespace.split(line, 1) - firstpart = parts[0] - if self.re_hexsha_only.search(firstpart): - # handles - # 634396b2f541a9f2d58b00be1a07f0c358b999b3 1 1 7 - indicates blame-data start - # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - digits = parts[-1].split(" ") - if len(digits) == 3: - info = {'id': firstpart} - blames.append([None, []]) - # END blame data initialization - else: - m = self.re_author_committer_start.search(firstpart) - if m: - # handles: - # author Tom Preston-Werner - # author-mail - # author-time 1192271832 - # author-tz -0700 - # committer Tom Preston-Werner - # committer-mail - # committer-time 1192271832 - # committer-tz -0700 - IGNORED BY US - role = m.group(0) - if firstpart.endswith('-mail'): - info["%s_email" % role] = parts[-1] - elif firstpart.endswith('-time'): - info["%s_date" % role] = int(parts[-1]) - elif role == firstpart: - info[role] = parts[-1] - # END distinguish mail,time,name - else: - # handle - # filename lib/grit.rb - # summary add Blob - # - if firstpart.startswith('filename'): - info['filename'] = parts[-1] - elif firstpart.startswith('summary'): - info['summary'] = parts[-1] - elif firstpart == '': - if info: - sha = info['id'] - c = commits.get(sha) - if c is None: - c = Commit( self, hex_to_bin(sha), - 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[sha] = c - # END if commit objects needs initial creation - m = self.re_tab_full_line.search(line) - text, = m.groups() - blames[-1][0] = c - blames[-1][1].append( text ) - info = None - # END if we collected commit info - # END distinguish filename,summary,rest - # END distinguish author|committer vs filename,summary,rest - # END distinguish hexsha vs other information - return blames - - @classmethod - def init(cls, path=None, mkdir=True, **kwargs): - """Initialize a git repository at the given path if specified - - :param path: - is the full path to the repo (traditionally ends with /.git) - or None in which case the repository will be created in the current - working directory - - :parm mkdir: - if specified will create the repository directory if it doesn't - already exists. Creates the directory with a mode=0755. - Only effective if a path is explicitly given - - :parm kwargs: - keyword arguments serving as additional options to the git-init command - - :return: ``git.Repo`` (the newly created repo)""" - - if mkdir and path and not os.path.exists(path): - os.makedirs(path, 0755) - - # git command automatically chdir into the directory - git = Git(path) - output = git.init(**kwargs) - return Repo(path) - - def clone(self, path, **kwargs): - """Create a clone from this repository. - :param path: - is the full path of the new repo (traditionally ends with ./.git). - - :param kwargs: - odbt = ObjectDatabase Type, allowing to determine the object database - implementation used by the returned Repo instance - - All remaining keyword arguments are given to the git-clone command - - :return: - ``git.Repo`` (the newly cloned repo)""" - # special handling for windows for path at which the clone should be - # created. - # tilde '~' will be expanded to the HOME no matter where the ~ occours. Hence - # we at least give a proper error instead of letting git fail - prev_cwd = None - prev_path = None - odbt = kwargs.pop('odbt', type(self.odb)) - if os.name == 'nt': - if '~' in path: - raise OSError("Git cannot handle the ~ character in path %r correctly" % path) - - # on windows, git will think paths like c: are relative and prepend the - # current working dir ( before it fails ). We temporarily adjust the working - # dir to make this actually work - match = re.match("(\w:[/\\\])(.*)", path) - if match: - prev_cwd = os.getcwd() - prev_path = path - drive, rest_of_path = match.groups() - os.chdir(drive) - path = rest_of_path - kwargs['with_keep_cwd'] = True - # END cwd preparation - # END windows handling - - try: - self.git.clone(self.git_dir, path, **kwargs) - finally: - if prev_cwd is not None: - os.chdir(prev_cwd) - path = prev_path - # END reset previous working dir - # END bad windows handling - - # our git command could have a different working dir than our actual - # environment, hence we prepend its working dir if required - if not os.path.isabs(path) and self.git.working_dir: - path = os.path.join(self.git._working_dir, path) - return Repo(os.path.abspath(path), odbt = odbt) - - - def archive(self, ostream, treeish=None, prefix=None, **kwargs): - """Archive the tree at the given revision. - :parm ostream: file compatible stream object to which the archive will be written - :parm treeish: is the treeish name/id, defaults to active branch - :parm prefix: is the optional prefix to prepend to each filename in the archive - :parm kwargs: - Additional arguments passed to git-archive - NOTE: Use the 'format' argument to define the kind of format. Use - specialized ostreams to write any format supported by python - - :raise GitCommandError: in case something went wrong - :return: self""" - if treeish is None: - treeish = self.active_branch - if prefix and 'prefix' not in kwargs: - kwargs['prefix'] = prefix - kwargs['output_stream'] = ostream - - self.git.archive(treeish, **kwargs) - return self - - def rev_parse(self, rev): - """ - :return: Object at the given revision, either Commit, Tag, Tree or Blob - :param rev: git-rev-parse compatible revision specification, please see - http://www.kernel.org/pub/software/scm/git/docs/git-rev-parse.html - for details - :note: Currently there is no access to the rev-log, rev-specs may only contain - topological tokens such ~ and ^. - :raise BadObject: if the given revision could not be found""" - if '@' in rev: - raise ValueError("There is no rev-log support yet") - - - # colon search mode ? - if rev.startswith(':/'): - # colon search mode - raise NotImplementedError("commit by message search ( regex )") - # END handle search - - # return object specified by the given name - def name_to_object(name): - hexsha = None - - # is it a hexsha ? Try the most common ones, which is 7 to 40 - if self.re_hexsha_shortened.match(name): - if len(name) != 40: - # find long sha for short sha - raise NotImplementedError("short sha parsing") - else: - hexsha = name - # END handle short shas - else: - for base in ('%s', 'refs/%s', 'refs/tags/%s', 'refs/heads/%s', 'refs/remotes/%s', 'refs/remotes/%s/HEAD'): - try: - hexsha = SymbolicReference.dereference_recursive(self, base % name) - break - except ValueError: - pass - # END for each base - # END handle hexsha - - # tried everything ? fail - if hexsha is None: - # it could also be a very short ( less than 7 ) hexsha, which - # wasnt tested in the first run - if len(name) < 7 and self.re_hexsha_domain.match(name): - raise NotImplementedError() - # END try short name - raise BadObject(name) - # END assert hexsha was found - - return Object.new_from_sha(self, hex_to_bin(hexsha)) - # END object by name - - def deref_tag(tag): - while True: - try: - tag = tag.object - except AttributeError: - break - # END dereference tag - return tag - - def to_commit(obj): - if obj.type == 'tag': - obj = deref_tag(obj) - - if obj.type != "commit": - raise ValueError("Cannot convert object %r to type commit" % obj) - # END verify type - return obj - # END commit converter - - obj = None - output_type = "commit" - start = 0 - parsed_to = 0 - lr = len(rev) - while start < lr: - if rev[start] not in "^~:": - start += 1 - continue - # END handle start - - if obj is None: - # token is a rev name - obj = name_to_object(rev[:start]) - # END initialize obj on first token - - token = rev[start] - start += 1 - - # try to parse {type} - if start < lr and rev[start] == '{': - end = rev.find('}', start) - if end == -1: - raise ValueError("Missing closing brace to define type in %s" % rev) - output_type = rev[start+1:end] # exclude brace - - # handle type - if output_type == 'commit': - pass # default - elif output_type == 'tree': - try: - obj = to_commit(obj).tree - except (AttributeError, ValueError): - pass # error raised later - # END exception handling - elif output_type in ('', 'blob'): - if obj.type == 'tag': - obj = deref_tag(obj) - else: - # cannot do anything for non-tags - pass - # END handle tag - else: - raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev)) - # END handle output type - - # empty output types don't require any specific type, its just about dereferencing tags - if output_type and obj.type != output_type: - raise ValueError("Could not accomodate requested object type %r, got %s" % (output_type, obj.type)) - # END verify ouput type - - start = end+1 # skip brace - parsed_to = start - continue - # END parse type - - # try to parse a number - num = 0 - if token != ":": - found_digit = False - while start < lr: - if rev[start] in digits: - num = num * 10 + int(rev[start]) - start += 1 - found_digit = True - else: - break - # END handle number - # END number parse loop - - # no explicit number given, 1 is the default - # It could be 0 though - if not found_digit: - num = 1 - # END set default num - # END number parsing only if non-blob mode - - - parsed_to = start - # handle hiererarchy walk - try: - if token == "~": - obj = to_commit(obj) - for item in xrange(num): - obj = obj.parents[0] - # END for each history item to walk - elif token == "^": - obj = to_commit(obj) - # must be n'th parent - if num: - obj = obj.parents[num-1] - elif token == ":": - if obj.type != "tree": - obj = obj.tree - # END get tree type - obj = obj[rev[start:]] - parsed_to = lr - else: - raise ValueError("Invalid token: %r" % token) - # END end handle tag - except (IndexError, AttributeError): - raise BadObject("Invalid Revision in %s" % rev) - # END exception handling - # END parse loop - - # still no obj ? Its probably a simple name - if obj is None: - obj = name_to_object(rev) - parsed_to = lr - # END handle simple name - - if obj is None: - raise ValueError("Revision specifier could not be parsed: %s" % rev) - - if parsed_to != lr: - raise ValueError("Didn't consume complete rev spec %s, consumed part: %s" % (rev, rev[:parsed_to])) - - return obj - - def __repr__(self): - return '' % self.git_dir diff --git a/lib/git/repo/__init__.py b/lib/git/repo/__init__.py new file mode 100644 index 00000000..8902a254 --- /dev/null +++ b/lib/git/repo/__init__.py @@ -0,0 +1,3 @@ +"""Initialize the Repo package""" + +from base import * \ No newline at end of file diff --git a/lib/git/repo/base.py b/lib/git/repo/base.py new file mode 100644 index 00000000..976a68bf --- /dev/null +++ b/lib/git/repo/base.py @@ -0,0 +1,695 @@ +# repo.py +# Copyright (C) 2008, 2009 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 git.exc import InvalidGitRepositoryError, NoSuchPathError +from git.cmd import Git +from git.objects import Actor +from git.refs import * +from git.index import IndexFile +from git.objects import * +from git.config import GitConfigParser +from git.remote import Remote +from git.db import ( + GitCmdObjectDB, + GitDB + ) + + +from gitdb.util import ( + join, + isfile, + hex_to_bin + ) + +from fun import ( + rev_parse, + is_git_dir, + touch + ) + +import os +import sys +import re + + +__all__ = ('Repo', ) + + +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. + + The following attributes are worth using: + + 'working_dir' is the working directory of the git command, wich is the working tree + directory if available or the .git directory in case of bare repositories + + 'working_tree_dir' is the working tree directory, but will raise AssertionError + if we are a bare repository. + + 'git_dir' is the .git repository directoy, which is always set.""" + DAEMON_EXPORT_FILE = 'git-daemon-export-ok' + __slots__ = ( "working_dir", "_working_tree_dir", "git_dir", "_bare", "git", "odb" ) + + # precompiled regex + re_whitespace = re.compile(r'\s+') + re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$') + re_hexsha_shortened = re.compile('^[0-9A-Fa-f]{7,40}$') + re_hexsha_domain = re.compile('^[0-9A-Fa-f]{1,40}$') + re_author_committer_start = re.compile(r'^(author|committer)') + re_tab_full_line = re.compile(r'^\t(.*)$') + + # invariants + # represents the configuration level of a configuration file + config_level = ("system", "global", "repository") + + def __init__(self, path=None, odbt = GitDB): + """Create a new Repo instance + + :param path: is the path to either the root git directory or the bare git repo:: + + repo = Repo("/Users/mtrier/Development/git-python") + repo = Repo("/Users/mtrier/Development/git-python.git") + repo = Repo("~/Development/git-python.git") + repo = Repo("$REPOSITORIES/Development/git-python.git") + + :param odbt: Object DataBase type - a type which is constructed by providing + the directory containing the database objects, i.e. .git/objects. It will + be used to access all object data + :raise InvalidGitRepositoryError: + :raise NoSuchPathError: + :return: git.Repo """ + epath = os.path.abspath(os.path.expandvars(os.path.expanduser(path or os.getcwd()))) + + if not os.path.exists(epath): + raise NoSuchPathError(epath) + + self.working_dir = None + self._working_tree_dir = None + self.git_dir = None + curpath = epath + + # walk up the path to find the .git dir + while curpath: + if is_git_dir(curpath): + self.git_dir = curpath + self._working_tree_dir = os.path.dirname(curpath) + break + gitpath = join(curpath, '.git') + if is_git_dir(gitpath): + self.git_dir = gitpath + self._working_tree_dir = curpath + break + curpath, dummy = os.path.split(curpath) + if not dummy: + break + # END while curpath + + if self.git_dir is None: + raise InvalidGitRepositoryError(epath) + + self._bare = False + try: + self._bare = self.config_reader("repository").getboolean('core','bare') + except Exception: + # lets not assume the option exists, although it should + pass + + # adjust the wd in case we are actually bare - we didn't know that + # in the first place + if self._bare: + self._working_tree_dir = None + # END working dir handling + + self.working_dir = self._working_tree_dir or self.git_dir + self.git = Git(self.working_dir) + + # special handling, in special times + args = [join(self.git_dir, 'objects')] + if issubclass(odbt, GitCmdObjectDB): + args.append(self.git) + self.odb = odbt(*args) + + def __eq__(self, rhs): + if isinstance(rhs, Repo): + return self.git_dir == rhs.git_dir + return False + + def __ne__(self, rhs): + return not self.__eq__(rhs) + + def __hash__(self): + return hash(self.git_dir) + + def __repr__(self): + return "%s(%r)" % (type(self).__name__, self.git_dir) + + # Description property + def _get_description(self): + filename = join(self.git_dir, 'description') + return file(filename).read().rstrip() + + def _set_description(self, descr): + filename = join(self.git_dir, '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 working_tree_dir(self): + """:return: The working tree directory of our git repository + :raise AssertionError: If we are a bare repository""" + if self._working_tree_dir is None: + raise AssertionError( "Repository at %r is bare and does not have a working tree directory" % self.git_dir ) + return self._working_tree_dir + + @property + def bare(self): + """:return: True if the repository is bare""" + return self._bare + + @property + def heads(self): + """A list of ``Head`` objects representing the branch heads in + this repo + + :return: ``git.IterableList(Head, ...)``""" + return Head.list_items(self) + + @property + def references(self): + """A list of Reference objects representing tags, heads and remote references. + + :return: IterableList(Reference, ...)""" + return Reference.list_items(self) + + # alias for references + refs = references + + # alias for heads + branches = heads + + @property + def index(self): + """:return: IndexFile representing this repository's index.""" + return IndexFile(self) + + @property + def head(self): + """:return: HEAD Object pointing to the current head reference""" + return HEAD(self,'HEAD') + + @property + def remotes(self): + """A list of Remote objects allowing to access and manipulate remotes + :return: ``git.IterableList(Remote, ...)``""" + return Remote.list_items(self) + + def remote(self, name='origin'): + """:return: Remote with the specified name + :raise ValueError: if no remote with such a name exists""" + return Remote(self, name) + + @property + def tags(self): + """A list of ``Tag`` objects that are available in this repo + :return: ``git.IterableList(TagReference, ...)`` """ + return TagReference.list_items(self) + + def tag(self,path): + """:return: TagReference Object, reference pointing to a Commit or Tag + :param path: path to the tag reference, i.e. 0.1.5 or tags/0.1.5 """ + return TagReference(self, path) + + def create_head(self, path, commit='HEAD', force=False, **kwargs ): + """Create a new head within the repository. + For more documentation, please see the Head.create method. + + :return: newly created Head Reference""" + return Head.create(self, path, commit, force, **kwargs) + + def delete_head(self, *heads, **kwargs): + """Delete the given heads + + :param kwargs: Additional keyword arguments to be passed to git-branch""" + return Head.delete(self, *heads, **kwargs) + + def create_tag(self, path, ref='HEAD', message=None, force=False, **kwargs): + """Create a new tag reference. + For more documentation, please see the TagReference.create method. + + :return: TagReference object """ + return TagReference.create(self, path, ref, message, force, **kwargs) + + def delete_tag(self, *tags): + """Delete the given tag references""" + return TagReference.delete(self, *tags) + + def create_remote(self, name, url, **kwargs): + """Create a new remote. + + For more information, please see the documentation of the Remote.create + methods + + :return: Remote reference""" + return Remote.create(self, name, url, **kwargs) + + def delete_remote(self, remote): + """Delete the given remote.""" + return Remote.remove(self, remote) + + def _get_config_path(self, config_level ): + # we do not support an absolute path of the gitconfig on windows , + # use the global config instead + if sys.platform == "win32" and config_level == "system": + config_level = "global" + + if config_level == "system": + return "/etc/gitconfig" + elif config_level == "global": + return os.path.normpath(os.path.expanduser("~/.gitconfig")) + elif config_level == "repository": + return join(self.git_dir, "config") + + raise ValueError( "Invalid configuration level: %r" % config_level ) + + def config_reader(self, config_level=None): + """ + :return: + GitConfigParser allowing to read the full git configuration, but not to write it + + The configuration will include values from the system, user and repository + configuration files. + + :param config_level: + For possible values, see config_writer method + If None, all applicable levels will be used. Specify a level in case + you know which exact file you whish to read to prevent reading multiple files for + instance + :note: On windows, system configuration cannot currently be read as the path is + unknown, instead the global path will be used.""" + files = None + if config_level is None: + files = [ self._get_config_path(f) for f in self.config_level ] + else: + files = [ self._get_config_path(config_level) ] + return GitConfigParser(files, read_only=True) + + def config_writer(self, config_level="repository"): + """ + :return: + GitConfigParser allowing to write values of the specified configuration file level. + Config writers should be retrieved, used to change the configuration ,and written + right away as they will lock the configuration file in question and prevent other's + to write it. + + :param config_level: + One of the following values + system = sytem wide configuration file + global = user level configuration file + repository = configuration file for this repostory only""" + return GitConfigParser(self._get_config_path(config_level), read_only = False) + + def commit(self, rev=None): + """The Commit object for the specified revision + :param rev: revision specifier, see git-rev-parse for viable options. + :return: ``git.Commit``""" + if rev is None: + rev = self.active_branch + + c = Object.new(self, rev) + assert c.type == "commit", "Revision %s did not point to a commit, but to %s" % (rev, c) + return c + + def iter_trees(self, *args, **kwargs): + """:return: Iterator yielding Tree objects + :note: Takes all arguments known to iter_commits method""" + return ( c.tree for c in self.iter_commits(*args, **kwargs) ) + + def tree(self, rev=None): + """The Tree object for the given treeish revision + Examples:: + + repo.tree(repo.heads[0]) + + :param rev: is a revision pointing to a Treeish ( being a commit or tree ) + :return: ``git.Tree`` + + :note: + If you need a non-root level tree, find it by iterating the root tree. Otherwise + it cannot know about its path relative to the repository root and subsequent + operations might have unexpected results.""" + if rev is None: + rev = self.active_branch + + c = Object.new(self, rev) + if c.type == "commit": + return c.tree + elif c.type == "tree": + return c + raise ValueError( "Revision %s did not point to a treeish, but to %s" % (rev, c)) + + def iter_commits(self, rev=None, paths='', **kwargs): + """A list of Commit objects representing the history of a given ref/commit + + :parm rev: + revision specifier, see git-rev-parse for viable options. + If None, the active branch will be used. + + :parm paths: + is an optional path or a list of paths to limit the returned commits to + Commits that do not contain that path or the paths will not be returned. + + :parm kwargs: + Arguments to be passed to git-rev-list - common ones are + max_count and skip + + :note: to receive only commits between two named revisions, use the + "revA..revB" revision specifier + + :return ``git.Commit[]``""" + if rev is None: + rev = self.active_branch + + return Commit.iter_items(self, rev, paths, **kwargs) + + def _get_daemon_export(self): + filename = join(self.git_dir, self.DAEMON_EXPORT_FILE) + return os.path.exists(filename) + + def _set_daemon_export(self, value): + filename = join(self.git_dir, 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 + + :return: list of strings being pathnames of alternates""" + alternates_path = join(self.git_dir, '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 list() + + def _set_alternates(self, alts): + """Sets the alternates + + :parm alts: + is the array of string paths representing the alternates at which + git should look for objects, i.e. /home/user/repo/.git/objects + + :raise NoSuchPathError: + :note: + The method does not check for the existance of the paths in alts + as the caller is responsible.""" + alternates_path = join(self.git_dir, 'objects', 'info', 'alternates') + if not alts: + if 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") + + def is_dirty(self, index=True, working_tree=True, untracked_files=False): + """ + :return: + ``True``, the repository is considered dirty. By default it will react + like a git-status without untracked files, hence it is dirty if the + index or the working copy have changes.""" + if self._bare: + # Bare repositories with no associated working directory are + # always consired to be clean. + return False + + # start from the one which is fastest to evaluate + default_args = ('--abbrev=40', '--full-index', '--raw') + if index: + # diff index against HEAD + if isfile(self.index.path) and self.head.is_valid() and \ + len(self.git.diff('HEAD', '--cached', *default_args)): + return True + # END index handling + if working_tree: + # diff index against working tree + if len(self.git.diff(*default_args)): + return True + # END working tree handling + if untracked_files: + if len(self.untracked_files): + return True + # END untracked files + return False + + @property + def untracked_files(self): + """ + :return: + list(str,...) + + Files currently untracked as they have not been staged yet. Paths + are relative to the current working directory of the git command. + + :note: + ignored files will not appear here, i.e. files mentioned in .gitignore""" + # make sure we get all files, no only untracked directores + proc = self.git.status(untracked_files=True, as_process=True) + stream = iter(proc.stdout) + untracked_files = list() + for line in stream: + if not line.startswith("# Untracked files:"): + continue + # skip two lines + stream.next() + stream.next() + + for untracked_info in stream: + if not untracked_info.startswith("#\t"): + break + untracked_files.append(untracked_info.replace("#\t", "").rstrip()) + # END for each utracked info line + # END for each line + return untracked_files + + @property + def active_branch(self): + """The name of the currently active branch. + + :return: Head to the active branch""" + return self.head.reference + + def blame(self, rev, file): + """The blame information for the given file at the given revision. + + :parm rev: revision specifier, see git-rev-parse for viable options. + :return: + 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 = self.git.blame(rev, '--', file, p=True) + commits = dict() + blames = list() + info = None + + for line in data.splitlines(False): + parts = self.re_whitespace.split(line, 1) + firstpart = parts[0] + if self.re_hexsha_only.search(firstpart): + # handles + # 634396b2f541a9f2d58b00be1a07f0c358b999b3 1 1 7 - indicates blame-data start + # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 + digits = parts[-1].split(" ") + if len(digits) == 3: + info = {'id': firstpart} + blames.append([None, []]) + # END blame data initialization + else: + m = self.re_author_committer_start.search(firstpart) + if m: + # handles: + # author Tom Preston-Werner + # author-mail + # author-time 1192271832 + # author-tz -0700 + # committer Tom Preston-Werner + # committer-mail + # committer-time 1192271832 + # committer-tz -0700 - IGNORED BY US + role = m.group(0) + if firstpart.endswith('-mail'): + info["%s_email" % role] = parts[-1] + elif firstpart.endswith('-time'): + info["%s_date" % role] = int(parts[-1]) + elif role == firstpart: + info[role] = parts[-1] + # END distinguish mail,time,name + else: + # handle + # filename lib/grit.rb + # summary add Blob + # + if firstpart.startswith('filename'): + info['filename'] = parts[-1] + elif firstpart.startswith('summary'): + info['summary'] = parts[-1] + elif firstpart == '': + if info: + sha = info['id'] + c = commits.get(sha) + if c is None: + c = Commit( self, hex_to_bin(sha), + 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[sha] = c + # END if commit objects needs initial creation + m = self.re_tab_full_line.search(line) + text, = m.groups() + blames[-1][0] = c + blames[-1][1].append( text ) + info = None + # END if we collected commit info + # END distinguish filename,summary,rest + # END distinguish author|committer vs filename,summary,rest + # END distinguish hexsha vs other information + return blames + + @classmethod + def init(cls, path=None, mkdir=True, **kwargs): + """Initialize a git repository at the given path if specified + + :param path: + is the full path to the repo (traditionally ends with /.git) + or None in which case the repository will be created in the current + working directory + + :parm mkdir: + if specified will create the repository directory if it doesn't + already exists. Creates the directory with a mode=0755. + Only effective if a path is explicitly given + + :parm kwargs: + keyword arguments serving as additional options to the git-init command + + :return: ``git.Repo`` (the newly created repo)""" + + if mkdir and path and not os.path.exists(path): + os.makedirs(path, 0755) + + # git command automatically chdir into the directory + git = Git(path) + output = git.init(**kwargs) + return Repo(path) + + def clone(self, path, **kwargs): + """Create a clone from this repository. + :param path: + is the full path of the new repo (traditionally ends with ./.git). + + :param kwargs: + odbt = ObjectDatabase Type, allowing to determine the object database + implementation used by the returned Repo instance + + All remaining keyword arguments are given to the git-clone command + + :return: + ``git.Repo`` (the newly cloned repo)""" + # special handling for windows for path at which the clone should be + # created. + # tilde '~' will be expanded to the HOME no matter where the ~ occours. Hence + # we at least give a proper error instead of letting git fail + prev_cwd = None + prev_path = None + odbt = kwargs.pop('odbt', type(self.odb)) + if os.name == 'nt': + if '~' in path: + raise OSError("Git cannot handle the ~ character in path %r correctly" % path) + + # on windows, git will think paths like c: are relative and prepend the + # current working dir ( before it fails ). We temporarily adjust the working + # dir to make this actually work + match = re.match("(\w:[/\\\])(.*)", path) + if match: + prev_cwd = os.getcwd() + prev_path = path + drive, rest_of_path = match.groups() + os.chdir(drive) + path = rest_of_path + kwargs['with_keep_cwd'] = True + # END cwd preparation + # END windows handling + + try: + self.git.clone(self.git_dir, path, **kwargs) + finally: + if prev_cwd is not None: + os.chdir(prev_cwd) + path = prev_path + # END reset previous working dir + # END bad windows handling + + # our git command could have a different working dir than our actual + # environment, hence we prepend its working dir if required + if not os.path.isabs(path) and self.git.working_dir: + path = join(self.git._working_dir, path) + return Repo(os.path.abspath(path), odbt = odbt) + + + def archive(self, ostream, treeish=None, prefix=None, **kwargs): + """Archive the tree at the given revision. + :parm ostream: file compatible stream object to which the archive will be written + :parm treeish: is the treeish name/id, defaults to active branch + :parm prefix: is the optional prefix to prepend to each filename in the archive + :parm kwargs: + Additional arguments passed to git-archive + NOTE: Use the 'format' argument to define the kind of format. Use + specialized ostreams to write any format supported by python + + :raise GitCommandError: in case something went wrong + :return: self""" + if treeish is None: + treeish = self.active_branch + if prefix and 'prefix' not in kwargs: + kwargs['prefix'] = prefix + kwargs['output_stream'] = ostream + + self.git.archive(treeish, **kwargs) + return self + + rev_parse = rev_parse + + def __repr__(self): + return '' % self.git_dir diff --git a/lib/git/repo/fun.py b/lib/git/repo/fun.py new file mode 100644 index 00000000..ab2eb8be --- /dev/null +++ b/lib/git/repo/fun.py @@ -0,0 +1,224 @@ +"""Package with general repository related functions""" + +from gitdb.exc import BadObject +from git.refs import SymbolicReference +from git.objects import Object +from gitdb.util import ( + join, + isdir, + isfile, + hex_to_bin + ) + +from string import digits + +__all__ = ('rev_parse', 'is_git_dir', 'touch') + +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 isdir(d) and \ + isdir(join(d, 'objects')) and \ + isdir(join(d, 'refs')): + headref = join(d, 'HEAD') + return isfile(headref) or \ + (os.path.islink(headref) and + os.readlink(headref).startswith('refs')) + return False + +def name_to_object(repo, name): + """:return: object specified by the given name, hexshas ( short and long ) + as well as references are supported""" + hexsha = None + + # is it a hexsha ? Try the most common ones, which is 7 to 40 + if repo.re_hexsha_shortened.match(name): + if len(name) != 40: + # find long sha for short sha + raise NotImplementedError("short sha parsing") + else: + hexsha = name + # END handle short shas + else: + for base in ('%s', 'refs/%s', 'refs/tags/%s', 'refs/heads/%s', 'refs/remotes/%s', 'refs/remotes/%s/HEAD'): + try: + hexsha = SymbolicReference.dereference_recursive(repo, base % name) + break + except ValueError: + pass + # END for each base + # END handle hexsha + + # tried everything ? fail + if hexsha is None: + # it could also be a very short ( less than 7 ) hexsha, which + # wasnt tested in the first run + if len(name) < 7 and repo.re_hexsha_domain.match(name): + raise NotImplementedError() + # END try short name + raise BadObject(name) + # END assert hexsha was found + + return Object.new_from_sha(repo, hex_to_bin(hexsha)) + +def deref_tag(tag): + """Recursively dereerence a tag and return the resulting object""" + while True: + try: + tag = tag.object + except AttributeError: + break + # END dereference tag + return tag + +def to_commit(obj): + """Convert the given object to a commit if possible and return it""" + if obj.type == 'tag': + obj = deref_tag(obj) + + if obj.type != "commit": + raise ValueError("Cannot convert object %r to type commit" % obj) + # END verify type + return obj + +def rev_parse(repo, rev): + """ + :return: Object at the given revision, either Commit, Tag, Tree or Blob + :param rev: git-rev-parse compatible revision specification, please see + http://www.kernel.org/pub/software/scm/git/docs/git-rev-parse.html + for details + :note: Currently there is no access to the rev-log, rev-specs may only contain + topological tokens such ~ and ^. + :raise BadObject: if the given revision could not be found""" + if '@' in rev: + raise ValueError("There is no rev-log support yet") + + + # colon search mode ? + if rev.startswith(':/'): + # colon search mode + raise NotImplementedError("commit by message search ( regex )") + # END handle search + + obj = None + output_type = "commit" + start = 0 + parsed_to = 0 + lr = len(rev) + while start < lr: + if rev[start] not in "^~:": + start += 1 + continue + # END handle start + + if obj is None: + # token is a rev name + obj = name_to_object(repo, rev[:start]) + # END initialize obj on first token + + token = rev[start] + start += 1 + + # try to parse {type} + if start < lr and rev[start] == '{': + end = rev.find('}', start) + if end == -1: + raise ValueError("Missing closing brace to define type in %s" % rev) + output_type = rev[start+1:end] # exclude brace + + # handle type + if output_type == 'commit': + pass # default + elif output_type == 'tree': + try: + obj = to_commit(obj).tree + except (AttributeError, ValueError): + pass # error raised later + # END exception handling + elif output_type in ('', 'blob'): + if obj.type == 'tag': + obj = deref_tag(obj) + else: + # cannot do anything for non-tags + pass + # END handle tag + else: + raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev)) + # END handle output type + + # empty output types don't require any specific type, its just about dereferencing tags + if output_type and obj.type != output_type: + raise ValueError("Could not accomodate requested object type %r, got %s" % (output_type, obj.type)) + # END verify ouput type + + start = end+1 # skip brace + parsed_to = start + continue + # END parse type + + # try to parse a number + num = 0 + if token != ":": + found_digit = False + while start < lr: + if rev[start] in digits: + num = num * 10 + int(rev[start]) + start += 1 + found_digit = True + else: + break + # END handle number + # END number parse loop + + # no explicit number given, 1 is the default + # It could be 0 though + if not found_digit: + num = 1 + # END set default num + # END number parsing only if non-blob mode + + + parsed_to = start + # handle hiererarchy walk + try: + if token == "~": + obj = to_commit(obj) + for item in xrange(num): + obj = obj.parents[0] + # END for each history item to walk + elif token == "^": + obj = to_commit(obj) + # must be n'th parent + if num: + obj = obj.parents[num-1] + elif token == ":": + if obj.type != "tree": + obj = obj.tree + # END get tree type + obj = obj[rev[start:]] + parsed_to = lr + else: + raise ValueError("Invalid token: %r" % token) + # END end handle tag + except (IndexError, AttributeError): + raise BadObject("Invalid Revision in %s" % rev) + # END exception handling + # END parse loop + + # still no obj ? Its probably a simple name + if obj is None: + obj = name_to_object(repo, rev) + parsed_to = lr + # END handle simple name + + if obj is None: + raise ValueError("Revision specifier could not be parsed: %s" % rev) + + if parsed_to != lr: + raise ValueError("Didn't consume complete rev spec %s, consumed part: %s" % (rev, rev[:parsed_to])) + + return obj -- cgit v1.2.3 From f068cdc5a1a13539c4a1d756ae950aab65f5348b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 7 Jul 2010 12:16:13 +0200 Subject: Initially working implementation of short-sha parsing and interpretation, thanks to new gitdb functionality --- lib/git/db.py | 27 ++++++++++++++++++++++++--- lib/git/ext/gitdb | 2 +- lib/git/repo/base.py | 3 +-- lib/git/repo/fun.py | 23 +++++++++++++++-------- 4 files changed, 41 insertions(+), 14 deletions(-) (limited to 'lib') diff --git a/lib/git/db.py b/lib/git/db.py index c36446d0..945031bb 100644 --- a/lib/git/db.py +++ b/lib/git/db.py @@ -1,11 +1,15 @@ -"""Module with our own gitdb implementation - it uses the git command""" +"""Module with our own gitdb implementation - it uses the git command""" +from exc import GitCommandError + from gitdb.base import ( OInfo, OStream ) -from gitdb.util import bin_to_hex - +from gitdb.util import ( + bin_to_hex, + hex_to_bin + ) from gitdb.db import GitDB from gitdb.db import LooseObjectDB @@ -35,3 +39,20 @@ class GitCmdObjectDB(LooseObjectDB): t = self._git.stream_object_data(bin_to_hex(sha)) return OStream(*t) + + # { Interface + + def partial_to_complete_sha_hex(partial_hexsha): + """:return: Full binary 20 byte sha from the given partial hexsha + :raise AmbiguousObjectName: + :raise BadObject: + :note: currently we only raise BadObject as git does not communicate + AmbiguousObjects separately""" + try: + hexsha, typename, size = self._git.get_object_header(partial_hexsha) + return hex_to_bin(hexsha) + except GitCommandError: + raise BadObject(partial_hexsha) + # END handle exceptions + + #} END interface diff --git a/lib/git/ext/gitdb b/lib/git/ext/gitdb index 46bf4710..ac7d4757 160000 --- a/lib/git/ext/gitdb +++ b/lib/git/ext/gitdb @@ -1 +1 @@ -Subproject commit 46bf4710e0f7184ac4875e8037de30b5081bfda2 +Subproject commit ac7d4757ab4041f5f0f5806934130024b098bb82 diff --git a/lib/git/repo/base.py b/lib/git/repo/base.py index 976a68bf..e659225e 100644 --- a/lib/git/repo/base.py +++ b/lib/git/repo/base.py @@ -58,8 +58,7 @@ class Repo(object): # precompiled regex re_whitespace = re.compile(r'\s+') re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$') - re_hexsha_shortened = re.compile('^[0-9A-Fa-f]{7,40}$') - re_hexsha_domain = re.compile('^[0-9A-Fa-f]{1,40}$') + re_hexsha_shortened = re.compile('^[0-9A-Fa-f]{4,40}$') re_author_committer_start = re.compile(r'^(author|committer)') re_tab_full_line = re.compile(r'^\t(.*)$') diff --git a/lib/git/repo/fun.py b/lib/git/repo/fun.py index ab2eb8be..a0f66fe5 100644 --- a/lib/git/repo/fun.py +++ b/lib/git/repo/fun.py @@ -7,9 +7,9 @@ from gitdb.util import ( join, isdir, isfile, - hex_to_bin + hex_to_bin, + bin_to_hex ) - from string import digits __all__ = ('rev_parse', 'is_git_dir', 'touch') @@ -30,6 +30,18 @@ def is_git_dir(d): os.readlink(headref).startswith('refs')) return False + +def short_to_long(odb, hexsha): + """:return: long hexadecimal sha1 from the given less-than-40 byte hexsha + or None if no candidate could be found. + :param hexsha: hexsha with less than 40 byte""" + try: + return bin_to_hex(odb.partial_to_complete_sha_hex(hexsha)) + except BadObject: + return None + # END exception handling + + def name_to_object(repo, name): """:return: object specified by the given name, hexshas ( short and long ) as well as references are supported""" @@ -39,7 +51,7 @@ def name_to_object(repo, name): if repo.re_hexsha_shortened.match(name): if len(name) != 40: # find long sha for short sha - raise NotImplementedError("short sha parsing") + hexsha = short_to_long(repo.odb, name) else: hexsha = name # END handle short shas @@ -55,11 +67,6 @@ def name_to_object(repo, name): # tried everything ? fail if hexsha is None: - # it could also be a very short ( less than 7 ) hexsha, which - # wasnt tested in the first run - if len(name) < 7 and repo.re_hexsha_domain.match(name): - raise NotImplementedError() - # END try short name raise BadObject(name) # END assert hexsha was found -- cgit v1.2.3 From bc31651674648f026464fd4110858c4ffeac3c18 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 7 Jul 2010 17:30:47 +0200 Subject: Adjusted previous object creators to use the rev_parse method directly. rev_parse could be adjusted not to return Objects anymore, providing better performance for those who just want a sha only. On the other hand, the method is high-level and should be convenient to use as well, its a starting point for more usually, hence its unlikely to call it in tight loops --- lib/git/index/base.py | 2 +- lib/git/objects/base.py | 5 +---- lib/git/refs.py | 2 +- lib/git/remote.py | 2 +- lib/git/repo/base.py | 19 ++++++------------- 5 files changed, 10 insertions(+), 20 deletions(-) (limited to 'lib') diff --git a/lib/git/index/base.py b/lib/git/index/base.py index 4b3197a2..0f02352f 100644 --- a/lib/git/index/base.py +++ b/lib/git/index/base.py @@ -1122,7 +1122,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): # item. Handle existing -R flags properly. Transform strings to the object # so that we can call diff on it if isinstance(other, basestring): - other = Object.new(self.repo, other) + other = self.repo.rev_parse(other) # END object conversion if isinstance(other, Object): diff --git a/lib/git/objects/base.py b/lib/git/objects/base.py index 21b9b1ea..41862ac2 100644 --- a/lib/git/objects/base.py +++ b/lib/git/objects/base.py @@ -49,10 +49,7 @@ class Object(LazyMixin): :note: This cannot be a __new__ method as it would always call __init__ with the input id which is not necessarily a binsha.""" - hexsha, typename, size = repo.git.get_object_header(id) - inst = get_object_type_by_name(typename)(repo, hex_to_bin(hexsha)) - inst.size = size - return inst + return repo.rev_parse(str(id)) @classmethod def new_from_sha(cls, repo, sha1): diff --git a/lib/git/refs.py b/lib/git/refs.py index be094d01..03b80690 100644 --- a/lib/git/refs.py +++ b/lib/git/refs.py @@ -345,7 +345,7 @@ class SymbolicReference(object): # figure out target data target = reference if resolve: - target = Object.new(repo, reference) + target = repo.rev_parse(str(reference)) if not force and isfile(abs_ref_path): target_data = str(target) diff --git a/lib/git/remote.py b/lib/git/remote.py index 1598e55a..801dcd62 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -391,7 +391,7 @@ class FetchInfo(object): split_token = '...' if control_character == ' ': split_token = split_token[:-1] - old_commit = Commit.new(repo, operation.split(split_token)[0]) + old_commit = repo.rev_parse(operation.split(split_token)[0]) # END handle refspec # END reference flag handling diff --git a/lib/git/repo/base.py b/lib/git/repo/base.py index e659225e..ed805991 100644 --- a/lib/git/repo/base.py +++ b/lib/git/repo/base.py @@ -323,11 +323,9 @@ class Repo(object): :param rev: revision specifier, see git-rev-parse for viable options. :return: ``git.Commit``""" if rev is None: - rev = self.active_branch - - c = Object.new(self, rev) - assert c.type == "commit", "Revision %s did not point to a commit, but to %s" % (rev, c) - return c + return self.active_branch.commit + else: + return self.rev_parse(str(rev)+"^0") def iter_trees(self, *args, **kwargs): """:return: Iterator yielding Tree objects @@ -348,14 +346,9 @@ class Repo(object): it cannot know about its path relative to the repository root and subsequent operations might have unexpected results.""" if rev is None: - rev = self.active_branch - - c = Object.new(self, rev) - if c.type == "commit": - return c.tree - elif c.type == "tree": - return c - raise ValueError( "Revision %s did not point to a treeish, but to %s" % (rev, c)) + return self.active_branch.commit.tree + else: + return self.rev_parse(str(rev)+"^{tree}") def iter_commits(self, rev=None, paths='', **kwargs): """A list of Commit objects representing the history of a given ref/commit -- cgit v1.2.3 From 01ab5b96e68657892695c99a93ef909165456689 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 7 Jul 2010 17:42:42 +0200 Subject: Added test for GitCmdObjectDB in order to verify the partial_to_complete_sha_hex is working as expected with different input ( it wasn't, of course ;) ) --- lib/git/db.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/git/db.py b/lib/git/db.py index 945031bb..6339569f 100644 --- a/lib/git/db.py +++ b/lib/git/db.py @@ -1,5 +1,8 @@ """Module with our own gitdb implementation - it uses the git command""" -from exc import GitCommandError +from exc import ( + GitCommandError, + BadObject + ) from gitdb.base import ( OInfo, @@ -42,7 +45,7 @@ class GitCmdObjectDB(LooseObjectDB): # { Interface - def partial_to_complete_sha_hex(partial_hexsha): + def partial_to_complete_sha_hex(self, partial_hexsha): """:return: Full binary 20 byte sha from the given partial hexsha :raise AmbiguousObjectName: :raise BadObject: @@ -51,7 +54,7 @@ class GitCmdObjectDB(LooseObjectDB): try: hexsha, typename, size = self._git.get_object_header(partial_hexsha) return hex_to_bin(hexsha) - except GitCommandError: + except (GitCommandError, ValueError): raise BadObject(partial_hexsha) # END handle exceptions -- cgit v1.2.3