diff options
Diffstat (limited to 'lib/git')
-rw-r--r-- | lib/git/__init__.py | 1 | ||||
-rw-r--r-- | lib/git/config.py | 83 | ||||
-rw-r--r-- | lib/git/refs.py | 67 | ||||
-rw-r--r-- | lib/git/remote.py | 225 | ||||
-rw-r--r-- | lib/git/repo.py | 10 |
5 files changed, 355 insertions, 31 deletions
diff --git a/lib/git/__init__.py b/lib/git/__init__.py index 041d69f7..45364b88 100644 --- a/lib/git/__init__.py +++ b/lib/git/__init__.py @@ -20,6 +20,7 @@ from git.repo import Repo from git.stats import Stats from git.utils import dashify from git.utils import touch +from git.remote import Remote __all__ = [ name for name, obj in locals().items() diff --git a/lib/git/config.py b/lib/git/config.py index b555677e..6f979c73 100644 --- a/lib/git/config.py +++ b/lib/git/config.py @@ -13,6 +13,7 @@ import os import ConfigParser as cp from git.odict import OrderedDict import inspect +import cStringIO class _MetaParserBuilder(type): """ @@ -221,15 +222,88 @@ class GitConfigParser(cp.RawConfigParser, object): """ return optionstr + def _read(self, fp, fpname): + """ + A direct copy of the py2.4 version of the super class's _read method + to assure it uses ordered dicts. Had to change one line to make it work. + + Future versions have this fixed, but in fact its quite embarassing for the + guys not to have done it right in the first place ! + + Removed big comments to make it more compact. + + Made sure it ignores initial whitespace as git uses tabs + """ + cursect = None # None, or a dictionary + optname = None + lineno = 0 + e = None # None, or an exception + while True: + line = fp.readline() + if not line: + break + lineno = lineno + 1 + # comment or blank line? + if line.strip() == '' or line[0] in '#;': + continue + if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR": + # no leading whitespace + continue + else: + # is it a section header? + mo = self.SECTCRE.match(line) + if mo: + sectname = mo.group('header') + if sectname in self._sections: + cursect = self._sections[sectname] + elif sectname == cp.DEFAULTSECT: + cursect = self._defaults + else: + # THE ONLY LINE WE CHANGED ! + cursect = OrderedDict((('__name__', sectname),)) + self._sections[sectname] = cursect + # So sections can't start with a continuation line + optname = None + # no section header in the file? + elif cursect is None: + raise cp.MissingSectionHeaderError(fpname, lineno, line) + # an option line? + else: + mo = self.OPTCRE.match(line) + if mo: + optname, vi, optval = mo.group('option', 'vi', 'value') + if vi in ('=', ':') and ';' in optval: + pos = optval.find(';') + if pos != -1 and optval[pos-1].isspace(): + optval = optval[:pos] + optval = optval.strip() + if optval == '""': + optval = '' + optname = self.optionxform(optname.rstrip()) + cursect[optname] = optval + else: + if not e: + e = cp.ParsingError(fpname) + e.append(lineno, repr(line)) + # END + # END ? + # END ? + # END while reading + # if any parsing errors occurred, raise an exception + if e: + raise e + + def read(self): """ - Reads the data stored in the files we have been initialized with + Reads the data stored in the files we have been initialized with. It will + ignore files that cannot be read, possibly leaving an empty configuration Returns Nothing Raises - IOError if not all files could be read + IOError if a file cannot be handled """ if self._is_initialized: return @@ -244,7 +318,10 @@ class GitConfigParser(cp.RawConfigParser, object): close_fp = False # assume a path if it is not a file-object if not hasattr(file_object, "seek"): - fp = open(file_object) + try: + fp = open(file_object) + except IOError,e: + continue close_fp = True # END fp handling diff --git a/lib/git/refs.py b/lib/git/refs.py index 4445f252..618babc2 100644 --- a/lib/git/refs.py +++ b/lib/git/refs.py @@ -15,6 +15,7 @@ class Reference(LazyMixin, Iterable): Represents a named reference to any object """ __slots__ = ("repo", "path") + _common_path_default = "refs" def __init__(self, repo, path, object = None): """ @@ -75,7 +76,7 @@ class Reference(LazyMixin, Iterable): return Object.new(self.repo, self.path) @classmethod - def iter_items(cls, repo, common_path = "refs", **kwargs): + def iter_items(cls, repo, common_path = None, **kwargs): """ Find all refs in the repository @@ -84,7 +85,9 @@ class Reference(LazyMixin, Iterable): ``common_path`` Optional keyword argument to the path which is to be shared by all - returned Ref objects + returned Ref objects. + Defaults to class specific portion if None assuring that only + refs suitable for the actual class are returned. ``kwargs`` Additional options given as keyword arguments, will be passed @@ -100,7 +103,10 @@ class Reference(LazyMixin, Iterable): options = {'sort': "committerdate", 'format': "%(refname)%00%(objectname)%00%(objecttype)%00%(objectsize)"} - + + if common_path is None: + common_path = cls._common_path_default + options.update(kwargs) output = repo.git.for_each_ref(common_path, **options) @@ -157,7 +163,8 @@ class Head(Reference): >>> head.commit.id '1c09f116cbc2cb4100fb6935bb162daa4723f455' """ - + _common_path_default = "refs/heads" + @property def commit(self): """ @@ -166,20 +173,6 @@ class Head(Reference): """ return self.object - @classmethod - def iter_items(cls, repo, common_path = "refs/heads", **kwargs): - """ - Returns - Iterator yielding Head items - - For more documentation, please refer to git.base.Ref.list_items - """ - return super(Head,cls).iter_items(repo, common_path, **kwargs) - - def __repr__(self): - return '<git.Head "%s">' % self.name - - class TagRef(Reference): """ @@ -197,6 +190,7 @@ class TagRef(Reference): """ __slots__ = tuple() + _common_path_default = "refs/tags" @property def commit(self): @@ -223,16 +217,35 @@ class TagRef(Reference): return self.object return None - @classmethod - def iter_items(cls, repo, common_path = "refs/tags", **kwargs): + +# provide an alias +Tag = TagRef + +class RemoteRef(Head): + """ + Represents a reference pointing to a remote head. + """ + _common_path_default = "refs/remotes" + + @property + def remote_name(self): """ Returns - Iterator yielding commit items - - For more documentation, please refer to git.base.Ref.list_items + Name of the remote we are a reference of, such as 'origin' for a reference + named 'origin/master' """ - return super(TagRef,cls).iter_items(repo, common_path, **kwargs) - + tokens = self.path.split('/') + # /refs/remotes/<remote name>/<branch_name> + return tokens[2] -# provide an alias -Tag = TagRef + @property + def remote_branch(self): + """ + Returns + Name of the remote branch itself, i.e. master. + + NOTE: The returned name is usually not qualified enough to uniquely identify + a branch + """ + tokens = self.path.split('/') + return '/'.join(tokens[3:]) diff --git a/lib/git/remote.py b/lib/git/remote.py new file mode 100644 index 00000000..24efd900 --- /dev/null +++ b/lib/git/remote.py @@ -0,0 +1,225 @@ +# remote.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 implementing a remote object allowing easy access to git remotes +""" + +from git.utils import LazyMixin, Iterable +from refs import RemoteRef + +class _SectionConstraint(object): + """ + Constrains a ConfigParser to only option commands which are constrained to + always use the section we have been initialized with. + + It supports all ConfigParser methods that operate on an option + """ + __slots__ = ("_config", "_section_name") + _valid_attrs_ = ("get", "set", "getint", "getfloat", "getboolean", "has_option") + + def __init__(self, config, section): + self._config = config + self._section_name = section + + def __getattr__(self, attr): + if attr in self._valid_attrs_: + return lambda *args: self._call_config(attr, *args) + return super(_SectionConstraint,self).__getattribute__(attr) + + def _call_config(self, method, *args): + """Call the configuration at the given method which must take a section name + as first argument""" + return getattr(self._config, method)(self._section_name, *args) + + +class Remote(LazyMixin, Iterable): + """ + Provides easy read and write access to a git remote. + + Everything not part of this interface is considered an option for the current + remote, allowing constructs like remote.pushurl to query the pushurl. + + NOTE: When querying configuration, the configuration accessor will be cached + to speed up subsequent accesses. + """ + + __slots__ = ( "repo", "name", "_config_reader" ) + + def __init__(self, repo, name): + """ + Initialize a remote instance + + ``repo`` + The repository we are a remote of + + ``name`` + the name of the remote, i.e. 'origin' + """ + self.repo = repo + self.name = name + + def __getattr__(self, attr): + """ + Allows to call this instance like + remote.special( *args, **kwargs) to call git-remote special self.name + """ + if attr == "_config_reader": + return super(Remote, self).__getattr__(attr) + + return self._config_reader.get(attr) + + def _config_section_name(self): + return 'remote "%s"' % self.name + + def _set_cache_(self, attr): + if attr == "_config_reader": + self._config_reader = _SectionConstraint(self.repo.config_reader, self._config_section_name()) + else: + super(Remote, self)._set_cache_(attr) + + + def __str__(self): + return self.name + + def __repr__(self): + return '<git.%s "%s">' % (self.__class__.__name__, self.name) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return not ( self == other ) + + def __hash__(self): + return hash(self.name) + + @classmethod + def iter_items(cls, repo): + """ + Returns + Iterator yielding Remote objects of the given repository + """ + # parse them using refs, as their query can be faster as it is + # purely based on the file system + seen_remotes = set() + for ref in RemoteRef.iter_items(repo): + remote_name = ref.remote_name + if remote_name in seen_remotes: + continue + # END if remote done already + seen_remotes.add(remote_name) + yield Remote(repo, remote_name) + # END for each ref + + @property + def refs(self): + """ + Returns + List of RemoteRef objects + """ + out_refs = list() + for ref in RemoteRef.list_items(self.repo): + if ref.remote_name == self.name: + out_refs.append(ref) + # END if names match + # END for each ref + assert out_refs, "Remote %s did not have any references" % self.name + return out_refs + + @classmethod + def create(cls, repo, name, url, **kwargs): + """ + Create a new remote to the given repository + ``repo`` + Repository instance that is to receive the new remote + + ``name`` + Desired name of the remote + + ``url`` + URL which corresponds to the remote's name + + ``**kwargs`` + Additional arguments to be passed to the git-remote add command + + Returns + New Remote instance + + Raise + GitCommandError in case an origin with that name already exists + """ + repo.git.remote( "add", name, url, **kwargs ) + return cls(repo, name) + + # add is an alias + add = create + + @classmethod + def remove(cls, repo, name ): + """ + Remove the remote with the given name + """ + repo.git.remote("rm", name) + + # alias + rm = remove + + def rename(self, new_name): + """ + Rename self to the given new_name + + Returns + self + """ + if self.name == new_name: + return self + + self.repo.git.remote("rename", self.name, new_name) + self.name = new_name + del(self._config_reader) # it contains cached values, section names are different now + return self + + def update(self, **kwargs): + """ + Fetch all changes for this remote, including new branches + + ``kwargs`` + Additional arguments passed to git-remote update + + Returns + self + """ + self.repo.git.remote("update", self.name) + return self + + @property + def config_reader(self): + """ + Returns + GitConfigParser compatible object able to read options for only our remote. + Hence you may simple type config.get("pushurl") to obtain the information + """ + return self._config_reader + + @property + def config_writer(self): + """ + Return + GitConfigParser compatible object able to write options for this remote. + + Note + You can only own one writer at a time - delete it to release the + configuration file and make it useable by others. + + To assure consistent results, you should only query options through the + writer. Once you are done writing, you are free to use the config reader + once again. + """ + writer = self.repo.config_writer() + + # clear our cache to assure we re-read the possibly changed configuration + del(self._config_reader) + return _SectionConstraint(writer, self._config_section_name()) diff --git a/lib/git/repo.py b/lib/git/repo.py index c53a4d9b..d5cc9782 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -17,6 +17,7 @@ from actor import Actor from refs import * from objects import * from config import GitConfigParser +from remote import Remote class Repo(object): """ @@ -107,6 +108,13 @@ class Repo(object): ``git.Head[]`` """ return Head.list_items(self) + + @property + def remotes(self): + """ + A list of Remote objects allowing to access and manipulate remotes + """ + return Remote.list_items(self) # alias heads branches = heads @@ -141,7 +149,7 @@ class Repo(object): elif config_level == "global": return os.path.expanduser("~/.gitconfig") elif config_level == "repository": - return "%s/config" % self.git.git_dir + return "%s/config" % self.path raise ValueError( "Invalid configuration level: %r" % config_level ) |