Skip to content

Commit fca1ff6

Browse files
authored
[Issue 63]: Conventional Commits (#69)
* update pre-commit version, add conventional commit option * update black version to 24.8.0 * feat: add conventional commit message generation feature * feat: refactor commit process to include error handling and push options * refactor: refactor git push handling into a utility function * remove comments from conventional_commit_handler.py * feat: add command to generate conventional commit messages * feat: add ai suggestions for commit type selection and handling * docs(docs for conventional command): update command description in README.md * update pre-commit hooks version, suggest commit scope * docs: remove example command from README documentation * refactor: add blank line before available commit types * refactor(cli): refactor ai commit type and scope suggestion logic * refactor(cli): refactor git push handling into separate function * remove commented code lines from conventional_commit_handler
1 parent fe0b9b8 commit fca1ff6

File tree

9 files changed

+310
-26
lines changed

9 files changed

+310
-26
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,31 @@ git-ai-commit summarize
179179

180180
---
181181

182+
🏷️ `git-ai-commit conventional`
183+
184+
Generate commit messages in the [Conventional Commits](https://www.conventionalcommits.org/) format (`type(scope): description`).
185+
186+
This command:
187+
1. Analyzes your staged changes using AI
188+
2. Suggests the most appropriate commit type based on your changes
189+
3. Suggests a relevant scope based on the affected components
190+
4. Allows you to accept the suggestions or choose your own
191+
5. Formats the message according to conventional commit standards
192+
6. Gives you the option to commit and push
193+
194+
Available commit types:
195+
196+
- `feat`: New feature
197+
- `fix`: Bug fix
198+
- `docs`: Documentation changes
199+
- `style`: Formatting changes
200+
- `refactor`: Code refactoring
201+
- `perf`: Performance improvements
202+
- `test`: Adding or modifying tests
203+
- `chore`: Maintenance tasks
204+
205+
---
206+
182207
📌 `git-ai-commit help`, `-h`
183208

184209
Displays a list of available command and options to help you setup our tool.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
from ai_commit_msg.core.gen_commit_msg import generate_commit_message
2+
from ai_commit_msg.services.git_service import GitService
3+
from ai_commit_msg.utils.logger import Logger
4+
from ai_commit_msg.utils.utils import execute_cli_command
5+
from ai_commit_msg.utils.error import AIModelHandlerError
6+
from ai_commit_msg.utils.git_utils import handle_git_push
7+
8+
9+
COMMIT_TYPES = {
10+
"feat": "New feature",
11+
"fix": "Bug fix",
12+
"docs": "Documentation changes",
13+
"style": "Formatting changes",
14+
"refactor": "Code refactoring",
15+
"perf": "Performance improvements",
16+
"test": "Adding or modifying tests",
17+
"chore": "Maintenance tasks",
18+
}
19+
20+
21+
def print_conventional_commit(commit_type, scope, message):
22+
formatted_commit = f"{commit_type}"
23+
if scope:
24+
formatted_commit += f"({scope})"
25+
formatted_commit += f": {message}"
26+
27+
Logger().log(
28+
f"""Here is your conventional commit message:
29+
30+
{formatted_commit}
31+
32+
to use this commit message run: `git commit -m "{formatted_commit}"`
33+
"""
34+
)
35+
return formatted_commit
36+
37+
38+
def select_commit_type(suggested_type=None):
39+
logger = Logger()
40+
41+
if suggested_type and suggested_type in COMMIT_TYPES:
42+
logger.log(
43+
f"AI suggests commit type: {suggested_type} ({COMMIT_TYPES[suggested_type]})"
44+
)
45+
use_suggested = (
46+
input(f"Use suggested type '{suggested_type}'? (Y/n): ").strip().lower()
47+
)
48+
if use_suggested == "" or use_suggested == "y":
49+
return suggested_type
50+
51+
logger.log("Select a commit type:")
52+
53+
# Display commit types with descriptions
54+
for i, (type_key, description) in enumerate(COMMIT_TYPES.items(), 1):
55+
# Highlight the suggested type if it exists
56+
highlight = "→ " if suggested_type == type_key else " "
57+
logger.log(f"{highlight}{i}. {type_key}: {description}")
58+
59+
# Add custom option
60+
logger.log(f" {len(COMMIT_TYPES) + 1}. custom: Enter a custom type")
61+
62+
while True:
63+
try:
64+
choice = input("Enter the number of your choice: ")
65+
choice_num = int(choice)
66+
67+
if 1 <= choice_num <= len(COMMIT_TYPES):
68+
return list(COMMIT_TYPES.keys())[choice_num - 1]
69+
elif choice_num == len(COMMIT_TYPES) + 1:
70+
custom_type = input("Enter your custom commit type: ")
71+
return custom_type
72+
else:
73+
logger.log("Invalid choice. Please try again.")
74+
except ValueError:
75+
logger.log("Please enter a valid number.")
76+
77+
78+
def get_scope(suggested_scope=None):
79+
logger = Logger()
80+
81+
if suggested_scope and suggested_scope.strip() and suggested_scope != "none":
82+
logger.log(f"AI suggests scope: '{suggested_scope}'")
83+
use_suggested = (
84+
input(f"Use suggested scope '{suggested_scope}'? (Y/n): ").strip().lower()
85+
)
86+
if use_suggested == "" or use_suggested == "y":
87+
return suggested_scope
88+
89+
scope = input("Enter scope (optional, press Enter to skip): ")
90+
return scope.strip()
91+
92+
93+
def conventional_commit_handler(args):
94+
logger = Logger()
95+
96+
logger.log("Fetching your staged changes...\n")
97+
98+
if len(GitService.get_staged_files()) == 0:
99+
logger.log(
100+
"🚨 No files are staged for commit. Run `git add` to stage some of your changes"
101+
)
102+
return
103+
104+
staged_changes_diff = execute_cli_command(["git", "diff", "--staged"])
105+
diff = staged_changes_diff.stdout
106+
107+
try:
108+
logger.log("🤖 AI is analyzing your changes to suggest a commit type...\n")
109+
suggested_type = generate_commit_message(diff, classify_type=True)
110+
111+
if suggested_type not in COMMIT_TYPES:
112+
logger.log(
113+
f"AI suggested an invalid type: '{suggested_type}'. Falling back to manual selection."
114+
)
115+
suggested_type = None
116+
except AIModelHandlerError as e:
117+
logger.log(f"Error classifying commit type: {e}")
118+
suggested_type = None
119+
120+
suggested_scope = None
121+
try:
122+
logger.log("🤖 AI is analyzing your changes to suggest a scope...\n")
123+
suggested_scope = generate_commit_message(
124+
diff, conventional=False, classify_type=False, classify_scope=True
125+
)
126+
logger.log(f"Debug - AI suggested scope: '{suggested_scope}'")
127+
128+
if suggested_scope == "none" or not suggested_scope:
129+
suggested_scope = None
130+
except AIModelHandlerError as e:
131+
logger.log(f"Error suggesting scope: {e}")
132+
suggested_scope = None
133+
134+
try:
135+
ai_commit_msg = generate_commit_message(diff, conventional=True)
136+
except AIModelHandlerError as e:
137+
logger.log(f"Error generating commit message: {e}")
138+
logger.log("Please enter your commit message manually:")
139+
ai_commit_msg = input().strip()
140+
if not ai_commit_msg:
141+
logger.log("No commit message provided. Exiting.")
142+
return
143+
144+
commit_type = select_commit_type(suggested_type)
145+
146+
scope = get_scope(suggested_scope)
147+
148+
formatted_commit = print_conventional_commit(commit_type, scope, ai_commit_msg)
149+
150+
command_string = f"""
151+
git commit -m "{formatted_commit}"
152+
git push
153+
154+
Would you like to commit your changes? (y/n): """
155+
156+
should_push_changes = input(command_string)
157+
158+
if should_push_changes == "n":
159+
logger.log("👋 Goodbye!")
160+
return
161+
elif should_push_changes != "y":
162+
logger.log("🚨 Invalid input. Exiting.")
163+
return
164+
165+
execute_cli_command(["git", "commit", "-m", f'"{formatted_commit}"'], output=True)
166+
167+
handle_git_push()

ai_commit_msg/cli/gen_ai_commit_message_handler.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from ai_commit_msg.utils.utils import execute_cli_command
55
from ai_commit_msg.utils.error import AIModelHandlerError
66
from ai_commit_msg.utils.logger import Logger
7+
from ai_commit_msg.utils.git_utils import handle_git_push
78

89

910
def gen_ai_commit_message_handler():
@@ -45,22 +46,7 @@ def gen_ai_commit_message_handler():
4546
return
4647

4748
execute_cli_command(["git", "commit", "-m", ai_gen_commit_msg], output=True)
48-
current_branch = GitService.get_current_branch()
49-
has_upstream = GitService.has_upstream_branch(current_branch)
5049

51-
if has_upstream:
52-
execute_cli_command(["git", "push"], output=True)
53-
return
54-
55-
set_upstream = input(
56-
f"No upstream branch found for '{current_branch}'. This will run: 'git push --set-upstream origin {current_branch}'. Set upstream? (y/n): "
57-
)
58-
if set_upstream.lower() == "y":
59-
execute_cli_command(
60-
["git", "push", "--set-upstream", "origin", current_branch], output=True
61-
)
62-
print(f"🔄 Upstream branch set for '{current_branch}'")
63-
else:
64-
print("Skipping push. You can set upstream manually")
50+
handle_git_push()
6551

6652
return 0

ai_commit_msg/cli/summary_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def summaryFromDiffFile(diff_file_path):
2424

2525
def summary_handler(args):
2626

27-
if hasattr(args, 'diff') and args.diff is not None:
27+
if hasattr(args, "diff") and args.diff is not None:
2828
summaryFromDiffFile(args.diff)
2929
return
3030

ai_commit_msg/core/gen_commit_msg.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,26 @@
33
from ai_commit_msg.services.config_service import ConfigService
44

55

6-
def generate_commit_message(diff: str = None) -> str:
6+
def generate_commit_message(
7+
diff: str = None,
8+
conventional: bool = False,
9+
classify_type: bool = False,
10+
classify_scope: bool = False,
11+
) -> str:
712

813
if diff is None:
914
raise ValueError("Diff is required to generate a commit message")
1015

11-
prompt = get_prompt(diff)
16+
prompt = get_prompt(
17+
diff,
18+
conventional=conventional,
19+
classify_type=classify_type,
20+
classify_scope=classify_scope,
21+
)
1222
ai_gen_commit_msg = llm_chat_completion(prompt)
1323

14-
prefix = ConfigService().prefix
15-
return prefix + ai_gen_commit_msg
24+
if not classify_type and not classify_scope:
25+
prefix = ConfigService().prefix
26+
return prefix + ai_gen_commit_msg
27+
else:
28+
return ai_gen_commit_msg.strip().lower()

ai_commit_msg/core/prompt.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,65 @@
11
from ai_commit_msg.services.config_service import ConfigService
22

33

4-
def get_prompt(diff):
4+
def get_prompt(diff, conventional=False, classify_type=False, classify_scope=False):
55
max_length = ConfigService().max_length
66

7-
COMMIT_MSG_SYSTEM_MESSAGE = f"""
7+
if classify_type:
8+
COMMIT_MSG_SYSTEM_MESSAGE = f"""
9+
You are a software engineer reviewing code changes to classify them according to conventional commit standards.
10+
You will be provided with a set of code changes in diff format.
11+
12+
Your task is to analyze the changes and determine the most appropriate conventional commit type.
13+
Choose ONE type from the following options:
14+
- feat: New feature
15+
- fix: Bug fix
16+
- docs: Documentation changes
17+
- style: Formatting changes
18+
- refactor: Code refactoring
19+
- perf: Performance improvements
20+
- test: Adding or modifying tests
21+
- chore: Maintenance tasks
22+
23+
Respond with ONLY the type (e.g., "feat", "fix", etc.) without any additional text or explanation.
24+
"""
25+
elif classify_scope:
26+
COMMIT_MSG_SYSTEM_MESSAGE = f"""
27+
You are a software engineer reviewing code changes to suggest an appropriate scope for a conventional commit.
28+
You will be provided with a set of code changes in diff format.
29+
30+
Your task is to analyze the changes and suggest a concise, meaningful scope that indicates what part of the codebase or functionality is being modified.
31+
Good scopes are typically:
32+
- Short (1-3 words)
33+
- Descriptive of the component or feature being changed
34+
- Lowercase with no spaces (use hyphens if needed)
35+
36+
Examples of good scopes:
37+
- "auth" for authentication changes
38+
- "user-profile" for user profile features
39+
- "api" for API-related changes
40+
- "docs" for documentation
41+
- "deps" for dependency updates
42+
- "ui" for user interface changes
43+
44+
If you can't determine a meaningful scope, respond with "none".
45+
Respond with ONLY the suggested scope without any additional text or explanation.
46+
"""
47+
elif conventional:
48+
COMMIT_MSG_SYSTEM_MESSAGE = f"""
49+
You are a software engineer reviewing code changes.
50+
You will be provided with a set of code changes in diff format.
51+
52+
Your task is to write a concise commit message body that summarizes the changes. This will be used in a conventional commit format.
53+
54+
These are your requirements for the commit message body:
55+
- Write in the imperative mood (e.g., "add feature" not "added feature")
56+
- Focus only on the description part - do NOT include type prefixes like "feat:" or "fix:" as these will be added separately
57+
- Be specific but concise about what was changed
58+
- You don't need to add any punctuation or capitalization
59+
- Your response cannot be more than {max_length} characters
60+
"""
61+
else:
62+
COMMIT_MSG_SYSTEM_MESSAGE = f"""
863
Your a software engineer and you are reviewing a set of code changes.
964
You will be provided with a set of code changes in diff format.
1065

ai_commit_msg/main.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ai_commit_msg.services.config_service import ConfigService
1616
from ai_commit_msg.services.pip_service import PipService
1717
from ai_commit_msg.utils.logger import Logger
18+
from ai_commit_msg.cli.conventional_commit_handler import conventional_commit_handler
1819

1920

2021
def called_from_git_hook():
@@ -135,9 +136,14 @@ def main(argv: Sequence[str] = sys.argv[1:]) -> int:
135136
help="Setup the prepare-commit-msg hook",
136137
)
137138
summary_cmd_parser.add_argument(
138-
"-d", "--diff",
139+
"-d",
140+
"--diff",
139141
default=None,
140-
help="🔍 Provide a diff to generate a commit message"
142+
help="🔍 Provide a diff to generate a commit message",
143+
)
144+
145+
conventional_commit_parser = subparsers.add_parser(
146+
"conventional", help="🏷️ Generate a conventional commit message"
141147
)
142148

143149
args = parser.parse_args(argv)
@@ -160,6 +166,8 @@ def get_full_help_menu():
160166
hook_handler(args)
161167
elif args.command == "summarize" or args.command == "summary":
162168
summary_handler(args)
169+
elif args.command == "conventional":
170+
conventional_commit_handler(args)
163171

164172
## Only in main script, we return zero instead of None when the return value is unused
165173
return 0

0 commit comments

Comments
 (0)