1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
"""
Post-commit hook helpers: force-with-lease push to remotes via the git CLI.
"""
from __future__ import annotations
import os
import re
import shutil
import subprocess
from sys import stderr
import pygit2
from pygittools.hooks import HookResult, PostCommit
def git_executable() -> str:
"""Return the git binary to invoke (``GIT`` env, then ``PATH``, else ``git``)."""
return os.environ.get("GIT") or shutil.which("git") or "git"
def force_with_lease_push(
repo: pygit2.Repository,
remote_name: str,
branch: str,
*,
git: str | None = None,
) -> str | None:
"""Force-with-lease push ``branch`` to ``remote_name``. Returns an error string or None."""
workdir: str | None = repo.workdir
if workdir is None:
return "bare repository has no working tree"
if branch not in repo.branches.local:
return f"branch {branch!r} does not exist locally"
if remote_name not in repo.remotes.names():
return f"remote {remote_name!r} not configured"
binary: str = git or git_executable()
proc: subprocess.CompletedProcess[str] = subprocess.run(
[binary, "-C", workdir, "push", "--force-with-lease", remote_name, branch],
capture_output=True,
text=True,
)
if proc.returncode != 0:
return (proc.stderr or proc.stdout or "push failed").strip()
return None
class PostCommitPushRemotes(PostCommit):
"""After each commit, force-with-lease push the current branch to every remote if it matches a pattern."""
def __init__(self, repo: pygit2.Repository, branch_pattern: str) -> None:
super().__init__(repo)
self._branch_exp: re.Pattern[str] = re.compile(branch_pattern)
def run(self) -> HookResult:
if self.repo.head_is_detached:
return HookResult.SUCCESS
branch: str = self.repo.head.shorthand
if not self._branch_exp.match(branch):
return HookResult.SUCCESS
if self.repo.workdir is None:
stderr.write("PostCommitPushRemotes: bare repository; skipping push.\n")
return HookResult.SUCCESS
remote_names: list[str] = list(self.repo.remotes.names())
if not remote_names:
return HookResult.SUCCESS
failures: list[str] = []
for remote_name in remote_names:
error: str | None = force_with_lease_push(self.repo, remote_name, branch)
if error is not None:
failures.append(f"{remote_name}: {error}")
if failures:
stderr.write(f"PostCommitPushRemotes ({branch}): push failed:\n")
for line in failures:
stderr.write(f" {line}\n")
return HookResult.SUCCESS