diff options
-rw-r--r-- | lib/git/db.py | 30 | ||||
-rw-r--r-- | lib/git/exc.py | 4 | ||||
m--------- | lib/git/ext/gitdb | 0 | ||||
-rw-r--r-- | lib/git/index/__init__.py | 2 | ||||
-rw-r--r-- | lib/git/index/base.py | 2 | ||||
-rw-r--r-- | lib/git/objects/base.py | 16 | ||||
-rw-r--r-- | lib/git/refs.py | 22 | ||||
-rw-r--r-- | lib/git/remote.py | 2 | ||||
-rw-r--r-- | lib/git/repo/__init__.py | 3 | ||||
-rw-r--r-- | lib/git/repo/base.py (renamed from lib/git/repo.py) | 94 | ||||
-rw-r--r-- | lib/git/repo/fun.py | 231 | ||||
-rw-r--r-- | test/git/test_db.py | 25 | ||||
-rw-r--r-- | test/git/test_refs.py | 822 | ||||
-rw-r--r-- | test/git/test_repo.py | 171 |
14 files changed, 935 insertions, 489 deletions
diff --git a/lib/git/db.py b/lib/git/db.py index c36446d0..6339569f 100644 --- a/lib/git/db.py +++ b/lib/git/db.py @@ -1,11 +1,18 @@ -"""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, + BadObject + ) + 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 +42,20 @@ class GitCmdObjectDB(LooseObjectDB): t = self._git.stream_object_data(bin_to_hex(sha)) return OStream(*t) + + # { Interface + + def partial_to_complete_sha_hex(self, 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, ValueError): + raise BadObject(partial_hexsha) + # END handle exceptions + + #} END interface 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 -Subproject 6c8721a7d5d32e54bb4ffd3725ed23ac5d76a59 +Subproject ac7d4757ab4041f5f0f5806934130024b098bb8 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/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 d4a46788..41862ac2 100644 --- a/lib/git/objects/base.py +++ b/lib/git/objects/base.py @@ -49,10 +49,18 @@ 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): + """ + :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..03b80690 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 @@ -195,9 +208,8 @@ class SymbolicReference(object): try: write_value = ref.commit.hexsha except AttributeError: - sha = str(ref) try: - obj = Object.new(self.repo, 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 @@ -333,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) @@ -523,7 +535,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): """ 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/__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.py b/lib/git/repo/base.py index 62202364..ed805991 100644 --- a/lib/git/repo.py +++ b/lib/git/repo/base.py @@ -4,27 +4,32 @@ # 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 db import ( +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, - isdir, isfile, - join, hex_to_bin ) + +from fun import ( + rev_parse, + is_git_dir, + touch + ) + import os import sys import re @@ -32,23 +37,6 @@ 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, @@ -70,6 +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]{4,40}$') re_author_committer_start = re.compile(r'^(author|committer)') re_tab_full_line = re.compile(r'^\t(.*)$') @@ -109,7 +98,7 @@ class Repo(object): self.git_dir = curpath self._working_tree_dir = os.path.dirname(curpath) break - gitpath = os.path.join(curpath, '.git') + gitpath = join(curpath, '.git') if is_git_dir(gitpath): self.git_dir = gitpath self._working_tree_dir = curpath @@ -139,7 +128,7 @@ class Repo(object): self.git = Git(self.working_dir) # special handling, in special times - args = [os.path.join(self.git_dir, 'objects')] + args = [join(self.git_dir, 'objects')] if issubclass(odbt, GitCmdObjectDB): args.append(self.git) self.odb = odbt(*args) @@ -160,11 +149,11 @@ class Repo(object): # Description property def _get_description(self): - filename = os.path.join(self.git_dir, 'description') + filename = join(self.git_dir, 'description') return file(filename).read().rstrip() def _set_description(self, descr): - filename = os.path.join(self.git_dir, 'description') + filename = join(self.git_dir, 'description') file(filename, 'w').write(descr+'\n') description = property(_get_description, _set_description, @@ -334,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 @@ -359,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 @@ -393,11 +375,11 @@ class Repo(object): return Commit.iter_items(self, rev, paths, **kwargs) def _get_daemon_export(self): - filename = os.path.join(self.git_dir, self.DAEMON_EXPORT_FILE) + filename = 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) + filename = join(self.git_dir, self.DAEMON_EXPORT_FILE) fileexists = os.path.exists(filename) if value and not fileexists: touch(filename) @@ -413,7 +395,7 @@ class Repo(object): """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') + alternates_path = join(self.git_dir, 'objects', 'info', 'alternates') if os.path.exists(alternates_path): try: @@ -436,9 +418,9 @@ class Repo(object): :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') + alternates_path = join(self.git_dir, 'objects', 'info', 'alternates') if not alts: - if os.path.isfile(alternates_path): + if isfile(alternates_path): os.remove(alternates_path) else: try: @@ -466,7 +448,7 @@ class Repo(object): 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 \ + if isfile(self.index.path) and self.head.is_valid() and \ len(self.git.diff('HEAD', '--cached', *default_args)): return True # END index handling @@ -674,7 +656,7 @@ class Repo(object): # 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) + path = join(self.git._working_dir, path) return Repo(os.path.abspath(path), odbt = odbt) @@ -693,11 +675,13 @@ class Repo(object): if treeish is None: treeish = self.active_branch if prefix and 'prefix' not in kwargs: - kwargs['prefix'] = prefix + kwargs['prefix'] = prefix kwargs['output_stream'] = ostream self.git.archive(treeish, **kwargs) return self - + + rev_parse = rev_parse + def __repr__(self): return '<git.Repo "%s">' % self.git_dir diff --git a/lib/git/repo/fun.py b/lib/git/repo/fun.py new file mode 100644 index 00000000..a0f66fe5 --- /dev/null +++ b/lib/git/repo/fun.py @@ -0,0 +1,231 @@ +"""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, + bin_to_hex + ) +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 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""" + 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 + hexsha = short_to_long(repo.odb, name) + 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: + 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 diff --git a/test/git/test_db.py b/test/git/test_db.py new file mode 100644 index 00000000..9da13bd8 --- /dev/null +++ b/test/git/test_db.py @@ -0,0 +1,25 @@ +# test_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 test.testlib import * +from git.db import * +from gitdb.util import bin_to_hex +from git.exc import BadObject +import os + +class TestDB(TestBase): + + def test_base(self): + gdb = GitCmdObjectDB(os.path.join(self.rorepo.git_dir, 'objects'), self.rorepo.git) + + # partial to complete - works with everything + hexsha = bin_to_hex(gdb.partial_to_complete_sha_hex("0.1.6")) + assert len(hexsha) == 40 + + assert bin_to_hex(gdb.partial_to_complete_sha_hex(hexsha[:20])) == hexsha + + # fails with BadObject + for invalid_rev in ("0000", "bad/ref", "super bad"): + self.failUnlessRaises(BadObject, gdb.partial_to_complete_sha_hex, invalid_rev) diff --git a/test/git/test_refs.py b/test/git/test_refs.py index 958bcf85..b73d574b 100644 --- a/test/git/test_refs.py +++ b/test/git/test_refs.py @@ -14,413 +14,415 @@ import os class TestRefs(TestBase): - def test_from_path(self): - # should be able to create any reference directly - for ref_type in ( Reference, Head, TagReference, RemoteReference ): - for name in ('rela_name', 'path/rela_name'): - full_path = ref_type.to_full_path(name) - instance = ref_type.from_path(self.rorepo, full_path) - assert isinstance(instance, ref_type) - # END for each name - # END for each type - - def test_tag_base(self): - tag_object_refs = list() - for tag in self.rorepo.tags: - assert "refs/tags" in tag.path - assert tag.name - assert isinstance( tag.commit, Commit ) - if tag.tag is not None: - tag_object_refs.append( tag ) - tagobj = tag.tag - assert isinstance( tagobj, TagObject ) - assert tagobj.tag == tag.name - assert isinstance( tagobj.tagger, Actor ) - assert isinstance( tagobj.tagged_date, int ) - assert isinstance( tagobj.tagger_tz_offset, int ) - assert tagobj.message - assert tag.object == tagobj - # can't assign the object - self.failUnlessRaises(AttributeError, setattr, tag, 'object', tagobj) - # END if we have a tag object - # END for tag in repo-tags - assert tag_object_refs - assert isinstance(self.rorepo.tags['0.1.5'], TagReference) - - def test_tags(self): - # tag refs can point to tag objects or to commits - s = set() - ref_count = 0 - for ref in chain(self.rorepo.tags, self.rorepo.heads): - ref_count += 1 - assert isinstance(ref, refs.Reference) - assert str(ref) == ref.name - assert repr(ref) - assert ref == ref - assert not ref != ref - s.add(ref) - # END for each ref - assert len(s) == ref_count - assert len(s|s) == ref_count - - def test_heads(self): - for head in self.rorepo.heads: - assert head.name - assert head.path - assert "refs/heads" in head.path - prev_object = head.object - cur_object = head.object - assert prev_object == cur_object # represent the same git object - assert prev_object is not cur_object # but are different instances - # END for each head - - def test_refs(self): - types_found = set() - for ref in self.rorepo.refs: - types_found.add(type(ref)) - assert len(types_found) == 3 - - def test_is_valid(self): - assert Reference(self.rorepo, 'refs/doesnt/exist').is_valid() == False - assert self.rorepo.head.is_valid() - assert self.rorepo.head.reference.is_valid() - assert SymbolicReference(self.rorepo, 'hellothere').is_valid() == False - - @with_rw_repo('0.1.6') - def test_head_reset(self, rw_repo): - cur_head = rw_repo.head - new_head_commit = cur_head.ref.commit.parents[0] - cur_head.reset(new_head_commit, index=True) # index only - assert cur_head.reference.commit == new_head_commit - - self.failUnlessRaises(ValueError, cur_head.reset, new_head_commit, index=False, working_tree=True) - new_head_commit = new_head_commit.parents[0] - cur_head.reset(new_head_commit, index=True, working_tree=True) # index + wt - assert cur_head.reference.commit == new_head_commit - - # paths - cur_head.reset(new_head_commit, paths = "lib") - - - # now that we have a write write repo, change the HEAD reference - its - # like git-reset --soft - heads = rw_repo.heads - assert heads - for head in heads: - cur_head.reference = head - assert cur_head.reference == head - assert isinstance(cur_head.reference, Head) - assert cur_head.commit == head.commit - assert not cur_head.is_detached - # END for each head - - # detach - active_head = heads[0] - curhead_commit = active_head.commit - cur_head.reference = curhead_commit - assert cur_head.commit == curhead_commit - assert cur_head.is_detached - self.failUnlessRaises(TypeError, getattr, cur_head, "reference") - - # tags are references, hence we can point to them - some_tag = rw_repo.tags[0] - cur_head.reference = some_tag - assert not cur_head.is_detached - assert cur_head.commit == some_tag.commit - assert isinstance(cur_head.reference, TagReference) - - # put HEAD back to a real head, otherwise everything else fails - cur_head.reference = active_head - - # type check - self.failUnlessRaises(ValueError, setattr, cur_head, "reference", "that") - - # head handling - commit = 'HEAD' - prev_head_commit = cur_head.commit - for count, new_name in enumerate(("my_new_head", "feature/feature1")): - actual_commit = commit+"^"*count - new_head = Head.create(rw_repo, new_name, actual_commit) - assert cur_head.commit == prev_head_commit - assert isinstance(new_head, Head) - # already exists - self.failUnlessRaises(GitCommandError, Head.create, rw_repo, new_name) - - # force it - new_head = Head.create(rw_repo, new_name, actual_commit, force=True) - old_path = new_head.path - old_name = new_head.name - - assert new_head.rename("hello").name == "hello" - assert new_head.rename("hello/world").name == "hello/world" - assert new_head.rename(old_name).name == old_name and new_head.path == old_path - - # rename with force - tmp_head = Head.create(rw_repo, "tmphead") - self.failUnlessRaises(GitCommandError, tmp_head.rename, new_head) - tmp_head.rename(new_head, force=True) - assert tmp_head == new_head and tmp_head.object == new_head.object - - Head.delete(rw_repo, tmp_head) - heads = rw_repo.heads - assert tmp_head not in heads and new_head not in heads - # force on deletion testing would be missing here, code looks okay though ;) - # END for each new head name - self.failUnlessRaises(TypeError, RemoteReference.create, rw_repo, "some_name") - - # tag ref - tag_name = "1.0.2" - light_tag = TagReference.create(rw_repo, tag_name) - self.failUnlessRaises(GitCommandError, TagReference.create, rw_repo, tag_name) - light_tag = TagReference.create(rw_repo, tag_name, "HEAD~1", force = True) - assert isinstance(light_tag, TagReference) - assert light_tag.name == tag_name - assert light_tag.commit == cur_head.commit.parents[0] - assert light_tag.tag is None - - # tag with tag object - other_tag_name = "releases/1.0.2RC" - msg = "my mighty tag\nsecond line" - obj_tag = TagReference.create(rw_repo, other_tag_name, message=msg) - assert isinstance(obj_tag, TagReference) - assert obj_tag.name == other_tag_name - assert obj_tag.commit == cur_head.commit - assert obj_tag.tag is not None - - TagReference.delete(rw_repo, light_tag, obj_tag) - tags = rw_repo.tags - assert light_tag not in tags and obj_tag not in tags - - # remote deletion - remote_refs_so_far = 0 - remotes = rw_repo.remotes - assert remotes - for remote in remotes: - refs = remote.refs - RemoteReference.delete(rw_repo, *refs) - remote_refs_so_far += len(refs) - # END for each ref to delete - assert remote_refs_so_far - - for remote in remotes: - # remotes without references throw - self.failUnlessRaises(AssertionError, getattr, remote, 'refs') - # END for each remote - - # change where the active head points to - if cur_head.is_detached: - cur_head.reference = rw_repo.heads[0] - - head = cur_head.reference - old_commit = head.commit - head.commit = old_commit.parents[0] - assert head.commit == old_commit.parents[0] - assert head.commit == cur_head.commit - head.commit = old_commit - - # setting a non-commit as commit fails, but succeeds as object - head_tree = head.commit.tree - self.failUnlessRaises(ValueError, setattr, head, 'commit', head_tree) - assert head.commit == old_commit # and the ref did not change - self.failUnlessRaises(GitCommandError, setattr, head, 'object', head_tree) - - # set the commit directly using the head. This would never detach the head - assert not cur_head.is_detached - head.object = old_commit - cur_head.reference = head.commit - assert cur_head.is_detached - parent_commit = head.commit.parents[0] - assert cur_head.is_detached - cur_head.commit = parent_commit - assert cur_head.is_detached and cur_head.commit == parent_commit - - cur_head.reference = head - assert not cur_head.is_detached - cur_head.commit = parent_commit - assert not cur_head.is_detached - assert head.commit == parent_commit - - # test checkout - active_branch = rw_repo.active_branch - for head in rw_repo.heads: - checked_out_head = head.checkout() - assert checked_out_head == head - # END for each head to checkout - - # checkout with branch creation - new_head = active_branch.checkout(b="new_head") - assert active_branch != rw_repo.active_branch - assert new_head == rw_repo.active_branch - - # checkout with force as we have a changed a file - # clear file - open(new_head.commit.tree.blobs[-1].abspath,'w').close() - assert len(new_head.commit.diff(None)) - - # create a new branch that is likely to touch the file we changed - far_away_head = rw_repo.create_head("far_head",'HEAD~100') - self.failUnlessRaises(GitCommandError, far_away_head.checkout) - assert active_branch == active_branch.checkout(force=True) - assert rw_repo.head.reference != far_away_head - - # test reference creation - partial_ref = 'sub/ref' - full_ref = 'refs/%s' % partial_ref - ref = Reference.create(rw_repo, partial_ref) - assert ref.path == full_ref - assert ref.object == rw_repo.head.commit - - self.failUnlessRaises(OSError, Reference.create, rw_repo, full_ref, 'HEAD~20') - # it works if it is at the same spot though and points to the same reference - assert Reference.create(rw_repo, full_ref, 'HEAD').path == full_ref - Reference.delete(rw_repo, full_ref) - - # recreate the reference using a full_ref - ref = Reference.create(rw_repo, full_ref) - assert ref.path == full_ref - assert ref.object == rw_repo.head.commit - - # recreate using force - ref = Reference.create(rw_repo, partial_ref, 'HEAD~1', force=True) - assert ref.path == full_ref - assert ref.object == rw_repo.head.commit.parents[0] - - # rename it - orig_obj = ref.object - for name in ('refs/absname', 'rela_name', 'feature/rela_name'): - ref_new_name = ref.rename(name) - assert isinstance(ref_new_name, Reference) - assert name in ref_new_name.path - assert ref_new_name.object == orig_obj - assert ref_new_name == ref - # END for each name type - - # References that don't exist trigger an error if we want to access them - self.failUnlessRaises(ValueError, getattr, Reference(rw_repo, "refs/doesntexist"), 'commit') - - # exists, fail unless we force - ex_ref_path = far_away_head.path - self.failUnlessRaises(OSError, ref.rename, ex_ref_path) - # if it points to the same commit it works - far_away_head.commit = ref.commit - ref.rename(ex_ref_path) - assert ref.path == ex_ref_path and ref.object == orig_obj - assert ref.rename(ref.path).path == ex_ref_path # rename to same name - - # create symbolic refs - symref_path = "symrefs/sym" - symref = SymbolicReference.create(rw_repo, symref_path, cur_head.reference) - assert symref.path == symref_path - assert symref.reference == cur_head.reference - - self.failUnlessRaises(OSError, SymbolicReference.create, rw_repo, symref_path, cur_head.reference.commit) - # it works if the new ref points to the same reference - SymbolicReference.create(rw_repo, symref.path, symref.reference).path == symref.path - SymbolicReference.delete(rw_repo, symref) - # would raise if the symref wouldn't have been deletedpbl - symref = SymbolicReference.create(rw_repo, symref_path, cur_head.reference) - - # test symbolic references which are not at default locations like HEAD - # or FETCH_HEAD - they may also be at spots in refs of course - symbol_ref_path = "refs/symbol_ref" - symref = SymbolicReference(rw_repo, symbol_ref_path) - assert symref.path == symbol_ref_path - symbol_ref_abspath = os.path.join(rw_repo.git_dir, symref.path) - - # set it - symref.reference = new_head - assert symref.reference == new_head - assert os.path.isfile(symbol_ref_abspath) - assert symref.commit == new_head.commit - - for name in ('absname','folder/rela_name'): - symref_new_name = symref.rename(name) - assert isinstance(symref_new_name, SymbolicReference) - assert name in symref_new_name.path - assert symref_new_name.reference == new_head - assert symref_new_name == symref - assert not symref.is_detached - # END for each ref - - # create a new non-head ref just to be sure we handle it even if packed - Reference.create(rw_repo, full_ref) - - # test ref listing - assure we have packed refs - rw_repo.git.pack_refs(all=True, prune=True) - heads = rw_repo.heads - assert heads - assert new_head in heads - assert active_branch in heads - assert rw_repo.tags - - # we should be able to iterate all symbolic refs as well - in that case - # we should expect only symbolic references to be returned - for symref in SymbolicReference.iter_items(rw_repo): - assert not symref.is_detached - - # when iterating references, we can get references and symrefs - # when deleting all refs, I'd expect them to be gone ! Even from - # the packed ones - # For this to work, we must not be on any branch - rw_repo.head.reference = rw_repo.head.commit - deleted_refs = set() - for ref in Reference.iter_items(rw_repo): - if ref.is_detached: - ref.delete(rw_repo, ref) - deleted_refs.add(ref) - # END delete ref - # END for each ref to iterate and to delete - assert deleted_refs - - for ref in Reference.iter_items(rw_repo): - if ref.is_detached: - assert ref not in deleted_refs - # END for each ref - - # reattach head - head will not be returned if it is not a symbolic - # ref - rw_repo.head.reference = Head.create(rw_repo, "master") - - # At least the head should still exist - assert os.path.isfile(os.path.join(rw_repo.git_dir, 'HEAD')) - refs = list(SymbolicReference.iter_items(rw_repo)) - assert len(refs) == 1 - - - # test creation of new refs from scratch - for path in ("basename", "dir/somename", "dir2/subdir/basename"): - # REFERENCES - ############ - fpath = Reference.to_full_path(path) - ref_fp = Reference.from_path(rw_repo, fpath) - assert not ref_fp.is_valid() - ref = Reference(rw_repo, fpath) - assert ref == ref_fp - - # can be created by assigning a commit - ref.commit = rw_repo.head.commit - assert ref.is_valid() - - # if the assignment raises, the ref doesn't exist - Reference.delete(ref.repo, ref.path) - assert not ref.is_valid() - self.failUnlessRaises(ValueError, setattr, ref, 'commit', "nonsense") - assert not ref.is_valid() - - # I am sure I had my reason to make it a class method at first, but - # now it doesn't make so much sense anymore, want an instance method as well - # See http://byronimo.lighthouseapp.com/projects/51787-gitpython/tickets/27 - Reference.delete(ref.repo, ref.path) - assert not ref.is_valid() - - ref.object = rw_repo.head.commit - assert ref.is_valid() - - Reference.delete(ref.repo, ref.path) - assert not ref.is_valid() - self.failUnlessRaises(GitCommandError, setattr, ref, 'object', "nonsense") - assert not ref.is_valid() - - # END for each path - - + def test_from_path(self): + # should be able to create any reference directly + for ref_type in ( Reference, Head, TagReference, RemoteReference ): + for name in ('rela_name', 'path/rela_name'): + full_path = ref_type.to_full_path(name) + instance = ref_type.from_path(self.rorepo, full_path) + assert isinstance(instance, ref_type) + # END for each name + # END for each type + + def test_tag_base(self): + tag_object_refs = list() + for tag in self.rorepo.tags: + assert "refs/tags" in tag.path + assert tag.name + assert isinstance( tag.commit, Commit ) + if tag.tag is not None: + tag_object_refs.append( tag ) + tagobj = tag.tag + assert isinstance( tagobj, TagObject ) + assert tagobj.tag == tag.name + assert isinstance( tagobj.tagger, Actor ) + assert isinstance( tagobj.tagged_date, int ) + assert isinstance( tagobj.tagger_tz_offset, int ) + assert tagobj.message + assert tag.object == tagobj + # can't assign the object + self.failUnlessRaises(AttributeError, setattr, tag, 'object', tagobj) + # END if we have a tag object + # END for tag in repo-tags + assert tag_object_refs + assert isinstance(self.rorepo.tags['0.1.5'], TagReference) + + def test_tags(self): + # tag refs can point to tag objects or to commits + s = set() + ref_count = 0 + for ref in chain(self.rorepo.tags, self.rorepo.heads): + ref_count += 1 + assert isinstance(ref, refs.Reference) + assert str(ref) == ref.name + assert repr(ref) + assert ref == ref + assert not ref != ref + s.add(ref) + # END for each ref + assert len(s) == ref_count + assert len(s|s) == ref_count + + def test_heads(self): + for head in self.rorepo.heads: + assert head.name + assert head.path + assert "refs/heads" in head.path + prev_object = head.object + cur_object = head.object + assert prev_object == cur_object # represent the same git object + assert prev_object is not cur_object # but are different instances + # END for each head + + def test_refs(self): + types_found = set() + for ref in self.rorepo.refs: + types_found.add(type(ref)) + assert len(types_found) == 3 + + def test_is_valid(self): + assert Reference(self.rorepo, 'refs/doesnt/exist').is_valid() == False + assert self.rorepo.head.is_valid() + assert self.rorepo.head.reference.is_valid() + assert SymbolicReference(self.rorepo, 'hellothere').is_valid() == False + + @with_rw_repo('0.1.6') + def test_head_reset(self, rw_repo): + cur_head = rw_repo.head + new_head_commit = cur_head.ref.commit.parents[0] + cur_head.reset(new_head_commit, index=True) # index only + assert cur_head.reference.commit == new_head_commit + + self.failUnlessRaises(ValueError, cur_head.reset, new_head_commit, index=False, working_tree=True) + new_head_commit = new_head_commit.parents[0] + cur_head.reset(new_head_commit, index=True, working_tree=True) # index + wt + assert cur_head.reference.commit == new_head_commit + + # paths + cur_head.reset(new_head_commit, paths = "lib") + + + # now that we have a write write repo, change the HEAD reference - its + # like git-reset --soft + heads = rw_repo.heads + assert heads + for head in heads: + cur_head.reference = head + assert cur_head.reference == head + assert isinstance(cur_head.reference, Head) + assert cur_head.commit == head.commit + assert not cur_head.is_detached + # END for each head + + # detach + active_head = heads[0] + curhead_commit = active_head.commit + cur_head.reference = curhead_commit + assert cur_head.commit == curhead_commit + assert cur_head.is_detached + self.failUnlessRaises(TypeError, getattr, cur_head, "reference") + + # tags are references, hence we can point to them + some_tag = rw_repo.tags[0] + cur_head.reference = some_tag + assert not cur_head.is_detached + assert cur_head.commit == some_tag.commit + assert isinstance(cur_head.reference, TagReference) + + # put HEAD back to a real head, otherwise everything else fails + cur_head.reference = active_head + + # type check + self.failUnlessRaises(ValueError, setattr, cur_head, "reference", "that") + + # head handling + commit = 'HEAD' + prev_head_commit = cur_head.commit + for count, new_name in enumerate(("my_new_head", "feature/feature1")): + actual_commit = commit+"^"*count + new_head = Head.create(rw_repo, new_name, actual_commit) + assert cur_head.commit == prev_head_commit + assert isinstance(new_head, Head) + # already exists + self.failUnlessRaises(GitCommandError, Head.create, rw_repo, new_name) + + # force it + new_head = Head.create(rw_repo, new_name, actual_commit, force=True) + old_path = new_head.path + old_name = new_head.name + + assert new_head.rename("hello").name == "hello" + assert new_head.rename("hello/world").name == "hello/world" + assert new_head.rename(old_name).name == old_name and new_head.path == old_path + + # rename with force + tmp_head = Head.create(rw_repo, "tmphead") + self.failUnlessRaises(GitCommandError, tmp_head.rename, new_head) + tmp_head.rename(new_head, force=True) + assert tmp_head == new_head and tmp_head.object == new_head.object + + Head.delete(rw_repo, tmp_head) + heads = rw_repo.heads + assert tmp_head not in heads and new_head not in heads + # force on deletion testing would be missing here, code looks okay though ;) + # END for each new head name + self.failUnlessRaises(TypeError, RemoteReference.create, rw_repo, "some_name") + + # tag ref + tag_name = "1.0.2" + light_tag = TagReference.create(rw_repo, tag_name) + self.failUnlessRaises(GitCommandError, TagReference.create, rw_repo, tag_name) + light_tag = TagReference.create(rw_repo, tag_name, "HEAD~1", force = True) + assert isinstance(light_tag, TagReference) + assert light_tag.name == tag_name + assert light_tag.commit == cur_head.commit.parents[0] + assert light_tag.tag is None + + # tag with tag object + other_tag_name = "releases/1.0.2RC" + msg = "my mighty tag\nsecond line" + obj_tag = TagReference.create(rw_repo, other_tag_name, message=msg) + assert isinstance(obj_tag, TagReference) + assert obj_tag.name == other_tag_name + assert obj_tag.commit == cur_head.commit + assert obj_tag.tag is not None + + TagReference.delete(rw_repo, light_tag, obj_tag) + tags = rw_repo.tags + assert light_tag not in tags and obj_tag not in tags + + # remote deletion + remote_refs_so_far = 0 + remotes = rw_repo.remotes + assert remotes + for remote in remotes: + refs = remote.refs + RemoteReference.delete(rw_repo, *refs) + remote_refs_so_far += len(refs) + # END for each ref to delete + assert remote_refs_so_far + + for remote in remotes: + # remotes without references throw + self.failUnlessRaises(AssertionError, getattr, remote, 'refs') + # END for each remote + + # change where the active head points to + if cur_head.is_detached: + cur_head.reference = rw_repo.heads[0] + + head = cur_head.reference + old_commit = head.commit + head.commit = old_commit.parents[0] + assert head.commit == old_commit.parents[0] + assert head.commit == cur_head.commit + head.commit = old_commit + + # setting a non-commit as commit fails, but succeeds as object + head_tree = head.commit.tree + self.failUnlessRaises(ValueError, setattr, head, 'commit', head_tree) + assert head.commit == old_commit # and the ref did not change + self.failUnlessRaises(GitCommandError, setattr, head, 'object', head_tree) + + # set the commit directly using the head. This would never detach the head + assert not cur_head.is_detached + head.object = old_commit + cur_head.reference = head.commit + assert cur_head.is_detached + parent_commit = head.commit.parents[0] + assert cur_head.is_detached + cur_head.commit = parent_commit + assert cur_head.is_detached and cur_head.commit == parent_commit + + cur_head.reference = head + assert not cur_head.is_detached + cur_head.commit = parent_commit + assert not cur_head.is_detached + assert head.commit == parent_commit + + # test checkout + active_branch = rw_repo.active_branch + for head in rw_repo.heads: + checked_out_head = head.checkout() + assert checked_out_head == head + # END for each head to checkout + + # checkout with branch creation + new_head = active_branch.checkout(b="new_head") + assert active_branch != rw_repo.active_branch + assert new_head == rw_repo.active_branch + + # checkout with force as we have a changed a file + # clear file + open(new_head.commit.tree.blobs[-1].abspath,'w').close() + assert len(new_head.commit.diff(None)) + + # create a new branch that is likely to touch the file we changed + far_away_head = rw_repo.create_head("far_head",'HEAD~100') + self.failUnlessRaises(GitCommandError, far_away_head.checkout) + assert active_branch == active_branch.checkout(force=True) + assert rw_repo.head.reference != far_away_head + + # test reference creation + partial_ref = 'sub/ref' + full_ref = 'refs/%s' % partial_ref + ref = Reference.create(rw_repo, partial_ref) + assert ref.path == full_ref + assert ref.object == rw_repo.head.commit + + self.failUnlessRaises(OSError, Reference.create, rw_repo, full_ref, 'HEAD~20') + # it works if it is at the same spot though and points to the same reference + assert Reference.create(rw_repo, full_ref, 'HEAD').path == full_ref + Reference.delete(rw_repo, full_ref) + + # recreate the reference using a full_ref + ref = Reference.create(rw_repo, full_ref) + assert ref.path == full_ref + assert ref.object == rw_repo.head.commit + + # recreate using force + ref = Reference.create(rw_repo, partial_ref, 'HEAD~1', force=True) + assert ref.path == full_ref + assert ref.object == rw_repo.head.commit.parents[0] + + # rename it + orig_obj = ref.object + for name in ('refs/absname', 'rela_name', 'feature/rela_name'): + ref_new_name = ref.rename(name) + assert isinstance(ref_new_name, Reference) + assert name in ref_new_name.path + assert ref_new_name.object == orig_obj + assert ref_new_name == ref + # END for each name type + + # References that don't exist trigger an error if we want to access them + self.failUnlessRaises(ValueError, getattr, Reference(rw_repo, "refs/doesntexist"), 'commit') + + # exists, fail unless we force + ex_ref_path = far_away_head.path + self.failUnlessRaises(OSError, ref.rename, ex_ref_path) + # if it points to the same commit it works + far_away_head.commit = ref.commit + ref.rename(ex_ref_path) + assert ref.path == ex_ref_path and ref.object == orig_obj + assert ref.rename(ref.path).path == ex_ref_path # rename to same name + + # create symbolic refs + symref_path = "symrefs/sym" + symref = SymbolicReference.create(rw_repo, symref_path, cur_head.reference) + assert symref.path == symref_path + assert symref.reference == cur_head.reference + + self.failUnlessRaises(OSError, SymbolicReference.create, rw_repo, symref_path, cur_head.reference.commit) + # it works if the new ref points to the same reference + SymbolicReference.create(rw_repo, symref.path, symref.reference).path == symref.path + SymbolicReference.delete(rw_repo, symref) + # would raise if the symref wouldn't have been deletedpbl + symref = SymbolicReference.create(rw_repo, symref_path, cur_head.reference) + + # test symbolic references which are not at default locations like HEAD + # or FETCH_HEAD - they may also be at spots in refs of course + symbol_ref_path = "refs/symbol_ref" + symref = SymbolicReference(rw_repo, symbol_ref_path) + assert symref.path == symbol_ref_path + symbol_ref_abspath = os.path.join(rw_repo.git_dir, symref.path) + + # set it + symref.reference = new_head + assert symref.reference == new_head + assert os.path.isfile(symbol_ref_abspath) + assert symref.commit == new_head.commit + + for name in ('absname','folder/rela_name'): + symref_new_name = symref.rename(name) + assert isinstance(symref_new_name, SymbolicReference) + assert name in symref_new_name.path + assert symref_new_name.reference == new_head + assert symref_new_name == symref + assert not symref.is_detached + # END for each ref + + # create a new non-head ref just to be sure we handle it even if packed + Reference.create(rw_repo, full_ref) + + # test ref listing - assure we have packed refs + rw_repo.git.pack_refs(all=True, prune=True) + heads = rw_repo.heads + assert heads + assert new_head in heads + assert active_branch in heads + assert rw_repo.tags + + # we should be able to iterate all symbolic refs as well - in that case + # we should expect only symbolic references to be returned + for symref in SymbolicReference.iter_items(rw_repo): + assert not symref.is_detached + + # when iterating references, we can get references and symrefs + # when deleting all refs, I'd expect them to be gone ! Even from + # the packed ones + # For this to work, we must not be on any branch + rw_repo.head.reference = rw_repo.head.commit + deleted_refs = set() + for ref in Reference.iter_items(rw_repo): + if ref.is_detached: + ref.delete(rw_repo, ref) + deleted_refs.add(ref) + # END delete ref + # END for each ref to iterate and to delete + assert deleted_refs + + for ref in Reference.iter_items(rw_repo): + if ref.is_detached: + assert ref not in deleted_refs + # END for each ref + + # reattach head - head will not be returned if it is not a symbolic + # ref + rw_repo.head.reference = Head.create(rw_repo, "master") + + # At least the head should still exist + assert os.path.isfile(os.path.join(rw_repo.git_dir, 'HEAD')) + refs = list(SymbolicReference.iter_items(rw_repo)) + assert len(refs) == 1 + + + # test creation of new refs from scratch + for path in ("basename", "dir/somename", "dir2/subdir/basename"): + # REFERENCES + ############ + fpath = Reference.to_full_path(path) + ref_fp = Reference.from_path(rw_repo, fpath) + assert not ref_fp.is_valid() + ref = Reference(rw_repo, fpath) + assert ref == ref_fp + + # can be created by assigning a commit + ref.commit = rw_repo.head.commit + assert ref.is_valid() + + # if the assignment raises, the ref doesn't exist + Reference.delete(ref.repo, ref.path) + assert not ref.is_valid() + self.failUnlessRaises(ValueError, setattr, ref, 'commit', "nonsense") + assert not ref.is_valid() + + # I am sure I had my reason to make it a class method at first, but + # now it doesn't make so much sense anymore, want an instance method as well + # See http://byronimo.lighthouseapp.com/projects/51787-gitpython/tickets/27 + Reference.delete(ref.repo, ref.path) + assert not ref.is_valid() + + ref.object = rw_repo.head.commit + assert ref.is_valid() + + Reference.delete(ref.repo, ref.path) + assert not ref.is_valid() + self.failUnlessRaises(GitCommandError, setattr, ref, 'object', "nonsense") + assert not ref.is_valid() + + # END for each path + + def test_dereference_recursive(self): + # for now, just test the HEAD + assert SymbolicReference.dereference_recursive(self.rorepo, 'HEAD') diff --git a/test/git/test_repo.py b/test/git/test_repo.py index 11c7c2e6..53829556 100644 --- a/test/git/test_repo.py +++ b/test/git/test_repo.py @@ -3,15 +3,18 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php - -import os, sys from test.testlib import * from git import * from git.util import join_path_native +from git.exc import BadObject +from gitdb.util import hex_to_bin, bin_to_hex + +import os, sys import tempfile import shutil from cStringIO import StringIO + class TestRepo(TestBase): @raises(InvalidGitRepositoryError) @@ -54,7 +57,12 @@ class TestRepo(TestBase): assert self.rorepo.tree(tree) == tree # try from invalid revision that does not exist - self.failUnlessRaises(ValueError, self.rorepo.tree, 'hello world') + self.failUnlessRaises(BadObject, self.rorepo.tree, 'hello world') + + def test_commit_from_revision(self): + commit = self.rorepo.commit('0.1.4') + assert commit.type == 'commit' + assert self.rorepo.commit(commit) == commit def test_commits(self): mc = 10 @@ -88,7 +96,7 @@ class TestRepo(TestBase): assert num_trees == mc - def _test_empty_repo(self, repo): + def _assert_empty_repo(self, repo): # test all kinds of things with an empty, freshly initialized repo. # It should throw good errors @@ -130,12 +138,12 @@ class TestRepo(TestBase): assert r.bare == True assert os.path.isdir(r.git_dir) - self._test_empty_repo(r) + self._assert_empty_repo(r) # test clone clone_path = path + "_clone" rc = r.clone(clone_path) - self._test_empty_repo(rc) + self._assert_empty_repo(rc) shutil.rmtree(git_dir_abs) try: @@ -152,7 +160,7 @@ class TestRepo(TestBase): r = Repo.init(bare=False) r.bare == False - self._test_empty_repo(r) + self._assert_empty_repo(r) finally: try: shutil.rmtree(del_dir_abs) @@ -275,7 +283,7 @@ class TestRepo(TestBase): reader = self.rorepo.config_reader("repository") # single config file assert reader.read_only - def _test_config_writer(self): + def test_config_writer(self): for config_level in self.rorepo.config_level: try: writer = self.rorepo.config_writer(config_level) @@ -376,3 +384,150 @@ class TestRepo(TestBase): assert s._stream.tell() == 2 assert s.read() == l1[2:ts] assert s._stream.tell() == ts+1 + + def _assert_rev_parse_types(self, name, rev_obj): + rev_parse = self.rorepo.rev_parse + + if rev_obj.type == 'tag': + rev_obj = rev_obj.object + + # tree and blob type + obj = rev_parse(name + '^{tree}') + assert obj == rev_obj.tree + + obj = rev_parse(name + ':CHANGES') + assert obj.type == 'blob' and obj.path == 'CHANGES' + assert rev_obj.tree['CHANGES'] == obj + + + def _assert_rev_parse(self, name): + """tries multiple different rev-parse syntaxes with the given name + :return: parsed object""" + rev_parse = self.rorepo.rev_parse + orig_obj = rev_parse(name) + if orig_obj.type == 'tag': + obj = orig_obj.object + else: + obj = orig_obj + # END deref tags by default + + # try history + rev = name + "~" + obj2 = rev_parse(rev) + assert obj2 == obj.parents[0] + self._assert_rev_parse_types(rev, obj2) + + # history with number + ni = 11 + history = [obj.parents[0]] + for pn in range(ni): + history.append(history[-1].parents[0]) + # END get given amount of commits + + for pn in range(11): + rev = name + "~%i" % (pn+1) + obj2 = rev_parse(rev) + assert obj2 == history[pn] + self._assert_rev_parse_types(rev, obj2) + # END history check + + # parent ( default ) + rev = name + "^" + obj2 = rev_parse(rev) + assert obj2 == obj.parents[0] + self._assert_rev_parse_types(rev, obj2) + + # parent with number + for pn, parent in enumerate(obj.parents): + rev = name + "^%i" % (pn+1) + assert rev_parse(rev) == parent + self._assert_rev_parse_types(rev, obj2) + # END for each parent + + return orig_obj + + def test_rev_parse(self): + rev_parse = self.rorepo.rev_parse + + # try special case: This one failed beforehand + assert rev_parse("33ebe").hexsha == "33ebe7acec14b25c5f84f35a664803fcab2f7781" + + # start from reference + num_resolved = 0 + for ref in Reference.iter_items(self.rorepo): + path_tokens = ref.path.split("/") + for pt in range(len(path_tokens)): + path_section = '/'.join(path_tokens[-(pt+1):]) + try: + obj = self._assert_rev_parse(path_section) + assert obj.type == ref.object.type + num_resolved += 1 + except BadObject: + print "failed on %s" % path_section + # is fine, in case we have something like 112, which belongs to remotes/rname/merge-requests/112 + pass + # END exception handling + # END for each token + # END for each reference + assert num_resolved + + # it works with tags ! + tag = self._assert_rev_parse('0.1.4') + assert tag.type == 'tag' + + # try full sha directly ( including type conversion ) + assert tag.object == rev_parse(tag.object.hexsha) + self._assert_rev_parse_types(tag.object.hexsha, tag.object) + + + # multiple tree types result in the same tree: HEAD^{tree}^{tree}:CHANGES + rev = '0.1.4^{tree}^{tree}' + assert rev_parse(rev) == tag.object.tree + assert rev_parse(rev+':CHANGES') == tag.object.tree['CHANGES'] + + + # try to get parents from first revision - it should fail as no such revision + # exists + first_rev = "33ebe7acec14b25c5f84f35a664803fcab2f7781" + commit = rev_parse(first_rev) + assert len(commit.parents) == 0 + assert commit.hexsha == first_rev + self.failUnlessRaises(BadObject, rev_parse, first_rev+"~") + self.failUnlessRaises(BadObject, rev_parse, first_rev+"^") + + # short SHA1 + commit2 = rev_parse(first_rev[:20]) + assert commit2 == commit + commit2 = rev_parse(first_rev[:5]) + assert commit2 == commit + + + # todo: dereference tag into a blob 0.1.7^{blob} - quite a special one + # needs a tag which points to a blob + + + # ref^0 returns commit being pointed to, same with ref~0, and ^{} + tag = rev_parse('0.1.4') + for token in (('~0', '^0', '^{}')): + assert tag.object == rev_parse('0.1.4%s' % token) + # END handle multiple tokens + + # try partial parsing + max_items = 40 + for i, binsha in enumerate(self.rorepo.odb.sha_iter()): + assert rev_parse(bin_to_hex(binsha)[:8-(i%2)]).binsha == binsha + if i > max_items: + # this is rather slow currently, as rev_parse returns an object + # which requires accessing packs, it has some additional overhead + break + # END for each binsha in repo + + # missing closing brace commit^{tree + self.failUnlessRaises(ValueError, rev_parse, '0.1.4^{tree') + + # missing starting brace + self.failUnlessRaises(ValueError, rev_parse, '0.1.4^tree}') + + + # cannot handle rev-log for now + self.failUnlessRaises(ValueError, rev_parse, "hi@there") |