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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from __future__ import annotations
from pathlib import Path
import pytest
from pygit2 import Oid, Repository, Signature, init_repository
from pygittools.hooks import HookResult
from pygittools.hooks_patterns import NULL_OID, PreReceiveProtectedPattern
MSG_PATTERN: str = r"^(\S+): (.+)"
PROTECTED_MAIN: str = r"^refs/heads/main$"
SIG = Signature("test", "test@example.com")
@pytest.fixture
def repo(tmp_path: Path) -> Repository:
repo_path = tmp_path / "repo.git"
repo_path.mkdir()
return init_repository(str(repo_path), bare=True)
def _commit(repo: Repository, message: str, parents: list[Oid] | None = None) -> Oid:
tb = repo.TreeBuilder()
tree = tb.write()
return repo.create_commit(None, SIG, SIG, message, tree, parents or [])
def _run(
repo: Repository,
ref: str,
old_oid: str,
new_oid: str,
*,
reject_merge_commits: bool = False,
) -> HookResult:
hook = PreReceiveProtectedPattern(
repo,
PROTECTED_MAIN,
MSG_PATTERN,
reject_merge_commits=reject_merge_commits,
)
return hook.run([(ref, old_oid, new_oid)])
def test_ignores_unprotected_ref(repo: Repository) -> None:
bad = _commit(repo, "not conventional")
assert _run(repo, "refs/heads/feature", NULL_OID, str(bad)) == HookResult.SUCCESS
def test_accepts_valid_commit_on_protected_ref(repo: Repository) -> None:
good = _commit(repo, "feat: add thing")
assert _run(repo, "refs/heads/main", NULL_OID, str(good)) == HookResult.SUCCESS
def test_rejects_invalid_commit_on_protected_ref(repo: Repository) -> None:
bad = _commit(repo, "not conventional")
assert _run(repo, "refs/heads/main", NULL_OID, str(bad)) == HookResult.FAILURE
def test_walks_every_commit_in_fast_forward_push(repo: Repository) -> None:
good = _commit(repo, "feat: first")
bad = _commit(repo, "not conventional", [good])
assert _run(repo, "refs/heads/main", str(good), str(bad)) == HookResult.FAILURE
def test_skips_merge_commit_message_by_default(repo: Repository) -> None:
base = _commit(repo, "feat: base")
feature = _commit(repo, "feat: side", [base])
merge = _commit(repo, "Merge branch 'feature'", [base, feature])
assert _run(repo, "refs/heads/main", str(base), str(merge)) == HookResult.SUCCESS
def test_rejects_merge_commit_when_configured(repo: Repository) -> None:
base = _commit(repo, "feat: base")
feature = _commit(repo, "feat: side", [base])
merge = _commit(repo, "Merge branch 'feature'", [base, feature])
result = _run(repo, "refs/heads/main", str(base), str(merge), reject_merge_commits=True)
assert result == HookResult.FAILURE
def test_validates_non_merge_commits_introduced_by_merge(repo: Repository) -> None:
base = _commit(repo, "feat: base")
bad = _commit(repo, "bad message on feature", [base])
merge = _commit(repo, "Merge branch 'feature'", [base, bad])
assert _run(repo, "refs/heads/main", str(base), str(merge)) == HookResult.FAILURE
def test_allows_ref_deletion(repo: Repository) -> None:
base = _commit(repo, "feat: base")
assert _run(repo, "refs/heads/main", str(base), NULL_OID) == HookResult.SUCCESS