diff options
Diffstat (limited to 'lib/git')
-rw-r--r-- | lib/git/cmd.py | 153 | ||||
-rw-r--r-- | lib/git/config.py | 762 | ||||
-rw-r--r-- | lib/git/db.py | 6 | ||||
-rw-r--r-- | lib/git/diff.py | 650 | ||||
m--------- | lib/git/ext/gitdb | 0 | ||||
-rw-r--r-- | lib/git/index/base.py | 117 | ||||
-rw-r--r-- | lib/git/index/fun.py | 2 | ||||
-rw-r--r-- | lib/git/index/typ.py | 5 | ||||
-rw-r--r-- | lib/git/objects/base.py | 196 | ||||
-rw-r--r-- | lib/git/objects/blob.py | 48 | ||||
-rw-r--r-- | lib/git/objects/commit.py | 196 | ||||
-rw-r--r-- | lib/git/objects/fun.py | 7 | ||||
-rw-r--r-- | lib/git/objects/submodule.py | 1 | ||||
-rw-r--r-- | lib/git/objects/tag.py | 126 | ||||
-rw-r--r-- | lib/git/objects/tree.py | 78 | ||||
-rw-r--r-- | lib/git/objects/utils.py | 62 | ||||
-rw-r--r-- | lib/git/refs.py | 1772 | ||||
-rw-r--r-- | lib/git/remote.py | 1456 | ||||
-rw-r--r-- | lib/git/repo.py | 327 | ||||
-rw-r--r-- | lib/git/utils.py | 95 |
20 files changed, 2768 insertions, 3291 deletions
diff --git a/lib/git/cmd.py b/lib/git/cmd.py index 1bdf38a8..d0f2a19e 100644 --- a/lib/git/cmd.py +++ b/lib/git/cmd.py @@ -5,10 +5,15 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php import os, sys -import subprocess from utils import * from errors import GitCommandError +from subprocess import ( + call, + Popen, + PIPE + ) + # Enables debugging of GitPython's git commands GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False) @@ -16,7 +21,7 @@ execute_kwargs = ('istream', 'with_keep_cwd', 'with_extended_output', 'with_exceptions', 'as_process', 'output_stream' ) - +__all__ = ('Git', ) def dashify(string): return string.replace('_', '-') @@ -43,14 +48,12 @@ class Git(object): max_chunk_size = 1024*64 class AutoInterrupt(object): - """ - Kill/Interrupt the stored process instance once this instance goes out of scope. It is + """Kill/Interrupt the stored process instance once this instance goes out of scope. It is used to prevent processes piling up in case iterators stop reading. Besides all attributes are wired through to the contained process object. The wait method was overridden to perform automatic status code checking - and possibly raise. - """ + and possibly raise.""" __slots__= ("proc", "args") def __init__(self, proc, args ): @@ -74,19 +77,16 @@ class Git(object): # for some reason, providing None for stdout/stderr still prints something. This is why # we simply use the shell and redirect to nul. Its slower than CreateProcess, question # is whether we really want to see all these messages. Its annoying no matter what. - subprocess.call(("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(self.proc.pid)), shell=True) + call(("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(self.proc.pid)), shell=True) # END exception handling def __getattr__(self, attr): return getattr(self.proc, attr) def wait(self): - """ - Wait for the process and return its status code. + """Wait for the process and return its status code. - Raise - GitCommandError if the return status is not 0 - """ + :raise GitCommandError: if the return status is not 0""" status = self.proc.wait() if status != 0: raise GitCommandError(self.args, status, self.proc.stderr.read()) @@ -196,15 +196,13 @@ class Git(object): def __init__(self, working_dir=None): - """ - Initialize this instance with: + """Initialize this instance with: - ``working_dir`` + :param working_dir: Git directory we should work in. If None, we always work in the current directory as returned by os.getcwd(). It is meant to be the working tree directory if available, or the - .git directory in case of bare repositories. - """ + .git directory in case of bare repositories.""" super(Git, self).__init__() self._working_dir = working_dir @@ -213,22 +211,16 @@ class Git(object): self.cat_file_all = None def __getattr__(self, name): - """ - A convenience method as it allows to call the command as if it was + """A convenience method as it allows to call the command as if it was an object. - Returns - Callable object that will execute call _call_process with your arguments. - """ + :return: Callable object that will execute call _call_process with your arguments.""" if name[:1] == '_': raise AttributeError(name) return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) @property def working_dir(self): - """ - Returns - Git directory we are working on - """ + """:return: Git directory we are working on""" return self._working_dir def execute(self, command, @@ -240,30 +232,29 @@ class Git(object): output_stream=None, **subprocess_kwargs ): - """ - Handles executing the command on the shell and consumes and returns + """Handles executing the command on the shell and consumes and returns the returned information (stdout) - ``command`` + :param command: The command argument list to execute. It should be a string, or a sequence of program arguments. The program to execute is the first item in the args sequence or string. - ``istream`` + :param istream: Standard input filehandle passed to subprocess.Popen. - ``with_keep_cwd`` + :param with_keep_cwd: Whether to use the current working directory from os.getcwd(). The cmd otherwise uses its own working_dir that it has been initialized with if possible. - ``with_extended_output`` + :param with_extended_output: Whether to return a (status, stdout, stderr) tuple. - ``with_exceptions`` + :param with_exceptions: Whether to raise an exception when git returns a non-zero status. - ``as_process`` + :param as_process: Whether to return the created process instance directly from which streams can be read on demand. This will render with_extended_output and with_exceptions ineffective - the caller will have @@ -273,7 +264,7 @@ class Git(object): use the command in iterators, you should pass the whole process instance instead of a single stream. - ``output_stream`` + :param output_stream: If set to a file-like object, data produced by the git command will be output to the given stream directly. This feature only has any effect if as_process is False. Processes will @@ -281,27 +272,24 @@ class Git(object): This merely is a workaround as data will be copied from the output pipe to the given output stream directly. - ``**subprocess_kwargs`` + :param **subprocess_kwargs: Keyword arguments to be passed to subprocess.Popen. Please note that some of the valid kwargs are already set by this method, the ones you specify may not be the same ones. - Returns:: - - str(output) # extended_output = False (Default) - tuple(int(status), str(stdout), str(stderr)) # extended_output = True - - if ouput_stream is True, the stdout value will be your output stream: - output_stream # extended_output = False - tuple(int(status), output_stream, str(stderr))# extended_output = True - - Raise - GitCommandError + :return: + * str(output) if extended_output = False (Default) + * tuple(int(status), str(stdout), str(stderr)) if extended_output = True + + if ouput_stream is True, the stdout value will be your output stream: + * output_stream if extended_output = False + * tuple(int(status), output_stream, str(stderr)) if extended_output = True + + :raise GitCommandError: - NOTE + :note: If you add additional keyword arguments to the signature of this method, - you must update the execute_kwargs tuple housed in this module. - """ + you must update the execute_kwargs tuple housed in this module.""" if GIT_PYTHON_TRACE and not GIT_PYTHON_TRACE == 'full': print ' '.join(command) @@ -312,14 +300,14 @@ class Git(object): cwd=self._working_dir # Start the process - proc = subprocess.Popen(command, - cwd=cwd, - stdin=istream, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - close_fds=(os.name=='posix'),# unsupported on linux - **subprocess_kwargs - ) + proc = Popen(command, + cwd=cwd, + stdin=istream, + stderr=PIPE, + stdout=PIPE, + close_fds=(os.name=='posix'),# unsupported on linux + **subprocess_kwargs + ) if as_process: return self.AutoInterrupt(proc, command) @@ -367,10 +355,8 @@ class Git(object): return stdout_value def transform_kwargs(self, **kwargs): - """ - Transforms Python style kwargs into git command line options. - """ - args = [] + """Transforms Python style kwargs into git command line options.""" + args = list() for k, v in kwargs.items(): if len(k) == 1: if v is True: @@ -400,34 +386,30 @@ class Git(object): return outlist def _call_process(self, method, *args, **kwargs): - """ - Run the given git command with the specified arguments and return + """Run the given git command with the specified arguments and return the result as a String - ``method`` + :param method: is the command. Contained "_" characters will be converted to dashes, such as in 'ls_files' to call 'ls-files'. - ``args`` + :param args: is the list of arguments. If None is included, it will be pruned. This allows your commands to call git more conveniently as None is realized as non-existent - ``kwargs`` + :param kwargs: is a dict of keyword arguments. This function accepts the same optional keyword arguments as execute(). - Examples:: + ``Examples``:: git.rev_list('master', max_count=10, header=True) - Returns - Same as execute() - """ - + :return: Same as ``execute``""" # Handle optional arguments prior to calling transform_kwargs # otherwise these'll end up in args, which is bad. - _kwargs = {} + _kwargs = dict() for kwarg in execute_kwargs: try: _kwargs[kwarg] = kwargs.pop(kwarg) @@ -447,16 +429,13 @@ class Git(object): def _parse_object_header(self, header_line): """ - ``header_line`` + :param header_line: <hex_sha> type_string size_as_int - Returns - (hex_sha, type_string, size_as_int) + :return: (hex_sha, type_string, size_as_int) - Raises - ValueError if the header contains indication for an error due to incorrect - input sha - """ + :raise ValueError: if the header contains indication for an error due to + incorrect input sha""" tokens = header_line.split() if len(tokens) != 3: if not tokens: @@ -482,7 +461,7 @@ class Git(object): if cur_val is not None: return cur_val - options = { "istream" : subprocess.PIPE, "as_process" : True } + options = { "istream" : PIPE, "as_process" : True } options.update( kwargs ) cmd = self._call_process( cmd_name, *args, **options ) @@ -501,15 +480,14 @@ class Git(object): :note: The method will only suffer from the costs of command invocation once and reuses the command in subsequent calls. - :return: (hexsha, type_string, size_as_int) """ + :return: (hexsha, type_string, size_as_int)""" cmd = self.__get_persistent_cmd("cat_file_header", "cat_file", batch_check=True) return self.__get_object_header(cmd, ref) def get_object_data(self, ref): """ As get_object_header, but returns object data as well :return: (hexsha, type_string, size_as_int,data_string) - :note: not threadsafe - """ + :note: not threadsafe""" hexsha, typename, size, stream = self.stream_object_data(ref) data = stream.read(size) del(stream) @@ -525,14 +503,11 @@ class Git(object): return (hexsha, typename, size, self.CatFileContentStream(size, cmd.stdout)) def clear_cache(self): - """ - Clear all kinds of internal caches to release resources. + """Clear all kinds of internal caches to release resources. Currently persistent commands will be interrupted. - Returns - self - """ + :return: self""" self.cat_file_all = None self.cat_file_header = None return self diff --git a/lib/git/config.py b/lib/git/config.py index e5fd9902..e4af57f0 100644 --- a/lib/git/config.py +++ b/lib/git/config.py @@ -3,10 +3,8 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -""" -Module containing module parser implementation able to properly read and write -configuration files -""" +"""Module containing module parser implementation able to properly read and write +configuration files""" import re import os @@ -17,404 +15,370 @@ import cStringIO from git.odict import OrderedDict from git.utils import LockFile -class _MetaParserBuilder(type): - """ - Utlity class wrapping base-class methods into decorators that assure read-only properties - """ - def __new__(metacls, name, bases, clsdict): - """ - Equip all base-class methods with a _needs_values decorator, and all non-const methods - with a _set_dirty_and_flush_changes decorator in addition to that. - """ - mutating_methods = clsdict['_mutating_methods_'] - for base in bases: - methods = ( t for t in inspect.getmembers(base, inspect.ismethod) if not t[0].startswith("_") ) - for name, method in methods: - if name in clsdict: - continue - method_with_values = _needs_values(method) - if name in mutating_methods: - method_with_values = _set_dirty_and_flush_changes(method_with_values) - # END mutating methods handling - - clsdict[name] = method_with_values - # END for each base - - new_type = super(_MetaParserBuilder, metacls).__new__(metacls, name, bases, clsdict) - return new_type - - +__all__ = ('GitConfigParser', ) -def _needs_values(func): - """ - Returns method assuring we read values (on demand) before we try to access them - """ - def assure_data_present(self, *args, **kwargs): - self.read() - return func(self, *args, **kwargs) - # END wrapper method - assure_data_present.__name__ = func.__name__ - return assure_data_present - -def _set_dirty_and_flush_changes(non_const_func): - """ - Return method that checks whether given non constant function may be called. - If so, the instance will be set dirty. - Additionally, we flush the changes right to disk - """ - def flush_changes(self, *args, **kwargs): - rval = non_const_func(self, *args, **kwargs) - self.write() - return rval - # END wrapper method - flush_changes.__name__ = non_const_func.__name__ - return flush_changes - - +class MetaParserBuilder(type): + """Utlity class wrapping base-class methods into decorators that assure read-only properties""" + def __new__(metacls, name, bases, clsdict): + """ + Equip all base-class methods with a needs_values decorator, and all non-const methods + with a set_dirty_and_flush_changes decorator in addition to that.""" + mutating_methods = clsdict['_mutating_methods_'] + for base in bases: + methods = ( t for t in inspect.getmembers(base, inspect.ismethod) if not t[0].startswith("_") ) + for name, method in methods: + if name in clsdict: + continue + method_with_values = needs_values(method) + if name in mutating_methods: + method_with_values = set_dirty_and_flush_changes(method_with_values) + # END mutating methods handling + + clsdict[name] = method_with_values + # END for each base + + new_type = super(MetaParserBuilder, metacls).__new__(metacls, name, bases, clsdict) + return new_type + + + +def needs_values(func): + """Returns method assuring we read values (on demand) before we try to access them""" + def assure_data_present(self, *args, **kwargs): + self.read() + return func(self, *args, **kwargs) + # END wrapper method + assure_data_present.__name__ = func.__name__ + return assure_data_present + +def set_dirty_and_flush_changes(non_const_func): + """Return method that checks whether given non constant function may be called. + If so, the instance will be set dirty. + Additionally, we flush the changes right to disk""" + def flush_changes(self, *args, **kwargs): + rval = non_const_func(self, *args, **kwargs) + self.write() + return rval + # END wrapper method + flush_changes.__name__ = non_const_func.__name__ + return flush_changes + + class GitConfigParser(cp.RawConfigParser, object): - """ - Implements specifics required to read git style configuration files. - - This variation behaves much like the git.config command such that the configuration - will be read on demand based on the filepath given during initialization. - - The changes will automatically be written once the instance goes out of scope, but - can be triggered manually as well. - - The configuration file will be locked if you intend to change values preventing other - instances to write concurrently. - - NOTE - The config is case-sensitive even when queried, hence section and option names - must match perfectly. - """ - __metaclass__ = _MetaParserBuilder - - - #{ Configuration - # The lock type determines the type of lock to use in new configuration readers. - # They must be compatible to the LockFile interface. - # A suitable alternative would be the BlockingLockFile - t_lock = LockFile - - #} END configuration - - OPTCRE = re.compile( - r'\s?(?P<option>[^:=\s][^:=]*)' # very permissive, incuding leading whitespace - r'\s*(?P<vi>[:=])\s*' # any number of space/tab, - # followed by separator - # (either : or =), followed - # by any # space/tab - r'(?P<value>.*)$' # everything up to eol - ) - - # list of RawConfigParser methods able to change the instance - _mutating_methods_ = ("add_section", "remove_section", "remove_option", "set") - __slots__ = ("_sections", "_defaults", "_file_or_files", "_read_only","_is_initialized", '_lock') - - def __init__(self, file_or_files, read_only=True): - """ - Initialize a configuration reader to read the given file_or_files and to - possibly allow changes to it by setting read_only False - - ``file_or_files`` - A single file path or file objects or multiple of these - - ``read_only`` - If True, the ConfigParser may only read the data , but not change it. - If False, only a single file path or file object may be given. - """ - super(GitConfigParser, self).__init__() - # initialize base with ordered dictionaries to be sure we write the same - # file back - self._sections = OrderedDict() - self._defaults = OrderedDict() - - self._file_or_files = file_or_files - self._read_only = read_only - self._is_initialized = False - self._lock = None - - if not read_only: - if isinstance(file_or_files, (tuple, list)): - raise ValueError("Write-ConfigParsers can operate on a single file only, multiple files have been passed") - # END single file check - - if not isinstance(file_or_files, basestring): - file_or_files = file_or_files.name - # END get filename from handle/stream - # initialize lock base - we want to write - self._lock = self.t_lock(file_or_files) - - self._lock._obtain_lock() - # END read-only check - - - def __del__(self): - """ - Write pending changes if required and release locks - """ - # checking for the lock here makes sure we do not raise during write() - # in case an invalid parser was created who could not get a lock - if self.read_only or not self._lock._has_lock(): - return - - try: - try: - self.write() - except IOError,e: - print "Exception during destruction of GitConfigParser: %s" % str(e) - finally: - self._lock._release_lock() - - def optionxform(self, optionstr): - """ - Do not transform options in any way when writing - """ - return optionstr - - def _read(self, fp, fpname): - """ - A direct copy of the py2.4 version of the super class's _read method - to assure it uses ordered dicts. Had to change one line to make it work. - - Future versions have this fixed, but in fact its quite embarassing for the - guys not to have done it right in the first place ! - - Removed big comments to make it more compact. - - Made sure it ignores initial whitespace as git uses tabs - """ - cursect = None # None, or a dictionary - optname = None - lineno = 0 - e = None # None, or an exception - while True: - line = fp.readline() - if not line: - break - lineno = lineno + 1 - # comment or blank line? - if line.strip() == '' or line[0] in '#;': - continue - if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR": - # no leading whitespace - continue - else: - # is it a section header? - mo = self.SECTCRE.match(line) - if mo: - sectname = mo.group('header') - if sectname in self._sections: - cursect = self._sections[sectname] - elif sectname == cp.DEFAULTSECT: - cursect = self._defaults - else: - # THE ONLY LINE WE CHANGED ! - cursect = OrderedDict((('__name__', sectname),)) - self._sections[sectname] = cursect - # So sections can't start with a continuation line - optname = None - # no section header in the file? - elif cursect is None: - raise cp.MissingSectionHeaderError(fpname, lineno, line) - # an option line? - else: - mo = self.OPTCRE.match(line) - if mo: - optname, vi, optval = mo.group('option', 'vi', 'value') - if vi in ('=', ':') and ';' in optval: - pos = optval.find(';') - if pos != -1 and optval[pos-1].isspace(): - optval = optval[:pos] - optval = optval.strip() - if optval == '""': - optval = '' - optname = self.optionxform(optname.rstrip()) - cursect[optname] = optval - else: - if not e: - e = cp.ParsingError(fpname) - e.append(lineno, repr(line)) - # END - # END ? - # END ? - # END while reading - # if any parsing errors occurred, raise an exception - if e: - raise e - - - def read(self): - """ - Reads the data stored in the files we have been initialized with. It will - ignore files that cannot be read, possibly leaving an empty configuration - - Returns - Nothing - - Raises - IOError if a file cannot be handled - """ - if self._is_initialized: - return - - - files_to_read = self._file_or_files - if not isinstance(files_to_read, (tuple, list)): - files_to_read = [ files_to_read ] - - for file_object in files_to_read: - fp = file_object - close_fp = False - # assume a path if it is not a file-object - if not hasattr(file_object, "seek"): - try: - fp = open(file_object) - except IOError,e: - continue - close_fp = True - # END fp handling - - try: - self._read(fp, fp.name) - finally: - if close_fp: - fp.close() - # END read-handling - # END for each file object to read - self._is_initialized = True - - def _write(self, fp): - """Write an .ini-format representation of the configuration state in - git compatible format""" - def write_section(name, section_dict): - fp.write("[%s]\n" % name) - for (key, value) in section_dict.items(): - if key != "__name__": - fp.write("\t%s = %s\n" % (key, str(value).replace('\n', '\n\t'))) - # END if key is not __name__ - # END section writing - - if self._defaults: - write_section(cp.DEFAULTSECT, self._defaults) - map(lambda t: write_section(t[0],t[1]), self._sections.items()) + """Implements specifics required to read git style configuration files. + + This variation behaves much like the git.config command such that the configuration + will be read on demand based on the filepath given during initialization. + + The changes will automatically be written once the instance goes out of scope, but + can be triggered manually as well. + + The configuration file will be locked if you intend to change values preventing other + instances to write concurrently. + + :note: + The config is case-sensitive even when queried, hence section and option names + must match perfectly.""" + __metaclass__ = MetaParserBuilder + + + #{ Configuration + # The lock type determines the type of lock to use in new configuration readers. + # They must be compatible to the LockFile interface. + # A suitable alternative would be the BlockingLockFile + t_lock = LockFile + + #} END configuration + + OPTCRE = re.compile( + r'\s?(?P<option>[^:=\s][^:=]*)' # very permissive, incuding leading whitespace + r'\s*(?P<vi>[:=])\s*' # any number of space/tab, + # followed by separator + # (either : or =), followed + # by any # space/tab + r'(?P<value>.*)$' # everything up to eol + ) + + # list of RawConfigParser methods able to change the instance + _mutating_methods_ = ("add_section", "remove_section", "remove_option", "set") + __slots__ = ("_sections", "_defaults", "_file_or_files", "_read_only","_is_initialized", '_lock') + + def __init__(self, file_or_files, read_only=True): + """Initialize a configuration reader to read the given file_or_files and to + possibly allow changes to it by setting read_only False + + :param file_or_files: + A single file path or file objects or multiple of these + + :param read_only: + If True, the ConfigParser may only read the data , but not change it. + If False, only a single file path or file object may be given.""" + super(GitConfigParser, self).__init__() + # initialize base with ordered dictionaries to be sure we write the same + # file back + self._sections = OrderedDict() + self._defaults = OrderedDict() + + self._file_or_files = file_or_files + self._read_only = read_only + self._is_initialized = False + self._lock = None + + if not read_only: + if isinstance(file_or_files, (tuple, list)): + raise ValueError("Write-ConfigParsers can operate on a single file only, multiple files have been passed") + # END single file check + + if not isinstance(file_or_files, basestring): + file_or_files = file_or_files.name + # END get filename from handle/stream + # initialize lock base - we want to write + self._lock = self.t_lock(file_or_files) + + self._lock._obtain_lock() + # END read-only check + + + def __del__(self): + """Write pending changes if required and release locks""" + # checking for the lock here makes sure we do not raise during write() + # in case an invalid parser was created who could not get a lock + if self.read_only or not self._lock._has_lock(): + return + + try: + try: + self.write() + except IOError,e: + print "Exception during destruction of GitConfigParser: %s" % str(e) + finally: + self._lock._release_lock() + + def optionxform(self, optionstr): + """Do not transform options in any way when writing""" + return optionstr + + def _read(self, fp, fpname): + """A direct copy of the py2.4 version of the super class's _read method + to assure it uses ordered dicts. Had to change one line to make it work. + + Future versions have this fixed, but in fact its quite embarassing for the + guys not to have done it right in the first place ! + + Removed big comments to make it more compact. + + Made sure it ignores initial whitespace as git uses tabs""" + cursect = None # None, or a dictionary + optname = None + lineno = 0 + e = None # None, or an exception + while True: + line = fp.readline() + if not line: + break + lineno = lineno + 1 + # comment or blank line? + if line.strip() == '' or line[0] in '#;': + continue + if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR": + # no leading whitespace + continue + else: + # is it a section header? + mo = self.SECTCRE.match(line) + if mo: + sectname = mo.group('header') + if sectname in self._sections: + cursect = self._sections[sectname] + elif sectname == cp.DEFAULTSECT: + cursect = self._defaults + else: + # THE ONLY LINE WE CHANGED ! + cursect = OrderedDict((('__name__', sectname),)) + self._sections[sectname] = cursect + # So sections can't start with a continuation line + optname = None + # no section header in the file? + elif cursect is None: + raise cp.MissingSectionHeaderError(fpname, lineno, line) + # an option line? + else: + mo = self.OPTCRE.match(line) + if mo: + optname, vi, optval = mo.group('option', 'vi', 'value') + if vi in ('=', ':') and ';' in optval: + pos = optval.find(';') + if pos != -1 and optval[pos-1].isspace(): + optval = optval[:pos] + optval = optval.strip() + if optval == '""': + optval = '' + optname = self.optionxform(optname.rstrip()) + cursect[optname] = optval + else: + if not e: + e = cp.ParsingError(fpname) + e.append(lineno, repr(line)) + # END + # END ? + # END ? + # END while reading + # if any parsing errors occurred, raise an exception + if e: + raise e + + + def read(self): + """Reads the data stored in the files we have been initialized with. It will + ignore files that cannot be read, possibly leaving an empty configuration + + :return: Nothing + :raise IOError: if a file cannot be handled""" + if self._is_initialized: + return + + files_to_read = self._file_or_files + if not isinstance(files_to_read, (tuple, list)): + files_to_read = [ files_to_read ] + + for file_object in files_to_read: + fp = file_object + close_fp = False + # assume a path if it is not a file-object + if not hasattr(file_object, "seek"): + try: + fp = open(file_object) + except IOError,e: + continue + close_fp = True + # END fp handling + + try: + self._read(fp, fp.name) + finally: + if close_fp: + fp.close() + # END read-handling + # END for each file object to read + self._is_initialized = True + + def _write(self, fp): + """Write an .ini-format representation of the configuration state in + git compatible format""" + def write_section(name, section_dict): + fp.write("[%s]\n" % name) + for (key, value) in section_dict.items(): + if key != "__name__": + fp.write("\t%s = %s\n" % (key, str(value).replace('\n', '\n\t'))) + # END if key is not __name__ + # END section writing + + if self._defaults: + write_section(cp.DEFAULTSECT, self._defaults) + map(lambda t: write_section(t[0],t[1]), self._sections.items()) - - @_needs_values - def write(self): - """ - Write changes to our file, if there are changes at all - - Raise - IOError if this is a read-only writer instance or if we could not obtain - a file lock - """ - self._assure_writable("write") - self._lock._obtain_lock() - - - fp = self._file_or_files - close_fp = False - - if not hasattr(fp, "seek"): - fp = open(self._file_or_files, "w") - close_fp = True - else: - fp.seek(0) - - # WRITE DATA - try: - self._write(fp) - finally: - if close_fp: - fp.close() - # END data writing - - # we do not release the lock - it will be done automatically once the - # instance vanishes - - def _assure_writable(self, method_name): - if self.read_only: - raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name)) - - @_needs_values - @_set_dirty_and_flush_changes - def add_section(self, section): - """ - Assures added options will stay in order - """ - super(GitConfigParser, self).add_section(section) - self._sections[section] = OrderedDict() - - @property - def read_only(self): - """ - Returns - True if this instance may change the configuration file - """ - return self._read_only - - def get_value(self, section, option, default = None): - """ - ``default`` - If not None, the given default value will be returned in case - the option did not exist - Returns - a properly typed value, either int, float or string - Raises TypeError in case the value could not be understood - Otherwise the exceptions known to the ConfigParser will be raised. - """ - try: - valuestr = self.get(section, option) - except Exception: - if default is not None: - return default - raise - - types = ( long, float ) - for numtype in types: - try: - val = numtype( valuestr ) + + @needs_values + def write(self): + """Write changes to our file, if there are changes at all + + :raise IOError: if this is a read-only writer instance or if we could not obtain + a file lock""" + self._assure_writable("write") + self._lock._obtain_lock() + + + fp = self._file_or_files + close_fp = False + + if not hasattr(fp, "seek"): + fp = open(self._file_or_files, "w") + close_fp = True + else: + fp.seek(0) + + # WRITE DATA + try: + self._write(fp) + finally: + if close_fp: + fp.close() + # END data writing + + # we do not release the lock - it will be done automatically once the + # instance vanishes + + def _assure_writable(self, method_name): + if self.read_only: + raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name)) + + @needs_values + @set_dirty_and_flush_changes + def add_section(self, section): + """Assures added options will stay in order""" + super(GitConfigParser, self).add_section(section) + self._sections[section] = OrderedDict() + + @property + def read_only(self): + """:return: True if this instance may change the configuration file""" + return self._read_only + + def get_value(self, section, option, default = None): + """ + :param default: + If not None, the given default value will be returned in case + the option did not exist + :return: a properly typed value, either int, float or string + + :raise TypeError: in case the value could not be understood + Otherwise the exceptions known to the ConfigParser will be raised.""" + try: + valuestr = self.get(section, option) + except Exception: + if default is not None: + return default + raise + + types = ( long, float ) + for numtype in types: + try: + val = numtype( valuestr ) - # truncated value ? - if val != float( valuestr ): - continue + # truncated value ? + if val != float( valuestr ): + continue - return val - except (ValueError,TypeError): - continue - # END for each numeric type - - # try boolean values as git uses them - vl = valuestr.lower() - if vl == 'false': - return False - if vl == 'true': - return True - - if not isinstance( valuestr, basestring ): - raise TypeError( "Invalid value type: only int, long, float and str are allowed", valuestr ) - - return valuestr - - @_needs_values - @_set_dirty_and_flush_changes - def set_value(self, section, option, value): - """Sets the given option in section to the given value. - It will create the section if required, and will not throw as opposed to the default - ConfigParser 'set' method. - - ``section`` - Name of the section in which the option resides or should reside - - ``option`` - Name of the options whose value to set - - ``value`` - Value to set the option to. It must be a string or convertible to a string - """ - if not self.has_section(section): - self.add_section(section) - self.set(section, option, str(value)) + return val + except (ValueError,TypeError): + continue + # END for each numeric type + + # try boolean values as git uses them + vl = valuestr.lower() + if vl == 'false': + return False + if vl == 'true': + return True + + if not isinstance( valuestr, basestring ): + raise TypeError( "Invalid value type: only int, long, float and str are allowed", valuestr ) + + return valuestr + + @needs_values + @set_dirty_and_flush_changes + def set_value(self, section, option, value): + """Sets the given option in section to the given value. + It will create the section if required, and will not throw as opposed to the default + ConfigParser 'set' method. + + :param section: Name of the section in which the option resides or should reside + :param option: Name of the options whose value to set + + :param value: Value to set the option to. It must be a string or convertible + to a string""" + if not self.has_section(section): + self.add_section(section) + self.set(section, option, str(value)) diff --git a/lib/git/db.py b/lib/git/db.py index b7cf0fc7..c36446d0 100644 --- a/lib/git/db.py +++ b/lib/git/db.py @@ -4,7 +4,7 @@ from gitdb.base import ( OStream ) -from gitdb.util import to_hex_sha +from gitdb.util import bin_to_hex from gitdb.db import GitDB from gitdb.db import LooseObjectDB @@ -27,11 +27,11 @@ class GitCmdObjectDB(LooseObjectDB): self._git = git def info(self, sha): - t = self._git.get_object_header(to_hex_sha(sha)) + t = self._git.get_object_header(bin_to_hex(sha)) return OInfo(*t) def stream(self, sha): """For now, all lookup is done by git itself""" - t = self._git.stream_object_data(to_hex_sha(sha)) + t = self._git.stream_object_data(bin_to_hex(sha)) return OStream(*t) diff --git a/lib/git/diff.py b/lib/git/diff.py index 36b216e3..b8585a4c 100644 --- a/lib/git/diff.py +++ b/lib/git/diff.py @@ -5,365 +5,341 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php import re -import objects.blob as blob +from objects.blob import Blob +from objects.utils import mode_str_to_int from errors import GitCommandError - + +from gitdb.util import hex_to_bin + +__all__ = ('Diffable', 'DiffIndex', 'Diff') + class Diffable(object): - """ - Common interface for all object that can be diffed against another object of compatible type. - - NOTE: - Subclasses require a repo member as it is the case for Object instances, for practical - reasons we do not derive from Object. - """ - __slots__ = tuple() - - # standin indicating you want to diff against the index - class Index(object): - pass - - def _process_diff_args(self, args): - """ - Returns - possibly altered version of the given args list. - Method is called right before git command execution. - Subclasses can use it to alter the behaviour of the superclass - """ - return args - - def diff(self, other=Index, paths=None, create_patch=False, **kwargs): - """ - Creates diffs between two items being trees, trees and index or an - index and the working tree. + """Common interface for all object that can be diffed against another object of compatible type. + + :note: + Subclasses require a repo member as it is the case for Object instances, for practical + reasons we do not derive from Object.""" + __slots__ = tuple() + + # standin indicating you want to diff against the index + class Index(object): + pass + + def _process_diff_args(self, args): + """ + :return: + possibly altered version of the given args list. + Method is called right before git command execution. + Subclasses can use it to alter the behaviour of the superclass""" + return args + + def diff(self, other=Index, paths=None, create_patch=False, **kwargs): + """Creates diffs between two items being trees, trees and index or an + index and the working tree. - ``other`` - Is the item to compare us with. - If None, we will be compared to the working tree. - If Treeish, it will be compared against the respective tree - If Index ( type ), it will be compared against the index. - It defaults to Index to assure the method will not by-default fail - on bare repositories. + :param other: + Is the item to compare us with. + If None, we will be compared to the working tree. + If Treeish, it will be compared against the respective tree + If Index ( type ), it will be compared against the index. + It defaults to Index to assure the method will not by-default fail + on bare repositories. - ``paths`` - is a list of paths or a single path to limit the diff to. - It will only include at least one of the givne path or paths. + :param paths: + is a list of paths or a single path to limit the diff to. + It will only include at least one of the givne path or paths. - ``create_patch`` - If True, the returned Diff contains a detailed patch that if applied - makes the self to other. Patches are somwhat costly as blobs have to be read - and diffed. + :param create_patch: + If True, the returned Diff contains a detailed patch that if applied + makes the self to other. Patches are somwhat costly as blobs have to be read + and diffed. - ``kwargs`` - Additional arguments passed to git-diff, such as - R=True to swap both sides of the diff. + :param kwargs: + Additional arguments passed to git-diff, such as + R=True to swap both sides of the diff. - Returns - git.DiffIndex - - Note - Rename detection will only work if create_patch is True. - - On a bare repository, 'other' needs to be provided as Index or as - as Tree/Commit, or a git command error will occour - """ - args = list() - args.append( "--abbrev=40" ) # we need full shas - args.append( "--full-index" ) # get full index paths, not only filenames - - if create_patch: - args.append("-p") - args.append("-M") # check for renames - else: - args.append("--raw") - - if paths is not None and not isinstance(paths, (tuple,list)): - paths = [ paths ] + :return: git.DiffIndex + + :note: + Rename detection will only work if create_patch is True. + + On a bare repository, 'other' needs to be provided as Index or as + as Tree/Commit, or a git command error will occour""" + args = list() + args.append( "--abbrev=40" ) # we need full shas + args.append( "--full-index" ) # get full index paths, not only filenames + + if create_patch: + args.append("-p") + args.append("-M") # check for renames + else: + args.append("--raw") + + if paths is not None and not isinstance(paths, (tuple,list)): + paths = [ paths ] - if other is not None and other is not self.Index: - args.insert(0, other) - if other is self.Index: - args.insert(0, "--cached") - - args.insert(0,self) - - # paths is list here or None - if paths: - args.append("--") - args.extend(paths) - # END paths handling - - kwargs['as_process'] = True - proc = self.repo.git.diff(*self._process_diff_args(args), **kwargs) - - diff_method = Diff._index_from_raw_format - if create_patch: - diff_method = Diff._index_from_patch_format - index = diff_method(self.repo, proc.stdout) - - status = proc.wait() - return index + if other is not None and other is not self.Index: + args.insert(0, other) + if other is self.Index: + args.insert(0, "--cached") + + args.insert(0,self) + + # paths is list here or None + if paths: + args.append("--") + args.extend(paths) + # END paths handling + + kwargs['as_process'] = True + proc = self.repo.git.diff(*self._process_diff_args(args), **kwargs) + + diff_method = Diff._index_from_raw_format + if create_patch: + diff_method = Diff._index_from_patch_format + index = diff_method(self.repo, proc.stdout) + + status = proc.wait() + return index class DiffIndex(list): - """ - Implements an Index for diffs, allowing a list of Diffs to be queried by - the diff properties. - - The class improves the diff handling convenience - """ - # change type invariant identifying possible ways a blob can have changed - # A = Added - # D = Deleted - # R = Renamed - # M = modified - change_type = ("A", "D", "R", "M") - - - def iter_change_type(self, change_type): - """ - Return - iterator yieling Diff instances that match the given change_type - - ``change_type`` - Member of DiffIndex.change_type, namely - - 'A' for added paths - - 'D' for deleted paths - - 'R' for renamed paths - - 'M' for paths with modified data - """ - if change_type not in self.change_type: - raise ValueError( "Invalid change type: %s" % change_type ) - - for diff in self: - if change_type == "A" and diff.new_file: - yield diff - elif change_type == "D" and diff.deleted_file: - yield diff - elif change_type == "R" and diff.renamed: - yield diff - elif change_type == "M" and diff.a_blob and diff.b_blob and diff.a_blob != diff.b_blob: - yield diff - # END for each diff - + """Implements an Index for diffs, allowing a list of Diffs to be queried by + the diff properties. + + The class improves the diff handling convenience""" + # change type invariant identifying possible ways a blob can have changed + # A = Added + # D = Deleted + # R = Renamed + # M = modified + change_type = ("A", "D", "R", "M") + + + def iter_change_type(self, change_type): + """ + :return: + iterator yieling Diff instances that match the given change_type + + :param change_type: + Member of DiffIndex.change_type, namely: + + * 'A' for added paths + * 'D' for deleted paths + * 'R' for renamed paths + * 'M' for paths with modified data""" + if change_type not in self.change_type: + raise ValueError( "Invalid change type: %s" % change_type ) + + for diff in self: + if change_type == "A" and diff.new_file: + yield diff + elif change_type == "D" and diff.deleted_file: + yield diff + elif change_type == "R" and diff.renamed: + yield diff + elif change_type == "M" and diff.a_blob and diff.b_blob and diff.a_blob != diff.b_blob: + yield diff + # END for each diff + class Diff(object): - """ - A Diff contains diff information between two Trees. - - It contains two sides a and b of the diff, members are prefixed with - "a" and "b" respectively to inidcate that. - - Diffs keep information about the changed blob objects, the file mode, renames, - deletions and new files. - - There are a few cases where None has to be expected as member variable value: - - ``New File``:: - - a_mode is None - a_blob is None - - ``Deleted File``:: - - b_mode is None - b_blob is None - - ``Working Tree Blobs`` - - When comparing to working trees, the working tree blob will have a null hexsha - as a corresponding object does not yet exist. The mode will be null as well. - But the path will be available though. - If it is listed in a diff the working tree version of the file must - be different to the version in the index or tree, and hence has been modified. - """ - - # precompiled regex - re_header = re.compile(r""" - #^diff[ ]--git - [ ]a/(?P<a_path>\S+)[ ]b/(?P<b_path>\S+)\n - (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n - ^rename[ ]from[ ](?P<rename_from>\S+)\n - ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))? - (?:^old[ ]mode[ ](?P<old_mode>\d+)\n - ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? - (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? - (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? - (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+) - \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))? - """, re.VERBOSE | re.MULTILINE) - # can be used for comparisons - NULL_HEX_SHA = "0"*40 - NULL_BIN_SHA = "\0"*20 - - __slots__ = ("a_blob", "b_blob", "a_mode", "b_mode", "new_file", "deleted_file", - "rename_from", "rename_to", "diff") + """A Diff contains diff information between two Trees. + + It contains two sides a and b of the diff, members are prefixed with + "a" and "b" respectively to inidcate that. + + Diffs keep information about the changed blob objects, the file mode, renames, + deletions and new files. + + There are a few cases where None has to be expected as member variable value: + + ``New File``:: + + a_mode is None + a_blob is None + + ``Deleted File``:: + + b_mode is None + b_blob is None + + ``Working Tree Blobs`` + + When comparing to working trees, the working tree blob will have a null hexsha + as a corresponding object does not yet exist. The mode will be null as well. + But the path will be available though. + If it is listed in a diff the working tree version of the file must + be different to the version in the index or tree, and hence has been modified.""" + + # precompiled regex + re_header = re.compile(r""" + #^diff[ ]--git + [ ]a/(?P<a_path>\S+)[ ]b/(?P<b_path>\S+)\n + (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n + ^rename[ ]from[ ](?P<rename_from>\S+)\n + ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))? + (?:^old[ ]mode[ ](?P<old_mode>\d+)\n + ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? + (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? + (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? + (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+) + \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))? + """, re.VERBOSE | re.MULTILINE) + # can be used for comparisons + NULL_HEX_SHA = "0"*40 + NULL_BIN_SHA = "\0"*20 + + __slots__ = ("a_blob", "b_blob", "a_mode", "b_mode", "new_file", "deleted_file", + "rename_from", "rename_to", "diff") - def __init__(self, repo, a_path, b_path, a_blob_id, b_blob_id, a_mode, - b_mode, new_file, deleted_file, rename_from, - rename_to, diff): - if a_blob_id is None: - self.a_blob = None - else: - self.a_blob = blob.Blob(repo, a_blob_id, mode=a_mode, path=a_path) - if b_blob_id is None: - self.b_blob = None - else: - self.b_blob = blob.Blob(repo, b_blob_id, mode=b_mode, path=b_path) + def __init__(self, repo, a_path, b_path, a_blob_id, b_blob_id, a_mode, + b_mode, new_file, deleted_file, rename_from, + rename_to, diff): + if a_blob_id is None: + self.a_blob = None + else: + self.a_blob = Blob(repo, hex_to_bin(a_blob_id), mode=a_mode, path=a_path) + if b_blob_id is None: + self.b_blob = None + else: + self.b_blob = Blob(repo, hex_to_bin(b_blob_id), mode=b_mode, path=b_path) - self.a_mode = a_mode - self.b_mode = b_mode - - if self.a_mode: - self.a_mode = blob.Blob._mode_str_to_int( self.a_mode ) - if self.b_mode: - self.b_mode = blob.Blob._mode_str_to_int( self.b_mode ) - - self.new_file = new_file - self.deleted_file = deleted_file - - # be clear and use None instead of empty strings - self.rename_from = rename_from or None - self.rename_to = rename_to or None - - self.diff = diff + self.a_mode = a_mode + self.b_mode = b_mode + + if self.a_mode: + self.a_mode = mode_str_to_int( self.a_mode ) + if self.b_mode: + self.b_mode = mode_str_to_int( self.b_mode ) + + self.new_file = new_file + self.deleted_file = deleted_file + + # be clear and use None instead of empty strings + self.rename_from = rename_from or None + self.rename_to = rename_to or None + + self.diff = diff - def __eq__(self, other): - for name in self.__slots__: - if getattr(self, name) != getattr(other, name): - return False - # END for each name - return True - - def __ne__(self, other): - return not ( self == other ) - - def __hash__(self): - return hash(tuple(getattr(self,n) for n in self.__slots__)) + def __eq__(self, other): + for name in self.__slots__: + if getattr(self, name) != getattr(other, name): + return False + # END for each name + return True + + def __ne__(self, other): + return not ( self == other ) + + def __hash__(self): + return hash(tuple(getattr(self,n) for n in self.__slots__)) - def __str__(self): - h = "%s" - if self.a_blob: - h %= self.a_blob.path - elif self.b_blob: - h %= self.b_blob.path - - msg = '' - l = None # temp line - ll = 0 # line length - for b,n in zip((self.a_blob, self.b_blob), ('lhs', 'rhs')): - if b: - l = "\n%s: %o | %s" % (n, b.mode, b.sha) - else: - l = "\n%s: None" % n - # END if blob is not None - ll = max(len(l), ll) - msg += l - # END for each blob - - # add headline - h += '\n' + '='*ll - - if self.deleted_file: - msg += '\nfile deleted in rhs' - if self.new_file: - msg += '\nfile added in rhs' - if self.rename_from: - msg += '\nfile renamed from %r' % self.rename_from - if self.rename_to: - msg += '\nfile renamed to %r' % self.rename_to - if self.diff: - msg += '\n---' - msg += self.diff - msg += '\n---' - # END diff info - - return h + msg + def __str__(self): + h = "%s" + if self.a_blob: + h %= self.a_blob.path + elif self.b_blob: + h %= self.b_blob.path + + msg = '' + l = None # temp line + ll = 0 # line length + for b,n in zip((self.a_blob, self.b_blob), ('lhs', 'rhs')): + if b: + l = "\n%s: %o | %s" % (n, b.mode, b.sha) + else: + l = "\n%s: None" % n + # END if blob is not None + ll = max(len(l), ll) + msg += l + # END for each blob + + # add headline + h += '\n' + '='*ll + + if self.deleted_file: + msg += '\nfile deleted in rhs' + if self.new_file: + msg += '\nfile added in rhs' + if self.rename_from: + msg += '\nfile renamed from %r' % self.rename_from + if self.rename_to: + msg += '\nfile renamed to %r' % self.rename_to + if self.diff: + msg += '\n---' + msg += self.diff + msg += '\n---' + # END diff info + + return h + msg - @property - def renamed(self): - """ - Returns: - True if the blob of our diff has been renamed - """ - return self.rename_from != self.rename_to + @property + def renamed(self): + """:returns: True if the blob of our diff has been renamed""" + return self.rename_from != self.rename_to - @classmethod - def _index_from_patch_format(cls, repo, stream): - """ - Create a new DiffIndex from the given text which must be in patch format - ``repo`` - is the repository we are operating on - it is required - - ``stream`` - result of 'git diff' as a stream (supporting file protocol) - - Returns - git.DiffIndex - """ - # for now, we have to bake the stream - text = stream.read() - index = DiffIndex() + @classmethod + def _index_from_patch_format(cls, repo, stream): + """Create a new DiffIndex from the given text which must be in patch format + :param repo: is the repository we are operating on - it is required + :param stream: result of 'git diff' as a stream (supporting file protocol) + :return: git.DiffIndex """ + # for now, we have to bake the stream + text = stream.read() + index = DiffIndex() - diff_header = cls.re_header.match - for diff in ('\n' + text).split('\ndiff --git')[1:]: - header = diff_header(diff) + diff_header = cls.re_header.match + for diff in ('\n' + text).split('\ndiff --git')[1:]: + header = diff_header(diff) - a_path, b_path, similarity_index, rename_from, rename_to, \ - old_mode, new_mode, new_file_mode, deleted_file_mode, \ - a_blob_id, b_blob_id, b_mode = header.groups() - new_file, deleted_file = bool(new_file_mode), bool(deleted_file_mode) + a_path, b_path, similarity_index, rename_from, rename_to, \ + old_mode, new_mode, new_file_mode, deleted_file_mode, \ + a_blob_id, b_blob_id, b_mode = header.groups() + new_file, deleted_file = bool(new_file_mode), bool(deleted_file_mode) - index.append(Diff(repo, a_path, b_path, a_blob_id, b_blob_id, - old_mode or deleted_file_mode, new_mode or new_file_mode or b_mode, - new_file, deleted_file, rename_from, rename_to, diff[header.end():])) + index.append(Diff(repo, a_path, b_path, a_blob_id, b_blob_id, + old_mode or deleted_file_mode, new_mode or new_file_mode or b_mode, + new_file, deleted_file, rename_from, rename_to, diff[header.end():])) - return index - - @classmethod - def _index_from_raw_format(cls, repo, stream): - """ - Create a new DiffIndex from the given stream which must be in raw format. - - NOTE: - This format is inherently incapable of detecting renames, hence we only - modify, delete and add files - - Returns - git.DiffIndex - """ - # handles - # :100644 100644 6870991011cc8d9853a7a8a6f02061512c6a8190 37c5e30c879213e9ae83b21e9d11e55fc20c54b7 M .gitignore - index = DiffIndex() - for line in stream: - if not line.startswith(":"): - continue - # END its not a valid diff line - old_mode, new_mode, a_blob_id, b_blob_id, change_type, path = line[1:].split(None, 5) - path = path.strip() - a_path = path - b_path = path - deleted_file = False - new_file = False - - # NOTE: We cannot conclude from the existance of a blob to change type - # as diffs with the working do not have blobs yet - if change_type == 'D': - b_blob_id = None - deleted_file = True - elif change_type == 'A': - a_blob_id = None - new_file = True - # END add/remove handling - - diff = Diff(repo, a_path, b_path, a_blob_id, b_blob_id, old_mode, new_mode, - new_file, deleted_file, None, None, '') - index.append(diff) - # END for each line - - return index + return index + + @classmethod + def _index_from_raw_format(cls, repo, stream): + """Create a new DiffIndex from the given stream which must be in raw format. + :note: + This format is inherently incapable of detecting renames, hence we only + modify, delete and add files + :return: git.DiffIndex""" + # handles + # :100644 100644 6870991011cc8d9853a7a8a6f02061512c6a8190 37c5e30c879213e9ae83b21e9d11e55fc20c54b7 M .gitignore + index = DiffIndex() + for line in stream: + if not line.startswith(":"): + continue + # END its not a valid diff line + old_mode, new_mode, a_blob_id, b_blob_id, change_type, path = line[1:].split(None, 5) + path = path.strip() + a_path = path + b_path = path + deleted_file = False + new_file = False + + # NOTE: We cannot conclude from the existance of a blob to change type + # as diffs with the working do not have blobs yet + if change_type == 'D': + b_blob_id = None + deleted_file = True + elif change_type == 'A': + a_blob_id = None + new_file = True + # END add/remove handling + + diff = Diff(repo, a_path, b_path, a_blob_id, b_blob_id, old_mode, new_mode, + new_file, deleted_file, None, None, '') + index.append(diff) + # END for each line + + return index diff --git a/lib/git/ext/gitdb b/lib/git/ext/gitdb -Subproject e3d5ad195d9dfa46af3d931f9769e965e337daf +Subproject 39b00429999eefc62b70230fb8e0261948a2f31 diff --git a/lib/git/index/base.py b/lib/git/index/base.py index fb23b1b7..b51d6251 100644 --- a/lib/git/index/base.py +++ b/lib/git/index/base.py @@ -11,7 +11,6 @@ import sys import subprocess import glob from cStringIO import StringIO -from binascii import b2a_hex from stat import ( S_ISLNK, @@ -69,6 +68,7 @@ from fun import ( from gitdb.base import IStream from gitdb.db import MemoryDB +from gitdb.util import to_bin_sha from itertools import izip __all__ = ( 'IndexFile', 'CheckoutError' ) @@ -91,18 +91,16 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): index.entries[index.entry_key(index_entry_instance)] = index_entry_instance Otherwise changes to it will be lost when changing the index using its methods. """ - __slots__ = ( "repo", "version", "entries", "_extension_data", "_file_path" ) + __slots__ = ("repo", "version", "entries", "_extension_data", "_file_path") _VERSION = 2 # latest version we support S_IFGITLINK = 0160000 # a submodule def __init__(self, repo, file_path=None): - """ - Initialize this Index instance, optionally from the given ``file_path``. + """Initialize this Index instance, optionally from the given ``file_path``. If no file_path is given, we will be created from the current index file. If a stream is not given, the stream will be initialized from the current - repository's index on demand. - """ + repository's index on demand.""" self.repo = repo self.version = self._VERSION self._extension_data = '' @@ -153,7 +151,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): #{ Serializable Interface def _deserialize(self, stream): - """ Initialize this instance with index values read from the given stream """ + """Initialize this instance with index values read from the given stream""" self.version, self.entries, self._extension_data, conten_sha = read_cache(stream) return self @@ -217,24 +215,23 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): As opposed to the from_tree_ method, this allows you to use an already existing tree as the left side of the merge - ``rhs`` + :param rhs: treeish reference pointing to the 'other' side of the merge. - ``base`` + :param base: optional treeish reference pointing to the common base of 'rhs' and this index which equals lhs - Returns + :return: self ( containing the merge and possibly unmerged entries in case of conflicts ) - Raise - GitCommandError in case there is a merge conflict. The error will + :raise GitCommandError: + If there is a merge conflict. The error will be raised at the first conflicting path. If you want to have proper merge resolution to be done by yourself, you have to commit the changed index ( or make a valid tree from it ) and retry with a three-way - index.from_tree call. - """ + index.from_tree call. """ # -i : ignore working tree status # --aggressive : handle more merge cases # -m : do an actual merge @@ -254,13 +251,13 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): :param repo: The repository treeish are located in. :param *tree_sha: - see ``from_tree`` + 20 byte or 40 byte tree sha or tree objects :return: New IndexFile instance. Its path will be undefined. If you intend to write such a merged Index, supply an alternate file_path to its 'write' method.""" - base_entries = aggressive_tree_merge(repo.odb, [str(t) for t in tree_sha]) + base_entries = aggressive_tree_merge(repo.odb, [to_bin_sha(str(t)) for t in tree_sha]) inst = cls(repo) # convert to entries dict @@ -273,16 +270,15 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): @classmethod def from_tree(cls, repo, *treeish, **kwargs): - """ - Merge the given treeish revisions into a new index which is returned. + """Merge the given treeish revisions into a new index which is returned. The original index will remain unaltered - ``repo`` + :param repo: The repository treeish are located in. - ``*treeish`` - One, two or three Tree Objects or Commits. The result changes according to the - amount of trees. + :param *treeish: + One, two or three Tree Objects, Commits or 40 byte hexshas. The result + changes according to the amount of trees. If 1 Tree is given, it will just be read into a new index If 2 Trees are given, they will be merged into a new index using a two way merge algorithm. Tree 1 is the 'current' tree, tree 2 is the 'other' @@ -291,15 +287,15 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current' tree, tree 3 is the 'other' one - ``**kwargs`` + :param **kwargs: Additional arguments passed to git-read-tree - Returns + :return: New IndexFile instance. It will point to a temporary index location which does not exist anymore. If you intend to write such a merged Index, supply an alternate file_path to its 'write' method. - Note: + :note: In the three-way merge case, --aggressive will be specified to automatically resolve more cases in a commonly correct manner. Specify trivial=True as kwarg to override that. @@ -420,19 +416,17 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): def iter_blobs(self, predicate = lambda t: True): """ - Returns - Iterator yielding tuples of Blob objects and stages, tuple(stage, Blob) + :return: Iterator yielding tuples of Blob objects and stages, tuple(stage, Blob) - ``predicate`` + :param predicate: Function(t) returning True if tuple(stage, Blob) should be yielded by the iterator. A default filter, the BlobFilter, allows you to yield blobs - only if they match a given list of paths. - """ + only if they match a given list of paths. """ for entry in self.entries.itervalues(): # TODO: is it necessary to convert the mode ? We did that when adding # it to the index, right ? mode = self._stat_mode_to_index_mode(entry.mode) - blob = Blob(self.repo, entry.hexsha, mode, entry.path) + blob = Blob(self.repo, entry.binsha, mode, entry.path) blob.size = entry.size output = (entry.stage, blob) if predicate(output): @@ -465,21 +459,17 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): return entry_key(*entry) def resolve_blobs(self, iter_blobs): - """ - Resolve the blobs given in blob iterator. This will effectively remove the + """Resolve the blobs given in blob iterator. This will effectively remove the index entries of the respective path at all non-null stages and add the given blob as new stage null blob. For each path there may only be one blob, otherwise a ValueError will be raised claiming the path is already at stage 0. - Raise - ValueError if one of the blobs already existed at stage 0 - - Returns: - self + :raise ValueError: if one of the blobs already existed at stage 0 + :return: self - Note + :note: You will have to write the index manually once you are done, i.e. index.resolve_blobs(blobs).write() """ @@ -504,17 +494,12 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): return self def update(self): - """ - Reread the contents of our index file, discarding all cached information + """Reread the contents of our index file, discarding all cached information we might have. - Note: - This is a possibly dangerious operations as it will discard your changes + :note: This is a possibly dangerious operations as it will discard your changes to index.entries - - Returns - self - """ + :return: self""" self._delete_entries_cache() # allows to lazily reread on demand return self @@ -541,7 +526,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): # note: additional deserialization could be saved if write_tree_from_cache # would return sorted tree entries - root_tree = Tree(self.repo, b2a_hex(binsha), path='') + root_tree = Tree(self.repo, binsha, path='') root_tree._cache = tree_items return root_tree @@ -675,7 +660,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): for path in paths: abspath = os.path.abspath(path) gitrelative_path = abspath[len(self.repo.working_tree_dir)+1:] - blob = Blob(self.repo, Blob.NULL_HEX_SHA, + blob = Blob(self.repo, Blob.NULL_BIN_SHA, self._stat_mode_to_index_mode(os.stat(abspath).st_mode), gitrelative_path) entries.append(BaseIndexEntry.from_blob(blob)) @@ -830,33 +815,29 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): @post_clear_cache @default_index def move(self, items, skip_errors=False, **kwargs): - """ - Rename/move the items, whereas the last item is considered the destination of + """Rename/move the items, whereas the last item is considered the destination of the move operation. If the destination is a file, the first item ( of two ) must be a file as well. If the destination is a directory, it may be preceeded by one or more directories or files. The working tree will be affected in non-bare repositories. - ``items`` + :parma items: Multiple types of items are supported, please see the 'remove' method for reference. - ``skip_errors`` + :param skip_errors: If True, errors such as ones resulting from missing source files will be skpped. - ``**kwargs`` + :param **kwargs: Additional arguments you would like to pass to git-mv, such as dry_run or force. - Returns - List(tuple(source_path_string, destination_path_string), ...) + :return:List(tuple(source_path_string, destination_path_string), ...) A list of pairs, containing the source file moved as well as its actual destination. Relative to the repository root. - Raises - ValueErorr: If only one item was given - GitCommandError: If git could not handle your request - """ + :raise ValueErorr: If only one item was given + GitCommandError: If git could not handle your request""" args = list() if skip_errors: args.append('-k') @@ -897,18 +878,15 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): return out def commit(self, message, parent_commits=None, head=True): - """ - Commit the current default index file, creating a commit object. + """Commit the current default index file, creating a commit object. For more information on the arguments, see tree.commit. - - ``NOTE``: + :note: If you have manually altered the .entries member of this instance, don't forget to write() your changes to disk beforehand. - Returns - Commit object representing the new commit - """ + :return: + Commit object representing the new commit""" tree = self.write_tree() return Commit.create_from_tree(self.repo, tree, message, parent_commits, head) @@ -1123,13 +1101,12 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): @default_index def diff(self, other=diff.Diffable.Index, paths=None, create_patch=False, **kwargs): - """ - Diff this index against the working copy or a Tree or Commit object + """Diff this index against the working copy or a Tree or Commit object For a documentation of the parameters and return values, see Diffable.diff - Note + :note: Will only work with indices that represent the default git index as they have not been initialized with a stream. """ diff --git a/lib/git/index/fun.py b/lib/git/index/fun.py index 14a47fdc..ef950761 100644 --- a/lib/git/index/fun.py +++ b/lib/git/index/fun.py @@ -207,7 +207,7 @@ def aggressive_tree_merge(odb, tree_shas): trees. All valid entries are on stage 0, whereas the conflicting ones are left on stage 1, 2 or 3, whereas stage 1 corresponds to the common ancestor tree, 2 to our tree and 3 to 'their' tree. - :param tree_shas: 1, 2 or 3 trees as identified by their shas + :param tree_shas: 1, 2 or 3 trees as identified by their binary 20 byte shas If 1 or two, the entries will effectively correspond to the last given tree If 3 are given, a 3 way merge is performed""" out = list() diff --git a/lib/git/index/typ.py b/lib/git/index/typ.py index d9cafa2e..7654b402 100644 --- a/lib/git/index/typ.py +++ b/lib/git/index/typ.py @@ -7,7 +7,6 @@ from util import ( from binascii import ( b2a_hex, - a2b_hex ) __all__ = ('BlobFilter', 'BaseIndexEntry', 'IndexEntry') @@ -101,7 +100,7 @@ class BaseIndexEntry(tuple): @classmethod def from_blob(cls, blob, stage = 0): """:return: Fully equipped BaseIndexEntry at the given stage""" - return cls((blob.mode, a2b_hex(blob.sha), stage << CE_STAGESHIFT, blob.path)) + return cls((blob.mode, blob.binsha, stage << CE_STAGESHIFT, blob.path)) class IndexEntry(BaseIndexEntry): @@ -164,6 +163,6 @@ class IndexEntry(BaseIndexEntry): def from_blob(cls, blob, stage = 0): """:return: Minimal entry resembling the given blob object""" time = pack(">LL", 0, 0) - return IndexEntry((blob.mode, a2b_hex(blob.sha), stage << CE_STAGESHIFT, blob.path, time, time, 0, 0, 0, 0, blob.size)) + return IndexEntry((blob.mode, blob.binsha, stage << CE_STAGESHIFT, blob.path, time, time, 0, 0, 0, 0, blob.size)) diff --git a/lib/git/objects/base.py b/lib/git/objects/base.py index 90aa8ca2..118bc3ca 100644 --- a/lib/git/objects/base.py +++ b/lib/git/objects/base.py @@ -3,179 +3,140 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -import os from git.utils import LazyMixin, join_path_native, stream_copy -import utils +from utils import get_object_type_by_name +from gitdb.util import ( + hex_to_bin, + bin_to_hex, + basename + ) + +import gitdb.typ as dbtyp _assertion_msg_format = "Created object %r whose python type %r disagrees with the acutal git object type %r" +__all__ = ("Object", "IndexObject") + class Object(LazyMixin): - """ - Implements an Object which may be Blobs, Trees, Commits and Tags - - This Object also serves as a constructor for instances of the correct type:: - - inst = Object.new(repo,id) - inst.sha # objects sha in hex - inst.size # objects uncompressed data size - inst.data # byte string containing the whole data of the object - """ + """Implements an Object which may be Blobs, Trees, Commits and Tags""" NULL_HEX_SHA = '0'*40 NULL_BIN_SHA = '\0'*20 - TYPES = ("blob", "tree", "commit", "tag") - __slots__ = ("repo", "sha", "size", "data" ) + + TYPES = (dbtyp.str_blob_type, dbtyp.str_tree_type, dbtyp.str_commit_type, dbtyp.str_tag_type) + __slots__ = ("repo", "binsha", "size" ) type = None # to be set by subclass - def __init__(self, repo, id): - """ - Initialize an object by identifying it by its id. All keyword arguments - will be set on demand if None. + def __init__(self, repo, binsha): + """Initialize an object by identifying it by its binary sha. + All keyword arguments will be set on demand if None. - ``repo`` - repository this object is located in + :param repo: repository this object is located in - ``id`` - SHA1 or ref suitable for git-rev-parse - """ + :param binsha: 20 byte SHA1""" super(Object,self).__init__() self.repo = repo - self.sha = id + self.binsha = binsha @classmethod def new(cls, repo, id): """ - Return - New Object instance of a type appropriate to the object type behind - id. The id of the newly created object will be a hexsha even though + :return: New Object instance of a type appropriate to the object type behind + id. The id of the newly created object will be a binsha even though the input id may have been a Reference or Rev-Spec - Note - This cannot be a __new__ method as it would always call __init__ - with the input id which is not necessarily a hexsha. - """ + :param id: reference, rev-spec, or hexsha + + :note: This cannot be a __new__ method as it would always call __init__ + with the input id which is not necessarily a binsha.""" hexsha, typename, size = repo.git.get_object_header(id) - obj_type = utils.get_object_type_by_name(typename) - inst = obj_type(repo, hexsha) + inst = get_object_type_by_name(typename)(repo, hex_to_bin(hexsha)) inst.size = size return inst def _set_self_from_args_(self, args_dict): - """ - Initialize attributes on self from the given dict that was retrieved + """Initialize attributes on self from the given dict that was retrieved from locals() in the calling method. Will only set an attribute on self if the corresponding value in args_dict - is not None - """ + is not None""" for attr, val in args_dict.items(): if attr != "self" and val is not None: setattr( self, attr, val ) # END set all non-None attributes def _set_cache_(self, attr): - """ - Retrieve object information - """ + """Retrieve object information""" if attr == "size": - oinfo = self.repo.odb.info(self.sha) + oinfo = self.repo.odb.info(self.binsha) self.size = oinfo.size - assert oinfo.type == self.type, _assertion_msg_format % (self.sha, oinfo.type, self.type) - elif attr == "data": - ostream = self.repo.odb.stream(self.sha) - self.size = ostream.size - self.data = ostream.read() - assert ostream.type == self.type, _assertion_msg_format % (self.sha, ostream.type, self.type) + # assert oinfo.type == self.type, _assertion_msg_format % (self.binsha, oinfo.type, self.type) else: super(Object,self)._set_cache_(attr) def __eq__(self, other): - """ - Returns - True if the objects have the same SHA1 - """ - return self.sha == other.sha + """:return: True if the objects have the same SHA1""" + return self.binsha == other.binsha def __ne__(self, other): - """ - Returns - True if the objects do not have the same SHA1 - """ - return self.sha != other.sha + """:return: True if the objects do not have the same SHA1 """ + return self.binsha != other.binsha def __hash__(self): - """ - Returns - Hash of our id allowing objects to be used in dicts and sets - """ - return hash(self.sha) + """:return: Hash of our id allowing objects to be used in dicts and sets""" + return hash(self.binsha) def __str__(self): - """ - Returns - string of our SHA1 as understood by all git commands - """ - return self.sha + """:return: string of our SHA1 as understood by all git commands""" + return bin_to_hex(self.binsha) def __repr__(self): - """ - Returns - string with pythonic representation of our object - """ - return '<git.%s "%s">' % (self.__class__.__name__, self.sha) + """:return: string with pythonic representation of our object""" + return '<git.%s "%s">' % (self.__class__.__name__, self.hexsha) + + @property + def hexsha(self): + """:return: 40 byte hex version of our 20 byte binary sha""" + return bin_to_hex(self.binsha) @property def data_stream(self): """ :return: File Object compatible stream to the uncompressed raw data of the object :note: returned streams must be read in order""" - return self.repo.odb.stream(self.sha) + return self.repo.odb.stream(self.binsha) def stream_data(self, ostream): """Writes our data directly to the given output stream :param ostream: File object compatible stream object. :return: self""" - istream = self.repo.odb.stream(self.sha) + istream = self.repo.odb.stream(self.binsha) stream_copy(istream, ostream) return self class IndexObject(Object): - """ - Base for all objects that can be part of the index file , namely Tree, Blob and - SubModule objects - """ + """Base for all objects that can be part of the index file , namely Tree, Blob and + SubModule objects""" __slots__ = ("path", "mode") - def __init__(self, repo, sha, mode=None, path=None): - """ - Initialize a newly instanced IndexObject - ``repo`` - is the Repo we are located in - - ``sha`` : string - is the git object id as hex sha - - ``mode`` : int - is the file mode as int, use the stat module to evaluate the infomration - - ``path`` : str + def __init__(self, repo, binsha, mode=None, path=None): + """Initialize a newly instanced IndexObject + :param repo: is the Repo we are located in + :param binsha: 20 byte sha1 + :param mode: is the stat compatible file mode as int, use the stat module + to evaluate the infomration + :param path: is the path to the file in the file system, relative to the git repository root, i.e. file.ext or folder/other.ext - - NOTE + :note: Path may not be set of the index object has been created directly as it cannot - be retrieved without knowing the parent tree. - """ - super(IndexObject, self).__init__(repo, sha) + be retrieved without knowing the parent tree.""" + super(IndexObject, self).__init__(repo, binsha) self._set_self_from_args_(locals()) - if isinstance(mode, basestring): - self.mode = self._mode_str_to_int(mode) def __hash__(self): - """ - Returns + """:return: Hash of our path as index items are uniquely identifyable by path, not - by their data ! - """ + by their data !""" return hash(self.path) def _set_cache_(self, attr): @@ -184,41 +145,20 @@ class IndexObject(Object): raise AttributeError( "path and mode attributes must have been set during %s object creation" % type(self).__name__ ) else: super(IndexObject, self)._set_cache_(attr) + # END hanlde slot attribute - @classmethod - def _mode_str_to_int(cls, modestr): - """ - ``modestr`` - string like 755 or 644 or 100644 - only the last 6 chars will be used - - Returns - String identifying a mode compatible to the mode methods ids of the - stat module regarding the rwx permissions for user, group and other, - special flags and file system flags, i.e. whether it is a symlink - for example. - """ - mode = 0 - for iteration, char in enumerate(reversed(modestr[-6:])): - mode += int(char) << iteration*3 - # END for each char - return mode - @property def name(self): - """ - Returns - Name portion of the path, effectively being the basename - """ - return os.path.basename(self.path) + """:return: Name portion of the path, effectively being the basename""" + return basename(self.path) @property def abspath(self): """ - Returns + :return: Absolute path to this index object in the file system ( as opposed to the .path field which is a path relative to the git repository ). - The returned path will be native to the system and contains '\' on windows. - """ + The returned path will be native to the system and contains '\' on windows. """ return join_path_native(self.repo.working_tree_dir, self.path) diff --git a/lib/git/objects/blob.py b/lib/git/objects/blob.py index 3f91d078..8263e9a2 100644 --- a/lib/git/objects/blob.py +++ b/lib/git/objects/blob.py @@ -4,33 +4,33 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -import mimetypes +from mimetypes import guess_type import base -class Blob(base.IndexObject): - """A Blob encapsulates a git blob object""" - DEFAULT_MIME_TYPE = "text/plain" - type = "blob" - - __slots__ = tuple() +__all__ = ('Blob', ) - - @property - def mime_type(self): - """ - The mime type of this file (based on the filename) +class Blob(base.IndexObject): + """A Blob encapsulates a git blob object""" + DEFAULT_MIME_TYPE = "text/plain" + type = "blob" - Returns - str - - NOTE - Defaults to 'text/plain' in case the actual file type is unknown. - """ - guesses = None - if self.path: - guesses = mimetypes.guess_type(self.path) - return guesses and guesses[0] or self.DEFAULT_MIME_TYPE + __slots__ = "data" + def _set_cache_(self, attr): + if attr == "data": + ostream = self.repo.odb.stream(self.binsha) + self.size = ostream.size + self.data = ostream.read() + # assert ostream.type == self.type, _assertion_msg_format % (self.binsha, ostream.type, self.type) + else: + super(Blob, self)._set_cache_(attr) + # END handle data - def __repr__(self): - return '<git.Blob "%s">' % self.sha + @property + def mime_type(self): + """ :return:String describing the mime type of this file (based on the filename) + :note: Defaults to 'text/plain' in case the actual file type is unknown. """ + guesses = None + if self.path: + guesses = guess_type(self.path) + return guesses and guesses[0] or self.DEFAULT_MIME_TYPE diff --git a/lib/git/objects/commit.py b/lib/git/objects/commit.py index f30a6dea..f365c994 100644 --- a/lib/git/objects/commit.py +++ b/lib/git/objects/commit.py @@ -8,24 +8,37 @@ from git.utils import ( Iterable, Stats, ) - -import git.diff as diff +from git.diff import Diffable from tree import Tree from gitdb import IStream from cStringIO import StringIO + import base -import utils -import time +from gitdb.util import ( + hex_to_bin + ) +from utils import ( + Traversable, + Serializable, + get_user_id, + parse_date, + Actor, + altz_to_utctz_str, + parse_actor_and_date + ) +from time import ( + time, + altzone + ) import os +__all__ = ('Commit', ) -class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Serializable): - """ - Wraps a git Commit object. +class Commit(base.Object, Iterable, Diffable, Traversable, Serializable): + """Wraps a git Commit object. This class will act lazily on some of its attributes and will query the - value on demand only if it involves calling the git binary. - """ + value on demand only if it involves calling the git binary.""" # ENVIRONMENT VARIABLES # read when creating new commits @@ -52,22 +65,19 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri "author", "authored_date", "author_tz_offset", "committer", "committed_date", "committer_tz_offset", "message", "parents", "encoding") - _id_attribute_ = "sha" + _id_attribute_ = "binsha" - def __init__(self, repo, sha, tree=None, author=None, authored_date=None, author_tz_offset=None, + def __init__(self, repo, binsha, tree=None, author=None, authored_date=None, author_tz_offset=None, committer=None, committed_date=None, committer_tz_offset=None, message=None, parents=None, encoding=None): - """ - Instantiate a new Commit. All keyword arguments taking None as default will - be implicitly set if id names a valid sha. + """Instantiate a new Commit. All keyword arguments taking None as default will + be implicitly set on first query. - The parameter documentation indicates the type of the argument after a colon ':'. - - :param sha: is the sha id of the commit or a ref + :param binsha: 20 byte sha1 :param parents: tuple( Commit, ... ) is a tuple of commit ids or actual Commits :param tree: Tree - is the corresponding tree id or an actual Tree + Tree object :param author: Actor is the author string ( will be implicitly converted into an Actor object ) :param authored_date: int_seconds_since_epoch @@ -86,13 +96,15 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri is the commit message :param encoding: string encoding of the message, defaults to UTF-8 + :param parents: + List or tuple of Commit objects which are our parent(s) in the commit + dependency graph :return: git.Commit :note: Timezone information is in the same format and in the same sign as what time.altzone returns. The sign is inverted compared to git's - UTC timezone. - """ - super(Commit,self).__init__(repo, sha) + UTC timezone.""" + super(Commit,self).__init__(repo, binsha) self._set_self_from_args_(locals()) @classmethod @@ -100,80 +112,61 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri return commit.parents def _set_cache_(self, attr): - """ Called by LazyMixin superclass when the given uninitialized member needs - to be set. - We set all values at once. """ if attr in Commit.__slots__: # read the data in a chunk, its faster - then provide a file wrapper - # Could use self.data, but lets try to get it with less calls - hexsha, typename, size, data = self.repo.git.get_object_data(self) - self._deserialize(StringIO(data)) + binsha, typename, self.size, stream = self.repo.odb.stream(self.binsha) + self._deserialize(StringIO(stream.read())) else: super(Commit, self)._set_cache_(attr) + # END handle attrs @property def summary(self): - """ - Returns - First line of the commit message. - """ + """:return: First line of the commit message""" return self.message.split('\n', 1)[0] def count(self, paths='', **kwargs): - """ - Count the number of commits reachable from this commit + """Count the number of commits reachable from this commit - ``paths`` + :param paths: is an optinal path or a list of paths restricting the return value to commits actually containing the paths - ``kwargs`` + :param kwargs: Additional options to be passed to git-rev-list. They must not alter the ouput style of the command, or parsing will yield incorrect results - Returns - int - """ + :return: int defining the number of reachable commits""" # yes, it makes a difference whether empty paths are given or not in our case # as the empty paths version will ignore merge commits for some reason. if paths: - return len(self.repo.git.rev_list(self.sha, '--', paths, **kwargs).splitlines()) + return len(self.repo.git.rev_list(self.hexsha, '--', paths, **kwargs).splitlines()) else: - return len(self.repo.git.rev_list(self.sha, **kwargs).splitlines()) + return len(self.repo.git.rev_list(self.hexsha, **kwargs).splitlines()) @property def name_rev(self): """ - Returns + :return: String describing the commits hex sha based on the closest Reference. - Mostly useful for UI purposes - """ + Mostly useful for UI purposes""" return self.repo.git.name_rev(self) @classmethod def iter_items(cls, repo, rev, paths='', **kwargs): - """ - Find all commits matching the given criteria. - - ``repo`` - is the Repo - - ``rev`` - revision specifier, see git-rev-parse for viable options + """Find all commits matching the given criteria. - ``paths`` + :param repo: is the Repo + :param rev: revision specifier, see git-rev-parse for viable options + :param paths: is an optinal path or list of paths, if set only Commits that include the path or paths will be considered - - ``kwargs`` + :param kwargs: optional keyword arguments to git rev-list where ``max_count`` is the maximum number of commits to fetch ``skip`` is the number of commits to skip ``since`` all commits since i.e. '1970-01-01' - - Returns - iterator yielding Commit items - """ + :return: iterator yielding Commit items""" if 'pretty' in kwargs: raise ValueError("--pretty cannot be used as parsing expects single sha's only") # END handle pretty @@ -186,45 +179,36 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri return cls._iter_from_process_or_stream(repo, proc) def iter_parents(self, paths='', **kwargs): - """ - Iterate _all_ parents of this commit. - - ``paths`` + """Iterate _all_ parents of this commit. + :param paths: Optional path or list of paths limiting the Commits to those that contain at least one of the paths - - ``kwargs`` - All arguments allowed by git-rev-list + :param kwargs: All arguments allowed by git-rev-list - Return: - Iterator yielding Commit objects which are parents of self - """ + :return: Iterator yielding Commit objects which are parents of self """ # skip ourselves skip = kwargs.get("skip", 1) if skip == 0: # skip ourselves skip = 1 kwargs['skip'] = skip - return self.iter_items( self.repo, self, paths, **kwargs ) + return self.iter_items(self.repo, self, paths, **kwargs) @property def stats(self): - """ - Create a git stat from changes between this commit and its first parent + """Create a git stat from changes between this commit and its first parent or from all changes done if this is the very first commit. - Return - git.Stats - """ + :return: git.Stats""" if not self.parents: - text = self.repo.git.diff_tree(self.sha, '--', numstat=True, root=True) + text = self.repo.git.diff_tree(self.hexsha, '--', numstat=True, root=True) text2 = "" for line in text.splitlines()[1:]: (insertions, deletions, filename) = line.split("\t") text2 += "%s\t%s\t%s\n" % (insertions, deletions, filename) text = text2 else: - text = self.repo.git.diff(self.parents[0].sha, self.sha, '--', numstat=True) + text = self.repo.git.diff(self.parents[0].hexsha, self.hexsha, '--', numstat=True) return Stats._list_from_string(self.repo, text) @classmethod @@ -244,14 +228,14 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri line = readline() if not line: break - sha = line.strip() - if len(sha) > 40: + hexsha = line.strip() + if len(hexsha) > 40: # split additional information, as returned by bisect for instance - sha, rest = line.split(None, 1) + hexsha, rest = line.split(None, 1) # END handle extra info - assert len(sha) == 40, "Invalid line: %s" % sha - yield Commit(repo, sha) + assert len(hexsha) == 40, "Invalid line: %s" % hexsha + yield Commit(repo, hex_to_bin(hexsha)) # END for each line in stream @@ -260,7 +244,8 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri """Commit the given tree, creating a commit object. :param repo: Repo object the commit should be part of - :param tree: Sha of a tree or a tree object to become the tree of the new commit + :param tree: Tree object or hex or bin sha + the tree of the new commit :param message: Commit message. It may be an empty string if no message is provided. It will be converted to a string in any case. :param parent_commits: @@ -279,8 +264,7 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri :note: Additional information about the committer and Author are taken from the environment or from the git configuration, see git-commit-tree for - more information - """ + more information""" parents = parent_commits if parent_commits is None: try: @@ -300,7 +284,7 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri # COMMITER AND AUTHOR INFO cr = repo.config_reader() env = os.environ - default_email = utils.get_user_id() + default_email = get_user_id() default_name = default_email.split('@')[0] conf_name = cr.get_value('user', cls.conf_name, default_name) @@ -313,19 +297,19 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri committer_email = env.get(cls.env_committer_email, conf_email) # PARSE THE DATES - unix_time = int(time.time()) - offset = time.altzone + unix_time = int(time()) + offset = altzone author_date_str = env.get(cls.env_author_date, '') if author_date_str: - author_time, author_offset = utils.parse_date(author_date_str) + author_time, author_offset = parse_date(author_date_str) else: author_time, author_offset = unix_time, offset # END set author time committer_date_str = env.get(cls.env_committer_date, '') if committer_date_str: - committer_time, committer_offset = utils.parse_date(committer_date_str) + committer_time, committer_offset = parse_date(committer_date_str) else: committer_time, committer_offset = unix_time, offset # END set committer time @@ -334,12 +318,18 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri enc_section, enc_option = cls.conf_encoding.split('.') conf_encoding = cr.get_value(enc_section, enc_option, cls.default_encoding) - author = utils.Actor(author_name, author_email) - committer = utils.Actor(committer_name, committer_email) + author = Actor(author_name, author_email) + committer = Actor(committer_name, committer_email) + + # if the tree is no object, make sure we create one - otherwise + # the created commit object is invalid + if isinstance(tree, str): + tree = repo.tree(tree) + # END tree conversion # CREATE NEW COMMIT - new_commit = cls(repo, cls.NULL_HEX_SHA, tree, + new_commit = cls(repo, cls.NULL_BIN_SHA, tree, author, author_time, author_offset, committer, committer_time, committer_offset, message, parent_commits, conf_encoding) @@ -350,7 +340,7 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri stream.seek(0) istream = repo.odb.store(IStream(cls.type, streamlen, stream)) - new_commit.sha = istream.sha + new_commit.binsha = istream.binsha if head: try: @@ -366,14 +356,6 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri return new_commit - - def __str__(self): - """ Convert commit to string which is SHA1 """ - return self.sha - - def __repr__(self): - return '<git.Commit "%s">' % self.sha - #{ Serializable Implementation def _serialize(self, stream): @@ -387,11 +369,11 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri fmt = "%s %s <%s> %s %s\n" write(fmt % ("author", a.name, a.email, self.authored_date, - utils.altz_to_utctz_str(self.author_tz_offset))) + altz_to_utctz_str(self.author_tz_offset))) write(fmt % ("committer", c.name, c.email, self.committed_date, - utils.altz_to_utctz_str(self.committer_tz_offset))) + altz_to_utctz_str(self.committer_tz_offset))) if self.encoding != self.default_encoding: write("encoding %s\n" % self.encoding) @@ -404,7 +386,7 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri """:param from_rev_list: if true, the stream format is coming from the rev-list command Otherwise it is assumed to be a plain data stream from our object""" readline = stream.readline - self.tree = Tree(self.repo, readline().split()[1], Tree.tree_id<<12, '') + self.tree = Tree(self.repo, hex_to_bin(readline().split()[1]), Tree.tree_id<<12, '') self.parents = list() next_line = None @@ -414,12 +396,12 @@ class Commit(base.Object, Iterable, diff.Diffable, utils.Traversable, utils.Seri next_line = parent_line break # END abort reading parents - self.parents.append(type(self)(self.repo, parent_line.split()[-1])) + self.parents.append(type(self)(self.repo, hex_to_bin(parent_line.split()[-1]))) # END for each parent line self.parents = tuple(self.parents) - self.author, self.authored_date, self.author_tz_offset = utils.parse_actor_and_date(next_line) - self.committer, self.committed_date, self.committer_tz_offset = utils.parse_actor_and_date(readline()) + self.author, self.authored_date, self.author_tz_offset = parse_actor_and_date(next_line) + self.committer, self.committed_date, self.committer_tz_offset = parse_actor_and_date(readline()) # now we can have the encoding line, or an empty line followed by the optional diff --git a/lib/git/objects/fun.py b/lib/git/objects/fun.py index 5b39ab0c..2d0fd634 100644 --- a/lib/git/objects/fun.py +++ b/lib/git/objects/fun.py @@ -1,9 +1,10 @@ """Module with functions which are supposed to be as fast as possible""" +from stat import S_ISDIR __all__ = ('tree_to_stream', 'tree_entries_from_data', 'traverse_trees_recursive', 'traverse_tree_recursive') -from stat import S_ISDIR + def tree_to_stream(entries, write): @@ -99,7 +100,7 @@ def _to_full_path(item, path_prefix): def traverse_trees_recursive(odb, tree_shas, path_prefix): """ - :return: list with entries according to the given tree-shas. + :return: list with entries according to the given binary tree-shas. The result is encoded in a list of n tuple|None per blob/commit, (n == len(tree_shas)), where * [0] == 20 byte sha @@ -165,7 +166,7 @@ def traverse_trees_recursive(odb, tree_shas, path_prefix): def traverse_tree_recursive(odb, tree_sha, path_prefix): """ - :return: list of entries of the tree pointed to by tree_sha. An entry + :return: list of entries of the tree pointed to by the binary tree_sha. An entry has the following format: * [0] 20 byte sha * [1] mode as int diff --git a/lib/git/objects/submodule.py b/lib/git/objects/submodule.py index 4742d448..1f571a48 100644 --- a/lib/git/objects/submodule.py +++ b/lib/git/objects/submodule.py @@ -1,5 +1,6 @@ import base +__all__ = ("Submodule", ) class Submodule(base.IndexObject): """Implements access to a git submodule. They are special in that their sha diff --git a/lib/git/objects/tag.py b/lib/git/objects/tag.py index 96363db6..702eae35 100644 --- a/lib/git/objects/tag.py +++ b/lib/git/objects/tag.py @@ -3,77 +3,63 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -""" -Module containing all object based types. -""" +""" Module containing all object based types. """ import base -import utils +from gitdb.util import hex_to_bin +from utils import ( + get_object_type_by_name, + parse_actor_and_date + ) -class TagObject(base.Object): - """ - Non-Lightweight tag carrying additional information about an object we are pointing - to. - """ - type = "tag" - __slots__ = ( "object", "tag", "tagger", "tagged_date", "tagger_tz_offset", "message" ) - - def __init__(self, repo, sha, object=None, tag=None, - tagger=None, tagged_date=None, tagger_tz_offset=None, message=None): - """ - Initialize a tag object with additional data - - ``repo`` - repository this object is located in - - ``sha`` - SHA1 or ref suitable for git-rev-parse - - ``object`` - Object instance of object we are pointing to - - ``tag`` - name of this tag - - ``tagger`` - Actor identifying the tagger - - ``tagged_date`` : int_seconds_since_epoch - is the DateTime of the tag creation - use time.gmtime to convert - it into a different format - - ``tagged_tz_offset``: int_seconds_west_of_utc - is the timezone that the authored_date is in +__all__ = ("TagObject", ) - """ - super(TagObject, self).__init__(repo, sha ) - self._set_self_from_args_(locals()) - - def _set_cache_(self, attr): - """ - Cache all our attributes at once - """ - if attr in TagObject.__slots__: - lines = self.data.splitlines() - - obj, hexsha = lines[0].split(" ") # object <hexsha> - type_token, type_name = lines[1].split(" ") # type <type_name> - self.object = utils.get_object_type_by_name(type_name)(self.repo, hexsha) - - self.tag = lines[2][4:] # tag <tag name> - - tagger_info = lines[3][7:]# tagger <actor> <date> - self.tagger, self.tagged_date, self.tagger_tz_offset = utils.parse_actor_and_date(tagger_info) - - # line 4 empty - it could mark the beginning of the next header - # in csse there really is no message, it would not exist. Otherwise - # a newline separates header from message - if len(lines) > 5: - self.message = "\n".join(lines[5:]) - else: - self.message = '' - # END check our attributes - else: - super(TagObject, self)._set_cache_(attr) - - +class TagObject(base.Object): + """Non-Lightweight tag carrying additional information about an object we are pointing to.""" + type = "tag" + __slots__ = ( "object", "tag", "tagger", "tagged_date", "tagger_tz_offset", "message" ) + + def __init__(self, repo, binsha, object=None, tag=None, + tagger=None, tagged_date=None, tagger_tz_offset=None, message=None): + """Initialize a tag object with additional data + + :param repo: repository this object is located in + :param binsha: 20 byte SHA1 + :param object: Object instance of object we are pointing to + :param tag: name of this tag + :param tagger: Actor identifying the tagger + :param tagged_date: int_seconds_since_epoch + is the DateTime of the tag creation - use time.gmtime to convert + it into a different format + :param tagged_tz_offset: int_seconds_west_of_utc is the timezone that the + authored_date is in, in a format similar to time.altzone""" + super(TagObject, self).__init__(repo, binsha ) + self._set_self_from_args_(locals()) + + def _set_cache_(self, attr): + """Cache all our attributes at once""" + if attr in TagObject.__slots__: + ostream = self.repo.odb.stream(self.binsha) + lines = ostream.read().splitlines() + + obj, hexsha = lines[0].split(" ") # object <hexsha> + type_token, type_name = lines[1].split(" ") # type <type_name> + self.object = get_object_type_by_name(type_name)(self.repo, hex_to_bin(hexsha)) + + self.tag = lines[2][4:] # tag <tag name> + + tagger_info = lines[3][7:]# tagger <actor> <date> + self.tagger, self.tagged_date, self.tagger_tz_offset = parse_actor_and_date(tagger_info) + + # line 4 empty - it could mark the beginning of the next header + # in case there really is no message, it would not exist. Otherwise + # a newline separates header from message + if len(lines) > 5: + self.message = "\n".join(lines[5:]) + else: + self.message = '' + # END check our attributes + else: + super(TagObject, self)._set_cache_(attr) + + diff --git a/lib/git/objects/tree.py b/lib/git/objects/tree.py index 6b1d13c1..056d3da9 100644 --- a/lib/git/objects/tree.py +++ b/lib/git/objects/tree.py @@ -3,23 +3,24 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php - -import os import utils -import base +from base import IndexObject from blob import Blob from submodule import Submodule import git.diff as diff -join = os.path.join from fun import ( tree_entries_from_data, tree_to_stream ) -from gitdb.util import to_bin_sha -from binascii import b2a_hex +from gitdb.util import ( + to_bin_sha, + join + ) + +__all__ = ("TreeModifier", "Tree") class TreeModifier(object): """A utility class providing methods to alter the underlying cache in a list-like @@ -99,12 +100,8 @@ class TreeModifier(object): #} END mutators -class Tree(base.IndexObject, diff.Diffable, utils.Traversable, utils.Serializable): - """ - Tress represent a ordered list of Blobs and other Trees. Hence it can be - accessed like a list. - - Tree's will cache their contents after first retrieval to improve efficiency. +class Tree(IndexObject, diff.Diffable, utils.Traversable, utils.Serializable): + """Tree objects represent an ordered list of Blobs and other Trees. ``Tree as a list``:: @@ -113,8 +110,6 @@ class Tree(base.IndexObject, diff.Diffable, utils.Traversable, utils.Serializabl You may as well access by index blob = tree[0] - - """ type = "tree" @@ -134,8 +129,8 @@ class Tree(base.IndexObject, diff.Diffable, utils.Traversable, utils.Serializabl } - def __init__(self, repo, sha, mode=tree_id<<12, path=None): - super(Tree, self).__init__(repo, sha, mode, path) + def __init__(self, repo, binsha, mode=tree_id<<12, path=None): + super(Tree, self).__init__(repo, binsha, mode, path) @classmethod def _get_intermediate_items(cls, index_object): @@ -146,39 +141,28 @@ class Tree(base.IndexObject, diff.Diffable, utils.Traversable, utils.Serializabl def _set_cache_(self, attr): if attr == "_cache": # Set the data when we need it - self._cache = tree_entries_from_data(self.data) + ostream = self.repo.odb.stream(self.binsha) + self._cache = tree_entries_from_data(ostream.read()) else: super(Tree, self)._set_cache_(attr) + # END handle attribute def _iter_convert_to_object(self, iterable): - """Iterable yields tuples of (hexsha, mode, name), which will be converted + """Iterable yields tuples of (binsha, mode, name), which will be converted to the respective object representation""" for binsha, mode, name in iterable: path = join(self.path, name) - type_id = mode >> 12 try: - yield self._map_id_to_type[type_id](self.repo, b2a_hex(binsha), mode, path) + yield self._map_id_to_type[mode >> 12](self.repo, binsha, mode, path) except KeyError: - raise TypeError( "Unknown type %i found in tree data for path '%s'" % (type_id, path)) + raise TypeError("Unknown mode %o found in tree data for path '%s'" % (mode, path)) # END for each item def __div__(self, file): - """ - Find the named object in this tree's contents - - Examples:: - - >>> Repo('/path/to/python-git').tree/'lib' - <git.Tree "6cc23ee138be09ff8c28b07162720018b244e95e"> - >>> Repo('/path/to/python-git').tree/'README.txt' - <git.Blob "8b1e02c0fb554eed2ce2ef737a68bb369d7527df"> - - Returns - ``git.Blob`` or ``git.Tree`` + """Find the named object in this tree's contents + :return: ``git.Blob`` or ``git.Tree`` or ``git.Submodule`` - Raise - KeyError if given file or tree does not exist in tree - """ + :raise KeyError: if given file or tree does not exist in tree""" msg = "Blob or Tree named %r not found" if '/' in file: tree = self @@ -201,29 +185,20 @@ class Tree(base.IndexObject, diff.Diffable, utils.Traversable, utils.Serializabl else: for info in self._cache: if info[2] == file: # [2] == name - return self._map_id_to_type[info[1] >> 12](self.repo, b2a_hex(info[0]), info[1], join(self.path, info[2])) + return self._map_id_to_type[info[1] >> 12](self.repo, info[0], info[1], join(self.path, info[2])) # END for each obj raise KeyError( msg % file ) # END handle long paths - def __repr__(self): - return '<git.Tree "%s">' % self.sha - @property def trees(self): - """ - Returns - list(Tree, ...) list of trees directly below this tree - """ + """:return: list(Tree, ...) list of trees directly below this tree""" return [ i for i in self if i.type == "tree" ] @property def blobs(self): - """ - Returns - list(Blob, ...) list of blobs directly below this tree - """ + """:return: list(Blob, ...) list of blobs directly below this tree""" return [ i for i in self if i.type == "blob" ] @property @@ -238,7 +213,6 @@ class Tree(base.IndexObject, diff.Diffable, utils.Traversable, utils.Serializabl prune = lambda i,d: False, depth = -1, branch_first=True, visit_once = False, ignore_self=1 ): """For documentation, see utils.Traversable.traverse - Trees are set to visit_once = False to gain more performance in the traversal""" return super(Tree, self).traverse(predicate, prune, depth, branch_first, visit_once, ignore_self) @@ -255,7 +229,7 @@ class Tree(base.IndexObject, diff.Diffable, utils.Traversable, utils.Serializabl def __getitem__(self, item): if isinstance(item, int): info = self._cache[item] - return self._map_id_to_type[info[1] >> 12](self.repo, b2a_hex(info[0]), info[1], join(self.path, info[2])) + return self._map_id_to_type[info[1] >> 12](self.repo, info[0], info[1], join(self.path, info[2])) if isinstance(item, basestring): # compatability @@ -266,9 +240,9 @@ class Tree(base.IndexObject, diff.Diffable, utils.Traversable, utils.Serializabl def __contains__(self, item): - if isinstance(item, base.IndexObject): + if isinstance(item, IndexObject): for info in self._cache: - if item.sha == info[0]: + if item.binsha == info[0]: return True # END compare sha # END for each entry diff --git a/lib/git/objects/utils.py b/lib/git/objects/utils.py index 072662ee..c0ddd6e6 100644 --- a/lib/git/objects/utils.py +++ b/lib/git/objects/utils.py @@ -3,9 +3,7 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -""" -Module for general utility functions -""" +"""Module for general utility functions""" import re from collections import deque as Deque import platform @@ -20,18 +18,28 @@ __all__ = ('get_object_type_by_name', 'get_user_id', 'parse_date', 'parse_actor_ #{ Functions +def mode_str_to_int(modestr): + """ + :param modestr: string like 755 or 644 or 100644 - only the last 6 chars will be used + :return: + String identifying a mode compatible to the mode methods ids of the + stat module regarding the rwx permissions for user, group and other, + special flags and file system flags, i.e. whether it is a symlink + for example.""" + mode = 0 + for iteration, char in enumerate(reversed(modestr[-6:])): + mode += int(char) << iteration*3 + # END for each char + return mode + def get_object_type_by_name(object_type_name): """ - Returns - type suitable to handle the given object type name. + :return: type suitable to handle the given object type name. Use the type to create new instances. - ``object_type_name`` - Member of TYPES + :param object_type_name: Member of TYPES - Raises - ValueError: In case object_type_name is unknown - """ + :raise ValueError: In case object_type_name is unknown""" if object_type_name == "commit": import commit return commit.Commit @@ -169,14 +177,11 @@ def parse_date(string_date): _re_actor_epoch = re.compile(r'^.+? (.*) (\d+) ([+-]\d+).*$') def parse_actor_and_date(line): - """ - Parse out the actor (author or committer) info from a line like:: + """Parse out the actor (author or committer) info from a line like:: - author Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700 + author Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700 - Returns - [Actor, int_seconds_since_epoch, int_timezone_offset] - """ + :return: [Actor, int_seconds_since_epoch, int_timezone_offset]""" m = _re_actor_epoch.search(line) actor, epoch, offset = m.groups() return (Actor._from_string(actor), int(epoch), utctz_to_altz(offset)) @@ -238,13 +243,11 @@ class Actor(object): class ProcessStreamAdapter(object): - """ - Class wireing all calls to the contained Process instance. + """Class wireing all calls to the contained Process instance. Use this type to hide the underlying process to provide access only to a specified stream. The process is usually wrapped into an AutoInterrupt class to kill - it if the instance goes out of scope. - """ + it if the instance goes out of scope.""" __slots__ = ("_proc", "_stream") def __init__(self, process, stream_name): self._proc = process @@ -274,36 +277,33 @@ class Traversable(object): def traverse( self, predicate = lambda i,d: True, prune = lambda i,d: False, depth = -1, branch_first=True, visit_once = True, ignore_self=1, as_edge = False ): - """ - ``Returns`` - iterator yieling of items found when traversing self + """:return: iterator yieling of items found when traversing self - ``predicate`` - f(i,d) returns False if item i at depth d should not be included in the result + :param predicate: f(i,d) returns False if item i at depth d should not be included in the result - ``prune`` + :param prune: f(i,d) return True if the search should stop at item i at depth d. Item i will not be returned. - ``depth`` + :param depth: define at which level the iteration should not go deeper if -1, there is no limit if 0, you would effectively only get self, the root of the iteration i.e. if 1, you would only get the first level of predessessors/successors - ``branch_first`` + :param branch_first: if True, items will be returned branch first, otherwise depth first - ``visit_once`` + :param visit_once: if True, items will only be returned once, although they might be encountered several times. Loops are prevented that way. - ``ignore_self`` + :param ignore_self: if True, self will be ignored and automatically pruned from the result. Otherwise it will be the first item to be returned. If as_edge is True, the source of the first edge is None - ``as_edge`` + :param as_edge: if True, return a pair of items, first being the source, second the destinatination, i.e. tuple(src, dest) with the edge spanning from source to destination""" diff --git a/lib/git/refs.py b/lib/git/refs.py index 6c57eb07..8258ca8d 100644 --- a/lib/git/refs.py +++ b/lib/git/refs.py @@ -6,956 +6,878 @@ """ Module containing all ref based objects """ import os -from objects import Object, Commit +from objects import ( + Object, + Commit + ) from objects.utils import get_object_type_by_name -from utils import LazyMixin, Iterable, join_path, join_path_native, to_native_path_linux +from utils import ( + LazyMixin, + Iterable, + join_path, + join_path_native, + to_native_path_linux + ) +from gitdb.util import ( + join, + dirname, + isdir, + exists, + isfile, + rename, + hex_to_bin + ) + + +__all__ = ("SymbolicReference", "Reference", "HEAD", "Head", "TagReference", + "RemoteReference", "Tag" ) class SymbolicReference(object): - """ - Represents a special case of a reference such that this reference is symbolic. - It does not point to a specific commit, but to another Head, which itself - specifies a commit. - - A typical example for a symbolic reference is HEAD. - """ - __slots__ = ("repo", "path") - _common_path_default = "" - _id_attribute_ = "name" - - def __init__(self, repo, path): - self.repo = repo - self.path = path - - def __str__(self): - return self.path - - def __repr__(self): - return '<git.%s "%s">' % (self.__class__.__name__, self.path) - - def __eq__(self, other): - return self.path == other.path - - def __ne__(self, other): - return not ( self == other ) - - def __hash__(self): - return hash(self.path) - - @property - def name(self): - """ - Returns - In case of symbolic references, the shortest assumable name - is the path itself. - """ - return self.path - - def _abs_path(self): - return join_path_native(self.repo.git_dir, self.path) - - @classmethod - def _get_packed_refs_path(cls, repo): - return os.path.join(repo.git_dir, 'packed-refs') - - @classmethod - def _iter_packed_refs(cls, repo): - """Returns an iterator yielding pairs of sha1/path pairs for the corresponding - refs. - NOTE: The packed refs file will be kept open as long as we iterate""" - try: - fp = open(cls._get_packed_refs_path(repo), 'r') - for line in fp: - line = line.strip() - if not line: - continue - if line.startswith('#'): - if line.startswith('# pack-refs with:') and not line.endswith('peeled'): - raise TypeError("PackingType of packed-Refs not understood: %r" % line) - # END abort if we do not understand the packing scheme - continue - # END parse comment - - # skip dereferenced tag object entries - previous line was actual - # tag reference for it - if line[0] == '^': - continue - - yield tuple(line.split(' ', 1)) - # END for each line - except (OSError,IOError): - raise StopIteration - # END no packed-refs file handling - # NOTE: Had try-finally block around here to close the fp, - # but some python version woudn't allow yields within that. - # I believe files are closing themselves on destruction, so it is - # alright. - - def _get_ref_info(self): - """Return: (sha, target_ref_path) if available, the sha the file at - rela_path points to, or None. target_ref_path is the reference we - point to, or None""" - tokens = None - try: - fp = open(self._abs_path(), 'r') - value = fp.read().rstrip() - fp.close() - tokens = value.split(" ") - except (OSError,IOError): - # Probably we are just packed, find our entry in the packed refs file - # NOTE: We are not a symbolic ref if we are in a packed file, as these - # are excluded explictly - for sha, path in self._iter_packed_refs(self.repo): - if path != self.path: continue - tokens = (sha, path) - break - # END for each packed ref - # END handle packed refs - - if tokens is None: - raise ValueError("Reference at %r does not exist" % self.path) - - # is it a reference ? - if tokens[0] == 'ref:': - return (None, tokens[1]) - - # its a commit - if self.repo.re_hexsha_only.match(tokens[0]): - return (tokens[0], None) - - raise ValueError("Failed to parse reference information from %r" % self.path) - - def _get_commit(self): - """ - Returns: - Commit object we point to, works for detached and non-detached - SymbolicReferences - """ - # we partially reimplement it to prevent unnecessary file access - sha, target_ref_path = self._get_ref_info() - - # it is a detached reference - if sha: - return Commit(self.repo, sha) - - return self.from_path(self.repo, target_ref_path).commit - - def _set_commit(self, commit): - """ - Set our commit, possibly dereference our symbolic reference first. - If the reference does not exist, it will be created - """ - is_detached = True - try: - is_detached = self.is_detached - except ValueError: - pass - # END handle non-existing ones - if is_detached: - return self._set_reference(commit) - - # set the commit on our reference - self._get_reference().commit = commit - - commit = property(_get_commit, _set_commit, doc="Query or set commits directly") - - def _get_reference(self): - """ - Returns - Reference Object we point to - """ - sha, target_ref_path = self._get_ref_info() - if target_ref_path is None: - raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha)) - return self.from_path(self.repo, target_ref_path) - - def _set_reference(self, ref): - """ - Set ourselves to the given ref. It will stay a symbol if the ref is a Reference. - Otherwise we try to get a commit from it using our interface. - - Strings are allowed but will be checked to be sure we have a commit - """ - write_value = None - if isinstance(ref, SymbolicReference): - write_value = "ref: %s" % ref.path - elif isinstance(ref, Commit): - write_value = ref.sha - else: - try: - write_value = ref.commit.sha - except AttributeError: - sha = str(ref) - try: - obj = Object.new(self.repo, sha) - if obj.type != "commit": - raise TypeError("Invalid object type behind sha: %s" % sha) - write_value = obj.sha - except Exception: - raise ValueError("Could not extract object from %s" % ref) - # END end try string - # END try commit attribute - - # if we are writing a ref, use symbolic ref to get the reflog and more - # checking - # Otherwise we detach it and have to do it manually - if write_value.startswith('ref:'): - self.repo.git.symbolic_ref(self.path, write_value[5:]) - return - # END non-detached handling - - path = self._abs_path() - directory = os.path.dirname(path) - if not os.path.isdir(directory): - os.makedirs(directory) - - fp = open(path, "wb") - try: - fp.write(write_value) - finally: - fp.close() - # END writing - - reference = property(_get_reference, _set_reference, doc="Returns the Reference we point to") - - # alias - ref = reference - - def is_valid(self): - """ - Returns - True if the reference is valid, hence it can be read and points to - a valid object or reference. - """ - try: - self.commit - except (OSError, ValueError): - return False - else: - return True - - @property - def is_detached(self): - """ - Returns - True if we are a detached reference, hence we point to a specific commit - instead to another reference - """ - try: - self.reference - return False - except TypeError: - return True - + """Represents a special case of a reference such that this reference is symbolic. + It does not point to a specific commit, but to another Head, which itself + specifies a commit. + + A typical example for a symbolic reference is HEAD.""" + __slots__ = ("repo", "path") + _common_path_default = "" + _id_attribute_ = "name" + + def __init__(self, repo, path): + self.repo = repo + self.path = path + + def __str__(self): + return self.path + + def __repr__(self): + return '<git.%s "%s">' % (self.__class__.__name__, self.path) + + def __eq__(self, other): + return self.path == other.path + + def __ne__(self, other): + return not ( self == other ) + + def __hash__(self): + return hash(self.path) + + @property + def name(self): + """:return: + In case of symbolic references, the shortest assumable name + is the path itself.""" + return self.path + + def _abs_path(self): + return join_path_native(self.repo.git_dir, self.path) + + @classmethod + def _get_packed_refs_path(cls, repo): + return join(repo.git_dir, 'packed-refs') + + @classmethod + def _iter_packed_refs(cls, repo): + """Returns an iterator yielding pairs of sha1/path pairs for the corresponding refs. + :note: The packed refs file will be kept open as long as we iterate""" + try: + fp = open(cls._get_packed_refs_path(repo), 'r') + for line in fp: + line = line.strip() + if not line: + continue + if line.startswith('#'): + if line.startswith('# pack-refs with:') and not line.endswith('peeled'): + raise TypeError("PackingType of packed-Refs not understood: %r" % line) + # END abort if we do not understand the packing scheme + continue + # END parse comment + + # skip dereferenced tag object entries - previous line was actual + # tag reference for it + if line[0] == '^': + continue + + yield tuple(line.split(' ', 1)) + # END for each line + except (OSError,IOError): + raise StopIteration + # END no packed-refs file handling + # NOTE: Had try-finally block around here to close the fp, + # but some python version woudn't allow yields within that. + # I believe files are closing themselves on destruction, so it is + # alright. + + def _get_ref_info(self): + """Return: (sha, target_ref_path) if available, the sha the file at + rela_path points to, or None. target_ref_path is the reference we + point to, or None""" + tokens = None + try: + fp = open(self._abs_path(), 'r') + value = fp.read().rstrip() + fp.close() + tokens = value.split(" ") + except (OSError,IOError): + # Probably we are just packed, find our entry in the packed refs file + # NOTE: We are not a symbolic ref if we are in a packed file, as these + # are excluded explictly + for sha, path in self._iter_packed_refs(self.repo): + if path != self.path: continue + tokens = (sha, path) + break + # END for each packed ref + # END handle packed refs + + if tokens is None: + raise ValueError("Reference at %r does not exist" % self.path) + + # is it a reference ? + if tokens[0] == 'ref:': + return (None, tokens[1]) + + # its a commit + if self.repo.re_hexsha_only.match(tokens[0]): + return (tokens[0], None) + + raise ValueError("Failed to parse reference information from %r" % self.path) + + def _get_commit(self): + """ + :return: + Commit object we point to, works for detached and non-detached + SymbolicReferences""" + # we partially reimplement it to prevent unnecessary file access + hexsha, target_ref_path = self._get_ref_info() + + # it is a detached reference + if hexsha: + return Commit(self.repo, hex_to_bin(hexsha)) + + return self.from_path(self.repo, target_ref_path).commit + + def _set_commit(self, commit): + """Set our commit, possibly dereference our symbolic reference first. + If the reference does not exist, it will be created""" + is_detached = True + try: + is_detached = self.is_detached + except ValueError: + pass + # END handle non-existing ones + if is_detached: + return self._set_reference(commit) + + # set the commit on our reference + self._get_reference().commit = commit + + commit = property(_get_commit, _set_commit, doc="Query or set commits directly") + + def _get_reference(self): + """:return: Reference Object we point to""" + sha, target_ref_path = self._get_ref_info() + if target_ref_path is None: + raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha)) + return self.from_path(self.repo, target_ref_path) + + def _set_reference(self, ref): + """Set ourselves to the given ref. It will stay a symbol if the ref is a Reference. + Otherwise we try to get a commit from it using our interface. + + Strings are allowed but will be checked to be sure we have a commit""" + write_value = None + if isinstance(ref, SymbolicReference): + write_value = "ref: %s" % ref.path + elif isinstance(ref, Commit): + write_value = ref.hexsha + else: + try: + write_value = ref.commit.hexsha + except AttributeError: + sha = str(ref) + try: + obj = Object.new(self.repo, sha) + if obj.type != "commit": + raise TypeError("Invalid object type behind sha: %s" % sha) + write_value = obj.hexsha + except Exception: + raise ValueError("Could not extract object from %s" % ref) + # END end try string + # END try commit attribute + + # if we are writing a ref, use symbolic ref to get the reflog and more + # checking + # Otherwise we detach it and have to do it manually + if write_value.startswith('ref:'): + self.repo.git.symbolic_ref(self.path, write_value[5:]) + return + # END non-detached handling + + path = self._abs_path() + directory = dirname(path) + if not isdir(directory): + os.makedirs(directory) + + fp = open(path, "wb") + try: + fp.write(write_value) + finally: + fp.close() + # END writing + + reference = property(_get_reference, _set_reference, doc="Returns the Reference we point to") + + # alias + ref = reference + + def is_valid(self): + """ + :return: + True if the reference is valid, hence it can be read and points to + a valid object or reference.""" + try: + self.commit + except (OSError, ValueError): + return False + else: + return True + + @property + def is_detached(self): + """:return: + True if we are a detached reference, hence we point to a specific commit + instead to another reference""" + try: + self.reference + return False + except TypeError: + return True + - @classmethod - def to_full_path(cls, path): - """:return: string with a full path name which can be used to initialize - a Reference instance, for instance by using ``Reference.from_path``""" - if isinstance(path, SymbolicReference): - path = path.path - full_ref_path = path - if not cls._common_path_default: - return full_ref_path - if not path.startswith(cls._common_path_default+"/"): - full_ref_path = '%s/%s' % (cls._common_path_default, path) - return full_ref_path - - @classmethod - def delete(cls, repo, path): - """Delete the reference at the given path - - ``repo`` - Repository to delete the reference from - - ``path`` - Short or full path pointing to the reference, i.e. refs/myreference - or just "myreference", hence 'refs/' is implied. - Alternatively the symbolic reference to be deleted - """ - full_ref_path = cls.to_full_path(path) - abs_path = os.path.join(repo.git_dir, full_ref_path) - if os.path.exists(abs_path): - os.remove(abs_path) - else: - # check packed refs - pack_file_path = cls._get_packed_refs_path(repo) - try: - reader = open(pack_file_path) - except (OSError,IOError): - pass # it didnt exist at all - else: - new_lines = list() - made_change = False - dropped_last_line = False - for line in reader: - # keep line if it is a comment or if the ref to delete is not - # in the line - # If we deleted the last line and this one is a tag-reference object, - # we drop it as well - if ( line.startswith('#') or full_ref_path not in line ) and \ - ( not dropped_last_line or dropped_last_line and not line.startswith('^') ): - new_lines.append(line) - dropped_last_line = False - continue - # END skip comments and lines without our path - - # drop this line - made_change = True - dropped_last_line = True - # END for each line in packed refs - reader.close() - - # write the new lines - if made_change: - open(pack_file_path, 'w').writelines(new_lines) - # END open exception handling - # END handle deletion - - @classmethod - def _create(cls, repo, path, resolve, reference, force): - """internal method used to create a new symbolic reference. - If resolve is False,, the reference will be taken as is, creating - a proper symbolic reference. Otherwise it will be resolved to the - corresponding object and a detached symbolic reference will be created - instead""" - full_ref_path = cls.to_full_path(path) - abs_ref_path = os.path.join(repo.git_dir, full_ref_path) - - # figure out target data - target = reference - if resolve: - target = Object.new(repo, reference) - - if not force and os.path.isfile(abs_ref_path): - target_data = str(target) - if isinstance(target, SymbolicReference): - target_data = target.path - if not resolve: - target_data = "ref: " + target_data - if open(abs_ref_path, 'rb').read().strip() != target_data: - raise OSError("Reference at %s does already exist" % full_ref_path) - # END no force handling - - ref = cls(repo, full_ref_path) - ref.reference = target - return ref - - @classmethod - def create(cls, repo, path, reference='HEAD', force=False ): - """ - Create a new symbolic reference, hence a reference pointing to another - reference. - ``repo`` - Repository to create the reference in - - ``path`` - full path at which the new symbolic reference is supposed to be - created at, i.e. "NEW_HEAD" or "symrefs/my_new_symref" - - ``reference`` - The reference to which the new symbolic reference should point to - - ``force`` - if True, force creation even if a symbolic reference with that name already exists. - Raise OSError otherwise - - Returns - Newly created symbolic Reference - - Raises OSError - If a (Symbolic)Reference with the same name but different contents - already exists. - Note - This does not alter the current HEAD, index or Working Tree - """ - return cls._create(repo, path, False, reference, force) - - def rename(self, new_path, force=False): - """ - Rename self to a new path - - ``new_path`` - Either a simple name or a full path, i.e. new_name or features/new_name. - The prefix refs/ is implied for references and will be set as needed. - In case this is a symbolic ref, there is no implied prefix - - ``force`` - If True, the rename will succeed even if a head with the target name - already exists. It will be overwritten in that case - - Returns - self - - Raises OSError: - In case a file at path but a different contents already exists - """ - new_path = self.to_full_path(new_path) - if self.path == new_path: - return self - - new_abs_path = os.path.join(self.repo.git_dir, new_path) - cur_abs_path = os.path.join(self.repo.git_dir, self.path) - if os.path.isfile(new_abs_path): - if not force: - # if they point to the same file, its not an error - if open(new_abs_path,'rb').read().strip() != open(cur_abs_path,'rb').read().strip(): - raise OSError("File at path %r already exists" % new_abs_path) - # else: we could remove ourselves and use the otherone, but - # but clarity we just continue as usual - # END not force handling - os.remove(new_abs_path) - # END handle existing target file - - dirname = os.path.dirname(new_abs_path) - if not os.path.isdir(dirname): - os.makedirs(dirname) - # END create directory - - os.rename(cur_abs_path, new_abs_path) - self.path = new_path - - return self - - @classmethod - def _iter_items(cls, repo, common_path = None): - if common_path is None: - common_path = cls._common_path_default - rela_paths = set() - - # walk loose refs - # Currently we do not follow links - for root, dirs, files in os.walk(join_path_native(repo.git_dir, common_path)): - if 'refs/' not in root: # skip non-refs subfolders - refs_id = [ i for i,d in enumerate(dirs) if d == 'refs' ] - if refs_id: - dirs[0:] = ['refs'] - # END prune non-refs folders - - for f in files: - abs_path = to_native_path_linux(join_path(root, f)) - rela_paths.add(abs_path.replace(to_native_path_linux(repo.git_dir) + '/', "")) - # END for each file in root directory - # END for each directory to walk - - # read packed refs - for sha, rela_path in cls._iter_packed_refs(repo): - if rela_path.startswith(common_path): - rela_paths.add(rela_path) - # END relative path matches common path - # END packed refs reading - - # return paths in sorted order - for path in sorted(rela_paths): - try: - yield cls.from_path(repo, path) - except ValueError: - continue - # END for each sorted relative refpath - - @classmethod - def iter_items(cls, repo, common_path = None): - """ - Find all refs in the repository + @classmethod + def to_full_path(cls, path): + """:return: string with a full path name which can be used to initialize + a Reference instance, for instance by using ``Reference.from_path``""" + if isinstance(path, SymbolicReference): + path = path.path + full_ref_path = path + if not cls._common_path_default: + return full_ref_path + if not path.startswith(cls._common_path_default+"/"): + full_ref_path = '%s/%s' % (cls._common_path_default, path) + return full_ref_path + + @classmethod + def delete(cls, repo, path): + """Delete the reference at the given path + + :param repo: + Repository to delete the reference from + + :param path: + Short or full path pointing to the reference, i.e. refs/myreference + or just "myreference", hence 'refs/' is implied. + Alternatively the symbolic reference to be deleted""" + full_ref_path = cls.to_full_path(path) + abs_path = join(repo.git_dir, full_ref_path) + if exists(abs_path): + os.remove(abs_path) + else: + # check packed refs + pack_file_path = cls._get_packed_refs_path(repo) + try: + reader = open(pack_file_path) + except (OSError,IOError): + pass # it didnt exist at all + else: + new_lines = list() + made_change = False + dropped_last_line = False + for line in reader: + # keep line if it is a comment or if the ref to delete is not + # in the line + # If we deleted the last line and this one is a tag-reference object, + # we drop it as well + if ( line.startswith('#') or full_ref_path not in line ) and \ + ( not dropped_last_line or dropped_last_line and not line.startswith('^') ): + new_lines.append(line) + dropped_last_line = False + continue + # END skip comments and lines without our path + + # drop this line + made_change = True + dropped_last_line = True + # END for each line in packed refs + reader.close() + + # write the new lines + if made_change: + open(pack_file_path, 'w').writelines(new_lines) + # END open exception handling + # END handle deletion + + @classmethod + def _create(cls, repo, path, resolve, reference, force): + """internal method used to create a new symbolic reference. + If resolve is False,, the reference will be taken as is, creating + a proper symbolic reference. Otherwise it will be resolved to the + corresponding object and a detached symbolic reference will be created + instead""" + full_ref_path = cls.to_full_path(path) + abs_ref_path = join(repo.git_dir, full_ref_path) + + # figure out target data + target = reference + if resolve: + target = Object.new(repo, reference) + + if not force and isfile(abs_ref_path): + target_data = str(target) + if isinstance(target, SymbolicReference): + target_data = target.path + if not resolve: + target_data = "ref: " + target_data + if open(abs_ref_path, 'rb').read().strip() != target_data: + raise OSError("Reference at %s does already exist" % full_ref_path) + # END no force handling + + ref = cls(repo, full_ref_path) + ref.reference = target + return ref + + @classmethod + def create(cls, repo, path, reference='HEAD', force=False ): + """Create a new symbolic reference, hence a reference pointing to another reference. + + :param repo: + Repository to create the reference in + + :param path: + full path at which the new symbolic reference is supposed to be + created at, i.e. "NEW_HEAD" or "symrefs/my_new_symref" + + :param reference: + The reference to which the new symbolic reference should point to + + :param force: + if True, force creation even if a symbolic reference with that name already exists. + Raise OSError otherwise + + :return: Newly created symbolic Reference + + :raise OSError: + If a (Symbolic)Reference with the same name but different contents + already exists. + :note: This does not alter the current HEAD, index or Working Tree""" + return cls._create(repo, path, False, reference, force) + + def rename(self, new_path, force=False): + """Rename self to a new path + + :param new_path: + Either a simple name or a full path, i.e. new_name or features/new_name. + The prefix refs/ is implied for references and will be set as needed. + In case this is a symbolic ref, there is no implied prefix + + :param force: + If True, the rename will succeed even if a head with the target name + already exists. It will be overwritten in that case + + :return: self + :raise OSError: In case a file at path but a different contents already exists """ + new_path = self.to_full_path(new_path) + if self.path == new_path: + return self + + new_abs_path = join(self.repo.git_dir, new_path) + cur_abs_path = join(self.repo.git_dir, self.path) + if isfile(new_abs_path): + if not force: + # if they point to the same file, its not an error + if open(new_abs_path,'rb').read().strip() != open(cur_abs_path,'rb').read().strip(): + raise OSError("File at path %r already exists" % new_abs_path) + # else: we could remove ourselves and use the otherone, but + # but clarity we just continue as usual + # END not force handling + os.remove(new_abs_path) + # END handle existing target file + + dname = dirname(new_abs_path) + if not isdir(dname): + os.makedirs(dname) + # END create directory + + rename(cur_abs_path, new_abs_path) + self.path = new_path + + return self + + @classmethod + def _iter_items(cls, repo, common_path = None): + if common_path is None: + common_path = cls._common_path_default + rela_paths = set() + + # walk loose refs + # Currently we do not follow links + for root, dirs, files in os.walk(join_path_native(repo.git_dir, common_path)): + if 'refs/' not in root: # skip non-refs subfolders + refs_id = [ i for i,d in enumerate(dirs) if d == 'refs' ] + if refs_id: + dirs[0:] = ['refs'] + # END prune non-refs folders + + for f in files: + abs_path = to_native_path_linux(join_path(root, f)) + rela_paths.add(abs_path.replace(to_native_path_linux(repo.git_dir) + '/', "")) + # END for each file in root directory + # END for each directory to walk + + # read packed refs + for sha, rela_path in cls._iter_packed_refs(repo): + if rela_path.startswith(common_path): + rela_paths.add(rela_path) + # END relative path matches common path + # END packed refs reading + + # return paths in sorted order + for path in sorted(rela_paths): + try: + yield cls.from_path(repo, path) + except ValueError: + continue + # END for each sorted relative refpath + + @classmethod + def iter_items(cls, repo, common_path = None): + """Find all refs in the repository - ``repo`` - is the Repo + :param repo: is the Repo - ``common_path`` - Optional keyword argument to the path which is to be shared by all - returned Ref objects. - Defaults to class specific portion if None assuring that only - refs suitable for the actual class are returned. + :param common_path: + Optional keyword argument to the path which is to be shared by all + returned Ref objects. + Defaults to class specific portion if None assuring that only + refs suitable for the actual class are returned. - Returns - git.SymbolicReference[], each of them is guaranteed to be a symbolic - ref which is not detached. - - List is lexigraphically sorted - The returned objects represent actual subclasses, such as Head or TagReference - """ - return ( r for r in cls._iter_items(repo, common_path) if r.__class__ == SymbolicReference or not r.is_detached ) - - @classmethod - def from_path(cls, repo, path): - """ - Return - Instance of type Reference, Head, or Tag - depending on the given path - """ - if not path: - raise ValueError("Cannot create Reference from %r" % path) - - for ref_type in (HEAD, Head, RemoteReference, TagReference, Reference, SymbolicReference): - try: - instance = ref_type(repo, path) - if instance.__class__ == SymbolicReference and instance.is_detached: - raise ValueError("SymbolRef was detached, we drop it") - return instance - except ValueError: - pass - # END exception handling - # END for each type to try - raise ValueError("Could not find reference type suitable to handle path %r" % path) - + :return: + git.SymbolicReference[], each of them is guaranteed to be a symbolic + ref which is not detached. + + List is lexigraphically sorted + The returned objects represent actual subclasses, such as Head or TagReference""" + return ( r for r in cls._iter_items(repo, common_path) if r.__class__ == SymbolicReference or not r.is_detached ) + + @classmethod + def from_path(cls, repo, path): + """ + :return: + Instance of type Reference, Head, or Tag + depending on the given path""" + if not path: + raise ValueError("Cannot create Reference from %r" % path) + + for ref_type in (HEAD, Head, RemoteReference, TagReference, Reference, SymbolicReference): + try: + instance = ref_type(repo, path) + if instance.__class__ == SymbolicReference and instance.is_detached: + raise ValueError("SymbolRef was detached, we drop it") + return instance + except ValueError: + pass + # END exception handling + # END for each type to try + raise ValueError("Could not find reference type suitable to handle path %r" % path) + class Reference(SymbolicReference, LazyMixin, Iterable): - """ - Represents a named reference to any object. Subclasses may apply restrictions though, - i.e. Heads can only point to commits. - """ - __slots__ = tuple() - _common_path_default = "refs" - - def __init__(self, repo, path): - """ - Initialize this instance - ``repo`` - Our parent repository - - ``path`` - Path relative to the .git/ directory pointing to the ref in question, i.e. - refs/heads/master - - """ - if not path.startswith(self._common_path_default+'/'): - raise ValueError("Cannot instantiate %r from path %s" % ( self.__class__.__name__, path )) - super(Reference, self).__init__(repo, path) - + """Represents a named reference to any object. Subclasses may apply restrictions though, + i.e. Heads can only point to commits.""" + __slots__ = tuple() + _common_path_default = "refs" + + def __init__(self, repo, path): + """Initialize this instance + :param repo: Our parent repository + + :param path: + Path relative to the .git/ directory pointing to the ref in question, i.e. + refs/heads/master""" + if not path.startswith(self._common_path_default+'/'): + raise ValueError("Cannot instantiate %r from path %s" % ( self.__class__.__name__, path )) + super(Reference, self).__init__(repo, path) + - def __str__(self): - return self.name + def __str__(self): + return self.name - def _get_object(self): - """ - Returns - The object our ref currently refers to. Refs can be cached, they will - always point to the actual object as it gets re-created on each query - """ - # have to be dynamic here as we may be a tag which can point to anything - # Our path will be resolved to the hexsha which will be used accordingly - return Object.new(self.repo, self.path) - - def _set_object(self, ref): - """ - Set our reference to point to the given ref. It will be converted - to a specific hexsha. - If the reference does not exist, it will be created. - - Note: - TypeChecking is done by the git command - """ - # check for existence, touch it if required - abs_path = self._abs_path() - existed = True - if not os.path.isfile(abs_path): - existed = False - open(abs_path, 'wb').write(Object.NULL_HEX_SHA) - # END quick create - - # do it safely by specifying the old value - try: - self.repo.git.update_ref(self.path, ref, (existed and self._get_object().sha) or None) - except: - if not existed: - os.remove(abs_path) - # END remove file on error if it didn't exist before - raise - # END exception handling - - object = property(_get_object, _set_object, doc="Return the object our ref currently refers to") - - @property - def name(self): - """ - Returns - (shortest) Name of this reference - it may contain path components - """ - # first two path tokens are can be removed as they are - # refs/heads or refs/tags or refs/remotes - tokens = self.path.split('/') - if len(tokens) < 3: - return self.path # could be refs/HEAD - return '/'.join(tokens[2:]) - - - @classmethod - def create(cls, repo, path, commit='HEAD', force=False ): - """ - Create a new reference. - ``repo`` - Repository to create the reference in - - ``path`` - The relative path of the reference, i.e. 'new_branch' or - feature/feature1. The path prefix 'refs/' is implied if not - given explicitly - - ``commit`` - Commit to which the new reference should point, defaults to the - current HEAD - - ``force`` - if True, force creation even if a reference with that name already exists. - Raise OSError otherwise - - Returns - Newly created Reference - - Note - This does not alter the current HEAD, index or Working Tree - """ - return cls._create(repo, path, True, commit, force) - - @classmethod - def iter_items(cls, repo, common_path = None): - """ - Equivalent to SymbolicReference.iter_items, but will return non-detached - references as well. - """ - return cls._iter_items(repo, common_path) - - + def _get_object(self): + """ + :return: + The object our ref currently refers to. Refs can be cached, they will + always point to the actual object as it gets re-created on each query""" + # have to be dynamic here as we may be a tag which can point to anything + # Our path will be resolved to the hexsha which will be used accordingly + return Object.new(self.repo, self.path) + + def _set_object(self, ref): + """ + Set our reference to point to the given ref. It will be converted + to a specific hexsha. + If the reference does not exist, it will be created. + + :note: + TypeChecking is done by the git command""" + # check for existence, touch it if required + abs_path = self._abs_path() + existed = True + if not isfile(abs_path): + existed = False + open(abs_path, 'wb').write(Object.NULL_HEX_SHA) + # END quick create + + # do it safely by specifying the old value + try: + self.repo.git.update_ref(self.path, ref, (existed and self._get_object().hexsha) or None) + except: + if not existed: + os.remove(abs_path) + # END remove file on error if it didn't exist before + raise + # END exception handling + + object = property(_get_object, _set_object, doc="Return the object our ref currently refers to") + + @property + def name(self): + """:return: (shortest) Name of this reference - it may contain path components""" + # first two path tokens are can be removed as they are + # refs/heads or refs/tags or refs/remotes + tokens = self.path.split('/') + if len(tokens) < 3: + return self.path # could be refs/HEAD + return '/'.join(tokens[2:]) + + + @classmethod + def create(cls, repo, path, commit='HEAD', force=False ): + """Create a new reference. + :param repo: Repository to create the reference in + :param path: + The relative path of the reference, i.e. 'new_branch' or + feature/feature1. The path prefix 'refs/' is implied if not + given explicitly + :param commit: + Commit to which the new reference should point, defaults to the + current HEAD + :param force: + if True, force creation even if a reference with that name already exists. + Raise OSError otherwise + :return: Newly created Reference + + :note: This does not alter the current HEAD, index or Working Tree""" + return cls._create(repo, path, True, commit, force) + + @classmethod + def iter_items(cls, repo, common_path = None): + """Equivalent to SymbolicReference.iter_items, but will return non-detached + references as well.""" + return cls._iter_items(repo, common_path) + + class HEAD(SymbolicReference): - """ - Special case of a Symbolic Reference as it represents the repository's - HEAD reference. - """ - _HEAD_NAME = 'HEAD' - __slots__ = tuple() - - def __init__(self, repo, path=_HEAD_NAME): - if path != self._HEAD_NAME: - raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path)) - super(HEAD, self).__init__(repo, path) - - - def reset(self, commit='HEAD', index=True, working_tree = False, - paths=None, **kwargs): - """ - Reset our HEAD to the given commit optionally synchronizing - the index and working tree. The reference we refer to will be set to - commit as well. - - ``commit`` - Commit object, Reference Object or string identifying a revision we - should reset HEAD to. - - ``index`` - If True, the index will be set to match the given commit. Otherwise - it will not be touched. - - ``working_tree`` - If True, the working tree will be forcefully adjusted to match the given - commit, possibly overwriting uncommitted changes without warning. - If working_tree is True, index must be true as well - - ``paths`` - Single path or list of paths relative to the git root directory - that are to be reset. This allow to partially reset individual files. - - ``kwargs`` - Additional arguments passed to git-reset. - - Returns - self - """ - mode = "--soft" - if index: - mode = "--mixed" - - if working_tree: - mode = "--hard" - if not index: - raise ValueError( "Cannot reset the working tree if the index is not reset as well") - # END working tree handling - - self.repo.git.reset(mode, commit, paths, **kwargs) - - return self - + """Special case of a Symbolic Reference as it represents the repository's + HEAD reference.""" + _HEAD_NAME = 'HEAD' + __slots__ = tuple() + + def __init__(self, repo, path=_HEAD_NAME): + if path != self._HEAD_NAME: + raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path)) + super(HEAD, self).__init__(repo, path) + + + def reset(self, commit='HEAD', index=True, working_tree = False, + paths=None, **kwargs): + """Reset our HEAD to the given commit optionally synchronizing + the index and working tree. The reference we refer to will be set to + commit as well. + + :param commit: + Commit object, Reference Object or string identifying a revision we + should reset HEAD to. + + :param index: + If True, the index will be set to match the given commit. Otherwise + it will not be touched. + + :param working_tree: + If True, the working tree will be forcefully adjusted to match the given + commit, possibly overwriting uncommitted changes without warning. + If working_tree is True, index must be true as well + + :param paths: + Single path or list of paths relative to the git root directory + that are to be reset. This allow to partially reset individual files. + + :param kwargs: + Additional arguments passed to git-reset. + + :return: self""" + mode = "--soft" + if index: + mode = "--mixed" + + if working_tree: + mode = "--hard" + if not index: + raise ValueError( "Cannot reset the working tree if the index is not reset as well") + # END working tree handling + + self.repo.git.reset(mode, commit, paths, **kwargs) + + return self + class Head(Reference): - """ - A Head is a named reference to a Commit. Every Head instance contains a name - and a Commit object. + """A Head is a named reference to a Commit. Every Head instance contains a name + and a Commit object. - Examples:: + Examples:: - >>> repo = Repo("/path/to/repo") - >>> head = repo.heads[0] + >>> repo = Repo("/path/to/repo") + >>> head = repo.heads[0] - >>> head.name - 'master' + >>> head.name + 'master' - >>> head.commit - <git.Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455"> + >>> head.commit + <git.Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455"> - >>> head.commit.sha - '1c09f116cbc2cb4100fb6935bb162daa4723f455' - """ - _common_path_default = "refs/heads" - - @classmethod - def create(cls, repo, path, commit='HEAD', force=False, **kwargs ): - """ - Create a new head. - ``repo`` - Repository to create the head in - - ``path`` - The name or path of the head, i.e. 'new_branch' or - feature/feature1. The prefix refs/heads is implied. - - ``commit`` - Commit to which the new head should point, defaults to the - current HEAD - - ``force`` - if True, force creation even if branch with that name already exists. - - ``**kwargs`` - Additional keyword arguments to be passed to git-branch, i.e. - track, no-track, l - - Returns - Newly created Head - - Note - This does not alter the current HEAD, index or Working Tree - """ - if cls is not Head: - raise TypeError("Only Heads can be created explicitly, not objects of type %s" % cls.__name__) - - args = ( path, commit ) - if force: - kwargs['f'] = True - - repo.git.branch(*args, **kwargs) - return cls(repo, "%s/%s" % ( cls._common_path_default, path)) - - - @classmethod - def delete(cls, repo, *heads, **kwargs): - """ - Delete the given heads - - ``force`` - If True, the heads will be deleted even if they are not yet merged into - the main development stream. - Default False - """ - force = kwargs.get("force", False) - flag = "-d" - if force: - flag = "-D" - repo.git.branch(flag, *heads) - - - def rename(self, new_path, force=False): - """ - Rename self to a new path - - ``new_path`` - Either a simple name or a path, i.e. new_name or features/new_name. - The prefix refs/heads is implied - - ``force`` - If True, the rename will succeed even if a head with the target name - already exists. - - Returns - self - - Note - respects the ref log as git commands are used - """ - flag = "-m" - if force: - flag = "-M" - - self.repo.git.branch(flag, self, new_path) - self.path = "%s/%s" % (self._common_path_default, new_path) - return self - - def checkout(self, force=False, **kwargs): - """ - Checkout this head by setting the HEAD to this reference, by updating the index - to reflect the tree we point to and by updating the working tree to reflect - the latest index. - - The command will fail if changed working tree files would be overwritten. - - ``force`` - If True, changes to the index and the working tree will be discarded. - If False, GitCommandError will be raised in that situation. - - ``**kwargs`` - Additional keyword arguments to be passed to git checkout, i.e. - b='new_branch' to create a new branch at the given spot. - - Returns - The active branch after the checkout operation, usually self unless - a new branch has been created. - - Note - By default it is only allowed to checkout heads - everything else - will leave the HEAD detached which is allowed and possible, but remains - a special state that some tools might not be able to handle. - """ - args = list() - kwargs['f'] = force - if kwargs['f'] == False: - kwargs.pop('f') - - self.repo.git.checkout(self, **kwargs) - return self.repo.active_branch - + >>> head.commit.hexsha + '1c09f116cbc2cb4100fb6935bb162daa4723f455'""" + _common_path_default = "refs/heads" + + @classmethod + def create(cls, repo, path, commit='HEAD', force=False, **kwargs): + """Create a new head. + :param repo: Repository to create the head in + :param path: + The name or path of the head, i.e. 'new_branch' or + feature/feature1. The prefix refs/heads is implied. + :param commit: + Commit to which the new head should point, defaults to the + current HEAD + :param force: + if True, force creation even if branch with that name already exists. + + :param **kwargs: + Additional keyword arguments to be passed to git-branch, i.e. + track, no-track, l + :return: Newly created Head + :note: This does not alter the current HEAD, index or Working Tree""" + if cls is not Head: + raise TypeError("Only Heads can be created explicitly, not objects of type %s" % cls.__name__) + + args = ( path, commit ) + if force: + kwargs['f'] = True + + repo.git.branch(*args, **kwargs) + return cls(repo, "%s/%s" % ( cls._common_path_default, path)) + + + @classmethod + def delete(cls, repo, *heads, **kwargs): + """Delete the given heads + :param force: + If True, the heads will be deleted even if they are not yet merged into + the main development stream. + Default False""" + force = kwargs.get("force", False) + flag = "-d" + if force: + flag = "-D" + repo.git.branch(flag, *heads) + + + def rename(self, new_path, force=False): + """Rename self to a new path + + :param new_path: + Either a simple name or a path, i.e. new_name or features/new_name. + The prefix refs/heads is implied + + :param force: + If True, the rename will succeed even if a head with the target name + already exists. + + :return: self + :note: respects the ref log as git commands are used""" + flag = "-m" + if force: + flag = "-M" + + self.repo.git.branch(flag, self, new_path) + self.path = "%s/%s" % (self._common_path_default, new_path) + return self + + def checkout(self, force=False, **kwargs): + """Checkout this head by setting the HEAD to this reference, by updating the index + to reflect the tree we point to and by updating the working tree to reflect + the latest index. + + The command will fail if changed working tree files would be overwritten. + + :param force: + If True, changes to the index and the working tree will be discarded. + If False, GitCommandError will be raised in that situation. + + :param **kwargs: + Additional keyword arguments to be passed to git checkout, i.e. + b='new_branch' to create a new branch at the given spot. + + :return: + The active branch after the checkout operation, usually self unless + a new branch has been created. + + :note: + By default it is only allowed to checkout heads - everything else + will leave the HEAD detached which is allowed and possible, but remains + a special state that some tools might not be able to handle.""" + args = list() + kwargs['f'] = force + if kwargs['f'] == False: + kwargs.pop('f') + + self.repo.git.checkout(self, **kwargs) + return self.repo.active_branch + class TagReference(Reference): - """ - Class representing a lightweight tag reference which either points to a commit - ,a tag object or any other object. In the latter case additional information, - like the signature or the tag-creator, is available. - - This tag object will always point to a commit object, but may carray additional - information in a tag object:: - - tagref = TagReference.list_items(repo)[0] - print tagref.commit.message - if tagref.tag is not None: - print tagref.tag.message - """ - - __slots__ = tuple() - _common_path_default = "refs/tags" - - @property - def commit(self): - """ - Returns - Commit object the tag ref points to - """ - obj = self.object - if obj.type == "commit": - return obj - elif obj.type == "tag": - # it is a tag object which carries the commit as an object - we can point to anything - return obj.object - else: - raise ValueError( "Tag %s points to a Blob or Tree - have never seen that before" % self ) + """Class representing a lightweight tag reference which either points to a commit + ,a tag object or any other object. In the latter case additional information, + like the signature or the tag-creator, is available. + + This tag object will always point to a commit object, but may carray additional + information in a tag object:: + + tagref = TagReference.list_items(repo)[0] + print tagref.commit.message + if tagref.tag is not None: + print tagref.tag.message""" + + __slots__ = tuple() + _common_path_default = "refs/tags" + + @property + def commit(self): + """:return: Commit object the tag ref points to""" + obj = self.object + if obj.type == "commit": + return obj + elif obj.type == "tag": + # it is a tag object which carries the commit as an object - we can point to anything + return obj.object + else: + raise ValueError( "Tag %s points to a Blob or Tree - have never seen that before" % self ) - @property - def tag(self): - """ - Returns - Tag object this tag ref points to or None in case - we are a light weight tag - """ - obj = self.object - if obj.type == "tag": - return obj - return None - - # make object read-only - # It should be reasonably hard to adjust an existing tag - object = property(Reference._get_object) - - @classmethod - def create(cls, repo, path, ref='HEAD', message=None, force=False, **kwargs): - """ - Create a new tag reference. - - ``path`` - The name of the tag, i.e. 1.0 or releases/1.0. - The prefix refs/tags is implied - - ``ref`` - A reference to the object you want to tag. It can be a commit, tree or - blob. - - ``message`` - If not None, the message will be used in your tag object. This will also - create an additional tag object that allows to obtain that information, i.e.:: - tagref.tag.message - - ``force`` - If True, to force creation of a tag even though that tag already exists. - - ``**kwargs`` - Additional keyword arguments to be passed to git-tag - - Returns - A new TagReference - """ - args = ( path, ref ) - if message: - kwargs['m'] = message - if force: - kwargs['f'] = True - - repo.git.tag(*args, **kwargs) - return TagReference(repo, "%s/%s" % (cls._common_path_default, path)) - - @classmethod - def delete(cls, repo, *tags): - """ - Delete the given existing tag or tags - """ - repo.git.tag("-d", *tags) - - - + @property + def tag(self): + """ + :return: Tag object this tag ref points to or None in case + we are a light weight tag""" + obj = self.object + if obj.type == "tag": + return obj + return None + + # make object read-only + # It should be reasonably hard to adjust an existing tag + object = property(Reference._get_object) + + @classmethod + def create(cls, repo, path, ref='HEAD', message=None, force=False, **kwargs): + """Create a new tag reference. + + :param path: + The name of the tag, i.e. 1.0 or releases/1.0. + The prefix refs/tags is implied + + :param ref: + A reference to the object you want to tag. It can be a commit, tree or + blob. + + :param message: + If not None, the message will be used in your tag object. This will also + create an additional tag object that allows to obtain that information, i.e.:: + + tagref.tag.message + + :param force: + If True, to force creation of a tag even though that tag already exists. + + :param **kwargs: + Additional keyword arguments to be passed to git-tag + + :return: A new TagReference""" + args = ( path, ref ) + if message: + kwargs['m'] = message + if force: + kwargs['f'] = True + + repo.git.tag(*args, **kwargs) + return TagReference(repo, "%s/%s" % (cls._common_path_default, path)) + + @classmethod + def delete(cls, repo, *tags): + """Delete the given existing tag or tags""" + repo.git.tag("-d", *tags) + + + - + # provide an alias Tag = TagReference class RemoteReference(Head): - """ - Represents a reference pointing to a remote head. - """ - _common_path_default = "refs/remotes" - - @property - def remote_name(self): - """ - Returns - Name of the remote we are a reference of, such as 'origin' for a reference - named 'origin/master' - """ - tokens = self.path.split('/') - # /refs/remotes/<remote name>/<branch_name> - return tokens[2] - - @property - def remote_head(self): - """ - Returns - Name of the remote head itself, i.e. master. - - NOTE: The returned name is usually not qualified enough to uniquely identify - a branch - """ - tokens = self.path.split('/') - return '/'.join(tokens[3:]) - - @classmethod - def delete(cls, repo, *refs, **kwargs): - """ - Delete the given remote references. - - Note - kwargs are given for compatability with the base class method as we - should not narrow the signature. - """ - repo.git.branch("-d", "-r", *refs) - # the official deletion method will ignore remote symbolic refs - these - # are generally ignored in the refs/ folder. We don't though - # and delete remainders manually - for ref in refs: - try: - os.remove(os.path.join(repo.git_dir, ref.path)) - except OSError: - pass - # END for each ref + """Represents a reference pointing to a remote head.""" + _common_path_default = "refs/remotes" + + @property + def remote_name(self): + """ + :return: + Name of the remote we are a reference of, such as 'origin' for a reference + named 'origin/master'""" + tokens = self.path.split('/') + # /refs/remotes/<remote name>/<branch_name> + return tokens[2] + + @property + def remote_head(self): + """:return: Name of the remote head itself, i.e. master. + :note: The returned name is usually not qualified enough to uniquely identify + a branch""" + tokens = self.path.split('/') + return '/'.join(tokens[3:]) + + @classmethod + def delete(cls, repo, *refs, **kwargs): + """Delete the given remote references. + :note: + kwargs are given for compatability with the base class method as we + should not narrow the signature.""" + repo.git.branch("-d", "-r", *refs) + # the official deletion method will ignore remote symbolic refs - these + # are generally ignored in the refs/ folder. We don't though + # and delete remainders manually + for ref in refs: + try: + os.remove(join(repo.git_dir, ref.path)) + except OSError: + pass + # END for each ref diff --git a/lib/git/remote.py b/lib/git/remote.py index 65e07bd3..94bd285b 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -6,778 +6,708 @@ """Module implementing a remote object allowing easy access to git remotes""" from errors import GitCommandError -from git.utils import LazyMixin, Iterable, IterableList from objects import Commit -from refs import Reference, RemoteReference, SymbolicReference, TagReference +from git.utils import ( + LazyMixin, + Iterable, + IterableList + ) + +from refs import ( + Reference, + RemoteReference, + SymbolicReference, + TagReference + ) + +from gitdb.util import ( + join, + ) import re -import os + +__all__ = ('RemoteProgress', 'PushInfo', 'FetchInfo', 'Remote') class _SectionConstraint(object): - """ - Constrains a ConfigParser to only option commands which are constrained to - always use the section we have been initialized with. - - It supports all ConfigParser methods that operate on an option - """ - __slots__ = ("_config", "_section_name") - _valid_attrs_ = ("get_value", "set_value", "get", "set", "getint", "getfloat", "getboolean", "has_option") - - def __init__(self, config, section): - self._config = config - self._section_name = section - - def __getattr__(self, attr): - if attr in self._valid_attrs_: - return lambda *args, **kwargs: self._call_config(attr, *args, **kwargs) - return super(_SectionConstraint,self).__getattribute__(attr) - - def _call_config(self, method, *args, **kwargs): - """Call the configuration at the given method which must take a section name - as first argument""" - return getattr(self._config, method)(self._section_name, *args, **kwargs) - - + """Constrains a ConfigParser to only option commands which are constrained to + always use the section we have been initialized with. + + It supports all ConfigParser methods that operate on an option""" + __slots__ = ("_config", "_section_name") + _valid_attrs_ = ("get_value", "set_value", "get", "set", "getint", "getfloat", "getboolean", "has_option") + + def __init__(self, config, section): + self._config = config + self._section_name = section + + def __getattr__(self, attr): + if attr in self._valid_attrs_: + return lambda *args, **kwargs: self._call_config(attr, *args, **kwargs) + return super(_SectionConstraint,self).__getattribute__(attr) + + def _call_config(self, method, *args, **kwargs): + """Call the configuration at the given method which must take a section name + as first argument""" + return getattr(self._config, method)(self._section_name, *args, **kwargs) + + class RemoteProgress(object): - """ - Handler providing an interface to parse progress information emitted by git-push - and git-fetch and to dispatch callbacks allowing subclasses to react to the progress. - """ - BEGIN, END, COUNTING, COMPRESSING, WRITING = [ 1 << x for x in range(5) ] - STAGE_MASK = BEGIN|END - OP_MASK = COUNTING|COMPRESSING|WRITING - - __slots__ = ("_cur_line", "_seen_ops") - re_op_absolute = re.compile("(remote: )?([\w\s]+):\s+()(\d+)()(.*)") - re_op_relative = re.compile("(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)") - - def __init__(self): - self._seen_ops = list() - - def _parse_progress_line(self, line): - """ - Parse progress information from the given line as retrieved by git-push - or git-fetch - @return: list(line, ...) list of lines that could not be processed""" - # handle - # Counting objects: 4, done. - # Compressing objects: 50% (1/2) \rCompressing objects: 100% (2/2) \rCompressing objects: 100% (2/2), done. - self._cur_line = line - sub_lines = line.split('\r') - failed_lines = list() - for sline in sub_lines: - # find esacpe characters and cut them away - regex will not work with - # them as they are non-ascii. As git might expect a tty, it will send them - last_valid_index = None - for i,c in enumerate(reversed(sline)): - if ord(c) < 32: - # its a slice index - last_valid_index = -i-1 - # END character was non-ascii - # END for each character in sline - if last_valid_index is not None: - sline = sline[:last_valid_index] - # END cut away invalid part - sline = sline.rstrip() - - cur_count, max_count = None, None - match = self.re_op_relative.match(sline) - if match is None: - match = self.re_op_absolute.match(sline) - - if not match: - self.line_dropped(sline) - failed_lines.append(sline) - continue - # END could not get match - - op_code = 0 - remote, op_name, percent, cur_count, max_count, message = match.groups() - - # get operation id - if op_name == "Counting objects": - op_code |= self.COUNTING - elif op_name == "Compressing objects": - op_code |= self.COMPRESSING - elif op_name == "Writing objects": - op_code |= self.WRITING - else: - raise ValueError("Operation name %r unknown" % op_name) - - # figure out stage - if op_code not in self._seen_ops: - self._seen_ops.append(op_code) - op_code |= self.BEGIN - # END begin opcode - - if message is None: - message = '' - # END message handling - - message = message.strip() - done_token = ', done.' - if message.endswith(done_token): - op_code |= self.END - message = message[:-len(done_token)] - # END end message handling - - self.update(op_code, cur_count, max_count, message) - # END for each sub line - return failed_lines - - def line_dropped(self, line): - """ - Called whenever a line could not be understood and was therefore dropped. - """ - pass - - def update(self, op_code, cur_count, max_count=None, message=''): - """ - Called whenever the progress changes - - ``op_code`` - Integer allowing to be compared against Operation IDs and stage IDs. - - Stage IDs are BEGIN and END. BEGIN will only be set once for each Operation - ID as well as END. It may be that BEGIN and END are set at once in case only - one progress message was emitted due to the speed of the operation. - Between BEGIN and END, none of these flags will be set - - Operation IDs are all held within the OP_MASK. Only one Operation ID will - be active per call. - - ``cur_count`` - Current absolute count of items - - ``max_count`` - The maximum count of items we expect. It may be None in case there is - no maximum number of items or if it is (yet) unknown. - - ``message`` - In case of the 'WRITING' operation, it contains the amount of bytes - transferred. It may possibly be used for other purposes as well. - - You may read the contents of the current line in self._cur_line - """ - pass - - + """Handler providing an interface to parse progress information emitted by git-push + and git-fetch and to dispatch callbacks allowing subclasses to react to the progress.""" + BEGIN, END, COUNTING, COMPRESSING, WRITING = [ 1 << x for x in range(5) ] + STAGE_MASK = BEGIN|END + OP_MASK = COUNTING|COMPRESSING|WRITING + + __slots__ = ("_cur_line", "_seen_ops") + re_op_absolute = re.compile("(remote: )?([\w\s]+):\s+()(\d+)()(.*)") + re_op_relative = re.compile("(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)") + + def __init__(self): + self._seen_ops = list() + + def _parse_progress_line(self, line): + """Parse progress information from the given line as retrieved by git-push + or git-fetch + + :return: list(line, ...) list of lines that could not be processed""" + # handle + # Counting objects: 4, done. + # Compressing objects: 50% (1/2) \rCompressing objects: 100% (2/2) \rCompressing objects: 100% (2/2), done. + self._cur_line = line + sub_lines = line.split('\r') + failed_lines = list() + for sline in sub_lines: + # find esacpe characters and cut them away - regex will not work with + # them as they are non-ascii. As git might expect a tty, it will send them + last_valid_index = None + for i,c in enumerate(reversed(sline)): + if ord(c) < 32: + # its a slice index + last_valid_index = -i-1 + # END character was non-ascii + # END for each character in sline + if last_valid_index is not None: + sline = sline[:last_valid_index] + # END cut away invalid part + sline = sline.rstrip() + + cur_count, max_count = None, None + match = self.re_op_relative.match(sline) + if match is None: + match = self.re_op_absolute.match(sline) + + if not match: + self.line_dropped(sline) + failed_lines.append(sline) + continue + # END could not get match + + op_code = 0 + remote, op_name, percent, cur_count, max_count, message = match.groups() + + # get operation id + if op_name == "Counting objects": + op_code |= self.COUNTING + elif op_name == "Compressing objects": + op_code |= self.COMPRESSING + elif op_name == "Writing objects": + op_code |= self.WRITING + else: + raise ValueError("Operation name %r unknown" % op_name) + + # figure out stage + if op_code not in self._seen_ops: + self._seen_ops.append(op_code) + op_code |= self.BEGIN + # END begin opcode + + if message is None: + message = '' + # END message handling + + message = message.strip() + done_token = ', done.' + if message.endswith(done_token): + op_code |= self.END + message = message[:-len(done_token)] + # END end message handling + + self.update(op_code, cur_count, max_count, message) + # END for each sub line + return failed_lines + + def line_dropped(self, line): + """Called whenever a line could not be understood and was therefore dropped.""" + pass + + def update(self, op_code, cur_count, max_count=None, message=''): + """Called whenever the progress changes + + :param op_code: + Integer allowing to be compared against Operation IDs and stage IDs. + + Stage IDs are BEGIN and END. BEGIN will only be set once for each Operation + ID as well as END. It may be that BEGIN and END are set at once in case only + one progress message was emitted due to the speed of the operation. + Between BEGIN and END, none of these flags will be set + + Operation IDs are all held within the OP_MASK. Only one Operation ID will + be active per call. + :param cur_count: Current absolute count of items + + :param max_count: + The maximum count of items we expect. It may be None in case there is + no maximum number of items or if it is (yet) unknown. + + :param message: + In case of the 'WRITING' operation, it contains the amount of bytes + transferred. It may possibly be used for other purposes as well. + + You may read the contents of the current line in self._cur_line""" + pass + + class PushInfo(object): - """ - Carries information about the result of a push operation of a single head:: - - info = remote.push()[0] - info.flags # bitflags providing more information about the result - info.local_ref # Reference pointing to the local reference that was pushed - # It is None if the ref was deleted. - info.remote_ref_string # path to the remote reference located on the remote side - info.remote_ref # Remote Reference on the local side corresponding to - # the remote_ref_string. It can be a TagReference as well. - info.old_commit # commit at which the remote_ref was standing before we pushed - # it to local_ref.commit. Will be None if an error was indicated - info.summary # summary line providing human readable english text about the push - - """ - __slots__ = ('local_ref', 'remote_ref_string', 'flags', 'old_commit', '_remote', 'summary') - - NEW_TAG, NEW_HEAD, NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \ - FORCED_UPDATE, FAST_FORWARD, UP_TO_DATE, ERROR = [ 1 << x for x in range(11) ] + """Carries information about the result of a push operation of a single head:: + + info = remote.push()[0] + info.flags # bitflags providing more information about the result + info.local_ref # Reference pointing to the local reference that was pushed + # It is None if the ref was deleted. + info.remote_ref_string # path to the remote reference located on the remote side + info.remote_ref # Remote Reference on the local side corresponding to + # the remote_ref_string. It can be a TagReference as well. + info.old_commit # commit at which the remote_ref was standing before we pushed + # it to local_ref.commit. Will be None if an error was indicated + info.summary # summary line providing human readable english text about the push""" + __slots__ = ('local_ref', 'remote_ref_string', 'flags', 'old_commit', '_remote', 'summary') + + NEW_TAG, NEW_HEAD, NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \ + FORCED_UPDATE, FAST_FORWARD, UP_TO_DATE, ERROR = [ 1 << x for x in range(11) ] - _flag_map = { 'X' : NO_MATCH, '-' : DELETED, '*' : 0, - '+' : FORCED_UPDATE, ' ' : FAST_FORWARD, - '=' : UP_TO_DATE, '!' : ERROR } - - def __init__(self, flags, local_ref, remote_ref_string, remote, old_commit=None, - summary=''): - """ - Initialize a new instance - """ - self.flags = flags - self.local_ref = local_ref - self.remote_ref_string = remote_ref_string - self._remote = remote - self.old_commit = old_commit - self.summary = summary - - @property - def remote_ref(self): - """ - Returns - Remote Reference or TagReference in the local repository corresponding - to the remote_ref_string kept in this instance. - """ - # translate heads to a local remote, tags stay as they are - if self.remote_ref_string.startswith("refs/tags"): - return TagReference(self._remote.repo, self.remote_ref_string) - elif self.remote_ref_string.startswith("refs/heads"): - remote_ref = Reference(self._remote.repo, self.remote_ref_string) - return RemoteReference(self._remote.repo, "refs/remotes/%s/%s" % (str(self._remote), remote_ref.name)) - else: - raise ValueError("Could not handle remote ref: %r" % self.remote_ref_string) - # END - - @classmethod - def _from_line(cls, remote, line): - """ - Create a new PushInfo instance as parsed from line which is expected to be like - c refs/heads/master:refs/heads/master 05d2687..1d0568e - """ - control_character, from_to, summary = line.split('\t', 3) - flags = 0 - - # control character handling - try: - flags |= cls._flag_map[ control_character ] - except KeyError: - raise ValueError("Control Character %r unknown as parsed from line %r" % (control_character, line)) - # END handle control character - - # from_to handling - from_ref_string, to_ref_string = from_to.split(':') - if flags & cls.DELETED: - from_ref = None - else: - from_ref = Reference.from_path(remote.repo, from_ref_string) - - # commit handling, could be message or commit info - old_commit = None - if summary.startswith('['): - if "[rejected]" in summary: - flags |= cls.REJECTED - elif "[remote rejected]" in summary: - flags |= cls.REMOTE_REJECTED - elif "[remote failure]" in summary: - flags |= cls.REMOTE_FAILURE - elif "[no match]" in summary: - flags |= cls.ERROR - elif "[new tag]" in summary: - flags |= cls.NEW_TAG - elif "[new branch]" in summary: - flags |= cls.NEW_HEAD - # uptodate encoded in control character - else: - # fast-forward or forced update - was encoded in control character, - # but we parse the old and new commit - split_token = "..." - if control_character == " ": - split_token = ".." - old_sha, new_sha = summary.split(' ')[0].split(split_token) - old_commit = Commit(remote.repo, old_sha) - # END message handling - - return PushInfo(flags, from_ref, to_ref_string, remote, old_commit, summary) - + _flag_map = { 'X' : NO_MATCH, '-' : DELETED, '*' : 0, + '+' : FORCED_UPDATE, ' ' : FAST_FORWARD, + '=' : UP_TO_DATE, '!' : ERROR } + + def __init__(self, flags, local_ref, remote_ref_string, remote, old_commit=None, + summary=''): + """ Initialize a new instance """ + self.flags = flags + self.local_ref = local_ref + self.remote_ref_string = remote_ref_string + self._remote = remote + self.old_commit = old_commit + self.summary = summary + + @property + def remote_ref(self): + """:return: + Remote Reference or TagReference in the local repository corresponding + to the remote_ref_string kept in this instance.""" + # translate heads to a local remote, tags stay as they are + if self.remote_ref_string.startswith("refs/tags"): + return TagReference(self._remote.repo, self.remote_ref_string) + elif self.remote_ref_string.startswith("refs/heads"): + remote_ref = Reference(self._remote.repo, self.remote_ref_string) + return RemoteReference(self._remote.repo, "refs/remotes/%s/%s" % (str(self._remote), remote_ref.name)) + else: + raise ValueError("Could not handle remote ref: %r" % self.remote_ref_string) + # END + + @classmethod + def _from_line(cls, remote, line): + """Create a new PushInfo instance as parsed from line which is expected to be like + refs/heads/master:refs/heads/master 05d2687..1d0568e""" + control_character, from_to, summary = line.split('\t', 3) + flags = 0 + + # control character handling + try: + flags |= cls._flag_map[ control_character ] + except KeyError: + raise ValueError("Control Character %r unknown as parsed from line %r" % (control_character, line)) + # END handle control character + + # from_to handling + from_ref_string, to_ref_string = from_to.split(':') + if flags & cls.DELETED: + from_ref = None + else: + from_ref = Reference.from_path(remote.repo, from_ref_string) + + # commit handling, could be message or commit info + old_commit = None + if summary.startswith('['): + if "[rejected]" in summary: + flags |= cls.REJECTED + elif "[remote rejected]" in summary: + flags |= cls.REMOTE_REJECTED + elif "[remote failure]" in summary: + flags |= cls.REMOTE_FAILURE + elif "[no match]" in summary: + flags |= cls.ERROR + elif "[new tag]" in summary: + flags |= cls.NEW_TAG + elif "[new branch]" in summary: + flags |= cls.NEW_HEAD + # uptodate encoded in control character + else: + # fast-forward or forced update - was encoded in control character, + # but we parse the old and new commit + split_token = "..." + if control_character == " ": + split_token = ".." + old_sha, new_sha = summary.split(' ')[0].split(split_token) + # have to use constructor here as the sha usually is abbreviated + old_commit = remote.repo.commit(old_sha) + # END message handling + + return PushInfo(flags, from_ref, to_ref_string, remote, old_commit, summary) + class FetchInfo(object): - """ - Carries information about the results of a fetch operation of a single head:: - - info = remote.fetch()[0] - info.ref # Symbolic Reference or RemoteReference to the changed - # remote head or FETCH_HEAD - info.flags # additional flags to be & with enumeration members, - # i.e. info.flags & info.REJECTED - # is 0 if ref is SymbolicReference - info.note # additional notes given by git-fetch intended for the user - info.old_commit # if info.flags & info.FORCED_UPDATE|info.FAST_FORWARD, - # field is set to the previous location of ref, otherwise None - """ - __slots__ = ('ref','old_commit', 'flags', 'note') - - NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \ - FAST_FORWARD, ERROR = [ 1 << x for x in range(8) ] - - # %c %-*s %-*s -> %s (%s) - re_fetch_result = re.compile("^\s*(.) (\[?[\w\s\.]+\]?)\s+(.+) -> ([/\w_\.-]+)( \(.*\)?$)?") - - _flag_map = { '!' : ERROR, '+' : FORCED_UPDATE, '-' : TAG_UPDATE, '*' : 0, - '=' : HEAD_UPTODATE, ' ' : FAST_FORWARD } - - def __init__(self, ref, flags, note = '', old_commit = None): - """ - Initialize a new instance - """ - self.ref = ref - self.flags = flags - self.note = note - self.old_commit = old_commit - - def __str__(self): - return self.name - - @property - def name(self): - """ - Returns - Name of our remote ref - """ - return self.ref.name - - @property - def commit(self): - """ - Returns - Commit of our remote ref - """ - return self.ref.commit - - @classmethod - def _from_line(cls, repo, line, fetch_line): - """ - Parse information from the given line as returned by git-fetch -v - and return a new FetchInfo object representing this information. - - We can handle a line as follows - "%c %-*s %-*s -> %s%s" - - Where c is either ' ', !, +, -, *, or = - ! means error - + means success forcing update - - means a tag was updated - * means birth of new branch or tag - = means the head was up to date ( and not moved ) - ' ' means a fast-forward - - fetch line is the corresponding line from FETCH_HEAD, like - acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo - """ - match = cls.re_fetch_result.match(line) - if match is None: - raise ValueError("Failed to parse line: %r" % line) - - # parse lines - control_character, operation, local_remote_ref, remote_local_ref, note = match.groups() - try: - new_hex_sha, fetch_operation, fetch_note = fetch_line.split("\t") - ref_type_name, fetch_note = fetch_note.split(' ', 1) - except ValueError: # unpack error - raise ValueError("Failed to parse FETCH__HEAD line: %r" % fetch_line) - - # handle FETCH_HEAD and figure out ref type - # If we do not specify a target branch like master:refs/remotes/origin/master, - # the fetch result is stored in FETCH_HEAD which destroys the rule we usually - # have. In that case we use a symbolic reference which is detached - ref_type = None - if remote_local_ref == "FETCH_HEAD": - ref_type = SymbolicReference - elif ref_type_name == "branch": - ref_type = RemoteReference - elif ref_type_name == "tag": - ref_type = TagReference - else: - raise TypeError("Cannot handle reference type: %r" % ref_type_name) - - # create ref instance - if ref_type is SymbolicReference: - remote_local_ref = ref_type(repo, "FETCH_HEAD") - else: - remote_local_ref = Reference.from_path(repo, os.path.join(ref_type._common_path_default, remote_local_ref.strip())) - # END create ref instance - - note = ( note and note.strip() ) or '' - - # parse flags from control_character - flags = 0 - try: - flags |= cls._flag_map[control_character] - except KeyError: - raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) - # END control char exception hanlding - - # parse operation string for more info - makes no sense for symbolic refs - old_commit = None - if isinstance(remote_local_ref, Reference): - if 'rejected' in operation: - flags |= cls.REJECTED - if 'new tag' in operation: - flags |= cls.NEW_TAG - if 'new branch' in operation: - flags |= cls.NEW_HEAD - if '...' in operation or '..' in operation: - split_token = '...' - if control_character == ' ': - split_token = split_token[:-1] - old_commit = Commit(repo, operation.split(split_token)[0]) - # END handle refspec - # END reference flag handling - - return cls(remote_local_ref, flags, note, old_commit) - + """Carries information about the results of a fetch operation of a single head:: + + info = remote.fetch()[0] + info.ref # Symbolic Reference or RemoteReference to the changed + # remote head or FETCH_HEAD + info.flags # additional flags to be & with enumeration members, + # i.e. info.flags & info.REJECTED + # is 0 if ref is SymbolicReference + info.note # additional notes given by git-fetch intended for the user + info.old_commit # if info.flags & info.FORCED_UPDATE|info.FAST_FORWARD, + # field is set to the previous location of ref, otherwise None""" + __slots__ = ('ref','old_commit', 'flags', 'note') + + NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \ + FAST_FORWARD, ERROR = [ 1 << x for x in range(8) ] + + # %c %-*s %-*s -> %s (%s) + re_fetch_result = re.compile("^\s*(.) (\[?[\w\s\.]+\]?)\s+(.+) -> ([/\w_\.-]+)( \(.*\)?$)?") + + _flag_map = { '!' : ERROR, '+' : FORCED_UPDATE, '-' : TAG_UPDATE, '*' : 0, + '=' : HEAD_UPTODATE, ' ' : FAST_FORWARD } + + def __init__(self, ref, flags, note = '', old_commit = None): + """ + Initialize a new instance + """ + self.ref = ref + self.flags = flags + self.note = note + self.old_commit = old_commit + + def __str__(self): + return self.name + + @property + def name(self): + """:return: Name of our remote ref""" + return self.ref.name + + @property + def commit(self): + """:return: Commit of our remote ref""" + return self.ref.commit + + @classmethod + def _from_line(cls, repo, line, fetch_line): + """Parse information from the given line as returned by git-fetch -v + and return a new FetchInfo object representing this information. + + We can handle a line as follows + "%c %-*s %-*s -> %s%s" + + Where c is either ' ', !, +, -, *, or = + ! means error + + means success forcing update + - means a tag was updated + * means birth of new branch or tag + = means the head was up to date ( and not moved ) + ' ' means a fast-forward + + fetch line is the corresponding line from FETCH_HEAD, like + acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo""" + match = cls.re_fetch_result.match(line) + if match is None: + raise ValueError("Failed to parse line: %r" % line) + + # parse lines + control_character, operation, local_remote_ref, remote_local_ref, note = match.groups() + try: + new_hex_sha, fetch_operation, fetch_note = fetch_line.split("\t") + ref_type_name, fetch_note = fetch_note.split(' ', 1) + except ValueError: # unpack error + raise ValueError("Failed to parse FETCH__HEAD line: %r" % fetch_line) + + # handle FETCH_HEAD and figure out ref type + # If we do not specify a target branch like master:refs/remotes/origin/master, + # the fetch result is stored in FETCH_HEAD which destroys the rule we usually + # have. In that case we use a symbolic reference which is detached + ref_type = None + if remote_local_ref == "FETCH_HEAD": + ref_type = SymbolicReference + elif ref_type_name == "branch": + ref_type = RemoteReference + elif ref_type_name == "tag": + ref_type = TagReference + else: + raise TypeError("Cannot handle reference type: %r" % ref_type_name) + + # create ref instance + if ref_type is SymbolicReference: + remote_local_ref = ref_type(repo, "FETCH_HEAD") + else: + remote_local_ref = Reference.from_path(repo, join(ref_type._common_path_default, remote_local_ref.strip())) + # END create ref instance + + note = ( note and note.strip() ) or '' + + # parse flags from control_character + flags = 0 + try: + flags |= cls._flag_map[control_character] + except KeyError: + raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) + # END control char exception hanlding + + # parse operation string for more info - makes no sense for symbolic refs + old_commit = None + if isinstance(remote_local_ref, Reference): + if 'rejected' in operation: + flags |= cls.REJECTED + if 'new tag' in operation: + flags |= cls.NEW_TAG + if 'new branch' in operation: + flags |= cls.NEW_HEAD + if '...' in operation or '..' in operation: + split_token = '...' + if control_character == ' ': + split_token = split_token[:-1] + old_commit = Commit(repo, operation.split(split_token)[0]) + # END handle refspec + # END reference flag handling + + return cls(remote_local_ref, flags, note, old_commit) + class Remote(LazyMixin, Iterable): - """ - Provides easy read and write access to a git remote. - - Everything not part of this interface is considered an option for the current - remote, allowing constructs like remote.pushurl to query the pushurl. - - NOTE: When querying configuration, the configuration accessor will be cached - to speed up subsequent accesses. - """ - - __slots__ = ( "repo", "name", "_config_reader" ) - _id_attribute_ = "name" - - def __init__(self, repo, name): - """ - Initialize a remote instance - - ``repo`` - The repository we are a remote of - - ``name`` - the name of the remote, i.e. 'origin' - """ - self.repo = repo - self.name = name - - def __getattr__(self, attr): - """ - Allows to call this instance like - remote.special( *args, **kwargs) to call git-remote special self.name - """ - if attr == "_config_reader": - return super(Remote, self).__getattr__(attr) - - return self._config_reader.get(attr) - - def _config_section_name(self): - return 'remote "%s"' % self.name - - def _set_cache_(self, attr): - if attr == "_config_reader": - self._config_reader = _SectionConstraint(self.repo.config_reader(), self._config_section_name()) - else: - super(Remote, self)._set_cache_(attr) - - - def __str__(self): - return self.name - - def __repr__(self): - return '<git.%s "%s">' % (self.__class__.__name__, self.name) - - def __eq__(self, other): - return self.name == other.name - - def __ne__(self, other): - return not ( self == other ) - - def __hash__(self): - return hash(self.name) - - @classmethod - def iter_items(cls, repo): - """ - Returns - Iterator yielding Remote objects of the given repository - """ - for section in repo.config_reader("repository").sections(): - if not section.startswith('remote'): - continue - lbound = section.find('"') - rbound = section.rfind('"') - if lbound == -1 or rbound == -1: - raise ValueError("Remote-Section has invalid format: %r" % section) - yield Remote(repo, section[lbound+1:rbound]) - # END for each configuration section - - @property - def refs(self): - """ - Returns - IterableList of RemoteReference objects. It is prefixed, allowing - you to omit the remote path portion, i.e.:: - remote.refs.master # yields RemoteReference('/refs/remotes/origin/master') - """ - out_refs = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) - for ref in RemoteReference.list_items(self.repo): - if ref.remote_name == self.name: - out_refs.append(ref) - # END if names match - # END for each ref - assert out_refs, "Remote %s did not have any references" % self.name - return out_refs - - @property - def stale_refs(self): - """ - Returns - IterableList RemoteReference objects that do not have a corresponding - head in the remote reference anymore as they have been deleted on the - remote side, but are still available locally. - - The IterableList is prefixed, hence the 'origin' must be omitted. See - 'refs' property for an example. - """ - out_refs = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) - for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]: - # expecting - # * [would prune] origin/new_branch - token = " * [would prune] " - if not line.startswith(token): - raise ValueError("Could not parse git-remote prune result: %r" % line) - fqhn = "%s/%s" % (RemoteReference._common_path_default,line.replace(token, "")) - out_refs.append(RemoteReference(self.repo, fqhn)) - # END for each line - return out_refs - - @classmethod - def create(cls, repo, name, url, **kwargs): - """ - Create a new remote to the given repository - ``repo`` - Repository instance that is to receive the new remote - - ``name`` - Desired name of the remote - - ``url`` - URL which corresponds to the remote's name - - ``**kwargs`` - Additional arguments to be passed to the git-remote add command - - Returns - New Remote instance - - Raise - GitCommandError in case an origin with that name already exists - """ - repo.git.remote( "add", name, url, **kwargs ) - return cls(repo, name) - - # add is an alias - add = create - - @classmethod - def remove(cls, repo, name ): - """ - Remove the remote with the given name - """ - repo.git.remote("rm", name) - - # alias - rm = remove - - def rename(self, new_name): - """ - Rename self to the given new_name - - Returns - self - """ - if self.name == new_name: - return self - - self.repo.git.remote("rename", self.name, new_name) - self.name = new_name - del(self._config_reader) # it contains cached values, section names are different now - return self - - def update(self, **kwargs): - """ - Fetch all changes for this remote, including new branches which will - be forced in ( in case your local remote branch is not part the new remote branches - ancestry anymore ). - - ``kwargs`` - Additional arguments passed to git-remote update - - Returns - self - """ - self.repo.git.remote("update", self.name) - return self - - def _digest_process_messages(self, fh, progress): - """Read progress messages from file-like object fh, supplying the respective - progress messages to the progress instance. - @return: list(line, ...) list of lines without linebreaks that did - not contain progress information""" - line_so_far = '' - dropped_lines = list() - while True: - char = fh.read(1) - if not char: - break - - if char in ('\r', '\n'): - dropped_lines.extend(progress._parse_progress_line(line_so_far)) - line_so_far = '' - else: - line_so_far += char - # END process parsed line - # END while file is not done reading - return dropped_lines - - - def _finalize_proc(self, proc): - """Wait for the process (fetch, pull or push) and handle its errors accordingly""" - try: - proc.wait() - except GitCommandError,e: - # if a push has rejected items, the command has non-zero return status - # a return status of 128 indicates a connection error - reraise the previous one - if proc.poll() == 128: - raise - pass - # END exception handling - - - def _get_fetch_info_from_stderr(self, proc, progress): - # skip first line as it is some remote info we are not interested in - output = IterableList('name') - - - # lines which are no progress are fetch info lines - # this also waits for the command to finish - # Skip some progress lines that don't provide relevant information - fetch_info_lines = list() - for line in self._digest_process_messages(proc.stderr, progress): - if line.startswith('From') or line.startswith('remote: Total'): - continue - fetch_info_lines.append(line) - # END for each line - - # read head information - fp = open(os.path.join(self.repo.git_dir, 'FETCH_HEAD'),'r') - fetch_head_info = fp.readlines() - fp.close() - - assert len(fetch_info_lines) == len(fetch_head_info) - - output.extend(FetchInfo._from_line(self.repo, err_line, fetch_line) - for err_line,fetch_line in zip(fetch_info_lines, fetch_head_info)) - - self._finalize_proc(proc) - return output - - def _get_push_info(self, proc, progress): - # read progress information from stderr - # we hope stdout can hold all the data, it should ... - # read the lines manually as it will use carriage returns between the messages - # to override the previous one. This is why we read the bytes manually - self._digest_process_messages(proc.stderr, progress) - - output = IterableList('name') - for line in proc.stdout.readlines(): - try: - output.append(PushInfo._from_line(self, line)) - except ValueError: - # if an error happens, additional info is given which we cannot parse - pass - # END exception handling - # END for each line - - self._finalize_proc(proc) - return output - - - def fetch(self, refspec=None, progress=None, **kwargs): - """ - Fetch the latest changes for this remote - - ``refspec`` - A "refspec" is used by fetch and push to describe the mapping - between remote ref and local ref. They are combined with a colon in - the format <src>:<dst>, preceded by an optional plus sign, +. - For example: git fetch $URL refs/heads/master:refs/heads/origin means - "grab the master branch head from the $URL and store it as my origin - branch head". And git push $URL refs/heads/master:refs/heads/to-upstream - means "publish my master branch head as to-upstream branch at $URL". - See also git-push(1). - - Taken from the git manual - ``progress`` - See 'push' method - - ``**kwargs`` - Additional arguments to be passed to git-fetch - - Returns - IterableList(FetchInfo, ...) list of FetchInfo instances providing detailed - information about the fetch results - - Note - As fetch does not provide progress information to non-ttys, we cannot make - it available here unfortunately as in the 'push' method. - """ - proc = self.repo.git.fetch(self, refspec, with_extended_output=True, as_process=True, v=True, **kwargs) - return self._get_fetch_info_from_stderr(proc, progress or RemoteProgress()) - - def pull(self, refspec=None, progress=None, **kwargs): - """ - Pull changes from the given branch, being the same as a fetch followed - by a merge of branch with your local branch. - - ``refspec`` - see 'fetch' method - - ``progress`` - see 'push' method - - ``**kwargs`` - Additional arguments to be passed to git-pull - - Returns - Please see 'fetch' method - """ - proc = self.repo.git.pull(self, refspec, with_extended_output=True, as_process=True, v=True, **kwargs) - return self._get_fetch_info_from_stderr(proc, progress or RemoteProgress()) - - def push(self, refspec=None, progress=None, **kwargs): - """ - Push changes from source branch in refspec to target branch in refspec. - - ``refspec`` - see 'fetch' method - - ``progress`` - Instance of type RemoteProgress allowing the caller to receive - progress information until the method returns. - If None, progress information will be discarded - - ``**kwargs`` - Additional arguments to be passed to git-push - - Returns - IterableList(PushInfo, ...) iterable list of PushInfo instances, each - one informing about an individual head which had been updated on the remote - side. - If the push contains rejected heads, these will have the PushInfo.ERROR bit set - in their flags. - If the operation fails completely, the length of the returned IterableList will - be null. - """ - proc = self.repo.git.push(self, refspec, porcelain=True, as_process=True, **kwargs) - return self._get_push_info(proc, progress or RemoteProgress()) - - @property - def config_reader(self): - """ - Returns - GitConfigParser compatible object able to read options for only our remote. - Hence you may simple type config.get("pushurl") to obtain the information - """ - return self._config_reader - - @property - def config_writer(self): - """ - Return - GitConfigParser compatible object able to write options for this remote. - - Note - You can only own one writer at a time - delete it to release the - configuration file and make it useable by others. - - To assure consistent results, you should only query options through the - writer. Once you are done writing, you are free to use the config reader - once again. - """ - writer = self.repo.config_writer() - - # clear our cache to assure we re-read the possibly changed configuration - del(self._config_reader) - return _SectionConstraint(writer, self._config_section_name()) + """Provides easy read and write access to a git remote. + + Everything not part of this interface is considered an option for the current + remote, allowing constructs like remote.pushurl to query the pushurl. + + NOTE: When querying configuration, the configuration accessor will be cached + to speed up subsequent accesses.""" + + __slots__ = ( "repo", "name", "_config_reader" ) + _id_attribute_ = "name" + + def __init__(self, repo, name): + """Initialize a remote instance + + :param repo: The repository we are a remote of + :param name: the name of the remote, i.e. 'origin'""" + self.repo = repo + self.name = name + + def __getattr__(self, attr): + """Allows to call this instance like + remote.special( *args, **kwargs) to call git-remote special self.name""" + if attr == "_config_reader": + return super(Remote, self).__getattr__(attr) + + return self._config_reader.get(attr) + + def _config_section_name(self): + return 'remote "%s"' % self.name + + def _set_cache_(self, attr): + if attr == "_config_reader": + self._config_reader = _SectionConstraint(self.repo.config_reader(), self._config_section_name()) + else: + super(Remote, self)._set_cache_(attr) + + + def __str__(self): + return self.name + + def __repr__(self): + return '<git.%s "%s">' % (self.__class__.__name__, self.name) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return not ( self == other ) + + def __hash__(self): + return hash(self.name) + + @classmethod + def iter_items(cls, repo): + """:return: Iterator yielding Remote objects of the given repository""" + for section in repo.config_reader("repository").sections(): + if not section.startswith('remote'): + continue + lbound = section.find('"') + rbound = section.rfind('"') + if lbound == -1 or rbound == -1: + raise ValueError("Remote-Section has invalid format: %r" % section) + yield Remote(repo, section[lbound+1:rbound]) + # END for each configuration section + + @property + def refs(self): + """ + :return: + IterableList of RemoteReference objects. It is prefixed, allowing + you to omit the remote path portion, i.e.:: + remote.refs.master # yields RemoteReference('/refs/remotes/origin/master')""" + out_refs = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) + for ref in RemoteReference.list_items(self.repo): + if ref.remote_name == self.name: + out_refs.append(ref) + # END if names match + # END for each ref + assert out_refs, "Remote %s did not have any references" % self.name + return out_refs + + @property + def stale_refs(self): + """ + :return: + IterableList RemoteReference objects that do not have a corresponding + head in the remote reference anymore as they have been deleted on the + remote side, but are still available locally. + + The IterableList is prefixed, hence the 'origin' must be omitted. See + 'refs' property for an example.""" + out_refs = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) + for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]: + # expecting + # * [would prune] origin/new_branch + token = " * [would prune] " + if not line.startswith(token): + raise ValueError("Could not parse git-remote prune result: %r" % line) + fqhn = "%s/%s" % (RemoteReference._common_path_default,line.replace(token, "")) + out_refs.append(RemoteReference(self.repo, fqhn)) + # END for each line + return out_refs + + @classmethod + def create(cls, repo, name, url, **kwargs): + """Create a new remote to the given repository + :param repo: Repository instance that is to receive the new remote + :param name: Desired name of the remote + :param url: URL which corresponds to the remote's name + :param **kwargs: + Additional arguments to be passed to the git-remote add command + + :return: New Remote instance + + :raise GitCommandError: in case an origin with that name already exists""" + repo.git.remote( "add", name, url, **kwargs ) + return cls(repo, name) + + # add is an alias + add = create + + @classmethod + def remove(cls, repo, name ): + """Remove the remote with the given name""" + repo.git.remote("rm", name) + + # alias + rm = remove + + def rename(self, new_name): + """Rename self to the given new_name + :return: self """ + if self.name == new_name: + return self + + self.repo.git.remote("rename", self.name, new_name) + self.name = new_name + del(self._config_reader) # it contains cached values, section names are different now + return self + + def update(self, **kwargs): + """Fetch all changes for this remote, including new branches which will + be forced in ( in case your local remote branch is not part the new remote branches + ancestry anymore ). + + :param kwargs: + Additional arguments passed to git-remote update + + :return: self """ + self.repo.git.remote("update", self.name) + return self + + def _digest_process_messages(self, fh, progress): + """Read progress messages from file-like object fh, supplying the respective + progress messages to the progress instance. + + :return: list(line, ...) list of lines without linebreaks that did + not contain progress information""" + line_so_far = '' + dropped_lines = list() + while True: + char = fh.read(1) + if not char: + break + + if char in ('\r', '\n'): + dropped_lines.extend(progress._parse_progress_line(line_so_far)) + line_so_far = '' + else: + line_so_far += char + # END process parsed line + # END while file is not done reading + return dropped_lines + + + def _finalize_proc(self, proc): + """Wait for the process (fetch, pull or push) and handle its errors accordingly""" + try: + proc.wait() + except GitCommandError,e: + # if a push has rejected items, the command has non-zero return status + # a return status of 128 indicates a connection error - reraise the previous one + if proc.poll() == 128: + raise + pass + # END exception handling + + + def _get_fetch_info_from_stderr(self, proc, progress): + # skip first line as it is some remote info we are not interested in + output = IterableList('name') + + + # lines which are no progress are fetch info lines + # this also waits for the command to finish + # Skip some progress lines that don't provide relevant information + fetch_info_lines = list() + for line in self._digest_process_messages(proc.stderr, progress): + if line.startswith('From') or line.startswith('remote: Total'): + continue + fetch_info_lines.append(line) + # END for each line + + # read head information + fp = open(join(self.repo.git_dir, 'FETCH_HEAD'),'r') + fetch_head_info = fp.readlines() + fp.close() + + assert len(fetch_info_lines) == len(fetch_head_info) + + output.extend(FetchInfo._from_line(self.repo, err_line, fetch_line) + for err_line,fetch_line in zip(fetch_info_lines, fetch_head_info)) + + self._finalize_proc(proc) + return output + + def _get_push_info(self, proc, progress): + # read progress information from stderr + # we hope stdout can hold all the data, it should ... + # read the lines manually as it will use carriage returns between the messages + # to override the previous one. This is why we read the bytes manually + self._digest_process_messages(proc.stderr, progress) + + output = IterableList('name') + for line in proc.stdout.readlines(): + try: + output.append(PushInfo._from_line(self, line)) + except ValueError: + # if an error happens, additional info is given which we cannot parse + pass + # END exception handling + # END for each line + + self._finalize_proc(proc) + return output + + + def fetch(self, refspec=None, progress=None, **kwargs): + """Fetch the latest changes for this remote + + :param refspec: + A "refspec" is used by fetch and push to describe the mapping + between remote ref and local ref. They are combined with a colon in + the format <src>:<dst>, preceded by an optional plus sign, +. + For example: git fetch $URL refs/heads/master:refs/heads/origin means + "grab the master branch head from the $URL and store it as my origin + branch head". And git push $URL refs/heads/master:refs/heads/to-upstream + means "publish my master branch head as to-upstream branch at $URL". + See also git-push(1). + + Taken from the git manual + :param progress: See 'push' method + :param **kwargs: Additional arguments to be passed to git-fetch + :return: + IterableList(FetchInfo, ...) list of FetchInfo instances providing detailed + information about the fetch results + + :note: + As fetch does not provide progress information to non-ttys, we cannot make + it available here unfortunately as in the 'push' method.""" + proc = self.repo.git.fetch(self, refspec, with_extended_output=True, as_process=True, v=True, **kwargs) + return self._get_fetch_info_from_stderr(proc, progress or RemoteProgress()) + + def pull(self, refspec=None, progress=None, **kwargs): + """Pull changes from the given branch, being the same as a fetch followed + by a merge of branch with your local branch. + + :param refspec: see 'fetch' method + :param progress: see 'push' method + :param kwargs: Additional arguments to be passed to git-pull + :return: Please see 'fetch' method """ + proc = self.repo.git.pull(self, refspec, with_extended_output=True, as_process=True, v=True, **kwargs) + return self._get_fetch_info_from_stderr(proc, progress or RemoteProgress()) + + def push(self, refspec=None, progress=None, **kwargs): + """Push changes from source branch in refspec to target branch in refspec. + + :param refspec: see 'fetch' method + :param progress: + Instance of type RemoteProgress allowing the caller to receive + progress information until the method returns. + If None, progress information will be discarded + + :param **kwargs: Additional arguments to be passed to git-push + :return: + IterableList(PushInfo, ...) iterable list of PushInfo instances, each + one informing about an individual head which had been updated on the remote + side. + If the push contains rejected heads, these will have the PushInfo.ERROR bit set + in their flags. + If the operation fails completely, the length of the returned IterableList will + be null.""" + proc = self.repo.git.push(self, refspec, porcelain=True, as_process=True, **kwargs) + return self._get_push_info(proc, progress or RemoteProgress()) + + @property + def config_reader(self): + """ + :return: + GitConfigParser compatible object able to read options for only our remote. + Hence you may simple type config.get("pushurl") to obtain the information""" + return self._config_reader + + @property + def config_writer(self): + """ + :return: GitConfigParser compatible object able to write options for this remote. + :note: + You can only own one writer at a time - delete it to release the + configuration file and make it useable by others. + + To assure consistent results, you should only query options through the + writer. Once you are done writing, you are free to use the config reader + once again.""" + writer = self.repo.config_writer() + + # clear our cache to assure we re-read the possibly changed configuration + del(self._config_reader) + return _SectionConstraint(writer, self._config_section_name()) diff --git a/lib/git/repo.py b/lib/git/repo.py index d665a40c..9b25653f 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -18,34 +18,39 @@ from db import ( GitDB ) +from gitdb.util import ( + join, + isdir, + isfile, + hex_to_bin + ) import os import sys import re -import gzip -import StringIO +__all__ = ('Repo', ) + def touch(filename): fp = open(filename, "a") fp.close() def is_git_dir(d): """ This is taken from the git setup.c:is_git_directory - function.""" + 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 \ + if isdir(d) and \ + isdir(join(d, 'objects')) and \ + isdir(join(d, 'refs')): + headref = join(d, 'HEAD') + return 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, + """Represents a git repository and allows you to query references, gather commit information, generate diffs, create and clone repositories query the log. @@ -57,8 +62,7 @@ class Repo(object): '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. - """ + '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" ) @@ -73,7 +77,7 @@ class Repo(object): config_level = ("system", "global", "repository") def __init__(self, path=None, odbt = GitDB): - """ Create a new Repo instance + """Create a new Repo instance :param path: is the path to either the root git directory or the bare git repo:: @@ -171,44 +175,30 @@ class Repo(object): @property def working_tree_dir(self): - """ - Returns - The working tree directory of our git repository - - Raises AssertionError - If we are a bare repository - """ + """: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 ) return self._working_tree_dir @property def bare(self): - """ - Returns - True if the repository is bare - """ + """:return: True if the repository is bare""" return self._bare @property def heads(self): - """ - A list of ``Head`` objects representing the branch heads in + """A list of ``Head`` objects representing the branch heads in this repo - Returns - ``git.IterableList(Head, ...)`` - """ + :return: ``git.IterableList(Head, ...)``""" return Head.list_items(self) @property def references(self): - """ - A list of Reference objects representing tags, heads and remote references. + """A list of Reference objects representing tags, heads and remote references. - Returns - IterableList(Reference, ...) - """ + :return: IterableList(Reference, ...)""" return Reference.list_items(self) # alias for references @@ -219,113 +209,71 @@ class Repo(object): @property def index(self): - """ - Returns - IndexFile representing this repository's index. - """ + """:return: IndexFile representing this repository's index.""" return IndexFile(self) @property def head(self): - """ - Return - HEAD Object pointing to the current head reference - """ + """: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, ...)`` - """ + """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 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, ...)`` - """ + """A list of ``Tag`` objects that are available in this repo + :return: ``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 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, **kwargs ): - """ - Create a new head within the repository. - + """Create a new head within the repository. For more documentation, please see the Head.create method. - Returns - newly created Head Reference - """ + :return: newly created Head Reference""" return Head.create(self, path, commit, force, **kwargs) def delete_head(self, *heads, **kwargs): - """ - Delete the given heads + """Delete the given heads - ``kwargs`` - Additional keyword arguments to be passed to git-branch - """ + :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. - + """Create a new tag reference. For more documentation, please see the TagReference.create method. - Returns - TagReference object - """ + :return: TagReference object """ return TagReference.create(self, path, ref, message, force, **kwargs) def delete_tag(self, *tags): - """ - Delete the given tag references - """ + """Delete the given tag references""" return TagReference.delete(self, *tags) def create_remote(self, name, url, **kwargs): - """ - Create a new remote. + """Create a new remote. For more information, please see the documentation of the Remote.create methods - Returns - Remote reference - """ + :return: Remote reference""" return Remote.create(self, name, url, **kwargs) def delete_remote(self, remote): - """ - Delete the given remote. - """ + """Delete the given remote.""" return Remote.remove(self, remote) def _get_config_path(self, config_level ): @@ -345,21 +293,19 @@ class Repo(object): def config_reader(self, config_level=None): """ - Returns + :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 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`` + :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 instance - """ + :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 ] @@ -369,30 +315,23 @@ class Repo(object): def config_writer(self, config_level="repository"): """ - Returns + :return: 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`` + :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 - """ + 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`` - """ + """The Commit object for the specified revision + :param rev: revision specifier, see git-rev-parse for viable options. + :return: ``git.Commit``""" if rev is None: rev = self.active_branch @@ -401,33 +340,23 @@ class Repo(object): return c def iter_trees(self, *args, **kwargs): - """ - Returns - Iterator yielding Tree objects - - Note: Takes all arguments known to iter_commits method - """ + """: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) ) 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 ) - + """The Tree object for the given treeish revision Examples:: + + repo.tree(repo.heads[0]) - repo.tree(repo.heads[0]) - - Returns - ``git.Tree`` + :param rev: is a revision pointing to a Treeish ( being a commit or tree ) + :return: ``git.Tree`` - NOTE + :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. - """ + operations might have unexpected results.""" if rev is None: rev = self.active_branch @@ -439,27 +368,24 @@ class Repo(object): 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 + """A list of Commit objects representing the history of a given ref/commit - ``rev`` + :parm rev: revision specifier, see git-rev-parse for viable options. If None, the active branch will be used. - ``paths`` + :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. - ``kwargs`` + :parm 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 + :note: to receive only commits between two named revisions, use the + "revA..revB" revision specifier - Returns - ``git.Commit[]`` - """ + :return ``git.Commit[]``""" if rev is None: rev = self.active_branch @@ -483,12 +409,9 @@ class Repo(object): del _set_daemon_export def _get_alternates(self): - """ - The list of alternates for this repo from which objects can be retrieved + """The list of alternates for this repo from which objects can be retrieved - Returns - list of strings being pathnames of alternates - """ + :return: list of strings being pathnames of alternates""" alternates_path = os.path.join(self.git_dir, 'objects', 'info', 'alternates') if os.path.exists(alternates_path): @@ -499,26 +422,19 @@ class Repo(object): f.close() return alts.strip().splitlines() else: - return [] + return list() def _set_alternates(self, alts): - """ - Sets the alternates + """Sets the alternates - ``alts`` + :parm 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 + :raise NoSuchPathError: + :note: The method does not check for the existance of the paths in alts - as the caller is responsible. - - Returns - None - """ + as the caller is responsible.""" alternates_path = os.path.join(self.git_dir, 'objects', 'info', 'alternates') if not alts: if os.path.isfile(alternates_path): @@ -536,11 +452,10 @@ class Repo(object): def is_dirty(self, index=True, working_tree=True, untracked_files=False): """ - Returns + :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 - index or the working copy have changes. - """ + index or the working copy have changes.""" if self._bare: # Bare repositories with no associated working directory are # always consired to be clean. @@ -568,15 +483,14 @@ class Repo(object): @property def untracked_files(self): """ - Returns + :return: 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 - """ + :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) @@ -598,30 +512,23 @@ class Repo(object): @property def active_branch(self): - """ - The name of the currently active branch. + """The name of the currently active branch. - Returns - Head to the active branch - """ + :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. + """The blame information for the given file at the given revision. - ``rev`` - revision specifier, see git-rev-parse for viable options. - - Returns + :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 changed within the given commit. The Commit objects will be given in order - of appearance. - """ + of appearance.""" data = self.git.blame(rev, '--', file, p=True) - commits = {} - blames = [] + commits = dict() + blames = list() info = None for line in data.splitlines(False): @@ -670,7 +577,7 @@ class Repo(object): sha = info['id'] c = commits.get(sha) if c is None: - c = Commit( self, 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']), @@ -691,29 +598,22 @@ class Repo(object): @classmethod def init(cls, path=None, mkdir=True, **kwargs): - """ - Initialize a git repository at the given path if specified + """Initialize a git repository at the given path if specified - ``path`` + :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 working directory - ``mkdir`` + :parm 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`` + :parm 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) - """ + :return: ``git.Repo`` (the newly created repo)""" if mkdir and path and not os.path.exists(path): os.makedirs(path, 0755) @@ -735,8 +635,7 @@ class Repo(object): All remaining keyword arguments are given to the git-clone command :return: - ``git.Repo`` (the newly cloned repo) - """ + ``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 @@ -774,33 +673,17 @@ class Repo(object): 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`` + """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 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 - """ + :raise GitCommandError: in case something went wrong + :return: self""" if treeish is None: treeish = self.active_branch if prefix and 'prefix' not in kwargs: diff --git a/lib/git/utils.py b/lib/git/utils.py index 7dd50621..e49fcc2a 100644 --- a/lib/git/utils.py +++ b/lib/git/utils.py @@ -18,6 +18,9 @@ from gitdb.util import ( to_bin_sha ) +__all__ = ( "stream_copy", "join_path", "to_native_path_windows", "to_native_path_linux", + "join_path_native", "Stats", "IndexFileSHA1Writer", "Iterable", "IterableList", + "BlockingLockFile", "LockFile" ) def stream_copy(source, destination, chunk_size=512*1024): """Copy all data from the source stream into the destination stream in chunks @@ -92,9 +95,7 @@ class Stats(object): In addition to the items in the stat-dict, it features additional information:: - files = number of changed files as int - - """ + files = number of changed files as int""" __slots__ = ("total", "files") def __init__(self, total, files): @@ -103,13 +104,10 @@ class Stats(object): @classmethod def _list_from_string(cls, repo, text): - """ - Create a Stat object from output retrieved by git-diff. + """Create a Stat object from output retrieved by git-diff. - Returns - git.Stat - """ - hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, 'files': {}} + :return: git.Stat""" + hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, 'files': dict()} for line in text.splitlines(): (raw_insertions, raw_deletions, filename) = line.split("\t") insertions = raw_insertions != '-' and int(raw_insertions) or 0 @@ -125,16 +123,13 @@ class Stats(object): class IndexFileSHA1Writer(object): - """ - Wrapper around a file-like object that remembers the SHA1 of + """Wrapper around a file-like object that remembers the SHA1 of the data written to it. It will write a sha when the stream is closed or if the asked for explicitly usign write_sha. Only useful to the indexfile - Note: - Based on the dulwich project - """ + :note: Based on the dulwich project""" __slots__ = ("f", "sha1") def __init__(self, f): @@ -160,13 +155,12 @@ class IndexFileSHA1Writer(object): class LockFile(object): - """ - Provides methods to obtain, check for, and release a file based lock which + """Provides methods to obtain, check for, and release a file based lock which should be used to handle concurrent access to the same file. As we are a utility class to be derived from, we only use protected methods. - Locks will automatically be released on destruction """ + Locks will automatically be released on destruction""" __slots__ = ("_file_path", "_owns_lock") def __init__(self, file_path): @@ -177,32 +171,21 @@ class LockFile(object): self._release_lock() def _lock_file_path(self): - """ - Return - Path to lockfile - """ + """:return: Path to lockfile""" return "%s.lock" % (self._file_path) def _has_lock(self): - """ - Return - True if we have a lock and if the lockfile still exists - - Raise - AssertionError if our lock-file does not exist - """ + """:return: True if we have a lock and if the lockfile still exists + :raise AssertionError: if our lock-file does not exist""" if not self._owns_lock: return False return True def _obtain_lock_or_raise(self): - """ - Create a lock file as flag for other instances, mark our instance as lock-holder + """Create a lock file as flag for other instances, mark our instance as lock-holder - Raise - IOError if a lock was already present or a lock file could not be written - """ + :raise IOError: if a lock was already present or a lock file could not be written""" if self._has_lock(): return lock_file = self._lock_file_path() @@ -218,16 +201,12 @@ class LockFile(object): self._owns_lock = True def _obtain_lock(self): - """ - The default implementation will raise if a lock cannot be obtained. - Subclasses may override this method to provide a different implementation - """ + """The default implementation will raise if a lock cannot be obtained. + Subclasses may override this method to provide a different implementation""" return self._obtain_lock_or_raise() def _release_lock(self): - """ - Release our lock if we have one - """ + """Release our lock if we have one""" if not self._has_lock(): return @@ -252,13 +231,11 @@ class BlockingLockFile(LockFile): def __init__(self, file_path, check_interval_s=0.3, max_block_time_s=sys.maxint): """Configure the instance - ``check_interval_s`` + :parm check_interval_s: Period of time to sleep until the lock is checked the next time. By default, it waits a nearly unlimited time - ``max_block_time_s`` - Maximum amount of seconds we may lock - """ + :parm max_block_time_s: Maximum amount of seconds we may lock""" super(BlockingLockFile, self).__init__(file_path) self._check_interval = check_interval_s self._max_block_time = max_block_time_s @@ -292,8 +269,7 @@ class BlockingLockFile(LockFile): class IterableList(list): - """ - List of iterable objects allowing to query an object by id or by named index:: + """List of iterable objects allowing to query an object by id or by named index:: heads = repo.heads heads.master @@ -305,8 +281,7 @@ class IterableList(list): A prefix can be specified which is to be used in case the id returned by the items always contains a prefix that does not matter to the user, so it - can be left out. - """ + can be left out.""" __slots__ = ('_id_attr', '_prefix') def __new__(cls, id_attr, prefix=''): @@ -333,26 +308,21 @@ class IterableList(list): except AttributeError: raise IndexError( "No item found with id %r" % (self._prefix + index) ) + class Iterable(object): - """ - Defines an interface for iterable items which is to assure a uniform - way to retrieve and iterate items within the git repository - """ + """Defines an interface for iterable items which is to assure a uniform + way to retrieve and iterate items within the git repository""" __slots__ = tuple() _id_attribute_ = "attribute that most suitably identifies your instance" @classmethod def list_items(cls, repo, *args, **kwargs): - """ - Find all items of this type - subclasses can specify args and kwargs differently. + """Find all items of this type - subclasses can specify args and kwargs differently. If no args are given, subclasses are obliged to return all items if no additional arguments arg given. - Note: Favor the iter_items method as it will - - Returns: - list(Item,...) list of item instances - """ + :note: Favor the iter_items method as it will + :return:list(Item,...) list of item instances""" out_list = IterableList( cls._id_attribute_ ) out_list.extend(cls.iter_items(repo, *args, **kwargs)) return out_list @@ -360,11 +330,8 @@ class Iterable(object): @classmethod def iter_items(cls, repo, *args, **kwargs): - """ - For more information about the arguments, see list_items - Return: - iterator yielding Items - """ + """For more information about the arguments, see list_items + :return: iterator yielding Items""" raise NotImplementedError("To be implemented by Subclass") |