Loading .pre-commit-config.yaml +6 −5 Original line number Diff line number Diff line Loading @@ -42,11 +42,6 @@ repos: hooks: - id: gitlint - repo: https://github.com/jumanjihouse/pre-commit-hooks rev: 2.1.5 hooks: - id: protect-first-parent - repo: local hooks: - id: copyright-notice Loading @@ -62,6 +57,12 @@ repos: entry: hooks/squash.py pass_filenames: false stages: [push] - id: protect-first-parent name: Check for "git pull" violations language: python entry: hooks/first_parent.py pass_filenames: false stages: [merge-commit, commit, push] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.8.0 Loading .pre-commit-hooks.yaml +8 −0 Original line number Diff line number Diff line Loading @@ -65,3 +65,11 @@ entry: check-for-squash pass_filenames: false stages: [push] - id: protect-first-parent name: Check for "git pull" violations language: python entry: check-first-parent always_run: true pass_filenames: false stages: [merge-commit, commit, push] hooks/first_parent.py 0 → 100755 +91 −0 Original line number Diff line number Diff line #!/usr/bin/env python3 # Copyright 2021 Dominik Sekotill <dom.sekotill@kodo.org.uk> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Check that the first-parent commit graph includes the upstream branch The first-parent graph is the linear graph of commits which always follows the first parent of any merge commits. It is often seen as the canonical commit graph, with any n-th parent lines (where n is greater than 1) seen as merged topic branches. This hook checks that the upstream head exists in the first-parent graph and blocks a commit or push if it does not. If there is no upstream branch, this hook always allows the commit/push to go ahead. For some background reading on the situation where an upstream head is not on the first-parent graph (dubbed a "foxtrot merge") I suggest the following article: - https://bit-booster.blogspot.com/2016/02/no-foxtrots-allowed.html """ import sys from subprocess import PIPE from subprocess import CalledProcessError from subprocess import run from typing import Optional from typing import Set FAIL_MSG = """ [FAIL] The upstream branch {upstream} is not on the first-parent linear commit graph. You probably ran "git pull" without --rebase. If you see this message as output from "git pull" then abort with "git merge --abort" and re-run "git pull --rebase"; otherwise try running "git rebase". """ def get_fp_commits(ref: str) -> Set[str]: """ Return the set of commits that are in the first-parent linear graph from ref """ cmd = ['git', 'rev-list', '--first-parent', ref] proc = run(cmd, stdout=PIPE, text=True, check=True) if proc.stdout is None: raise RuntimeError return set(proc.stdout.split()) def resolve_ref(ref: str) -> Optional[str]: """ Return a named head for a given ref, or None if there is no head If 'ref' does not exist None is also returned. """ cmd = ['git', 'rev-parse', '--abbrev-ref', ref] try: proc = run(cmd, stdout=PIPE, text=True, check=True) except CalledProcessError as exc: if exc.returncode == 128: return None raise return proc.stdout.strip() or None def main() -> None: """ CLI entrypoint """ if not resolve_ref('@{upstream}'): return head_commits = get_fp_commits('HEAD') upstream_commits = get_fp_commits('@{upstream}') if not upstream_commits.difference(head_commits): return sys.stderr.write(FAIL_MSG.format(upstream=resolve_ref('@{upstream}'))) sys.exit(1) if __name__ == '__main__': main() pyproject.toml +1 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ requires-python = "~=3.6" [tool.flit.scripts] check-copyright-notice = "hooks.copyright:main" check-for-squash = "hooks.squash:main" check-first-parent = "hooks.first_parent:main" [tool.isort] force_single_line = true Loading Loading
.pre-commit-config.yaml +6 −5 Original line number Diff line number Diff line Loading @@ -42,11 +42,6 @@ repos: hooks: - id: gitlint - repo: https://github.com/jumanjihouse/pre-commit-hooks rev: 2.1.5 hooks: - id: protect-first-parent - repo: local hooks: - id: copyright-notice Loading @@ -62,6 +57,12 @@ repos: entry: hooks/squash.py pass_filenames: false stages: [push] - id: protect-first-parent name: Check for "git pull" violations language: python entry: hooks/first_parent.py pass_filenames: false stages: [merge-commit, commit, push] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.8.0 Loading
.pre-commit-hooks.yaml +8 −0 Original line number Diff line number Diff line Loading @@ -65,3 +65,11 @@ entry: check-for-squash pass_filenames: false stages: [push] - id: protect-first-parent name: Check for "git pull" violations language: python entry: check-first-parent always_run: true pass_filenames: false stages: [merge-commit, commit, push]
hooks/first_parent.py 0 → 100755 +91 −0 Original line number Diff line number Diff line #!/usr/bin/env python3 # Copyright 2021 Dominik Sekotill <dom.sekotill@kodo.org.uk> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Check that the first-parent commit graph includes the upstream branch The first-parent graph is the linear graph of commits which always follows the first parent of any merge commits. It is often seen as the canonical commit graph, with any n-th parent lines (where n is greater than 1) seen as merged topic branches. This hook checks that the upstream head exists in the first-parent graph and blocks a commit or push if it does not. If there is no upstream branch, this hook always allows the commit/push to go ahead. For some background reading on the situation where an upstream head is not on the first-parent graph (dubbed a "foxtrot merge") I suggest the following article: - https://bit-booster.blogspot.com/2016/02/no-foxtrots-allowed.html """ import sys from subprocess import PIPE from subprocess import CalledProcessError from subprocess import run from typing import Optional from typing import Set FAIL_MSG = """ [FAIL] The upstream branch {upstream} is not on the first-parent linear commit graph. You probably ran "git pull" without --rebase. If you see this message as output from "git pull" then abort with "git merge --abort" and re-run "git pull --rebase"; otherwise try running "git rebase". """ def get_fp_commits(ref: str) -> Set[str]: """ Return the set of commits that are in the first-parent linear graph from ref """ cmd = ['git', 'rev-list', '--first-parent', ref] proc = run(cmd, stdout=PIPE, text=True, check=True) if proc.stdout is None: raise RuntimeError return set(proc.stdout.split()) def resolve_ref(ref: str) -> Optional[str]: """ Return a named head for a given ref, or None if there is no head If 'ref' does not exist None is also returned. """ cmd = ['git', 'rev-parse', '--abbrev-ref', ref] try: proc = run(cmd, stdout=PIPE, text=True, check=True) except CalledProcessError as exc: if exc.returncode == 128: return None raise return proc.stdout.strip() or None def main() -> None: """ CLI entrypoint """ if not resolve_ref('@{upstream}'): return head_commits = get_fp_commits('HEAD') upstream_commits = get_fp_commits('@{upstream}') if not upstream_commits.difference(head_commits): return sys.stderr.write(FAIL_MSG.format(upstream=resolve_ref('@{upstream}'))) sys.exit(1) if __name__ == '__main__': main()
pyproject.toml +1 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ requires-python = "~=3.6" [tool.flit.scripts] check-copyright-notice = "hooks.copyright:main" check-for-squash = "hooks.squash:main" check-first-parent = "hooks.first_parent:main" [tool.isort] force_single_line = true Loading