xref: /aosp_15_r20/tools/repohooks/pre-upload.py (revision d68f33bc6fb0cc2476107c2af0573a2f5a63dfc1)
1*d68f33bcSAndroid Build Coastguard Worker#!/usr/bin/env python3
2*d68f33bcSAndroid Build Coastguard Worker# Copyright 2016 The Android Open Source Project
3*d68f33bcSAndroid Build Coastguard Worker#
4*d68f33bcSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
5*d68f33bcSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
6*d68f33bcSAndroid Build Coastguard Worker# You may obtain a copy of the License at
7*d68f33bcSAndroid Build Coastguard Worker#
8*d68f33bcSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
9*d68f33bcSAndroid Build Coastguard Worker#
10*d68f33bcSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
11*d68f33bcSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
12*d68f33bcSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13*d68f33bcSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
14*d68f33bcSAndroid Build Coastguard Worker# limitations under the License.
15*d68f33bcSAndroid Build Coastguard Worker
16*d68f33bcSAndroid Build Coastguard Worker"""Repo pre-upload hook.
17*d68f33bcSAndroid Build Coastguard Worker
18*d68f33bcSAndroid Build Coastguard WorkerNormally this is loaded indirectly by repo itself, but it can be run directly
19*d68f33bcSAndroid Build Coastguard Workerwhen developing.
20*d68f33bcSAndroid Build Coastguard Worker"""
21*d68f33bcSAndroid Build Coastguard Worker
22*d68f33bcSAndroid Build Coastguard Workerimport argparse
23*d68f33bcSAndroid Build Coastguard Workerimport concurrent.futures
24*d68f33bcSAndroid Build Coastguard Workerimport datetime
25*d68f33bcSAndroid Build Coastguard Workerimport os
26*d68f33bcSAndroid Build Coastguard Workerimport signal
27*d68f33bcSAndroid Build Coastguard Workerimport sys
28*d68f33bcSAndroid Build Coastguard Workerfrom typing import List, Optional
29*d68f33bcSAndroid Build Coastguard Worker
30*d68f33bcSAndroid Build Coastguard Worker
31*d68f33bcSAndroid Build Coastguard Worker# Assert some minimum Python versions as we don't test or support any others.
32*d68f33bcSAndroid Build Coastguard Workerif sys.version_info < (3, 6):
33*d68f33bcSAndroid Build Coastguard Worker    print('repohooks: error: Python-3.6+ is required', file=sys.stderr)
34*d68f33bcSAndroid Build Coastguard Worker    sys.exit(1)
35*d68f33bcSAndroid Build Coastguard Worker
36*d68f33bcSAndroid Build Coastguard Worker
37*d68f33bcSAndroid Build Coastguard Worker_path = os.path.dirname(os.path.realpath(__file__))
38*d68f33bcSAndroid Build Coastguard Workerif sys.path[0] != _path:
39*d68f33bcSAndroid Build Coastguard Worker    sys.path.insert(0, _path)
40*d68f33bcSAndroid Build Coastguard Workerdel _path
41*d68f33bcSAndroid Build Coastguard Worker
42*d68f33bcSAndroid Build Coastguard Worker# We have to import our local modules after the sys.path tweak.  We can't use
43*d68f33bcSAndroid Build Coastguard Worker# relative imports because this is an executable program, not a module.
44*d68f33bcSAndroid Build Coastguard Worker# pylint: disable=wrong-import-position
45*d68f33bcSAndroid Build Coastguard Workerimport rh
46*d68f33bcSAndroid Build Coastguard Workerimport rh.results
47*d68f33bcSAndroid Build Coastguard Workerimport rh.config
48*d68f33bcSAndroid Build Coastguard Workerimport rh.git
49*d68f33bcSAndroid Build Coastguard Workerimport rh.hooks
50*d68f33bcSAndroid Build Coastguard Workerimport rh.terminal
51*d68f33bcSAndroid Build Coastguard Workerimport rh.utils
52*d68f33bcSAndroid Build Coastguard Worker
53*d68f33bcSAndroid Build Coastguard Worker
54*d68f33bcSAndroid Build Coastguard Worker# Repohooks homepage.
55*d68f33bcSAndroid Build Coastguard WorkerREPOHOOKS_URL = 'https://android.googlesource.com/platform/tools/repohooks/'
56*d68f33bcSAndroid Build Coastguard Worker
57*d68f33bcSAndroid Build Coastguard Worker
58*d68f33bcSAndroid Build Coastguard Workerclass Output(object):
59*d68f33bcSAndroid Build Coastguard Worker    """Class for reporting hook status."""
60*d68f33bcSAndroid Build Coastguard Worker
61*d68f33bcSAndroid Build Coastguard Worker    COLOR = rh.terminal.Color()
62*d68f33bcSAndroid Build Coastguard Worker    COMMIT = COLOR.color(COLOR.CYAN, 'COMMIT')
63*d68f33bcSAndroid Build Coastguard Worker    RUNNING = COLOR.color(COLOR.YELLOW, 'RUNNING')
64*d68f33bcSAndroid Build Coastguard Worker    PASSED = COLOR.color(COLOR.GREEN, 'PASSED')
65*d68f33bcSAndroid Build Coastguard Worker    FAILED = COLOR.color(COLOR.RED, 'FAILED')
66*d68f33bcSAndroid Build Coastguard Worker    WARNING = COLOR.color(COLOR.YELLOW, 'WARNING')
67*d68f33bcSAndroid Build Coastguard Worker    FIXUP = COLOR.color(COLOR.MAGENTA, 'FIXUP')
68*d68f33bcSAndroid Build Coastguard Worker
69*d68f33bcSAndroid Build Coastguard Worker    # How long a hook is allowed to run before we warn that it is "too slow".
70*d68f33bcSAndroid Build Coastguard Worker    _SLOW_HOOK_DURATION = datetime.timedelta(seconds=30)
71*d68f33bcSAndroid Build Coastguard Worker
72*d68f33bcSAndroid Build Coastguard Worker    def __init__(self, project_name):
73*d68f33bcSAndroid Build Coastguard Worker        """Create a new Output object for a specified project.
74*d68f33bcSAndroid Build Coastguard Worker
75*d68f33bcSAndroid Build Coastguard Worker        Args:
76*d68f33bcSAndroid Build Coastguard Worker          project_name: name of project.
77*d68f33bcSAndroid Build Coastguard Worker        """
78*d68f33bcSAndroid Build Coastguard Worker        self.project_name = project_name
79*d68f33bcSAndroid Build Coastguard Worker        self.hooks = None
80*d68f33bcSAndroid Build Coastguard Worker        self.num_hooks = None
81*d68f33bcSAndroid Build Coastguard Worker        self.num_commits = None
82*d68f33bcSAndroid Build Coastguard Worker        self.commit_index = 0
83*d68f33bcSAndroid Build Coastguard Worker        self.success = True
84*d68f33bcSAndroid Build Coastguard Worker        self.start_time = datetime.datetime.now()
85*d68f33bcSAndroid Build Coastguard Worker        self.hook_start_time = None
86*d68f33bcSAndroid Build Coastguard Worker        # Cache number of invisible characters in our banner.
87*d68f33bcSAndroid Build Coastguard Worker        self._banner_esc_chars = len(self.COLOR.color(self.COLOR.YELLOW, ''))
88*d68f33bcSAndroid Build Coastguard Worker
89*d68f33bcSAndroid Build Coastguard Worker    def set_num_commits(self, num_commits: int) -> None:
90*d68f33bcSAndroid Build Coastguard Worker        """Keep track of how many commits we'll be running.
91*d68f33bcSAndroid Build Coastguard Worker
92*d68f33bcSAndroid Build Coastguard Worker        Args:
93*d68f33bcSAndroid Build Coastguard Worker          num_commits: Number of commits to be run.
94*d68f33bcSAndroid Build Coastguard Worker        """
95*d68f33bcSAndroid Build Coastguard Worker        self.num_commits = num_commits
96*d68f33bcSAndroid Build Coastguard Worker        self.commit_index = 1
97*d68f33bcSAndroid Build Coastguard Worker
98*d68f33bcSAndroid Build Coastguard Worker    def commit_start(self, hooks, commit, commit_summary):
99*d68f33bcSAndroid Build Coastguard Worker        """Emit status for new commit.
100*d68f33bcSAndroid Build Coastguard Worker
101*d68f33bcSAndroid Build Coastguard Worker        Args:
102*d68f33bcSAndroid Build Coastguard Worker          hooks: All the hooks to be run for this commit.
103*d68f33bcSAndroid Build Coastguard Worker          commit: commit hash.
104*d68f33bcSAndroid Build Coastguard Worker          commit_summary: commit summary.
105*d68f33bcSAndroid Build Coastguard Worker        """
106*d68f33bcSAndroid Build Coastguard Worker        status_line = (
107*d68f33bcSAndroid Build Coastguard Worker            f'[{self.COMMIT} '
108*d68f33bcSAndroid Build Coastguard Worker            f'{self.commit_index}/{self.num_commits} '
109*d68f33bcSAndroid Build Coastguard Worker            f'{commit[0:12]}] {commit_summary}'
110*d68f33bcSAndroid Build Coastguard Worker        )
111*d68f33bcSAndroid Build Coastguard Worker        rh.terminal.print_status_line(status_line, print_newline=True)
112*d68f33bcSAndroid Build Coastguard Worker        self.commit_index += 1
113*d68f33bcSAndroid Build Coastguard Worker
114*d68f33bcSAndroid Build Coastguard Worker        # Initialize the pending hooks line too.
115*d68f33bcSAndroid Build Coastguard Worker        self.hooks = set(hooks)
116*d68f33bcSAndroid Build Coastguard Worker        self.num_hooks = len(hooks)
117*d68f33bcSAndroid Build Coastguard Worker        self.hook_banner()
118*d68f33bcSAndroid Build Coastguard Worker
119*d68f33bcSAndroid Build Coastguard Worker    def hook_banner(self):
120*d68f33bcSAndroid Build Coastguard Worker        """Display the banner for current set of hooks."""
121*d68f33bcSAndroid Build Coastguard Worker        pending = ', '.join(x.name for x in self.hooks)
122*d68f33bcSAndroid Build Coastguard Worker        status_line = (
123*d68f33bcSAndroid Build Coastguard Worker            f'[{self.RUNNING} '
124*d68f33bcSAndroid Build Coastguard Worker            f'{self.num_hooks - len(self.hooks)}/{self.num_hooks}] '
125*d68f33bcSAndroid Build Coastguard Worker            f'{pending}'
126*d68f33bcSAndroid Build Coastguard Worker        )
127*d68f33bcSAndroid Build Coastguard Worker        if self._banner_esc_chars and sys.stderr.isatty():
128*d68f33bcSAndroid Build Coastguard Worker            cols = os.get_terminal_size(sys.stderr.fileno()).columns
129*d68f33bcSAndroid Build Coastguard Worker            status_line = status_line[0:cols + self._banner_esc_chars]
130*d68f33bcSAndroid Build Coastguard Worker        rh.terminal.print_status_line(status_line)
131*d68f33bcSAndroid Build Coastguard Worker
132*d68f33bcSAndroid Build Coastguard Worker    def hook_finish(self, hook, duration):
133*d68f33bcSAndroid Build Coastguard Worker        """Finish processing any per-hook state."""
134*d68f33bcSAndroid Build Coastguard Worker        self.hooks.remove(hook)
135*d68f33bcSAndroid Build Coastguard Worker        if duration >= self._SLOW_HOOK_DURATION:
136*d68f33bcSAndroid Build Coastguard Worker            d = rh.utils.timedelta_str(duration)
137*d68f33bcSAndroid Build Coastguard Worker            self.hook_warning(
138*d68f33bcSAndroid Build Coastguard Worker                hook,
139*d68f33bcSAndroid Build Coastguard Worker                f'This hook took {d} to finish which is fairly slow for '
140*d68f33bcSAndroid Build Coastguard Worker                'developers.\nPlease consider moving the check to the '
141*d68f33bcSAndroid Build Coastguard Worker                'server/CI system instead.')
142*d68f33bcSAndroid Build Coastguard Worker
143*d68f33bcSAndroid Build Coastguard Worker        # Show any hooks still pending.
144*d68f33bcSAndroid Build Coastguard Worker        if self.hooks:
145*d68f33bcSAndroid Build Coastguard Worker            self.hook_banner()
146*d68f33bcSAndroid Build Coastguard Worker
147*d68f33bcSAndroid Build Coastguard Worker    def hook_error(self, hook, error):
148*d68f33bcSAndroid Build Coastguard Worker        """Print an error for a single hook.
149*d68f33bcSAndroid Build Coastguard Worker
150*d68f33bcSAndroid Build Coastguard Worker        Args:
151*d68f33bcSAndroid Build Coastguard Worker          hook: The hook that generated the output.
152*d68f33bcSAndroid Build Coastguard Worker          error: error string.
153*d68f33bcSAndroid Build Coastguard Worker        """
154*d68f33bcSAndroid Build Coastguard Worker        self.error(f'{hook.name} hook', error)
155*d68f33bcSAndroid Build Coastguard Worker
156*d68f33bcSAndroid Build Coastguard Worker    def hook_warning(self, hook, warning):
157*d68f33bcSAndroid Build Coastguard Worker        """Print a warning for a single hook.
158*d68f33bcSAndroid Build Coastguard Worker
159*d68f33bcSAndroid Build Coastguard Worker        Args:
160*d68f33bcSAndroid Build Coastguard Worker          hook: The hook that generated the output.
161*d68f33bcSAndroid Build Coastguard Worker          warning: warning string.
162*d68f33bcSAndroid Build Coastguard Worker        """
163*d68f33bcSAndroid Build Coastguard Worker        status_line = f'[{self.WARNING}] {hook.name}'
164*d68f33bcSAndroid Build Coastguard Worker        rh.terminal.print_status_line(status_line, print_newline=True)
165*d68f33bcSAndroid Build Coastguard Worker        print(warning, file=sys.stderr)
166*d68f33bcSAndroid Build Coastguard Worker
167*d68f33bcSAndroid Build Coastguard Worker    def error(self, header, error):
168*d68f33bcSAndroid Build Coastguard Worker        """Print a general error.
169*d68f33bcSAndroid Build Coastguard Worker
170*d68f33bcSAndroid Build Coastguard Worker        Args:
171*d68f33bcSAndroid Build Coastguard Worker          header: A unique identifier for the source of this error.
172*d68f33bcSAndroid Build Coastguard Worker          error: error string.
173*d68f33bcSAndroid Build Coastguard Worker        """
174*d68f33bcSAndroid Build Coastguard Worker        status_line = f'[{self.FAILED}] {header}'
175*d68f33bcSAndroid Build Coastguard Worker        rh.terminal.print_status_line(status_line, print_newline=True)
176*d68f33bcSAndroid Build Coastguard Worker        print(error, file=sys.stderr)
177*d68f33bcSAndroid Build Coastguard Worker        self.success = False
178*d68f33bcSAndroid Build Coastguard Worker
179*d68f33bcSAndroid Build Coastguard Worker    def hook_fixups(
180*d68f33bcSAndroid Build Coastguard Worker        self,
181*d68f33bcSAndroid Build Coastguard Worker        project_results: rh.results.ProjectResults,
182*d68f33bcSAndroid Build Coastguard Worker        hook_results: List[rh.results.HookResult],
183*d68f33bcSAndroid Build Coastguard Worker    ) -> None:
184*d68f33bcSAndroid Build Coastguard Worker        """Display summary of possible fixups for a single hook."""
185*d68f33bcSAndroid Build Coastguard Worker        for result in (x for x in hook_results if x.fixup_cmd):
186*d68f33bcSAndroid Build Coastguard Worker            cmd = result.fixup_cmd + list(result.files)
187*d68f33bcSAndroid Build Coastguard Worker            for line in (
188*d68f33bcSAndroid Build Coastguard Worker                f'[{self.FIXUP}] {result.hook} has automated fixups available',
189*d68f33bcSAndroid Build Coastguard Worker                f'  cd {rh.shell.quote(project_results.workdir)} && \\',
190*d68f33bcSAndroid Build Coastguard Worker                f'    {rh.shell.cmd_to_str(cmd)}',
191*d68f33bcSAndroid Build Coastguard Worker            ):
192*d68f33bcSAndroid Build Coastguard Worker                rh.terminal.print_status_line(line, print_newline=True)
193*d68f33bcSAndroid Build Coastguard Worker
194*d68f33bcSAndroid Build Coastguard Worker    def finish(self):
195*d68f33bcSAndroid Build Coastguard Worker        """Print summary for all the hooks."""
196*d68f33bcSAndroid Build Coastguard Worker        header = self.PASSED if self.success else self.FAILED
197*d68f33bcSAndroid Build Coastguard Worker        status = 'passed' if self.success else 'failed'
198*d68f33bcSAndroid Build Coastguard Worker        d = rh.utils.timedelta_str(datetime.datetime.now() - self.start_time)
199*d68f33bcSAndroid Build Coastguard Worker        rh.terminal.print_status_line(
200*d68f33bcSAndroid Build Coastguard Worker            f'[{header}] repohooks for {self.project_name} {status} in {d}',
201*d68f33bcSAndroid Build Coastguard Worker            print_newline=True)
202*d68f33bcSAndroid Build Coastguard Worker
203*d68f33bcSAndroid Build Coastguard Worker
204*d68f33bcSAndroid Build Coastguard Workerdef _process_hook_results(results):
205*d68f33bcSAndroid Build Coastguard Worker    """Returns an error string if an error occurred.
206*d68f33bcSAndroid Build Coastguard Worker
207*d68f33bcSAndroid Build Coastguard Worker    Args:
208*d68f33bcSAndroid Build Coastguard Worker      results: A list of HookResult objects, or None.
209*d68f33bcSAndroid Build Coastguard Worker
210*d68f33bcSAndroid Build Coastguard Worker    Returns:
211*d68f33bcSAndroid Build Coastguard Worker      error output if an error occurred, otherwise None
212*d68f33bcSAndroid Build Coastguard Worker      warning output if an error occurred, otherwise None
213*d68f33bcSAndroid Build Coastguard Worker    """
214*d68f33bcSAndroid Build Coastguard Worker    if not results:
215*d68f33bcSAndroid Build Coastguard Worker        return (None, None)
216*d68f33bcSAndroid Build Coastguard Worker
217*d68f33bcSAndroid Build Coastguard Worker    # We track these as dedicated fields in case a hook doesn't output anything.
218*d68f33bcSAndroid Build Coastguard Worker    # We want to treat silent non-zero exits as failures too.
219*d68f33bcSAndroid Build Coastguard Worker    has_error = False
220*d68f33bcSAndroid Build Coastguard Worker    has_warning = False
221*d68f33bcSAndroid Build Coastguard Worker
222*d68f33bcSAndroid Build Coastguard Worker    error_ret = ''
223*d68f33bcSAndroid Build Coastguard Worker    warning_ret = ''
224*d68f33bcSAndroid Build Coastguard Worker    for result in results:
225*d68f33bcSAndroid Build Coastguard Worker        if result or result.is_warning():
226*d68f33bcSAndroid Build Coastguard Worker            ret = ''
227*d68f33bcSAndroid Build Coastguard Worker            if result.files:
228*d68f33bcSAndroid Build Coastguard Worker                ret += f'  FILES: {rh.shell.cmd_to_str(result.files)}\n'
229*d68f33bcSAndroid Build Coastguard Worker            lines = result.error.splitlines()
230*d68f33bcSAndroid Build Coastguard Worker            ret += '\n'.join(f'    {x}' for x in lines)
231*d68f33bcSAndroid Build Coastguard Worker            if result.is_warning():
232*d68f33bcSAndroid Build Coastguard Worker                has_warning = True
233*d68f33bcSAndroid Build Coastguard Worker                warning_ret += ret
234*d68f33bcSAndroid Build Coastguard Worker            else:
235*d68f33bcSAndroid Build Coastguard Worker                has_error = True
236*d68f33bcSAndroid Build Coastguard Worker                error_ret += ret
237*d68f33bcSAndroid Build Coastguard Worker
238*d68f33bcSAndroid Build Coastguard Worker    return (error_ret if has_error else None,
239*d68f33bcSAndroid Build Coastguard Worker            warning_ret if has_warning else None)
240*d68f33bcSAndroid Build Coastguard Worker
241*d68f33bcSAndroid Build Coastguard Worker
242*d68f33bcSAndroid Build Coastguard Workerdef _get_project_config(from_git=False):
243*d68f33bcSAndroid Build Coastguard Worker    """Returns the configuration for a project.
244*d68f33bcSAndroid Build Coastguard Worker
245*d68f33bcSAndroid Build Coastguard Worker    Args:
246*d68f33bcSAndroid Build Coastguard Worker      from_git: If true, we are called from git directly and repo should not be
247*d68f33bcSAndroid Build Coastguard Worker          used.
248*d68f33bcSAndroid Build Coastguard Worker    Expects to be called from within the project root.
249*d68f33bcSAndroid Build Coastguard Worker    """
250*d68f33bcSAndroid Build Coastguard Worker    if from_git:
251*d68f33bcSAndroid Build Coastguard Worker        global_paths = (rh.git.find_repo_root(),)
252*d68f33bcSAndroid Build Coastguard Worker    else:
253*d68f33bcSAndroid Build Coastguard Worker        global_paths = (
254*d68f33bcSAndroid Build Coastguard Worker            # Load the global config found in the manifest repo.
255*d68f33bcSAndroid Build Coastguard Worker            (os.path.join(rh.git.find_repo_root(), '.repo', 'manifests')),
256*d68f33bcSAndroid Build Coastguard Worker            # Load the global config found in the root of the repo checkout.
257*d68f33bcSAndroid Build Coastguard Worker            rh.git.find_repo_root(),
258*d68f33bcSAndroid Build Coastguard Worker        )
259*d68f33bcSAndroid Build Coastguard Worker
260*d68f33bcSAndroid Build Coastguard Worker    paths = (
261*d68f33bcSAndroid Build Coastguard Worker        # Load the config for this git repo.
262*d68f33bcSAndroid Build Coastguard Worker        '.',
263*d68f33bcSAndroid Build Coastguard Worker    )
264*d68f33bcSAndroid Build Coastguard Worker    return rh.config.PreUploadSettings(paths=paths, global_paths=global_paths)
265*d68f33bcSAndroid Build Coastguard Worker
266*d68f33bcSAndroid Build Coastguard Worker
267*d68f33bcSAndroid Build Coastguard Workerdef _attempt_fixes(projects_results: List[rh.results.ProjectResults]) -> None:
268*d68f33bcSAndroid Build Coastguard Worker    """Attempts to fix fixable results."""
269*d68f33bcSAndroid Build Coastguard Worker    # Filter out any result that has a fixup.
270*d68f33bcSAndroid Build Coastguard Worker    fixups = []
271*d68f33bcSAndroid Build Coastguard Worker    for project_results in projects_results:
272*d68f33bcSAndroid Build Coastguard Worker        fixups.extend((project_results.workdir, x)
273*d68f33bcSAndroid Build Coastguard Worker                      for x in project_results.fixups)
274*d68f33bcSAndroid Build Coastguard Worker    if not fixups:
275*d68f33bcSAndroid Build Coastguard Worker        return
276*d68f33bcSAndroid Build Coastguard Worker
277*d68f33bcSAndroid Build Coastguard Worker    if len(fixups) > 1:
278*d68f33bcSAndroid Build Coastguard Worker        banner = f'Multiple fixups ({len(fixups)}) are available.'
279*d68f33bcSAndroid Build Coastguard Worker    else:
280*d68f33bcSAndroid Build Coastguard Worker        banner = 'Automated fixups are available.'
281*d68f33bcSAndroid Build Coastguard Worker    print(Output.COLOR.color(Output.COLOR.MAGENTA, banner), file=sys.stderr)
282*d68f33bcSAndroid Build Coastguard Worker
283*d68f33bcSAndroid Build Coastguard Worker    # If there's more than one fixup available, ask if they want to blindly run
284*d68f33bcSAndroid Build Coastguard Worker    # them all, or prompt for them one-by-one.
285*d68f33bcSAndroid Build Coastguard Worker    mode = 'some'
286*d68f33bcSAndroid Build Coastguard Worker    if len(fixups) > 1:
287*d68f33bcSAndroid Build Coastguard Worker        while True:
288*d68f33bcSAndroid Build Coastguard Worker            response = rh.terminal.str_prompt(
289*d68f33bcSAndroid Build Coastguard Worker                'What would you like to do',
290*d68f33bcSAndroid Build Coastguard Worker                ('Run (A)ll', 'Run (S)ome', '(D)ry-run', '(N)othing [default]'))
291*d68f33bcSAndroid Build Coastguard Worker            if not response:
292*d68f33bcSAndroid Build Coastguard Worker                print('', file=sys.stderr)
293*d68f33bcSAndroid Build Coastguard Worker                return
294*d68f33bcSAndroid Build Coastguard Worker            if response.startswith('a') or response.startswith('y'):
295*d68f33bcSAndroid Build Coastguard Worker                mode = 'all'
296*d68f33bcSAndroid Build Coastguard Worker                break
297*d68f33bcSAndroid Build Coastguard Worker            elif response.startswith('s'):
298*d68f33bcSAndroid Build Coastguard Worker                mode = 'some'
299*d68f33bcSAndroid Build Coastguard Worker                break
300*d68f33bcSAndroid Build Coastguard Worker            elif response.startswith('d'):
301*d68f33bcSAndroid Build Coastguard Worker                mode = 'dry-run'
302*d68f33bcSAndroid Build Coastguard Worker                break
303*d68f33bcSAndroid Build Coastguard Worker            elif response.startswith('n'):
304*d68f33bcSAndroid Build Coastguard Worker                print('', file=sys.stderr)
305*d68f33bcSAndroid Build Coastguard Worker                return
306*d68f33bcSAndroid Build Coastguard Worker
307*d68f33bcSAndroid Build Coastguard Worker    # Walk all the fixups and run them one-by-one.
308*d68f33bcSAndroid Build Coastguard Worker    for workdir, result in fixups:
309*d68f33bcSAndroid Build Coastguard Worker        if mode == 'some':
310*d68f33bcSAndroid Build Coastguard Worker            if not rh.terminal.boolean_prompt(
311*d68f33bcSAndroid Build Coastguard Worker                f'Run {result.hook} fixup for {result.commit}'
312*d68f33bcSAndroid Build Coastguard Worker            ):
313*d68f33bcSAndroid Build Coastguard Worker                continue
314*d68f33bcSAndroid Build Coastguard Worker
315*d68f33bcSAndroid Build Coastguard Worker        cmd = tuple(result.fixup_cmd) + tuple(result.files)
316*d68f33bcSAndroid Build Coastguard Worker        print(
317*d68f33bcSAndroid Build Coastguard Worker            f'\n[{Output.RUNNING}] cd {rh.shell.quote(workdir)} && '
318*d68f33bcSAndroid Build Coastguard Worker            f'{rh.shell.cmd_to_str(cmd)}', file=sys.stderr)
319*d68f33bcSAndroid Build Coastguard Worker        if mode == 'dry-run':
320*d68f33bcSAndroid Build Coastguard Worker            continue
321*d68f33bcSAndroid Build Coastguard Worker
322*d68f33bcSAndroid Build Coastguard Worker        cmd_result = rh.utils.run(cmd, cwd=workdir, check=False)
323*d68f33bcSAndroid Build Coastguard Worker        if cmd_result.returncode:
324*d68f33bcSAndroid Build Coastguard Worker            print(f'[{Output.WARNING}] command exited {cmd_result.returncode}',
325*d68f33bcSAndroid Build Coastguard Worker                  file=sys.stderr)
326*d68f33bcSAndroid Build Coastguard Worker        else:
327*d68f33bcSAndroid Build Coastguard Worker            print(f'[{Output.PASSED}] great success', file=sys.stderr)
328*d68f33bcSAndroid Build Coastguard Worker
329*d68f33bcSAndroid Build Coastguard Worker    print(f'\n[{Output.FIXUP}] Please amend & rebase your tree before '
330*d68f33bcSAndroid Build Coastguard Worker          'attempting to upload again.\n', file=sys.stderr)
331*d68f33bcSAndroid Build Coastguard Worker
332*d68f33bcSAndroid Build Coastguard Workerdef _run_project_hooks_in_cwd(
333*d68f33bcSAndroid Build Coastguard Worker    project_name: str,
334*d68f33bcSAndroid Build Coastguard Worker    proj_dir: str,
335*d68f33bcSAndroid Build Coastguard Worker    output: Output,
336*d68f33bcSAndroid Build Coastguard Worker    jobs: Optional[int] = None,
337*d68f33bcSAndroid Build Coastguard Worker    from_git: bool = False,
338*d68f33bcSAndroid Build Coastguard Worker    commit_list: Optional[List[str]] = None,
339*d68f33bcSAndroid Build Coastguard Worker) -> rh.results.ProjectResults:
340*d68f33bcSAndroid Build Coastguard Worker    """Run the project-specific hooks in the cwd.
341*d68f33bcSAndroid Build Coastguard Worker
342*d68f33bcSAndroid Build Coastguard Worker    Args:
343*d68f33bcSAndroid Build Coastguard Worker      project_name: The name of this project.
344*d68f33bcSAndroid Build Coastguard Worker      proj_dir: The directory for this project (for passing on in metadata).
345*d68f33bcSAndroid Build Coastguard Worker      output: Helper for summarizing output/errors to the user.
346*d68f33bcSAndroid Build Coastguard Worker      jobs: How many hooks to run in parallel.
347*d68f33bcSAndroid Build Coastguard Worker      from_git: If true, we are called from git directly and repo should not be
348*d68f33bcSAndroid Build Coastguard Worker          used.
349*d68f33bcSAndroid Build Coastguard Worker      commit_list: A list of commits to run hooks against.  If None or empty
350*d68f33bcSAndroid Build Coastguard Worker          list then we'll automatically get the list of commits that would be
351*d68f33bcSAndroid Build Coastguard Worker          uploaded.
352*d68f33bcSAndroid Build Coastguard Worker
353*d68f33bcSAndroid Build Coastguard Worker    Returns:
354*d68f33bcSAndroid Build Coastguard Worker      All the results for this project.
355*d68f33bcSAndroid Build Coastguard Worker    """
356*d68f33bcSAndroid Build Coastguard Worker    ret = rh.results.ProjectResults(project_name, proj_dir)
357*d68f33bcSAndroid Build Coastguard Worker
358*d68f33bcSAndroid Build Coastguard Worker    try:
359*d68f33bcSAndroid Build Coastguard Worker        config = _get_project_config(from_git)
360*d68f33bcSAndroid Build Coastguard Worker    except rh.config.ValidationError as e:
361*d68f33bcSAndroid Build Coastguard Worker        output.error('Loading config files', str(e))
362*d68f33bcSAndroid Build Coastguard Worker        return ret._replace(internal_failure=True)
363*d68f33bcSAndroid Build Coastguard Worker
364*d68f33bcSAndroid Build Coastguard Worker    builtin_hooks = list(config.callable_builtin_hooks())
365*d68f33bcSAndroid Build Coastguard Worker    custom_hooks = list(config.callable_custom_hooks())
366*d68f33bcSAndroid Build Coastguard Worker
367*d68f33bcSAndroid Build Coastguard Worker    # If the repo has no pre-upload hooks enabled, then just return.
368*d68f33bcSAndroid Build Coastguard Worker    if not builtin_hooks and not custom_hooks:
369*d68f33bcSAndroid Build Coastguard Worker        return ret
370*d68f33bcSAndroid Build Coastguard Worker
371*d68f33bcSAndroid Build Coastguard Worker    # Set up the environment like repo would with the forall command.
372*d68f33bcSAndroid Build Coastguard Worker    try:
373*d68f33bcSAndroid Build Coastguard Worker        remote = rh.git.get_upstream_remote()
374*d68f33bcSAndroid Build Coastguard Worker        upstream_branch = rh.git.get_upstream_branch()
375*d68f33bcSAndroid Build Coastguard Worker    except rh.utils.CalledProcessError as e:
376*d68f33bcSAndroid Build Coastguard Worker        output.error('Upstream remote/tracking branch lookup',
377*d68f33bcSAndroid Build Coastguard Worker                     f'{e}\nDid you run repo start?  Is your HEAD detached?')
378*d68f33bcSAndroid Build Coastguard Worker        return ret._replace(internal_failure=True)
379*d68f33bcSAndroid Build Coastguard Worker
380*d68f33bcSAndroid Build Coastguard Worker    project = rh.Project(name=project_name, dir=proj_dir)
381*d68f33bcSAndroid Build Coastguard Worker    rel_proj_dir = os.path.relpath(proj_dir, rh.git.find_repo_root())
382*d68f33bcSAndroid Build Coastguard Worker
383*d68f33bcSAndroid Build Coastguard Worker    # Filter out the hooks to process.
384*d68f33bcSAndroid Build Coastguard Worker    builtin_hooks = [x for x in builtin_hooks if rel_proj_dir not in x.scope]
385*d68f33bcSAndroid Build Coastguard Worker    custom_hooks = [x for x in custom_hooks if rel_proj_dir not in x.scope]
386*d68f33bcSAndroid Build Coastguard Worker
387*d68f33bcSAndroid Build Coastguard Worker    if not builtin_hooks and not custom_hooks:
388*d68f33bcSAndroid Build Coastguard Worker        return ret
389*d68f33bcSAndroid Build Coastguard Worker
390*d68f33bcSAndroid Build Coastguard Worker    os.environ.update({
391*d68f33bcSAndroid Build Coastguard Worker        'REPO_LREV': rh.git.get_commit_for_ref(upstream_branch),
392*d68f33bcSAndroid Build Coastguard Worker        'REPO_PATH': rel_proj_dir,
393*d68f33bcSAndroid Build Coastguard Worker        'REPO_PROJECT': project_name,
394*d68f33bcSAndroid Build Coastguard Worker        'REPO_REMOTE': remote,
395*d68f33bcSAndroid Build Coastguard Worker        'REPO_RREV': rh.git.get_remote_revision(upstream_branch, remote),
396*d68f33bcSAndroid Build Coastguard Worker    })
397*d68f33bcSAndroid Build Coastguard Worker
398*d68f33bcSAndroid Build Coastguard Worker    if not commit_list:
399*d68f33bcSAndroid Build Coastguard Worker        commit_list = rh.git.get_commits(
400*d68f33bcSAndroid Build Coastguard Worker            ignore_merged_commits=config.ignore_merged_commits)
401*d68f33bcSAndroid Build Coastguard Worker    output.set_num_commits(len(commit_list))
402*d68f33bcSAndroid Build Coastguard Worker
403*d68f33bcSAndroid Build Coastguard Worker    def _run_hook(hook, project, commit, desc, diff):
404*d68f33bcSAndroid Build Coastguard Worker        """Run a hook, gather stats, and process its results."""
405*d68f33bcSAndroid Build Coastguard Worker        start = datetime.datetime.now()
406*d68f33bcSAndroid Build Coastguard Worker        results = hook.hook(project, commit, desc, diff)
407*d68f33bcSAndroid Build Coastguard Worker        (error, warning) = _process_hook_results(results)
408*d68f33bcSAndroid Build Coastguard Worker        duration = datetime.datetime.now() - start
409*d68f33bcSAndroid Build Coastguard Worker        return (hook, results, error, warning, duration)
410*d68f33bcSAndroid Build Coastguard Worker
411*d68f33bcSAndroid Build Coastguard Worker    with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as executor:
412*d68f33bcSAndroid Build Coastguard Worker        for commit in commit_list:
413*d68f33bcSAndroid Build Coastguard Worker            # Mix in some settings for our hooks.
414*d68f33bcSAndroid Build Coastguard Worker            os.environ['PREUPLOAD_COMMIT'] = commit
415*d68f33bcSAndroid Build Coastguard Worker            diff = rh.git.get_affected_files(commit)
416*d68f33bcSAndroid Build Coastguard Worker            desc = rh.git.get_commit_desc(commit)
417*d68f33bcSAndroid Build Coastguard Worker            os.environ['PREUPLOAD_COMMIT_MESSAGE'] = desc
418*d68f33bcSAndroid Build Coastguard Worker
419*d68f33bcSAndroid Build Coastguard Worker            commit_summary = desc.split('\n', 1)[0]
420*d68f33bcSAndroid Build Coastguard Worker            output.commit_start(builtin_hooks + custom_hooks, commit, commit_summary)
421*d68f33bcSAndroid Build Coastguard Worker
422*d68f33bcSAndroid Build Coastguard Worker            def run_hooks(hooks):
423*d68f33bcSAndroid Build Coastguard Worker                futures = (
424*d68f33bcSAndroid Build Coastguard Worker                    executor.submit(_run_hook, hook, project, commit, desc, diff)
425*d68f33bcSAndroid Build Coastguard Worker                    for hook in hooks
426*d68f33bcSAndroid Build Coastguard Worker                )
427*d68f33bcSAndroid Build Coastguard Worker                future_results = (
428*d68f33bcSAndroid Build Coastguard Worker                    x.result() for x in concurrent.futures.as_completed(futures)
429*d68f33bcSAndroid Build Coastguard Worker                )
430*d68f33bcSAndroid Build Coastguard Worker                for hook, hook_results, error, warning, duration in future_results:
431*d68f33bcSAndroid Build Coastguard Worker                    ret.add_results(hook_results)
432*d68f33bcSAndroid Build Coastguard Worker                    if error is not None or warning is not None:
433*d68f33bcSAndroid Build Coastguard Worker                        if warning is not None:
434*d68f33bcSAndroid Build Coastguard Worker                            output.hook_warning(hook, warning)
435*d68f33bcSAndroid Build Coastguard Worker                        if error is not None:
436*d68f33bcSAndroid Build Coastguard Worker                            output.hook_error(hook, error)
437*d68f33bcSAndroid Build Coastguard Worker                            output.hook_fixups(ret, hook_results)
438*d68f33bcSAndroid Build Coastguard Worker                    output.hook_finish(hook, duration)
439*d68f33bcSAndroid Build Coastguard Worker
440*d68f33bcSAndroid Build Coastguard Worker            run_hooks(builtin_hooks)
441*d68f33bcSAndroid Build Coastguard Worker            run_hooks(custom_hooks)
442*d68f33bcSAndroid Build Coastguard Worker
443*d68f33bcSAndroid Build Coastguard Worker    return ret
444*d68f33bcSAndroid Build Coastguard Worker
445*d68f33bcSAndroid Build Coastguard Worker
446*d68f33bcSAndroid Build Coastguard Workerdef _run_project_hooks(
447*d68f33bcSAndroid Build Coastguard Worker    project_name: str,
448*d68f33bcSAndroid Build Coastguard Worker    proj_dir: Optional[str] = None,
449*d68f33bcSAndroid Build Coastguard Worker    jobs: Optional[int] = None,
450*d68f33bcSAndroid Build Coastguard Worker    from_git: bool = False,
451*d68f33bcSAndroid Build Coastguard Worker    commit_list: Optional[List[str]] = None,
452*d68f33bcSAndroid Build Coastguard Worker) -> rh.results.ProjectResults:
453*d68f33bcSAndroid Build Coastguard Worker    """Run the project-specific hooks in |proj_dir|.
454*d68f33bcSAndroid Build Coastguard Worker
455*d68f33bcSAndroid Build Coastguard Worker    Args:
456*d68f33bcSAndroid Build Coastguard Worker      project_name: The name of project to run hooks for.
457*d68f33bcSAndroid Build Coastguard Worker      proj_dir: If non-None, this is the directory the project is in.  If None,
458*d68f33bcSAndroid Build Coastguard Worker          we'll ask repo.
459*d68f33bcSAndroid Build Coastguard Worker      jobs: How many hooks to run in parallel.
460*d68f33bcSAndroid Build Coastguard Worker      from_git: If true, we are called from git directly and repo should not be
461*d68f33bcSAndroid Build Coastguard Worker          used.
462*d68f33bcSAndroid Build Coastguard Worker      commit_list: A list of commits to run hooks against.  If None or empty
463*d68f33bcSAndroid Build Coastguard Worker          list then we'll automatically get the list of commits that would be
464*d68f33bcSAndroid Build Coastguard Worker          uploaded.
465*d68f33bcSAndroid Build Coastguard Worker
466*d68f33bcSAndroid Build Coastguard Worker    Returns:
467*d68f33bcSAndroid Build Coastguard Worker      All the results for this project.
468*d68f33bcSAndroid Build Coastguard Worker    """
469*d68f33bcSAndroid Build Coastguard Worker    output = Output(project_name)
470*d68f33bcSAndroid Build Coastguard Worker
471*d68f33bcSAndroid Build Coastguard Worker    if proj_dir is None:
472*d68f33bcSAndroid Build Coastguard Worker        cmd = ['repo', 'forall', project_name, '-c', 'pwd']
473*d68f33bcSAndroid Build Coastguard Worker        result = rh.utils.run(cmd, capture_output=True)
474*d68f33bcSAndroid Build Coastguard Worker        proj_dirs = result.stdout.split()
475*d68f33bcSAndroid Build Coastguard Worker        if not proj_dirs:
476*d68f33bcSAndroid Build Coastguard Worker            print(f'{project_name} cannot be found.', file=sys.stderr)
477*d68f33bcSAndroid Build Coastguard Worker            print('Please specify a valid project.', file=sys.stderr)
478*d68f33bcSAndroid Build Coastguard Worker            return False
479*d68f33bcSAndroid Build Coastguard Worker        if len(proj_dirs) > 1:
480*d68f33bcSAndroid Build Coastguard Worker            print(f'{project_name} is associated with multiple directories.',
481*d68f33bcSAndroid Build Coastguard Worker                  file=sys.stderr)
482*d68f33bcSAndroid Build Coastguard Worker            print('Please specify a directory to help disambiguate.',
483*d68f33bcSAndroid Build Coastguard Worker                  file=sys.stderr)
484*d68f33bcSAndroid Build Coastguard Worker            return False
485*d68f33bcSAndroid Build Coastguard Worker        proj_dir = proj_dirs[0]
486*d68f33bcSAndroid Build Coastguard Worker
487*d68f33bcSAndroid Build Coastguard Worker    pwd = os.getcwd()
488*d68f33bcSAndroid Build Coastguard Worker    try:
489*d68f33bcSAndroid Build Coastguard Worker        # Hooks assume they are run from the root of the project.
490*d68f33bcSAndroid Build Coastguard Worker        os.chdir(proj_dir)
491*d68f33bcSAndroid Build Coastguard Worker        return _run_project_hooks_in_cwd(
492*d68f33bcSAndroid Build Coastguard Worker            project_name, proj_dir, output, jobs=jobs, from_git=from_git,
493*d68f33bcSAndroid Build Coastguard Worker            commit_list=commit_list)
494*d68f33bcSAndroid Build Coastguard Worker    finally:
495*d68f33bcSAndroid Build Coastguard Worker        output.finish()
496*d68f33bcSAndroid Build Coastguard Worker        os.chdir(pwd)
497*d68f33bcSAndroid Build Coastguard Worker
498*d68f33bcSAndroid Build Coastguard Worker
499*d68f33bcSAndroid Build Coastguard Workerdef _run_projects_hooks(
500*d68f33bcSAndroid Build Coastguard Worker    project_list: List[str],
501*d68f33bcSAndroid Build Coastguard Worker    worktree_list: List[Optional[str]],
502*d68f33bcSAndroid Build Coastguard Worker    jobs: Optional[int] = None,
503*d68f33bcSAndroid Build Coastguard Worker    from_git: bool = False,
504*d68f33bcSAndroid Build Coastguard Worker    commit_list: Optional[List[str]] = None,
505*d68f33bcSAndroid Build Coastguard Worker) -> bool:
506*d68f33bcSAndroid Build Coastguard Worker    """Run all the hooks
507*d68f33bcSAndroid Build Coastguard Worker
508*d68f33bcSAndroid Build Coastguard Worker    Args:
509*d68f33bcSAndroid Build Coastguard Worker      project_list: List of project names.
510*d68f33bcSAndroid Build Coastguard Worker      worktree_list: List of project checkouts.
511*d68f33bcSAndroid Build Coastguard Worker      jobs: How many hooks to run in parallel.
512*d68f33bcSAndroid Build Coastguard Worker      from_git: If true, we are called from git directly and repo should not be
513*d68f33bcSAndroid Build Coastguard Worker          used.
514*d68f33bcSAndroid Build Coastguard Worker      commit_list: A list of commits to run hooks against.  If None or empty
515*d68f33bcSAndroid Build Coastguard Worker          list then we'll automatically get the list of commits that would be
516*d68f33bcSAndroid Build Coastguard Worker          uploaded.
517*d68f33bcSAndroid Build Coastguard Worker
518*d68f33bcSAndroid Build Coastguard Worker    Returns:
519*d68f33bcSAndroid Build Coastguard Worker      True if everything passed, else False.
520*d68f33bcSAndroid Build Coastguard Worker    """
521*d68f33bcSAndroid Build Coastguard Worker    results = []
522*d68f33bcSAndroid Build Coastguard Worker    for project, worktree in zip(project_list, worktree_list):
523*d68f33bcSAndroid Build Coastguard Worker        result = _run_project_hooks(
524*d68f33bcSAndroid Build Coastguard Worker            project,
525*d68f33bcSAndroid Build Coastguard Worker            proj_dir=worktree,
526*d68f33bcSAndroid Build Coastguard Worker            jobs=jobs,
527*d68f33bcSAndroid Build Coastguard Worker            from_git=from_git,
528*d68f33bcSAndroid Build Coastguard Worker            commit_list=commit_list,
529*d68f33bcSAndroid Build Coastguard Worker        )
530*d68f33bcSAndroid Build Coastguard Worker        results.append(result)
531*d68f33bcSAndroid Build Coastguard Worker        if result:
532*d68f33bcSAndroid Build Coastguard Worker            # If a repo had failures, add a blank line to help break up the
533*d68f33bcSAndroid Build Coastguard Worker            # output.  If there were no failures, then the output should be
534*d68f33bcSAndroid Build Coastguard Worker            # very minimal, so we don't add it then.
535*d68f33bcSAndroid Build Coastguard Worker            print('', file=sys.stderr)
536*d68f33bcSAndroid Build Coastguard Worker
537*d68f33bcSAndroid Build Coastguard Worker    _attempt_fixes(results)
538*d68f33bcSAndroid Build Coastguard Worker    return not any(results)
539*d68f33bcSAndroid Build Coastguard Worker
540*d68f33bcSAndroid Build Coastguard Worker
541*d68f33bcSAndroid Build Coastguard Workerdef main(project_list, worktree_list=None, **_kwargs):
542*d68f33bcSAndroid Build Coastguard Worker    """Main function invoked directly by repo.
543*d68f33bcSAndroid Build Coastguard Worker
544*d68f33bcSAndroid Build Coastguard Worker    We must use the name "main" as that is what repo requires.
545*d68f33bcSAndroid Build Coastguard Worker
546*d68f33bcSAndroid Build Coastguard Worker    This function will exit directly upon error so that repo doesn't print some
547*d68f33bcSAndroid Build Coastguard Worker    obscure error message.
548*d68f33bcSAndroid Build Coastguard Worker
549*d68f33bcSAndroid Build Coastguard Worker    Args:
550*d68f33bcSAndroid Build Coastguard Worker      project_list: List of projects to run on.
551*d68f33bcSAndroid Build Coastguard Worker      worktree_list: A list of directories.  It should be the same length as
552*d68f33bcSAndroid Build Coastguard Worker          project_list, so that each entry in project_list matches with a
553*d68f33bcSAndroid Build Coastguard Worker          directory in worktree_list.  If None, we will attempt to calculate
554*d68f33bcSAndroid Build Coastguard Worker          the directories automatically.
555*d68f33bcSAndroid Build Coastguard Worker      kwargs: Leave this here for forward-compatibility.
556*d68f33bcSAndroid Build Coastguard Worker    """
557*d68f33bcSAndroid Build Coastguard Worker    if not worktree_list:
558*d68f33bcSAndroid Build Coastguard Worker        worktree_list = [None] * len(project_list)
559*d68f33bcSAndroid Build Coastguard Worker    if not _run_projects_hooks(project_list, worktree_list):
560*d68f33bcSAndroid Build Coastguard Worker        color = rh.terminal.Color()
561*d68f33bcSAndroid Build Coastguard Worker        print(color.color(color.RED, 'FATAL') +
562*d68f33bcSAndroid Build Coastguard Worker              ': Preupload failed due to above error(s).\n'
563*d68f33bcSAndroid Build Coastguard Worker              f'For more info, see: {REPOHOOKS_URL}',
564*d68f33bcSAndroid Build Coastguard Worker              file=sys.stderr)
565*d68f33bcSAndroid Build Coastguard Worker        sys.exit(1)
566*d68f33bcSAndroid Build Coastguard Worker
567*d68f33bcSAndroid Build Coastguard Worker
568*d68f33bcSAndroid Build Coastguard Workerdef _identify_project(path, from_git=False):
569*d68f33bcSAndroid Build Coastguard Worker    """Identify the repo project associated with the given path.
570*d68f33bcSAndroid Build Coastguard Worker
571*d68f33bcSAndroid Build Coastguard Worker    Returns:
572*d68f33bcSAndroid Build Coastguard Worker      A string indicating what project is associated with the path passed in or
573*d68f33bcSAndroid Build Coastguard Worker      a blank string upon failure.
574*d68f33bcSAndroid Build Coastguard Worker    """
575*d68f33bcSAndroid Build Coastguard Worker    if from_git:
576*d68f33bcSAndroid Build Coastguard Worker        cmd = ['git', 'rev-parse', '--show-toplevel']
577*d68f33bcSAndroid Build Coastguard Worker        project_path = rh.utils.run(cmd, capture_output=True).stdout.strip()
578*d68f33bcSAndroid Build Coastguard Worker        cmd = ['git', 'rev-parse', '--show-superproject-working-tree']
579*d68f33bcSAndroid Build Coastguard Worker        superproject_path = rh.utils.run(
580*d68f33bcSAndroid Build Coastguard Worker            cmd, capture_output=True).stdout.strip()
581*d68f33bcSAndroid Build Coastguard Worker        module_path = project_path[len(superproject_path) + 1:]
582*d68f33bcSAndroid Build Coastguard Worker        cmd = ['git', 'config', '-f', '.gitmodules',
583*d68f33bcSAndroid Build Coastguard Worker               '--name-only', '--get-regexp', r'^submodule\..*\.path$',
584*d68f33bcSAndroid Build Coastguard Worker               f"^{module_path}$"]
585*d68f33bcSAndroid Build Coastguard Worker        module_name = rh.utils.run(cmd, cwd=superproject_path,
586*d68f33bcSAndroid Build Coastguard Worker                                   capture_output=True).stdout.strip()
587*d68f33bcSAndroid Build Coastguard Worker        return module_name[len('submodule.'):-len(".path")]
588*d68f33bcSAndroid Build Coastguard Worker    else:
589*d68f33bcSAndroid Build Coastguard Worker        cmd = ['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}']
590*d68f33bcSAndroid Build Coastguard Worker        return rh.utils.run(cmd, capture_output=True, cwd=path).stdout.strip()
591*d68f33bcSAndroid Build Coastguard Worker
592*d68f33bcSAndroid Build Coastguard Worker
593*d68f33bcSAndroid Build Coastguard Workerdef direct_main(argv):
594*d68f33bcSAndroid Build Coastguard Worker    """Run hooks directly (outside of the context of repo).
595*d68f33bcSAndroid Build Coastguard Worker
596*d68f33bcSAndroid Build Coastguard Worker    Args:
597*d68f33bcSAndroid Build Coastguard Worker      argv: The command line args to process.
598*d68f33bcSAndroid Build Coastguard Worker
599*d68f33bcSAndroid Build Coastguard Worker    Returns:
600*d68f33bcSAndroid Build Coastguard Worker      0 if no pre-upload failures, 1 if failures.
601*d68f33bcSAndroid Build Coastguard Worker
602*d68f33bcSAndroid Build Coastguard Worker    Raises:
603*d68f33bcSAndroid Build Coastguard Worker      BadInvocation: On some types of invocation errors.
604*d68f33bcSAndroid Build Coastguard Worker    """
605*d68f33bcSAndroid Build Coastguard Worker    parser = argparse.ArgumentParser(description=__doc__)
606*d68f33bcSAndroid Build Coastguard Worker    parser.add_argument('--git', action='store_true',
607*d68f33bcSAndroid Build Coastguard Worker                        help='This hook is called from git instead of repo')
608*d68f33bcSAndroid Build Coastguard Worker    parser.add_argument('--dir', default=None,
609*d68f33bcSAndroid Build Coastguard Worker                        help='The directory that the project lives in.  If not '
610*d68f33bcSAndroid Build Coastguard Worker                        'specified, use the git project root based on the cwd.')
611*d68f33bcSAndroid Build Coastguard Worker    parser.add_argument('--project', default=None,
612*d68f33bcSAndroid Build Coastguard Worker                        help='The project repo path; this can affect how the '
613*d68f33bcSAndroid Build Coastguard Worker                        'hooks get run, since some hooks are project-specific.'
614*d68f33bcSAndroid Build Coastguard Worker                        'If not specified, `repo` will be used to figure this '
615*d68f33bcSAndroid Build Coastguard Worker                        'out based on the dir.')
616*d68f33bcSAndroid Build Coastguard Worker    parser.add_argument('-j', '--jobs', type=int,
617*d68f33bcSAndroid Build Coastguard Worker                        help='Run up to this many hooks in parallel. Setting '
618*d68f33bcSAndroid Build Coastguard Worker                        'to 1 forces serial execution, and the default '
619*d68f33bcSAndroid Build Coastguard Worker                        'automatically chooses an appropriate number for the '
620*d68f33bcSAndroid Build Coastguard Worker                        'current system.')
621*d68f33bcSAndroid Build Coastguard Worker    parser.add_argument('commits', nargs='*',
622*d68f33bcSAndroid Build Coastguard Worker                        help='Check specific commits')
623*d68f33bcSAndroid Build Coastguard Worker    opts = parser.parse_args(argv)
624*d68f33bcSAndroid Build Coastguard Worker
625*d68f33bcSAndroid Build Coastguard Worker    # Check/normalize git dir; if unspecified, we'll use the root of the git
626*d68f33bcSAndroid Build Coastguard Worker    # project from CWD.
627*d68f33bcSAndroid Build Coastguard Worker    if opts.dir is None:
628*d68f33bcSAndroid Build Coastguard Worker        cmd = ['git', 'rev-parse', '--git-dir']
629*d68f33bcSAndroid Build Coastguard Worker        git_dir = rh.utils.run(cmd, capture_output=True).stdout.strip()
630*d68f33bcSAndroid Build Coastguard Worker        if not git_dir:
631*d68f33bcSAndroid Build Coastguard Worker            parser.error('The current directory is not part of a git project.')
632*d68f33bcSAndroid Build Coastguard Worker        opts.dir = os.path.dirname(os.path.abspath(git_dir))
633*d68f33bcSAndroid Build Coastguard Worker    elif not os.path.isdir(opts.dir):
634*d68f33bcSAndroid Build Coastguard Worker        parser.error(f'Invalid dir: {opts.dir}')
635*d68f33bcSAndroid Build Coastguard Worker    elif not rh.git.is_git_repository(opts.dir):
636*d68f33bcSAndroid Build Coastguard Worker        parser.error(f'Not a git repository: {opts.dir}')
637*d68f33bcSAndroid Build Coastguard Worker
638*d68f33bcSAndroid Build Coastguard Worker    # Identify the project if it wasn't specified; this _requires_ the repo
639*d68f33bcSAndroid Build Coastguard Worker    # tool to be installed and for the project to be part of a repo checkout.
640*d68f33bcSAndroid Build Coastguard Worker    if not opts.project:
641*d68f33bcSAndroid Build Coastguard Worker        opts.project = _identify_project(opts.dir, opts.git)
642*d68f33bcSAndroid Build Coastguard Worker        if not opts.project:
643*d68f33bcSAndroid Build Coastguard Worker            parser.error(f"Couldn't identify the project of {opts.dir}")
644*d68f33bcSAndroid Build Coastguard Worker
645*d68f33bcSAndroid Build Coastguard Worker    try:
646*d68f33bcSAndroid Build Coastguard Worker        if _run_projects_hooks([opts.project], [opts.dir], jobs=opts.jobs,
647*d68f33bcSAndroid Build Coastguard Worker                               from_git=opts.git, commit_list=opts.commits):
648*d68f33bcSAndroid Build Coastguard Worker            return 0
649*d68f33bcSAndroid Build Coastguard Worker    except KeyboardInterrupt:
650*d68f33bcSAndroid Build Coastguard Worker        print('Aborting execution early due to user interrupt', file=sys.stderr)
651*d68f33bcSAndroid Build Coastguard Worker        return 128 + signal.SIGINT
652*d68f33bcSAndroid Build Coastguard Worker    return 1
653*d68f33bcSAndroid Build Coastguard Worker
654*d68f33bcSAndroid Build Coastguard Worker
655*d68f33bcSAndroid Build Coastguard Workerif __name__ == '__main__':
656*d68f33bcSAndroid Build Coastguard Worker    sys.exit(direct_main(sys.argv[1:]))
657