def hash_code(repo_path, catalogue_dir): """ Get commit digest for current HEAD commit Returns the current HEAD commit digest for the code that is run. If the current working directory is dirty (or has untracked files other than those held in `catalogue_dir`), it raises a `RepositoryDirtyError`. Parameters ---------- repo_path: str Path to analysis directory git repository. catalogue_dir: str Path to directory with catalogue output files. Returns ------- str Git commit digest for the current HEAD commit of the git repository """ try: repo = git.Repo(repo_path, search_parent_directories=True) except InvalidGitRepositoryError: raise InvalidGitRepositoryError( "provided code directory is not a valid git repository") untracked = prune_files(repo.untracked_files, catalogue_dir) if repo.is_dirty() or len(untracked) != 0: raise RepositoryDirtyError( repo, "git repository contains uncommitted changes") return repo.head.commit.hexsha
def __init__(self, repo: Repo = None): if not repo: try: self.repo = Repo(Path.cwd(), search_parent_directories=True) except InvalidGitRepositoryError: raise InvalidGitRepositoryError("Unable to find Repository from current working directory - aborting") else: self.repo = repo
def module(self): if self._is_detached: raise TypeError("Raised intentionally to test behavior of " "submodules with detached HEAD") elif not self._is_initialized: raise InvalidGitRepositoryError("Raised intentionally to test " "behavior of submodules that " "haven't been initialized") else: return MockRepo(self._full_path)
def __init__(self, path=os.getcwd()) -> None: try: self.repo = Repo(path, search_parent_directories=True) self.repo_path = os.path.dirname(self.repo.git_dir) self.branch = self.repo.active_branch self.display = Display() self.interact = Interact() except InvalidGitRepositoryError: raise InvalidGitRepositoryError( 'Not a git repo, I have no power here...')
def get_git_repo(self, repo_url, local_repo): """ Get a git repository. Parameters ---------- repo_url: str The location of the git repository (an url if local is False, a local path otherwise) local_repo: bool If True, get the repository from a local directory instead of the web Returns ------- str The temporary path to which the repository has been copied GitRepo The repository object Raises ------ FileNotFoundError If repo_url is not an existing directory git.InvalidGitRepositoryError If the directory in repo_url is not a git repository git.GitCommandError If the url in repo_url is not a git repository, or access to the repository is denied """ project_path = tempfile.mkdtemp() if local_repo: project_path = os.path.join(tempfile.mkdtemp(), 'repo') try: shutil.copytree(repo_url, project_path) repo = GitRepo(project_path) except FileNotFoundError as e: shutil.rmtree(project_path) raise e except InvalidGitRepositoryError as e: shutil.rmtree(project_path) raise InvalidGitRepositoryError( f"\"{repo_url}\" is not a local git repository.") from e else: try: GitRepo.clone_from(repo_url, project_path) repo = GitRepo(project_path) except GitCommandError as e: shutil.rmtree(project_path) raise e return project_path, repo
def test_repo_error(self, mock_repo): # setup step_implementer = GitMixin() # set up mocks mock_get_value = MagicMock() step_implementer.get_value = mock_get_value def mock_get_value_side_effect(key): if 'git-repo-root' in key: return 'mock/bad/repo/path' mock_get_value.side_effect = mock_get_value_side_effect mock_repo.side_effect = InvalidGitRepositoryError('mock error') # run test with self.assertRaisesRegex( StepRunnerException, "Given git-repo-root \(mock/bad/repo/path\) is not a Git repository: mock error" ): step_implementer.git_repo # validate mock_repo.assert_called_once_with('mock/bad/repo/path')
def git_query(repo_path, catalogue_dir, commit_changes=False): """ Check status of a git repository Checks the git status of the repository on the provided path - if clean, returns true - if there are uncommitted changes (including untracked files other than those held in `catalogue_dir`), and the `commit_changes` flag is `True`, offer the user to stage and commit all tracked (and untracked) files and continue (returns `True`) otherwise, returns `False` If the `commit_changes` flag is `True` and the user accepts the offer to commit changes, a new branch is created with the name `"catalogue-%Y%m%d-%H%M%S"` and all tracked files that have been changed & any untracked files will be staged and committed. Parameters: ------------ repo_path : str path to the code directory catalogue_dir : str path to directory with catalogue output files commit_changes : bool boolean indicating if the user should be prompted to stage and commit changes (optional, default is False) Returns: --------- Boolean indicating if git directory is clean """ try: repo = git.Repo(repo_path, search_parent_directories=True) except InvalidGitRepositoryError: raise InvalidGitRepositoryError( "provided code directory is not a valid git repository") untracked = prune_files(repo.untracked_files, catalogue_dir) if repo.is_dirty() or (len(untracked) != 0): if commit_changes: print("Working directory contains uncommitted changes.") print("Do you want to stage and commit all changes? (y/[n])") user_choice = input().strip().lower() if user_choice == "y" or user_choice == "yes": timestamp = create_timestamp() try: new_branch = repo.create_head("catalogue-" + timestamp) except BadName: print( "\nCannot create a branch for this commit as there are no existing commits.\nMake a commit manually, then run catalogue engage again.\n" ) return False new_branch.checkout() changed_files = [item.a_path for item in repo.index.diff(None)] changed_files.extend(untracked) repo.index.add(changed_files) repo.index.commit("repro-catalogue tool auto commit at " + timestamp) return True elif user_choice == "n" or user_choice == "no" or user_choice == "": return False else: print("Unrecognized response, leaving repository unchaged") return False else: return False else: return True
def run(self, *args, **kwargs): self.validate_config() try: local_directory = args[0] except IndexError: local_directory = config['source']['local'] try: repo_directory = args[1] except IndexError: repo_directory = config['source']['git']['local'] branch = config['source']['git']['branch'] repo = config['source']['git']['repo'] env.host_string = 'localhost' pretty_print('[+] Repository check start: %s' % local_directory, 'info') # if not len(directory): # pretty_print('Directory not selected, assuming current one.', 'info') # directory = os.getcwd() # if os.path.isdir(directory): # pretty_print('Directory found.', 'info') # else: # try: # pretty_print('Directory not found, creating.', 'info') # os.mkdir(directory) # except: # raise Exception('Cannot create directory %s, please create the folder manually' % directory) old_dir = os.getcwd() os.chdir(local_directory) pretty_print("Checking if repository exists %s" % os.path.join(local_directory, repo_directory), "info") try: if not os.path.isdir(os.path.join(repo_directory, ".git")): # repo = Repo(dir) if os.path.isdir(repo_directory): raise IOError('Directory already exists and is not repository. Clone will fail. Please check your ' 'configuration') raise InvalidGitRepositoryError() repo = Repo(repo_directory) pretty_print('Repository found.', 'info') except InvalidGitRepositoryError: # Repo doesn't exists pretty_print('Repository not found. Creating new one, using %s.' % repo, 'info') if len(repo) == 0: pretty_print('Repository not selected. Returning.', 'info') raise InvalidGitRepositoryError repo = Repo.clone_from(repo, repo_directory) if not len(branch): branch = repo.active_branch if repo.active_branch is not branch: pretty_print('Changing branch', 'info') repo.git.checkout(branch) pretty_print('Pulling changes', 'info') repo.git.pull('origin', branch) pretty_print('Fetching submodules', 'info') pretty_print('init') repo.git.submodule("init") pretty_print('update') try: repo.submodule_update(init=True, force_remove=True) except InvalidGitRepositoryError: pretty_print("Cannot update submodules, ommiting", 'info') os.chdir(old_dir) #repo.create_remote('origin', config.GIT_REPO) pretty_print('[+] Repository clone finished', 'info')
def test_get_git_commit_without_git(self, mock): mock.side_effect = InvalidGitRepositoryError() self.assertEqual(('unknown', '', ANY), get_git_commit())
def _single_repo_status(repo, verbose, follow_submodules): """ :param repo: git.Repo.base.Repo a Repo object referencing a local repository :param verbose: int verbosity level :param follow_submodules: bool whether or not to include submodules :return: dict {field: info} pairs. Fields (keys) are sufficient to create a "git-status"-like output for a repository, though many are set to None at lower `verbose` values """ # TODO (future?): option to get info about branches other than current status = { # local branch compared to remote tracking branch 'local_branch': None, 'remote_branch': None, 'n_commits_ahead': None, 'n_commits_behind': None, # uncommitted local changes 'n_staged': None, 'files_staged': None, 'n_not_staged': None, 'files_not_staged': None, 'n_untracked': None, 'files_untracked': None, # alternate info for repos in a detached HEAD state 'is_detached': False, 'hexsha': None, 'from_branch': None, 'ref_sha': None, 'detached_commits': None, # info for submodules (if any) 'submodules': None } try: headcommit = repo.head.commit except ValueError as e: raise InvalidGitRepositoryError( "GitTracker currently doesn't support tracking newly " f"initialized repositories (can't track {repo.working_dir}" ) from e if repo.head.is_detached: # if HEAD is detached, report some slightly different information status['is_detached'] = True status['hexsha'] = headcommit.hexsha[:7] from_branch, ref_sha, n_new_commits = _detached_status(repo) status['from_branch'] = from_branch if ref_sha != status['hexsha']: # if commits have been made since detaching HEAD, report hash # where initially detached and number of new commits status['ref_sha'] = ref_sha status['detached_commits'] = n_new_commits else: local_branch = repo.active_branch local_branch_name = local_branch.name try: remote_branch_name = local_branch.tracking_branch().name n_ahead = len(list(repo.iter_commits( f"{remote_branch_name}..{local_branch_name}" ))) n_behind = len(list(repo.iter_commits( f"{local_branch_name}..{remote_branch_name}" ))) except AttributeError: # local branch isn't tracking a remote remote_branch_name = '' n_ahead = None n_behind = None status['local_branch'] = local_branch_name status['remote_branch'] = remote_branch_name status['n_commits_ahead'] = n_ahead status['n_commits_behind'] = n_behind staged = headcommit.diff() unstaged = repo.index.diff(None) untracked = repo.untracked_files status['n_staged'] = len(staged) status['n_not_staged'] = len(unstaged) status['n_untracked'] = len(untracked) # always computing individual file diff info would make Displayer # format methods and unit tests simpler, but skipping when # unnecessary saves noticeable time if dealing with many repositories if verbose == 3: # go through any staged changes & manually to handle renames files_staged = [] for diff in staged: a_path = diff.a_path change_type = diff.change_type # new filepath only matters if file was renamed b_path = diff.b_path if change_type == 'R' else None files_staged.append((change_type, a_path, b_path)) status['files_staged'] = files_staged status['files_untracked'] = untracked status['files_not_staged'] = [ (diff.change_type, diff.a_path, None) for diff in unstaged ] if follow_submodules > 0 and any(repo.submodules): submodules = {} for sm in repo.submodules: sm_status = _submodule_status(sm, depth=follow_submodules) submodules[sm.path] = sm_status status['submodules'] = submodules return status
def module_status(module, dirty_warning=False, notag_warning=False, nogit_ok=False, nogit_warning=False): """Get status info (current hash, dirty/clean repo, tag) of module(s). Parameters ---------- - module or list/iterable of modules (each must belong to a git repository) - dirty_warning: if True, prints a warning if some git repos are dirty. - notag_warning: if True, prints a warning if some git repos don't have tags - nogit_ok: if True, if some modules are not in a git repo, simply get their version number. If False (default), raise an error. - nogit_warning: if some modules are not in a git repo and nogit_ok is True, print a warning when this happens. Output ------ Dict with module name as keys, and a dict {hash:, status:, tag:} as values """ modules = _make_iterable(module) mods = {} # dict {module name: dict of module info} for module in modules: name = module.__name__ try: info = path_status(module.__file__) except InvalidGitRepositoryError: if nogit_ok: tag = 'v' + module.__version__ info = {'status': 'not a git repository', 'tag': tag} else: raise InvalidGitRepositoryError(f'{module} not a git repo') mods[name] = info # Manage warnings if necessary ------------------------------------------- if dirty_warning: dirty_modules = [module for module, info in mods.items() if info['status'] == 'dirty'] if len(dirty_modules) > 0: msg = '\nWarning: these modules have dirty git repositories: ' msg += ', '.join(dirty_modules) print(msg) if notag_warning: tagless_modules = [module for module, info in mods.items() if 'tag' not in info] if len(tagless_modules) > 0: msg = '\nWarning: these modules are missing a tag: ' msg += ', '.join(tagless_modules) print(msg) if nogit_ok and nogit_warning: nogit_modules = [module for module, info in mods.items() if info['status'] == 'not a git repository'] if len(nogit_modules) > 0: msg = '\nWarning: these modules are not in a git repository: ' msg += ', '.join(nogit_modules) print(msg) return mods
await repo.clone.__wrapped__(repo) kwargs = {"recursive": True, "depth": 1, "shallow-submodules": True} if branch: kwargs["branch"] = branch clone_from.assert_called_once_with( REPO_URL, str(tmp_path), **kwargs, ) @pytest.mark.parametrize( "git_error", [InvalidGitRepositoryError(), NoSuchPathError(), GitCommandError("clone")], ) async def test_git_clone_error(coresys: CoreSys, tmp_path: Path, clone_from: AsyncMock, git_error: GitError): """Test git clone error.""" repo = GitRepo(coresys, tmp_path, REPO_URL) clone_from.side_effect = git_error with pytest.raises(StoreGitCloneError): await repo.clone.__wrapped__(repo) assert len(coresys.resolution.suggestions) == 0