summaryrefslogtreecommitdiff
path: root/lib/git
diff options
context:
space:
mode:
Diffstat (limited to 'lib/git')
-rw-r--r--lib/git/__init__.py1
-rw-r--r--lib/git/config.py83
-rw-r--r--lib/git/refs.py67
-rw-r--r--lib/git/remote.py225
-rw-r--r--lib/git/repo.py10
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 )