| #!src/build/run_python |
| |
| # Copyright 2014 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """A git pre-push hook script.""" |
| |
| import fnmatch |
| import md5 |
| import os.path |
| import subprocess |
| import sys |
| |
| from src.build import build_common |
| from src.build import check_chrome_lkgr |
| from src.build import convert_docs |
| from src.build import lint_source |
| from src.build import staging |
| from src.build import suggest_reviewers |
| from src.build.util import git |
| |
| |
| def _is_patch_to_next_pastry(): |
| """Determines if the current patch is to arc/next-pastry or not.""" |
| return git.get_branch_tracked_remote_url().endswith('arc/next-pastry') |
| |
| |
| def _check_uncommitted_change(): |
| uncommitted_files = git.get_uncommitted_files() |
| if uncommitted_files: |
| print '' |
| print 'Please commit or stash the following uncommitted files.' |
| print '\n'.join(uncommitted_files) |
| print '' |
| return -1 |
| return 0 |
| |
| |
| def _check_lint(push_files): |
| ignore_file = os.path.join('src', 'build', 'lint_ignore.txt') |
| # If push_files contains any directories (representing submodules), filter |
| # them out. Passing a directory to the lint_source.process will cause all |
| # the files in that directory to be checked for lint errors, when those files |
| # may not even conform to the standards of the current project. |
| push_files = filter(os.path.isfile, push_files) |
| result = lint_source.process(push_files, ignore_file) |
| if result != 0: |
| print '' |
| print 'lint_source.py reports there are issues with the code you are trying' |
| print 'to submit. Buildbot will run the same checks and fail if you do not' |
| print 'fix or suppress them. See docs/source-code-style-tools.md for how' |
| print 'to suppress the errors if they are false alarms.' |
| print '' |
| return result |
| |
| |
| def _check_build_steps(): |
| build_steps = os.path.join('src', 'buildbot', 'build_steps.py') |
| result = subprocess.call([build_steps, '--test', '--silent']) |
| if result != 0: |
| print '' |
| print 'build_steps.py reports there are issues with the recipes you are' |
| print 'trying to submit. Buildbot will run the same checks and fail.' |
| print 'Run "./src/buildbot/build_steps.py --train" to update the recipes.' |
| print '' |
| return result |
| |
| |
| def _check_prebuilt_chrome_deps(push_files): |
| chrome_deps_file = build_common.get_chrome_deps_file() |
| if chrome_deps_file not in push_files: |
| return 0 |
| with open(chrome_deps_file) as f: |
| revision = int(f.read().strip()) |
| if not check_chrome_lkgr.has_prebuilt_chrome_for_all_platforms( |
| revision, True): |
| print '' |
| print 'check_chrome_lkgr.py reports there is no prebuilt chrome binary ' |
| print 'corresponding to the chrome revision you are trying to submit. ' |
| print 'Run "./src/build/check_chrome_lkgr.py" to pick an LKGR.' |
| print '' |
| return -1 |
| return 0 |
| |
| |
| def _check_android_deps(push_files): |
| android_deps_file = build_common.get_android_deps_file() |
| if android_deps_file not in push_files: |
| return 0 |
| with open(android_deps_file) as f: |
| linecount = len(f.readlines()) |
| # For normal purposes, we expect that the DEPS.android file will contain just |
| # a single line naming an Android release branch tag, or a very small file |
| # mapping a few subprojects to specific release tags, with a default mapping |
| # of the official Android branch tag used for everything else. |
| # For arc/next-pastry we allow it to be an Android manifest file, which |
| # identifies git hash tags for each Android sub-project. However these |
| # sub-project names may be confidential, and should not be published, so we |
| # have this check to help ensure that does not happen accidentally. |
| if linecount > 10: |
| if not _is_patch_to_next_pastry(): |
| print '' |
| print 'DEPS.android appears to be something other than a simple file' |
| print 'naming a publicly visible branch tag. Any other content should' |
| print 'be restricted to arc/next-pastry.' |
| print '' |
| return -1 |
| |
| |
| def _check_ninja_lint_clean_after_deps_change(push_files): |
| # Watch out for any deps changes that could impact mods |
| prefix_pattern = os.path.join('src', 'build', 'DEPS.') |
| if not any(name.startswith(prefix_pattern) for name in push_files): |
| return 0 |
| return subprocess.call(['ninja', 'lint']) |
| |
| |
| def _check_commit_messages(): |
| MAX_COLS = 100 |
| error = False |
| changes = git.get_in_flight_commits() |
| for change in changes: |
| msg = git.get_commit_message(change) |
| seen_change_id = False |
| trailing_lines = False |
| for line_num, line in enumerate(msg): |
| if len(line) > MAX_COLS: |
| print 'Commit %s line %d is too long:' % (change, line_num + 1) |
| print line |
| print (' ' * MAX_COLS) + ('^' * (len(line) - MAX_COLS)) |
| error = True |
| if seen_change_id: |
| trailing_lines = True |
| if line.startswith('Change-Id:'): |
| seen_change_id = True |
| if not seen_change_id: |
| print 'Commit %s does not have a Change-Id label' % change |
| error = True |
| elif trailing_lines: |
| print 'Commit %s has trailing lines after Change-Id label' % change |
| error = True |
| if error: |
| return -1 |
| return 0 |
| |
| |
| def _check_docs(push_files): |
| """Check if files in 'docs' directory are valid. |
| |
| Ensure that files in 'docs' are named and formatted correctly. |
| """ |
| |
| doc_files = fnmatch.filter(push_files, 'docs/*.*') |
| if not convert_docs.validate_docs(doc_files): |
| print '' |
| print 'convert_docs.py reports there are issues with the docs you are ' |
| print 'trying to submit. Run ' |
| print '"./src/build/convert_docs --validate docs/*.md" to validate.' |
| print '' |
| return -1 |
| return 0 |
| |
| |
| def _get_file_list_digest(files): |
| digest = md5.new() |
| for f in sorted(files): |
| digest.update(f) |
| # Add a file path separator. chr(1) is unlikely to be part of a |
| # file path. |
| digest.update(chr(1)) |
| return digest.hexdigest() |
| |
| |
| def _has_file_list_changed_since_last_push(files): |
| file_list_digest = _get_file_list_digest(files) |
| old_file_list_digest = git.get_branch_specific_config('filelist') |
| if not old_file_list_digest: |
| return True |
| return old_file_list_digest != file_list_digest |
| |
| |
| def _save_file_list(files): |
| git.set_branch_specific_config('filelist', _get_file_list_digest(files)) |
| |
| |
| # Export for other repo to reuse. |
| def get_push_files(): |
| last_landed_commit = git.get_last_landed_commit() |
| # Find out what files have changed since last landed commit |
| # * That are staged (--cached) -- this speeds up the check |
| # * Returning their names as a simple list (--name-only) |
| # * That are not deleted (--diff-filter=ACM) |
| cmd = ('git diff %s --cached --name-only --diff-filter=ACM' % |
| last_landed_commit) |
| push_files = subprocess.check_output(cmd, shell=True).splitlines() |
| |
| # Remove files that will confuse analyze_diffs |
| return filter( |
| lambda f: not f.startswith(staging.TESTS_BASE_PATH), |
| push_files) |
| |
| |
| def main(): |
| push_files = get_push_files() |
| if not push_files: |
| return 0 |
| |
| non_push_file_checks = [ |
| _check_uncommitted_change, |
| _check_commit_messages, |
| _check_build_steps, |
| ] |
| |
| push_file_checks = [ |
| _check_lint, |
| _check_prebuilt_chrome_deps, |
| _check_android_deps, |
| _check_ninja_lint_clean_after_deps_change, |
| _check_docs, |
| ] |
| |
| for check in non_push_file_checks: |
| result = check() |
| if result != 0: |
| return result |
| |
| for check in push_file_checks: |
| result = check(push_files) |
| if result != 0: |
| return result |
| |
| if _has_file_list_changed_since_last_push(push_files): |
| suggest_reviewers.suggest_reviewer_set_for_in_flight_commits(False) |
| # Add some space to make sure this is visible to the user as |
| # lots of extra push noise will be shown immediately after. |
| print '' |
| _save_file_list(push_files) |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |