diff --git a/.gitignore b/.gitignore index e6b56ac..e095838 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ __pycache__/ *.pyc *.egg-info/ -build/ \ No newline at end of file +build/ +.penify/ +.penify/* diff --git a/penify_hook/commands/commit_commands.py b/penify_hook/commands/commit_commands.py index 3c51aff..96ec297 100644 --- a/penify_hook/commands/commit_commands.py +++ b/penify_hook/commands/commit_commands.py @@ -1,21 +1,9 @@ import os import sys +import argparse -from penify_hook.ui_utils import print_error -from penify_hook.utils import recursive_search_git_folder -from ..commit_analyzer import CommitDocGenHook -from ..api_client import APIClient +from penify_hook.ui_utils import print_info, print_warning -# Try importing optional dependencies -try: - from ..llm_client import LLMClient -except ImportError: - LLMClient = None - -try: - from ..jira_client import JiraClient -except ImportError: - JiraClient = None def commit_code(api_url, token, message, open_terminal, generate_description, llm_model=None, llm_api_base=None, llm_api_key=None, @@ -23,6 +11,22 @@ def commit_code(api_url, token, message, open_terminal, generate_description, """ Enhance Git commits with AI-powered commit messages. """ + + from penify_hook.ui_utils import print_error + from penify_hook.utils import recursive_search_git_folder + from ..commit_analyzer import CommitDocGenHook + from ..api_client import APIClient + + # Try importing optional dependencies + try: + from ..llm_client import LLMClient + except ImportError: + LLMClient = None + + try: + from ..jira_client import JiraClient + except ImportError: + JiraClient = None # Create API client api_client = APIClient(api_url, token) @@ -35,10 +39,10 @@ def commit_code(api_url, token, message, open_terminal, generate_description, api_base=llm_api_base, api_key=llm_api_key ) - print(f"Using LLM model: {llm_model}") + print_info(f"Using LLM model: {llm_model}") except Exception as e: - print(f"Error initializing LLM client: {e}") - print("Falling back to API for commit summary generation") + print_error(f"Error initializing LLM client: {e}") + print_error("Falling back to API for commit summary generation") else: if not token: print_error("No LLM model or API token provided. Please provide an LLM model or API token.") @@ -53,12 +57,12 @@ def commit_code(api_url, token, message, open_terminal, generate_description, jira_api_token=jira_api_token ) if jira_client.is_connected(): - print(f"Connected to JIRA: {jira_url}") + print_info(f"Connected to JIRA: {jira_url}") else: - print(f"Failed to connect to JIRA: {jira_url}") + print_warning(f"Failed to connect to JIRA: {jira_url}") jira_client = None except Exception as e: - print(f"Error initializing JIRA client: {e}") + print_warning(f"Error initializing JIRA client: {e}") jira_client = None try: @@ -69,3 +73,52 @@ def commit_code(api_url, token, message, open_terminal, generate_description, except Exception as e: print(f"Error: {e}") sys.exit(1) + + + + + +def setup_commit_parser(parser): + commit_parser_description = """ +It generates smart commit messages. By default, it will just generate just the Title of the commit message. +1. If you have not configured LLM, it will give an error. You either need to configure LLM or use the API key. +2. If you have not configured JIRA. It will not enhance the commit message with JIRA issue details. +3. For more information, visit https://docs.penify.dev/ +""" + parser.help = "Generate smart commit messages using local-LLM(no login required)." + parser.description = commit_parser_description + parser.formatter_class = argparse.RawDescriptionHelpFormatter + + # Add the message argument with better help + parser.add_argument("-m", "--message", required=False, help="Commit with contextual commit message.", default="N/A") + parser.add_argument("-e", "--terminal", action="store_true", help="Open edit terminal before committing.") + parser.add_argument("-d", "--description", action="store_false", help="It will generate commit message with title and description.", default=False) + +def handle_commit(args): + from penify_hook.commands.commit_commands import commit_code + from penify_hook.commands.config_commands import get_jira_config, get_llm_config, get_token + from penify_hook.constants import API_URL + + # Only import dependencies needed for commit functionality here + open_terminal = args.terminal + generate_description = args.description + print_info(f"Generate Commit Description: {generate_description}") + # Try to get from config + llm_config = get_llm_config() + llm_model = llm_config.get('model') + llm_api_base = llm_config.get('api_base') + llm_api_key = llm_config.get('api_key') + token = get_token() + + + + # Try to get from config + jira_config = get_jira_config() + jira_url = jira_config.get('url') + jira_user = jira_config.get('username') + jira_api_token = jira_config.get('api_token') + + + commit_code(API_URL, token, args.message, open_terminal, generate_description, + llm_model, llm_api_base, llm_api_key, + jira_url, jira_user, jira_api_token) \ No newline at end of file diff --git a/penify_hook/commands/config_commands.py b/penify_hook/commands/config_commands.py index 8125407..77779f4 100644 --- a/penify_hook/commands/config_commands.py +++ b/penify_hook/commands/config_commands.py @@ -1,4 +1,5 @@ import json +import os import random import webbrowser import http.server @@ -8,12 +9,46 @@ from threading import Thread import logging + +def get_penify_config() -> Path: + """ + Get the home directory for the .penify configuration file. + This function searches for the .penify file in the current directory + and its parent directories until it finds it or reaches the home directory. + """ + from penify_hook.utils import recursive_search_git_folder + + current_dir = os.getcwd() + home_dir = recursive_search_git_folder(current_dir) + + + if not home_dir: + home_dir = Path.home() + else: + home_dir = Path(home_dir) + + penify_dir = home_dir / '.penify' + if penify_dir.exists(): + return penify_dir / 'config.json' + else: + # Create the .penify directory if it doesn't exist + os.makedirs(penify_dir, exist_ok=True) + ## update gitignore + + # Create the .penify directory + os.makedirs(penify_dir, exist_ok=True) + # Create an empty config.json file + with open(penify_dir / 'config.json', 'w') as f: + json.dump({}, f) + return penify_dir / 'config.json' + + def save_llm_config(model, api_base, api_key): """ Save LLM configuration settings in the .penify file. """ - home_dir = Path.home() - penify_file = home_dir / '.penify' + + penify_file = get_penify_config() config = {} if penify_file.exists(): @@ -43,6 +78,8 @@ def save_jira_config(url, username, api_token): """ Save JIRA configuration settings in the .penify file. """ + from penify_hook.utils import recursive_search_git_folder + home_dir = Path.home() penify_file = home_dir / '.penify' @@ -74,7 +111,7 @@ def get_llm_config(): """ Get LLM configuration from the .penify file. """ - config_file = Path.home() / '.penify' + config_file = get_penify_config() if config_file.exists(): try: with open(config_file, 'r') as f: @@ -89,7 +126,7 @@ def get_jira_config(): """ Get JIRA configuration from the .penify file. """ - config_file = Path.home() / '.penify' + config_file = get_penify_config() if config_file.exists(): try: with open(config_file, 'r') as f: diff --git a/penify_hook/commands/doc_commands.py b/penify_hook/commands/doc_commands.py index a3fafcd..36bd7dc 100644 --- a/penify_hook/commands/doc_commands.py +++ b/penify_hook/commands/doc_commands.py @@ -1,11 +1,14 @@ + +import logging import os -import sys -from ..folder_analyzer import FolderAnalyzerGenHook -from ..file_analyzer import FileAnalyzerGenHook -from ..git_analyzer import GitDocGenHook -from ..api_client import APIClient def generate_doc(api_url, token, location=None): + import os + import sys + from ..folder_analyzer import FolderAnalyzerGenHook + from ..file_analyzer import FileAnalyzerGenHook + from ..git_analyzer import GitDocGenHook + from ..api_client import APIClient """Generates documentation based on the given parameters. This function initializes an API client using the provided API URL and @@ -46,4 +49,56 @@ def generate_doc(api_url, token, location=None): analyzer.run() except Exception as e: print(f"Error: {e}") - sys.exit(1) \ No newline at end of file + sys.exit(1) + + +# Define the docgen description text +docgen_description = """Generate code documentation using Penify. + +This command requires you to be logged in to your Penify account. +You can generate documentation for: +- Current Git diff (default) +- Specific file +- Specific folder +""" + +def setup_docgen_parser(parser): + # We don't need to create a new docgen_parser since it's passed as a parameter + docgen_subparsers = parser.add_subparsers(title="docgen_subcommand", dest="docgen_subcommand") + + # Docgen main options (for direct documentation generation) + parser.add_argument("-l", "--location", help="[Optional] Path to the folder or file to Generate Documentation. By default it will pick the root directory.", default=None) + + # Subcommand: install-hook (as part of docgen) + install_hook_parser = docgen_subparsers.add_parser("install-hook", help="Install the Git post-commit hook.") + install_hook_parser.add_argument("-l", "--location", required=False, + help="Location in which to install the Git hook. Defaults to current directory.", + default=os.getcwd()) + + # Subcommand: uninstall-hook (as part of docgen) + uninstall_hook_parser = docgen_subparsers.add_parser("uninstall-hook", help="Uninstall the Git post-commit hook.") + uninstall_hook_parser.add_argument("-l", "--location", required=False, + help="Location from which to uninstall the Git hook. Defaults to current directory.", + default=os.getcwd()) + +def handle_docgen(args): + # Only import dependencies needed for docgen functionality here + from penify_hook.commands.config_commands import get_token + import sys + from penify_hook.commands.doc_commands import generate_doc + from penify_hook.commands.hook_commands import install_git_hook, uninstall_git_hook + from penify_hook.constants import API_URL + + token = get_token() + if not token: + logging.error("Error: Unable to authenticate. Please run 'penifycli login'.") + sys.exit(1) + + if args.docgen_subcommand == "install-hook": + install_git_hook(args.location, token) + + elif args.docgen_subcommand == "uninstall-hook": + uninstall_git_hook(args.location) + + else: # Direct documentation generation + generate_doc(API_URL, token, args.location) \ No newline at end of file diff --git a/penify_hook/commit_analyzer.py b/penify_hook/commit_analyzer.py index 65511e5..5b89c6d 100644 --- a/penify_hook/commit_analyzer.py +++ b/penify_hook/commit_analyzer.py @@ -7,6 +7,8 @@ from tqdm import tqdm from penify_hook.base_analyzer import BaseAnalyzer +from penify_hook.jira_client import JiraClient +from penify_hook.ui_utils import print_info, print_success, print_warning from .api_client import APIClient class CommitDocGenHook(BaseAnalyzer): @@ -14,7 +16,7 @@ def __init__(self, repo_path: str, api_client: APIClient, llm_client=None, jira_ super().__init__(repo_path, api_client) self.llm_client = llm_client # Add LLM client as an optional parameter - self.jira_client = jira_client # Add JIRA client as an optional parameter + self.jira_client: JiraClient = jira_client # Add JIRA client as an optional parameter def get_summary(self, instruction: str, generate_description: bool) -> dict: """Generate a summary for the commit based on the staged changes. @@ -50,11 +52,11 @@ def get_summary(self, instruction: str, generate_description: bool) -> dict: # If issues found in branch, get context if issue_keys: jira_context = self.jira_client.get_commit_context_from_issues(issue_keys) - print(f"Adding JIRA context from issues: {', '.join(issue_keys)}") except Exception as e: print(f"Could not get JIRA context: {e}") # Use LLM client if provided, otherwise use API client + print_info("Fetching commit summary from LLM...") if self.llm_client: return self.api_client.generate_commit_summary_with_llm( diff, instruction, generate_description, self.repo_details, self.llm_client, jira_context @@ -81,7 +83,7 @@ def run(self, msg: Optional[str], edit_commit_message: bool, generate_descriptio Raises: Exception: If there is an error generating the commit summary. """ - summary: dict = self.get_summary(msg, generate_description) + summary: dict = self.get_summary(msg, True) if not summary: raise Exception("Error generating commit summary") @@ -91,15 +93,16 @@ def run(self, msg: Optional[str], edit_commit_message: bool, generate_descriptio # If JIRA client is available, integrate JIRA information if self.jira_client and self.jira_client.is_connected(): # Add JIRA information to commit message - title, description = self.process_jira_integration(title, description, msg) + self.process_jira_integration(title, description, msg) # commit the changes to the repository with above details - commit_msg = f"{title}\n\n{description}" + commit_msg = f"{title}\n\n{description}" if generate_description else title self.repo.git.commit('-m', commit_msg) + print_success(f"Commit: {commit_msg}") if edit_commit_message: # Open the git commit edit terminal - print("Opening git commit edit terminal...") + print_info("Opening git commit edit terminal...") self._amend_commit() def process_jira_integration(self, title: str, description: str, msg: str) -> tuple: @@ -129,17 +132,14 @@ def process_jira_integration(self, title: str, description: str, msg: str) -> tu for key in branch_issue_keys: if key not in issue_keys: issue_keys.append(key) - print(f"Added JIRA issue {key} from branch name: {current_branch}") + print_info(f"Added JIRA issue {key} from branch name: {current_branch}") except Exception as e: - print(f"Could not extract JIRA issues from branch name: {e}") + print_warning(f"Could not extract JIRA issues from branch name: {e}") if issue_keys: - print(f"Found JIRA issues: {', '.join(issue_keys)}") + print_info(f"Found JIRA issues: {', '.join(issue_keys)}") # Format commit message with JIRA info - title, description = self.jira_client.format_commit_message_with_jira_info( - title, description, issue_keys - ) # Add comments to JIRA issues for issue_key in issue_keys: @@ -147,11 +147,10 @@ def process_jira_integration(self, title: str, description: str, msg: str) -> tu f"Commit related to this issue:\n\n" f"**{title}**\n\n" f"{description}\n\n" - f"Repository: {self.repo_details.get('organization_name')}/{self.repo_details.get('repo_name')}" ) self.jira_client.add_comment(issue_key, comment) else: - print("No JIRA issues found in commit message or branch name") + print_warning("No JIRA issues found in commit message or branch name") return title, description diff --git a/penify_hook/commit_command.py b/penify_hook/commit_command.py deleted file mode 100644 index a04a27d..0000000 --- a/penify_hook/commit_command.py +++ /dev/null @@ -1,48 +0,0 @@ -import argparse - - - -def setup_commit_parser(parser): - commit_parser_description = """ -It generates smart commit messages. By default, it will just generate just the Title of the commit message. -1. If you have not configured LLM, it will give an error. You either need to configure LLM or use the API key. -2. If you have not configured JIRA. It will not enhance the commit message with JIRA issue details. -3. For more information, visit https://docs.penify.dev/ -""" - parser.help = "Generate smart commit messages using local-LLM(no login required)." - parser.description = commit_parser_description - parser.formatter_class = argparse.RawDescriptionHelpFormatter - - # Add the message argument with better help - parser.add_argument("-m", "--message", required=False, help="Commit with contextual commit message.", default="N/A") - parser.add_argument("-e", "--terminal", action="store_true", help="Open edit terminal before committing.") - parser.add_argument("-d", "--description", action="store_false", help="It will generate commit message with title and description.", default=False) - -def handle_commit(args): - from penify_hook.commands.commit_commands import commit_code - from penify_hook.commands.config_commands import get_jira_config, get_llm_config, get_token - from penify_hook.constants import API_URL - - # Only import dependencies needed for commit functionality here - open_terminal = args.terminal - generate_description = args.description - print(f"Generate Description: {generate_description}") - # Try to get from config - llm_config = get_llm_config() - llm_model = llm_config.get('model') - llm_api_base = llm_config.get('api_base') - llm_api_key = llm_config.get('api_key') - token = get_token() - - - - # Try to get from config - jira_config = get_jira_config() - jira_url = jira_config.get('url') - jira_user = jira_config.get('username') - jira_api_token = jira_config.get('api_token') - - - commit_code(API_URL, token, args.message, open_terminal, generate_description, - llm_model, llm_api_base, llm_api_key, - jira_url, jira_user, jira_api_token) diff --git a/penify_hook/docgen_command.py b/penify_hook/docgen_command.py deleted file mode 100644 index a638872..0000000 --- a/penify_hook/docgen_command.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging -import os - - - -# Define the docgen description text -docgen_description = """Generate code documentation using Penify. - -This command requires you to be logged in to your Penify account. -You can generate documentation for: -- Current Git diff (default) -- Specific file -- Specific folder -""" - -def setup_docgen_parser(parser): - # We don't need to create a new docgen_parser since it's passed as a parameter - docgen_subparsers = parser.add_subparsers(title="docgen_subcommand", dest="docgen_subcommand") - - # Docgen main options (for direct documentation generation) - parser.add_argument("-l", "--location", help="[Optional] Path to the folder or file to Generate Documentation. By default it will pick the root directory.", default=None) - - # Subcommand: install-hook (as part of docgen) - install_hook_parser = docgen_subparsers.add_parser("install-hook", help="Install the Git post-commit hook.") - install_hook_parser.add_argument("-l", "--location", required=False, - help="Location in which to install the Git hook. Defaults to current directory.", - default=os.getcwd()) - - # Subcommand: uninstall-hook (as part of docgen) - uninstall_hook_parser = docgen_subparsers.add_parser("uninstall-hook", help="Uninstall the Git post-commit hook.") - uninstall_hook_parser.add_argument("-l", "--location", required=False, - help="Location from which to uninstall the Git hook. Defaults to current directory.", - default=os.getcwd()) - -def handle_docgen(args): - # Only import dependencies needed for docgen functionality here - from penify_hook.commands.config_commands import get_token - import sys - from penify_hook.commands.doc_commands import generate_doc - from penify_hook.commands.hook_commands import install_git_hook, uninstall_git_hook - from penify_hook.constants import API_URL - - token = get_token() - if not token: - logging.error("Error: Unable to authenticate. Please run 'penifycli login'.") - sys.exit(1) - - if args.docgen_subcommand == "install-hook": - install_git_hook(args.location, token) - - elif args.docgen_subcommand == "uninstall-hook": - uninstall_git_hook(args.location) - - else: # Direct documentation generation - generate_doc(API_URL, token, args.location) diff --git a/penify_hook/jira_client.py b/penify_hook/jira_client.py index c4f5927..a367405 100644 --- a/penify_hook/jira_client.py +++ b/penify_hook/jira_client.py @@ -1,6 +1,8 @@ import re import logging from typing import Optional, Dict, List, Any + +from penify_hook.ui_utils import print_info, print_success try: from jira import JIRA JIRA_AVAILABLE = True @@ -69,7 +71,7 @@ def extract_issue_keys_from_branch(self, branch_name: str) -> List[str]: pattern = r'[A-Z][A-Z0-9_]+-[0-9]+' matches = re.findall(pattern, branch_name) if matches: - logging.warning(f"Found JIRA issue in branch name: {matches[0]}") + print_info(f"Fetching relevant JIRA issues") return list(set(matches)) # Remove duplicates def extract_issue_keys(self, text: str) -> List[str]: @@ -85,7 +87,6 @@ def extract_issue_keys(self, text: str) -> List[str]: # Common JIRA issue key pattern: PROJECT-123 pattern = r'[A-Z][A-Z0-9_]+-[0-9]+' matches = re.findall(pattern, text) - print(f"Matches: {matches}") return list(set(matches)) # Remove duplicates def get_issue_details(self, issue_key: str) -> Optional[Dict[str, Any]]: diff --git a/penify_hook/llm_client.py b/penify_hook/llm_client.py index 211d306..fb429ba 100644 --- a/penify_hook/llm_client.py +++ b/penify_hook/llm_client.py @@ -1,5 +1,6 @@ import json import os +import sys from typing import Dict, Optional, List, Any, Union import litellm @@ -16,12 +17,9 @@ def __init__(self, model: str = None, api_base: str = None, api_key: str = None) model: LLM model to use (e.g., "gpt-4", "ollama/llama2", etc.) api_base: Base URL for API requests (e.g., "http://localhost:11434" for Ollama) api_key: API key for the LLM service - """ - self.model = model - self.api_base = api_base - self.api_key = api_key - + """ # Configure litellm if parameters are provided + self.model = model if api_base: os.environ["OPENAI_API_BASE"] = api_base if api_key: @@ -113,8 +111,7 @@ def generate_commit_summary(self, diff: str, message: str, generate_description: messages=[{"role": "user", "content": prompt}], temperature=0.2, max_tokens=800 # Increased token limit to accommodate detailed descriptions - ) - + ) content = response.choices[0].message.content # Extract JSON from the response @@ -146,6 +143,7 @@ def generate_commit_summary(self, diff: str, message: str, generate_description: return result except Exception as e: + sys.exit(f"Error generating commit summary: {e}") # Fallback to a basic summary if LLM fails print(f"Error generating commit summary with LLM: {e}") return { diff --git a/penify_hook/main.py b/penify_hook/main.py index 19c0750..c05faf0 100644 --- a/penify_hook/main.py +++ b/penify_hook/main.py @@ -2,6 +2,7 @@ import sys import time + def main(): parser = argparse.ArgumentParser( description="""Penify CLI tool for: @@ -26,7 +27,7 @@ def main(): # Set up subparsers with proper imports upfront commit_parser = subparsers.add_parser("commit", help="Generate smart commit messages using local-LLM(no login required).") - from .commit_command import setup_commit_parser + from .commands.commit_commands import setup_commit_parser setup_commit_parser(commit_parser) config_parser = subparsers.add_parser("config", help="Configure local-LLM and JIRA.") @@ -38,7 +39,7 @@ def main(): setup_login_parser(login_parser) docgen_parser = subparsers.add_parser("docgen", help="[REQUIRES LOGIN] Generate code documentation for the Git diff, file or folder.") - from .docgen_command import setup_docgen_parser + from .commands.doc_commands import setup_docgen_parser setup_docgen_parser(docgen_parser) # Parse args without validation first to check for simple flags like --version @@ -54,9 +55,9 @@ def main(): args = parser.parse_args() # Handle the commands if args.subcommands == "commit": - print("Please wait while we generate the commit message...") - from .commit_command import handle_commit - time.sleep(1) + from penify_hook.ui_utils import print_info + print_info("Please wait while we generate the commit message...") + from .commands.commit_commands import handle_commit return handle_commit(args) elif args.subcommands == "config": from .config_command import handle_config @@ -65,7 +66,7 @@ def main(): from .login_command import handle_login return handle_login(args) elif args.subcommands == "docgen": - from .docgen_command import handle_docgen + from .commands.doc_commands import handle_docgen return handle_docgen(args) else: parser.print_help()