From 96606248a07cdab14e42c23440be761a3150d6ec Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Tue, 4 Mar 2025 20:13:52 -0600 Subject: [PATCH 01/15] update pre-commit version, add conventional commit option --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index baba9d0..0784577 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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)$' From 942b690b70b0b21ad3dc7d242b1c13a3d1175e48 Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Tue, 4 Mar 2025 20:14:44 -0600 Subject: [PATCH 02/15] update black version to 24.8.0 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5f97595..27e6b41 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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 \ No newline at end of file +black==24.8.0 \ No newline at end of file From f8d0f2a10bc88deb7c5fd27a78f89bb37db1f0c1 Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Tue, 4 Mar 2025 21:06:35 -0600 Subject: [PATCH 03/15] feat: add conventional commit message generation feature --- .../cli/conventional_commit_handler.py | 105 ++++++++++++++++++ ai_commit_msg/cli/summary_handler.py | 2 +- ai_commit_msg/core/gen_commit_msg.py | 4 +- ai_commit_msg/core/prompt.py | 19 +++- ai_commit_msg/main.py | 24 +++- 5 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 ai_commit_msg/cli/conventional_commit_handler.py diff --git a/ai_commit_msg/cli/conventional_commit_handler.py b/ai_commit_msg/cli/conventional_commit_handler.py new file mode 100644 index 0000000..1479747 --- /dev/null +++ b/ai_commit_msg/cli/conventional_commit_handler.py @@ -0,0 +1,105 @@ +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 + + +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): + # 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 with the conventional parameter set to True + ai_commit_msg = generate_commit_message(diff, conventional=True) + + # Get commit type and scope + commit_type = select_commit_type() + scope = get_scope() + + # Format and print the conventional commit + formatted_commit = print_conventional_commit(commit_type, scope, ai_commit_msg) + + # Ask if user wants to commit + should_commit = input("Would you like to commit with this message? (y/n): ") + + if should_commit.lower() == "y": + execute_cli_command(["git", "commit", "-m", formatted_commit], output=True) + Logger().log("Commit successful!") + else: + Logger().log("Commit cancelled.") diff --git a/ai_commit_msg/cli/summary_handler.py b/ai_commit_msg/cli/summary_handler.py index f7c102b..64ac11d 100644 --- a/ai_commit_msg/cli/summary_handler.py +++ b/ai_commit_msg/cli/summary_handler.py @@ -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 diff --git a/ai_commit_msg/core/gen_commit_msg.py b/ai_commit_msg/core/gen_commit_msg.py index 306e339..d3e1486 100644 --- a/ai_commit_msg/core/gen_commit_msg.py +++ b/ai_commit_msg/core/gen_commit_msg.py @@ -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 diff --git a/ai_commit_msg/core/prompt.py b/ai_commit_msg/core/prompt.py index f9887e4..08e9821 100644 --- a/ai_commit_msg/core/prompt.py +++ b/ai_commit_msg/core/prompt.py @@ -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. diff --git a/ai_commit_msg/main.py b/ai_commit_msg/main.py index 11f39ee..710f021 100644 --- a/ai_commit_msg/main.py +++ b/ai_commit_msg/main.py @@ -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(): @@ -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) @@ -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 From 420097bda7c31c9453949e57419aa71f04a8baa4 Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Tue, 4 Mar 2025 21:12:25 -0600 Subject: [PATCH 04/15] feat: refactor commit process to include error handling and push options --- .../cli/conventional_commit_handler.py | 66 +++++++++++++++---- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/ai_commit_msg/cli/conventional_commit_handler.py b/ai_commit_msg/cli/conventional_commit_handler.py index 1479747..3682d81 100644 --- a/ai_commit_msg/cli/conventional_commit_handler.py +++ b/ai_commit_msg/cli/conventional_commit_handler.py @@ -2,6 +2,7 @@ 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 COMMIT_TYPES = { @@ -65,19 +66,21 @@ def get_scope(): 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") + 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") + logger.log("Fetching your staged changes...\n") if len(GitService.get_staged_files()) == 0: - Logger().log( + logger.log( "🚨 No files are staged for commit. Run `git add` to stage some of your changes" ) return @@ -85,21 +88,60 @@ def conventional_commit_handler(args): staged_changes_diff = execute_cli_command(["git", "diff", "--staged"]) diff = staged_changes_diff.stdout - # Generate the commit message body with the conventional parameter set to True - ai_commit_msg = generate_commit_message(diff, conventional=True) + # 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 and print the conventional commit + # Format the conventional commit formatted_commit = print_conventional_commit(commit_type, scope, ai_commit_msg) - # Ask if user wants to commit - should_commit = input("Would you like to commit with this message? (y/n): ") + # 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 - if should_commit.lower() == "y": - execute_cli_command(["git", "commit", "-m", formatted_commit], output=True) - Logger().log("Commit successful!") + # Commit the changes + execute_cli_command(["git", "commit", "-m", formatted_commit], output=True) + + # Handle git push with upstream setting if needed + 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 + ) + logger.log(f"🔄 Upstream branch set for '{current_branch}'") else: - Logger().log("Commit cancelled.") + logger.log("Skipping push. You can set upstream manually") + + return 0 From c1ca024356a4ac5b48cbe76fa3ead047cdad1e1e Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Tue, 4 Mar 2025 21:46:25 -0600 Subject: [PATCH 05/15] refactor: refactor git push handling into a utility function --- .../cli/conventional_commit_handler.py | 21 ++----------- .../cli/gen_ai_commit_message_handler.py | 18 ++--------- ai_commit_msg/utils/git_utils.py | 30 +++++++++++++++++++ 3 files changed, 35 insertions(+), 34 deletions(-) create mode 100644 ai_commit_msg/utils/git_utils.py diff --git a/ai_commit_msg/cli/conventional_commit_handler.py b/ai_commit_msg/cli/conventional_commit_handler.py index 3682d81..b5b3cc9 100644 --- a/ai_commit_msg/cli/conventional_commit_handler.py +++ b/ai_commit_msg/cli/conventional_commit_handler.py @@ -3,6 +3,7 @@ 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 = { @@ -125,23 +126,7 @@ def conventional_commit_handler(args): # Commit the changes execute_cli_command(["git", "commit", "-m", formatted_commit], output=True) - # Handle git push with upstream setting if needed - 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 - ) - logger.log(f"🔄 Upstream branch set for '{current_branch}'") - else: - logger.log("Skipping push. You can set upstream manually") + # Handle git push with the shared utility function + handle_git_push() return 0 diff --git a/ai_commit_msg/cli/gen_ai_commit_message_handler.py b/ai_commit_msg/cli/gen_ai_commit_message_handler.py index 6053b26..ba03e17 100644 --- a/ai_commit_msg/cli/gen_ai_commit_message_handler.py +++ b/ai_commit_msg/cli/gen_ai_commit_message_handler.py @@ -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(): @@ -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 diff --git a/ai_commit_msg/utils/git_utils.py b/ai_commit_msg/utils/git_utils.py new file mode 100644 index 0000000..10e3bfd --- /dev/null +++ b/ai_commit_msg/utils/git_utils.py @@ -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 From bff82da20e6b0f346caa2ade178d77d6fefef4b7 Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Tue, 4 Mar 2025 21:50:15 -0600 Subject: [PATCH 06/15] remove comments from conventional_commit_handler.py --- ai_commit_msg/cli/conventional_commit_handler.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ai_commit_msg/cli/conventional_commit_handler.py b/ai_commit_msg/cli/conventional_commit_handler.py index b5b3cc9..9ad587b 100644 --- a/ai_commit_msg/cli/conventional_commit_handler.py +++ b/ai_commit_msg/cli/conventional_commit_handler.py @@ -123,10 +123,8 @@ def conventional_commit_handler(args): 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 From 926ac64b71c17c261477379e7d091a91d236b59f Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Tue, 4 Mar 2025 22:07:41 -0600 Subject: [PATCH 07/15] feat: add command to generate conventional commit messages --- README.md | 27 ++++++++++++++++++ .../cli/conventional_commit_handler.py | 28 +++++++------------ ai_commit_msg/main.py | 12 -------- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index bdb936b..72d9923 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,33 @@ git-ai-commit summarize --- +🏷️ `git-ai-commit conventional` + +Generate commit messages in the [Conventional Commits](https://www.conventionalcommits.org/) format (`type(scope): description`). + +```bash +git-ai-commit conventional +``` + +This command: +1. Analyzes your changes using AI +2. Prompts you to select a commit type (feat, fix, docs, etc.) +3. Allows you to add an optional scope +4. Formats the message according to conventional commit standards +5. 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. diff --git a/ai_commit_msg/cli/conventional_commit_handler.py b/ai_commit_msg/cli/conventional_commit_handler.py index 9ad587b..96505fa 100644 --- a/ai_commit_msg/cli/conventional_commit_handler.py +++ b/ai_commit_msg/cli/conventional_commit_handler.py @@ -69,25 +69,17 @@ def get_scope(): 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 + # Simplify the diff handling - only use staged changes + 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 + staged_changes_diff = execute_cli_command(["git", "diff", "--staged"]) + diff = staged_changes_diff.stdout # Generate the commit message body try: diff --git a/ai_commit_msg/main.py b/ai_commit_msg/main.py index 710f021..cf0c562 100644 --- a/ai_commit_msg/main.py +++ b/ai_commit_msg/main.py @@ -145,18 +145,6 @@ def main(argv: Sequence[str] = sys.argv[1:]) -> int: 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) From 026e836dc1f4517b8d2e74b9bd9e7719fad053dd Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Wed, 5 Mar 2025 12:13:30 -0600 Subject: [PATCH 08/15] feat: add ai suggestions for commit type selection and handling --- .../cli/conventional_commit_handler.py | 67 ++++++++++++++++--- ai_commit_msg/core/gen_commit_msg.py | 13 ++-- ai_commit_msg/core/prompt.py | 22 +++++- 3 files changed, 86 insertions(+), 16 deletions(-) diff --git a/ai_commit_msg/cli/conventional_commit_handler.py b/ai_commit_msg/cli/conventional_commit_handler.py index 96505fa..45c8ad9 100644 --- a/ai_commit_msg/cli/conventional_commit_handler.py +++ b/ai_commit_msg/cli/conventional_commit_handler.py @@ -35,15 +35,29 @@ def print_conventional_commit(commit_type, scope, message): return formatted_commit -def select_commit_type(): - Logger().log("Select a commit type:") +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): - Logger().log(f"{i}. {type_key}: {description}") + # 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") + logger.log(f" {len(COMMIT_TYPES) + 1}. custom: Enter a custom type") while True: try: @@ -56,9 +70,9 @@ def select_commit_type(): custom_type = input("Enter your custom commit type: ") return custom_type else: - Logger().log("Invalid choice. Please try again.") + logger.log("Invalid choice. Please try again.") except ValueError: - Logger().log("Please enter a valid number.") + logger.log("Please enter a valid number.") def get_scope(): @@ -69,7 +83,7 @@ def get_scope(): def conventional_commit_handler(args): logger = Logger() - # Simplify the diff handling - only use staged changes + # Get the diff from staged changes logger.log("Fetching your staged changes...\n") if len(GitService.get_staged_files()) == 0: @@ -81,6 +95,21 @@ def conventional_commit_handler(args): staged_changes_diff = execute_cli_command(["git", "diff", "--staged"]) diff = staged_changes_diff.stdout + # First, have the AI classify the commit type + try: + logger.log("🤖 AI is analyzing your changes to suggest a commit type...\n") + suggested_type = generate_commit_message(diff, classify_type=True) + + # Validate the suggested type + 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 + # Generate the commit message body try: ai_commit_msg = generate_commit_message(diff, conventional=True) @@ -92,8 +121,8 @@ def conventional_commit_handler(args): logger.log("No commit message provided. Exiting.") return - # Get commit type and scope - commit_type = select_commit_type() + # Get commit type (with AI suggestion) and scope + commit_type = select_commit_type(suggested_type) scope = get_scope() # Format the conventional commit @@ -115,8 +144,26 @@ def conventional_commit_handler(args): logger.log("🚨 Invalid input. Exiting.") return + # Commit the changes execute_cli_command(["git", "commit", "-m", formatted_commit], output=True) - handle_git_push() + # Handle git push + 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 + ) + logger.log(f"🔄 Upstream branch set for '{current_branch}'") + else: + logger.log("Skipping push. You can set upstream manually") return 0 diff --git a/ai_commit_msg/core/gen_commit_msg.py b/ai_commit_msg/core/gen_commit_msg.py index d3e1486..71813d9 100644 --- a/ai_commit_msg/core/gen_commit_msg.py +++ b/ai_commit_msg/core/gen_commit_msg.py @@ -3,13 +3,18 @@ from ai_commit_msg.services.config_service import ConfigService -def generate_commit_message(diff: str = None, conventional: bool = False) -> str: +def generate_commit_message( + diff: str = None, conventional: bool = False, classify_type: bool = False +) -> str: if diff is None: raise ValueError("Diff is required to generate a commit message") - prompt = get_prompt(diff, conventional=conventional) + prompt = get_prompt(diff, conventional=conventional, classify_type=classify_type) ai_gen_commit_msg = llm_chat_completion(prompt) - prefix = ConfigService().prefix - return prefix + ai_gen_commit_msg + if not classify_type: + prefix = ConfigService().prefix + return prefix + ai_gen_commit_msg + else: + return ai_gen_commit_msg.strip().lower() diff --git a/ai_commit_msg/core/prompt.py b/ai_commit_msg/core/prompt.py index 08e9821..2e7b846 100644 --- a/ai_commit_msg/core/prompt.py +++ b/ai_commit_msg/core/prompt.py @@ -1,10 +1,28 @@ from ai_commit_msg.services.config_service import ConfigService -def get_prompt(diff, conventional=False): +def get_prompt(diff, conventional=False, classify_type=False): max_length = ConfigService().max_length - if conventional: + 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 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. From 4495d6728e1e9a57a0a709a6b52ec10a6f60e559 Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Wed, 5 Mar 2025 12:14:57 -0600 Subject: [PATCH 09/15] docs(docs for conventional command): update command description in README.md --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 72d9923..450bd01 100644 --- a/README.md +++ b/README.md @@ -188,11 +188,12 @@ git-ai-commit conventional ``` This command: -1. Analyzes your changes using AI -2. Prompts you to select a commit type (feat, fix, docs, etc.) -3. Allows you to add an optional scope -4. Formats the message according to conventional commit standards -5. Gives you the option to commit and push +1. Analyzes your staged changes using AI +2. Suggests the most appropriate commit type based on your changes +3. Allows you to accept the suggestion or choose a different type +4. Allows you to add an optional scope +5. Formats the message according to conventional commit standards +6. Gives you the option to commit and push Available commit types: - `feat`: New feature From 91d1897870bff2f5984ebb421de408c4de8d7bb8 Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Wed, 5 Mar 2025 12:36:03 -0600 Subject: [PATCH 10/15] update pre-commit hooks version, suggest commit scope --- .pre-commit-config.yaml | 2 +- README.md | 4 +-- .../cli/conventional_commit_handler.py | 36 +++++++++++++++++-- ai_commit_msg/core/gen_commit_msg.py | 14 ++++++-- ai_commit_msg/core/prompt.py | 24 ++++++++++++- 5 files changed, 70 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0784577..baba9d0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ default_install_hook_types: - prepare-commit-msg repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v4.6.0 hooks: - id: trailing-whitespace exclude: '^.*\.(bin|exe|jpg|png|gif|md)$' diff --git a/README.md b/README.md index 450bd01..977c859 100644 --- a/README.md +++ b/README.md @@ -190,8 +190,8 @@ git-ai-commit conventional This command: 1. Analyzes your staged changes using AI 2. Suggests the most appropriate commit type based on your changes -3. Allows you to accept the suggestion or choose a different type -4. Allows you to add an optional scope +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 diff --git a/ai_commit_msg/cli/conventional_commit_handler.py b/ai_commit_msg/cli/conventional_commit_handler.py index 45c8ad9..d9cd0bf 100644 --- a/ai_commit_msg/cli/conventional_commit_handler.py +++ b/ai_commit_msg/cli/conventional_commit_handler.py @@ -75,7 +75,17 @@ def select_commit_type(suggested_type=None): logger.log("Please enter a valid number.") -def get_scope(): +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() @@ -110,6 +120,24 @@ def conventional_commit_handler(args): logger.log(f"Error classifying commit type: {e}") suggested_type = None + # Have the AI suggest a scope - make sure this is called separately + suggested_scope = None + try: + logger.log("🤖 AI is analyzing your changes to suggest a scope...\n") + # Make sure we're explicitly setting classify_scope=True and other params to False + suggested_scope = generate_commit_message( + diff, conventional=False, classify_type=False, classify_scope=True + ) + + # Add debug logging to see what's being returned + 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 + # Generate the commit message body try: ai_commit_msg = generate_commit_message(diff, conventional=True) @@ -123,7 +151,9 @@ def conventional_commit_handler(args): # Get commit type (with AI suggestion) and scope commit_type = select_commit_type(suggested_type) - scope = get_scope() + + # Make sure we're passing the suggested scope to get_scope + scope = get_scope(suggested_scope) # Format the conventional commit formatted_commit = print_conventional_commit(commit_type, scope, ai_commit_msg) @@ -145,7 +175,7 @@ def conventional_commit_handler(args): return # Commit the changes - execute_cli_command(["git", "commit", "-m", formatted_commit], output=True) + execute_cli_command(["git", "commit", "-m", f'"{formatted_commit}"'], output=True) # Handle git push current_branch = GitService.get_current_branch() diff --git a/ai_commit_msg/core/gen_commit_msg.py b/ai_commit_msg/core/gen_commit_msg.py index 71813d9..4ba838c 100644 --- a/ai_commit_msg/core/gen_commit_msg.py +++ b/ai_commit_msg/core/gen_commit_msg.py @@ -4,16 +4,24 @@ def generate_commit_message( - diff: str = None, conventional: bool = False, classify_type: bool = False + 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, conventional=conventional, classify_type=classify_type) + prompt = get_prompt( + diff, + conventional=conventional, + classify_type=classify_type, + classify_scope=classify_scope, + ) ai_gen_commit_msg = llm_chat_completion(prompt) - if not classify_type: + if not classify_type and not classify_scope: prefix = ConfigService().prefix return prefix + ai_gen_commit_msg else: diff --git a/ai_commit_msg/core/prompt.py b/ai_commit_msg/core/prompt.py index 2e7b846..400e4ca 100644 --- a/ai_commit_msg/core/prompt.py +++ b/ai_commit_msg/core/prompt.py @@ -1,7 +1,7 @@ from ai_commit_msg.services.config_service import ConfigService -def get_prompt(diff, conventional=False, classify_type=False): +def get_prompt(diff, conventional=False, classify_type=False, classify_scope=False): max_length = ConfigService().max_length if classify_type: @@ -21,6 +21,28 @@ def get_prompt(diff, conventional=False, classify_type=False): - 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""" From 5e11ccbd5510a94f9b495b14b88bef973b88d8e1 Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Wed, 5 Mar 2025 12:39:45 -0600 Subject: [PATCH 11/15] docs: remove example command from README documentation --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 977c859..66351e5 100644 --- a/README.md +++ b/README.md @@ -183,10 +183,6 @@ git-ai-commit summarize Generate commit messages in the [Conventional Commits](https://www.conventionalcommits.org/) format (`type(scope): description`). -```bash -git-ai-commit conventional -``` - This command: 1. Analyzes your staged changes using AI 2. Suggests the most appropriate commit type based on your changes From 8428d72c7333952c99a5a4f0426d4c35c8ba4599 Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Wed, 5 Mar 2025 12:51:39 -0600 Subject: [PATCH 12/15] refactor: add blank line before available commit types --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 66351e5..a2dcfb9 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,7 @@ This command: 6. Gives you the option to commit and push Available commit types: + - `feat`: New feature - `fix`: Bug fix - `docs`: Documentation changes From 275f274fd7102e817dfe7ec337ff0a1456e03238 Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Wed, 5 Mar 2025 12:55:35 -0600 Subject: [PATCH 13/15] refactor(cli): refactor ai commit type and scope suggestion logic --- ai_commit_msg/cli/conventional_commit_handler.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ai_commit_msg/cli/conventional_commit_handler.py b/ai_commit_msg/cli/conventional_commit_handler.py index d9cd0bf..a7719af 100644 --- a/ai_commit_msg/cli/conventional_commit_handler.py +++ b/ai_commit_msg/cli/conventional_commit_handler.py @@ -105,12 +105,11 @@ def conventional_commit_handler(args): staged_changes_diff = execute_cli_command(["git", "diff", "--staged"]) diff = staged_changes_diff.stdout - # First, have the AI classify the commit type + # AI suggests a commit type try: logger.log("🤖 AI is analyzing your changes to suggest a commit type...\n") suggested_type = generate_commit_message(diff, classify_type=True) - # Validate the suggested type if suggested_type not in COMMIT_TYPES: logger.log( f"AI suggested an invalid type: '{suggested_type}'. Falling back to manual selection." @@ -120,16 +119,12 @@ def conventional_commit_handler(args): logger.log(f"Error classifying commit type: {e}") suggested_type = None - # Have the AI suggest a scope - make sure this is called separately suggested_scope = None try: logger.log("🤖 AI is analyzing your changes to suggest a scope...\n") - # Make sure we're explicitly setting classify_scope=True and other params to False suggested_scope = generate_commit_message( diff, conventional=False, classify_type=False, classify_scope=True ) - - # Add debug logging to see what's being returned logger.log(f"Debug - AI suggested scope: '{suggested_scope}'") if suggested_scope == "none" or not suggested_scope: @@ -152,7 +147,6 @@ def conventional_commit_handler(args): # Get commit type (with AI suggestion) and scope commit_type = select_commit_type(suggested_type) - # Make sure we're passing the suggested scope to get_scope scope = get_scope(suggested_scope) # Format the conventional commit From 1fabdd72dbe98269ca5d9f03249686d0b83d8f1d Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Wed, 5 Mar 2025 12:57:38 -0600 Subject: [PATCH 14/15] refactor(cli): refactor git push handling into separate function --- .../cli/conventional_commit_handler.py | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/ai_commit_msg/cli/conventional_commit_handler.py b/ai_commit_msg/cli/conventional_commit_handler.py index a7719af..a58af87 100644 --- a/ai_commit_msg/cli/conventional_commit_handler.py +++ b/ai_commit_msg/cli/conventional_commit_handler.py @@ -172,22 +172,4 @@ def conventional_commit_handler(args): execute_cli_command(["git", "commit", "-m", f'"{formatted_commit}"'], output=True) # Handle git push - 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 - ) - logger.log(f"🔄 Upstream branch set for '{current_branch}'") - else: - logger.log("Skipping push. You can set upstream manually") - - return 0 + handle_git_push() \ No newline at end of file From 9672a5ab3912e2442a2d36dd2731d84e14a5f257 Mon Sep 17 00:00:00 2001 From: Seif-Mamdouh Date: Wed, 5 Mar 2025 12:59:01 -0600 Subject: [PATCH 15/15] remove commented code lines from conventional_commit_handler --- ai_commit_msg/cli/conventional_commit_handler.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ai_commit_msg/cli/conventional_commit_handler.py b/ai_commit_msg/cli/conventional_commit_handler.py index a58af87..3f5d094 100644 --- a/ai_commit_msg/cli/conventional_commit_handler.py +++ b/ai_commit_msg/cli/conventional_commit_handler.py @@ -93,7 +93,6 @@ def get_scope(suggested_scope=None): def conventional_commit_handler(args): logger = Logger() - # Get the diff from staged changes logger.log("Fetching your staged changes...\n") if len(GitService.get_staged_files()) == 0: @@ -105,7 +104,6 @@ def conventional_commit_handler(args): staged_changes_diff = execute_cli_command(["git", "diff", "--staged"]) diff = staged_changes_diff.stdout - # AI suggests a commit type try: logger.log("🤖 AI is analyzing your changes to suggest a commit type...\n") suggested_type = generate_commit_message(diff, classify_type=True) @@ -133,7 +131,6 @@ def conventional_commit_handler(args): logger.log(f"Error suggesting scope: {e}") suggested_scope = None - # Generate the commit message body try: ai_commit_msg = generate_commit_message(diff, conventional=True) except AIModelHandlerError as e: @@ -144,15 +141,12 @@ def conventional_commit_handler(args): logger.log("No commit message provided. Exiting.") return - # Get commit type (with AI suggestion) and scope commit_type = select_commit_type(suggested_type) scope = get_scope(suggested_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 @@ -168,8 +162,6 @@ def conventional_commit_handler(args): logger.log("🚨 Invalid input. Exiting.") return - # Commit the changes execute_cli_command(["git", "commit", "-m", f'"{formatted_commit}"'], output=True) - # Handle git push handle_git_push() \ No newline at end of file