def _create_branch_local_tracking(self, branch: str, remote: str, depth: int, fetch: bool = True, remove_dir: bool = False) -> None: """Create and checkout tracking branch :param str branch: Branch name :param str remote: Remote name :param int depth: Git clone depth. 0 indicates full clone, otherwise must be a positive integer :param bool fetch: Whether to fetch before creating branch :param bool remove_dir: Whether to remove the directory if commands fail """ origin = self._remote(remote, remove_dir=remove_dir) if fetch: self.fetch(remote, depth=depth, ref=GitRef(branch=branch), remove_dir=remove_dir) try: CONSOLE.stdout(f' - Create branch {fmt.ref(branch)}') self.repo.create_head(branch, origin.refs[branch]) except BaseException: LOG.error(f'Failed to create branch {fmt.ref(branch)}') if remove_dir: remove_directory(self.repo_path, check=False) raise else: self._set_tracking_branch(remote, branch, remove_dir=remove_dir) self._checkout_branch_local(branch, remove_dir=remove_dir)
def config_set_projects(args) -> None: """Clowder config set projects command entry point""" CONSOLE.stdout(' - Set projects config value') config = Config() config.projects = tuple(args.projects) config.save()
def config_set_rebase(_) -> None: """Clowder config set rebase command entry point""" CONSOLE.stdout(' - Set rebase config value') config = Config() config.rebase = True config.save()
def stash(self) -> None: """Stash changes for project if dirty""" if self.is_dirty: self.repo.stash() else: CONSOLE.stdout(" - No changes to stash")
def status(self) -> None: """Print git status Equivalent to: ``git status`` """ CONSOLE.stdout(self.repo.git.status())
def config_clear_all(_) -> None: """Clowder config clear all command entry point""" CONSOLE.stdout(' - Clear all config values') config = Config() config.clear() config.save()
def _checkout_new_repo_tag(self, tag: str, remote: str, depth: int, remove_dir: bool = False) -> None: """Checkout tag or fail and delete repo if it doesn't exist :param str tag: Tag name :param str remote: Remote name :param int depth: Git clone depth. 0 indicates full clone, otherwise must be a positive integer :param bool remove_dir: Whether to remove the directory if commands fail """ remote_tag = self._get_remote_tag(tag, remote, depth=depth, remove_dir=remove_dir) if remote_tag is None: return try: CONSOLE.stdout(f' - Checkout tag {fmt.ref(tag)}') self.repo.git.checkout(remote_tag) except BaseException: LOG.error(f'Failed to checkout tag {fmt.ref(tag)}') if remove_dir: remove_directory(self.repo_path, check=False) raise
def config_clear_protocol(_) -> None: """Clowder config clear protocol command entry point""" CONSOLE.stdout(' - Clear protocol config value') config = Config() config.protocol = None config.save()
def config_clear_jobs(_) -> None: """Clowder config clear jobs command entry point""" CONSOLE.stdout(' - Clear jobs config value') config = Config() config.jobs = None config.save()
def config_set_jobs(args) -> None: """Clowder config set jobs command entry point""" CONSOLE.stdout(' - Set jobs config value') config = Config() config.jobs = args.jobs[0] config.save()
def config_clear_rebase(_) -> None: """Clowder config clear rebase command entry point""" CONSOLE.stdout(' - Clear rebase config value') config = Config() config.rebase = None config.save()
def herd(projects: Tuple[ResolvedProject, ...], jobs: int, branch: Optional[str] = None, tag: Optional[str] = None, depth: Optional[int] = None, rebase: bool = False) -> None: """Clone projects or update latest from upstream in parallel :param Tuple[ResolvedProject, ...] projects: Projects to herd :param int jobs: Number of jobs to use running parallel commands :param Optional[str] branch: Branch to attempt to herd :param Optional[str] tag: Tag to attempt to herd :param Optional[int] depth: Git clone depth. 0 indicates full clone, otherwise must be a positive integer :param bool rebase: Whether to use rebase instead of pulling latest changes """ CONSOLE.stdout(' - Herd projects in parallel\n') CLOWDER_CONTROLLER.validate_print_output(projects) run_func = partial(run_parallel, jobs, projects, 'herd', branch=branch, tag=tag, depth=depth, rebase=rebase) trio.run(run_func)
def run(self, command: str, ignore_errors: bool) -> None: """Run commands or script in project directory :param str command: Commands to run :param bool ignore_errors: Whether to exit if command returns a non-zero exit code """ if not existing_git_repo(self.full_path): CONSOLE.stdout(fmt.red(" - Project missing\n")) return forall_env = { 'CLOWDER_PATH': ENVIRONMENT.clowder_dir, 'PROJECT_PATH': self.full_path, 'PROJECT_NAME': self.name, 'PROJECT_REMOTE': self.remote, 'PROJECT_REF': self.ref.formatted_ref } # TODO: Add tests for presence of these variables in test scripts # if self.branch: # forall_env['UPSTREAM_BRANCH'] = self.branch # if self.tag: # forall_env['UPSTREAM_TAG'] = self.tag # if self.commit: # forall_env['UPSTREAM_COMMIT'] = self.commit if self.upstream: forall_env['UPSTREAM_REMOTE'] = self.upstream.remote forall_env['UPSTREAM_NAME'] = self.upstream.name forall_env['UPSTREAM_REF'] = self.upstream.ref.formatted_ref self._run_forall_command(command, forall_env, ignore_errors)
def _forall_impl(command: str, ignore_errors: bool, projects: List[str], jobs: Optional[int] = None) -> None: """Runs script in project directories specified :param str command: Command or script and optional arguments :param bool ignore_errors: Whether to exit if command returns a non-zero exit code :param List[str] projects: Project names to clean :param Optional[int] jobs: Number of jobs to use running parallel commands """ projects = Config().process_projects_arg(projects) projects = CLOWDER_CONTROLLER.filter_projects(CLOWDER_CONTROLLER.projects, projects) jobs_config = Config().jobs jobs = jobs_config if jobs_config is not None else jobs if jobs is not None and jobs != 1 and os.name == "posix": if jobs <= 0: jobs = 4 parallel.forall(projects, jobs, command, ignore_errors) return for project in projects: CONSOLE.stdout(project.status()) project.run(command, ignore_errors=ignore_errors)
def start(self, remote: str, branch: str, depth: int, tracking: bool) -> None: """Start new branch in repository and checkout :param str remote: Remote name :param str branch: Local branch name to create :param int depth: Git clone depth. 0 indicates full clone, otherwise must be a positive integer :param bool tracking: Whether to create a remote branch with tracking relationship """ if branch not in self.repo.heads: if not is_offline(): self.fetch(remote, ref=GitRef(branch=branch), depth=depth) try: self._create_branch_local(branch) self._checkout_branch_local(branch) except BaseException as err: LOG.debug('Failed to create and checkout branch', err) raise else: CONSOLE.stdout(f' - {fmt.ref(branch)} already exists') if self._is_branch_checked_out(branch): CONSOLE.stdout(' - On correct branch') else: self._checkout_branch_local(branch) if tracking and not is_offline(): self._create_branch_remote_tracking(branch, remote, depth)
def reset(self, depth: int = 0) -> None: """Reset branch to upstream or checkout tag/sha as detached HEAD :param int depth: Git clone depth. 0 indicates full clone, otherwise must be a positive integer :raise ClowderGitError: :raise UnknownTypeError: """ if self.default_ref.ref_type is GitRefEnum.TAG: self.fetch(self.remote, ref=self.default_ref, depth=depth) self._checkout_tag(self.default_ref.short_ref) elif self.default_ref.ref_type is GitRefEnum.COMMIT: self.fetch(self.remote, ref=self.default_ref, depth=depth) self._checkout_sha(self.default_ref.short_ref) elif self.default_ref.ref_type is GitRefEnum.BRANCH: branch = self.default_ref.short_ref if not self.has_local_branch(branch): self._create_branch_local_tracking(branch, self.remote, depth=depth, fetch=True) return self._checkout_branch(branch) if not self.has_remote_branch(branch, self.remote): raise ClowderGitError(f'No existing remote branch {fmt.remote(self.remote)} {fmt.ref(branch)}') self.fetch(self.remote, ref=self.default_ref, depth=depth) CONSOLE.stdout(f' - Reset branch {fmt.ref(branch)} to {fmt.remote(self.remote)} {fmt.ref(branch)}') self._reset_head(branch=f'{self.remote}/{branch}') else: raise UnknownTypeError('Unknown GitRefEnum type')
def config_set_protocol(args) -> None: """Clowder config set protocol command entry point""" CONSOLE.stdout(' - Set protocol config value') config = Config() config.protocol = GitProtocol(args.protocol[0]) config.save()
def wrapper(*args, **kwargs): """Wrapper""" instance = args[0] if not Path(instance.full_path / '.git').is_dir(): CONSOLE.stdout(fmt.red("- Project missing")) return return func(*args, **kwargs)
def run_command(self, command: str) -> None: """Run command in clowder repo :param str command: Command to run """ CONSOLE.stdout(fmt.command(command)) execute_command(command.split(), self.repo_path)
def wrapper(*args, **kwargs): """Wrapper""" instance = args[0] if instance.is_detached: CONSOLE.stdout(' - HEAD is detached') return return func(*args, **kwargs)
def push(self) -> None: """Push changes""" try: CONSOLE.stdout(' - Push local changes') CONSOLE.stdout(self.repo.git.push()) except GitError: LOG.error('Failed to push local changes') raise
def print_local_branches(self) -> None: """Print local git branches""" for branch in self.repo.git.branch().split('\n'): if branch.startswith('* '): branch_name = fmt.green(branch[2:]) CONSOLE.stdout(f"* {branch_name}") else: CONSOLE.stdout(branch)
def install_lfs_hooks(self) -> None: """Install git lfs hooks""" CONSOLE.stdout(" - Update git lfs hooks") try: self.repo.git.lfs('install', '--local') except GitError: LOG.error('Failed to update git lfs hooks') raise
def pull(self) -> None: """Pull upstream changes""" try: CONSOLE.stdout(' - Pull latest changes') CONSOLE.stdout(self.repo.git.pull()) except GitError: LOG.error('Failed to pull latest changes') raise
def pull_lfs(self) -> None: """Pull lfs files""" try: CONSOLE.stdout(' - Pull git lfs files') self.repo.git.lfs('pull') except GitError: LOG.error('Failed to pull git lfs files') raise
def stash(self) -> None: """Stash current changes in repository""" if not self.repo.is_dirty(): CONSOLE.stdout(' - No changes to stash') return CONSOLE.stdout(' - Stash current changes') self.repo.git.stash()
def print_validation(self, allow_missing_repo: bool = True) -> None: """Print validation message for project :param bool allow_missing_repo: Whether to allow validation to succeed with missing repo """ if not self.is_valid(allow_missing_repo=allow_missing_repo): CONSOLE.stdout(self.status()) self.repo.print_validation()
def install_project_git_herd_alias(self) -> None: """Install 'git herd' alias for project""" from clowder.environment import ENVIRONMENT config_variable = 'alias.herd' config_value = f'!clowder herd {self.repo_path.relative_to(ENVIRONMENT.clowder_dir)}' CONSOLE.stdout(" - Update git herd alias") self.git_config_unset_all_local(config_variable) self.git_config_add_local(config_variable, config_value)
def checkout(args) -> None: """Clowder checkout command private implementation""" projects = Config().process_projects_arg(args.projects) projects = CLOWDER_CONTROLLER.filter_projects(CLOWDER_CONTROLLER.projects, projects) for project in projects: CONSOLE.stdout(project.status()) project.checkout(args.branch[0])
def print_validation(self) -> None: """Print validation messages""" if not existing_git_repo(self.repo_path): return if not self.validate_repo(): CONSOLE.stdout( f'Dirty repo. Please stash, commit, or discard your changes') self.status_verbose()