# Copyright (C) 2008 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 collections import functools import itertools from command import Command from command import DEFAULT_LOCAL_JOBS from error import RepoError from error import RepoExitError from git_command import git from progress import Progress from repo_logging import RepoLogger logger = RepoLogger(__file__) class AbandonError(RepoExitError): """Exit error when abandon command fails.""" class Abandon(Command): COMMON = True helpSummary = "Permanently abandon a development branch" helpUsage = """ %prog [--all | ] [...] This subcommand permanently abandons a development branch by deleting it (and all its history) from your local repository. It is equivalent to "git branch -D ". """ PARALLEL_JOBS = DEFAULT_LOCAL_JOBS def _Options(self, p): p.add_option( "--all", dest="all", action="store_true", help="delete all branches in all projects", ) def ValidateOptions(self, opt, args): if not opt.all and not args: self.Usage() if not opt.all: branches = args[0].split() invalid_branches = [ x for x in branches if not git.check_ref_format(f"heads/{x}") ] if invalid_branches: self.OptionParser.error( f"{invalid_branches} are not valid branch names" ) else: args.insert(0, "'All local branches'") @classmethod def _ExecuteOne(cls, all_branches, nb, project_idx): """Abandon one project.""" project = cls.get_parallel_context()["projects"][project_idx] if all_branches: branches = project.GetBranches() else: branches = nb ret = {} errors = [] for name in branches: status = None try: status = project.AbandonBranch(name) except RepoError as e: status = False errors.append(e) if status is not None: ret[name] = status return (ret, project_idx, errors) def Execute(self, opt, args): nb = args[0].split() err = collections.defaultdict(list) success = collections.defaultdict(list) aggregate_errors = [] all_projects = self.GetProjects( args[1:], all_manifests=not opt.this_manifest_only ) _RelPath = lambda p: p.RelPath(local=opt.this_manifest_only) def _ProcessResults(_pool, pm, states): for results, project_idx, errors in states: project = all_projects[project_idx] for branch, status in results.items(): if status: success[branch].append(project) else: err[branch].append(project) aggregate_errors.extend(errors) pm.update(msg="") with self.ParallelContext(): self.get_parallel_context()["projects"] = all_projects self.ExecuteInParallel( opt.jobs, functools.partial(self._ExecuteOne, opt.all, nb), range(len(all_projects)), callback=_ProcessResults, output=Progress( f"Abandon {nb}", len(all_projects), quiet=opt.quiet ), chunksize=1, ) width = max( itertools.chain( [25], (len(x) for x in itertools.chain(success, err)) ) ) if err: for br in err.keys(): err_msg = "error: cannot abandon %s" % br logger.error(err_msg) for proj in err[br]: logger.error(" " * len(err_msg) + " | %s", _RelPath(proj)) raise AbandonError(aggregate_errors=aggregate_errors) elif not success: logger.error("error: no project has local branch(es) : %s", nb) raise AbandonError(aggregate_errors=aggregate_errors) else: # Everything below here is displaying status. if opt.quiet: return print("Abandoned branches:") for br in success.keys(): if len(all_projects) > 1 and len(all_projects) == len( success[br] ): result = "all project" else: result = "%s" % ( ("\n" + " " * width + "| ").join( _RelPath(p) for p in success[br] ) ) print(f"{br}{' ' * (width - len(br))}| {result}\n")