# repo.py # Copyright (C) 2008 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 import os import re from errors import InvalidGitRepositoryError, NoSuchPathError from utils import touch, is_git_dir from cmd import Git from head import Head from blob import Blob from tag import Tag from commit import Commit from tree import Tree class Repo(object): DAEMON_EXPORT_FILE = 'git-daemon-export-ok' def __init__(self, path=None): """ Create a new Repo instance ``path`` is the path to either the root git directory or the bare git repo Examples:: repo = Repo("/Users/mtrier/Development/git-python") repo = Repo("/Users/mtrier/Development/git-python.git") Returns ``GitPython.Repo`` """ epath = os.path.abspath(os.path.expanduser(path or os.getcwd())) if not os.path.exists(epath): raise NoSuchPathError(epath) self.path = None curpath = epath while curpath: if is_git_dir(curpath): self.bare = True self.path = curpath self.wd = curpath break gitpath = os.path.join(curpath, '.git') if is_git_dir(gitpath): self.bare = False self.path = gitpath self.wd = curpath break curpath, dummy = os.path.split(curpath) if not dummy: break if self.path is None: raise InvalidGitRepositoryError(epath) self.git = Git(self.wd) @property def description(self): """ The project's description. Taken verbatim from GIT_REPO/description Returns str """ try: f = open(os.path.join(self.path, 'description')) result = f.read() return result.rstrip() finally: f.close() @property def heads(self): """ A list of ``Head`` objects representing the branch heads in this repo Returns ``GitPython.Head[]`` """ return Head.find_all(self) # alias heads branches = heads @property def tags(self): """ A list of ``Tag`` objects that are available in this repo Returns ``GitPython.Tag[]`` """ return Tag.find_all(self) def commits(self, start = 'master', max_count = 10, skip = 0): """ A list of Commit objects representing the history of a given ref/commit ``start`` is the branch/commit name (default 'master') ``max_count`` is the maximum number of commits to return (default 10) ``skip`` is the number of commits to skip (default 0) Returns ``GitPython.Commit[]`` """ options = {'max_count': max_count, 'skip': skip} return Commit.find_all(self, start, **options) def commits_between(self, frm, to): """ The Commits objects that are reachable via ``to`` but not via ``frm`` Commits are returned in chronological order. ``from`` is the branch/commit name of the younger item ``to`` is the branch/commit name of the older item Returns ``GitPython.Commit[]`` """ return Commit.find_all(self, "%s..%s" % (frm, to)).reverse() def commits_since(self, start = 'master', since = '1970-01-01'): """ The Commits objects that are newer than the specified date. Commits are returned in chronological order. ``start`` is the branch/commit name (default 'master') ``since`` is a string represeting a date/time Returns ``GitPython.Commit[]`` """ options = {'since': since} return Commit.find_all(self, start, **options) def commit_count(self, start = 'master'): """ The number of commits reachable by the given branch/commit ``start`` is the branch/commit name (default 'master') Returns int """ return Commit.count(self, start) def commit(self, id): """ The Commit object for the specified id ``id`` is the SHA1 identifier of the commit Returns GitPython.Commit """ options = {'max_count': 1} commits = Commit.find_all(self, id, **options) if not commits: raise ValueError, 'Invalid identifier %s' % id return commits[0] def commit_deltas_from(self, other_repo, ref = 'master', other_ref = 'master'): """ Returns a list of commits that is in ``other_repo`` but not in self Returns ``GitPython.Commit[]`` """ repo_refs = self.git.rev_list(ref).strip().splitlines() other_repo_refs = other_repo.git.rev_list(other_ref).strip().splitlines() diff_refs = list(set(other_repo_refs) - set(repo_refs)) return map(lambda ref: Commit.find_all(other_repo, ref, max_count=1)[0], diff_refs) def tree(self, treeish = 'master', paths = []): """ The Tree object for the given treeish reference ``treeish`` is the reference (default 'master') ``paths`` is an optional Array of directory paths to restrict the tree (default []) Examples:: repo.tree('master', ['lib/']) Returns ``GitPython.Tree`` """ return Tree.construct(self, treeish, paths) def blob(self, id): """ The Blob object for the given id ``id`` is the SHA1 id of the blob Returns ``GitPython.Blob`` """ return Blob(self, id=id) def log(self, commit = 'master', path = None, **kwargs): """ The commit log for a treeish Returns ``GitPython.Commit[]`` """ options = {'pretty': 'raw'} options.update(kwargs) if path: arg = [commit, '--', path] else: arg = [commit] commits = self.git.log(*arg, **options) return Commit.list_from_string(self, commits) def diff(self, a, b, *paths): """ The diff from commit ``a`` to commit ``b``, optionally restricted to the given file(s) ``a`` is the base commit ``b`` is the other commit ``paths`` is an optional list of file paths on which to restrict the diff """ return self.git.diff(a, b, '--', *paths) def commit_diff(self, commit): """ The commit diff for the given commit ``commit`` is the commit name/id Returns ``GitPython.Diff[]`` """ return Commit.diff(self, commit) @classmethod def init_bare(self, path, mkdir=True, **kwargs): """ Initialize a bare git repository at the given path ``path`` is the full path to the repo (traditionally ends with /.git) ``mkdir`` if specified will create the repository directory if it doesn't already exists. Creates the directory with a mode=0755. ``kwargs`` is any additional options to the git init command Examples:: GitPython.Repo.init_bare('/var/git/myrepo.git') Returns ``GitPython.Repo`` (the newly created repo) """ if mkdir and not os.path.exists(path): os.makedirs(path, 0755) git = Git(path) output = git.init('--bare', **kwargs) return Repo(path) create = init_bare def fork_bare(self, path, **kwargs): """ Fork a bare git repository from this repo ``path`` is the full path of the new repo (traditionally ends with /.git) ``options`` is any additional options to the git clone command Returns ``GitPython.Repo`` (the newly forked repo) """ options = {'bare': True} options.update(kwargs) self.git.clone(self.path, path, **options) return Repo(path) def archive_tar(self, treeish = 'master', prefix = None): """ Archive the given treeish ``treeish`` is the treeish name/id (default 'master') ``prefix`` is the optional prefix Examples:: >>> repo.archive_tar >>> repo.archive_tar('a87ff14') >>> repo.archive_tar('master', 'myproject/') Returns str (containing tar archive) """ options = {} if prefix: options['prefix'] = prefix return self.git.archive(treeish, **options) def archive_tar_gz(self, treeish = 'master', prefix = None): """ Archive and gzip the given treeish ``treeish`` is the treeish name/id (default 'master') ``prefix`` is the optional prefix Examples:: >>> repo.archive_tar_gz >>> repo.archive_tar_gz('a87ff14') >>> repo.archive_tar_gz('master', 'myproject/') Returns str (containing tar.gz archive) """ kwargs = {} if prefix: kwargs['prefix'] = prefix self.git.archive(treeish, "| gzip", **kwargs) def enable_daemon_serve(self): """ Enable git-daemon serving of this repository by writing the git-daemon-export-ok file to its git directory Returns None """ touch(os.path.join(self.path, DAEMON_EXPORT_FILE)) def disable_daemon_serve(self): """ Disable git-daemon serving of this repository by ensuring there is no git-daemon-export-ok file in its git directory Returns None """ return os.remove(os.path.join(self.path, DAEMON_EXPORT_FILE)) def _get_alternates(self): """ The list of alternates for this repo Returns list[str] (pathnames of alternates) """ alternates_path = os.path.join(self.path, '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 Returns None """ for alt in alts: if not os.path.exists(alt): raise NoSuchPathError("Could not set alternates. Alternate path %s must exist" % alt) if not alts: os.remove(os.path.join(self.path, 'objects', 'info', 'alternates')) else: try: f = open(os.path.join(self.path, 'objects', 'info', 'alternates'), 'w') f.write("\n".join(alts)) finally: f.close() alternates = property(_get_alternates, _set_alternates) @property def is_dirty(self): """Returns the status of the working directory. Returns ``True``, if the working directory has any uncommitted changes, otherwise ``False`` """ if self.bare: # Bare repositories with no associated working directory are # always consired to be clean. return False return len(self.git.diff('HEAD').strip()) > 0 def __repr__(self): return '' % self.path