Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
__pycache__/
*.pyc
*.egg-info/
build/
build/
.penify/
.penify/*
93 changes: 73 additions & 20 deletions penify_hook/commands/commit_commands.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
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,
jira_url=None, jira_user=None, jira_api_token=None):
"""
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)

Expand All @@ -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.")
Expand All @@ -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:
Expand All @@ -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)
45 changes: 41 additions & 4 deletions penify_hook/commands/config_commands.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import os
import random
import webbrowser
import http.server
Expand All @@ -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():
Expand Down Expand Up @@ -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'

Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
67 changes: 61 additions & 6 deletions penify_hook/commands/doc_commands.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
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)
27 changes: 13 additions & 14 deletions penify_hook/commit_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
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):
def __init__(self, repo_path: str, api_client: APIClient, llm_client=None, jira_client=None):
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.
Expand Down Expand Up @@ -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
Expand All @@ -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")

Expand All @@ -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:
Expand Down Expand Up @@ -129,29 +132,25 @@ 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:
comment = (
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

Expand Down
Loading