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 5 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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ default_install_hook_types:
- prepare-commit-msg
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: '^.*\.(bin|exe|jpg|png|gif|md)$'
Expand Down
132 changes: 132 additions & 0 deletions ai_commit_msg/cli/conventional_commit_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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():
Logger().log("Select a commit type:")

# Display commit types with descriptions
for i, (type_key, description) in enumerate(COMMIT_TYPES.items(), 1):
Logger().log(f"{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():
scope = input("Enter scope (optional, press Enter to skip): ")
return scope.strip()


def conventional_commit_handler(args):
logger = Logger()

# Get the diff
if hasattr(args, "diff") and args.diff is not None:
with open(args.diff, "r") as file:
diff = file.read()
elif hasattr(args, "unstaged") and args.unstaged:
logger.log("Fetching your unstaged changes...\n")
unstaged_changes_diff = execute_cli_command(["git", "diff"])
diff = unstaged_changes_diff.stdout
else:
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

# Generate the commit message body
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

# Get commit type and scope
commit_type = select_commit_type()
scope = get_scope()

# Format the conventional commit
formatted_commit = print_conventional_commit(commit_type, scope, ai_commit_msg)

# Ask if user wants to commit and push
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

# Commit the changes
execute_cli_command(["git", "commit", "-m", formatted_commit], output=True)

# Handle git push with the shared utility function
handle_git_push()

return 0
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
4 changes: 2 additions & 2 deletions ai_commit_msg/core/gen_commit_msg.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
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) -> 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)
ai_gen_commit_msg = llm_chat_completion(prompt)

prefix = ConfigService().prefix
Expand Down
19 changes: 17 additions & 2 deletions ai_commit_msg/core/prompt.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
from ai_commit_msg.services.config_service import ConfigService


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

COMMIT_MSG_SYSTEM_MESSAGE = f"""
if 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
24 changes: 22 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,26 @@ 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"
)
conventional_commit_parser.add_argument(
"-u",
"--unstaged",
action="store_true",
help="Use unstaged changes instead of staged changes",
)
conventional_commit_parser.add_argument(
"-d",
"--diff",
default=None,
help="🔍 Provide a diff file to generate a commit message",
)

args = parser.parse_args(argv)
Expand All @@ -160,6 +178,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
30 changes: 30 additions & 0 deletions ai_commit_msg/utils/git_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from ai_commit_msg.services.git_service import GitService
from ai_commit_msg.utils.utils import execute_cli_command
from ai_commit_msg.utils.logger import Logger


def handle_git_push():
"""
Handle git push operation with upstream branch setting if needed.
Returns True if push was successful, False otherwise.
"""
logger = Logger()
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 True

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
)
logger.log(f"🔄 Upstream branch set for '{current_branch}'")
return True
else:
logger.log("Skipping push. You can set upstream manually")
return False
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ prompt_toolkit==3.0.47
requests==2.32.3
rich==13.7.1
pre-commit==3.8.0
black=24.8.0
black==24.8.0