diff options
author | Sebastian Thiel <byronimo@gmail.com> | 2011-05-29 21:59:12 +0200 |
---|---|---|
committer | Sebastian Thiel <byronimo@gmail.com> | 2011-05-29 21:59:12 +0200 |
commit | 0996049122842a343e0ea7fbbecafddb2b4ba9d3 (patch) | |
tree | caf5682e79d1317cd29d9bbab965cf932a1b67ff | |
parent | cd26aaebbda94dc3740e41bbd3f91ba6b1a25c10 (diff) | |
download | gitpython-0996049122842a343e0ea7fbbecafddb2b4ba9d3.tar.gz |
Intermediate commit with quite some progress in order to put all extra methods on the default Repo implementation into interfaces or something that can be abstracted. It shows that it would indeed be good to keep the differentiation between Repositories which contain an object database as it is clearly easier to setup any combination of repositories that use git and those that do not, with just the addition of one more level of indirection. Lets see how it will end up
-rw-r--r-- | doc/source/changes.rst | 4 | ||||
-rw-r--r-- | git/db/cmd/advanced.py | 368 | ||||
-rw-r--r-- | git/db/cmd/complex.py | 25 | ||||
-rw-r--r-- | git/db/compat.py | 17 | ||||
-rw-r--r-- | git/db/interface.py | 89 | ||||
-rw-r--r-- | git/db/py/base.py | 74 | ||||
-rw-r--r-- | git/db/py/complex.py | 18 | ||||
-rw-r--r-- | git/db/py/resolve.py | 44 | ||||
-rw-r--r-- | git/db/py/transport.py | 61 | ||||
-rw-r--r-- | git/repo/base.py | 635 | ||||
-rw-r--r-- | git/test/test_util.py | 4 |
11 files changed, 650 insertions, 689 deletions
diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 6d09f144..d7fd02ff 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -10,11 +10,11 @@ NEXT * ### Interface Changes ### - * SymbolicReference + * **SymbolicReference** * object_binsha property added - * Blob Type + * **Blob** Type * Added mode constants to ease the manual creation of blobs diff --git a/git/db/cmd/advanced.py b/git/db/cmd/advanced.py new file mode 100644 index 00000000..c8bd2cd6 --- /dev/null +++ b/git/db/cmd/advanced.py @@ -0,0 +1,368 @@ +# 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 + +class AdvancedFunctionalityMixin(object): + """An intermediate interface carrying advanced git functionality that can be used + in other comound repositories which do not implement this functionality themselves. + + The mixin must be used with repositories that provide a git command object under + self.git. + + :note: at some point, methods provided here are supposed to be provided by custom interfaces""" + DAEMON_EXPORT_FILE = 'git-daemon-export-ok' + + # 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(.*)$') + + @property + def index(self): + """:return: IndexFile representing this repository's index.""" + return IndexFile(self) + + 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: + return self.head.commit + else: + return self.rev_parse(str(rev)+"^0") + + 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: + return self.head.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 + + :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.head.commit + + 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 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 + + 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: [<line>]] + 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 <tom@mojombo.com> + # author-time 1192271832 + # author-tz -0700 + # committer Tom Preston-Werner + # committer-mail <tom@mojombo.com> + # 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 + # <and rest> + 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 /<name>.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) + + @classmethod + def _clone(cls, git, url, path, odb_default_type, **kwargs): + # 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', odb_default_type) + 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: + git.clone(url, 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 git.working_dir: + path = join(git._working_dir, path) + + # adjust remotes - there may be operating systems which use backslashes, + # These might be given as initial paths, but when handling the config file + # that contains the remote from which we were clones, git stops liking it + # as it will escape the backslashes. Hence we undo the escaping just to be + # sure + repo = cls(os.path.abspath(path), odbt = odbt) + if repo.remotes: + repo.remotes[0].config_writer.set_value('url', repo.remotes[0].url.replace("\\\\", "\\").replace("\\", "/")) + # END handle remote repo + return repo + + def clone(self, path, **kwargs): + """Create a clone from this repository. + :param path: + is the full path of the new repo (traditionally ends with ./<name>.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)""" + return self._clone(self.git, self.git_dir, path, type(self.odb), **kwargs) + + @classmethod + def clone_from(cls, url, to_path, **kwargs): + """Create a clone from the given URL + :param url: valid git url, see http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS + :param to_path: Path to which the repository should be cloned to + :param kwargs: see the ``clone`` method + :return: Repo instance pointing to the cloned directory""" + return cls._clone(Git(os.getcwd()), url, to_path, CmdGitDB, **kwargs) + + 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.head.commit + 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, name): + return self.odb.resolve(name) + + def __repr__(self): + return '<git.Repo "%s">' % self.git_dir diff --git a/git/db/cmd/complex.py b/git/db/cmd/complex.py index 73e2048f..2eed17ad 100644 --- a/git/db/cmd/complex.py +++ b/git/db/cmd/complex.py @@ -14,8 +14,13 @@ from git.util import ( hex_to_bin ) from git.db.py.loose import PureLooseObjectODB +from git.db.compat import RepoCompatInterface from git.util import RemoteProgress -from git.db.py.base import TransportDB +from git.db.py.base import ( + TransportDB, + PureRepositoryPathsMixin, + PureAlternatesFileMixin + ) from git.db.interface import FetchInfo as GitdbFetchInfo from git.db.interface import PushInfo as GitdbPushInfo @@ -33,7 +38,7 @@ import re import sys -__all__ = ('CmdGitDB', 'RemoteProgress' ) +__all__ = ('CmdGitDB', 'RemoteProgress', 'CmdCompatibilityGitDB' ) class PushInfo(GitdbPushInfo): @@ -266,7 +271,7 @@ class FetchInfo(GitdbFetchInfo): return cls(remote_local_ref, flags, note, old_commit_binsha) -class CmdGitDB(PureLooseObjectODB, TransportDB): +class CmdGitDB(PureLooseObjectODB, TransportDB, PureRepositoryPathsMixin, PureAlternatesFileMixin): """A database representing the default git object store, which includes loose objects, pack files and an alternates file @@ -276,7 +281,8 @@ class CmdGitDB(PureLooseObjectODB, TransportDB): """ def __init__(self, root_path, git): """Initialize this instance with the root and a git command""" - super(CmdGitDB, self).__init__(root_path) + self._initialize(root_path) + super(CmdGitDB, self).__init__(self.objects_dir) self._git = git @classmethod @@ -373,7 +379,8 @@ class CmdGitDB(PureLooseObjectODB, TransportDB): #{ ODB Interface - + # overrides from PureOdb Implementation, which is responsible only for writing + # objects def info(self, sha): hexsha, typename, size = self._git.get_object_header(bin_to_hex(sha)) return OInfo(hex_to_bin(hexsha), typename, size) @@ -399,6 +406,10 @@ class CmdGitDB(PureLooseObjectODB, TransportDB): except (GitCommandError, ValueError): raise BadObject(partial_hexsha) # END handle exceptions + + @property + def git(self): + return self._git #} END interface @@ -432,3 +443,7 @@ class CmdGitDB(PureLooseObjectODB, TransportDB): return self._get_fetch_info_from_stderr(proc, progress or RemoteProgress()) #} end transport db interface + + +class CmdCompatibilityGitDB(CmdGitDB, RepoCompatInterface): + """Command git database with the compatabilty interface added for 0.3x code""" diff --git a/git/db/compat.py b/git/db/compat.py index 1c0ba6f5..16bf0562 100644 --- a/git/db/compat.py +++ b/git/db/compat.py @@ -11,3 +11,20 @@ class RepoCompatInterface(object): @property def bare(self): return self.is_bare + + def rev_parse(self, *args, **kwargs): + return self.resolve_object(*args, **kwargs) + + @property + def odb(self): + """The odb is now an integrated part of each repository""" + return self + + @property + def active_branch(self): + """The name of the currently active branch. + + :return: Head to the active branch""" + return self.head.reference + + diff --git a/git/db/interface.py b/git/db/interface.py index bdda70b3..90421433 100644 --- a/git/db/interface.py +++ b/git/db/interface.py @@ -180,6 +180,7 @@ class CachingDB(object): # END interface + class CompoundDB(object): """A database which delegates calls to sub-databases. They should usually be cached and lazy-loaded""" @@ -282,6 +283,7 @@ class TransportDB(object): As refspecs involve symbolic names for references to be handled, we require RefParse functionality. How this is done is up to the actual implementation.""" # The following variables need to be set by the derived class + __slots__ = tuple() #{ Interface @@ -325,6 +327,22 @@ class TransportDB(object): :note: Remote objects can also be used for the actual push or fetch operation""" raise NotImplementedError() + def remote(self, name='origin'): + """:return: Remote object with the given name + :note: it does not necessarily exist, hence this is just a more convenient way + to construct Remote objects""" + raise NotImplementedError() + + def create_remote(self, name, url, **kwargs): + """Create a new remote with the given name pointing to the given url + :return: Remote instance, compatible to the Remote interface""" + return Remote.create(self, name, url, **kwargs) + + def delete_remote(self, remote): + """Delete the given remote. + :param remote: a Remote instance""" + return Remote.remove(self, remote) + #}end interface @@ -334,6 +352,7 @@ class ReferencesMixin(object): The returned types are compatible to the interfaces of the pure python reference implementation in GitDB.ref""" + __slots__ = tuple() def resolve(self, name): """Resolve the given name into a binary sha. Valid names are as defined @@ -342,6 +361,13 @@ class ReferencesMixin(object): :raise AmbiguousObjectName: :raise BadObject: """ raise NotImplementedError() + + def resolve_object(self, name): + """As ``resolve()``, but returns the Objecft instance pointed to by the + resolved binary sha + :return: Object instance of the correct type, e.g. shas pointing to commits + will be represented by a Commit object""" + raise NotImplementedError() @property def references(self): @@ -357,10 +383,64 @@ class ReferencesMixin(object): raise NotImplementedError() @property + def head(self): + """:return: HEAD Object pointing to the current head reference""" + raise NotImplementedError() + + @property def tags(self): - """:return: An IterableList of TagReferences that are available in this repo""" + """:return: An IterableList of TagReferences or compatible items that + are available in this repo""" + raise NotImplementedError() + + def tag(self, name): + """:return: Tag with the given name + :note: It does not necessarily exist, hence this is just a more convenient + way to construct TagReference objects""" raise NotImplementedError() + def create_head(self, path, commit='HEAD', force=False, logmsg=None ): + """Create a new head within the repository. + :param commit: a resolvable name to the commit or a Commit or Reference instance the new head should point to + :param force: if True, a head will be created even though it already exists + Otherwise an exception will be raised. + :param logmsg: message to append to the reference log. If None, a default message + will be used + :return: newly created Head instances""" + raise NotImplementedError() + + def delete_head(self, *heads): + """Delete the given heads + :param heads: list of Head references that are to be deleted""" + raise NotImplementedError() + + def create_tag(self, path, ref='HEAD', message=None, force=False): + """Create a new tag reference. + :param path: name or path of the new tag. + :param ref: resolvable name of the reference or commit, or Commit or Reference + instance describing the commit the tag should point to. + :param message: message to be attached to the tag reference. This will + create an actual Tag object carrying the message. Otherwise a TagReference + will be generated. + :param force: if True, the Tag will be created even if another tag does already + exist at the given path. Otherwise an exception will be thrown + :return: TagReference object """ + raise NotImplementedError() + + def delete_tag(self, *tags): + """Delete the given tag references + :param tags: TagReferences to delete""" + raise NotImplementedError() + + + #{ Backward Compatability + # These aliases need to be provided by the implementing interface as well + refs = references + branches = heads + #} END backward compatability + + + class RepositoryPathsMixin(object): """Represents basic functionality of a full git repository. This involves an @@ -385,6 +465,13 @@ class RepositoryPathsMixin(object): only. Plain object databases need to be fed the "objects" directory path. :param path: the path to initialize the repository with + It is a 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") + :raise InvalidDBRoot: """ raise NotImplementedError() diff --git a/git/db/py/base.py b/git/db/py/base.py index f45711d5..cc326c27 100644 --- a/git/db/py/base.py +++ b/git/db/py/base.py @@ -35,7 +35,7 @@ import os __all__ = ( 'PureObjectDBR', 'PureObjectDBW', 'PureRootPathDB', 'PureCompoundDB', - 'PureConfigurationMixin', 'PureRepositoryPathsMixin') + 'PureConfigurationMixin', 'PureRepositoryPathsMixin', 'PureAlternatesFileMixin') class PureObjectDBR(ObjectDBR): @@ -385,3 +385,75 @@ class PureConfigurationMixin(ConfigurationMixin): #} END interface + +class PureAlternatesFileMixin(object): + """Utility able to read and write an alternates file through the alternates property + It needs to be part of a type with the git_dir or db_path property. + + The file by default is assumed to be located at the default location as imposed + by the standard git repository layout""" + + #{ Configuration + alternates_filepath = os.path.join('info', 'alternates') # relative path to alternates file + + #} END configuration + + def __init__(self, *args, **kwargs): + super(PureAlternatesFileMixin, self).__init__(*args, **kwargs) + self._alternates_path() # throws on incompatible type + + #{ Interface + + def _alternates_path(self): + if hasattr(self, 'git_dir'): + return join(self.git_dir, 'objects', self.alternates_filepath) + elif hasattr(self, 'db_path'): + return self.db_path(self.alternates_filepath) + else: + raise AssertionError("This mixin requires a parent type with either the git_dir property or db_path method") + #END handle path + + 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 = self._alternates_path() + + if os.path.exists(alternates_path): + try: + f = open(alternates_path) + alts = f.read() + finally: + f.close() + return alts.strip().splitlines() + else: + return list() + # END handle path exists + + 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 = self._alternates_path() + 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") + + #} END interface diff --git a/git/db/py/complex.py b/git/db/py/complex.py index de68d4fd..6504b3ed 100644 --- a/git/db/py/complex.py +++ b/git/db/py/complex.py @@ -8,6 +8,7 @@ from base import ( PureRootPathDB, PureRepositoryPathsMixin, PureConfigurationMixin, + PureAlternatesFileMixin, ) from resolve import PureReferencesMixin @@ -17,6 +18,8 @@ from pack import PurePackedODB from ref import PureReferenceDB from submodule import PureSubmoduleDB +from git.db.compat import RepoCompatInterface + from git.util import ( LazyMixin, normpath, @@ -30,10 +33,11 @@ from git.exc import ( ) import os -__all__ = ('PureGitODB', 'PureGitDB') +__all__ = ('PureGitODB', 'PureGitDB', 'PureCompatibilityGitDB') -class PureGitODB(PureRootPathDB, PureObjectDBW, PureCompoundDB, PureSubmoduleDB): +class PureGitODB(PureRootPathDB, PureObjectDBW, PureCompoundDB, + PureSubmoduleDB, PureAlternatesFileMixin): """A git-style object-only database, which contains all objects in the 'objects' subdirectory. :note: The type needs to be initialized on the ./objects directory to function, @@ -47,7 +51,7 @@ class PureGitODB(PureRootPathDB, PureObjectDBW, PureCompoundDB, PureSubmoduleDB) # Directories packs_dir = 'pack' loose_dir = '' - alternates_dir = os.path.join('info', 'alternates') + def __init__(self, root_path): """Initialize ourselves on a git ./objects directory""" @@ -59,7 +63,7 @@ class PureGitODB(PureRootPathDB, PureObjectDBW, PureCompoundDB, PureSubmoduleDB) loose_db = None for subpath, dbcls in ((self.packs_dir, self.PackDBCls), (self.loose_dir, self.LooseDBCls), - (self.alternates_dir, self.PureReferenceDBCls)): + (self.alternates_filepath, self.PureReferenceDBCls)): path = self.db_path(subpath) if os.path.exists(path): self._dbs.append(dbcls(path)) @@ -75,7 +79,7 @@ class PureGitODB(PureRootPathDB, PureObjectDBW, PureCompoundDB, PureSubmoduleDB) # END handle error # we the first one should have the store method - assert loose_db is not None and hasattr(loose_db, 'store'), "First database needs store functionality" + assert loose_db is not None and hasattr(loose_db, 'store'), "One database needs store functionality" # finally set the value self._loose_db = loose_db @@ -97,6 +101,7 @@ class PureGitODB(PureRootPathDB, PureObjectDBW, PureCompoundDB, PureSubmoduleDB) #} END objectdbw interface + class PureGitDB(PureGitODB, PureRepositoryPathsMixin, PureConfigurationMixin, PureReferencesMixin): """Git like database with support for object lookup as well as reference resolution. Our rootpath is set to the actual .git directory (bare on unbare). @@ -112,3 +117,6 @@ class PureGitDB(PureGitODB, PureRepositoryPathsMixin, PureConfigurationMixin, Pu +class PureCompatibilityGitDB(PureGitDB, RepoCompatInterface): + """Pure git database with a compatability layer required by 0.3x code""" + diff --git a/git/db/py/resolve.py b/git/db/py/resolve.py index 9cce8efe..94992d11 100644 --- a/git/db/py/resolve.py +++ b/git/db/py/resolve.py @@ -5,6 +5,9 @@ from git.db.interface import ReferencesMixin from git.exc import BadObject from git.refs import SymbolicReference from git.objects.base import Object +from git.refs.head import HEAD +from git.refs.headref import Head +from git.refs.tag import TagReference from git.util import ( join, isdir, @@ -281,17 +284,52 @@ class PureReferencesMixin(ReferencesMixin): re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$') re_hexsha_shortened = re.compile('^[0-9A-Fa-f]{4,40}$') + #{ Configuration + # Types to use when instatiating references + TagReferenceCls = TagReference + HeadCls = Head + ReferenceCls = Reference + HEADCls = HEAD + #} END configuration + def resolve(self, name): + return self.resolve_object(name).binsha + + def resolve_object(self, name): return rev_parse(self, name) @property def references(self): - raise NotImplementedError() + return self.ReferenceCls.list_items(self) @property def heads(self): - raise NotImplementedError() + return self.HeadCls.list_items(self) @property def tags(self): - raise NotImplementedError() + return self.TagReferenceCls.list_items(self) + + def tag(self, name): + return self.tags[name] + + @property + def head(self): + return self.HEADCls(self,'HEAD') + + def create_head(self, path, commit='HEAD', force=False, logmsg=None ): + return self.HeadCls.create(self, path, commit, force, logmsg) + + def delete_head(self, *heads, **kwargs): + return self.HeadCls.delete(self, *heads, **kwargs) + + def create_tag(self, path, ref='HEAD', message=None, force=False, **kwargs): + return self.TagReferenceCls.create(self, path, ref, message, force, **kwargs) + + def delete_tag(self, *tags): + return self.TagReferenceCls.delete(self, *tags) + + + # compat + branches = heads + refs = references diff --git a/git/db/py/transport.py b/git/db/py/transport.py index f8edfb23..00d222b0 100644 --- a/git/db/py/transport.py +++ b/git/db/py/transport.py @@ -9,6 +9,10 @@ from git.db.interface import ( TransportDB, FetchInfo, RefSpec ) +from git.refs.remote import RemoteReference +from git.remote import Remote + + __all__ = ["PureTransportDB"] class PurePushInfo(PushInfo): @@ -23,67 +27,32 @@ class PureFetchInfo(FetchInfo): class PureTransportDB(TransportDB): - """A database which allows to transport objects from and to different locations - which are specified by urls (location) and refspecs (what to transport, - see http://www.kernel.org/pub/software/scm/git/docs/git-fetch.html). - - At the beginning of a transport operation, it will be determined which objects - have to be sent (either by this or by the other side). - - Afterwards a pack with the required objects is sent (or received). If there is - nothing to send, the pack will be empty. - - The communication itself if implemented using a protocol instance which deals - with the actual formatting of the lines sent. - - As refspecs involve symbolic names for references to be handled, we require - RefParse functionality. How this is done is up to the actual implementation.""" # The following variables need to be set by the derived class #{Configuration protocol = None + RemoteCls = Remote #}end configuraiton #{ Interface def fetch(self, url, refspecs, progress=None, **kwargs): - """Fetch the objects defined by the given refspec from the given url. - :param url: url identifying the source of the objects. It may also be - a symbol from which the respective url can be resolved, like the - name of the remote. The implementation should allow objects as input - as well, these are assumed to resovle to a meaningful string though. - :param refspecs: iterable of reference specifiers or RefSpec instance, - identifying the references to be fetch from the remote. - :param progress: callable which receives progress messages for user consumption - :param kwargs: may be used for additional parameters that the actual implementation could - find useful. - :return: List of PureFetchInfo compatible instances which provide information about what - was previously fetched, in the order of the input refspecs. - :note: even if the operation fails, one of the returned PureFetchInfo instances - may still contain errors or failures in only part of the refspecs. - :raise: if any issue occours during the transport or if the url is not - supported by the protocol. - """ raise NotImplementedError() def push(self, url, refspecs, progress=None, **kwargs): - """Transport the objects identified by the given refspec to the remote - at the given url. - :param url: Decribes the location which is to receive the objects - see fetch() for more details - :param refspecs: iterable of refspecs strings or RefSpec instances - to identify the objects to push - :param progress: see fetch() - :param kwargs: additional arguments which may be provided by the caller - as they may be useful to the actual implementation - :todo: what to return ? - :raise: if any issue arises during transport or if the url cannot be handled""" raise NotImplementedError() @property def remotes(self): - """:return: An IterableList of Remote objects allowing to access and manipulate remotes - :note: Remote objects can also be used for the actual push or fetch operation""" - raise NotImplementedError() + return self.RemoteCls.list_items(self) + + def remote(self, name='origin'): + return self.remotes[name] + def create_remote(self, name, url, **kwargs): + return self.RemoteCls.create(self, name, url, **kwargs) + + def delete_remote(self, remote): + return self.RemoteCls.remove(self, remote) + #}end interface diff --git a/git/repo/base.py b/git/repo/base.py index 36c19ac8..0f86a6bf 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -12,7 +12,7 @@ from git.index import IndexFile from git.objects import * from git.config import GitConfigParser from git.remote import Remote -from git.db.cmd import CmdGitDB +from git.db.cmd import CmdCompatibilityGitDB from git.db.py import PureGitDB @@ -31,6 +31,8 @@ import os import sys import re +import warnings + DefaultDBType = PureGitDB if sys.version_info[1] < 5: # python 2.4 compatiblity DefaultDBType = CmdGitDB @@ -40,7 +42,7 @@ if sys.version_info[1] < 5: # python 2.4 compatiblity __all__ = ('Repo', ) -class Repo(object): +class Repo(CmdCompatibilityGitDB): """Represents a git repository and allows you to query references, gather commit information, generate diffs, create and clone repositories query the log. @@ -54,17 +56,8 @@ class Repo(object): 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]{4,40}$') - re_author_committer_start = re.compile(r'^(author|committer)') - re_tab_full_line = re.compile(r'^\t(.*)$') - def __init__(self, path=None, odbt = DefaultDBType): + def __init__(self, path=None, odbt = None): """Create a new Repo instance :param path: is the path to either the root git directory or the bare git repo:: @@ -73,619 +66,13 @@ class Repo(object): 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: + :raise InvalidDBRoot: :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, CmdGitDB): - 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 config_level(self): - return self.odb.config_level - - @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, logmsg=None ): - """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, logmsg) - - 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 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.""" - return self.odb.config_reader(config_level) - - 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 self.odb.config_writer(config_level) - - 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: - return self.head.commit - else: - return self.rev_parse(str(rev)+"^0") - - 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: - return self.head.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 - - :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.head.commit - - 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: [<line>]] - 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 <tom@mojombo.com> - # author-time 1192271832 - # author-tz -0700 - # committer Tom Preston-Werner - # committer-mail <tom@mojombo.com> - # 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 - # <and rest> - 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 /<name>.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) - - @classmethod - def _clone(cls, git, url, path, odb_default_type, **kwargs): - # 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', odb_default_type) - 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: - git.clone(url, 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 git.working_dir: - path = join(git._working_dir, path) - - # adjust remotes - there may be operating systems which use backslashes, - # These might be given as initial paths, but when handling the config file - # that contains the remote from which we were clones, git stops liking it - # as it will escape the backslashes. Hence we undo the escaping just to be - # sure - repo = cls(os.path.abspath(path), odbt = odbt) - if repo.remotes: - repo.remotes[0].config_writer.set_value('url', repo.remotes[0].url.replace("\\\\", "\\").replace("\\", "/")) - # END handle remote repo - return repo - - def clone(self, path, **kwargs): - """Create a clone from this repository. - :param path: - is the full path of the new repo (traditionally ends with ./<name>.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)""" - return self._clone(self.git, self.git_dir, path, type(self.odb), **kwargs) - - @classmethod - def clone_from(cls, url, to_path, **kwargs): - """Create a clone from the given URL - :param url: valid git url, see http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS - :param to_path: Path to which the repository should be cloned to - :param kwargs: see the ``clone`` method - :return: Repo instance pointing to the cloned directory""" - return cls._clone(Git(os.getcwd()), url, to_path, CmdGitDB, **kwargs) - - 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.head.commit - 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, name): - return self.odb.resolve(name) + if odbt is not None: + warnings.warn("deprecated use of odbt", DeprecationWarning) + #END handle old parameter + super(Repo, self).__init__(path) + self._git = Git(self.working_dir) def __repr__(self): return '<git.Repo "%s">' % self.git_dir diff --git a/git/test/test_util.py b/git/test/test_util.py index 06dc2941..f737e660 100644 --- a/git/test/test_util.py +++ b/git/test/test_util.py @@ -33,8 +33,8 @@ class TestUtils(TestBase): } def test_it_should_dashify(self): - assert_equal('this-is-my-argument', dashify('this_is_my_argument')) - assert_equal('foo', dashify('foo')) + assert 'this-is-my-argument' == dashify('this_is_my_argument') + assert 'foo' == dashify('foo') def test_lock_file(self): |