Skip to content

Instantly share code, notes, and snippets.

@Konfekt
Last active April 28, 2024 05:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Konfekt/d9640c390deea4beafcf74e29410b8a6 to your computer and use it in GitHub Desktop.
Save Konfekt/d9640c390deea4beafcf74e29410b8a6 to your computer and use it in GitHub Desktop.
let ChatGPT write a sensible commit message using an adaptable Python script
#!/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