User Tools

Site Tools


python:gpt-patcher

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
python:gpt-patcher [2025/11/05 12:40] – created Wuffpython:gpt-patcher [2025/11/07 16:26] (current) Wuff
Line 1: Line 1:
 ====== GPT Patcher ====== ====== GPT Patcher ======
  
-ChatGPT can provide a unified patch as output for code related changes. These unified patches are in a format not understood by git or the patch command. They do not contain specific line numbers but context surrounding the changes. To avoid issues with GPT hallucinations, strict checks need to be performed.+ChatGPT can provide a unified patch as output for code related changes (project instructions or prompt example 'return diffs of the changes proposed for ease of applying them.'). These unified patches are in a format not understood by git or the patch command. They do not contain specific line numbers but context surrounding the changes. To avoid issues with GPT hallucinations, strict checks need to be performed.
  
 The script below can help apply these patches. The script below can help apply these patches.
-It accepts file, stdin, clipboard or interactive manual paste as source for the patch, parses it for the target filename, checks if it can apply the patch and outputs the newly patched file.+It accepts file, stdin, clipboard or interactive manual paste as source for the patch, parses it for the target filename or prompts if none found, checks if it can apply the patch and outputs the newly patched file.
 Command line option -i can replace the original file immediately, but creates backup files. Command line option -i can replace the original file immediately, but creates backup files.
 Command line option -t can be used to patch a different filename. Command line option -t can be used to patch a different filename.
Line 37: Line 37:
     else:     else:
         clip = pyperclip.paste().strip() if pyperclip else ""         clip = pyperclip.paste().strip() if pyperclip else ""
-        if clip and PATCH_BEGIN in clip and PATCH_END in clip:+        if clip:
             print("Using patch from clipboard.")             print("Using patch from clipboard.")
             data = clip             data = clip
         else:         else:
-            print("No valid patch detected in clipboard.")+            if pyperclip: 
 +                print("No valid patch detected in clipboard.")
             print("Please paste your patch below, then press Ctrl-D (Linux/macOS) or Ctrl-Z (Windows) when done:\n")             print("Please paste your patch below, then press Ctrl-D (Linux/macOS) or Ctrl-Z (Windows) when done:\n")
             try:             try:
Line 50: Line 51:
  
  
-def validate_patch_format(data: str): +def detect_patch_format(data: str): 
-    """Ensure patch contains Begin/End Patch and Update File headers.""" +    """Return (has_header, has_update_line) flags.""" 
-    if PATCH_BEGIN not in data or PATCH_END not in data+    has_header = PATCH_BEGIN in data and PATCH_END in data 
-        sys.exit("Error: Patch must contain '*** Begin Patch' and '*** End Patch'.") +    has_update_line = PATCH_UPDATE_FILE in data 
-    if PATCH_UPDATE_FILE not in data+    return has_header, has_update_line
-        sys.exit("Error: Patch must contain '*** Update File:' line.") +
-    return True+
  
  
-def parse_patch(data: str):+def parse_patch(data: str, filename_override=None):
     """Extract the target filename and list of hunks."""     """Extract the target filename and list of hunks."""
     match = re.search(r"\*\*\* Update File:\s*(.+)", data)     match = re.search(r"\*\*\* Update File:\s*(.+)", data)
-    if not match: +    filename = match.group(1).strip() if match else filename_override 
-        sys.exit("Error: Could not find file in patch header.") + 
-    filename = match.group(1).strip()+    if not filename
 +        filename = input("No target file specifiedPlease enter the filename to patch: ").strip() 
 +        if not filename: 
 +            sys.exit("Error: Target filename required.")
  
 +    # Extract hunks (between @@ markers)
     hunks = []     hunks = []
     current_hunk = []     current_hunk = []
     in_hunk = False     in_hunk = False
- 
     for line in data.splitlines():     for line in data.splitlines():
         if line.startswith("@@"):         if line.startswith("@@"):
Line 76: Line 78:
             current_hunk = []             current_hunk = []
             in_hunk = True             in_hunk = True
-        elif line.strip() == PATCH_END: 
-            if current_hunk: 
-                hunks.append(current_hunk) 
-            break 
         elif in_hunk:         elif in_hunk:
             current_hunk.append(line)             current_hunk.append(line)
 +    if current_hunk: 
 +        hunks.append(current_hunk)
     return filename, hunks     return filename, hunks
  
Line 114: Line 113:
     match = re.search(pattern, content)     match = re.search(pattern, content)
     if not match:     if not match:
-        # try without requiring both contexts (looser match)+        # Try looser match
         match = re.search(re.escape(old_block), content)         match = re.search(re.escape(old_block), content)
         if not match:         if not match:
Line 139: Line 138:
     old_block = "\n".join(minus_lines)     old_block = "\n".join(minus_lines)
     new_block = "\n".join(plus_lines)     new_block = "\n".join(plus_lines)
- 
-    # extract leading and trailing context if available 
     context_before = "\n".join(context_lines[:3]) if context_lines else None     context_before = "\n".join(context_lines[:3]) if context_lines else None
     context_after = "\n".join(context_lines[-3:]) if context_lines else None     context_after = "\n".join(context_lines[-3:]) if context_lines else None
Line 159: Line 156:
  
 def main(): def main():
-    parser = argparse.ArgumentParser(description="Apply ChatGPT unified diffs to files.")+    parser = argparse.ArgumentParser(description="Apply ChatGPT or raw unified diffs to files.")
     parser.add_argument("patchfile", nargs="?", help="File containing the patch (optional)")     parser.add_argument("patchfile", nargs="?", help="File containing the patch (optional)")
     parser.add_argument("-i", "--in-place", action="store_true", help="Replace original file (create backup)")     parser.add_argument("-i", "--in-place", action="store_true", help="Replace original file (create backup)")
Line 167: Line 164:
  
     data = read_patch_input(args.patchfile)     data = read_patch_input(args.patchfile)
-    validate_patch_format(data) +    has_headerhas_update_line detect_patch_format(data)
-    patch_filehunks parse_patch(data)+
  
-    target_file = args.target or patch_file+    # If header present but update line missing, prompt for file 
 +    patch_file, hunks = parse_patch(data, filename_override=args.target)
  
-    if not os.path.exists(target_file): +    if not os.path.exists(patch_file): 
-        sys.exit(f"Error: Target file '{target_file}' not found.")+        sys.exit(f"Error: Target file '{patch_file}' not found.")
  
-    result = apply_patch_to_file(target_file, hunks)+    result = apply_patch_to_file(patch_file, hunks)
  
     if args.in_place:     if args.in_place:
-        backup = make_backup(target_file+        backup = make_backup(patch_file
-        with open(target_file, "w", encoding="utf-8") as f:+        with open(patch_file, "w", encoding="utf-8") as f:
             f.write(result)             f.write(result)
-        print(f"Patched {target_file} (backup: {backup})")+        print(f"Patched {patch_file} (backup: {backup})")
     else:     else:
         sys.stdout.write(result)         sys.stdout.write(result)
Line 188: Line 185:
 if __name__ == "__main__": if __name__ == "__main__":
     main()     main()
- 
 </code> </code>
python/gpt-patcher.1762346405.txt.gz · Last modified: by Wuff