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. """ 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") Raises InvalidGitRepositoryError or NoSuchPathError Returns ``git.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) # Description property def _get_description(self): filename = os.path.join(self.path, 'description') return file(filename).read().rstrip() def _set_description(self, descr): filename = os.path.join(self.path, '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 heads(self): """ A list of ``Head`` objects representing the branch heads in this repo Returns ``git.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 ``git.Tag[]`` """ return Tag.find_all(self) def commits(self, start='master', path='', 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') ``path`` is an optional path to limit the returned commits to Commits that do not contain that path will not be returned. ``max_count`` is the maximum number of commits to return (default 10) ``skip`` is the number of commits to skip (default 0) which will effectively move your commit-window by the given number. Returns ``git.Commit[]`` """ options = {'max_count': max_count, 'skip': skip} return Commit.find_all(self, start, path, **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 ``git.Commit[]`` """ return reversed(Commit.find_all(self, "%s..%s" % (frm, to))) def commits_since(self, start='master', path='', 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') ``path`` is an optional path to limit the returned commits to. ``since`` is a string representing a date/time Returns ``git.Commit[]`` """ options = {'since': since} return Commit.find_all(self, start, path, **options) def commit_count(self, start='master', path=''): """ The number of commits reachable by the given branch/commit ``start`` is the branch/commit name (default 'master') ``path`` is an optional path Commits that do not contain the path will not contribute to the count. Returns ``int`` """ return Commit.count(self, start, path) def commit(self, id, path = ''): """ The Commit object for the specified id ``id`` is the SHA1 identifier of the commit ``path`` is an optional path, if set the returned commit must contain the path. Returns ``git.Commit`` """ options = {'max_count': 1} commits = Commit.find_all(self, id, path, **options) if not commits: raise ValueError, "Invalid identifier %s, or given path '%s' too restrictive" % ( id, path ) 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 git.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'): """ The Tree object for the given treeish reference ``treeish`` is the reference (default 'master') Examples:: repo.tree('master') Returns ``git.Tree`` """ return Tree(self, id=treeish) def blob(self, id): """ The Blob object for the given id ``id`` is the SHA1 id of the blob Returns ``git.Blob`` """ return Blob(self, id=id) def log(self, commit='master', path=None, **kwargs): """ The Commit for a treeish, and all commits leading to it. ``kwargs`` keyword arguments specifying flags to be used in git-log command, i.e.: max_count=1 to limit the amount of commits returned Returns ``git.Commit[]`` """ options = {'pretty': 'raw'} options.update(kwargs) arg = [commit, '--'] if path: arg.append(path) 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 Returns ``str`` """ 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 ``git.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 /<name>.git) ``mkdir`` if specified will create the repository directory if it doesn't already exists. Creates the directory with a mode=0755. ``kwargs`` keyword arguments serving as additional options to the git init command Examples:: git.Repo.init_bare('/var/git/myrepo.git') Returns ``git.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 /<name>.git) ``kwargs`` keyword arguments to be given to the git clone command Returns ``git.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 to prepend to each filename in the archive Examples:: >>> repo.archive_tar <String containing tar archive> >>> repo.archive_tar('a87ff14') <String containing tar archive for commit a87ff14> >>> repo.archive_tar('master', 'myproject/') <String containing tar bytes archive, whose files are prefixed with 'myproject/'> Returns str (containing bytes of 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 to prepend to each filename in the archive Examples:: >>> repo.archive_tar_gz <String containing tar.gz archive> >>> repo.archive_tar_gz('a87ff14') <String containing tar.gz archive for commit a87ff14> >>> repo.archive_tar_gz('master', 'myproject/') <String containing tar.gz archive and prefixed with 'myproject/'> Returns str (containing the bytes of tar.gz archive) """ kwargs = {} if prefix: kwargs['prefix'] = prefix resultstr = self.git.archive(treeish, **kwargs) sio = StringIO.StringIO() gf = gzip.GzipFile(fileobj=sio, mode ='wb') gf.write(resultstr) gf.close() return sio.getvalue() def _get_daemon_export(self): filename = os.path.join(self.path, self.DAEMON_EXPORT_FILE) return os.path.exists(filename) def _set_daemon_export(self, value): filename = os.path.join(self.path, 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.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 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.path, '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") @property def is_dirty(self): """ Return the status of the index. Returns ``True``, if the index has any uncommitted changes, otherwise ``False`` NOTE Working tree changes that have not been staged will not be detected ! """ if self.bare: # Bare repositories with no associated working directory are # always considered to be clean. return False return len(self.git.diff('HEAD', '--').strip()) > 0 @property def active_branch(self): """ The name of the currently active branch. Returns str (the branch name) """ branch = self.git.symbolic_ref('HEAD').strip() if branch.startswith('refs/heads/'): branch = branch[len('refs/heads/'):] return branch def __repr__(self): return '<git.Repo "%s">' % self.path
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 ``git.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) # Description property def _get_description(self): filename = os.path.join(self.path, 'description') return file(filename).read().rstrip() def _set_description(self, descr): filename = os.path.join(self.path, '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 heads(self): """ A list of ``Head`` objects representing the branch heads in this repo Returns ``git.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 ``git.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 ``git.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 ``git.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 ``git.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 git.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 ``git.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'): """ The Tree object for the given treeish reference ``treeish`` is the reference (default 'master') Examples:: repo.tree('master') Returns ``git.Tree`` """ return Tree(self, id=treeish) def blob(self, id): """ The Blob object for the given id ``id`` is the SHA1 id of the blob Returns ``git.Blob`` """ return Blob(self, id=id) def log(self, commit = 'master', path = None, **kwargs): """ The commit log for a treeish Returns ``git.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 ``git.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 /<name>.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:: git.Repo.init_bare('/var/git/myrepo.git') Returns ``git.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 /<name>.git) ``options`` is any additional options to the git clone command Returns ``git.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 <String containing tar archive> >>> repo.archive_tar('a87ff14') <String containing tar archive for commit a87ff14> >>> repo.archive_tar('master', 'myproject/') <String containing tar archive and prefixed with '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 <String containing tar.gz archive> >>> repo.archive_tar_gz('a87ff14') <String containing tar.gz archive for commit a87ff14> >>> repo.archive_tar_gz('master', 'myproject/') <String containing tar.gz archive and prefixed with 'myproject/'> Returns str (containing tar.gz archive) """ kwargs = {} if prefix: kwargs['prefix'] = prefix self.git.archive(treeish, "| gzip", **kwargs) def _get_daemon_export(self): filename = os.path.join(self.path, self.DAEMON_EXPORT_FILE) return os.path.exists(filename) def _set_daemon_export(self, value): filename = os.path.join(self.path, 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="git-daemon export of this repository") del _get_daemon_export del _set_daemon_export 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): """ Return 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 @property def active_branch(self): """ The name of the currently active branch. Returns str (the branch name) """ branch = self.git.symbolic_ref('HEAD').strip() if branch.startswith('refs/heads/'): branch = branch[len('refs/heads/'):] return branch def __repr__(self): return '<git.Repo "%s">' % self.path
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 ``git.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) # Description property def _get_description(self): filename = os.path.join(self.path, 'description') return file(filename).read().rstrip() def _set_description(self, descr): filename = os.path.join(self.path, '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 heads(self): """ A list of ``Head`` objects representing the branch heads in this repo Returns ``git.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 ``git.Tag[]`` """ return Tag.find_all(self) def commits(self, start='master', path='', 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') ``path`` is an optional path ``max_count`` is the maximum number of commits to return (default 10) ``skip`` is the number of commits to skip (default 0) Returns ``git.Commit[]`` """ options = {'max_count': max_count, 'skip': skip} return Commit.find_all(self, start, path, **options) def commits_between(self, frm, to, path=''): """ 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 ``path`` is an optional path Returns ``git.Commit[]`` """ return reversed(Commit.find_all(self, "%s..%s" % (frm, to))) def commits_since(self, start='master', path='', 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') ``path`` is an optinal path ``since`` is a string represeting a date/time Returns ``git.Commit[]`` """ options = {'since': since} return Commit.find_all(self, start, path, **options) def commit_count(self, start='master', path=''): """ The number of commits reachable by the given branch/commit ``start`` is the branch/commit name (default 'master') ``path`` is an optinal path Returns int """ return Commit.count(self, start, path) def commit(self, id, path=''): """ The Commit object for the specified id ``id`` is the SHA1 identifier of the commit ``path`` is an optinal path Returns git.Commit """ options = {'max_count': 1} commits = Commit.find_all(self, id, path, **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 ``git.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'): """ The Tree object for the given treeish reference ``treeish`` is the reference (default 'master') Examples:: repo.tree('master') Returns ``git.Tree`` """ return Tree(self, id=treeish) def blob(self, id): """ The Blob object for the given id ``id`` is the SHA1 id of the blob Returns ``git.Blob`` """ return Blob(self, id=id) def log(self, commit='master', path=None, **kwargs): """ The commit log for a treeish Returns ``git.Commit[]`` """ options = {'pretty': 'raw'} options.update(kwargs) arg = [commit, '--'] if path: arg.append(path) 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 ``git.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 /<name>.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:: git.Repo.init_bare('/var/git/myrepo.git') Returns ``git.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 /<name>.git) ``options`` is any additional options to the git clone command Returns ``git.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 <String containing tar archive> >>> repo.archive_tar('a87ff14') <String containing tar archive for commit a87ff14> >>> repo.archive_tar('master', 'myproject/') <String containing tar archive and prefixed with '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 <String containing tar.gz archive> >>> repo.archive_tar_gz('a87ff14') <String containing tar.gz archive for commit a87ff14> >>> repo.archive_tar_gz('master', 'myproject/') <String containing tar.gz archive and prefixed with 'myproject/'> Returns str (containing tar.gz archive) """ kwargs = {} if prefix: kwargs['prefix'] = prefix resultstr = self.git.archive(treeish, **kwargs) sio = StringIO.StringIO() gf = gzip.GzipFile(fileobj=sio, mode='wb') gf.write(resultstr) gf.close() return sio.getvalue() def _get_daemon_export(self): filename = os.path.join(self.path, self.DAEMON_EXPORT_FILE) return os.path.exists(filename) def _set_daemon_export(self, value): filename = os.path.join(self.path, 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="git-daemon export of this repository") del _get_daemon_export del _set_daemon_export 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): """ Return 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 @property def active_branch(self): """ The name of the currently active branch. Returns str (the branch name) """ branch = self.git.symbolic_ref('HEAD').strip() if branch.startswith('refs/heads/'): branch = branch[len('refs/heads/'):] return branch def __repr__(self): return '<git.Repo "%s">' % self.path
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" ) # 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): """ 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") repo = Repo("~/Development/git-python.git") repo = Repo("$REPOSITORIES/Development/git-python.git") Raises InvalidGitRepositoryError or NoSuchPathError Returns ``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) 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 <*****@*****.**> # author-time 1192271832 # author-tz -0700 # committer Tom Preston-Werner # committer-mail <*****@*****.**> # 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