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
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Penify CLI is a command-line tool for managing Git hooks, generating documentati
You can install Penify CLI using pip:

```bash
pip install penify-cli
pip install penifycli
```

## Usage
Expand All @@ -19,7 +19,7 @@ Penify CLI provides several subcommands for different functionalities:
To log in and obtain an API token:

```bash
penify-cli login
penifycli login
```

This command will open a browser window for authentication. After successful login, the API key will be saved locally for future use.
Expand All @@ -29,7 +29,7 @@ This command will open a browser window for authentication. After successful log
To install the Git post-commit hook:

```bash
penify-cli install-hook -l /path/to/git/repo
penifycli install-hook -l /path/to/git/repo
```

- `-l, --location`: The path to the Git repository where you want to install the hook.
Expand All @@ -39,7 +39,7 @@ penify-cli install-hook -l /path/to/git/repo
To uninstall the Git post-commit hook:

```bash
penify-cli uninstall-hook -l /path/to/git/repo
penifycli uninstall-hook -l /path/to/git/repo
```

- `-l, --location`: The path to the Git repository from which you want to uninstall the hook.
Expand All @@ -49,7 +49,7 @@ penify-cli uninstall-hook -l /path/to/git/repo
To generate documentation for files or folders:

```bash
penify-cli doc-gen [options]
penifycli doc-gen [options]
```

Options:
Expand All @@ -62,13 +62,27 @@ Options:
To commit code with an automatically generated commit message:

```bash
penify-cli commit -gf /path/to/git/repo [-m "Optional message"] [-e True/False]
penifycli commit -gf /path/to/git/repo [-m "Optional message"] [-e True/False]
```

- `-gf, --git_folder_path`: Path to the Git repository. Defaults to the current directory.
- `-m, --message`: Optional commit message. If not provided, a default message will be used.
- `-e, --terminal`: Set to "True" to open the terminal for editing the commit message. Defaults to "False".

### JIRA Integration

To integrate with JIRA and automate issue tracking:

```bash
penifycli jira [options]
```

Options:
- `-u, --url`: JIRA instance URL.
- `-p, --project`: JIRA project key.
- `-i, --issue`: JIRA issue key.
- `-a, --assignee`: Assignee for the JIRA issue.

## Authentication

Penify CLI uses an API token for authentication. The token is obtained and used in the following priority:
Expand Down Expand Up @@ -111,7 +125,7 @@ Contributions are welcome! Please feel free to submit a Pull Request.

## Issues

