-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathgpt-commit.py
executable file
·156 lines (126 loc) · 4.22 KB
/
gpt-commit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#!/usr/bin/env python3
import argparse
import asyncio
import os
import subprocess
import sys
import openai
DIFF_PROMPT = "Generate a succinct summary of the following code changes:"
COMMIT_MSG_PROMPT = (
"Using no more than 50 characters, "
"generate a descriptive commit message from these summaries:"
)
PROMPT_CUTOFF = 10000
openai.organization = os.getenv("OPENAI_ORG_ID")
openai.api_key = os.environ["OPENAI_API_KEY"]
def get_diff(ignore_whitespace=True):
arguments = [
"git",
"--no-pager",
"diff",
"--staged",
]
if ignore_whitespace:
arguments += [
"--ignore-space-change",
"--ignore-blank-lines",
]
diff_process = subprocess.run(arguments, capture_output=True, text=True)
diff_process.check_returncode()
return diff_process.stdout.strip()
def parse_diff(diff):
file_diffs = diff.split("\ndiff")
file_diffs = [file_diffs[0]] + [
"\ndiff" + file_diff for file_diff in file_diffs[1:]
]
chunked_file_diffs = []
for file_diff in file_diffs:
[head, *chunks] = file_diff.split("\n@@")
chunks = ["\n@@" + chunk for chunk in reversed(chunks)]
chunked_file_diffs.append((head, chunks))
return chunked_file_diffs
def assemble_diffs(parsed_diffs, cutoff):
"""
Create multiple well-formatted diff strings, each being shorter than cutoff
"""
assembled_diffs = [""]
def add_chunk(chunk):
if len(assembled_diffs[-1]) + len(chunk) <= cutoff:
assembled_diffs[-1] += "\n" + chunk
return True
else:
assembled_diffs.append(chunk)
return False
for head, chunks in parsed_diffs:
if not chunks:
add_chunk(head)
else:
add_chunk(head + chunks.pop())
while chunks:
if not add_chunk(chunks.pop()):
assembled_diffs[-1] = head + assembled_diffs[-1]
return assembled_diffs
async def complete(prompt):
completion_resp = await openai.ChatCompletion.acreate(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt[: PROMPT_CUTOFF + 100]}],
max_tokens=128,
)
completion = completion_resp.choices[0].message.content.strip()
return completion
async def summarize_diff(diff):
assert diff
return await complete(DIFF_PROMPT + "\n\n" + diff + "\n\n")
async def summarize_summaries(summaries):
assert summaries
return await complete(COMMIT_MSG_PROMPT + "\n\n" + summaries + "\n\n")
async def generate_commit_message(diff):
if not diff:
return "Fix whitespace"
assembled_diffs = assemble_diffs(parse_diff(diff), PROMPT_CUTOFF)
summaries = await asyncio.gather(
*[summarize_diff(diff) for diff in assembled_diffs]
)
return await summarize_summaries("\n".join(summaries))
def commit(message):
# will ignore message if diff is empty
return subprocess.run(["git", "commit", "--message", message, "--edit"]).returncode
def parse_args():
"""
Extract the CLI arguments from argparse
"""
parser = argparse.ArgumentParser(
description=(
"Generate a commit message for staged files and commit them. "
"Git will prompt you to edit the generated commit message."
)
)
parser.add_argument(
"-p",
"--print-message",
action="store_true",
default=False,
help="print message in place of performing commit",
)
return parser.parse_args()
async def main():
args = parse_args()
try:
if not get_diff(ignore_whitespace=False):
print(
"No changes staged. Use `git add` to stage files before invoking gpt-commit."
)
exit()
commit_message = await generate_commit_message(get_diff())
except UnicodeDecodeError:
print("gpt-commit does not support binary files", file=sys.stderr)
commit_message = (
"# gpt-commit does not support binary files. "
"Please enter a commit message manually or unstage any binary files."
)
if args.print_message:
print(commit_message)
else:
exit(commit(commit_message))
if __name__ == "__main__":
asyncio.run(main())