Skip to content

Commit ff68028

Browse files
committed
initial commit
CommitFormat is able to check for spelling mistakes and line length.
1 parent 15fbd24 commit ff68028

File tree

4 files changed

+217
-0
lines changed

4 files changed

+217
-0
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,28 @@
11
# commit-format
22
A tool to format git commit messages.
3+
4+
## Supported checkers
5+
6+
- Check that each message lines does not exceed a length limit. The option `-l <int>` allow custom length (default 80 chars).
7+
- Check that codespell does not find any spelling mistake on commit messages.
8+
9+
## Installation
10+
11+
```sh
12+
$ pip install git+https://github.com/AlexFabre/commit-format
13+
```
14+
15+
## Options
16+
17+
```sh
18+
$ commit-format --help
19+
usage: commit_format.py [-h] [-l LINESLIMIT] [-v]
20+
21+
Various checks on commit messages.
22+
23+
options:
24+
-h, --help show this help message and exit
25+
-l, --lineslimit LINESLIMIT
26+
commit message lines max length. (Default 80)
27+
-v, --verbosity increase output verbosity
28+
```

commit_format/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__author__ = 'Alex Fabre'

commit_format/commit_format.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import argparse
2+
import subprocess
3+
import re
4+
5+
# ANSI escape codes for colors
6+
RED = '\033[91m'
7+
YELLOW = '\033[93m'
8+
GREEN = '\033[92m'
9+
BLUE = '\033[94m'
10+
RESET = '\033[0m'
11+
12+
class CommitFormat:
13+
def __init__(self, verbosity=False):
14+
self.verbosity = verbosity
15+
16+
def error(self, text: str):
17+
"""Prints the given text in red."""
18+
print(f"{RED}{text}{RESET}")
19+
20+
def warning(self, text: str):
21+
"""Prints the given text in yellow."""
22+
print(f"{YELLOW}{text}{RESET}")
23+
24+
def highlight_words_in_txt(self, text: str, words="", highlight_color=f"{RED}") -> str:
25+
"""Prints the given text and highlights the words in the list."""
26+
for word in words:
27+
word = self.remove_ansi_color_codes(word)
28+
text = text[::-1].replace(f" {word}"[::-1], f"{highlight_color} {word}{RESET}"[::-1], 1)[::-1]
29+
30+
return text
31+
32+
def remove_ansi_color_codes(self, text: str) -> str:
33+
ansi_escape_pattern = re.compile(r'\x1B[@-_][0-?]*[ -/]*[@-~]')
34+
return ansi_escape_pattern.sub('', text)
35+
36+
def info(self, text: str):
37+
"""Prints the given text in blue."""
38+
print(text)
39+
40+
def debug(self, text: str):
41+
"""Prints the given text in green."""
42+
if self.verbosity:
43+
print(text)
44+
45+
def get_current_branch(self) -> str:
46+
result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], capture_output=True, text=True)
47+
return result.stdout.strip()
48+
49+
def list_unique_commits(self, current_branch, base_branch='main') -> list:
50+
if current_branch != base_branch:
51+
result = subprocess.run(['git', 'log', '--pretty=format:%h', f'{base_branch}..{current_branch}'], capture_output=True, text=True)
52+
return result.stdout.split()
53+
else:
54+
self.error(f"Running on branch {base_branch}. Abort checking commits.")
55+
exit(0)
56+
57+
def get_commit_message(self, commit_sha: str) -> str:
58+
result = subprocess.run(['git', 'show', '-s', '--format=%B', commit_sha], capture_output=True, text=True)
59+
return result.stdout.strip()
60+
61+
def run_codespell(self, message: str) -> tuple:
62+
result = subprocess.run(['codespell', '-c', '-', '-'], input=message, capture_output=True, text=True)
63+
lines = result.stdout.strip().split('\n')
64+
selected_lines = [line for index, line in enumerate(lines) if index % 2 != 0]
65+
faulty_words = [line.split()[0] for line in selected_lines if line]
66+
return '\n'.join(selected_lines), faulty_words
67+
68+
def spell_check(self, commit: str, commit_message: str) -> bool:
69+
spell_error = 0
70+
71+
# Run codespell
72+
codespell_proposition, faulty_words = self.run_codespell(commit_message)
73+
if codespell_proposition:
74+
spell_error += 1
75+
self.warning(f"Commit {commit} has spelling mistakes")
76+
self.info(self.highlight_words_in_txt(f"---\n{commit_message}", faulty_words))
77+
self.info(f"---\nCodespell fix proposition:\n{codespell_proposition}\n---")
78+
79+
# Run another spelling tool:
80+
# ...
81+
82+
return spell_error
83+
84+
def lines_length(self, commit: str, commit_message: str, length_limit=80) -> bool:
85+
length_exceeded = 0
86+
line_number = 0
87+
highlighted_commit_message = ""
88+
89+
# Split the commit message into lines
90+
lines = commit_message.split('\n')
91+
92+
# Check if any line exceeds the length limit
93+
for line in lines:
94+
line_number += 1
95+
removed_words = []
96+
97+
if (line_number > 1):
98+
highlighted_commit_message += "\n"
99+
100+
line_length = len(line)
101+
if line_length > length_limit:
102+
length_exceeded += 1
103+
104+
line_copy = line
105+
# Split the line into words
106+
while len(line_copy) > length_limit:
107+
# Find the last space in the line
108+
last_space_index = line_copy.rfind(' ')
109+
110+
# If there's no space, break out of the loop
111+
if last_space_index == -1:
112+
break
113+
114+
removed_word = line_copy[(last_space_index+1):]
115+
removed_words.append(removed_word)
116+
117+
# Remove the last word by slicing up to the last space
118+
line_copy = line_copy[:last_space_index]
119+
120+
highlighted_commit_message += f"{self.highlight_words_in_txt(line, removed_words)}"
121+
122+
if (length_exceeded):
123+
self.warning(f"Commit {commit}: exceeds {length_limit} chars limit")
124+
self.info(f"---\n{highlighted_commit_message}\n---")
125+
126+
return length_exceeded
127+
128+
129+
130+
def main():
131+
parser = argparse.ArgumentParser(description="Various checks on commit messages.")
132+
parser.add_argument('-l', '--lineslimit', type=int, default=80, help="commit message lines max length. (Default 80)")
133+
parser.add_argument('-v', '--verbosity', action='store_true', help="increase output verbosity")
134+
args = parser.parse_args()
135+
136+
commit_format = CommitFormat(verbosity=args.verbosity)
137+
138+
error_found = 0
139+
current_branch = commit_format.get_current_branch()
140+
if not current_branch:
141+
commit_format.error("Not inside an active git repository")
142+
exit(1)
143+
144+
commit_list = commit_format.list_unique_commits(current_branch)
145+
if not commit_list:
146+
commit_format.debug(f"No unique commits on branch {GREEN}{current_branch}{RESET}")
147+
exit(0)
148+
149+
commit_format.debug(f"Checking {GREEN}{len(commit_list)}{RESET} commits on branch {GREEN}{current_branch}{RESET}")
150+
151+
for commit in commit_list:
152+
error_on_commit = 0
153+
commit_message = commit_format.get_commit_message(commit)
154+
error_on_commit += commit_format.spell_check(commit, commit_message)
155+
error_on_commit += commit_format.lines_length(commit, commit_message, args.lineslimit)
156+
157+
if not error_on_commit:
158+
commit_format.info(f"{GREEN}Commit {commit}{RESET}")
159+
else:
160+
error_found += error_on_commit
161+
162+
exit(error_found)
163+
164+
if __name__ == '__main__':
165+
main()

pyproject.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[project]
2+
name = "commit_format"
3+
version = "0.0.1"
4+
authors = [
5+
{ name="Alex Fabre", email="[email protected]" },
6+
]
7+
description = "A tool to format git commit messages"
8+
readme = "README.md"
9+
requires-python = ">=3.8"
10+
classifiers = [
11+
"Programming Language :: Python :: 3",
12+
"Operating System :: OS Independent",
13+
]
14+
license = {file = "LICENSE"}
15+
16+
dependencies = [
17+
"codespell"
18+
]
19+
20+
[project.urls]
21+
Homepage = "https://github.com/AlexFabre/commit-format"
22+
Issues = "https://github.com/AlexFabre/commit-format/issues"
23+
24+
[project.scripts]
25+
commit-format = "commit_format.commit_format:main"

0 commit comments

Comments
 (0)