Loading .pre-commit-config.yaml +6 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,12 @@ repos: require_serial: true types: [python] stages: [commit] - id: check-for-squash name: Check for commits that need squashing language: system entry: hooks/squash.py pass_filenames: false stages: [push] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.5.1 Loading .pre-commit-hooks.yaml +7 −0 Original line number Diff line number Diff line Loading @@ -8,3 +8,10 @@ require_serial: true types: [python] stages: [commit] - id: check-for-squash name: Check for commits that need squashing language: python entry: check-for-squash pass_filenames: false stages: [push] hooks/squash.py 0 → 100755 +139 −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. """ Block commit starting with "squash!" or "fixup!" from being pushed These commits must be squashed before a push to a remote """ import os import sys from collections import defaultdict from pathlib import Path from subprocess import PIPE from subprocess import Popen Z40 = '0' * 40 GIT_EXEC_PATH = 'GIT_EXEC_PATH' PRE_COMMIT_TO_REF = 'PRE_COMMIT_TO_REF' PRE_COMMIT_FROM_REF = 'PRE_COMMIT_FROM_REF' class CommitDict(defaultdict): """ A dict for holding Commit objects mapped from their subject lines """ def __missing__(self, subject): self[subject] = commit = Commit(subject) return commit class Commit: """ A class for holding information about regular commits """ __slots__ = 'subject', 'sha', 'updates' def __init__(self, subject: str, sha: str = Z40): if len(sha) != 40 or not sha.isalnum(): raise ValueError("'sha' must be a SHA-1 hash") self.subject = subject.strip() self.sha = sha self.updates = [] def git_log_cmd(): """ Return the first arguments for running a git-log command """ if GIT_EXEC_PATH in os.environ: git_log = Path.cwd() / os.environ[GIT_EXEC_PATH] / 'git-log' if git_log.exists() and git_log.stat().st_mode & os.X_OK: return [git_log] return ['git', 'log'] def catalogue_commits(): """ Yield all regular commits as Commit objects, containing any update (squash) commits """ cmd = git_log_cmd() cmd.extend([ '--reverse', '--format=format:%H %s', f"{os.environ[PRE_COMMIT_FROM_REF]}...{os.environ[PRE_COMMIT_TO_REF]}", ]) commits = CommitDict() with Popen(cmd, stdout=PIPE, text=True) as proc: for line in proc.stdout.readlines(): sha, _, subject = line.partition(' ') keyword, has_marker, target = subject.partition('! ') if has_marker and keyword in ('squash', 'fixup'): # Squash commit, add it to its target list commits[target].updates.append(sha) continue if subject in commits: # Regular commit, but another exists with the same message, so yield the # previous one first yield commits.pop(subject) commits[subject] = Commit(subject, sha) yield from commits.values() def main(): """ CLI entrypoint """ retcode = 0 output = sys.stderr.write for commit in catalogue_commits(): if not commit.updates: continue if not retcode: retcode = 1 output("The following commits need to be squashed before pushing:\n\n") output(" target:\n") output( f" {commit.subject[:60]}\n" if commit.sha == Z40 else f" {commit.subject[:60]} ({commit.sha[:8]})\n", ) output(" commits:\n") output(f" {', '.join(commit.updates)}\n") if commit.sha == Z40: output(" note:\n") output(" The target for the above commits is not part of this update set.\n") output(" You should squash them into a new commit.\n") output("\n") sys.exit(retcode) if __name__ == '__main__': main() pyproject.toml +1 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ requires-python = "~=3.6" [tool.flit.scripts] check-copyright-notice = "hooks.copyright:main" check-for-squash = "hooks.squash:main" [tool.isort] force_single_line = true Loading
.pre-commit-config.yaml +6 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,12 @@ repos: require_serial: true types: [python] stages: [commit] - id: check-for-squash name: Check for commits that need squashing language: system entry: hooks/squash.py pass_filenames: false stages: [push] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.5.1 Loading
.pre-commit-hooks.yaml +7 −0 Original line number Diff line number Diff line Loading @@ -8,3 +8,10 @@ require_serial: true types: [python] stages: [commit] - id: check-for-squash name: Check for commits that need squashing language: python entry: check-for-squash pass_filenames: false stages: [push]
hooks/squash.py 0 → 100755 +139 −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. """ Block commit starting with "squash!" or "fixup!" from being pushed These commits must be squashed before a push to a remote """ import os import sys from collections import defaultdict from pathlib import Path from subprocess import PIPE from subprocess import Popen Z40 = '0' * 40 GIT_EXEC_PATH = 'GIT_EXEC_PATH' PRE_COMMIT_TO_REF = 'PRE_COMMIT_TO_REF' PRE_COMMIT_FROM_REF = 'PRE_COMMIT_FROM_REF' class CommitDict(defaultdict): """ A dict for holding Commit objects mapped from their subject lines """ def __missing__(self, subject): self[subject] = commit = Commit(subject) return commit class Commit: """ A class for holding information about regular commits """ __slots__ = 'subject', 'sha', 'updates' def __init__(self, subject: str, sha: str = Z40): if len(sha) != 40 or not sha.isalnum(): raise ValueError("'sha' must be a SHA-1 hash") self.subject = subject.strip() self.sha = sha self.updates = [] def git_log_cmd(): """ Return the first arguments for running a git-log command """ if GIT_EXEC_PATH in os.environ: git_log = Path.cwd() / os.environ[GIT_EXEC_PATH] / 'git-log' if git_log.exists() and git_log.stat().st_mode & os.X_OK: return [git_log] return ['git', 'log'] def catalogue_commits(): """ Yield all regular commits as Commit objects, containing any update (squash) commits """ cmd = git_log_cmd() cmd.extend([ '--reverse', '--format=format:%H %s', f"{os.environ[PRE_COMMIT_FROM_REF]}...{os.environ[PRE_COMMIT_TO_REF]}", ]) commits = CommitDict() with Popen(cmd, stdout=PIPE, text=True) as proc: for line in proc.stdout.readlines(): sha, _, subject = line.partition(' ') keyword, has_marker, target = subject.partition('! ') if has_marker and keyword in ('squash', 'fixup'): # Squash commit, add it to its target list commits[target].updates.append(sha) continue if subject in commits: # Regular commit, but another exists with the same message, so yield the # previous one first yield commits.pop(subject) commits[subject] = Commit(subject, sha) yield from commits.values() def main(): """ CLI entrypoint """ retcode = 0 output = sys.stderr.write for commit in catalogue_commits(): if not commit.updates: continue if not retcode: retcode = 1 output("The following commits need to be squashed before pushing:\n\n") output(" target:\n") output( f" {commit.subject[:60]}\n" if commit.sha == Z40 else f" {commit.subject[:60]} ({commit.sha[:8]})\n", ) output(" commits:\n") output(f" {', '.join(commit.updates)}\n") if commit.sha == Z40: output(" note:\n") output(" The target for the above commits is not part of this update set.\n") output(" You should squash them into a new commit.\n") output("\n") sys.exit(retcode) if __name__ == '__main__': main()
pyproject.toml +1 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ requires-python = "~=3.6" [tool.flit.scripts] check-copyright-notice = "hooks.copyright:main" check-for-squash = "hooks.squash:main" [tool.isort] force_single_line = true