diff options
Diffstat (limited to 'git/repo/base.py')
-rw-r--r-- | git/repo/base.py | 263 |
1 files changed, 131 insertions, 132 deletions
diff --git a/git/repo/base.py b/git/repo/base.py index 6a311f0b..a5bbaa64 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -7,9 +7,9 @@ from git.exc import InvalidGitRepositoryError, NoSuchPathError from git.cmd import Git from git.util import ( - Actor, - finalize_process - ) + Actor, + finalize_process + ) from git.refs import * from git.index import IndexFile from git.objects import * @@ -21,7 +21,7 @@ from git.remote import ( ) from git.db import ( - GitCmdObjectDB, + GitCmdObjectDB, GitDB ) @@ -52,34 +52,35 @@ __all__ = ('Repo', ) class Repo(object): - """Represents a git repository and allows you to query references, + + """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, which is the working tree + + 'working_dir' is the working directory of the git command, which 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 directory, which is always set.""" DAEMON_EXPORT_FILE = 'git-daemon-export-ok' - __slots__ = ( "working_dir", "_working_tree_dir", "git_dir", "_bare", "git", "odb" ) - + __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_hexsha_shortened = re.compile('^[0-9A-Fa-f]{4,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 = DefaultDBType): + def __init__(self, path=None, odbt=DefaultDBType): """Create a new Repo instance :param path: is the path to either the root git directory or the bare git repo:: @@ -88,8 +89,8 @@ class Repo(object): 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 + + :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: @@ -104,7 +105,7 @@ class Repo(object): 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): @@ -120,26 +121,26 @@ class Repo(object): 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') + 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 + # 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 = [join(self.git_dir, 'objects')] if issubclass(odbt, GitCmdObjectDB): @@ -150,10 +151,10 @@ class Repo(object): 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) @@ -167,23 +168,21 @@ class Repo(object): def _set_description(self, descr): filename = join(self.git_dir, 'description') - file(filename, 'w').write(descr+'\n') + 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): """:return: The working tree directory of our git repository :raise 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 ) + 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): """:return: True if the repository is bare""" @@ -196,20 +195,20 @@ class Repo(object): :return: ``git.IterableList(Head, ...)``""" return Head.list_items(self) - + @property def references(self): """A list of Reference objects representing tags, heads and remote references. - + :return: IterableList(Reference, ...)""" return Reference.list_items(self) - + # alias for references refs = references # alias for heads branches = heads - + @property def index(self): """:return: IndexFile representing this repository's index.""" @@ -218,57 +217,57 @@ class Repo(object): @property def head(self): """:return: HEAD Object pointing to the current head reference""" - return HEAD(self,'HEAD') - + return HEAD(self, 'HEAD') + @property def remotes(self): """A list of Remote objects allowing to access and manipulate remotes :return: ``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) - + #{ Submodules - + @property def submodules(self): """ :return: git.IterableList(Submodule, ...) of direct submodules available from the current head""" return Submodule.list_items(self) - + def submodule(self, name): - """ :return: Submodule with the given name + """ :return: Submodule with the given name :raise ValueError: If no such submodule exists""" try: return self.submodules[name] except IndexError: raise ValueError("Didn't find submodule named %r" % name) # END exception handling - + def create_submodule(self, *args, **kwargs): """Create a new submodule - - :note: See the documentation of Submodule.add for a description of the + + :note: See the documentation of Submodule.add for a description of the applicable parameters :return: created submodules""" return Submodule.add(self, *args, **kwargs) - + def iter_submodules(self, *args, **kwargs): """An iterator yielding Submodule instances, see Traversable interface for a description of args and kwargs :return: Iterator""" return RootModule(self).traverse(*args, **kwargs) - + def submodule_update(self, *args, **kwargs): - """Update the submodules, keeping the repository consistent as it will + """Update the submodules, keeping the repository consistent as it will take the previous state into consideration. For more information, please see the documentation of RootModule.update""" return RootModule(self).update(*args, **kwargs) - + #}END submodules @property @@ -276,101 +275,101 @@ class Repo(object): """A list of ``Tag`` objects that are available in this repo :return: ``git.IterableList(TagReference, ...)`` """ return TagReference.list_items(self) - - def tag(self,path): + + def tag(self, path): """:return: TagReference Object, reference pointing to a Commit or Tag :param 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, logmsg=None ): - """Create a new head within the repository. + + def create_head(self, path, commit='HEAD', force=False, logmsg=None): + """Create a new head within the repository. For more documentation, please see the Head.create method. - + :return: newly created Head Reference""" return Head.create(self, path, commit, force, logmsg) - + def delete_head(self, *heads, **kwargs): """Delete the given heads - + :param 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. - + :return: 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 - + + For more information, please see the documentation of the Remote.create + methods + :return: 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 , + + 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.normpath(os.path.expanduser("~/.gitconfig")) elif config_level == "repository": return join(self.git_dir, "config") - - raise ValueError( "Invalid configuration level: %r" % config_level ) - + + raise ValueError("Invalid configuration level: %r" % config_level) + def config_reader(self, config_level=None): """ :return: GitConfigParser allowing to read the full git configuration, but not to write it - - The configuration will include values from the system, user and repository + + The configuration will include values from the system, user and repository configuration files. - + :param 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 + 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 - :note: On windows, system configuration cannot currently be read as the path is + :note: On windows, system configuration cannot currently be read as the path is unknown, instead the global path will be used.""" files = None if config_level is None: - files = [ self._get_config_path(f) for f in self.config_level ] + files = [self._get_config_path(f) for f in self.config_level] else: - files = [ self._get_config_path(config_level) ] + files = [self._get_config_path(config_level)] return GitConfigParser(files, read_only=True) - + def config_writer(self, config_level="repository"): """ :return: GitConfigParser allowing to write values of the specified configuration file level. - Config writers should be retrieved, used to change the configuration ,and written + 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. - + :param 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) - + return GitConfigParser(self._get_config_path(config_level), read_only=False) + def commit(self, rev=None): """The Commit object for the specified revision :param rev: revision specifier, see git-rev-parse for viable options. @@ -378,30 +377,30 @@ class Repo(object): if rev is None: return self.head.commit else: - return self.rev_parse(unicode(rev)+"^0") - + return self.rev_parse(unicode(rev) + "^0") + def iter_trees(self, *args, **kwargs): """:return: Iterator yielding Tree objects :note: Takes all arguments known to iter_commits method""" - return ( c.tree for c in self.iter_commits(*args, **kwargs) ) + return (c.tree for c in self.iter_commits(*args, **kwargs)) def tree(self, rev=None): """The Tree object for the given treeish revision Examples:: - + repo.tree(repo.heads[0]) :param rev: is a revision pointing to a Treeish ( being a commit or tree ) :return: ``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 + it cannot know about its path relative to the repository root and subsequent operations might have unexpected results.""" if rev is None: return self.head.commit.tree else: - return self.rev_parse(unicode(rev)+"^{tree}") + return self.rev_parse(unicode(rev) + "^{tree}") def iter_commits(self, rev=None, paths='', **kwargs): """A list of Commit objects representing the history of a given ref/commit @@ -413,18 +412,18 @@ class Repo(object): :parm 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. - + :parm kwargs: - Arguments to be passed to git-rev-list - common ones are + 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 + :note: to receive only commits between two named revisions, use the "revA..revB" revision specifier :return ``git.Commit[]``""" if rev is None: rev = self.head.commit - + return Commit.iter_items(self, rev, paths, **kwargs) def _get_daemon_export(self): @@ -464,14 +463,14 @@ class Repo(object): """Sets the alternates :parm alts: - is the array of string paths representing the alternates at which + is the array of string paths representing the alternates at which git should look for objects, i.e. /home/user/repo/.git/objects :raise NoSuchPathError: :note: The method does not check for the existance of the paths in alts as the caller is responsible.""" - alternates_path = join(self.git_dir, 'objects', 'info', 'alternates') + alternates_path = join(self.git_dir, 'objects', 'info', 'alternates') if not alts: if isfile(alternates_path): os.remove(alternates_path) @@ -481,7 +480,7 @@ class Repo(object): f.write("\n".join(alts)) finally: f.close() - # END file handling + # 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") @@ -490,16 +489,16 @@ class Repo(object): """ :return: ``True``, the repository is considered dirty. By default it will react - like a git-status without untracked files, hence it is dirty if the + 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: + if index: # diff index against HEAD if isfile(self.index.path) and \ len(self.git.diff('--cached', *default_args)): @@ -551,14 +550,14 @@ class Repo(object): :return: 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. :parm rev: revision specifier, see git-rev-parse for viable options. :return: list: [git.Commit, list: [<line>]] - A list of tuples associating a Commit object with a list of lines that + 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) @@ -570,7 +569,7 @@ class Repo(object): parts = self.re_whitespace.split(line, 1) firstpart = parts[0] if self.re_hexsha_only.search(firstpart): - # handles + # handles # 634396b2f541a9f2d58b00be1a07f0c358b999b3 1 1 7 - indicates blame-data start # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates another line of blame with the same data digits = parts[-1].split(" ") @@ -584,7 +583,7 @@ class Repo(object): else: m = self.re_author_committer_start.search(firstpart) if m: - # handles: + # handles: # author Tom Preston-Werner # author-mail <tom@mojombo.com> # author-time 1192271832 @@ -615,7 +614,7 @@ class Repo(object): sha = info['id'] c = commits.get(sha) if c is None: - c = Commit( self, hex_to_bin(sha), + c = Commit(self, hex_to_bin(sha), author=Actor._from_string(info['author'] + ' ' + info['author_email']), authored_date=info['author_date'], committer=Actor._from_string(info['committer'] + ' ' + info['committer_email']), @@ -624,9 +623,9 @@ class Repo(object): commits[sha] = c # END if commit objects needs initial creation m = self.re_tab_full_line.search(line) - text, = m.groups() + text, = m.groups() blames[-1][0] = c - blames[-1][1].append( text ) + blames[-1][1].append(text) info = {'id': sha} # END if we collected commit info # END distinguish filename,summary,rest @@ -640,12 +639,12 @@ class Repo(object): :param 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 + or None in which case the repository will be created in the current working directory :parm mkdir: if specified will create the repository directory if it doesn't - already exists. Creates the directory with a mode=0755. + already exists. Creates the directory with a mode=0755. Only effective if a path is explicitly given :parm kwargs: @@ -663,7 +662,7 @@ class Repo(object): @classmethod def _clone(cls, git, url, path, odb_default_type, progress, **kwargs): - # special handling for windows for path at which the clone should be + # 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 @@ -673,9 +672,9 @@ class Repo(object): 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 + + # 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: @@ -685,9 +684,9 @@ class Repo(object): os.chdir(drive) path = rest_of_path kwargs['with_keep_cwd'] = True - # END cwd preparation - # END windows handling - + # END cwd preparation + # END windows handling + try: proc = git.clone(url, path, with_extended_output=True, as_process=True, v=True, **add_progress(kwargs, git, progress)) if progress: @@ -700,18 +699,18 @@ class Repo(object): path = prev_path # END reset previous working dir # END bad windows handling - - # our git command could have a different working dir than our actual + + # our git command could have a different working dir than our actual # environment, hence we prepend its working dir if required if not os.path.isabs(path) and git.working_dir: path = join(git._working_dir, path) - - # adjust remotes - there may be operating systems which use backslashes, + + # adjust remotes - there may be operating systems which use backslashes, # These might be given as initial paths, but when handling the config file # that contains the remote from which we were clones, git stops liking it - # as it will escape the backslashes. Hence we undo the escaping just to be + # as it will escape the backslashes. Hence we undo the escaping just to be # sure - repo = cls(os.path.abspath(path), odbt = odbt) + repo = cls(os.path.abspath(path), odbt=odbt) if repo.remotes: repo.remotes[0].config_writer.set_value('url', repo.remotes[0].url.replace("\\\\", "\\").replace("\\", "/")) # END handle remote repo @@ -727,9 +726,9 @@ class Repo(object): :param kwargs: odbt = ObjectDatabase Type, allowing to determine the object database implementation used by the returned Repo instance - + All remaining keyword arguments are given to the git-clone command - + :return: ``git.Repo`` (the newly cloned repo)""" return self._clone(self.git, self.git_dir, path, type(self.odb), progress, **kwargs) @@ -743,14 +742,14 @@ class Repo(object): :return: Repo instance pointing to the cloned directory""" return cls._clone(Git(os.getcwd()), url, to_path, GitCmdObjectDB, progress, **kwargs) - def archive(self, ostream, treeish=None, prefix=None, **kwargs): + def archive(self, ostream, treeish=None, prefix=None, **kwargs): """Archive the tree at the given revision. :parm ostream: file compatible stream object to which the archive will be written :parm treeish: is the treeish name/id, defaults to active branch :parm prefix: is the optional prefix to prepend to each filename in the archive :parm kwargs: Additional arguments passed to git-archive - NOTE: Use the 'format' argument to define the kind of format. Use + NOTE: Use the 'format' argument to define the kind of format. Use specialized ostreams to write any format supported by python :raise GitCommandError: in case something went wrong @@ -758,13 +757,13 @@ class Repo(object): if treeish is None: treeish = self.head.commit if prefix and 'prefix' not in kwargs: - kwargs['prefix'] = prefix + kwargs['prefix'] = prefix kwargs['output_stream'] = ostream - + self.git.archive(treeish, **kwargs) return self - + rev_parse = rev_parse - + def __repr__(self): return '<git.Repo "%s">' % self.git_dir |