If you encounter any problems or have suggestions, please file an issue on the [GitHub repository](https://github.com/SingularityX-ai/penify-cli/issues).
If you encounter any problems or have suggestions, please file an issue on the [GitHub repository](https://github.com/SingularityX-ai/penifycli/issues).

## Support

Expand Down
Empty file added penify_hook/__init__.py
Empty file.
96 changes: 45 additions & 51 deletions penify_hook/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,40 +45,48 @@ def send_file_for_docstring_generation(self, file_name, content, line_numbers, r
print(f"Error: {response.text}")
return content

def generate_commit_summary(self, git_diff, instruction: str = "", repo_details = None):
"""Send file content and modified lines to the API and return modified
content.
def generate_commit_summary(self, git_diff, instruction: str = "", repo_details = None, jira_context: dict = None):
"""Generate a commit summary by sending a POST request to the API endpoint.

This function constructs a payload containing the file path, content,
and modified line numbers, and sends it to a specified API endpoint for
processing. It handles the response from the API, returning the modified
content if the request is successful. If the request fails, it logs the
error details and returns the original content.
This function constructs a payload containing the git diff and any
additional instructions provided. It then sends this payload to a
specified API endpoint to generate a summary of the commit. If the
request is successful, it returns the response from the API; otherwise,
it returns None.

Args:
file_name (str): The path to the file being sent.
content (str): The content of the file to be processed.
line_numbers (list): A list of line numbers that have been modified.
git_diff (str): The git diff of the commit.
instruction (str?): Additional instruction for the commit. Defaults to "".
repo_details (dict?): Details of the git repository. Defaults to None.
jira_context (dict?): JIRA issue details to enhance the commit summary. Defaults to None.

Returns:
str: The modified content returned by the API, or the original content if the
request fails.
dict: The response from the API if the request is successful, None otherwise.
"""
payload = {
'git_diff': git_diff,
'additional_instruction': instruction
}
if repo_details:
payload['git_repo'] = repo_details

# Add JIRA context if available
if jira_context:
payload['jira_context'] = jira_context

url = self.api_url+"/v1/hook/commit/summary"
response = requests.post(url, json=payload,headers={"api-key": f"{self.AUTH_TOKEN}"}, timeout=60*10)
if response.status_code == 200:
response = response.json()
return response
else:
print(f"Response: {response.status_code}")
print(f"Error: {response.text}")
try:
response = requests.post(url, json=payload, headers
={"api-key": f"{self.AUTH_TOKEN}"}, timeout=60*10)
if response.status_code == 200:
response = response.json()
return response
else:
print(f"Response: {response.status_code}")
print(f"Error: {response.text}")
return None
except Exception as e:
print(f"Error: {e}")
return None

def get_supported_file_types(self) -> list[str]:
Expand All @@ -100,40 +108,27 @@ def get_supported_file_types(self) -> list[str]:
return response
else:
return ["py", "js", "ts", "java", "kt", "cs", "c"]

def generate_commit_summary(self, git_diff, instruction: str = "", repo_details = None):
"""Generate a commit summary by sending a POST request to the API endpoint.

This function constructs a payload containing the git diff and any
additional instructions provided. It then sends this payload to a
specified API endpoint to generate a summary of the commit. If the
request is successful, it returns the response from the API; otherwise,
it returns None.

def generate_commit_summary_with_llm(self, diff, message, repo_details, llm_client, jira_context=None):
"""
Generate a commit summary using a local LLM client instead of the API.

Args:
git_diff (str): The git diff of the commit.
instruction (str?): Additional instruction for the commit. Defaults to "".
repo_details (dict?): Details of the git repository. Defaults to None.

diff: Git diff of changes
message: User-provided commit message or instructions
repo_details: Details about the repository
llm_client: Instance of LLMClient
jira_context: Optional JIRA issue context to enhance the summary

Returns:
dict: The response from the API if the request is successful, None otherwise.
Dict with title and description for the commit
"""
payload = {
'git_diff': git_diff,
'additional_instruction': instruction
}
if repo_details:
payload['git_repo'] = repo_details

url = self.api_url+"/v1/hook/commit/summary"
response = requests.post(url, json=payload,headers={"api-key": f"{self.AUTH_TOKEN}"}, timeout=60*10)
if response.status_code == 200:
response = response.json()
return response
else:
print(f"Response: {response.status_code}")
print(f"Error: {response.text}")
return None
try:
return llm_client.generate_commit_summary(diff, message, repo_details, jira_context)
except Exception as e:
print(f"Error using local LLM: {e}")
# Fall back to API for commit summary
return self.generate_commit_summary(diff, message, repo_details, jira_context)

def get_api_key(self):

Expand All @@ -147,4 +142,3 @@ def get_api_key(self):
print(f"Error: {response.text}")
return None


Empty file.
98 changes: 90 additions & 8 deletions penify_hook/commit_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
import re
import subprocess
import tempfile
from typing import Optional
from typing import Optional, List
from git import Repo
from tqdm import tqdm
from .api_client import APIClient

class CommitDocGenHook:
def __init__(self, repo_path: str, api_client: APIClient):
def __init__(self, repo_path: str, api_client: APIClient, llm_client=None, jira_client=None):
self.repo_path = repo_path
self.api_client = 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.repo = Repo(repo_path)
self.supported_file_types = set(self.api_client.get_supported_file_types())
self.repo_details = self.get_repo_details()
Expand Down Expand Up @@ -87,7 +89,8 @@ def get_summary(self, instruction: str):
This function retrieves the differences of the staged changes in the
repository and generates a commit summary using the provided
instruction. If there are no changes staged for commit, an exception is
raised.
raised. If an LLM client is provided, it will use that for generating
the summary, otherwise it will use the API client.

Args:
instruction (str): A string containing instructions for generating the commit summary.
Expand All @@ -99,11 +102,32 @@ def get_summary(self, instruction: str):
Raises:
Exception: If there are no changes staged for commit.
"""

diff = self.repo.git.diff('--cached')
if not diff:
raise Exception("No changes to commit")
return self.api_client.generate_commit_summary(diff, instruction, self.repo_details)
raise ValueError("No changes to commit")

# Get JIRA context if available
jira_context = None
if self.jira_client and self.jira_client.is_connected():
try:
# Check branch name for JIRA issues
current_branch = self.repo.active_branch.name
issue_keys = self.jira_client.extract_issue_keys_from_branch(current_branch)

# 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
if self.llm_client:
return self.api_client.generate_commit_summary_with_llm(
diff, instruction, self.repo_details, self.llm_client, jira_context
)
else:
return self.api_client.generate_commit_summary(diff, instruction, self.repo_details, jira_context)


def run(self, msg: Optional[str], edit_commit_message: bool):
Expand All @@ -127,17 +151,76 @@ def run(self, msg: Optional[str], edit_commit_message: bool):
summary: dict = self.get_summary(msg)
if not summary:
raise Exception("Error generating commit summary")

title = summary.get('title', "")
description = summary.get('description', "")


# 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)

# commit the changes to the repository with above details
commit_msg = f"{title}\n\n{description}"
self.repo.git.commit('-m', commit_msg)

if edit_commit_message:
# Open the git commit edit terminal
print("Opening git commit edit terminal...")
self._amend_commit()

def process_jira_integration(self, title: str, description: str, msg: str) -> tuple:
"""
Process JIRA integration for the commit message.

Args:
title: Generated commit title
description: Generated commit description
msg: Original user message that might contain JIRA references

Returns:
tuple: (updated_title, updated_description) with JIRA information
"""
# Look for JIRA issue keys in commit message, title, description and user message
issue_keys = []
if self.jira_client:
# Extract from message content
issue_keys = self.jira_client.extract_issue_keys(f"{title} {description} {msg}")

# Also check the branch name (which often follows JIRA naming conventions)
try:
current_branch = self.repo.active_branch.name
branch_issue_keys = self.jira_client.extract_issue_keys_from_branch(current_branch)

# Add any new keys found in branch name
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}")
except Exception as e:
print(f"Could not extract JIRA issues from branch name: {e}")

if issue_keys:
print(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")

return title, description

def _amend_commit(self):
"""Open the default git editor for editing the commit message.
Expand All @@ -160,4 +243,3 @@ def _amend_commit(self):
finally:
# Change back to the original directory
os.chdir(os.path.dirname(os.path.abspath(__file__)))

Loading