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