gptdiff 0.1.11__py3-none-any.whl → 0.1.14__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- gptdiff/gptdiff.py +81 -68
- gptdiff/gptpatch.py +76 -0
- {gptdiff-0.1.11.dist-info → gptdiff-0.1.14.dist-info}/METADATA +1 -1
- gptdiff-0.1.14.dist-info/RECORD +9 -0
- {gptdiff-0.1.11.dist-info → gptdiff-0.1.14.dist-info}/entry_points.txt +1 -0
- gptdiff-0.1.11.dist-info/RECORD +0 -8
- {gptdiff-0.1.11.dist-info → gptdiff-0.1.14.dist-info}/LICENSE.txt +0 -0
- {gptdiff-0.1.11.dist-info → gptdiff-0.1.14.dist-info}/WHEEL +0 -0
- {gptdiff-0.1.11.dist-info → gptdiff-0.1.14.dist-info}/top_level.txt +0 -0
gptdiff/gptdiff.py
CHANGED
@@ -747,6 +747,69 @@ def build_environment_from_filelist(file_list, cwd):
|
|
747
747
|
continue
|
748
748
|
return build_environment(files_dict)
|
749
749
|
|
750
|
+
def smart_apply_patch(project_dir, diff_text, user_prompt, args):
|
751
|
+
"""
|
752
|
+
Attempt to apply a diff via smartapply: process each file concurrently using the LLM.
|
753
|
+
"""
|
754
|
+
from pathlib import Path
|
755
|
+
parsed_diffs = parse_diff_per_file(diff_text)
|
756
|
+
print("Found", len(parsed_diffs), "files in diff, processing smart apply concurrently:")
|
757
|
+
if len(parsed_diffs) == 0:
|
758
|
+
print("\033[1;33mThere were no entries in this diff. The LLM may have returned something invalid.\033[0m")
|
759
|
+
if args.beep:
|
760
|
+
print("\a")
|
761
|
+
return
|
762
|
+
threads = []
|
763
|
+
|
764
|
+
def process_file(file_path, file_diff):
|
765
|
+
full_path = Path(project_dir) / file_path
|
766
|
+
print(f"Processing file: {file_path}")
|
767
|
+
if '+++ /dev/null' in file_diff:
|
768
|
+
if full_path.exists():
|
769
|
+
full_path.unlink()
|
770
|
+
print(f"\033[1;32mDeleted file {file_path}.\033[0m")
|
771
|
+
else:
|
772
|
+
print(f"\033[1;33mFile {file_path} not found - skipping deletion\033[0m")
|
773
|
+
return
|
774
|
+
original_content = ''
|
775
|
+
if full_path.exists():
|
776
|
+
try:
|
777
|
+
original_content = full_path.read_text()
|
778
|
+
except UnicodeDecodeError:
|
779
|
+
print(f"Skipping binary file {file_path}")
|
780
|
+
return
|
781
|
+
if not hasattr(args, "applymodel") or args.applymodel is None:
|
782
|
+
args.applymodel = args.model
|
783
|
+
if args.applymodel is None:
|
784
|
+
args.applymodel = os.getenv("GPTDIFF_MODEL")
|
785
|
+
|
786
|
+
print("-" * 40)
|
787
|
+
print("Running smartapply with", args.applymodel,"on",file_path)
|
788
|
+
print("-" * 40)
|
789
|
+
try:
|
790
|
+
updated_content = call_llm_for_apply_with_think_tool_available(
|
791
|
+
file_path, original_content, file_diff, args.applymodel,
|
792
|
+
extra_prompt=f"This changeset is from the following instructions:\n{user_prompt}",
|
793
|
+
max_tokens=args.max_tokens)
|
794
|
+
if updated_content.strip() == "":
|
795
|
+
print("Cowardly refusing to write empty file to", file_path, "merge failed")
|
796
|
+
return
|
797
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
798
|
+
full_path.write_text(updated_content)
|
799
|
+
print(f"\033[1;32mSuccessful 'smartapply' update {file_path}.\033[0m")
|
800
|
+
except Exception as e:
|
801
|
+
print(f"\033[1;31mFailed to process {file_path}: {str(e)}\033[0m")
|
802
|
+
|
803
|
+
for file_path, file_diff in parsed_diffs:
|
804
|
+
thread = threading.Thread(target=process_file, args=(file_path, file_diff))
|
805
|
+
thread.start()
|
806
|
+
threads.append(thread)
|
807
|
+
for thread in threads:
|
808
|
+
thread.join()
|
809
|
+
|
810
|
+
if args.beep:
|
811
|
+
print("\a")
|
812
|
+
|
750
813
|
def main():
|
751
814
|
# Adding color support for Windows CMD
|
752
815
|
if os.name == 'nt':
|
@@ -754,7 +817,6 @@ def main():
|
|
754
817
|
|
755
818
|
args = parse_arguments()
|
756
819
|
|
757
|
-
# TODO: The 'openai.api_base' option isn't read in the client API. You will need to pass it when you instantiate the client, e.g. 'OpenAI(base_url="https://nano-gpt.com/api/v1/")'
|
758
820
|
# openai.api_base = "https://nano-gpt.com/api/v1/"
|
759
821
|
if len(sys.argv) < 2:
|
760
822
|
print("Usage: python script.py '<user_prompt>' [--apply]")
|
@@ -778,11 +840,24 @@ def main():
|
|
778
840
|
project_files.extend(load_project_files(additional_path, project_dir))
|
779
841
|
|
780
842
|
if args.prepend:
|
781
|
-
prepend =
|
782
|
-
print("Including prepend",len(enc.encode(json.dumps(prepend))), "tokens")
|
843
|
+
prepend = args.prepend
|
783
844
|
else:
|
784
845
|
prepend = ""
|
785
846
|
|
847
|
+
if prepend.startswith("http://") or prepend.startswith("https://"):
|
848
|
+
try:
|
849
|
+
import urllib.request
|
850
|
+
with urllib.request.urlopen(prepend) as response:
|
851
|
+
prepend = response.read().decode('utf-8')
|
852
|
+
except Exception as e:
|
853
|
+
print(f"Error fetching prepend content from URL {prepend}: {e}")
|
854
|
+
prepend = ""
|
855
|
+
elif os.path.exists(prepend):
|
856
|
+
prepend = load_prepend_file(prepend)
|
857
|
+
else:
|
858
|
+
# If the specified prepend path does not exist, treat the value as literal content.
|
859
|
+
prepend = prepend
|
860
|
+
|
786
861
|
# Prepare system prompt
|
787
862
|
system_prompt = prepend + f"Output a git diff into a <diff> block."
|
788
863
|
|
@@ -851,73 +926,11 @@ def main():
|
|
851
926
|
print("\n</diff>")
|
852
927
|
print("Saved to patch.diff")
|
853
928
|
if apply_diff(project_dir, diff_text):
|
854
|
-
print(f"\033[1;32mPatch applied successfully with 'git apply'.\033[0m")
|
929
|
+
print(f"\033[1;32mPatch applied successfully with 'git apply'.\033[0m")
|
855
930
|
else:
|
856
931
|
print("Apply failed, attempting smart apply.")
|
857
|
-
|
858
|
-
print("Found", len(parsed_diffs), " files in diff, calling smartdiff for each file concurrently:")
|
859
|
-
|
860
|
-
if(len(parsed_diffs) == 0):
|
861
|
-
print(f"\033[1;33mThere were no entries in this diff. The LLM may have returned something invalid.\033[0m")
|
862
|
-
if args.beep:
|
863
|
-
print("\a") # Terminal bell for completion notification
|
864
|
-
return
|
865
|
-
|
866
|
-
threads = []
|
867
|
-
|
868
|
-
def process_file(file_path, file_diff):
|
869
|
-
full_path = Path(project_dir) / file_path
|
870
|
-
print(f"Processing file: {file_path}")
|
871
|
-
|
872
|
-
# Handle file deletions from diff
|
873
|
-
if '+++ /dev/null' in file_diff:
|
874
|
-
if full_path.exists():
|
875
|
-
full_path.unlink()
|
876
|
-
print(f"\033[1;32mDeleted file {file_path}.\033[0m")
|
877
|
-
else:
|
878
|
-
print(f"\033[1;33mFile {file_path} not found - skipping deletion\033[0m")
|
879
|
-
return
|
880
|
-
|
881
|
-
original_content = ''
|
882
|
-
if full_path.exists():
|
883
|
-
try:
|
884
|
-
original_content = full_path.read_text()
|
885
|
-
except UnicodeDecodeError:
|
886
|
-
print(f"Skipping binary file {file_path}")
|
887
|
-
return
|
888
|
-
|
889
|
-
print("-" * 40)
|
890
|
-
print("SMARTAPPLY")
|
891
|
-
print(file_diff)
|
892
|
-
print("-" * 40)
|
893
|
-
if args.applymodel is None:
|
894
|
-
args.applymodel = args.model
|
895
|
-
|
896
|
-
try:
|
897
|
-
updated_content = call_llm_for_apply_with_think_tool_available(file_path, original_content, file_diff, args.applymodel, extra_prompt=f"This changeset is from the following instructions:\n{user_prompt}", max_tokens=args.max_tokens)
|
898
|
-
|
899
|
-
if updated_content.strip() == "":
|
900
|
-
print("Cowardly refusing to write empty file to", file_path, "merge failed")
|
901
|
-
return
|
902
|
-
|
903
|
-
full_path.parent.mkdir(parents=True, exist_ok=True)
|
904
|
-
full_path.write_text(updated_content)
|
905
|
-
print(f"\033[1;32mSuccessful 'smartapply' update {file_path}.\033[0m")
|
906
|
-
except Exception as e:
|
907
|
-
print(f"\033[1;31mFailed to process {file_path}: {str(e)}\033[0m")
|
908
|
-
|
909
|
-
threads = []
|
910
|
-
for file_path, file_diff in parsed_diffs:
|
911
|
-
thread = threading.Thread(
|
912
|
-
target=process_file,
|
913
|
-
args=(file_path, file_diff)
|
914
|
-
)
|
915
|
-
thread.start()
|
916
|
-
threads.append(thread)
|
917
|
-
for thread in threads:
|
918
|
-
thread.join()
|
932
|
+
smart_apply_patch(project_dir, diff_text, user_prompt, args)
|
919
933
|
|
920
|
-
|
921
934
|
if args.beep:
|
922
935
|
print("\a") # Terminal bell for completion notification
|
923
936
|
|
@@ -927,4 +940,4 @@ def main():
|
|
927
940
|
#print(f"Total cost: ${cost:.4f}")
|
928
941
|
|
929
942
|
if __name__ == "__main__":
|
930
|
-
main()
|
943
|
+
main()
|
gptdiff/gptpatch.py
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Command line tool to apply a unified diff directly to a file system.
|
4
|
+
|
5
|
+
Usage:
|
6
|
+
gptpatch --diff "<diff text>"
|
7
|
+
or
|
8
|
+
gptpatch path/to/diff.patch
|
9
|
+
|
10
|
+
This tool uses the same patch-application logic as gptdiff.
|
11
|
+
"""
|
12
|
+
|
13
|
+
import sys
|
14
|
+
import argparse
|
15
|
+
from pathlib import Path
|
16
|
+
from gptdiff.gptdiff import apply_diff
|
17
|
+
|
18
|
+
|
19
|
+
def parse_arguments():
|
20
|
+
parser = argparse.ArgumentParser(
|
21
|
+
description="Apply a unified diff to the file system using GPTDiff's patch logic."
|
22
|
+
)
|
23
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
24
|
+
group.add_argument(
|
25
|
+
"--diff",
|
26
|
+
type=str,
|
27
|
+
help="Unified diff text to apply (provide as a string)."
|
28
|
+
)
|
29
|
+
group.add_argument(
|
30
|
+
"diff_file",
|
31
|
+
nargs="?",
|
32
|
+
help="Path to a file containing the unified diff."
|
33
|
+
)
|
34
|
+
parser.add_argument(
|
35
|
+
"--project-dir",
|
36
|
+
type=str,
|
37
|
+
default=".",
|
38
|
+
help="Project directory where the diff should be applied (default: current directory)."
|
39
|
+
)
|
40
|
+
parser.add_argument('--nobeep', action='store_false', dest='beep', default=True, help='Disable completion notification beep')
|
41
|
+
parser.add_argument(
|
42
|
+
"--model",
|
43
|
+
type=str,
|
44
|
+
default=None,
|
45
|
+
help="Model to use for applying the diff"
|
46
|
+
)
|
47
|
+
parser.add_argument(
|
48
|
+
"--max_tokens",
|
49
|
+
type=int,
|
50
|
+
default=30000,
|
51
|
+
help="Maximum tokens to use for LLM responses"
|
52
|
+
)
|
53
|
+
return parser.parse_args()
|
54
|
+
|
55
|
+
def main():
|
56
|
+
args = parse_arguments()
|
57
|
+
if args.diff:
|
58
|
+
diff_text = args.diff
|
59
|
+
else:
|
60
|
+
diff_path = Path(args.diff_file)
|
61
|
+
if not diff_path.exists():
|
62
|
+
print(f"Error: Diff file '{args.diff_file}' does not exist.")
|
63
|
+
sys.exit(1)
|
64
|
+
diff_text = diff_path.read_text(encoding="utf8")
|
65
|
+
|
66
|
+
project_dir = args.project_dir
|
67
|
+
success = apply_diff(project_dir, diff_text)
|
68
|
+
if success:
|
69
|
+
print("✅ Diff applied successfully.")
|
70
|
+
else:
|
71
|
+
print("❌ Failed to apply diff using git apply. Attempting smart apply.")
|
72
|
+
from gptdiff.gptdiff import smart_apply_patch
|
73
|
+
smart_apply_patch(project_dir, diff_text, "", args)
|
74
|
+
|
75
|
+
if __name__ == "__main__":
|
76
|
+
main()
|
@@ -0,0 +1,9 @@
|
|
1
|
+
gptdiff/__init__.py,sha256=yGjgwv7tNvH1ZLPsQyoo1CxpTOl1iCAwwDBp-_17ksQ,89
|
2
|
+
gptdiff/gptdiff.py,sha256=JWdlnnMZPmDOsg6jD2QQandaKZhpNlk8lQEgs2KmmM0,36872
|
3
|
+
gptdiff/gptpatch.py,sha256=Z8CWWIfIL2o7xPLVdhzN5GSyJq0vsK4XQRzu4hMWNQk,2194
|
4
|
+
gptdiff-0.1.14.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
|
5
|
+
gptdiff-0.1.14.dist-info/METADATA,sha256=imeKq8Q_B_PS4S0lMFvWAkoPxqWCZZW6OciaEKDsA3Q,7799
|
6
|
+
gptdiff-0.1.14.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
7
|
+
gptdiff-0.1.14.dist-info/entry_points.txt,sha256=0VlVNr-gc04a3SZD5_qKIBbtg_L5P2x3xlKE5ftcdkc,82
|
8
|
+
gptdiff-0.1.14.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
|
9
|
+
gptdiff-0.1.14.dist-info/RECORD,,
|
gptdiff-0.1.11.dist-info/RECORD
DELETED
@@ -1,8 +0,0 @@
|
|
1
|
-
gptdiff/__init__.py,sha256=yGjgwv7tNvH1ZLPsQyoo1CxpTOl1iCAwwDBp-_17ksQ,89
|
2
|
-
gptdiff/gptdiff.py,sha256=HbnFkP1o5jQ-WIC99y-et_X6BeBsArtKqiFb2FG6X28,36694
|
3
|
-
gptdiff-0.1.11.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
|
4
|
-
gptdiff-0.1.11.dist-info/METADATA,sha256=pCTb2Mu4w0Y6HSAY5A8F8jwrJrLfq_SQw80PyQvRIXA,7799
|
5
|
-
gptdiff-0.1.11.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
6
|
-
gptdiff-0.1.11.dist-info/entry_points.txt,sha256=0yvXYEVAZFI-p32kQ4-h3qKVWS0a86jsM9FAwF89t9w,49
|
7
|
-
gptdiff-0.1.11.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
|
8
|
-
gptdiff-0.1.11.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|