def is_git_dir(d): """ This is taken from the git setup.c:is_git_directory function.""" 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
def is_git_dir(d): """ This is taken from the git setup.c:is_git_directory function.""" 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
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 digest_process_messages(proc.stderr, progress): if line.startswith('From') or line.startswith('remote: Total'): continue elif line.startswith('warning:'): print >> sys.stderr, line continue elif line.startswith('fatal:'): raise GitCommandError(("Error when fetching: %s" % line,), 2) # END handle special messages 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), "len(%s) != len(%s)" % (fetch_head_info, fetch_info_lines) output.extend(FetchInfo._from_line(self.repo, err_line, fetch_line) for err_line,fetch_line in zip(fetch_info_lines, fetch_head_info)) finalize_process(proc) return output
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 _clone(cls, git, url, path, odb_default_type, progress, **kwargs): # special handling for windows for path at which the clone should be # created. # tilde '~' will be expanded to the HOME no matter where the ~ occours. Hence # we at least give a proper error instead of letting git fail prev_cwd = None prev_path = None odbt = kwargs.pop('odbt', odb_default_type) if os.name == 'nt': if '~' in path: raise OSError("Git cannot handle the ~ character in path %r correctly" % path) # on windows, git will think paths like c: are relative and prepend the # current working dir ( before it fails ). We temporarily adjust the working # dir to make this actually work match = re.match("(\w:[/\\\])(.*)", path) if match: prev_cwd = os.getcwd() prev_path = path drive, rest_of_path = match.groups() os.chdir(drive) path = rest_of_path kwargs['with_keep_cwd'] = True # END cwd preparation # END windows handling try: proc = git.clone(url, path, with_extended_output=True, as_process=True, v=True, **add_progress(kwargs, git, progress)) if progress: handle_process_output(proc, None, progress.new_message_handler(), finalize_process) else: (stdout, stderr) = proc.communicate() finalize_process(proc, stderr=stderr) # end handle progress finally: if prev_cwd is not None: os.chdir(prev_cwd) path = prev_path # END reset previous working dir # END bad windows handling # our git command could have a different working dir than our actual # environment, hence we prepend its working dir if required if not os.path.isabs(path) and git.working_dir: path = join(git._working_dir, path) # adjust remotes - there may be operating systems which use backslashes, # These might be given as initial paths, but when handling the config file # that contains the remote from which we were clones, git stops liking it # as it will escape the backslashes. Hence we undo the escaping just to be # sure repo = cls(os.path.abspath(path), odbt=odbt) if repo.remotes: writer = repo.remotes[0].config_writer writer.set_value('url', repo.remotes[0].url.replace("\\\\", "\\").replace("\\", "/")) # PY3: be sure cleanup is performed and lock is released writer.release() # END handle remote repo return repo
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_ref_info(cls, repo, ref_path): """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(join(repo.git_dir, ref_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 cls._iter_packed_refs(repo): if path != ref_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" % ref_path) # is it a reference ? if tokens[0] == 'ref:': return (None, tokens[1]) # its a commit if repo.re_hexsha_only.match(tokens[0]): return (tokens[0], None) raise ValueError("Failed to parse reference information from %r" % ref_path)
def path(cls, ref): """ :return: string to absolute path at which the reflog of the given ref instance would be found. The path is not guaranteed to point to a valid file though. :param ref: SymbolicReference instance""" return join(ref.repo.git_dir, "logs", to_native_path(ref.path))
def _create(cls, repo, path, resolve, reference, force, logmsg=None): """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 = repo.rev_parse(str(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 existing_data = open(abs_ref_path, 'rb').read().decode(defenc).strip() if existing_data != target_data: raise OSError( "Reference at %r does already exist, pointing to %r, requested was %r" % (full_ref_path, existing_data, target_data)) # END no force handling ref = cls(repo, full_ref_path) ref.set_reference(target, logmsg) return ref
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 digest_process_messages(proc.stderr, progress): if line.startswith('From') or line.startswith('remote: Total'): continue elif line.startswith('warning:'): print >> sys.stderr, line continue elif line.startswith('fatal:'): raise GitCommandError(("Error when fetching: %s" % line,), 2) # END handle special messages 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), "len(%s) != len(%s)" % ( fetch_head_info, fetch_info_lines) output.extend(FetchInfo._from_line(self.repo, err_line, fetch_line) for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info)) finalize_process(proc) return output
def _set_daemon_export(self, value): filename = join(self.git_dir, self.DAEMON_EXPORT_FILE) fileexists = os.path.exists(filename) if value and not fileexists: touch(filename) elif not value and fileexists: os.unlink(filename)
def _get_ref_info(cls, repo, ref_path): """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(join(repo.git_dir, ref_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 cls._iter_packed_refs(repo): if path != ref_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" % ref_path) # is it a reference ? if tokens[0] == 'ref:': return (None, tokens[1]) # its a commit if repo.re_hexsha_only.match(tokens[0]): return (tokens[0], None) raise ValueError("Failed to parse reference information from %r" % ref_path)
def _create(cls, repo, path, resolve, reference, force, logmsg=None): """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 = repo.rev_parse(str(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 existing_data = open(abs_ref_path, 'rb').read().decode(defenc).strip() if existing_data != target_data: raise OSError("Reference at %r does already exist, pointing to %r, requested was %r" % (full_ref_path, existing_data, target_data)) # END no force handling ref = cls(repo, full_ref_path) ref.set_reference(target, logmsg) return ref
def _clone(cls, git, url, path, odb_default_type, progress, **kwargs): # special handling for windows for path at which the clone should be # created. # tilde '~' will be expanded to the HOME no matter where the ~ occours. Hence # we at least give a proper error instead of letting git fail prev_cwd = None prev_path = None odbt = kwargs.pop('odbt', odb_default_type) if os.name == 'nt': if '~' in path: raise OSError("Git cannot handle the ~ character in path %r correctly" % path) # on windows, git will think paths like c: are relative and prepend the # current working dir ( before it fails ). We temporarily adjust the working # dir to make this actually work match = re.match("(\w:[/\\\])(.*)", path) if match: prev_cwd = os.getcwd() prev_path = path drive, rest_of_path = match.groups() os.chdir(drive) path = rest_of_path kwargs['with_keep_cwd'] = True # END cwd preparation # END windows handling try: proc = git.clone(url, path, with_extended_output=True, as_process=True, v=True, **add_progress(kwargs, git, progress)) if progress: handle_process_output(proc, None, progress.new_message_handler(), finalize_process) else: finalize_process(proc) # end handle progress finally: if prev_cwd is not None: os.chdir(prev_cwd) path = prev_path # END reset previous working dir # END bad windows handling # our git command could have a different working dir than our actual # environment, hence we prepend its working dir if required if not os.path.isabs(path) and git.working_dir: path = join(git._working_dir, path) # adjust remotes - there may be operating systems which use backslashes, # These might be given as initial paths, but when handling the config file # that contains the remote from which we were clones, git stops liking it # as it will escape the backslashes. Hence we undo the escaping just to be # sure repo = cls(os.path.abspath(path), odbt=odbt) if repo.remotes: writer = repo.remotes[0].config_writer writer.set_value('url', repo.remotes[0].url.replace("\\\\", "\\").replace("\\", "/")) # PY3: be sure cleanup is performed and lock is released writer.release() # END handle remote repo return repo
def _get_fetch_info_from_stderr(self, proc, progress): progress = to_progress_instance(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() # Basically we want all fetch info lines which appear to be in regular form, and thus have a # command character. Everything else we ignore, cmds = set(PushInfo._flag_map.keys()) & set(FetchInfo._flag_map.keys()) progress_handler = progress.new_message_handler() stderr_text = None for line in proc.stderr: line = force_text(line) for pline in progress_handler(line): # END handle special messages for cmd in cmds: if len(line) > 1 and line[0] == ' ' and line[1] == cmd: fetch_info_lines.append(line) continue # end find command code # end for each comand code we know # end for each line progress didn't handle # end if progress.error_lines(): stderr_text = '\n'.join(progress.error_lines()) finalize_process(proc, stderr=stderr_text) # read head information fp = open(join(self.repo.git_dir, 'FETCH_HEAD'), 'rb') fetch_head_info = [l.decode(defenc) for l in fp.readlines()] fp.close() l_fil = len(fetch_info_lines) l_fhi = len(fetch_head_info) if l_fil != l_fhi: msg = "Fetch head lines do not match lines provided via progress information\n" msg += "length of progress lines %i should be equal to lines in FETCH_HEAD file %i\n" msg += "Will ignore extra progress lines or fetch head lines." msg %= (l_fil, l_fhi) log.debug(msg) if l_fil < l_fhi: fetch_head_info = fetch_head_info[:l_fil] else: fetch_info_lines = fetch_info_lines[:l_fhi] # end truncate correct list # end sanity check + sanitization output.extend( FetchInfo._from_line(self.repo, err_line, fetch_line) for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info)) return output
def _get_config_path(self, config_level): # we do not support an absolute path of the gitconfig on windows , # use the global config instead if sys.platform == "win32" and config_level == "system": config_level = "global" if config_level == "system": return "/etc/gitconfig" elif config_level == "user": config_home = os.environ.get("XDG_CONFIG_HOME") or os.path.join(os.environ.get("HOME", '~'), ".config") return os.path.normpath(os.path.expanduser(join(config_home, "git", "config"))) elif config_level == "global": return os.path.normpath(os.path.expanduser("~/.gitconfig")) elif config_level == "repository": return os.path.normpath(join(self.git_dir, "config")) raise ValueError("Invalid configuration level: %r" % config_level)
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, 'rb') 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 line = line.decode(defenc) 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: # write-binary is required, otherwise windows will # open the file in text mode and change LF to CRLF ! open(pack_file_path, 'wb').writelines(l.encode(defenc) for l in new_lines) # END write out file # END open exception handling # END handle deletion # delete the reflog reflog_path = RefLog.path(cls(repo, full_ref_path)) if os.path.isfile(reflog_path): os.remove(reflog_path)
def _get_fetch_info_from_stderr(self, proc, progress): progress = to_progress_instance(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() # Basically we want all fetch info lines which appear to be in regular form, and thus have a # command character. Everything else we ignore, cmds = set(PushInfo._flag_map.keys()) & set(FetchInfo._flag_map.keys()) progress_handler = progress.new_message_handler() stderr_text = None for line in proc.stderr: line = force_text(line) for pline in progress_handler(line): # END handle special messages for cmd in cmds: if len(line) > 1 and line[0] == ' ' and line[1] == cmd: fetch_info_lines.append(line) continue # end find command code # end for each comand code we know # end for each line progress didn't handle # end if progress.error_lines(): stderr_text = '\n'.join(progress.error_lines()) finalize_process(proc, stderr=stderr_text) # read head information fp = open(join(self.repo.git_dir, 'FETCH_HEAD'), 'rb') fetch_head_info = [l.decode(defenc) for l in fp.readlines()] fp.close() l_fil = len(fetch_info_lines) l_fhi = len(fetch_head_info) if l_fil != l_fhi: msg = "Fetch head lines do not match lines provided via progress information\n" msg += "length of progress lines %i should be equal to lines in FETCH_HEAD file %i\n" msg += "Will ignore extra progress lines or fetch head lines." msg %= (l_fil, l_fhi) log.debug(msg) if l_fil < l_fhi: fetch_head_info = fetch_head_info[:l_fil] else: fetch_info_lines = fetch_info_lines[:l_fhi] # end truncate correct list # end sanity check + sanitization output.extend(FetchInfo._from_line(self.repo, err_line, fetch_line) for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info)) return output
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 with open(new_abs_path, 'rb') as fd1: f1 = fd1.read().strip() with open(cur_abs_path, 'rb') as fd2: f2 = fd2.read().strip() if f1 != f2: 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
def find_git_dir(d): if is_git_dir(d): return d elif isfile(d): with open(d) as fp: content = fp.read().rstrip() if content.startswith('gitdir: '): d = join(dirname(d), content[8:]) return find_git_dir(d) return None
def _get_fetch_info_from_stderr(self, proc, progress): # skip first line as it is some remote info we are not interested in # TODO: Use poll() to process stdout and stderr at same time 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() # Basically we want all fetch info lines which appear to be in regular form, and thus have a # command character. Everything else we ignore, cmds = set(PushInfo._flag_map.keys()) & set(FetchInfo._flag_map.keys()) progress_handler = progress.new_message_handler() for line in proc.stderr: line = line.decode(defenc) line = line.rstrip() for pline in progress_handler(line): if line.startswith('fatal:'): raise GitCommandError(("Error when fetching: %s" % line, ), 2) # END handle special messages for cmd in cmds: if len(line) > 1 and line[0] == ' ' and line[1] == cmd: fetch_info_lines.append(line) continue # end find command code # end for each comand code we know # end for each line progress didn't handle # end # We are only interested in stderr here ... try: finalize_process(proc) except Exception: if len(fetch_info_lines) == 0: raise # end exception handler # read head information fp = open(join(self.repo.git_dir, 'FETCH_HEAD'), 'rb') fetch_head_info = [l.decode(defenc) for l in fp.readlines()] fp.close() # NOTE: We assume to fetch at least enough progress lines to allow matching each fetch head line with it. assert len(fetch_info_lines) >= len( fetch_head_info), "len(%s) <= len(%s)" % (fetch_head_info, fetch_info_lines) output.extend( FetchInfo._from_line(self.repo, err_line, fetch_line) for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info)) return output
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
def _get_alternates(self): """The list of alternates for this repo from which objects can be retrieved :return: list of strings being pathnames of alternates""" alternates_path = join(self.git_dir, 'objects', 'info', 'alternates') if os.path.exists(alternates_path): with open(alternates_path, 'rb') as f: alts = f.read().decode(defenc) return alts.strip().splitlines() else: return list()
def _get_fetch_info_from_stderr(self, proc, progress): # skip first line as it is some remote info we are not interested in # TODO: Use poll() to process stdout and stderr at same time 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() # Basically we want all fetch info lines which appear to be in regular form, and thus have a # command character. Everything else we ignore, cmds = set(PushInfo._flag_map.keys()) & set(FetchInfo._flag_map.keys()) progress_handler = progress.new_message_handler() for line in proc.stderr: line = line.decode(defenc) line = line.rstrip() for pline in progress_handler(line): if line.startswith('fatal:') or line.startswith('error:'): raise GitCommandError(("Error when fetching: %s" % line,), 2) # END handle special messages for cmd in cmds: if len(line) > 1 and line[0] == ' ' and line[1] == cmd: fetch_info_lines.append(line) continue # end find command code # end for each comand code we know # end for each line progress didn't handle # end # We are only interested in stderr here ... try: finalize_process(proc) except Exception: if len(fetch_info_lines) == 0: raise # end exception handler # read head information fp = open(join(self.repo.git_dir, 'FETCH_HEAD'), 'rb') fetch_head_info = [l.decode(defenc) for l in fp.readlines()] fp.close() # NOTE: We assume to fetch at least enough progress lines to allow matching each fetch head line with it. assert len(fetch_info_lines) >= len(fetch_head_info), "len(%s) <= len(%s)" % (fetch_head_info, fetch_info_lines) output.extend(FetchInfo._from_line(self.repo, err_line, fetch_line) for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info)) return output
def _get_config_path(self, config_level): # we do not support an absolute path of the gitconfig on windows , # use the global config instead if sys.platform == "win32" and config_level == "system": config_level = "global" if config_level == "system": return "/etc/gitconfig" elif config_level == "global": return os.path.normpath(os.path.expanduser("~/.gitconfig")) elif config_level == "repository": return join(self.git_dir, "config") raise ValueError("Invalid configuration level: %r" % config_level)
def _get_config_path(self, config_level ): # we do not support an absolute path of the gitconfig on windows , # use the global config instead if sys.platform == "win32" and config_level == "system": config_level = "global" if config_level == "system": return "/etc/gitconfig" elif config_level == "global": return os.path.normpath(os.path.expanduser("~/.gitconfig")) elif config_level == "repository": return join(self.git_dir, "config") raise ValueError( "Invalid configuration level: %r" % config_level )
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
def _get_alternates(self): """The list of alternates for this repo from which objects can be retrieved :return: list of strings being pathnames of alternates""" alternates_path = join(self.git_dir, 'objects', 'info', 'alternates') if os.path.exists(alternates_path): try: f = open(alternates_path) alts = f.read() finally: f.close() return alts.strip().splitlines() else: return list()
def _get_alternates(self): """The list of alternates for this repo from which objects can be retrieved :return: list of strings being pathnames of alternates""" alternates_path = join(self.git_dir, 'objects', 'info', 'alternates') if os.path.exists(alternates_path): try: f = open(alternates_path, 'rb') alts = f.read().decode(defenc) finally: f.close() return alts.strip().splitlines() else: return list()
def find_git_dir(d): if is_git_dir(d): return d try: with open(d) as fp: content = fp.read().rstrip() except (IOError, OSError): # it's probably not a file pass else: if content.startswith('gitdir: '): path = content[8:] if not os.path.isabs(path): path = join(dirname(d), path) return find_git_dir(path) # end handle exception return None
def _set_alternates(self, alts): """Sets the alternates :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 :raise NoSuchPathError: :note: The method does not check for the existance of the paths in alts as the caller is responsible.""" alternates_path = join(self.git_dir, 'objects', 'info', 'alternates') if not alts: if isfile(alternates_path): os.remove(alternates_path) else: with open(alternates_path, 'wb') as f: f.write("\n".join(alts).encode(defenc))
def _clone(cls, git, url, path, odb_default_type, **kwargs): # special handling for windows for path at which the clone should be # created. # tilde '~' will be expanded to the HOME no matter where the ~ occours. Hence # we at least give a proper error instead of letting git fail prev_cwd = None prev_path = None odbt = kwargs.pop('odbt', odb_default_type) if os.name == 'nt': if '~' in path: raise OSError( "Git cannot handle the ~ character in path %r correctly" % path) # on windows, git will think paths like c: are relative and prepend the # current working dir ( before it fails ). We temporarily adjust the working # dir to make this actually work match = re.match("(\w:[/\\\])(.*)", path) if match: prev_cwd = os.getcwd() prev_path = path drive, rest_of_path = match.groups() os.chdir(drive) path = rest_of_path kwargs['with_keep_cwd'] = True # END cwd preparation # END windows handling try: git.clone(url, path, **kwargs) finally: if prev_cwd is not None: os.chdir(prev_cwd) path = prev_path # END reset previous working dir # END bad windows handling # our git command could have a different working dir than our actual # environment, hence we prepend its working dir if required if not os.path.isabs(path) and git.working_dir: path = join(git._working_dir, path) return cls(os.path.abspath(path), odbt=odbt)
def _set_alternates(self, alts): """Sets the alternates :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 :raise NoSuchPathError: :note: The method does not check for the existance of the paths in alts as the caller is responsible.""" alternates_path = join(self.git_dir, 'objects', 'info', 'alternates') if not alts: if isfile(alternates_path): os.remove(alternates_path) else: try: f = open(alternates_path, 'wb') f.write("\n".join(alts).encode(defenc)) finally: f.close()
def _clone(cls, git, url, path, odb_default_type, **kwargs): # special handling for windows for path at which the clone should be # created. # tilde '~' will be expanded to the HOME no matter where the ~ occours. Hence # we at least give a proper error instead of letting git fail prev_cwd = None prev_path = None odbt = kwargs.pop('odbt', odb_default_type) if os.name == 'nt': if '~' in path: raise OSError("Git cannot handle the ~ character in path %r correctly" % path) # on windows, git will think paths like c: are relative and prepend the # current working dir ( before it fails ). We temporarily adjust the working # dir to make this actually work match = re.match("(\w:[/\\\])(.*)", path) if match: prev_cwd = os.getcwd() prev_path = path drive, rest_of_path = match.groups() os.chdir(drive) path = rest_of_path kwargs['with_keep_cwd'] = True # END cwd preparation # END windows handling try: git.clone(url, path, **kwargs) finally: if prev_cwd is not None: os.chdir(prev_cwd) path = prev_path # END reset previous working dir # END bad windows handling # our git command could have a different working dir than our actual # environment, hence we prepend its working dir if required if not os.path.isabs(path) and git.working_dir: path = join(git._working_dir, path) return cls(os.path.abspath(path), odbt = odbt)
def is_git_dir(d): """ This is taken from the git setup.c:is_git_directory function. @throws WorkTreeRepositoryUnsupported if it sees a worktree directory. It's quite hacky to do that here, but at least clearly indicates that we don't support it. There is the unlikely danger to throw if we see directories which just look like a worktree dir, but are none.""" if isdir(d): if 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')) elif isfile(join(d, 'gitdir')) and isfile(join(d, 'commondir')) and isfile(join(d, 'gitfile')): raise WorkTreeRepositoryUnsupported(d) return False
def _get_ref_info(cls, repo, ref_path): """Return: (str(sha), str(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(join(repo.git_dir, ref_path), 'rt') value = fp.read().rstrip() fp.close() # Don't only split on spaces, but on whitespace, which allows to parse lines like # 60b64ef992065e2600bfef6187a97f92398a9144 branch 'master' of git-server:/path/to/repo tokens = value.split() assert (len(tokens) != 0) 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 cls._iter_packed_refs(repo): if path != ref_path: continue # sha will be used 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" % ref_path) # is it a reference ? if tokens[0] == 'ref:': return (None, tokens[1]) # its a commit if repo.re_hexsha_only.match(tokens[0]): return (tokens[0], None) raise ValueError("Failed to parse reference information from %r" % ref_path)
def _clone(cls, git, url, path, odb_default_type, progress, **kwargs): if progress is not None: progress = to_progress_instance(progress) odbt = kwargs.pop('odbt', odb_default_type) proc = git.clone(url, path, with_extended_output=True, as_process=True, v=True, **add_progress(kwargs, git, progress)) if progress: handle_process_output(proc, None, progress.new_message_handler(), finalize_process) else: (stdout, stderr ) = proc.communicate() # FIXME: Will block of outputs are big! log.debug("Cmd(%s)'s unused stdout: %s", getattr(proc, 'args', ''), stdout) finalize_process(proc, stderr=stderr) # our git command could have a different working dir than our actual # environment, hence we prepend its working dir if required if not os.path.isabs(path) and git.working_dir: path = join(git._working_dir, path) # adjust remotes - there may be operating systems which use backslashes, # These might be given as initial paths, but when handling the config file # that contains the remote from which we were clones, git stops liking it # as it will escape the backslashes. Hence we undo the escaping just to be # sure repo = cls(path, odbt=odbt) if repo.remotes: with repo.remotes[0].config_writer as writer: writer.set_value('url', Git.polish_url(repo.remotes[0].url)) # END handle remote repo return repo
def _get_ref_info(cls, repo, ref_path): """Return: (str(sha), str(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(join(repo.git_dir, ref_path), 'rt') value = fp.read().rstrip() fp.close() # Don't only split on spaces, but on whitespace, which allows to parse lines like # 60b64ef992065e2600bfef6187a97f92398a9144 branch 'master' of git-server:/path/to/repo tokens = value.split() assert(len(tokens) != 0) 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 cls._iter_packed_refs(repo): if path != ref_path: continue # sha will be used 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" % ref_path) # is it a reference ? if tokens[0] == 'ref:': return (None, tokens[1]) # its a commit if repo.re_hexsha_only.match(tokens[0]): return (tokens[0], None) raise ValueError("Failed to parse reference information from %r" % ref_path)
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 digest_process_messages(proc.stderr, progress): if line.startswith('From') or line.startswith('remote: Total') or line.startswith('POST') \ or line.startswith(' ='): continue elif line.startswith('warning:'): print >> sys.stderr, line continue elif line.startswith('fatal:'): raise GitCommandError(("Error when fetching: %s" % line, ), 2) # END handle special messages 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() # NOTE: HACK Just disabling this line will make github repositories work much better. # I simply couldn't stand it anymore, so here is the quick and dirty fix ... . # This project needs a lot of work ! # assert len(fetch_info_lines) == len(fetch_head_info), "len(%s) != len(%s)" % (fetch_head_info, fetch_info_lines) output.extend( FetchInfo._from_line(self.repo, err_line, fetch_line) for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info)) finalize_process(proc) return output
def _get_daemon_export(self): filename = join(self.git_dir, self.DAEMON_EXPORT_FILE) return os.path.exists(filename)
def _set_description(self, descr): filename = join(self.git_dir, 'description') open(filename, 'wb').write((descr + '\n').encode(defenc))
def _get_description(self): filename = join(self.git_dir, 'description') return open(filename, 'rb').read().rstrip().decode(defenc)
def __init__(self, path=None, odbt = DefaultDBType): """Create a new Repo instance :param path: is the path to either the root git directory or the bare git repo:: repo = Repo("/Users/mtrier/Development/git-python") repo = Repo("/Users/mtrier/Development/git-python.git") repo = Repo("~/Development/git-python.git") repo = Repo("$REPOSITORIES/Development/git-python.git") :param odbt: Object DataBase type - a type which is constructed by providing the directory containing the database objects, i.e. .git/objects. It will be used to access all object data :raise InvalidGitRepositoryError: :raise NoSuchPathError: :return: git.Repo """ epath = os.path.abspath(os.path.expandvars(os.path.expanduser(path or os.getcwd()))) if not os.path.exists(epath): raise NoSuchPathError(epath) self.working_dir = None self._working_tree_dir = None self.git_dir = None curpath = epath # walk up the path to find the .git dir while curpath: if is_git_dir(curpath): self.git_dir = curpath self._working_tree_dir = os.path.dirname(curpath) break gitpath = join(curpath, '.git') if is_git_dir(gitpath): self.git_dir = gitpath self._working_tree_dir = curpath break curpath, dummy = os.path.split(curpath) if not dummy: break # END while curpath if self.git_dir is None: raise InvalidGitRepositoryError(epath) self._bare = False try: self._bare = self.config_reader("repository").getboolean('core','bare') except Exception: # lets not assume the option exists, although it should pass # adjust the wd in case we are actually bare - we didn't know that # in the first place if self._bare: self._working_tree_dir = None # END working dir handling self.working_dir = self._working_tree_dir or self.git_dir self.git = Git(self.working_dir) # special handling, in special times args = [join(self.git_dir, 'objects')] if issubclass(odbt, GitCmdObjectDB): args.append(self.git) self.odb = odbt(*args)
def object_path(self, hexsha): """ :return: path at which the object with the given hexsha would be stored, relative to the database root""" return join(hexsha[:2], hexsha[2:])
def _get_packed_refs_path(cls, repo): return join(repo.git_dir, 'packed-refs')
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 = repo.rev_parse(operation.split(split_token)[0]) # END handle refspec # END reference flag handling return cls(remote_local_ref, flags, note, old_commit)
def __init__(self, path=None, odbt=DefaultDBType, search_parent_directories=False): """Create a new Repo instance :param path: the path to either the root git directory or the bare git repo:: repo = Repo("/Users/mtrier/Development/git-python") repo = Repo("/Users/mtrier/Development/git-python.git") repo = Repo("~/Development/git-python.git") repo = Repo("$REPOSITORIES/Development/git-python.git") :param odbt: Object DataBase type - a type which is constructed by providing the directory containing the database objects, i.e. .git/objects. It will be used to access all object data :param search_parent_directories: if True, all parent directories will be searched for a valid repo as well. Please note that this was the default behaviour in older versions of GitPython, which is considered a bug though. :raise InvalidGitRepositoryError: :raise NoSuchPathError: :return: git.Repo """ epath = _expand_path(path or os.getcwd()) self.git = None # should be set for __del__ not to fail in case we raise if not os.path.exists(epath): raise NoSuchPathError(epath) self.working_dir = None self._working_tree_dir = None self.git_dir = None curpath = epath # walk up the path to find the .git dir while curpath: # ABOUT os.path.NORMPATH # It's important to normalize the paths, as submodules will otherwise initialize their # repo instances with paths that depend on path-portions that will not exist after being # removed. It's just cleaner. if is_git_dir(curpath): self.git_dir = os.path.normpath(curpath) self._working_tree_dir = os.path.dirname(self.git_dir) break gitpath = find_git_dir(join(curpath, '.git')) if gitpath is not None: self.git_dir = os.path.normpath(gitpath) self._working_tree_dir = curpath break if not search_parent_directories: break curpath, dummy = os.path.split(curpath) if not dummy: break # END while curpath if self.git_dir is None: raise InvalidGitRepositoryError(epath) self._bare = False try: self._bare = self.config_reader("repository").getboolean('core', 'bare') except Exception: # lets not assume the option exists, although it should pass # adjust the wd in case we are actually bare - we didn't know that # in the first place if self._bare: self._working_tree_dir = None # END working dir handling self.working_dir = self._working_tree_dir or self.git_dir self.git = self.GitCommandWrapperType(self.working_dir) # special handling, in special times args = [join(self.git_dir, 'objects')] if issubclass(odbt, GitCmdObjectDB): args.append(self.git) self.odb = odbt(*args)
def __init__(self, path=None, odbt=DefaultDBType, search_parent_directories=False): """Create a new Repo instance :param path: the path to either the root git directory or the bare git repo:: repo = Repo("/Users/mtrier/Development/git-python") repo = Repo("/Users/mtrier/Development/git-python.git") repo = Repo("~/Development/git-python.git") repo = Repo("$REPOSITORIES/Development/git-python.git") :param odbt: Object DataBase type - a type which is constructed by providing the directory containing the database objects, i.e. .git/objects. It will be used to access all object data :param search_parent_directories: if True, all parent directories will be searched for a valid repo as well. Please note that this was the default behaviour in older versions of GitPython, which is considered a bug though. :raise InvalidGitRepositoryError: :raise NoSuchPathError: :return: git.Repo """ epath = _expand_path(path or os.getcwd()) self.git = None # should be set for __del__ not to fail in case we raise if not os.path.exists(epath): raise NoSuchPathError(epath) self.working_dir = None self._working_tree_dir = None self.git_dir = None curpath = epath # walk up the path to find the .git dir while curpath: # ABOUT os.path.NORMPATH # It's important to normalize the paths, as submodules will otherwise initialize their # repo instances with paths that depend on path-portions that will not exist after being # removed. It's just cleaner. if is_git_dir(curpath): self.git_dir = os.path.normpath(curpath) self._working_tree_dir = os.path.dirname(self.git_dir) break gitpath = find_git_dir(join(curpath, '.git')) if gitpath is not None: self.git_dir = os.path.normpath(gitpath) self._working_tree_dir = curpath break if not search_parent_directories: break curpath, dummy = os.path.split(curpath) if not dummy: break # END while curpath if self.git_dir is None: raise InvalidGitRepositoryError(epath) self._bare = False try: self._bare = self.config_reader("repository").getboolean( 'core', 'bare') except Exception: # lets not assume the option exists, although it should pass # adjust the wd in case we are actually bare - we didn't know that # in the first place if self._bare: self._working_tree_dir = None # END working dir handling self.working_dir = self._working_tree_dir or self.git_dir self.git = self.GitCommandWrapperType(self.working_dir) # special handling, in special times args = [join(self.git_dir, 'objects')] if issubclass(odbt, GitCmdObjectDB): args.append(self.git) self.odb = odbt(*args)