diff options
Diffstat (limited to 'lib/git/repo.py')
-rw-r--r-- | lib/git/repo.py | 1543 |
1 files changed, 773 insertions, 770 deletions
diff --git a/lib/git/repo.py b/lib/git/repo.py index 72f7ba06..ac8ee590 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -13,7 +13,10 @@ from objects import * from config import GitConfigParser from remote import Remote -from db import GitCmdObjectDB +from db import ( + GitCmdObjectDB, + GitDB + ) import os import sys @@ -23,786 +26,786 @@ import StringIO def touch(filename): - fp = open(filename, "a") - fp.close() + fp = open(filename, "a") + fp.close() def is_git_dir(d): - """ This is taken from the git setup.c:is_git_directory - function.""" - - if os.path.isdir(d) and \ - os.path.isdir(os.path.join(d, 'objects')) and \ - os.path.isdir(os.path.join(d, 'refs')): - headref = os.path.join(d, 'HEAD') - return os.path.isfile(headref) or \ - (os.path.islink(headref) and - os.readlink(headref).startswith('refs')) - return False + """ This is taken from the git setup.c:is_git_directory + function.""" + + if os.path.isdir(d) and \ + os.path.isdir(os.path.join(d, 'objects')) and \ + os.path.isdir(os.path.join(d, 'refs')): + headref = os.path.join(d, 'HEAD') + return os.path.isfile(headref) or \ + (os.path.islink(headref) and + os.readlink(headref).startswith('refs')) + return False class Repo(object): - """ - Represents a git repository and allows you to query references, - gather commit information, generate diffs, create and clone repositories query - the log. - - The following attributes are worth using: - - 'working_dir' is the working directory of the git command, wich is the working tree - directory if available or the .git directory in case of bare repositories - - 'working_tree_dir' is the working tree directory, but will raise AssertionError - if we are a bare repository. - - 'git_dir' is the .git repository directoy, which is always set. - """ - DAEMON_EXPORT_FILE = 'git-daemon-export-ok' - __slots__ = ( "working_dir", "_working_tree_dir", "git_dir", "_bare", "git", "odb" ) - - # precompiled regex - re_whitespace = re.compile(r'\s+') - re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$') - re_author_committer_start = re.compile(r'^(author|committer)') - re_tab_full_line = re.compile(r'^\t(.*)$') - - # invariants - # represents the configuration level of a configuration file - config_level = ("system", "global", "repository") - - def __init__(self, path=None, odbt = GitCmdObjectDB): - """ Create a new Repo instance + """ + Represents a git repository and allows you to query references, + gather commit information, generate diffs, create and clone repositories query + the log. + + The following attributes are worth using: + + 'working_dir' is the working directory of the git command, wich is the working tree + directory if available or the .git directory in case of bare repositories + + 'working_tree_dir' is the working tree directory, but will raise AssertionError + if we are a bare repository. + + 'git_dir' is the .git repository directoy, which is always set. + """ + DAEMON_EXPORT_FILE = 'git-daemon-export-ok' + __slots__ = ( "working_dir", "_working_tree_dir", "git_dir", "_bare", "git", "odb" ) + + # precompiled regex + re_whitespace = re.compile(r'\s+') + re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$') + re_author_committer_start = re.compile(r'^(author|committer)') + re_tab_full_line = re.compile(r'^\t(.*)$') + + # invariants + # represents the configuration level of a configuration file + config_level = ("system", "global", "repository") + + def __init__(self, path=None, odbt = GitCmdObjectDB): + """ Create a new Repo instance :param path: is the path to either the root git directory or the bare git repo:: - repo = Repo("/Users/mtrier/Development/git-python") - repo = Repo("/Users/mtrier/Development/git-python.git") - repo = Repo("~/Development/git-python.git") - repo = Repo("$REPOSITORIES/Development/git-python.git") - - :param odbt: Object DataBase type - a type which is constructed by providing - the directory containing the database objects, i.e. .git/objects. It will - be used to access all object data - :raise InvalidGitRepositoryError: - :raise NoSuchPathError: + repo = Repo("/Users/mtrier/Development/git-python") + repo = Repo("/Users/mtrier/Development/git-python.git") + repo = Repo("~/Development/git-python.git") + repo = Repo("$REPOSITORIES/Development/git-python.git") + + :param odbt: Object DataBase type - a type which is constructed by providing + the directory containing the database objects, i.e. .git/objects. It will + be used to access all object data + :raise InvalidGitRepositoryError: + :raise NoSuchPathError: :return: git.Repo """ - epath = os.path.abspath(os.path.expandvars(os.path.expanduser(path or os.getcwd()))) - - if not os.path.exists(epath): - raise NoSuchPathError(epath) - - self.working_dir = None - self._working_tree_dir = None - self.git_dir = None - curpath = epath - - # walk up the path to find the .git dir - while curpath: - if is_git_dir(curpath): - self.git_dir = curpath - self._working_tree_dir = os.path.dirname(curpath) - break - gitpath = os.path.join(curpath, '.git') - if is_git_dir(gitpath): - self.git_dir = gitpath - self._working_tree_dir = curpath - break - curpath, dummy = os.path.split(curpath) - if not dummy: - break - # END while curpath - - if self.git_dir is None: - raise InvalidGitRepositoryError(epath) - - self._bare = False - try: - self._bare = self.config_reader("repository").getboolean('core','bare') - except Exception: - # lets not assume the option exists, although it should - pass - - # adjust the wd in case we are actually bare - we didn't know that - # in the first place - if self._bare: - self._working_tree_dir = None - # END working dir handling - - self.working_dir = self._working_tree_dir or self.git_dir - self.git = Git(self.working_dir) - - # special handling, in special times - args = [os.path.join(self.git_dir, 'objects')] - if issubclass(odbt, GitCmdObjectDB): - args.append(self.git) + epath = os.path.abspath(os.path.expandvars(os.path.expanduser(path or os.getcwd()))) + + if not os.path.exists(epath): + raise NoSuchPathError(epath) + + self.working_dir = None + self._working_tree_dir = None + self.git_dir = None + curpath = epath + + # walk up the path to find the .git dir + while curpath: + if is_git_dir(curpath): + self.git_dir = curpath + self._working_tree_dir = os.path.dirname(curpath) + break + gitpath = os.path.join(curpath, '.git') + if is_git_dir(gitpath): + self.git_dir = gitpath + self._working_tree_dir = curpath + break + curpath, dummy = os.path.split(curpath) + if not dummy: + break + # END while curpath + + if self.git_dir is None: + raise InvalidGitRepositoryError(epath) + + self._bare = False + try: + self._bare = self.config_reader("repository").getboolean('core','bare') + except Exception: + # lets not assume the option exists, although it should + pass + + # adjust the wd in case we are actually bare - we didn't know that + # in the first place + if self._bare: + self._working_tree_dir = None + # END working dir handling + + self.working_dir = self._working_tree_dir or self.git_dir + self.git = Git(self.working_dir) + + # special handling, in special times + args = [os.path.join(self.git_dir, 'objects')] + if issubclass(odbt, GitCmdObjectDB): + args.append(self.git) self.odb = odbt(*args) - def __eq__(self, rhs): - if isinstance(rhs, Repo): - return self.git_dir == rhs.git_dir - return False - - def __ne__(self, rhs): - return not self.__eq__(rhs) - - def __hash__(self): - return hash(self.git_dir) + def __eq__(self, rhs): + if isinstance(rhs, Repo): + return self.git_dir == rhs.git_dir + return False + + def __ne__(self, rhs): + return not self.__eq__(rhs) + + def __hash__(self): + return hash(self.git_dir) def __repr__(self): return "%s(%r)" % (type(self).__name__, self.git_dir) - # Description property - def _get_description(self): - filename = os.path.join(self.git_dir, 'description') - return file(filename).read().rstrip() - - def _set_description(self, descr): - filename = os.path.join(self.git_dir, 'description') - file(filename, 'w').write(descr+'\n') - - description = property(_get_description, _set_description, - doc="the project's description") - del _get_description - del _set_description - - - - @property - def working_tree_dir(self): - """ - Returns - The working tree directory of our git repository - - Raises 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): - """ - Returns - 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 - - Returns - ``git.IterableList(Head, ...)`` - """ - return Head.list_items(self) - - @property - def references(self): - """ - A list of Reference objects representing tags, heads and remote references. - - Returns - IterableList(Reference, ...) - """ - return Reference.list_items(self) - - # alias for references - refs = references - - # alias for heads - branches = heads - - @property - def index(self): - """ - Returns - 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 - - Returns - ``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 - - Returns - ``git.IterableList(TagReference, ...)`` - """ - return TagReference.list_items(self) - - def tag(self,path): - """ - Return - TagReference Object, reference pointing to a Commit or Tag - - ``path`` - path to the tag reference, i.e. 0.1.5 or tags/0.1.5 - """ - return TagReference(self, path) - - def create_head(self, path, commit='HEAD', force=False, **kwargs ): - """ - Create a new head within the repository. - - For more documentation, please see the Head.create method. - - Returns - newly created Head Reference - """ - return Head.create(self, path, commit, force, **kwargs) - - def delete_head(self, *heads, **kwargs): - """ - Delete the given heads - - ``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. - - Returns - 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 - - Returns - Remote reference - """ - return Remote.create(self, name, url, **kwargs) - - def delete_remote(self, remote): - """ - Delete the given remote. - """ - return Remote.remove(self, remote) - - def _get_config_path(self, config_level ): - # we do not support an absolute path of the gitconfig on windows , - # use the global config instead - if sys.platform == "win32" and config_level == "system": - config_level = "global" - - if config_level == "system": - return "/etc/gitconfig" - elif config_level == "global": - return os.path.expanduser("~/.gitconfig") - elif config_level == "repository": - return "%s/config" % self.git_dir - - raise ValueError( "Invalid configuration level: %r" % config_level ) - - def config_reader(self, config_level=None): - """ - Returns - 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. - - NOTE: On windows, system configuration cannot currently be read as the path is - unknown, instead the global path will be used. - - ``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 - """ - files = None - if config_level is None: - files = [ self._get_config_path(f) for f in self.config_level ] - else: - files = [ self._get_config_path(config_level) ] - return GitConfigParser(files, read_only=True) - - def config_writer(self, config_level="repository"): - """ - Returns - 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. - - ``config_level`` - One of the following values - system = sytem wide configuration file - global = user level configuration file - repository = configuration file for this repostory only - """ - return GitConfigParser(self._get_config_path(config_level), read_only = False) - - def commit(self, rev=None): - """ - The Commit object for the specified revision - - ``rev`` - revision specifier, see git-rev-parse for viable options. - - Returns - ``git.Commit`` - """ - if rev is None: - rev = self.active_branch - - c = Object.new(self, rev) - assert c.type == "commit", "Revision %s did not point to a commit, but to %s" % (rev, c) - return c - - def iter_trees(self, *args, **kwargs): - """ - Returns - 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 - - ``rev`` - is a revision pointing to a Treeish ( being a commit or tree ) - - Examples:: - - repo.tree(repo.heads[0]) - - Returns - ``git.Tree`` - - NOTE - If you need a non-root level tree, find it by iterating the root tree. Otherwise - it cannot know about its path relative to the repository root and subsequent - operations might have unexpected results. - """ - if rev is None: - rev = self.active_branch - - c = Object.new(self, rev) - if c.type == "commit": - return c.tree - elif c.type == "tree": - return c - raise ValueError( "Revision %s did not point to a treeish, but to %s" % (rev, c)) - - def iter_commits(self, rev=None, paths='', **kwargs): - """ - A list of Commit objects representing the history of a given ref/commit - - ``rev`` - revision specifier, see git-rev-parse for viable options. - If None, the active branch will be used. - - ``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. - - ``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 - - Returns - ``git.Commit[]`` - """ - if rev is None: - rev = self.active_branch - - return Commit.iter_items(self, rev, paths, **kwargs) - - def _get_daemon_export(self): - filename = os.path.join(self.git_dir, self.DAEMON_EXPORT_FILE) - return os.path.exists(filename) - - def _set_daemon_export(self, value): - filename = os.path.join(self.git_dir, self.DAEMON_EXPORT_FILE) - fileexists = os.path.exists(filename) - if value and not fileexists: - touch(filename) - elif not value and fileexists: - os.unlink(filename) - - daemon_export = property(_get_daemon_export, _set_daemon_export, - doc="If True, git-daemon may export this repository") - del _get_daemon_export - del _set_daemon_export - - def _get_alternates(self): - """ - The list of alternates for this repo from which objects can be retrieved - - Returns - list of strings being pathnames of alternates - """ - alternates_path = os.path.join(self.git_dir, 'objects', 'info', 'alternates') - - if os.path.exists(alternates_path): - try: - f = open(alternates_path) - alts = f.read() - finally: - f.close() - return alts.strip().splitlines() - else: - return [] - - def _set_alternates(self, alts): - """ - Sets the alternates - - ``alts`` - is the array of string paths representing the alternates at which - git should look for objects, i.e. /home/user/repo/.git/objects - - Raises - NoSuchPathError - - Note - The method does not check for the existance of the paths in alts - as the caller is responsible. - - Returns - None - """ - alternates_path = os.path.join(self.git_dir, 'objects', 'info', 'alternates') - if not alts: - if os.path.isfile(alternates_path): - os.remove(alternates_path) - else: - try: - f = open(alternates_path, 'w') - f.write("\n".join(alts)) - finally: - f.close() - # END file handling - # END alts handling - - alternates = property(_get_alternates, _set_alternates, doc="Retrieve a list of alternates paths or set a list paths to be used as alternates") - - def is_dirty(self, index=True, working_tree=True, untracked_files=False): - """ - Returns - ``True``, the repository is considered dirty. By default it will react - like a git-status without untracked files, hence it is dirty if the - index or the working copy have changes. - """ - if self._bare: - # Bare repositories with no associated working directory are - # always consired to be clean. - return False - - # start from the one which is fastest to evaluate - default_args = ('--abbrev=40', '--full-index', '--raw') - if index: - # diff index against HEAD - if os.path.isfile(self.index.path) and self.head.is_valid() and \ - len(self.git.diff('HEAD', '--cached', *default_args)): - return True - # END index handling - if working_tree: - # diff index against working tree - if len(self.git.diff(*default_args)): - return True - # END working tree handling - if untracked_files: - if len(self.untracked_files): - return True - # END untracked files - return False - - @property - def untracked_files(self): - """ - Returns - 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. - - Returns - 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. - - ``rev`` - revision specifier, see git-rev-parse for viable options. - - Returns - 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 = {} - blames = [] - 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, 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 - - ``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 - - ``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 - - ``kwargs`` - keyword arguments serving as additional options to the git-init command - - Examples:: - - git.Repo.init('/var/git/myrepo.git',bare=True) - - Returns - ``git.Repo`` (the newly created repo) - """ - - if mkdir and path and not os.path.exists(path): - os.makedirs(path, 0755) - - # git command automatically chdir into the directory - git = Git(path) - output = git.init(**kwargs) - return Repo(path) - - def clone(self, path, **kwargs): - """ - Create a clone from this repository. - - ``path`` - is the full path of the new repo (traditionally ends with ./<name>.git). - - ``kwargs`` - keyword arguments to be given to the git-clone command - - Returns - ``git.Repo`` (the newly cloned repo) - """ - # special handling for windows for path at which the clone should be - # created. - # tilde '~' will be expanded to the HOME no matter where the ~ occours. Hence - # we at least give a proper error instead of letting git fail - prev_cwd = None - prev_path = None - if os.name == 'nt': - if '~' in path: - raise OSError("Git cannot handle the ~ character in path %r correctly" % path) - - # on windows, git will think paths like c: are relative and prepend the - # current working dir ( before it fails ). We temporarily adjust the working - # dir to make this actually work - match = re.match("(\w:[/\\\])(.*)", path) - if match: - prev_cwd = os.getcwd() - prev_path = path - drive, rest_of_path = match.groups() - os.chdir(drive) - path = rest_of_path - kwargs['with_keep_cwd'] = True - # END cwd preparation - # END windows handling - - try: - self.git.clone(self.git_dir, path, **kwargs) - finally: - if prev_cwd is not None: - os.chdir(prev_cwd) - path = prev_path - # END reset previous working dir - # END bad windows handling - return Repo(path) - - - def archive(self, ostream, treeish=None, prefix=None, **kwargs): - """ - Archive the tree at the given revision. - ``ostream`` - file compatible stream object to which the archive will be written - - ``treeish`` - is the treeish name/id, defaults to active branch - - ``prefix`` - is the optional prefix to prepend to each filename in the archive - - ``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 - - Examples:: - - >>> repo.archive(open("archive")) - <String containing tar.gz archive> - - Raise - GitCommandError in case something went wrong - - Returns - self - """ - if treeish is None: - treeish = self.active_branch - if prefix and 'prefix' not in kwargs: - kwargs['prefix'] = prefix - kwargs['output_stream'] = ostream - - self.git.archive(treeish, **kwargs) - return self - - def __repr__(self): - return '<git.Repo "%s">' % self.git_dir + # Description property + def _get_description(self): + filename = os.path.join(self.git_dir, 'description') + return file(filename).read().rstrip() + + def _set_description(self, descr): + filename = os.path.join(self.git_dir, 'description') + file(filename, 'w').write(descr+'\n') + + description = property(_get_description, _set_description, + doc="the project's description") + del _get_description + del _set_description + + + + @property + def working_tree_dir(self): + """ + Returns + The working tree directory of our git repository + + Raises 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): + """ + Returns + 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 + + Returns + ``git.IterableList(Head, ...)`` + """ + return Head.list_items(self) + + @property + def references(self): + """ + A list of Reference objects representing tags, heads and remote references. + + Returns + IterableList(Reference, ...) + """ + return Reference.list_items(self) + + # alias for references + refs = references + + # alias for heads + branches = heads + + @property + def index(self): + """ + Returns + 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 + + Returns + ``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 + + Returns + ``git.IterableList(TagReference, ...)`` + """ + return TagReference.list_items(self) + + def tag(self,path): + """ + Return + TagReference Object, reference pointing to a Commit or Tag + + ``path`` + path to the tag reference, i.e. 0.1.5 or tags/0.1.5 + """ + return TagReference(self, path) + + def create_head(self, path, commit='HEAD', force=False, **kwargs ): + """ + Create a new head within the repository. + + For more documentation, please see the Head.create method. + + Returns + newly created Head Reference + """ + return Head.create(self, path, commit, force, **kwargs) + + def delete_head(self, *heads, **kwargs): + """ + Delete the given heads + + ``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. + + Returns + 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 + + Returns + Remote reference + """ + return Remote.create(self, name, url, **kwargs) + + def delete_remote(self, remote): + """ + Delete the given remote. + """ + return Remote.remove(self, remote) + + def _get_config_path(self, config_level ): + # we do not support an absolute path of the gitconfig on windows , + # use the global config instead + if sys.platform == "win32" and config_level == "system": + config_level = "global" + + if config_level == "system": + return "/etc/gitconfig" + elif config_level == "global": + return os.path.expanduser("~/.gitconfig") + elif config_level == "repository": + return "%s/config" % self.git_dir + + raise ValueError( "Invalid configuration level: %r" % config_level ) + + def config_reader(self, config_level=None): + """ + Returns + 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. + + NOTE: On windows, system configuration cannot currently be read as the path is + unknown, instead the global path will be used. + + ``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 + """ + files = None + if config_level is None: + files = [ self._get_config_path(f) for f in self.config_level ] + else: + files = [ self._get_config_path(config_level) ] + return GitConfigParser(files, read_only=True) + + def config_writer(self, config_level="repository"): + """ + Returns + 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. + + ``config_level`` + One of the following values + system = sytem wide configuration file + global = user level configuration file + repository = configuration file for this repostory only + """ + return GitConfigParser(self._get_config_path(config_level), read_only = False) + + def commit(self, rev=None): + """ + The Commit object for the specified revision + + ``rev`` + revision specifier, see git-rev-parse for viable options. + + Returns + ``git.Commit`` + """ + if rev is None: + rev = self.active_branch + + c = Object.new(self, rev) + assert c.type == "commit", "Revision %s did not point to a commit, but to %s" % (rev, c) + return c + + def iter_trees(self, *args, **kwargs): + """ + Returns + 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 + + ``rev`` + is a revision pointing to a Treeish ( being a commit or tree ) + + Examples:: + + repo.tree(repo.heads[0]) + + Returns + ``git.Tree`` + + NOTE + If you need a non-root level tree, find it by iterating the root tree. Otherwise + it cannot know about its path relative to the repository root and subsequent + operations might have unexpected results. + """ + if rev is None: + rev = self.active_branch + + c = Object.new(self, rev) + if c.type == "commit": + return c.tree + elif c.type == "tree": + return c + raise ValueError( "Revision %s did not point to a treeish, but to %s" % (rev, c)) + + def iter_commits(self, rev=None, paths='', **kwargs): + """ + A list of Commit objects representing the history of a given ref/commit + + ``rev`` + revision specifier, see git-rev-parse for viable options. + If None, the active branch will be used. + + ``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. + + ``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 + + Returns + ``git.Commit[]`` + """ + if rev is None: + rev = self.active_branch + + return Commit.iter_items(self, rev, paths, **kwargs) + + def _get_daemon_export(self): + filename = os.path.join(self.git_dir, self.DAEMON_EXPORT_FILE) + return os.path.exists(filename) + + def _set_daemon_export(self, value): + filename = os.path.join(self.git_dir, self.DAEMON_EXPORT_FILE) + fileexists = os.path.exists(filename) + if value and not fileexists: + touch(filename) + elif not value and fileexists: + os.unlink(filename) + + daemon_export = property(_get_daemon_export, _set_daemon_export, + doc="If True, git-daemon may export this repository") + del _get_daemon_export + del _set_daemon_export + + def _get_alternates(self): + """ + The list of alternates for this repo from which objects can be retrieved + + Returns + list of strings being pathnames of alternates + """ + alternates_path = os.path.join(self.git_dir, 'objects', 'info', 'alternates') + + if os.path.exists(alternates_path): + try: + f = open(alternates_path) + alts = f.read() + finally: + f.close() + return alts.strip().splitlines() + else: + return [] + + def _set_alternates(self, alts): + """ + Sets the alternates + + ``alts`` + is the array of string paths representing the alternates at which + git should look for objects, i.e. /home/user/repo/.git/objects + + Raises + NoSuchPathError + + Note + The method does not check for the existance of the paths in alts + as the caller is responsible. + + Returns + None + """ + alternates_path = os.path.join(self.git_dir, 'objects', 'info', 'alternates') + if not alts: + if os.path.isfile(alternates_path): + os.remove(alternates_path) + else: + try: + f = open(alternates_path, 'w') + f.write("\n".join(alts)) + finally: + f.close() + # END file handling + # END alts handling + + alternates = property(_get_alternates, _set_alternates, doc="Retrieve a list of alternates paths or set a list paths to be used as alternates") + + def is_dirty(self, index=True, working_tree=True, untracked_files=False): + """ + Returns + ``True``, the repository is considered dirty. By default it will react + like a git-status without untracked files, hence it is dirty if the + index or the working copy have changes. + """ + if self._bare: + # Bare repositories with no associated working directory are + # always consired to be clean. + return False + + # start from the one which is fastest to evaluate + default_args = ('--abbrev=40', '--full-index', '--raw') + if index: + # diff index against HEAD + if os.path.isfile(self.index.path) and self.head.is_valid() and \ + len(self.git.diff('HEAD', '--cached', *default_args)): + return True + # END index handling + if working_tree: + # diff index against working tree + if len(self.git.diff(*default_args)): + return True + # END working tree handling + if untracked_files: + if len(self.untracked_files): + return True + # END untracked files + return False + + @property + def untracked_files(self): + """ + Returns + 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. + + Returns + 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. + + ``rev`` + revision specifier, see git-rev-parse for viable options. + + Returns + 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 = {} + blames = [] + 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, 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 + + ``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 + + ``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 + + ``kwargs`` + keyword arguments serving as additional options to the git-init command + + Examples:: + + git.Repo.init('/var/git/myrepo.git',bare=True) + + Returns + ``git.Repo`` (the newly created repo) + """ + + if mkdir and path and not os.path.exists(path): + os.makedirs(path, 0755) + + # git command automatically chdir into the directory + git = Git(path) + output = git.init(**kwargs) + return Repo(path) + + def clone(self, path, **kwargs): + """ + Create a clone from this repository. + + ``path`` + is the full path of the new repo (traditionally ends with ./<name>.git). + + ``kwargs`` + keyword arguments to be given to the git-clone command + + Returns + ``git.Repo`` (the newly cloned repo) + """ + # special handling for windows for path at which the clone should be + # created. + # tilde '~' will be expanded to the HOME no matter where the ~ occours. Hence + # we at least give a proper error instead of letting git fail + prev_cwd = None + prev_path = None + if os.name == 'nt': + if '~' in path: + raise OSError("Git cannot handle the ~ character in path %r correctly" % path) + + # on windows, git will think paths like c: are relative and prepend the + # current working dir ( before it fails ). We temporarily adjust the working + # dir to make this actually work + match = re.match("(\w:[/\\\])(.*)", path) + if match: + prev_cwd = os.getcwd() + prev_path = path + drive, rest_of_path = match.groups() + os.chdir(drive) + path = rest_of_path + kwargs['with_keep_cwd'] = True + # END cwd preparation + # END windows handling + + try: + self.git.clone(self.git_dir, path, **kwargs) + finally: + if prev_cwd is not None: + os.chdir(prev_cwd) + path = prev_path + # END reset previous working dir + # END bad windows handling + return Repo(path) + + + def archive(self, ostream, treeish=None, prefix=None, **kwargs): + """ + Archive the tree at the given revision. + ``ostream`` + file compatible stream object to which the archive will be written + + ``treeish`` + is the treeish name/id, defaults to active branch + + ``prefix`` + is the optional prefix to prepend to each filename in the archive + + ``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 + + Examples:: + + >>> repo.archive(open("archive")) + <String containing tar.gz archive> + + Raise + GitCommandError in case something went wrong + + Returns + self + """ + if treeish is None: + treeish = self.active_branch + if prefix and 'prefix' not in kwargs: + kwargs['prefix'] = prefix + kwargs['output_stream'] = ostream + + self.git.archive(treeish, **kwargs) + return self + + def __repr__(self): + return '<git.Repo "%s">' % self.git_dir |