blob: ec723e143a94f6abf14ad1111d4001aaa93756b1 [file] [log] [blame]
Mike Frysinger2e65c542016-03-08 16:17:00 -05001#!/usr/bin/python
2# -*- coding:utf-8 -*-
3# Copyright 2016 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Repo pre-upload hook.
18
19Normally this is loaded indirectly by repo itself, but it can be run directly
20when developing.
21"""
22
23from __future__ import print_function
24
25import argparse
26import collections
27import os
28import sys
29
30try:
31 __file__
32except NameError:
33 # Work around repo until it gets fixed.
34 # https://gerrit-review.googlesource.com/75481
35 __file__ = os.path.join(os.getcwd(), 'pre-upload.py')
36_path = os.path.dirname(os.path.realpath(__file__))
37if sys.path[0] != _path:
38 sys.path.insert(0, _path)
39del _path
40
41import rh.results
42import rh.config
43import rh.git
44import rh.hooks
45import rh.terminal
46import rh.utils
47
48
49# Repohooks homepage.
50REPOHOOKS_URL = 'https://android.googlesource.com/platform/tools/repohooks/'
51
52
53Project = collections.namedtuple('Project', ['name', 'dir', 'remote'])
54
55
56def _process_hook_results(project, commit, commit_desc, results):
57 """Prints the hook error to stderr with project and commit context
58
59 Args:
60 project: The project name.
61 commit: The commit hash the errors belong to.
62 commit_desc: A string containing the commit message.
63 results: A list of HookResult objects.
64
65 Returns:
66 False if any errors were found, else True.
67 """
68 color = rh.terminal.Color()
69 def _print_banner():
70 print('%s: %s: hooks failed' %
71 (color.color(color.RED, 'ERROR'), project),
72 file=sys.stderr)
73
74 commit_summary = commit_desc.splitlines()[0]
75 print('COMMIT: %s (%s)' % (commit[0:12], commit_summary),
76 file=sys.stderr)
77
78 ret = True
79 for result in results:
80 if result:
81 if ret:
82 _print_banner()
83 ret = False
84
85 print('%s: %s' % (color.color(color.CYAN, 'HOOK'), result.hook),
86 file=sys.stderr)
87 if result.files:
88 print(' FILES: %s' % (result.files,), file=sys.stderr)
89 lines = result.error.splitlines()
90 print('\n'.join(' %s' % (x,) for x in lines), file=sys.stderr)
91 print('', file=sys.stderr)
92
93 return ret
94
95
96def _get_project_hooks():
97 """Returns a list of hooks that need to be run for a project.
98
99 Expects to be called from within the project root.
100 """
Mike Frysingerca797702016-09-03 02:00:55 -0400101 global_paths = (
102 # Load the global config found in the manifest repo.
103 os.path.join(rh.git.find_repo_root(), '.repo', 'manifests'),
104 # Load the global config found in the root of the repo checkout.
105 rh.git.find_repo_root(),
106 )
107 paths = (
108 # Load the config for this git repo.
109 '.',
110 )
Mike Frysinger2e65c542016-03-08 16:17:00 -0500111 try:
Mike Frysingerca797702016-09-03 02:00:55 -0400112 config = rh.config.PreSubmitConfig(paths=paths,
113 global_paths=global_paths)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500114 except rh.config.ValidationError as e:
115 print('invalid config file: %s' % (e,), file=sys.stderr)
116 sys.exit(1)
117 return config.callable_hooks()
118
119
120def _run_project_hooks(project_name, proj_dir=None,
121 commit_list=None):
122 """For each project run its project specific hook from the hooks dictionary.
123
124 Args:
125 project_name: The name of project to run hooks for.
126 proj_dir: If non-None, this is the directory the project is in. If None,
127 we'll ask repo.
128 commit_list: A list of commits to run hooks against. If None or empty
129 list then we'll automatically get the list of commits that would be
130 uploaded.
131
132 Returns:
133 False if any errors were found, else True.
134 """
135 if proj_dir is None:
136 cmd = ['repo', 'forall', project_name, '-c', 'pwd']
137 result = rh.utils.run_command(cmd, capture_output=True)
138 proj_dirs = result.output.split()
139 if len(proj_dirs) == 0:
140 print('%s cannot be found.' % project_name, file=sys.stderr)
141 print('Please specify a valid project.', file=sys.stderr)
142 return 0
143 if len(proj_dirs) > 1:
144 print('%s is associated with multiple directories.' % project_name,
145 file=sys.stderr)
146 print('Please specify a directory to help disambiguate.',
147 file=sys.stderr)
148 return 0
149 proj_dir = proj_dirs[0]
150
151 pwd = os.getcwd()
152 # Hooks assume they are run from the root of the project.
153 os.chdir(proj_dir)
154
Mike Frysinger558aff42016-04-04 16:02:55 -0400155 # If the repo has no pre-upload hooks enabled, then just return.
156 hooks = list(_get_project_hooks())
157 if not hooks:
158 return True
159
Mike Frysinger2e65c542016-03-08 16:17:00 -0500160 # Set up the environment like repo would with the forall command.
Luis Hector Chavezd8f36752016-09-15 13:25:03 -0700161 try:
162 remote = rh.git.get_upstream_remote()
163 except rh.utils.RunCommandError as e:
164 print('upstream remote cannot be found: %s' % (e,), file=sys.stderr)
165 print('Did you run repo start?', file=sys.stderr)
166 sys.exit(1)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500167 os.environ.update({
168 'REPO_PROJECT': project_name,
169 'REPO_PATH': proj_dir,
170 'REPO_REMOTE': remote,
171 })
172
173 project = Project(name=project_name, dir=proj_dir, remote=remote)
174
175 if not commit_list:
176 commit_list = rh.git.get_commits()
177
Mike Frysinger2e65c542016-03-08 16:17:00 -0500178 ret = True
179 for commit in commit_list:
180 # Mix in some settings for our hooks.
Mike Frysingerdc253622016-04-04 15:43:02 -0400181 os.environ['PREUPLOAD_COMMIT'] = commit
Mike Frysinger2e65c542016-03-08 16:17:00 -0500182 diff = rh.git.get_affected_files(commit)
Mike Frysinger76b1bc72016-04-20 16:50:16 -0400183 desc = rh.git.get_commit_desc(commit)
Nicolas Sylvain6f798862016-05-02 18:58:22 -0700184 os.environ['PREUPLOAD_COMMIT_MESSAGE'] = desc
Mike Frysinger2e65c542016-03-08 16:17:00 -0500185
186 results = []
187 for hook in hooks:
Mike Frysinger76b1bc72016-04-20 16:50:16 -0400188 hook_results = hook(project, commit, desc, diff)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500189 if hook_results:
190 results.extend(hook_results)
191 if results:
Mike Frysinger2e65c542016-03-08 16:17:00 -0500192 if not _process_hook_results(project.name, commit, desc, results):
193 ret = False
194
195 os.chdir(pwd)
196 return ret
197
198
199def main(project_list, worktree_list=None, **_kwargs):
200 """Main function invoked directly by repo.
201
202 We must use the name "main" as that is what repo requires.
203
204 This function will exit directly upon error so that repo doesn't print some
205 obscure error message.
206
207 Args:
208 project_list: List of projects to run on.
209 worktree_list: A list of directories. It should be the same length as
210 project_list, so that each entry in project_list matches with a
211 directory in worktree_list. If None, we will attempt to calculate
212 the directories automatically.
213 kwargs: Leave this here for forward-compatibility.
214 """
215 found_error = False
216 if not worktree_list:
217 worktree_list = [None] * len(project_list)
218 for project, worktree in zip(project_list, worktree_list):
219 if not _run_project_hooks(project, proj_dir=worktree):
220 found_error = True
221
222 if found_error:
223 color = rh.terminal.Color()
224 print('%s: Preupload failed due to above error(s).\n'
225 'For more info, please see:\n%s' %
226 (color.color(color.RED, 'FATAL'), REPOHOOKS_URL),
227 file=sys.stderr)
228 sys.exit(1)
229
230
231def _identify_project(path):
232 """Identify the repo project associated with the given path.
233
234 Returns:
235 A string indicating what project is associated with the path passed in or
236 a blank string upon failure.
237 """
238 cmd = ['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}']
239 return rh.utils.run_command(cmd, capture_output=True, redirect_stderr=True,
240 cwd=path).output.strip()
241
242
243def direct_main(argv):
244 """Run hooks directly (outside of the context of repo).
245
246 Args:
247 argv: The command line args to process.
248
249 Returns:
250 0 if no pre-upload failures, 1 if failures.
251
252 Raises:
253 BadInvocation: On some types of invocation errors.
254 """
255 parser = argparse.ArgumentParser(description=__doc__)
256 parser.add_argument('--dir', default=None,
257 help='The directory that the project lives in. If not '
258 'specified, use the git project root based on the cwd.')
259 parser.add_argument('--project', default=None,
260 help='The project repo path; this can affect how the '
261 'hooks get run, since some hooks are project-specific.'
262 'If not specified, `repo` will be used to figure this '
263 'out based on the dir.')
264 parser.add_argument('commits', nargs='*',
265 help='Check specific commits')
266 opts = parser.parse_args(argv)
267
268 # Check/normalize git dir; if unspecified, we'll use the root of the git
269 # project from CWD.
270 if opts.dir is None:
271 cmd = ['git', 'rev-parse', '--git-dir']
272 git_dir = rh.utils.run_command(cmd, capture_output=True,
273 redirect_stderr=True).output.strip()
274 if not git_dir:
275 parser.error('The current directory is not part of a git project.')
276 opts.dir = os.path.dirname(os.path.abspath(git_dir))
277 elif not os.path.isdir(opts.dir):
278 parser.error('Invalid dir: %s' % opts.dir)
279 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
280 parser.error('Not a git directory: %s' % opts.dir)
281
282 # Identify the project if it wasn't specified; this _requires_ the repo
283 # tool to be installed and for the project to be part of a repo checkout.
284 if not opts.project:
285 opts.project = _identify_project(opts.dir)
286 if not opts.project:
287 parser.error("Repo couldn't identify the project of %s" % opts.dir)
288
289 if _run_project_hooks(opts.project, proj_dir=opts.dir,
290 commit_list=opts.commits):
291 return 0
292 else:
293 return 1
294
295
296if __name__ == '__main__':
297 sys.exit(direct_main(sys.argv[1:]))