def wrapper(self, *args, **kwargs): if self.repo.bare: raise InvalidGitRepositoryError( "Method '%s' cannot operate on bare repositories" % func.__name__) #END bare method return func(self, *args, **kwargs)
def module(self): """:return: Repo instance initialized from the repository at our submodule path :raise InvalidGitRepositoryError: if a repository was not available. This could also mean that it was not yet initialized""" # late import to workaround circular dependencies module_path = self.abspath try: repo = git.Repo(module_path) if repo != self.repo: return repo # END handle repo uninitialized except (InvalidGitRepositoryError, NoSuchPathError): raise InvalidGitRepositoryError("No valid repository at %s" % self.path) else: raise InvalidGitRepositoryError( "Repository at %r was not yet checked out" % module_path)
def find_first_remote_branch(remotes, branch_name): """Find the remote branch matching the name of the given branch or raise InvalidGitRepositoryError""" for remote in remotes: try: return remote.refs[branch_name] except IndexError: continue # END exception handling #END for remote raise InvalidGitRepositoryError("Didn't find remote branch %r in any of the given remotes", branch_name)
def _to_relative_path(self, path): """:return: Version of path relative to our git directory or raise ValueError if it is not within our git direcotory""" if not os.path.isabs(path): return path if self.repo.bare: raise InvalidGitRepositoryError("require non-bare repository") relative_path = path.replace(self.repo.working_tree_dir + os.sep, "") if relative_path == path: raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir)) return relative_path
def common_dir(self) -> PathLike: """ :return: The git dir that holds everything except possibly HEAD, FETCH_HEAD, ORIG_HEAD, COMMIT_EDITMSG, index, and logs/.""" if self._common_dir: return self._common_dir elif self.git_dir: return self.git_dir else: # or could return "" raise InvalidGitRepositoryError()
def module(self, repoType=None): """:return: Repository instance initialized from the repository at our submodule path :param repoType: The type of repository to be created. It must be possible to instatiate it from a single repository path. If None, a default repository type will be used :raise InvalidGitRepositoryError: if a repository was not available. This could also mean that it was not yet initialized""" # late import to workaround circular dependencies module_path = self.abspath repoType = repoType or git.Repo try: repo = repoType(module_path) if repo != self.repo: return repo # END handle repo uninitialized except (InvalidGitRepositoryError, NoSuchPathError): raise InvalidGitRepositoryError("No valid repository at %s" % self.path) else: raise InvalidGitRepositoryError("Repository at %r was not yet checked out" % module_path)
def find_first_remote_branch(remotes: Sequence['Remote'], branch_name: str) -> 'RemoteReference': """Find the remote branch matching the name of the given branch or raise InvalidGitRepositoryError""" for remote in remotes: try: return remote.refs[branch_name] except IndexError: continue # END exception handling # END for remote raise InvalidGitRepositoryError( "Didn't find remote branch '%r' in any of the given remotes" % branch_name)
def load(repo_dir: str, repo_url: str) -> RepoUtil: """ Load git repository from local directory into Python object. Arguments: :param repo_dir (str) directory where .git file is located :param repo_url (str) url to Github repository """ repo = (RepoUtil if 'yangmodels/yang' in repo_dir else ModifiableRepoUtil)( repo_url, clone=False) try: repo.repo = Repo(repo_dir) except InvalidGitRepositoryError: raise InvalidGitRepositoryError(repo_dir) repo.local_dir = repo_dir return repo
def __init__(self, remote_url: str, ssh_path: str = None, project_name: str = "",verbose=False) -> None: """Initializes Repository object The repository object holds information about the local directory as well as the remote of a repository. If Args: remote_url: URL of a remote repository i.e: ssh.github.om working_dir: local directory of the repository (working directory), i.e: ./repositories/ Returns: """ logger.remove() if verbose: logger.add(sys.stderr, format="{level: <8} | {message}", level="DEBUG") else: logger.add(sys.stderr, format="{level: <8} | {message}", level="INFO") # Check if complete URL is provided if not remote_url.endswith(".git"): raise InvalidGitRepositoryError("The URL doesn't seem valid. Please provide a valid url, e.g.: http://github.com/project/repo.git") self.repository_name = remote_url[(remote_url.rindex("/") + 1):remote_url.rindex(".git")] self.remote_url = remote_url self.path_prefix = project_name self.ssh_path = None self.progress_info = self.ProgressInfo() self.working_dir = os.path.join(get_project_root(), "temp", "repositories", self.path_prefix, self.repository_name) self.repository_git = git.Git(self.working_dir) if ssh_path is not None: self.ssh_path = str(ssh_path).replace("\\", "\\\\") self.repository_git.update_environment(GIT_SSH_COMMAND=f"ssh -i {self.ssh_path}") try: self.repository = git.Repo(self.working_dir) except InvalidGitRepositoryError: logger.info( f"{os.path.abspath(self.working_dir)} is not a valid git repository (no .git folder), clone {self.remote_url} into {os.path.abspath(self.working_dir)}") self.repository = self.__clone_from_remote() except NoSuchPathError: logger.info( f"{os.path.abspath(self.working_dir)} does not exist - trying to clone {self.remote_url} into {os.path.abspath(self.working_dir)}") self.repository = self.__clone_from_remote()
def iter_items(cls, repo, parent_commit='HEAD'): """:return: iterator yielding Submodule instances available in the given repository""" pc = repo.commit(parent_commit) # parent commit instance try: parser = cls._config_parser(repo, pc, read_only=True) except IOError: raise StopIteration # END handle empty iterator rt = pc.tree # root tree for sms in parser.sections(): n = sm_name(sms) p = parser.get_value(sms, 'path') u = parser.get_value(sms, 'url') b = cls.k_head_default if parser.has_option(sms, cls.k_head_option): b = str(parser.get_value(sms, cls.k_head_option)) # END handle optional information # get the binsha index = repo.index try: sm = rt[p] except KeyError: # try the index, maybe it was just added try: entry = index.entries[index.entry_key(p, 0)] sm = Submodule(repo, entry.binsha, entry.mode, entry.path) except KeyError: raise InvalidGitRepositoryError( "Gitmodule path %r did not exist in revision of parent commit %s" % (p, parent_commit)) # END handle keyerror # END handle critical error # fill in remaining info - saves time as it doesn't have to be parsed again sm._name = n sm._parent_commit = pc sm._branch_path = git.Head.to_full_path(b) sm._url = u yield sm
def _initialize(self, path): epath = abspath(expandvars(expanduser(path or os.getcwd()))) if not exists(epath): raise NoSuchPathError(epath) #END check file self._working_tree_dir = None self._git_path = None curpath = epath # walk up the path to find the .git dir while curpath: if is_git_dir(curpath): self._git_path = curpath self._working_tree_dir = os.path.dirname(curpath) break gitpath = join(curpath, self.repo_dir) if is_git_dir(gitpath): self._git_path = gitpath self._working_tree_dir = curpath break curpath, dummy = os.path.split(curpath) if not dummy: break # END while curpath if self._git_path is None: raise InvalidGitRepositoryError(epath) # END path not found self._bare = self._working_tree_dir is None if hasattr(self, 'config_reader'): try: self._bare = self.config_reader("repository").getboolean( 'core', 'bare') except Exception: # lets not assume the option exists, although it should pass #END handle exception #END check bare flag self._working_tree_dir = self._bare and None or self._working_tree_dir
def test_open_repository(self, repo_mock, print_error_mock, sys_exit_mock): config = {"main": {"repository": "./"}} repo_mock.side_effect = NoSuchPathError() _open_repository(config) print_error_mock.assert_called_once() sys_exit_mock.assert_called_with(ERROR_EXIT_CODE) repo_mock.side_effect = InvalidGitRepositoryError() print_error_mock.reset_mock() sys_exit_mock.reset_mock() _open_repository(config) print_error_mock.assert_called_once() sys_exit_mock.assert_called_with(ERROR_EXIT_CODE) repo_mock.side_effect = None print_error_mock.reset_mock() sys_exit_mock.reset_mock() _open_repository(config) repo_mock.assert_called_with(config["main"]["repository"]) self.assertEqual(sys_exit_mock.call_count, 0) self.assertEqual(print_error_mock.call_count, 0)
def __init__(self, path=None, odbt=DefaultDBType, search_parent_directories=False, expand_vars=True): """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") - In *Cygwin*, path may be a `'cygdrive/...'` prefixed path. - If it evaluates to false, :envvar:`GIT_DIR` is used, and if this also evals to false, the current-directory is used. :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 = path or os.getenv('GIT_DIR') if not epath: epath = os.getcwd() if Git.is_cygwin(): epath = decygpath(epath) epath = epath or path or os.getcwd() if expand_vars and ("%" in epath or "$" in epath): warnings.warn( "The use of environment variables in paths is deprecated" + "\nfor security reasons and may be removed in the future!!") epath = expand_path(epath, expand_vars) if not os.path.exists(epath): raise NoSuchPathError(epath) ## Walk up the path to find the `.git` dir. # curpath = epath while curpath: # ABOUT osp.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 = curpath self._working_tree_dir = os.getenv( 'GIT_WORK_TREE', os.path.dirname(self.git_dir)) break dotgit = osp.join(curpath, '.git') sm_gitpath = find_submodule_git_dir(dotgit) if sm_gitpath is not None: self.git_dir = osp.normpath(sm_gitpath) sm_gitpath = find_submodule_git_dir(dotgit) if sm_gitpath is None: sm_gitpath = find_worktree_git_dir(dotgit) if sm_gitpath is not None: self.git_dir = expand_path(sm_gitpath, expand_vars) self._working_tree_dir = curpath break if not search_parent_directories: break curpath, tail = osp.split(curpath) if not tail: 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 try: common_dir = open(osp.join(self.git_dir, 'commondir'), 'rt').readlines()[0].strip() self._common_dir = osp.join(self.git_dir, common_dir) except (OSError, IOError): self._common_dir = None # 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.common_dir self.git = self.GitCommandWrapperType(self.working_dir) # special handling, in special times args = [osp.join(self.common_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)
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 update(self, previous_commit=None, recursive=True, force_remove=False, init=True, to_latest_revision=False, progress=None, dry_run=False): """Update the submodules of this repository to the current HEAD commit. This method behaves smartly by determining changes of the path of a submodules repository, next to changes to the to-be-checked-out commit or the branch to be checked out. This works if the submodules ID does not change. Additionally it will detect addition and removal of submodules, which will be handled gracefully. :param previous_commit: If set to a commit'ish, the commit we should use as the previous commit the HEAD pointed to before it was set to the commit it points to now. If None, it defaults to HEAD@{1} otherwise :param recursive: if True, the children of submodules will be updated as well using the same technique :param force_remove: If submodules have been deleted, they will be forcibly removed. Otherwise the update may fail if a submodule's repository cannot be deleted as changes have been made to it (see Submodule.update() for more information) :param init: If we encounter a new module which would need to be initialized, then do it. :param to_latest_revision: If True, instead of checking out the revision pointed to by this submodule's sha, the checked out tracking branch will be merged with the newest remote branch fetched from the repository's origin :param progress: RootUpdateProgress instance or None if no progress should be sent :param dry_run: if True, operations will not actually be performed. Progress messages will change accordingly to indicate the WOULD DO state of the operation.""" if self.repo.bare: raise InvalidGitRepositoryError("Cannot update submodules in bare repositories") # END handle bare if progress is None: progress = RootUpdateProgress() # END assure progress is set prefix = '' if dry_run: prefix = 'DRY-RUN: ' repo = self.repo # SETUP BASE COMMIT ################### cur_commit = repo.head.commit if previous_commit is None: try: previous_commit = repo.commit(repo.head.log_entry(-1).oldhexsha) if previous_commit.binsha == previous_commit.NULL_BIN_SHA: raise IndexError # END handle initial commit except IndexError: # in new repositories, there is no previous commit previous_commit = cur_commit # END exception handling else: previous_commit = repo.commit(previous_commit) # obtain commit object # END handle previous commit psms = self.list_items(repo, parent_commit=previous_commit) sms = self.list_items(repo) spsms = set(psms) ssms = set(sms) # HANDLE REMOVALS ################### rrsm = (spsms - ssms) len_rrsm = len(rrsm) for i, rsm in enumerate(rrsm): op = REMOVE if i == 0: op |= BEGIN # END handle begin # fake it into thinking its at the current commit to allow deletion # of previous module. Trigger the cache to be updated before that progress.update(op, i, len_rrsm, prefix + "Removing submodule %r at %s" % (rsm.name, rsm.abspath)) rsm._parent_commit = repo.head.commit if not dry_run: rsm.remove(configuration=False, module=True, force=force_remove) # END handle dry-run if i == len_rrsm - 1: op |= END # END handle end progress.update(op, i, len_rrsm, prefix + "Done removing submodule %r" % rsm.name) # END for each removed submodule # HANDLE PATH RENAMES ##################### # url changes + branch changes csms = (spsms & ssms) len_csms = len(csms) for i, csm in enumerate(csms): psm = psms[csm.name] sm = sms[csm.name] # PATH CHANGES ############## if sm.path != psm.path and psm.module_exists(): progress.update(BEGIN | PATHCHANGE, i, len_csms, prefix + "Moving repository of submodule %r from %s to %s" % (sm.name, psm.abspath, sm.abspath)) # move the module to the new path if not dry_run: psm.move(sm.path, module=True, configuration=False) # END handle dry_run progress.update( END | PATHCHANGE, i, len_csms, prefix + "Done moving repository of submodule %r" % sm.name) # END handle path changes if sm.module_exists(): # HANDLE URL CHANGE ################### if sm.url != psm.url: # Add the new remote, remove the old one # This way, if the url just changes, the commits will not # have to be re-retrieved nn = '__new_origin__' smm = sm.module() rmts = smm.remotes # don't do anything if we already have the url we search in place if len([r for r in rmts if r.url == sm.url]) == 0: progress.update(BEGIN | URLCHANGE, i, len_csms, prefix + "Changing url of submodule %r from %s to %s" % (sm.name, psm.url, sm.url)) if not dry_run: assert nn not in [r.name for r in rmts] smr = smm.create_remote(nn, sm.url) smr.fetch(progress=progress) # If we have a tracking branch, it should be available # in the new remote as well. if len([r for r in smr.refs if r.remote_head == sm.branch_name]) == 0: raise ValueError( "Submodule branch named %r was not available in new submodule remote at %r" % (sm.branch_name, sm.url)) # END head is not detached # now delete the changed one rmt_for_deletion = None for remote in rmts: if remote.url == psm.url: rmt_for_deletion = remote break # END if urls match # END for each remote # if we didn't find a matching remote, but have exactly one, # we can safely use this one if rmt_for_deletion is None: if len(rmts) == 1: rmt_for_deletion = rmts[0] else: # if we have not found any remote with the original url # we may not have a name. This is a special case, # and its okay to fail here # Alternatively we could just generate a unique name and leave all # existing ones in place raise InvalidGitRepositoryError( "Couldn't find original remote-repo at url %r" % psm.url) # END handle one single remote # END handle check we found a remote orig_name = rmt_for_deletion.name smm.delete_remote(rmt_for_deletion) # NOTE: Currently we leave tags from the deleted remotes # as well as separate tracking branches in the possibly totally # changed repository ( someone could have changed the url to # another project ). At some point, one might want to clean # it up, but the danger is high to remove stuff the user # has added explicitly # rename the new remote back to what it was smr.rename(orig_name) # early on, we verified that the our current tracking branch # exists in the remote. Now we have to assure that the # sha we point to is still contained in the new remote # tracking branch. smsha = sm.binsha found = False rref = smr.refs[self.branch_name] for c in rref.commit.traverse(): if c.binsha == smsha: found = True break # END traverse all commits in search for sha # END for each commit if not found: # adjust our internal binsha to use the one of the remote # this way, it will be checked out in the next step # This will change the submodule relative to us, so # the user will be able to commit the change easily print >> sys.stderr, "WARNING: Current sha %s was not contained in the tracking branch at the new remote, setting it the the remote's tracking branch" % sm.hexsha sm.binsha = rref.commit.binsha # END reset binsha # NOTE: All checkout is performed by the base implementation of update # END handle dry_run progress.update( END | URLCHANGE, i, len_csms, prefix + "Done adjusting url of submodule %r" % (sm.name)) # END skip remote handling if new url already exists in module # END handle url # HANDLE PATH CHANGES ##################### if sm.branch_path != psm.branch_path: # finally, create a new tracking branch which tracks the # new remote branch progress.update(BEGIN | BRANCHCHANGE, i, len_csms, prefix + "Changing branch of submodule %r from %s to %s" % (sm.name, psm.branch_path, sm.branch_path)) if not dry_run: smm = sm.module() smmr = smm.remotes try: tbr = git.Head.create(smm, sm.branch_name, logmsg='branch: Created from HEAD') except OSError: # ... or reuse the existing one tbr = git.Head(smm, sm.branch_path) # END assure tracking branch exists tbr.set_tracking_branch(find_first_remote_branch(smmr, sm.branch_name)) # figure out whether the previous tracking branch contains # new commits compared to the other one, if not we can # delete it. try: tbr = find_first_remote_branch(smmr, psm.branch_name) if len(smm.git.cherry(tbr, psm.branch)) == 0: psm.branch.delete(smm, psm.branch) # END delete original tracking branch if there are no changes except InvalidGitRepositoryError: # ignore it if the previous branch couldn't be found in the # current remotes, this just means we can't handle it pass # END exception handling # NOTE: All checkout is done in the base implementation of update # END handle dry_run progress.update( END | BRANCHCHANGE, i, len_csms, prefix + "Done changing branch of submodule %r" % sm.name) # END handle branch # END handle # END for each common submodule # FINALLY UPDATE ALL ACTUAL SUBMODULES ###################################### for sm in sms: # update the submodule using the default method sm.update(recursive=False, init=init, to_latest_revision=to_latest_revision, progress=progress, dry_run=dry_run) # update recursively depth first - question is which inconsitent # state will be better in case it fails somewhere. Defective branch # or defective depth. The RootSubmodule type will never process itself, # which was done in the previous expression if recursive: # the module would exist by now if we are not in dry_run mode if sm.module_exists(): type(self)(sm.module()).update(recursive=True, force_remove=force_remove, init=init, to_latest_revision=to_latest_revision, progress=progress, dry_run=dry_run)
def __init__(self, path: Optional[PathLike] = None, odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, search_parent_directories: bool = False, expand_vars: bool = True) -> None: """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") repo = Repo("C:\\Users\\mtrier\\Development\\git-python\\.git") - In *Cygwin*, path may be a `'cygdrive/...'` prefixed path. - If it evaluates to false, :envvar:`GIT_DIR` is used, and if this also evals to false, the current-directory is used. :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 = path or os.getenv('GIT_DIR') if not epath: epath = os.getcwd() if Git.is_cygwin(): epath = decygpath(epath) epath = epath or path or os.getcwd() if not isinstance(epath, str): epath = str(epath) if expand_vars and re.search(self.re_envvars, epath): warnings.warn("The use of environment variables in paths is deprecated" + "\nfor security reasons and may be removed in the future!!") epath = expand_path(epath, expand_vars) if epath is not None: if not os.path.exists(epath): raise NoSuchPathError(epath) ## Walk up the path to find the `.git` dir. # curpath = epath while curpath: # ABOUT osp.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 = curpath # from man git-config : core.worktree # Set the path to the root of the working tree. If GIT_COMMON_DIR environment # variable is set, core.worktree is ignored and not used for determining the # root of working tree. This can be overridden by the GIT_WORK_TREE environment # variable. The value can be an absolute path or relative to the path to the .git # directory, which is either specified by GIT_DIR, or automatically discovered. # If GIT_DIR is specified but none of GIT_WORK_TREE and core.worktree is specified, # the current working directory is regarded as the top level of your working tree. self._working_tree_dir = os.path.dirname(self.git_dir) if os.environ.get('GIT_COMMON_DIR') is None: gitconf = self.config_reader("repository") if gitconf.has_option('core', 'worktree'): self._working_tree_dir = gitconf.get('core', 'worktree') if 'GIT_WORK_TREE' in os.environ: self._working_tree_dir = os.getenv('GIT_WORK_TREE') break dotgit = osp.join(curpath, '.git') sm_gitpath = find_submodule_git_dir(dotgit) if sm_gitpath is not None: self.git_dir = osp.normpath(sm_gitpath) sm_gitpath = find_submodule_git_dir(dotgit) if sm_gitpath is None: sm_gitpath = find_worktree_git_dir(dotgit) if sm_gitpath is not None: self.git_dir = expand_path(sm_gitpath, expand_vars) self._working_tree_dir = curpath break if not search_parent_directories: break curpath, tail = osp.split(curpath) if not tail: break # END while curpath if self.git_dir is None: self.git_dir = cast(PathLike, self.git_dir) 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 try: common_dir = open(osp.join(self.git_dir, 'commondir'), 'rt').readlines()[0].strip() self._common_dir = osp.join(self.git_dir, common_dir) except OSError: self._common_dir = None # 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.common_dir # type: Optional[PathLike] self.git = self.GitCommandWrapperType(self.working_dir) # special handling, in special times rootpath = osp.join(self.common_dir, 'objects') if issubclass(odbt, GitCmdObjectDB): self.odb = odbt(rootpath, self.git) else: self.odb = odbt(rootpath)
def remove(self, module=True, force=False, configuration=True, dry_run=False): """Remove this submodule from the repository. This will remove our entry from the .gitmodules file and the entry in the .git/config file. :param module: If True, the module we point to will be deleted as well. If the module is currently on a commit which is not part of any branch in the remote, if the currently checked out branch working tree, or untracked files, is ahead of its tracking branch, if you have modifications in the In case the removal of the repository fails for these reasons, the submodule status will not have been altered. If this submodule has child-modules on its own, these will be deleted prior to touching the own module. :param force: Enforces the deletion of the module even though it contains modifications. This basically enforces a brute-force file system based deletion. :param configuration: if True, the submodule is deleted from the configuration, otherwise it isn't. Although this should be enabled most of the times, this flag enables you to safely delete the repository of your submodule. :param dry_run: if True, we will not actually do anything, but throw the errors we would usually throw :return: self :note: doesn't work in bare repositories :raise InvalidGitRepositoryError: thrown if the repository cannot be deleted :raise OSError: if directories or files could not be removed""" if not (module + configuration): raise ValueError( "Need to specify to delete at least the module, or the configuration" ) # END handle params # DELETE MODULE REPOSITORY ########################## if module and self.module_exists(): if force: # take the fast lane and just delete everything in our module path # TODO: If we run into permission problems, we have a highly inconsistent # state. Delete the .git folders last, start with the submodules first mp = self.abspath method = None if os.path.islink(mp): method = os.remove elif os.path.isdir(mp): method = rmtree elif os.path.exists(mp): raise AssertionError( "Cannot forcibly delete repository as it was neither a link, nor a directory" ) #END handle brutal deletion if not dry_run: assert method method(mp) #END apply deletion method else: # verify we may delete our module mod = self.module() if mod.is_dirty(untracked_files=True): raise InvalidGitRepositoryError( "Cannot delete module at %s with any modifications, unless force is specified" % mod.working_tree_dir) # END check for dirt # figure out whether we have new commits compared to the remotes # NOTE: If the user pulled all the time, the remote heads might # not have been updated, so commits coming from the remote look # as if they come from us. But we stay strictly read-only and # don't fetch beforhand. for remote in mod.remotes: num_branches_with_new_commits = 0 rrefs = remote.refs for rref in rrefs: num_branches_with_new_commits = len( mod.git.cherry(rref)) != 0 # END for each remote ref # not a single remote branch contained all our commits if num_branches_with_new_commits == len(rrefs): raise InvalidGitRepositoryError( "Cannot delete module at %s as there are new commits" % mod.working_tree_dir) # END handle new commits # have to manually delete references as python's scoping is # not existing, they could keep handles open ( on windows this is a problem ) if len(rrefs): del (rref) #END handle remotes del (rrefs) del (remote) # END for each remote # gently remove all submodule repositories for sm in self.children(): sm.remove(module=True, force=False, configuration=False, dry_run=dry_run) del (sm) # END for each child-submodule # finally delete our own submodule if not dry_run: wtd = mod.working_tree_dir del (mod) # release file-handles (windows) rmtree(wtd) # END delete tree if possible # END handle force # END handle module deletion # DELETE CONFIGURATION ###################### if configuration and not dry_run: # first the index-entry index = self.repo.index try: del (index.entries[index.entry_key(self.path, 0)]) except KeyError: pass #END delete entry index.write() # now git config - need the config intact, otherwise we can't query # inforamtion anymore self.repo.config_writer().remove_section(sm_section(self.name)) self.config_writer().remove_section() # END delete configuration # void our data not to delay invalid access self._clear_cache() return self
def move(self, module_path, configuration=True, module=True): """Move the submodule to a another module path. This involves physically moving the repository at our current path, changing the configuration, as well as adjusting our index entry accordingly. :param module_path: the path to which to move our module, given as repository-relative path. Intermediate directories will be created accordingly. If the path already exists, it must be empty. Trailling (back)slashes are removed automatically :param configuration: if True, the configuration will be adjusted to let the submodule point to the given path. :param module: if True, the repository managed by this submodule will be moved, not the configuration. This will effectively leave your repository in an inconsistent state unless the configuration and index already point to the target location. :return: self :raise ValueError: if the module path existed and was not empty, or was a file :note: Currently the method is not atomic, and it could leave the repository in an inconsistent state if a sub-step fails for some reason """ if module + configuration < 1: raise ValueError( "You must specify to move at least the module or the configuration of the submodule" ) #END handle input module_path = to_native_path_linux(module_path) if module_path.endswith('/'): module_path = module_path[:-1] # END handle trailing slash # VERIFY DESTINATION if module_path == self.path: return self #END handle no change dest_path = join_path_native(self.repo.working_tree_dir, module_path) if os.path.isfile(dest_path): raise ValueError("Cannot move repository onto a file: %s" % dest_path) # END handle target files index = self.repo.index tekey = index.entry_key(module_path, 0) # if the target item already exists, fail if configuration and tekey in index.entries: raise ValueError("Index entry for target path did alredy exist") #END handle index key already there # remove existing destination if module: if os.path.exists(dest_path): if len(os.listdir(dest_path)): raise ValueError( "Destination module directory was not empty") #END handle non-emptyness if os.path.islink(dest_path): os.remove(dest_path) else: os.rmdir(dest_path) #END handle link else: # recreate parent directories # NOTE: renames() does that now pass #END handle existance # END handle module # move the module into place if possible cur_path = self.abspath renamed_module = False if module and os.path.exists(cur_path): os.renames(cur_path, dest_path) renamed_module = True #END move physical module # rename the index entry - have to manipulate the index directly as # git-mv cannot be used on submodules ... yeah try: if configuration: try: ekey = index.entry_key(self.path, 0) entry = index.entries[ekey] del (index.entries[ekey]) nentry = git.IndexEntry(entry[:3] + (module_path, ) + entry[4:]) index.entries[tekey] = nentry except KeyError: raise InvalidGitRepositoryError( "Submodule's entry at %r did not exist" % (self.path)) #END handle submodule doesn't exist # update configuration writer = self.config_writer(index=index) # auto-write writer.set_value('path', module_path) self.path = module_path del (writer) # END handle configuration flag except Exception: if renamed_module: os.renames(dest_path, cur_path) # END undo module renaming raise #END handle undo rename return self
def add(cls, repo, name, path, url=None, branch=None, no_checkout=False): """Add a new submodule to the given repository. This will alter the index as well as the .gitmodules file, but will not create a new commit. If the submodule already exists, no matter if the configuration differs from the one provided, the existing submodule will be returned. :param repo: Repository instance which should receive the submodule :param name: The name/identifier for the submodule :param path: repository-relative or absolute path at which the submodule should be located It will be created as required during the repository initialization. :param url: git-clone compatible URL, see git-clone reference for more information If None, the repository is assumed to exist, and the url of the first remote is taken instead. This is useful if you want to make an existing repository a submodule of anotherone. :param branch: name of branch at which the submodule should (later) be checked out. The given branch must exist in the remote repository, and will be checked out locally as a tracking branch. It will only be written into the configuration if it not None, which is when the checked out branch will be the one the remote HEAD pointed to. The result you get in these situation is somewhat fuzzy, and it is recommended to specify at least 'master' here. Examples are 'master' or 'feature/new' :param no_checkout: if True, and if the repository has to be cloned manually, no checkout will be performed :return: The newly created submodule instance :note: works atomically, such that no change will be done if the repository update fails for instance""" if repo.bare: raise InvalidGitRepositoryError( "Cannot add submodules to bare repositories") # END handle bare repos path = to_native_path_linux(path) if path.endswith('/'): path = path[:-1] # END handle trailing slash # assure we never put backslashes into the url, as some operating systems # like it ... if url != None: url = to_native_path_linux(url) #END assure url correctness # INSTANTIATE INTERMEDIATE SM sm = cls(repo, cls.NULL_BIN_SHA, cls.k_default_mode, path, name) if sm.exists(): # reretrieve submodule from tree try: return repo.head.commit.tree[path] except KeyError: # could only be in index index = repo.index entry = index.entries[index.entry_key(path, 0)] sm.binsha = entry.binsha return sm # END handle exceptions # END handle existing # fake-repo - we only need the functionality on the branch instance br = git.Head(repo, git.Head.to_full_path(str(branch) or cls.k_head_default)) has_module = sm.module_exists() branch_is_default = branch is None if has_module and url is not None: if url not in [r.url for r in sm.module().remotes]: raise ValueError( "Specified URL '%s' does not match any remote url of the repository at '%s'" % (url, sm.abspath)) # END check url # END verify urls match mrepo = None if url is None: if not has_module: raise ValueError( "A URL was not given and existing repository did not exsit at %s" % path) # END check url mrepo = sm.module() urls = [r.url for r in mrepo.remotes] if not urls: raise ValueError( "Didn't find any remote url in repository at %s" % sm.abspath) # END verify we have url url = urls[0] else: # clone new repo kwargs = {'n': no_checkout} if not branch_is_default: kwargs['b'] = br.name # END setup checkout-branch mrepo = git.Repo.clone_from(url, path, **kwargs) # END verify url # update configuration and index index = sm.repo.index writer = sm.config_writer(index=index, write=False) writer.set_value('url', url) writer.set_value('path', path) sm._url = url if not branch_is_default: # store full path writer.set_value(cls.k_head_option, br.path) sm._branch_path = br.path # END handle path del (writer) # we deliberatly assume that our head matches our index ! pcommit = repo.head.commit sm._parent_commit = pcommit sm.binsha = mrepo.head.commit.binsha index.add([sm], write=True) return sm
def test_git_repo_invalid(self): with patch("git.Repo") as mock: mock.side_effect = InvalidGitRepositoryError("Not a git repo") repo = GitRepo(".") self.assertFalse(repo.is_valid())