gptdiff 0.1.21__py3-none-any.whl → 0.1.24__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gptdiff/applydiff.py +264 -0
- gptdiff/gptdiff.py +131 -309
- gptdiff/gptpatch.py +9 -7
- {gptdiff-0.1.21.dist-info → gptdiff-0.1.24.dist-info}/METADATA +1 -1
- gptdiff-0.1.24.dist-info/RECORD +10 -0
- gptdiff-0.1.21.dist-info/RECORD +0 -9
- {gptdiff-0.1.21.dist-info → gptdiff-0.1.24.dist-info}/LICENSE.txt +0 -0
- {gptdiff-0.1.21.dist-info → gptdiff-0.1.24.dist-info}/WHEEL +0 -0
- {gptdiff-0.1.21.dist-info → gptdiff-0.1.24.dist-info}/entry_points.txt +0 -0
- {gptdiff-0.1.21.dist-info → gptdiff-0.1.24.dist-info}/top_level.txt +0 -0
gptdiff/applydiff.py
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
"""
|
2
|
+
Module: applydiff
|
3
|
+
|
4
|
+
Contains the function to apply unified git diffs to files on disk.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from pathlib import Path
|
8
|
+
import re
|
9
|
+
import hashlib
|
10
|
+
|
11
|
+
def apply_diff(project_dir, diff_text):
|
12
|
+
"""
|
13
|
+
Applies a unified diff (as generated by git diff) to the files in project_dir
|
14
|
+
using pure Python (without calling the external 'patch' command).
|
15
|
+
|
16
|
+
Handles file modifications, new file creation, and file deletions.
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
True if at least one file was modified (or deleted/created) as a result of the patch,
|
20
|
+
False otherwise.
|
21
|
+
"""
|
22
|
+
from pathlib import Path
|
23
|
+
import re, hashlib
|
24
|
+
|
25
|
+
def file_hash(filepath):
|
26
|
+
h = hashlib.sha256()
|
27
|
+
with open(filepath, "rb") as f:
|
28
|
+
h.update(f.read())
|
29
|
+
return h.hexdigest()
|
30
|
+
|
31
|
+
def apply_patch_to_file(file_path, patch):
|
32
|
+
"""
|
33
|
+
Applies a unified diff patch (for a single file) to file_path.
|
34
|
+
|
35
|
+
Returns True if the patch was applied successfully, False otherwise.
|
36
|
+
"""
|
37
|
+
# Read the original file lines; if the file doesn't exist, treat it as empty.
|
38
|
+
if file_path.exists():
|
39
|
+
original_lines = file_path.read_text(encoding="utf8").splitlines(keepends=True)
|
40
|
+
else:
|
41
|
+
original_lines = []
|
42
|
+
new_lines = []
|
43
|
+
current_index = 0
|
44
|
+
|
45
|
+
patch_lines = patch.splitlines()
|
46
|
+
# Regex for a hunk header, e.g., @@ -3,7 +3,6 @@
|
47
|
+
hunk_header_re = re.compile(r"^@@(?: -(\d+)(?:,(\d+))?)?(?: \+(\d+)(?:,(\d+))?)? @@")
|
48
|
+
i = 0
|
49
|
+
while i < len(patch_lines):
|
50
|
+
line = patch_lines[i]
|
51
|
+
if line.lstrip().startswith("@@"):
|
52
|
+
if line.strip() == "@@":
|
53
|
+
# Handle minimal hunk header without line numbers.
|
54
|
+
orig_start = 1
|
55
|
+
else:
|
56
|
+
m = hunk_header_re.match(line.strip())
|
57
|
+
if not m:
|
58
|
+
print("Invalid hunk header:", line)
|
59
|
+
return False
|
60
|
+
orig_start = int(m.group(1)) if m.group(1) is not None else 1
|
61
|
+
hunk_start_index = orig_start - 1 # diff headers are 1-indexed
|
62
|
+
if hunk_start_index > len(original_lines):
|
63
|
+
print("Hunk start index beyond file length")
|
64
|
+
return False
|
65
|
+
new_lines.extend(original_lines[current_index:hunk_start_index])
|
66
|
+
current_index = hunk_start_index
|
67
|
+
i += 1
|
68
|
+
# Process the hunk lines until the next hunk header.
|
69
|
+
while i < len(patch_lines) and not patch_lines[i].startswith("@@"):
|
70
|
+
pline = patch_lines[i]
|
71
|
+
if pline.startswith(" "):
|
72
|
+
# Context line must match exactly.
|
73
|
+
expected = pline[1:]
|
74
|
+
if current_index >= len(original_lines):
|
75
|
+
print("Context line expected but file ended")
|
76
|
+
return False
|
77
|
+
orig_line = original_lines[current_index].rstrip("\n")
|
78
|
+
if orig_line != expected:
|
79
|
+
print("Context line mismatch. Expected:", expected, "Got:", orig_line)
|
80
|
+
return False
|
81
|
+
new_lines.append(original_lines[current_index])
|
82
|
+
current_index += 1
|
83
|
+
elif pline.startswith("-"):
|
84
|
+
# Removal line: verify and skip from original.
|
85
|
+
expected = pline[1:]
|
86
|
+
if current_index >= len(original_lines):
|
87
|
+
print("Removal line expected but file ended")
|
88
|
+
return False
|
89
|
+
orig_line = original_lines[current_index].rstrip("\n")
|
90
|
+
if orig_line != expected:
|
91
|
+
print("Removal line mismatch. Expected:", expected, "Got:", orig_line)
|
92
|
+
return False
|
93
|
+
current_index += 1
|
94
|
+
elif pline.startswith("+"):
|
95
|
+
# Addition line: add to new_lines.
|
96
|
+
new_lines.append(pline[1:] + "\n")
|
97
|
+
else:
|
98
|
+
print("Unexpected line in hunk:", pline)
|
99
|
+
return False
|
100
|
+
i += 1
|
101
|
+
else:
|
102
|
+
# Skip non-hunk header lines.
|
103
|
+
i += 1
|
104
|
+
|
105
|
+
# Append any remaining lines from the original file.
|
106
|
+
new_lines.extend(original_lines[current_index:])
|
107
|
+
# Ensure parent directories exist before writing the file.
|
108
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
109
|
+
# Write the new content back to the file.
|
110
|
+
file_path.write_text("".join(new_lines), encoding="utf8")
|
111
|
+
return True
|
112
|
+
|
113
|
+
# Parse the diff into per-file patches.
|
114
|
+
file_patches = parse_diff_per_file(diff_text)
|
115
|
+
if not file_patches:
|
116
|
+
print("No file patches found in diff.")
|
117
|
+
return False
|
118
|
+
|
119
|
+
# Record original file hashes.
|
120
|
+
original_hashes = {}
|
121
|
+
for file_path, _ in file_patches:
|
122
|
+
target_file = Path(project_dir) / file_path
|
123
|
+
if target_file.exists():
|
124
|
+
original_hashes[file_path] = file_hash(target_file)
|
125
|
+
else:
|
126
|
+
original_hashes[file_path] = None
|
127
|
+
|
128
|
+
any_change = False
|
129
|
+
# Process each file patch.
|
130
|
+
for file_path, patch in file_patches:
|
131
|
+
target_file = Path(project_dir) / file_path
|
132
|
+
if "+++ /dev/null" in patch:
|
133
|
+
# Deletion patch: delete the file if it exists.
|
134
|
+
if target_file.exists():
|
135
|
+
target_file.unlink()
|
136
|
+
if not target_file.exists():
|
137
|
+
any_change = True
|
138
|
+
else:
|
139
|
+
print(f"Failed to delete file: {target_file}")
|
140
|
+
return False
|
141
|
+
else:
|
142
|
+
# Modification or new file creation.
|
143
|
+
success = apply_patch_to_file(target_file, patch)
|
144
|
+
if not success:
|
145
|
+
print(f"Failed to apply patch to file: {target_file}")
|
146
|
+
return False
|
147
|
+
|
148
|
+
# Verify that at least one file was changed by comparing hashes.
|
149
|
+
for file_path, patch in file_patches:
|
150
|
+
target_file = Path(project_dir) / file_path
|
151
|
+
if "+++ /dev/null" in patch:
|
152
|
+
if not target_file.exists():
|
153
|
+
any_change = True
|
154
|
+
else:
|
155
|
+
print(f"Expected deletion but file still exists: {target_file}")
|
156
|
+
return False
|
157
|
+
else:
|
158
|
+
old_hash = original_hashes.get(file_path)
|
159
|
+
if target_file.exists():
|
160
|
+
new_hash = file_hash(target_file)
|
161
|
+
if old_hash != new_hash:
|
162
|
+
any_change = True
|
163
|
+
else:
|
164
|
+
print(f"No change detected in file: {target_file}")
|
165
|
+
else:
|
166
|
+
print(f"Expected modification or creation but file is missing: {target_file}")
|
167
|
+
return False
|
168
|
+
|
169
|
+
if not any_change:
|
170
|
+
print("Patch applied but no file modifications detected.")
|
171
|
+
return False
|
172
|
+
return True
|
173
|
+
|
174
|
+
def parse_diff_per_file(diff_text):
|
175
|
+
"""Parse unified diff text into individual file patches.
|
176
|
+
|
177
|
+
Splits a multi-file diff into per-file entries for processing. Handles:
|
178
|
+
- File creations (+++ /dev/null)
|
179
|
+
- File deletions (--- /dev/null)
|
180
|
+
- Standard modifications
|
181
|
+
|
182
|
+
Args:
|
183
|
+
diff_text: Unified diff string as generated by `git diff`
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
List of tuples (file_path, patch) where:
|
187
|
+
- file_path: Relative path to modified file
|
188
|
+
- patch: Full diff fragment for this file
|
189
|
+
|
190
|
+
Note:
|
191
|
+
Uses 'b/' prefix detection from git diffs to determine target paths
|
192
|
+
"""
|
193
|
+
header_re = re.compile(r'^(?:diff --git\s+)?(a/[^ ]+)\s+(b/[^ ]+)\s*$', re.MULTILINE)
|
194
|
+
lines = diff_text.splitlines()
|
195
|
+
|
196
|
+
# Check if any header line exists.
|
197
|
+
if not any(header_re.match(line) for line in lines):
|
198
|
+
# Fallback strategy: detect file headers starting with '--- a/' or '-- a/'
|
199
|
+
diffs = []
|
200
|
+
current_lines = []
|
201
|
+
current_file = None
|
202
|
+
deletion_mode = False
|
203
|
+
header_line_re = re.compile(r'^-{2,3}\s+a/(.+)$')
|
204
|
+
|
205
|
+
for line in lines:
|
206
|
+
if header_line_re.match(line):
|
207
|
+
if current_file is not None and current_lines:
|
208
|
+
if deletion_mode and not any(l.startswith("+++ /dev/null") for l in current_lines):
|
209
|
+
current_lines.append("+++ /dev/null")
|
210
|
+
diffs.append((current_file, "\n".join(current_lines)))
|
211
|
+
current_lines = [line]
|
212
|
+
deletion_mode = False
|
213
|
+
file_from = header_line_re.match(line).group(1).strip()
|
214
|
+
current_file = file_from
|
215
|
+
else:
|
216
|
+
current_lines.append(line)
|
217
|
+
if "deleted file mode" in line:
|
218
|
+
deletion_mode = True
|
219
|
+
if line.startswith("+++ "):
|
220
|
+
parts = line.split()
|
221
|
+
if len(parts) >= 2:
|
222
|
+
file_to = parts[1].strip()
|
223
|
+
if file_to != "/dev/null":
|
224
|
+
current_file = file_to[2:] if (file_to.startswith("a/") or file_to.startswith("b/")) else file_to
|
225
|
+
if current_file is not None and current_lines:
|
226
|
+
if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
|
227
|
+
current_lines.append("+++ /dev/null")
|
228
|
+
diffs.append((current_file, "\n".join(current_lines)))
|
229
|
+
return diffs
|
230
|
+
else:
|
231
|
+
# Use header-based strategy.
|
232
|
+
diffs = []
|
233
|
+
current_lines = []
|
234
|
+
current_file = None
|
235
|
+
deletion_mode = False
|
236
|
+
for line in lines:
|
237
|
+
m = header_re.match(line)
|
238
|
+
if m:
|
239
|
+
if current_file is not None and current_lines:
|
240
|
+
if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
|
241
|
+
current_lines.append("+++ /dev/null")
|
242
|
+
diffs.append((current_file, "\n".join(current_lines)))
|
243
|
+
current_lines = [line]
|
244
|
+
deletion_mode = False
|
245
|
+
file_from = m.group(1) # e.g. "a/index.html"
|
246
|
+
file_to = m.group(2) # e.g. "b/index.html"
|
247
|
+
current_file = file_to[2:] if file_to.startswith("b/") else file_to
|
248
|
+
else:
|
249
|
+
current_lines.append(line)
|
250
|
+
if "deleted file mode" in line:
|
251
|
+
deletion_mode = True
|
252
|
+
if line.startswith("+++ "):
|
253
|
+
parts = line.split()
|
254
|
+
if len(parts) >= 2:
|
255
|
+
file_to = parts[1].strip()
|
256
|
+
if file_to != "/dev/null":
|
257
|
+
current_file = file_to[2:] if (file_to.startswith("a/") or file_to.startswith("b/")) else file_to
|
258
|
+
if current_file is not None and current_lines:
|
259
|
+
if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
|
260
|
+
current_lines.append("+++ /dev/null")
|
261
|
+
diffs.append((current_file, "\n".join(current_lines)))
|
262
|
+
return diffs
|
263
|
+
|
264
|
+
|
gptdiff/gptdiff.py
CHANGED
@@ -3,29 +3,38 @@ from pathlib import Path
|
|
3
3
|
import subprocess
|
4
4
|
import hashlib
|
5
5
|
import re
|
6
|
-
|
6
|
+
import time
|
7
|
+
import os
|
8
|
+
import json
|
9
|
+
import subprocess
|
10
|
+
import sys
|
11
|
+
import fnmatch
|
12
|
+
import argparse
|
13
|
+
import pkgutil
|
14
|
+
import contextvars
|
15
|
+
from pkgutil import get_data
|
16
|
+
import threading
|
7
17
|
|
8
18
|
import openai
|
9
19
|
from openai import OpenAI
|
10
|
-
|
11
20
|
import tiktoken
|
12
21
|
import time
|
13
|
-
|
14
22
|
import os
|
15
23
|
import json
|
16
24
|
import subprocess
|
17
|
-
from pathlib import Path
|
18
25
|
import sys
|
19
26
|
import fnmatch
|
20
27
|
import argparse
|
21
28
|
import pkgutil
|
22
|
-
import re
|
23
29
|
import contextvars
|
24
|
-
from ai_agent_toolbox import MarkdownParser, MarkdownPromptFormatter, Toolbox, FlatXMLParser, FlatXMLPromptFormatter
|
25
|
-
import threading
|
26
30
|
from pkgutil import get_data
|
31
|
+
import threading
|
32
|
+
from ai_agent_toolbox import MarkdownParser, MarkdownPromptFormatter, Toolbox, FlatXMLParser, FlatXMLPromptFormatter
|
33
|
+
from .applydiff import apply_diff, parse_diff_per_file
|
27
34
|
|
35
|
+
VERBOSE = False
|
28
36
|
diff_context = contextvars.ContextVar('diffcontent', default="")
|
37
|
+
|
29
38
|
def create_diff_toolbox():
|
30
39
|
toolbox = Toolbox()
|
31
40
|
|
@@ -97,7 +106,9 @@ def color_code_diff(diff_text: str) -> str:
|
|
97
106
|
|
98
107
|
def load_gitignore_patterns(gitignore_path):
|
99
108
|
with open(gitignore_path, 'r') as f:
|
100
|
-
patterns = [
|
109
|
+
patterns = [
|
110
|
+
line.strip() for line in f if line.strip() and not line.startswith('#')
|
111
|
+
]
|
101
112
|
return patterns
|
102
113
|
|
103
114
|
def is_ignored(filepath, gitignore_patterns):
|
@@ -165,7 +176,7 @@ def load_project_files(project_dir, cwd):
|
|
165
176
|
Prints skipped files to stdout for visibility
|
166
177
|
"""
|
167
178
|
ignore_paths = [Path(cwd) / ".gitignore", Path(cwd) / ".gptignore"]
|
168
|
-
gitignore_patterns = [".gitignore", "diff.patch", "prompt.txt", ".gptignore", "*.pdf", "*.docx", ".git", "*.orig", "*.rej", "*.diff"]
|
179
|
+
gitignore_patterns = [".gitignore", "diff.patch", "prompt.txt", ".*", ".gptignore", "*.pdf", "*.docx", ".git", "*.orig", "*.rej", "*.diff"]
|
169
180
|
|
170
181
|
for p in ignore_paths:
|
171
182
|
if p.exists():
|
@@ -175,14 +186,15 @@ def load_project_files(project_dir, cwd):
|
|
175
186
|
project_files = []
|
176
187
|
for file in list_files_and_dirs(project_dir, gitignore_patterns):
|
177
188
|
if os.path.isfile(file):
|
178
|
-
|
179
|
-
|
180
|
-
|
189
|
+
try:
|
190
|
+
with open(file, 'r') as f:
|
191
|
+
content = f.read()
|
192
|
+
if VERBOSE:
|
181
193
|
print(file)
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
194
|
+
project_files.append((file, content))
|
195
|
+
except UnicodeDecodeError:
|
196
|
+
print(f"Skipping file {file} due to UnicodeDecodeError")
|
197
|
+
continue
|
186
198
|
|
187
199
|
print("")
|
188
200
|
return project_files
|
@@ -194,37 +206,54 @@ def load_prepend_file(file):
|
|
194
206
|
# Function to call GPT-4 API and calculate the cost
|
195
207
|
def call_llm_for_diff(system_prompt, user_prompt, files_content, model, temperature=0.7, max_tokens=30000, api_key=None, base_url=None):
|
196
208
|
enc = tiktoken.get_encoding("o200k_base")
|
209
|
+
|
210
|
+
# Use colors in print statements
|
211
|
+
red = "\033[91m"
|
212
|
+
green = "\033[92m"
|
213
|
+
reset = "\033[0m"
|
197
214
|
start_time = time.time()
|
198
215
|
|
199
216
|
parser = MarkdownParser()
|
200
217
|
formatter = MarkdownPromptFormatter()
|
201
218
|
toolbox = create_diff_toolbox()
|
202
219
|
tool_prompt = formatter.usage_prompt(toolbox)
|
203
|
-
system_prompt += "\n"+tool_prompt
|
220
|
+
system_prompt += "\n" + tool_prompt
|
204
221
|
|
205
222
|
if 'gemini' in model:
|
206
|
-
user_prompt = system_prompt+"\n"+user_prompt
|
223
|
+
user_prompt = system_prompt + "\n" + user_prompt
|
207
224
|
|
208
225
|
messages = [
|
209
|
-
{"role": "system", "content": system_prompt},
|
210
|
-
{"role": "user", "content": user_prompt + "\n"+files_content},
|
226
|
+
{"role": "system", "content": f"{green}{system_prompt}{reset}"},
|
227
|
+
{"role": "user", "content": user_prompt + "\n" + files_content},
|
211
228
|
]
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
229
|
+
if VERBOSE:
|
230
|
+
print(f"{green}Using {model}{reset}")
|
231
|
+
print(f"{green}SYSTEM PROMPT{reset}")
|
232
|
+
print(system_prompt)
|
233
|
+
print(f"{green}USER PROMPT{reset}")
|
234
|
+
print(user_prompt, "+", len(enc.encode(files_content)), "tokens of file content")
|
235
|
+
else:
|
236
|
+
print("Generating diff...")
|
217
237
|
|
218
|
-
if api_key
|
238
|
+
if not api_key:
|
219
239
|
api_key = os.getenv('GPTDIFF_LLM_API_KEY')
|
220
|
-
if base_url
|
240
|
+
if not base_url:
|
221
241
|
base_url = os.getenv('GPTDIFF_LLM_BASE_URL', "https://nano-gpt.com/api/v1/")
|
242
|
+
base_url = base_url or "https://nano-gpt.com/api/v1/"
|
243
|
+
|
222
244
|
client = OpenAI(api_key=api_key, base_url=base_url)
|
223
245
|
response = client.chat.completions.create(model=model,
|
224
246
|
messages=messages,
|
225
247
|
max_tokens=max_tokens,
|
226
248
|
temperature=temperature)
|
227
249
|
|
250
|
+
if VERBOSE:
|
251
|
+
print("RESPONSE RAW-------------")
|
252
|
+
print(response.choices[0].message.content.strip())
|
253
|
+
print("/RESPONSE RAW-------------")
|
254
|
+
else:
|
255
|
+
print("Diff generated.")
|
256
|
+
|
228
257
|
prompt_tokens = response.usage.prompt_tokens
|
229
258
|
completion_tokens = response.usage.completion_tokens
|
230
259
|
total_tokens = response.usage.total_tokens
|
@@ -345,7 +374,8 @@ def smartapply(diff_text, files, model=None, api_key=None, base_url=None):
|
|
345
374
|
del files[path]
|
346
375
|
else:
|
347
376
|
updated = call_llm_for_apply_with_think_tool_available(path, original, patch, model, api_key=api_key, base_url=base_url)
|
348
|
-
|
377
|
+
cleaned = strip_bad_output(updated, original)
|
378
|
+
files[path] = cleaned
|
349
379
|
|
350
380
|
threads = []
|
351
381
|
|
@@ -360,172 +390,6 @@ def smartapply(diff_text, files, model=None, api_key=None, base_url=None):
|
|
360
390
|
|
361
391
|
return files
|
362
392
|
|
363
|
-
def apply_diff(project_dir, diff_text):
|
364
|
-
"""
|
365
|
-
Applies a unified diff (as generated by git diff) to the files in project_dir
|
366
|
-
using pure Python (without calling the external 'patch' command).
|
367
|
-
|
368
|
-
Handles file modifications, new file creation, and file deletions.
|
369
|
-
|
370
|
-
Returns:
|
371
|
-
True if at least one file was modified (or deleted/created) as a result of the patch,
|
372
|
-
False otherwise.
|
373
|
-
"""
|
374
|
-
from pathlib import Path
|
375
|
-
import re, hashlib
|
376
|
-
|
377
|
-
def file_hash(filepath):
|
378
|
-
h = hashlib.sha256()
|
379
|
-
with open(filepath, "rb") as f:
|
380
|
-
h.update(f.read())
|
381
|
-
return h.hexdigest()
|
382
|
-
|
383
|
-
def apply_patch_to_file(file_path, patch):
|
384
|
-
"""
|
385
|
-
Applies a unified diff patch (for a single file) to file_path.
|
386
|
-
|
387
|
-
Returns True if the patch was applied successfully, False otherwise.
|
388
|
-
"""
|
389
|
-
# Read the original file lines; if the file doesn't exist, treat it as empty.
|
390
|
-
if file_path.exists():
|
391
|
-
original_lines = file_path.read_text(encoding="utf8").splitlines(keepends=True)
|
392
|
-
else:
|
393
|
-
original_lines = []
|
394
|
-
new_lines = []
|
395
|
-
current_index = 0
|
396
|
-
|
397
|
-
patch_lines = patch.splitlines()
|
398
|
-
# Regex for a hunk header, e.g., @@ -3,7 +3,6 @@
|
399
|
-
hunk_header_re = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
|
400
|
-
i = 0
|
401
|
-
while i < len(patch_lines):
|
402
|
-
line = patch_lines[i]
|
403
|
-
if line.startswith("@@"):
|
404
|
-
m = hunk_header_re.match(line)
|
405
|
-
if not m:
|
406
|
-
print("Invalid hunk header:", line)
|
407
|
-
return False
|
408
|
-
orig_start = int(m.group(1))
|
409
|
-
# orig_len = int(m.group(2)) if m.group(2) else 1 # not used explicitly
|
410
|
-
# new_start = int(m.group(3))
|
411
|
-
# new_len = int(m.group(4)) if m.group(4) else 1
|
412
|
-
|
413
|
-
# Copy unchanged lines before the hunk.
|
414
|
-
hunk_start_index = orig_start - 1 # diff headers are 1-indexed
|
415
|
-
if hunk_start_index > len(original_lines):
|
416
|
-
print("Hunk start index beyond file length")
|
417
|
-
return False
|
418
|
-
new_lines.extend(original_lines[current_index:hunk_start_index])
|
419
|
-
current_index = hunk_start_index
|
420
|
-
|
421
|
-
i += 1
|
422
|
-
# Process the hunk lines until the next hunk header.
|
423
|
-
while i < len(patch_lines) and not patch_lines[i].startswith("@@"):
|
424
|
-
pline = patch_lines[i]
|
425
|
-
if pline.startswith(" "):
|
426
|
-
# Context line must match exactly.
|
427
|
-
expected = pline[1:]
|
428
|
-
if current_index >= len(original_lines):
|
429
|
-
print("Context line expected but file ended")
|
430
|
-
return False
|
431
|
-
orig_line = original_lines[current_index].rstrip("\n")
|
432
|
-
if orig_line != expected:
|
433
|
-
print("Context line mismatch. Expected:", expected, "Got:", orig_line)
|
434
|
-
return False
|
435
|
-
new_lines.append(original_lines[current_index])
|
436
|
-
current_index += 1
|
437
|
-
elif pline.startswith("-"):
|
438
|
-
# Removal line: verify and skip from original.
|
439
|
-
expected = pline[1:]
|
440
|
-
if current_index >= len(original_lines):
|
441
|
-
print("Removal line expected but file ended")
|
442
|
-
return False
|
443
|
-
orig_line = original_lines[current_index].rstrip("\n")
|
444
|
-
if orig_line != expected:
|
445
|
-
print("Removal line mismatch. Expected:", expected, "Got:", orig_line)
|
446
|
-
return False
|
447
|
-
current_index += 1
|
448
|
-
elif pline.startswith("+"):
|
449
|
-
# Addition line: add to new_lines.
|
450
|
-
new_lines.append(pline[1:] + "\n")
|
451
|
-
else:
|
452
|
-
print("Unexpected line in hunk:", pline)
|
453
|
-
return False
|
454
|
-
i += 1
|
455
|
-
else:
|
456
|
-
# Skip non-hunk header lines.
|
457
|
-
i += 1
|
458
|
-
|
459
|
-
# Append any remaining lines from the original file.
|
460
|
-
new_lines.extend(original_lines[current_index:])
|
461
|
-
# Ensure parent directories exist before writing the file.
|
462
|
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
463
|
-
# Write the new content back to the file.
|
464
|
-
file_path.write_text("".join(new_lines), encoding="utf8")
|
465
|
-
return True
|
466
|
-
|
467
|
-
# Parse the diff into per-file patches.
|
468
|
-
file_patches = parse_diff_per_file(diff_text)
|
469
|
-
if not file_patches:
|
470
|
-
print("No file patches found in diff.")
|
471
|
-
return False
|
472
|
-
|
473
|
-
# Record original file hashes.
|
474
|
-
original_hashes = {}
|
475
|
-
for file_path, _ in file_patches:
|
476
|
-
target_file = Path(project_dir) / file_path
|
477
|
-
if target_file.exists():
|
478
|
-
original_hashes[file_path] = file_hash(target_file)
|
479
|
-
else:
|
480
|
-
original_hashes[file_path] = None
|
481
|
-
|
482
|
-
any_change = False
|
483
|
-
# Process each file patch.
|
484
|
-
for file_path, patch in file_patches:
|
485
|
-
target_file = Path(project_dir) / file_path
|
486
|
-
if "+++ /dev/null" in patch:
|
487
|
-
# Deletion patch: delete the file if it exists.
|
488
|
-
if target_file.exists():
|
489
|
-
target_file.unlink()
|
490
|
-
if not target_file.exists():
|
491
|
-
any_change = True
|
492
|
-
else:
|
493
|
-
print(f"Failed to delete file: {target_file}")
|
494
|
-
return False
|
495
|
-
else:
|
496
|
-
# Modification or new file creation.
|
497
|
-
success = apply_patch_to_file(target_file, patch)
|
498
|
-
if not success:
|
499
|
-
print(f"Failed to apply patch to file: {target_file}")
|
500
|
-
return False
|
501
|
-
|
502
|
-
# Verify that at least one file was changed by comparing hashes.
|
503
|
-
for file_path, patch in file_patches:
|
504
|
-
target_file = Path(project_dir) / file_path
|
505
|
-
if "+++ /dev/null" in patch:
|
506
|
-
if not target_file.exists():
|
507
|
-
any_change = True
|
508
|
-
else:
|
509
|
-
print(f"Expected deletion but file still exists: {target_file}")
|
510
|
-
return False
|
511
|
-
else:
|
512
|
-
old_hash = original_hashes.get(file_path)
|
513
|
-
if target_file.exists():
|
514
|
-
new_hash = file_hash(target_file)
|
515
|
-
if old_hash != new_hash:
|
516
|
-
any_change = True
|
517
|
-
else:
|
518
|
-
print(f"No change detected in file: {target_file}")
|
519
|
-
else:
|
520
|
-
print(f"Expected modification or creation but file is missing: {target_file}")
|
521
|
-
return False
|
522
|
-
|
523
|
-
if not any_change:
|
524
|
-
print("Patch applied but no file modifications detected.")
|
525
|
-
return False
|
526
|
-
return True
|
527
|
-
|
528
|
-
|
529
393
|
def parse_arguments():
|
530
394
|
parser = argparse.ArgumentParser(description='Generate and optionally apply git diffs using GPT-4.')
|
531
395
|
parser.add_argument('prompt', type=str, help='Prompt that runs on the codebase.')
|
@@ -541,9 +405,8 @@ def parse_arguments():
|
|
541
405
|
parser.add_argument('--max_tokens', type=int, default=30000, help='Temperature parameter for model creativity (0.0 to 2.0)')
|
542
406
|
parser.add_argument('--model', type=str, default=None, help='Model to use for the API call.')
|
543
407
|
parser.add_argument('--applymodel', type=str, default=None, help='Model to use for applying the diff. Defaults to the value of --model if not specified.')
|
544
|
-
|
545
408
|
parser.add_argument('--nowarn', action='store_true', help='Disable large token warning')
|
546
|
-
|
409
|
+
parser.add_argument('--verbose', action='store_true', help='Enable verbose output with detailed information')
|
547
410
|
return parser.parse_args()
|
548
411
|
|
549
412
|
def absolute_to_relative(absolute_path):
|
@@ -551,95 +414,8 @@ def absolute_to_relative(absolute_path):
|
|
551
414
|
relative_path = os.path.relpath(absolute_path, cwd)
|
552
415
|
return relative_path
|
553
416
|
|
554
|
-
def
|
555
|
-
""
|
556
|
-
|
557
|
-
Splits a multi-file diff into per-file entries for processing. Handles:
|
558
|
-
- File creations (+++ /dev/null)
|
559
|
-
- File deletions (--- /dev/null)
|
560
|
-
- Standard modifications
|
561
|
-
|
562
|
-
Args:
|
563
|
-
diff_text: Unified diff string as generated by `git diff`
|
564
|
-
|
565
|
-
Returns:
|
566
|
-
List of tuples (file_path, patch) where:
|
567
|
-
- file_path: Relative path to modified file
|
568
|
-
- patch: Full diff fragment for this file
|
569
|
-
|
570
|
-
Note:
|
571
|
-
Uses 'b/' prefix detection from git diffs to determine target paths
|
572
|
-
"""
|
573
|
-
header_re = re.compile(r'^(?:diff --git\s+)?(a/[^ ]+)\s+(b/[^ ]+)\s*$', re.MULTILINE)
|
574
|
-
lines = diff_text.splitlines()
|
575
|
-
|
576
|
-
# Check if any header line exists.
|
577
|
-
if not any(header_re.match(line) for line in lines):
|
578
|
-
# Fallback strategy: detect file headers starting with '--- a/' or '-- a/'
|
579
|
-
diffs = []
|
580
|
-
current_lines = []
|
581
|
-
current_file = None
|
582
|
-
deletion_mode = False
|
583
|
-
header_line_re = re.compile(r'^-{2,3}\s+a/(.+)$')
|
584
|
-
|
585
|
-
for line in lines:
|
586
|
-
if header_line_re.match(line):
|
587
|
-
if current_file is not None and current_lines:
|
588
|
-
if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
|
589
|
-
current_lines.append("+++ /dev/null")
|
590
|
-
diffs.append((current_file, "\n".join(current_lines)))
|
591
|
-
current_lines = [line]
|
592
|
-
deletion_mode = False
|
593
|
-
file_from = header_line_re.match(line).group(1).strip()
|
594
|
-
current_file = file_from
|
595
|
-
else:
|
596
|
-
current_lines.append(line)
|
597
|
-
if "deleted file mode" in line:
|
598
|
-
deletion_mode = True
|
599
|
-
if line.startswith("+++ "):
|
600
|
-
parts = line.split()
|
601
|
-
if len(parts) >= 2:
|
602
|
-
file_to = parts[1].strip()
|
603
|
-
if file_to != "/dev/null":
|
604
|
-
current_file = file_to[2:] if (file_to.startswith("a/") or file_to.startswith("b/")) else file_to
|
605
|
-
if current_file is not None and current_lines:
|
606
|
-
if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
|
607
|
-
current_lines.append("+++ /dev/null")
|
608
|
-
diffs.append((current_file, "\n".join(current_lines)))
|
609
|
-
return diffs
|
610
|
-
else:
|
611
|
-
# Use header-based strategy.
|
612
|
-
diffs = []
|
613
|
-
current_lines = []
|
614
|
-
current_file = None
|
615
|
-
deletion_mode = False
|
616
|
-
for line in lines:
|
617
|
-
m = header_re.match(line)
|
618
|
-
if m:
|
619
|
-
if current_file is not None and current_lines:
|
620
|
-
if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
|
621
|
-
current_lines.append("+++ /dev/null")
|
622
|
-
diffs.append((current_file, "\n".join(current_lines)))
|
623
|
-
current_lines = [line]
|
624
|
-
deletion_mode = False
|
625
|
-
file_from = m.group(1) # e.g. "a/index.html"
|
626
|
-
file_to = m.group(2) # e.g. "b/index.html"
|
627
|
-
current_file = file_to[2:] if file_to.startswith("b/") else file_to
|
628
|
-
else:
|
629
|
-
current_lines.append(line)
|
630
|
-
if "deleted file mode" in line:
|
631
|
-
deletion_mode = True
|
632
|
-
if line.startswith("+++ "):
|
633
|
-
parts = line.split()
|
634
|
-
if len(parts) >= 2:
|
635
|
-
file_to = parts[1].strip()
|
636
|
-
if file_to != "/dev/null":
|
637
|
-
current_file = file_to[2:] if (file_to.startswith("a/") or file_to.startswith("b/")) else file_to
|
638
|
-
if current_file is not None and current_lines:
|
639
|
-
if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
|
640
|
-
current_lines.append("+++ /dev/null")
|
641
|
-
diffs.append((current_file, "\n".join(current_lines)))
|
642
|
-
return diffs
|
417
|
+
def colorize_warning_warning(message):
|
418
|
+
return f"\033[91m\033[1m{message}\033[0m"
|
643
419
|
|
644
420
|
def call_llm_for_apply_with_think_tool_available(file_path, original_content, file_diff, model, api_key=None, base_url=None, extra_prompt=None, max_tokens=30000):
|
645
421
|
parser = FlatXMLParser("think")
|
@@ -719,9 +495,9 @@ Diff to apply:
|
|
719
495
|
{"role": "system", "content": system_prompt},
|
720
496
|
{"role": "user", "content": user_prompt},
|
721
497
|
]
|
722
|
-
if api_key
|
498
|
+
if not api_key:
|
723
499
|
api_key = os.getenv('GPTDIFF_LLM_API_KEY')
|
724
|
-
if base_url
|
500
|
+
if not base_url:
|
725
501
|
base_url = os.getenv('GPTDIFF_LLM_BASE_URL', "https://nano-gpt.com/api/v1/")
|
726
502
|
client = OpenAI(api_key=api_key, base_url=base_url)
|
727
503
|
start_time = time.time()
|
@@ -733,8 +509,11 @@ Diff to apply:
|
|
733
509
|
elapsed = time.time() - start_time
|
734
510
|
minutes, seconds = divmod(int(elapsed), 60)
|
735
511
|
time_str = f"{minutes}m {seconds}s" if minutes else f"{seconds}s"
|
736
|
-
|
737
|
-
|
512
|
+
if VERBOSE:
|
513
|
+
print(f"Smartapply time: {time_str}")
|
514
|
+
print("-" * 40)
|
515
|
+
else:
|
516
|
+
print(f"Smartapply completed in {time_str}")
|
738
517
|
return full_response
|
739
518
|
|
740
519
|
def build_environment_from_filelist(file_list, cwd):
|
@@ -762,7 +541,7 @@ def smart_apply_patch(project_dir, diff_text, user_prompt, args):
|
|
762
541
|
parsed_diffs = parse_diff_per_file(diff_text)
|
763
542
|
print("Found", len(parsed_diffs), "files in diff, processing smart apply concurrently:")
|
764
543
|
if len(parsed_diffs) == 0:
|
765
|
-
print("
|
544
|
+
print(colorize_warning_warning("There were no entries in this diff. The LLM may have returned something invalid."))
|
766
545
|
if args.beep:
|
767
546
|
print("\a")
|
768
547
|
return
|
@@ -770,32 +549,50 @@ def smart_apply_patch(project_dir, diff_text, user_prompt, args):
|
|
770
549
|
|
771
550
|
def process_file(file_path, file_diff):
|
772
551
|
full_path = Path(project_dir) / file_path
|
773
|
-
|
552
|
+
if VERBOSE:
|
553
|
+
print(f"Processing file: {file_path}")
|
774
554
|
if '+++ /dev/null' in file_diff:
|
775
555
|
if full_path.exists():
|
776
556
|
full_path.unlink()
|
777
557
|
print(f"\033[1;32mDeleted file {file_path}.\033[0m")
|
778
558
|
else:
|
779
|
-
print(f"
|
559
|
+
print(colorize_warning_warning(f"File {file_path} not found - skipping deletion"))
|
780
560
|
return
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
if
|
791
|
-
|
561
|
+
|
562
|
+
try:
|
563
|
+
original_content = full_path.read_text()
|
564
|
+
except (UnicodeDecodeError, IOError):
|
565
|
+
print(f"Skipping file {file_path} due to read error")
|
566
|
+
return
|
567
|
+
|
568
|
+
# Use SMARTAPPLY-specific environment variables if set, otherwise fallback.
|
569
|
+
smart_apply_model = os.getenv("GPTDIFF_SMARTAPPLY_MODEL")
|
570
|
+
if smart_apply_model and smart_apply_model.strip():
|
571
|
+
model = smart_apply_model
|
572
|
+
elif hasattr(args, "applymodel") and args.applymodel:
|
573
|
+
model = args.applymodel
|
574
|
+
else:
|
575
|
+
model = os.getenv("GPTDIFF_MODEL", "deepseek-reasoner")
|
576
|
+
|
577
|
+
smart_api_key = os.getenv("GPTDIFF_SMARTAPPLY_API_KEY")
|
578
|
+
if smart_api_key and smart_api_key.strip():
|
579
|
+
api_key = smart_api_key
|
580
|
+
else:
|
581
|
+
api_key = os.getenv("GPTDIFF_LLM_API_KEY")
|
582
|
+
|
583
|
+
smart_base_url = os.getenv("GPTDIFF_SMARTAPPLY_BASE_URL")
|
584
|
+
if smart_base_url and smart_base_url.strip():
|
585
|
+
base_url = smart_base_url
|
586
|
+
else:
|
587
|
+
base_url = os.getenv("GPTDIFF_LLM_BASE_URL", "https://nano-gpt.com/api/v1/")
|
792
588
|
|
793
589
|
print("-" * 40)
|
794
|
-
print("Running smartapply with",
|
590
|
+
print("Running smartapply with", model, "on", file_path)
|
795
591
|
print("-" * 40)
|
796
592
|
try:
|
797
593
|
updated_content = call_llm_for_apply_with_think_tool_available(
|
798
|
-
file_path, original_content, file_diff,
|
594
|
+
file_path, original_content, file_diff, model,
|
595
|
+
api_key=api_key, base_url=base_url,
|
799
596
|
extra_prompt=f"This changeset is from the following instructions:\n{user_prompt}",
|
800
597
|
max_tokens=args.max_tokens)
|
801
598
|
if updated_content.strip() == "":
|
@@ -844,11 +641,13 @@ def save_files(files_dict, target_directory):
|
|
844
641
|
print(f"Saved: {full_path}")
|
845
642
|
|
846
643
|
def main():
|
644
|
+
global VERBOSE
|
847
645
|
# Adding color support for Windows CMD
|
848
646
|
if os.name == 'nt':
|
849
647
|
os.system('color')
|
850
648
|
|
851
649
|
args = parse_arguments()
|
650
|
+
VERBOSE = args.verbose
|
852
651
|
|
853
652
|
# openai.api_base = "https://nano-gpt.com/api/v1/"
|
854
653
|
if len(sys.argv) < 2:
|
@@ -898,9 +697,8 @@ def main():
|
|
898
697
|
|
899
698
|
files_content = ""
|
900
699
|
for file, content in project_files:
|
901
|
-
|
902
|
-
|
903
|
-
# Prepare the prompt for GPT-4
|
700
|
+
if VERBOSE:
|
701
|
+
print(f"Including {len(enc.encode(content)):5d} tokens", absolute_to_relative(file))
|
904
702
|
files_content += f"File: {absolute_to_relative(file)}\nContent:\n{content}\n"
|
905
703
|
|
906
704
|
full_prompt = f"{system_prompt}\n\n{user_prompt}\n\n{files_content}"
|
@@ -1003,5 +801,29 @@ def swallow_reasoning(full_response: str) -> (str, str):
|
|
1003
801
|
final_content = full_response.strip()
|
1004
802
|
return final_content, reasoning
|
1005
803
|
|
804
|
+
def strip_bad_output(updated: str, original: str) -> str:
|
805
|
+
"""
|
806
|
+
If the original file content does not start with a code fence but the LLM’s updated output
|
807
|
+
starts with triple backticks (possibly with an introductory message), extract and return only
|
808
|
+
the content within the first code block.
|
809
|
+
"""
|
810
|
+
updated_stripped = updated.strip()
|
811
|
+
# If the original file does not start with a code fence, but the updated output contains a code block,
|
812
|
+
# extract and return only the content inside the first code block.
|
813
|
+
if not original.lstrip().startswith("```"):
|
814
|
+
# Search for the first code block in the updated output.
|
815
|
+
m = re.search(r"```(.*?)```", updated_stripped, re.DOTALL)
|
816
|
+
if m:
|
817
|
+
content = m.group(1).strip()
|
818
|
+
lines = content.splitlines()
|
819
|
+
if len(lines) > 1:
|
820
|
+
first_line = lines[0].strip()
|
821
|
+
# If the first line appears to be a language specifier (i.e., a single word)
|
822
|
+
# and is not "diff", then drop it.
|
823
|
+
if " " not in first_line and first_line.lower() != "diff":
|
824
|
+
content = "\n".join(lines[1:]).strip()
|
825
|
+
return content
|
826
|
+
return updated_stripped
|
827
|
+
|
1006
828
|
if __name__ == "__main__":
|
1007
|
-
main()
|
829
|
+
main()
|
gptdiff/gptpatch.py
CHANGED
@@ -13,8 +13,7 @@ This tool uses the same patch-application logic as gptdiff.
|
|
13
13
|
import sys
|
14
14
|
import argparse
|
15
15
|
from pathlib import Path
|
16
|
-
from gptdiff.gptdiff import apply_diff
|
17
|
-
|
16
|
+
from gptdiff.gptdiff import apply_diff, smart_apply_patch
|
18
17
|
|
19
18
|
def parse_arguments():
|
20
19
|
parser = argparse.ArgumentParser(
|
@@ -50,6 +49,7 @@ def parse_arguments():
|
|
50
49
|
default=30000,
|
51
50
|
help="Maximum tokens to use for LLM responses"
|
52
51
|
)
|
52
|
+
parser.add_argument('--dumb', action='store_true', default=False, help='Attempt dumb apply before trying smart apply')
|
53
53
|
return parser.parse_args()
|
54
54
|
|
55
55
|
def main():
|
@@ -64,12 +64,14 @@ def main():
|
|
64
64
|
diff_text = diff_path.read_text(encoding="utf8")
|
65
65
|
|
66
66
|
project_dir = args.project_dir
|
67
|
-
|
68
|
-
|
69
|
-
|
67
|
+
if args.dumb:
|
68
|
+
success = apply_diff(project_dir, diff_text)
|
69
|
+
if success:
|
70
|
+
print("✅ Diff applied successfully.")
|
71
|
+
else:
|
72
|
+
print("❌ Failed to apply diff using git apply. Attempting smart apply.")
|
73
|
+
smart_apply_patch(project_dir, diff_text, "", args)
|
70
74
|
else:
|
71
|
-
print("❌ Failed to apply diff using git apply. Attempting smart apply.")
|
72
|
-
from gptdiff.gptdiff import smart_apply_patch
|
73
75
|
smart_apply_patch(project_dir, diff_text, "", args)
|
74
76
|
|
75
77
|
if __name__ == "__main__":
|
@@ -0,0 +1,10 @@
|
|
1
|
+
gptdiff/__init__.py,sha256=o1hrK4GFvbfKcHPlLVArz4OunE3euIicEBYaLrdDo0k,198
|
2
|
+
gptdiff/applydiff.py,sha256=nvTerBtFuXWf1j6nisGY7CQ6qJCIM8J9UHxgoiWReoY,11116
|
3
|
+
gptdiff/gptdiff.py,sha256=XN05Zbr1H69_iG8Bx8RQ34vTXXg3WHDANRcGo3ihrhA,31518
|
4
|
+
gptdiff/gptpatch.py,sha256=opakY6j_I05ZNx2ACYgxB8SxoZ3POf9iFxDkV5Yn1oU,2393
|
5
|
+
gptdiff-0.1.24.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
|
6
|
+
gptdiff-0.1.24.dist-info/METADATA,sha256=TE_nbtNX0IMjDek5MHxvDDUaAeVhCXw7p5kCmh0TpZg,8785
|
7
|
+
gptdiff-0.1.24.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
8
|
+
gptdiff-0.1.24.dist-info/entry_points.txt,sha256=0VlVNr-gc04a3SZD5_qKIBbtg_L5P2x3xlKE5ftcdkc,82
|
9
|
+
gptdiff-0.1.24.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
|
10
|
+
gptdiff-0.1.24.dist-info/RECORD,,
|
gptdiff-0.1.21.dist-info/RECORD
DELETED
@@ -1,9 +0,0 @@
|
|
1
|
-
gptdiff/__init__.py,sha256=o1hrK4GFvbfKcHPlLVArz4OunE3euIicEBYaLrdDo0k,198
|
2
|
-
gptdiff/gptdiff.py,sha256=kDp7gDgBydfKxNm73QIT54AKnv117cZdXhRYQnfJm6A,39426
|
3
|
-
gptdiff/gptpatch.py,sha256=Z8CWWIfIL2o7xPLVdhzN5GSyJq0vsK4XQRzu4hMWNQk,2194
|
4
|
-
gptdiff-0.1.21.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
|
5
|
-
gptdiff-0.1.21.dist-info/METADATA,sha256=Y5O4deytuqvxRV4WaK2vAw9jFuz0OdR3Rxm3lIBNxHk,8785
|
6
|
-
gptdiff-0.1.21.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
7
|
-
gptdiff-0.1.21.dist-info/entry_points.txt,sha256=0VlVNr-gc04a3SZD5_qKIBbtg_L5P2x3xlKE5ftcdkc,82
|
8
|
-
gptdiff-0.1.21.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
|
9
|
-
gptdiff-0.1.21.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|