| # Copyright (C) 2012 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import enum |
| import functools |
| import io |
| import json |
| import optparse |
| import sys |
| from typing import Any, Dict, List, NamedTuple |
| |
| from color import Coloring |
| from command import DEFAULT_LOCAL_JOBS |
| from command import PagedCommand |
| from git_refs import R_HEADS |
| from git_refs import R_M |
| |
| |
| class BranchInfo(NamedTuple): |
| """Holds information about a branch in a project.""" |
| |
| relpath: str |
| name: str |
| commits: Any |
| date: str |
| is_current: bool |
| |
| |
| class OutputFormat(enum.Enum): |
| """Type for the requested output format.""" |
| |
| # Human-readable text output. |
| TEXT = enum.auto() |
| |
| # Machine-readable JSON output. |
| JSON = enum.auto() |
| |
| |
| class _Coloring(Coloring): |
| def __init__(self, config): |
| Coloring.__init__(self, config, "status") |
| |
| |
| class Info(PagedCommand): |
| COMMON = True |
| PARALLEL_JOBS = DEFAULT_LOCAL_JOBS |
| helpSummary = ( |
| "Get info on the manifest branch, current branch or unmerged branches" |
| ) |
| helpUsage = "%prog [-dl] [-o [-c]] [--format=<format>] [<project>...]" |
| |
| def _Options(self, p): |
| p.add_option( |
| "-d", |
| "--diff", |
| dest="all", |
| action="store_true", |
| help="show full info and commit diff including remote branches", |
| ) |
| p.add_option( |
| "-o", |
| "--overview", |
| action="store_true", |
| help="show overview of all local commits", |
| ) |
| p.add_option( |
| "--include-summary", |
| action="store_true", |
| default=True, |
| help="include manifest summary (default: true)", |
| ) |
| p.add_option( |
| "--no-include-summary", |
| dest="include_summary", |
| action="store_false", |
| help="exclude manifest summary", |
| ) |
| p.add_option( |
| "--include-projects", |
| action="store_true", |
| default=True, |
| help="include project details (default: true)", |
| ) |
| p.add_option( |
| "--no-include-projects", |
| dest="include_projects", |
| action="store_false", |
| help="exclude project details", |
| ) |
| p.add_option( |
| "-c", |
| "--current-branch", |
| action="store_true", |
| help="consider only checked out branches", |
| ) |
| p.add_option( |
| "--no-current-branch", |
| dest="current_branch", |
| action="store_false", |
| help="consider all local branches", |
| ) |
| # Turn this into a warning & remove this someday. |
| p.add_option( |
| "-b", |
| dest="current_branch", |
| action="store_true", |
| help=optparse.SUPPRESS_HELP, |
| ) |
| p.add_option( |
| "-l", |
| "--local-only", |
| dest="local", |
| action="store_true", |
| help="disable all remote operations", |
| ) |
| formats = tuple(x.lower() for x in OutputFormat.__members__.keys()) |
| p.add_option( |
| "--format", |
| default=OutputFormat.TEXT.name.lower(), |
| choices=formats, |
| help=f"output format: {', '.join(formats)} (default: %default)", |
| ) |
| |
| def WantPager(self, opt): |
| return OutputFormat[opt.format.upper()] == OutputFormat.TEXT |
| |
| def ValidateOptions(self, opt, args): |
| output_format = OutputFormat[opt.format.upper()] |
| if output_format == OutputFormat.JSON: |
| if opt.all: |
| self.OptionParser.error("--diff is not supported with JSON") |
| if opt.overview: |
| self.OptionParser.error("--overview is not supported with JSON") |
| |
| def Execute(self, opt, args): |
| if not opt.this_manifest_only: |
| self.manifest = self.manifest.outer_client |
| |
| self.TryOverrideManifestWithSmartSync() |
| |
| output_format = OutputFormat[opt.format.upper()] |
| if output_format == OutputFormat.JSON: |
| self._ExecuteJson(opt, args) |
| else: |
| self._ExecuteText(opt, args) |
| |
| def _ExecuteText(self, opt, args) -> None: |
| """Output info as human-readable text.""" |
| self.out = _Coloring(self.client.globalConfig) |
| self.heading = self.out.printer("heading", attr="bold") |
| self.headtext = self.out.nofmt_printer("headtext", fg="yellow") |
| self.redtext = self.out.printer("redtext", fg="red") |
| self.sha = self.out.printer("sha", fg="yellow") |
| self.text = self.out.nofmt_printer("text") |
| self.dimtext = self.out.printer("dimtext", attr="dim") |
| |
| self.opt = opt |
| |
| if opt.include_summary: |
| self._printSummary() |
| |
| if not opt.include_projects: |
| return |
| elif not opt.overview: |
| self._printDiffInfo(opt, args) |
| else: |
| self._printCommitOverview(opt, args) |
| |
| def _getSummaryData(self) -> Dict[str, Any]: |
| """Gather manifest summary data as a dict.""" |
| manifestConfig = self.manifest.manifestProject.config |
| mergeBranch = manifestConfig.GetBranch("default").merge |
| manifestGroups = self.manifest.GetManifestGroupsStr() |
| sp = self.manifest.superproject |
| srev = sp.commit_id if sp and sp.commit_id else None |
| return { |
| "manifest_branch": self.manifest.default.revisionExpr or "", |
| "manifest_merge_branch": mergeBranch or "", |
| "manifest_groups": manifestGroups, |
| "superproject_revision": srev, |
| } |
| |
| @classmethod |
| def _getProjectData(cls, project) -> Dict[str, Any]: |
| """Gather project data as a dict.""" |
| data = { |
| "name": project.name, |
| "mount_path": project.worktree, |
| "current_revision": project.GetHeadRevisionId() |
| or project.GetRevisionId(), |
| "manifest_revision": project.revisionExpr, |
| "local_branches": list(project.GetBranches()), |
| } |
| currentBranch = project.CurrentBranch |
| if currentBranch: |
| data["current_branch"] = currentBranch |
| return data |
| |
| @classmethod |
| def _ProjectDataHelper(cls, project_idx: int) -> Dict[str, Any]: |
| """Helper to get project data in parallel.""" |
| project = cls.get_parallel_context()["projects"][project_idx] |
| return cls._getProjectData(project) |
| |
| def _ExecuteJson(self, opt, args) -> None: |
| """Output info as JSON.""" |
| result = {} |
| if opt.include_summary: |
| result["summary"] = self._getSummaryData() |
| if opt.include_projects: |
| projs = self.GetProjects( |
| args, all_manifests=not opt.this_manifest_only |
| ) |
| project_data = [] |
| |
| def _ProcessResults(_pool, _output, results): |
| project_data.extend(results) |
| |
| with self.ParallelContext(): |
| self.get_parallel_context()["projects"] = projs |
| self.ExecuteInParallel( |
| opt.jobs, |
| self._ProjectDataHelper, |
| range(len(projs)), |
| callback=_ProcessResults, |
| ordered=True, |
| chunksize=1, |
| ) |
| result["projects"] = project_data |
| |
| json_settings = { |
| # JSON style guide says Unicode characters are fully allowed. |
| "ensure_ascii": False, |
| # We use 2 space indent to match JSON style guide. |
| "indent": 2, |
| "separators": (",", ": "), |
| "sort_keys": True, |
| } |
| sys.stdout.write(json.dumps(result, **json_settings) + "\n") |
| |
| def _printSummary(self) -> None: |
| """Print manifest summary in text format.""" |
| data = self._getSummaryData() |
| self.heading("Manifest branch: ") |
| self.headtext(data["manifest_branch"]) |
| self.out.nl() |
| self.heading("Manifest merge branch: ") |
| self.headtext(data["manifest_merge_branch"]) |
| self.out.nl() |
| self.heading("Manifest groups: ") |
| self.headtext(data["manifest_groups"]) |
| self.out.nl() |
| self.heading("Superproject revision: ") |
| self.headtext(data["superproject_revision"] or "None") |
| self.out.nl() |
| self.printSeparator() |
| |
| def printSeparator(self): |
| self.text("----------------------------") |
| self.out.nl() |
| |
| @classmethod |
| def _DiffHelper(cls, project_idx: int, opt: Any) -> str: |
| """Helper for ParallelContext to get diff info for a project.""" |
| buf = io.StringIO() |
| project = cls.get_parallel_context()["projects"][project_idx] |
| config = cls.get_parallel_context()["config"] |
| |
| out = _Coloring(config) |
| out.redirect(buf) |
| |
| heading = out.printer("heading", attr="bold") |
| headtext = out.nofmt_printer("headtext", fg="yellow") |
| redtext = out.printer("redtext", fg="red") |
| sha = out.printer("sha", fg="yellow") |
| text = out.nofmt_printer("text") |
| dimtext = out.printer("dimtext", attr="dim") |
| |
| heading("Project: ") |
| headtext(project.name) |
| out.nl() |
| |
| heading("Mount path: ") |
| headtext(project.worktree) |
| out.nl() |
| |
| heading("Current revision: ") |
| headtext(project.GetHeadRevisionId() or project.GetRevisionId()) |
| out.nl() |
| |
| currentBranch = project.CurrentBranch |
| if currentBranch: |
| heading("Current branch: ") |
| headtext(currentBranch) |
| out.nl() |
| |
| heading("Manifest revision: ") |
| headtext(project.revisionExpr) |
| out.nl() |
| |
| localBranches = list(project.GetBranches().keys()) |
| heading("Local Branches: ") |
| redtext(str(len(localBranches))) |
| if localBranches: |
| text(" [") |
| text(", ".join(localBranches)) |
| text("]") |
| out.nl() |
| |
| if opt.all: |
| if not opt.local: |
| project.Sync_NetworkHalf(quiet=True, current_branch_only=True) |
| |
| branch = project.manifest.manifestProject.config.GetBranch( |
| "default" |
| ).merge |
| if branch.startswith(R_HEADS): |
| branch = branch[len(R_HEADS) :] |
| logTarget = R_M + branch |
| |
| bareTmp = project.bare_git._bare |
| project.bare_git._bare = False |
| localCommits = project.bare_git.rev_list( |
| "--abbrev=8", |
| "--abbrev-commit", |
| "--pretty=oneline", |
| logTarget + "..", |
| "--", |
| ) |
| |
| originCommits = project.bare_git.rev_list( |
| "--abbrev=8", |
| "--abbrev-commit", |
| "--pretty=oneline", |
| ".." + logTarget, |
| "--", |
| ) |
| project.bare_git._bare = bareTmp |
| |
| heading("Local Commits: ") |
| redtext(str(len(localCommits))) |
| dimtext(" (on current branch)") |
| out.nl() |
| |
| for c in localCommits: |
| split = c.split() |
| sha(split[0] + " ") |
| text(" ".join(split[1:])) |
| out.nl() |
| |
| text("----------------------------") |
| out.nl() |
| |
| heading("Remote Commits: ") |
| redtext(str(len(originCommits))) |
| out.nl() |
| |
| for c in originCommits: |
| split = c.split() |
| sha(split[0] + " ") |
| text(" ".join(split[1:])) |
| out.nl() |
| |
| text("----------------------------") |
| out.nl() |
| |
| return buf.getvalue() |
| |
| def _printDiffInfo(self, opt, args): |
| projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only) |
| |
| def _ProcessResults(_pool, _output, results): |
| for output in results: |
| if output: |
| print(output, end="") |
| |
| with self.ParallelContext(): |
| self.get_parallel_context()["projects"] = projs |
| self.get_parallel_context()[ |
| "config" |
| ] = self.manifest.manifestProject.config |
| |
| self.ExecuteInParallel( |
| opt.jobs, |
| functools.partial(self._DiffHelper, opt=opt), |
| range(len(projs)), |
| callback=_ProcessResults, |
| ordered=True, |
| chunksize=1, |
| ) |
| |
| @classmethod |
| def _OverviewHelper(cls, project_idx: int, opt: Any) -> List[BranchInfo]: |
| """Helper to get overview of uploadable branches.""" |
| project = cls.get_parallel_context()["projects"][project_idx] |
| |
| branches = [] |
| br = [project.GetUploadableBranch(x) for x in project.GetBranches()] |
| br = [x for x in br if x] |
| if opt.current_branch: |
| br = [x for x in br if x.name == project.CurrentBranch] |
| |
| for b in br: |
| branches.append( |
| BranchInfo( |
| relpath=project.RelPath(local=opt.this_manifest_only), |
| name=b.name, |
| commits=b.commits, |
| date=b.date, |
| is_current=b.name == project.CurrentBranch, |
| ) |
| ) |
| return branches |
| |
| def _printCommitOverview(self, opt, args): |
| projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only) |
| |
| all_branches = [] |
| |
| def _ProcessResults(_pool, _output, results): |
| for branches in results: |
| all_branches.extend(branches) |
| |
| with self.ParallelContext(): |
| self.get_parallel_context()["projects"] = projs |
| |
| self.ExecuteInParallel( |
| opt.jobs, |
| functools.partial(self._OverviewHelper, opt=opt), |
| range(len(projs)), |
| callback=_ProcessResults, |
| ordered=True, |
| chunksize=1, |
| ) |
| |
| if not all_branches: |
| return |
| |
| self.out.nl() |
| self.heading("Projects Overview") |
| current_relpath = None |
| |
| for branch in all_branches: |
| if current_relpath != branch.relpath: |
| current_relpath = branch.relpath |
| self.out.nl() |
| self.headtext(current_relpath) |
| self.out.nl() |
| |
| commits = branch.commits |
| date = branch.date |
| self.text( |
| "%s %-33s (%2d commit%s, %s)" |
| % ( |
| branch.is_current and "*" or " ", |
| branch.name, |
| len(commits), |
| len(commits) != 1 and "s" or "", |
| date, |
| ) |
| ) |
| self.out.nl() |
| |
| for commit in commits: |
| split = commit.split() |
| self.text(f"{'':38}{'-'} ") |
| self.sha(split[0] + " ") |
| self.text(" ".join(split[1:])) |
| self.out.nl() |