diff --git a/Scripts/git_hooks/README.md b/Scripts/git_hooks/README.md new file mode 100644 index 000000000..146c2df48 --- /dev/null +++ b/Scripts/git_hooks/README.md @@ -0,0 +1 @@ +Copy these git hooks into .git/hooks diff --git a/Scripts/git_hooks/post-commit b/Scripts/git_hooks/post-commit new file mode 100755 index 000000000..ed392f169 --- /dev/null +++ b/Scripts/git_hooks/post-commit @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +Scripts/reverse_integration_check.py diff --git a/Scripts/git_hooks/pre-commit b/Scripts/git_hooks/pre-commit new file mode 100755 index 000000000..7c72b84e5 --- /dev/null +++ b/Scripts/git_hooks/pre-commit @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import datetime +import argparse +import commands + + +git_repo_path = os.path.abspath(subprocess.check_output(['git', 'rev-parse', '--show-toplevel']).strip()) + + +def splitall(path): + allparts = [] + while 1: + parts = os.path.split(path) + if parts[0] == path: # sentinel for absolute paths + allparts.insert(0, parts[0]) + break + elif parts[1] == path: # sentinel for relative paths + allparts.insert(0, parts[1]) + break + else: + path = parts[0] + allparts.insert(0, parts[1]) + return allparts + + +def process(filepath): + + short_filepath = filepath[len(git_repo_path):] + if short_filepath.startswith(os.sep): + short_filepath = short_filepath[len(os.sep):] + + filename = os.path.basename(filepath) + if filename.startswith('.'): + return + file_ext = os.path.splitext(filename)[1] + if file_ext in ('.swift'): + env_copy = os.environ.copy() + env_copy["SCRIPT_INPUT_FILE_COUNT"] = "1" + env_copy["SCRIPT_INPUT_FILE_0"] = '%s' % ( short_filepath, ) + lint_output = subprocess.check_output(['swiftlint', 'autocorrect', '--use-script-input-files'], env=env_copy) + print lint_output + try: + lint_output = subprocess.check_output(['swiftlint', 'lint', '--use-script-input-files'], env=env_copy) + except subprocess.CalledProcessError, e: + lint_output = e.output + print lint_output + + with open(filepath, 'rt') as f: + text = f.read() + original_text = text + + lines = text.split('\n') + while lines and lines[0].startswith('//'): + lines = lines[1:] + text = '\n'.join(lines) + text = text.strip() + + header = '''// +// Copyright (c) %s Open Whisper Systems. All rights reserved. +// + +''' % ( + datetime.datetime.now().year, + ) + text = header + text + '\n' + + if original_text == text: + return + + print 'Updating:', short_filepath + + with open(filepath, 'wt') as f: + f.write(text) + + +def should_ignore_path(path): + ignore_paths = [ + os.path.join(git_repo_path, '.git') + ] + for ignore_path in ignore_paths: + if path.startswith(ignore_path): + return True + for component in splitall(path): + if component.startswith('.'): + return True + if component.endswith('.framework'): + return True + if component in ('Pods', 'ThirdParty', 'Carthage',): + return True + + return False + + +def process_if_appropriate(filepath): + filename = os.path.basename(filepath) + if filename.startswith('.'): + return + file_ext = os.path.splitext(filename)[1] + if file_ext not in ('.h', '.hpp', '.cpp', '.m', '.mm', '.pch', '.swift'): + return + if should_ignore_path(filepath): + return + process(filepath) + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description='Precommit script.') + parser.add_argument('--all', action='store_true', help='process all files in or below current dir') + args = parser.parse_args() + + if args.all: + for rootdir, dirnames, filenames in os.walk(git_repo_path): + for filename in filenames: + file_path = os.path.abspath(os.path.join(rootdir, filename)) + process_if_appropriate(file_path) + else: + filepaths = [] + + # Staging + output = commands.getoutput('git diff --cached --name-only --diff-filter=ACMR') + filepaths.extend([line.strip() for line in output.split('\n')]) + + # Working + output = commands.getoutput('git diff --name-only --diff-filter=ACMR') + filepaths.extend([line.strip() for line in output.split('\n')]) + + # Only process each path once. + filepaths = sorted(set(filepaths)) + + for filepath in filepaths: + filepath = os.path.abspath(os.path.join(git_repo_path, filepath)) + process_if_appropriate(filepath) + + print 'git clang-format...' + print commands.getoutput('git clang-format') diff --git a/Scripts/reverse_integration_check.py b/Scripts/reverse_integration_check.py new file mode 100755 index 000000000..c366354ba --- /dev/null +++ b/Scripts/reverse_integration_check.py @@ -0,0 +1,45 @@ +#!/usr/bin/python + +# When we make a hotfix, we need to reverse integrate our hotfix back into +# master. After commiting to master, this script audits that all tags have been +# reverse integrated. +import subprocess +from distutils.version import LooseVersion +import logging + +#logging.basicConfig(level=logging.DEBUG) + +def is_on_master(): + output = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).strip() + logging.debug("branch output: %s" % output) + return output == "master" + +def main(): + if not is_on_master(): + # Don't interfere while on a feature or hotfix branch + logging.debug("not on master branch") + return + + logging.debug("on master branch") + + unmerged_tags_output = subprocess.check_output(["git", "tag", "--no-merged", "master"]) + unmerged_tags = [line.strip() for line in unmerged_tags_output.split("\n") if len(line) > 0] + + logging.debug("All unmerged tags: %s" % unmerged_tags) + + # Before this point we weren't always reverse integrating our tags. As we + # audit old tags, we can ratchet this version number back. + epoch_tag="2.21.0" + + logging.debug("ignoring tags before epoch_tag: %s" % epoch_tag) + + tags_of_concern = [unmerged_tag for unmerged_tag in unmerged_tags if LooseVersion(unmerged_tag) > LooseVersion(epoch_tag)] + + if len(tags_of_concern) > 0: + logging.debug("Found unmerged tags newer than epoch: %s" % tags_of_concern) + raise RuntimeError("Found unmerged tags: %s" % tags_of_concern) + else: + logging.debug("No unmerged tags newer than epoch. All good!") + +if __name__ == "__main__": + main()