blob: 81720a5f1d30dbb7f8816434b89608be5d46c087 [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.
Mike Frysinger05c689e2016-04-04 18:51:38 -0400161 remote = rh.git.get_upstream_remote()
Mike Frysinger2e65c542016-03-08 16:17:00 -0500162 os.environ.update({
163 'REPO_PROJECT': project_name,
164 'REPO_PATH': proj_dir,
165 'REPO_REMOTE': remote,
166 })
167
168 project = Project(name=project_name, dir=proj_dir, remote=remote)
169
170 if not commit_list:
171 commit_list = rh.git.get_commits()
172
Mike Frysinger2e65c542016-03-08 16:17:00 -0500173 ret = True
174 for commit in commit_list:
175 # Mix in some settings for our hooks.
Mike Frysingerdc253622016-04-04 15:43:02 -0400176 os.environ['PREUPLOAD_COMMIT'] = commit
Mike Frysinger2e65c542016-03-08 16:17:00 -0500177 diff = rh.git.get_affected_files(commit)
Mike Frysinger76b1bc72016-04-20 16:50:16 -0400178 desc = rh.git.get_commit_desc(commit)
Nicolas Sylvain6f798862016-05-02 18:58:22 -0700179 os.environ['PREUPLOAD_COMMIT_MESSAGE'] = desc
Mike Frysinger2e65c542016-03-08 16:17:00 -0500180
181 results = []
182 for hook in hooks:
Mike Frysinger76b1bc72016-04-20 16:50:16 -0400183 hook_results = hook(project, commit, desc, diff)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500184 if hook_results:
185 results.extend(hook_results)
186 if results:
Mike Frysinger2e65c542016-03-08 16:17:00 -0500187 if not _process_hook_results(project.name, commit, desc, results):
188 ret = False
189
190 os.chdir(pwd)
191 return ret
192
193
194def main(project_list, worktree_list=None, **_kwargs):
195 """Main function invoked directly by repo.
196
197 We must use the name "main" as that is what repo requires.
198
199 This function will exit directly upon error so that repo doesn't print some
200 obscure error message.
201
202 Args:
203 project_list: List of projects to run on.
204 worktree_list: A list of directories. It should be the same length as
205 project_list, so that each entry in project_list matches with a
206 directory in worktree_list. If None, we will attempt to calculate
207 the directories automatically.
208 kwargs: Leave this here for forward-compatibility.
209 """
210 found_error = False
211 if not worktree_list:
212 worktree_list = [None] * len(project_list)
213 for project, worktree in zip(project_list, worktree_list):
214 if not _run_project_hooks(project, proj_dir=worktree):
215 found_error = True
216
217 if found_error:
218 color = rh.terminal.Color()
219 print('%s: Preupload failed due to above error(s).\n'
220 'For more info, please see:\n%s' %
221 (color.color(color.RED, 'FATAL'), REPOHOOKS_URL),
222 file=sys.stderr)
223 sys.exit(1)
224
225
226def _identify_project(path):
227 """Identify the repo project associated with the given path.
228
229 Returns:
230 A string indicating what project is associated with the path passed in or
231 a blank string upon failure.
232 """
233 cmd = ['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}']
234 return rh.utils.run_command(cmd, capture_output=True, redirect_stderr=True,
235 cwd=path).output.strip()
236
237
238def direct_main(argv):
239 """Run hooks directly (outside of the context of repo).
240
241 Args:
242 argv: The command line args to process.
243
244 Returns:
245 0 if no pre-upload failures, 1 if failures.
246
247 Raises:
248 BadInvocation: On some types of invocation errors.
249 """
250 parser = argparse.ArgumentParser(description=__doc__)
251 parser.add_argument('--dir', default=None,
252 help='The directory that the project lives in. If not '
253 'specified, use the git project root based on the cwd.')
254 parser.add_argument('--project', default=None,
255 help='The project repo path; this can affect how the '
256 'hooks get run, since some hooks are project-specific.'
257 'If not specified, `repo` will be used to figure this '
258 'out based on the dir.')
259 parser.add_argument('commits', nargs='*',
260 help='Check specific commits')
261 opts = parser.parse_args(argv)
262
263 # Check/normalize git dir; if unspecified, we'll use the root of the git
264 # project from CWD.
265 if opts.dir is None:
266 cmd = ['git', 'rev-parse', '--git-dir']
267 git_dir = rh.utils.run_command(cmd, capture_output=True,
268 redirect_stderr=True).output.strip()
269 if not git_dir:
270 parser.error('The current directory is not part of a git project.')
271 opts.dir = os.path.dirname(os.path.abspath(git_dir))
272 elif not os.path.isdir(opts.dir):
273 parser.error('Invalid dir: %s' % opts.dir)
274 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
275 parser.error('Not a git directory: %s' % opts.dir)
276
277 # Identify the project if it wasn't specified; this _requires_ the repo
278 # tool to be installed and for the project to be part of a repo checkout.
279 if not opts.project:
280 opts.project = _identify_project(opts.dir)
281 if not opts.project:
282 parser.error("Repo couldn't identify the project of %s" % opts.dir)
283
284 if _run_project_hooks(opts.project, proj_dir=opts.dir,
285 commit_list=opts.commits):
286 return 0
287 else:
288 return 1
289
290
291if __name__ == '__main__':
292 sys.exit(direct_main(sys.argv[1:]))