blob: 495a4a346c653806387ce0f1452416c645445ccb [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 """
101 try:
102 config = rh.config.PreSubmitConfig()
103 except rh.config.ValidationError as e:
104 print('invalid config file: %s' % (e,), file=sys.stderr)
105 sys.exit(1)
106 return config.callable_hooks()
107
108
109def _run_project_hooks(project_name, proj_dir=None,
110 commit_list=None):
111 """For each project run its project specific hook from the hooks dictionary.
112
113 Args:
114 project_name: The name of project to run hooks for.
115 proj_dir: If non-None, this is the directory the project is in. If None,
116 we'll ask repo.
117 commit_list: A list of commits to run hooks against. If None or empty
118 list then we'll automatically get the list of commits that would be
119 uploaded.
120
121 Returns:
122 False if any errors were found, else True.
123 """
124 if proj_dir is None:
125 cmd = ['repo', 'forall', project_name, '-c', 'pwd']
126 result = rh.utils.run_command(cmd, capture_output=True)
127 proj_dirs = result.output.split()
128 if len(proj_dirs) == 0:
129 print('%s cannot be found.' % project_name, file=sys.stderr)
130 print('Please specify a valid project.', file=sys.stderr)
131 return 0
132 if len(proj_dirs) > 1:
133 print('%s is associated with multiple directories.' % project_name,
134 file=sys.stderr)
135 print('Please specify a directory to help disambiguate.',
136 file=sys.stderr)
137 return 0
138 proj_dir = proj_dirs[0]
139
140 pwd = os.getcwd()
141 # Hooks assume they are run from the root of the project.
142 os.chdir(proj_dir)
143
Mike Frysinger558aff42016-04-04 16:02:55 -0400144 # If the repo has no pre-upload hooks enabled, then just return.
145 hooks = list(_get_project_hooks())
146 if not hooks:
147 return True
148
Mike Frysinger2e65c542016-03-08 16:17:00 -0500149 # Set up the environment like repo would with the forall command.
Mike Frysinger05c689e2016-04-04 18:51:38 -0400150 remote = rh.git.get_upstream_remote()
Mike Frysinger2e65c542016-03-08 16:17:00 -0500151 os.environ.update({
152 'REPO_PROJECT': project_name,
153 'REPO_PATH': proj_dir,
154 'REPO_REMOTE': remote,
155 })
156
157 project = Project(name=project_name, dir=proj_dir, remote=remote)
158
159 if not commit_list:
160 commit_list = rh.git.get_commits()
161
Mike Frysinger2e65c542016-03-08 16:17:00 -0500162 ret = True
163 for commit in commit_list:
164 # Mix in some settings for our hooks.
Mike Frysingerdc253622016-04-04 15:43:02 -0400165 os.environ['PREUPLOAD_COMMIT'] = commit
Mike Frysinger2e65c542016-03-08 16:17:00 -0500166 diff = rh.git.get_affected_files(commit)
167
168 results = []
169 for hook in hooks:
170 hook_results = hook(project, commit, diff)
171 if hook_results:
172 results.extend(hook_results)
173 if results:
174 desc = rh.git.get_commit_desc(commit)
175 if not _process_hook_results(project.name, commit, desc, results):
176 ret = False
177
178 os.chdir(pwd)
179 return ret
180
181
182def main(project_list, worktree_list=None, **_kwargs):
183 """Main function invoked directly by repo.
184
185 We must use the name "main" as that is what repo requires.
186
187 This function will exit directly upon error so that repo doesn't print some
188 obscure error message.
189
190 Args:
191 project_list: List of projects to run on.
192 worktree_list: A list of directories. It should be the same length as
193 project_list, so that each entry in project_list matches with a
194 directory in worktree_list. If None, we will attempt to calculate
195 the directories automatically.
196 kwargs: Leave this here for forward-compatibility.
197 """
198 found_error = False
199 if not worktree_list:
200 worktree_list = [None] * len(project_list)
201 for project, worktree in zip(project_list, worktree_list):
202 if not _run_project_hooks(project, proj_dir=worktree):
203 found_error = True
204
205 if found_error:
206 color = rh.terminal.Color()
207 print('%s: Preupload failed due to above error(s).\n'
208 'For more info, please see:\n%s' %
209 (color.color(color.RED, 'FATAL'), REPOHOOKS_URL),
210 file=sys.stderr)
211 sys.exit(1)
212
213
214def _identify_project(path):
215 """Identify the repo project associated with the given path.
216
217 Returns:
218 A string indicating what project is associated with the path passed in or
219 a blank string upon failure.
220 """
221 cmd = ['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}']
222 return rh.utils.run_command(cmd, capture_output=True, redirect_stderr=True,
223 cwd=path).output.strip()
224
225
226def direct_main(argv):
227 """Run hooks directly (outside of the context of repo).
228
229 Args:
230 argv: The command line args to process.
231
232 Returns:
233 0 if no pre-upload failures, 1 if failures.
234
235 Raises:
236 BadInvocation: On some types of invocation errors.
237 """
238 parser = argparse.ArgumentParser(description=__doc__)
239 parser.add_argument('--dir', default=None,
240 help='The directory that the project lives in. If not '
241 'specified, use the git project root based on the cwd.')
242 parser.add_argument('--project', default=None,
243 help='The project repo path; this can affect how the '
244 'hooks get run, since some hooks are project-specific.'
245 'If not specified, `repo` will be used to figure this '
246 'out based on the dir.')
247 parser.add_argument('commits', nargs='*',
248 help='Check specific commits')
249 opts = parser.parse_args(argv)
250
251 # Check/normalize git dir; if unspecified, we'll use the root of the git
252 # project from CWD.
253 if opts.dir is None:
254 cmd = ['git', 'rev-parse', '--git-dir']
255 git_dir = rh.utils.run_command(cmd, capture_output=True,
256 redirect_stderr=True).output.strip()
257 if not git_dir:
258 parser.error('The current directory is not part of a git project.')
259 opts.dir = os.path.dirname(os.path.abspath(git_dir))
260 elif not os.path.isdir(opts.dir):
261 parser.error('Invalid dir: %s' % opts.dir)
262 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
263 parser.error('Not a git directory: %s' % opts.dir)
264
265 # Identify the project if it wasn't specified; this _requires_ the repo
266 # tool to be installed and for the project to be part of a repo checkout.
267 if not opts.project:
268 opts.project = _identify_project(opts.dir)
269 if not opts.project:
270 parser.error("Repo couldn't identify the project of %s" % opts.dir)
271
272 if _run_project_hooks(opts.project, proj_dir=opts.dir,
273 commit_list=opts.commits):
274 return 0
275 else:
276 return 1
277
278
279if __name__ == '__main__':
280 sys.exit(direct_main(sys.argv[1:]))