Skip to content

[Issue 63]: Conventional Commits #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,31 @@ git-ai-commit summarize

---

🏷️ `git-ai-commit conventional`

Generate commit messages in the [Conventional Commits](https://www.conventionalcommits.org/) format (`type(scope): description`).

This command:
1. Analyzes your staged changes using AI
2. Suggests the most appropriate commit type based on your changes
3. Suggests a relevant scope based on the affected components
4. Allows you to accept the suggestions or choose your own
5. Formats the message according to conventional commit standards
6. Gives you the option to commit and push

Available commit types:

- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `style`: Formatting changes
- `refactor`: Code refactoring
- `perf`: Performance improvements
- `test`: Adding or modifying tests
- `chore`: Maintenance tasks

---

📌 `git-ai-commit help`, `-h`

Displays a list of available command and options to help you setup our tool.
Expand Down
167 changes: 167 additions & 0 deletions ai_commit_msg/cli/conventional_commit_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from ai_commit_msg.core.gen_commit_msg import generate_commit_message
from ai_commit_msg.services.git_service import GitService
from ai_commit_msg.utils.logger import Logger
from ai_commit_msg.utils.utils import execute_cli_command
from ai_commit_msg.utils.error import AIModelHandlerError
from ai_commit_msg.utils.git_utils import handle_git_push


COMMIT_TYPES = {
"feat": "New feature",
"fix": "Bug fix",
"docs": "Documentation changes",
"style": "Formatting changes",
"refactor": "Code refactoring",
"perf": "Performance improvements",
"test": "Adding or modifying tests",
"chore": "Maintenance tasks",
}


def print_conventional_commit(commit_type, scope, message):
formatted_commit = f"{commit_type}"
if scope:
formatted_commit += f"({scope})"
formatted_commit += f": {message}"

Logger().log(
f"""Here is your conventional commit message:

{formatted_commit}

to use this commit message run: `git commit -m "{formatted_commit}"`
"""
)
return formatted_commit


def select_commit_type(suggested_type=None):
logger = Logger()

if suggested_type and suggested_type in COMMIT_TYPES:
logger.log(
f"AI suggests commit type: {suggested_type} ({COMMIT_TYPES[suggested_type]})"
)
use_suggested = (
input(f"Use suggested type '{suggested_type}'? (Y/n): ").strip().lower()
)
if use_suggested == "" or use_suggested == "y":
return suggested_type

logger.log("Select a commit type:")

# Display commit types with descriptions
for i, (type_key, description) in enumerate(COMMIT_TYPES.items(), 1):
# Highlight the suggested type if it exists
highlight = "→ " if suggested_type == type_key else " "
logger.log(f"{highlight}{i}. {type_key}: {description}")

# Add custom option
logger.log(f" {len(COMMIT_TYPES) + 1}. custom: Enter a custom type")

while True:
try:
choice = input("Enter the number of your choice: ")
choice_num = int(choice)

if 1 <= choice_num <= len(COMMIT_TYPES):
return list(COMMIT_TYPES.keys())[choice_num - 1]
elif choice_num == len(COMMIT_TYPES) + 1:
custom_type = input("Enter your custom commit type: ")
return custom_type
else:
logger.log("Invalid choice. Please try again.")
except ValueError:
logger.log("Please enter a valid number.")


def get_scope(suggested_scope=None):
logger = Logger()

if suggested_scope and suggested_scope.strip() and suggested_scope != "none":
logger.log(f"AI suggests scope: '{suggested_scope}'")
use_suggested = (
input(f"Use suggested scope '{suggested_scope}'? (Y/n): ").strip().lower()
)
if use_suggested == "" or use_suggested == "y":
return suggested_scope

scope = input("Enter scope (optional, press Enter to skip): ")
return scope.strip()


def conventional_commit_handler(args):
logger = Logger()

logger.log("Fetching your staged changes...\n")

if len(GitService.get_staged_files()) == 0:
logger.log(
"🚨 No files are staged for commit. Run `git add` to stage some of your changes"
)
return

staged_changes_diff = execute_cli_command(["git", "diff", "--staged"])
diff = staged_changes_diff.stdout

try:
logger.log("🤖 AI is analyzing your changes to suggest a commit type...\n")
suggested_type = generate_commit_message(diff, classify_type=True)

if suggested_type not in COMMIT_TYPES:
logger.log(
f"AI suggested an invalid type: '{suggested_type}'. Falling back to manual selection."
)
suggested_type = None
except AIModelHandlerError as e:
logger.log(f"Error classifying commit type: {e}")
suggested_type = None

suggested_scope = None
try:
logger.log("🤖 AI is analyzing your changes to suggest a scope...\n")
suggested_scope = generate_commit_message(
diff, conventional=False, classify_type=False, classify_scope=True
)
logger.log(f"Debug - AI suggested scope: '{suggested_scope}'")

if suggested_scope == "none" or not suggested_scope:
suggested_scope = None
except AIModelHandlerError as e:
logger.log(f"Error suggesting scope: {e}")
suggested_scope = None

try:
ai_commit_msg = generate_commit_message(diff, conventional=True)
except AIModelHandlerError as e:
logger.log(f"Error generating commit message: {e}")
logger.log("Please enter your commit message manually:")
ai_commit_msg = input().strip()
if not ai_commit_msg:
logger.log("No commit message provided. Exiting.")
return

commit_type = select_commit_type(suggested_type)

scope = get_scope(suggested_scope)

formatted_commit = print_conventional_commit(commit_type, scope, ai_commit_msg)

command_string = f"""
git commit -m "{formatted_commit}"
git push

Would you like to commit your changes? (y/n): """

should_push_changes = input(command_string)

if should_push_changes == "n":
logger.log("👋 Goodbye!")
return
elif should_push_changes != "y":
logger.log("🚨 Invalid input. Exiting.")
return

execute_cli_command(["git", "commit", "-m", f'"{formatted_commit}"'], output=True)

handle_git_push()
18 changes: 2 additions & 16 deletions ai_commit_msg/cli/gen_ai_commit_message_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from ai_commit_msg.utils.utils import execute_cli_command
from ai_commit_msg.utils.error import AIModelHandlerError
from ai_commit_msg.utils.logger import Logger
from ai_commit_msg.utils.git_utils import handle_git_push


def gen_ai_commit_message_handler():
Expand Down Expand Up @@ -45,22 +46,7 @@ def gen_ai_commit_message_handler():
return

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

if has_upstream:
execute_cli_command(["git", "push"], output=True)
return

set_upstream = input(
f"No upstream branch found for '{current_branch}'. This will run: 'git push --set-upstream origin {current_branch}'. Set upstream? (y/n): "
)
if set_upstream.lower() == "y":
execute_cli_command(
["git", "push", "--set-upstream", "origin", current_branch], output=True
)
print(f"🔄 Upstream branch set for '{current_branch}'")
else:
print("Skipping push. You can set upstream manually")
handle_git_push()

return 0
2 changes: 1 addition & 1 deletion ai_commit_msg/cli/summary_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def summaryFromDiffFile(diff_file_path):

def summary_handler(args):

if hasattr(args, 'diff') and args.diff is not None:
if hasattr(args, "diff") and args.diff is not None:
summaryFromDiffFile(args.diff)
return

Expand Down
21 changes: 17 additions & 4 deletions ai_commit_msg/core/gen_commit_msg.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,26 @@
from ai_commit_msg.services.config_service import ConfigService


def generate_commit_message(diff: str = None) -> str:
def generate_commit_message(
diff: str = None,
conventional: bool = False,
classify_type: bool = False,
classify_scope: bool = False,
) -> str:

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

prompt = get_prompt(diff)
prompt = get_prompt(
diff,
conventional=conventional,
classify_type=classify_type,
classify_scope=classify_scope,
)
ai_gen_commit_msg = llm_chat_completion(prompt)

prefix = ConfigService().prefix
return prefix + ai_gen_commit_msg
if not classify_type and not classify_scope:
prefix = ConfigService().prefix
return prefix + ai_gen_commit_msg
else:
return ai_gen_commit_msg.strip().lower()
59 changes: 57 additions & 2 deletions ai_commit_msg/core/prompt.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,65 @@
from ai_commit_msg.services.config_service import ConfigService


def get_prompt(diff):
def get_prompt(diff, conventional=False, classify_type=False, classify_scope=False):
max_length = ConfigService().max_length

COMMIT_MSG_SYSTEM_MESSAGE = f"""
if classify_type:
COMMIT_MSG_SYSTEM_MESSAGE = f"""
You are a software engineer reviewing code changes to classify them according to conventional commit standards.
You will be provided with a set of code changes in diff format.

Your task is to analyze the changes and determine the most appropriate conventional commit type.
Choose ONE type from the following options:
- feat: New feature
- fix: Bug fix
- docs: Documentation changes
- style: Formatting changes
- refactor: Code refactoring
- perf: Performance improvements
- test: Adding or modifying tests
- chore: Maintenance tasks

Respond with ONLY the type (e.g., "feat", "fix", etc.) without any additional text or explanation.
"""
elif classify_scope:
COMMIT_MSG_SYSTEM_MESSAGE = f"""
You are a software engineer reviewing code changes to suggest an appropriate scope for a conventional commit.
You will be provided with a set of code changes in diff format.

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.
Good scopes are typically:
- Short (1-3 words)
- Descriptive of the component or feature being changed
- Lowercase with no spaces (use hyphens if needed)

Examples of good scopes:
- "auth" for authentication changes
- "user-profile" for user profile features
- "api" for API-related changes
- "docs" for documentation
- "deps" for dependency updates
- "ui" for user interface changes

If you can't determine a meaningful scope, respond with "none".
Respond with ONLY the suggested scope without any additional text or explanation.
"""
elif conventional:
COMMIT_MSG_SYSTEM_MESSAGE = f"""
You are a software engineer reviewing code changes.
You will be provided with a set of code changes in diff format.

Your task is to write a concise commit message body that summarizes the changes. This will be used in a conventional commit format.

These are your requirements for the commit message body:
- Write in the imperative mood (e.g., "add feature" not "added feature")
- Focus only on the description part - do NOT include type prefixes like "feat:" or "fix:" as these will be added separately
- Be specific but concise about what was changed
- You don't need to add any punctuation or capitalization
- Your response cannot be more than {max_length} characters
"""
else:
COMMIT_MSG_SYSTEM_MESSAGE = f"""
Your a software engineer and you are reviewing a set of code changes.
You will be provided with a set of code changes in diff format.

Expand Down
12 changes: 10 additions & 2 deletions ai_commit_msg/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ai_commit_msg.services.config_service import ConfigService
from ai_commit_msg.services.pip_service import PipService
from ai_commit_msg.utils.logger import Logger
from ai_commit_msg.cli.conventional_commit_handler import conventional_commit_handler


def called_from_git_hook():
Expand Down Expand Up @@ -135,9 +136,14 @@ def main(argv: Sequence[str] = sys.argv[1:]) -> int:
help="Setup the prepare-commit-msg hook",
)
summary_cmd_parser.add_argument(
"-d", "--diff",
"-d",
"--diff",
default=None,
help="🔍 Provide a diff to generate a commit message"
help="🔍 Provide a diff to generate a commit message",
)

conventional_commit_parser = subparsers.add_parser(
"conventional", help="🏷️ Generate a conventional commit message"
)

args = parser.parse_args(argv)
Expand All @@ -160,6 +166,8 @@ def get_full_help_menu():
hook_handler(args)
elif args.command == "summarize" or args.command == "summary":
summary_handler(args)
elif args.command == "conventional":
conventional_commit_handler(args)

## Only in main script, we return zero instead of None when the return value is unused
return 0
Expand Down
Loading