Last active
May 4, 2024 00:02
-
-
Save Konfekt/d9640c390deea4beafcf74e29410b8a6 to your computer and use it in GitHub Desktop.
let ChatGPT write a sensible commit message using an adaptable Python script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Adaption of https://github.com/tom-doerr/chatgpt_commit_message_hook/main/prepare-commit-msg | |
# | |
# Mark this file as executable and add it into the global hooks folder | |
# whose path is given by the core.hooksPath configuration variable | |
# skip during rebase | |
import sys | |
if len(sys.argv) > 2: | |
sys.exit(0) | |
# first check whether repo is a submodule | |
import os | |
import locale | |
git_dir_path = ".git" | |
if os.path.isfile(".git"): | |
with open(".git", encoding=locale.getpreferredencoding(False)) as f: | |
git_dir = f.read().split(" ")[1].strip() | |
git_dir_path = git_dir | |
# skip during rebase or merges | |
if ( | |
os.path.exists(git_dir_path + "/rebase-merge") | |
or os.path.exists(git_dir_path + "/rebase-apply") | |
or os.path.exists(git_dir_path + "/MERGE_HEAD") | |
or os.path.exists(git_dir_path + "/CHERRY_PICK_HEAD") | |
): | |
print("chatgpt-write-msg: skipping because rebase or merge is happening") | |
sys.exit(0) | |
import platform | |
sys.stdin = open("CON" if platform.system() == "Windows" else "/dev/tty", | |
encoding=locale.getpreferredencoding(False)) | |
user_input = input("Let ChatGPT write commit message? (y/N) ").strip().lower() | |
if user_input != "y": | |
print("chatgpt-write-msg: exiting as user requested") | |
sys.exit(0) | |
max_changed_lines = 80 | |
model = os.getenv("OPENAI_MODEL") | |
if model is None: | |
print("chatgpt-write-msg: exiting because OpenAI Model missing") | |
sys.exit(1) | |
api_key = os.getenv("OPENAI_KEY") | |
if api_key is None: | |
print("chatgpt-write-msg: exiting because OpenAI Key missing") | |
sys.exit(1) | |
# Attempt to establish a TCP connection to www.openai.com on port 80 (HTTP) | |
# Handle the case where the connection attempt times out | |
import socket | |
timeout = 3.0 | |
socket.setdefaulttimeout(timeout) | |
try: | |
connection = socket.create_connection(("www.openai.com", 80)) | |
connection.close() # Close the connection once established | |
except socket.timeout: | |
print("chatgpt-write-msg: exiting because OpenAI connection timed out") | |
sys.exit(1) | |
except socket.gaierror: | |
# Handle the case where the address of the server could not be resolved | |
print("chatgpt-write-msg: exiting because OpenAI could not be reached") | |
sys.exit(1) | |
except Exception as e: | |
print(f"chatgpt-write-msg: An error occurred: {e}") | |
sys.exit(1) | |
COMMIT_MSG_FILE = sys.argv[1] | |
import sys | |
# Counts the number of staged changes in a given git repository | |
def count_staged_changes(repo_path="."): | |
from git import GitCommandError, InvalidGitRepositoryError, NoSuchPathError, Repo | |
try: | |
# Initialize the repository object | |
repo = Repo(repo_path) | |
try: | |
# Attempt to get the current head commit | |
head_commit = repo.head.commit | |
except ValueError: | |
# Handle cases where HEAD is not available | |
print("HEAD does not exist in this repository. Diffing against the empty tree ...") | |
head_commit = None | |
# Access the repository index (staging area) | |
index = repo.index | |
# Diff staged changes against HEAD or an empty tree if HEAD is not available | |
if head_commit: | |
staged_diff = index.diff("HEAD", create_patch=True) | |
else: | |
staged_diff = index.diff("4b825dc642cb6eb9a060e54bf8d69288fbee4904", create_patch=True) | |
except (InvalidGitRepositoryError, NoSuchPathError): | |
# Handle invalid or nonexistent repository path | |
print(f"The path {repo_path} does not appear to be a valid git repository.") | |
sys.exit(1) | |
except GitCommandError as e: | |
# Handle errors from Git commands | |
print(f"A Git command error occurred: {e}") | |
sys.exit(1) | |
# Process the diff to count added and deleted lines per file | |
changes_dict = {} | |
for diff in staged_diff: | |
diff_text = diff.diff.decode("utf-8") | |
added_lines = sum(1 for line in diff_text.splitlines() if line.startswith("+")) | |
deleted_lines = sum(1 for line in diff_text.splitlines() if line.startswith("-")) | |
changes_dict[diff.b_path] = (added_lines, deleted_lines, diff_text) | |
return changes_dict | |
# Generates a summary of staged changes, showing a full diff if the total changed lines are below a threshold | |
def get_staged_changes_summary(changes_dict, n): | |
# Calculate the total number of changed lines | |
total_changed_lines = sum(added_lines + deleted_lines for _, (added_lines, deleted_lines, _) in changes_dict.items()) | |
# Return full diff if total changed lines are less than n | |
if total_changed_lines < n: | |
return get_full_diff(changes_dict) | |
# Otherwise, generate a summary of changes | |
summary = "" | |
for file_path, (added_lines, deleted_lines, _) in changes_dict.items(): | |
summary += f"{file_path} | {added_lines + deleted_lines} " | |
if added_lines > 0: | |
summary += "+" * added_lines | |
if deleted_lines > 0: | |
summary += "-" * deleted_lines | |
summary += "\n" | |
return summary | |
# Returns the full diff of all staged changes | |
def get_full_diff(changes_dict): | |
full_diff = "" | |
import re | |
# Construct the full diff text for each file | |
for diff_path, (_, _, diff_text) in changes_dict.items(): | |
full_diff += f"diff --git a/{diff_path} b/{diff_path}:\n" | |
full_diff += "\n".join(filter(lambda line: re.match(r'^[+\-]', line), diff_text.split("\n"))) | |
full_diff += "\n" | |
return full_diff | |
# Fetches a response from OpenAI's Chat API based on a given prompt | |
def get_openai_chat_response(prompt, model, api_key, proxy_server=None): | |
import requests | |
url = "https://api.openai.com/v1/chat/completions" | |
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"} | |
data = {"model": model, "messages": prompt} | |
# Send the request, optionally through a proxy | |
response = requests.post(url, headers=headers, json=data, proxies={'http': proxy_server, 'https': proxy_server} if proxy_server else None) | |
# Handle successful response | |
if response.status_code == 200: | |
return response.json()["choices"][0]["message"]["content"] | |
# Handle errors | |
print("Error:", response.text) | |
sys.exit(1) | |
current_commit_file_content = open(COMMIT_MSG_FILE, encoding=locale.getpreferredencoding(False)).read() | |
changes_dict = count_staged_changes() | |
summary = get_staged_changes_summary(changes_dict, max_changed_lines) | |
messages = [ | |
{ | |
"role": "system", | |
"content": | |
""" | |
Your output is a brief commit message. | |
Omit enclosing code fence markers ``` and ```. | |
In the first line of your output, please summarize in imperative mood the purpose of the following diff, which problem it solves and how, using at most 72 characters. | |
Then add a blank line. | |
State succinctly in simple english the problem that was solved. | |
Describe its solution in imperative mood. | |
Ensure that each line of your output has at most 72 characters. | |
Your input is a diff: | |
""", | |
}, | |
{ | |
"role": "user", | |
"content": f"{current_commit_file_content}\n\n", | |
}, | |
] | |
print("Waiting for ChatGPT...") | |
response_text = get_openai_chat_response( | |
prompt=messages, | |
model=model, | |
api_key=api_key, | |
) | |
print("... done") | |
content_whole_file = response_text + "\n\n" + summary | |
with open(COMMIT_MSG_FILE, "w", encoding=locale.getpreferredencoding(False)) as f: | |
f.write(content_whole_file) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment