def sync_repo_to_ref(self, repo: Repo, ref: str) -> None: repo_path = self.workspace_path / repo.dest status = get_git_status(repo_path) if status.dirty: raise Error(f"git repo is dirty: cannot sync to ref: {ref}") try: self.run_git(repo_path, "reset", "--hard", ref) except Error: raise Error("updating ref failed")
def check_branch(self, repo: Repo) -> Tuple[Optional[Error], str]: """Check that the current branch: * exists * matches the one in the manifest * Raise Error if the branch does not exist (because we can't do anything else in that case) * _Return_ on Error if the current branch does not match the one in the manifest - because we still want to run `git merge @upstream` in that case * Otherwise, return the current branch """ repo_path = self.workspace_path / repo.dest current_branch = None try: current_branch = get_current_branch(repo_path) except Error: raise Error("Not on any branch") if current_branch and current_branch != repo.branch: return ( IncorrectBranch(actual=current_branch, expected=repo.branch), current_branch, ) else: return None, current_branch
def run(args: argparse.Namespace) -> None: workspace_path = args.workspace_path or Path.cwd() num_jobs = get_num_jobs(args) cfg_path = workspace_path / ".tsrc" / "config.yml" if cfg_path.exists(): raise Error( f"Workspace already configured. `{cfg_path}` already exists") ui.info_1("Configuring workspace in", ui.bold, workspace_path) clone_path = workspace_path / ".tsrc/manifest" local_manifest = LocalManifest(clone_path) local_manifest.init(url=args.manifest_url, branch=args.manifest_branch) manifest_branch = local_manifest.current_branch() workspace_config = WorkspaceConfig( manifest_url=args.manifest_url, manifest_branch=manifest_branch, clone_all_repos=args.clone_all_repos, repo_groups=args.groups or [], shallow_clones=args.shallow_clones, singular_remote=args.singular_remote, ) workspace_config.save_to_file(cfg_path) workspace = Workspace(workspace_path) manifest = workspace.get_manifest() workspace.repos = repos_from_config(manifest, workspace_config) workspace.clone_missing(num_jobs=num_jobs) workspace.set_remotes(num_jobs=num_jobs) workspace.perform_filesystem_operations() ui.info_2("Workspace initialized") ui.info_2("Configuration written in", ui.bold, workspace.cfg_path)
def clone_repo(self, repo: Repo) -> str: """Clone a missing repo.""" # Note: # Must use the correct remote(s) and branch when cloning, # *and* must reset the repo to the correct state if `tag` or # `sha1` were set in the manifest configuration. repo_path = self.workspace_path / repo.dest parent = repo_path.parent name = repo_path.name parent.mkdir(parents=True, exist_ok=True) remote = self._choose_remote(repo) remote_name = remote.name remote_url = remote.url clone_args = ["clone", "--origin", remote_name, remote_url] ref = None if repo.tag: ref = repo.tag elif repo.branch: ref = repo.branch if ref: clone_args.extend(["--branch", ref]) if self.shallow: clone_args.extend(["--depth", "1"]) if not repo.ignore_submodules: clone_args.append("--recurse-submodules") clone_args.append(name) try: self.run_git(parent, *clone_args) summary = f"{repo.dest} cloned from {remote_url}" if ref: summary += f" (on {ref})" return summary except Error: raise Error("Cloning failed")
def check_shallow_with_sha1(self, repo: Repo) -> None: if not repo.sha1: return if self.shallow: message = textwrap.dedent( f"Cannot use --shallow with a fixed sha1 ({repo.sha1})\n" "Consider using a tag instead") raise Error(message)
def _pick_remotes(self, repo: Repo) -> List[Remote]: if self.remote_name: for remote in repo.remotes: if remote.name == self.remote_name: return [remote] message = f"Remote {self.remote_name} not found for repository {repo.dest}" raise Error(message) return repo.remotes
def process(self, index: int, count: int, repo: Repo) -> Outcome: # We just need to compute a summary here with the log between # self.from_ref and self.to_ref # # Note: make sure that when there is no diff between # self.from_ref and self.to_ref, the summary is empty, # so that the repo is not shown by OutcomeCollection.print_summary() repo_path = self.workspace_path / repo.dest if not repo_path.exists(): raise MissingRepo(repo.dest) # The main reason for the `git log` command to fail is if `self.from_ref` or # `self.to_ref` references are not found for the repo, so check for this case # explicitly rc, _ = run_git_captured(repo_path, "rev-parse", self.from_ref, check=False) if rc != 0: raise Error(f"{self.from_ref} not found") rc, _ = run_git_captured(repo_path, "rev-parse", self.to_ref, check=False) if rc != 0: raise Error(f"{self.to_ref} not found") colors = ["green", "reset", "yellow", "reset", "bold blue", "reset"] log_format = "%m {}%h{} - {}%d{} %s {}<%an>{}" log_format = log_format.format(*("%C({})".format(x) for x in colors)) cmd = [ "log", "--color=always", f"--pretty=format:{log_format}", f"{self.from_ref}...{self.to_ref}", ] rc, out = run_git_captured(repo_path, *cmd, check=True) if out: lines = [repo.dest, "-" * len(repo.dest), out] return Outcome.from_lines(lines) else: return Outcome.empty()
def fetch(self, repo: Repo) -> None: repo_path = self.workspace_path / repo.dest for remote in self._pick_remotes(repo): try: self.info_3("Fetching", remote.name) cmd = ["fetch", "--tags", "--prune", remote.name] if self.force: cmd.append("--force") self.run_git(repo_path, *cmd) except Error: raise Error(f"fetch from '{remote.name}' failed")
def process(self, index: int, count: int, item: FileSystemOperation) -> Outcome: # Note: we don't want to run this Task in parallel, just in case # the order of filesystem operations matters, so we can always # return an empty Outcome description = item.describe(self.workspace_path) self.info_count(index, count, description) try: item.perform(self.workspace_path) except OSError as e: raise Error(str(e)) return Outcome.empty()
def _choose_remote(self, repo: Repo) -> Remote: if self.remote_name: for remote in repo.remotes: if remote.name == self.remote_name: return remote message = ( f"Remote '{self.remote_name}' not found for repository '{repo.dest}'" ) raise Error(message) return repo.remotes[0]
def reset_repo(self, repo: Repo) -> str: ref = repo.sha1 if not ref: return "" else: self.info_2("Resetting", repo.dest, "to", ref) repo_path = self.workspace_path / repo.dest try: self.run_git(repo_path, "reset", "--hard", ref) except Error: raise Error("Resetting to", ref, "failed") summary = f" and reset to {ref}" return summary
def check_link(*, source: Path, target: Path) -> bool: remove_link = False if source.exists() and not source.is_symlink(): raise Error("Specified symlink source exists but is not a link") return False if source.is_symlink(): if source.exists(): # symlink exists and points to some target current_target = Path(os.readlink(str(source))) if current_target.resolve() == target.resolve(): ui.info_3("Leaving existing link") return False else: ui.info_3("Replacing existing link") remove_link = True else: # symlink exists, but points to a non-existent target ui.info_3("Replacing broken link") remove_link = True if remove_link: os.unlink(source) return True
def sync_repo_to_branch(self, repo: Repo, *, current_branch: str) -> str: repo_path = self.workspace_path / repo.dest if self.parallel: # Note: we want the summary to: # * be empty if the repo was already up-to-date # * contain the diffstat if the merge with upstream succeeds rc, out = run_git_captured( repo_path, "log", "--oneline", "HEAD..@{upstream}", check=False ) if rc == 0 and not out: return "" _, merge_output = run_git_captured( repo_path, "merge", "--ff-only", "@{upstream}", check=True ) return merge_output else: # Note: no summary here, because the output of `git merge` # is not captured, so the diffstat or the "Already up to # date" message are directly shown to the user try: self.run_git(repo_path, "merge", "--ff-only", "@{upstream}") except Error: raise Error("updating branch failed") return ""