Skip to content

Commit 979eb16

Browse files
authored
Merge pull request #3 from MiraGeoscience/DEVOPS-404
DEVOPS-404: Fix pre-commit hook preparing commit message
2 parents fe7d77e + 34fae5c commit 979eb16

6 files changed

Lines changed: 149 additions & 10 deletions

File tree

mirageoscience/hooks/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
# All rights reserved. '
77
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
88

9-
__version__ = "1.0.0"
9+
__version__ = "1.0.1"

mirageoscience/hooks/git_message_hook.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class JiraPattern:
3131
making sure it gets compiled only once."""
3232

3333
__pattern = re.compile(
34-
r"(?:GEOPY|GI|GA|GMS|VPem1D|VPem3D|VPmg|UBCGIF|LICMGR)-\d+"
34+
r"(?:\w*!)?\s*\S?\b((?:GEOPY|GI|GA|GMS|VPem1D|VPem3D|VPmg|UBCGIF|LICMGR|DEVOPS|QA)-\d+)"
3535
)
3636

3737
@staticmethod
@@ -41,8 +41,29 @@ def get():
4141

4242
# use re.match() rather than re.search() to enforce the JIRA reference to be at the beginning
4343
match = re.match(JiraPattern.get(), text.strip())
44-
return match.group(0) if match else ""
44+
return match.group(1) if match else ""
4545

46+
def get_message_prefix_bang(line: str) -> str:
47+
"""Capture the standard commit message prefix, if any, such as 'fixup!', 'amend!',
48+
etc.
49+
50+
:return: the standard commit message prefix if found, else empty string.
51+
"""
52+
class BangPattern:
53+
"""Internal class that encapsulates the regular expression for the Bnag pattern,
54+
making sure it gets compiled only once."""
55+
56+
__pattern = re.compile(
57+
r"(\w*!\s)"
58+
)
59+
60+
@staticmethod
61+
def get():
62+
""":return: the compiled regular expression for the JIRA pattern"""
63+
return BangPattern.__pattern
64+
# use re.match() rather than re.search() to enforce pattern at the beginning
65+
match = re.match(BangPattern.get(), line.strip())
66+
return match.group(1) if match else ""
4667

4768
def get_branch_name() -> str | None:
4869
""":return: the name of the current branch"""
@@ -51,6 +72,7 @@ def get_branch_name() -> str | None:
5172
shlex.split("git branch --list"),
5273
stdout=subprocess.PIPE,
5374
text=True,
75+
check=False
5476
)
5577

5678
if git_proc.returncode != 0:
@@ -100,18 +122,20 @@ def check_commit_message(filepath: str) -> tuple[bool, str]:
100122

101123
message_jira_id = ""
102124
first_line = None
103-
with open(filepath) as message_file:
125+
with open(filepath, encoding="utf-8") as message_file:
104126
for line in message_file:
105127
if not line.startswith("#") and len(line.strip()) > 0:
106128
# test only the first non-comment line that is not empty
107129
# (should we reject messages with empty first line?)
108-
first_line = line
130+
first_line = line.strip()
131+
prefix_bang = get_message_prefix_bang(first_line)
132+
first_line = first_line[len(prefix_bang) :].strip()
109133
message_jira_id = get_jira_id(first_line)
110134
break
111135
assert first_line is not None
112136

113137
if not branch_jira_id and not (
114-
message_jira_id or first_line.strip().lower().startswith("merge")
138+
message_jira_id or (not prefix_bang and first_line.lower().startswith("merge"))
115139
):
116140
return (
117141
False,
@@ -121,7 +145,8 @@ def check_commit_message(filepath: str) -> tuple[bool, str]:
121145
if branch_jira_id and message_jira_id and branch_jira_id != message_jira_id:
122146
return (
123147
False,
124-
f"Different JIRA ID in commit message {message_jira_id} and in branch name {branch_jira_id}.",
148+
f"Different JIRA ID in commit message {message_jira_id} "
149+
f"and in branch name {branch_jira_id}.",
125150
)
126151

127152
stripped_message_line = ""
@@ -176,24 +201,31 @@ def prepare_commit_msg(filepath: str, source: str | None = None) -> None:
176201
if source not in [None, "message", "template"]:
177202
return
178203

204+
prefix_bang = ""
179205
with open(
180206
filepath,
181207
"r+",
208+
encoding="utf-8"
182209
) as message_file:
183210
message_has_jira_id = False
184211
message_lines = message_file.readlines()
185212
for line_index, line_content in enumerate(message_lines):
186213
if not line_content.startswith("#"):
187214
# test only the first non-comment line
215+
line_content = line_content.strip()
216+
prefix_bang = get_message_prefix_bang(line_content)
217+
line_content = line_content[len(prefix_bang):].strip()
188218
message_jira_id = get_jira_id(line_content)
189219
if not message_jira_id:
190-
message_lines[line_index] = branch_jira_id + ": " + line_content
220+
message_lines[line_index] = (
221+
f"{prefix_bang}[{branch_jira_id}] {line_content}\n"
222+
)
191223
message_has_jira_id = True
192224
break
193225

194226
if not message_has_jira_id:
195227
# message is empty or all lines are comments: insert JIRA ID at the very beginning
196-
message_lines.insert(0, branch_jira_id + ": ")
228+
message_lines.insert(0, f"{prefix_bang}[{branch_jira_id}]\n")
197229

198230
message_file.seek(0, 0)
199231
message_file.write("".join(message_lines))

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.poetry]
22
name = "mirageoscience.pre-commit-hooks"
33

4-
version = "1.0.0"
4+
version = "1.0.1"
55

66
license = "MIT"
77
description = ""
@@ -25,6 +25,7 @@ python = "^3.10"
2525
Pygments = "*"
2626
pylint = "*"
2727
pytest = "*"
28+
pytest-mock = "*"
2829
pytest-cov = "*"
2930
tomli = "*"
3031

tests/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
2+
# Copyright (c) 2024 Mira Geoscience Ltd. '
3+
# '
4+
# This file is part of mirageoscience.pre-commit-hooks package. '
5+
# '
6+
# All rights reserved. '
7+
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

tests/git_message_hook.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
2+
# Copyright (c) 2024 Mira Geoscience Ltd. '
3+
# '
4+
# This file is part of mirageoscience.pre-commit-hooks package. '
5+
# '
6+
# All rights reserved. '
7+
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
8+
9+
from __future__ import annotations
10+
import pytest
11+
12+
from mirageoscience.hooks.git_message_hook import *
13+
14+
15+
@pytest.fixture
16+
def mock_get_branch_name(mocker):
17+
def _mock_get_branch_name(branch_name):
18+
mocker.patch('mirageoscience.hooks.git_message_hook.get_branch_name',
19+
return_value=branch_name)
20+
return get_jira_id(branch_name)
21+
return _mock_get_branch_name
22+
23+
24+
def test_get_jira_id():
25+
text = "[GEOPY-1233] Git commit message"
26+
assert get_jira_id(text) == "GEOPY-1233"
27+
28+
29+
def test_get_message_prefix_bang_with_bang():
30+
"""Tests if get_message_prefix_bang can extract the prefix bang from a line."""
31+
line = "fixup! This is a fix"
32+
expected_prefix_bang = "fixup! "
33+
actual_prefix_bang = get_message_prefix_bang(line)
34+
assert actual_prefix_bang == expected_prefix_bang
35+
36+
37+
def test_get_message_prefix_bang_no_bang():
38+
"""Tests if get_message_prefix_bang returns empty string for a line without bang."""
39+
line = "This is a commit message"
40+
expected_prefix_bang = ""
41+
actual_prefix_bang = get_message_prefix_bang(line)
42+
assert actual_prefix_bang == expected_prefix_bang
43+
44+
45+
def test_check_commit_message_valid_with_message_jira(mock_get_branch_name):
46+
"""Test avec identifiant JIRA dans le message de commit"""
47+
branch_name = "feature_branch"
48+
mock_get_branch_name(branch_name)
49+
message_content = "GEOPY-123 Fix a bug xx"
50+
filepath = "test_commit_message.txt"
51+
with open(filepath, "w") as f:
52+
f.write(message_content)
53+
54+
is_valid, error_message = check_commit_message(filepath)
55+
assert is_valid
56+
assert error_message == ""
57+
58+
59+
def test_check_commit_message_invalid_no_jira(mock_get_branch_name):
60+
"""Test without JIRA id in the branch name or message content"""
61+
branch_name = "feature_branch"
62+
mock_get_branch_name(branch_name)
63+
message_content = "Fix a bug"
64+
filepath = "test_commit_message.txt"
65+
with open(filepath, "w") as f:
66+
f.write(message_content)
67+
68+
is_valid, error_message = check_commit_message(filepath)
69+
assert not is_valid
70+
assert error_message == "Either the branch name or the commit message must start with a JIRA ID."
71+
72+
73+
def test_check_commit_message_invalid_different_jira(mock_get_branch_name):
74+
"""Test with different JIRA id in the branch name and in the message content"""
75+
branch_name = "GEOPY-123_fix_bug"
76+
mock_get_branch_name(branch_name)
77+
message_content = "GI-456 Fix a bug"
78+
filepath = "test_commit_message.txt"
79+
with open(filepath, "w") as f:
80+
f.write(message_content)
81+
82+
is_valid, error_message = check_commit_message(filepath)
83+
assert not is_valid
84+
assert error_message.startswith("Different JIRA ID in commit message")
85+
86+
87+
def test_check_commit_message_invalid_short_message(mock_get_branch_name):
88+
"""Test with a too short message content"""
89+
branch_name = "GEOPY-123_fix_bug"
90+
mock_get_branch_name(branch_name)
91+
message_content = "Fix"
92+
filepath = "test_commit_message.txt"
93+
with open(filepath, "w") as f:
94+
f.write(message_content)
95+
96+
is_valid, error_message = check_commit_message(filepath)
97+
assert not is_valid
98+
assert error_message.startswith("First line of commit message must be at least")

tests/test_commit_message.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix

0 commit comments

Comments
 (0)