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