gptdiff 0.1.10__py3-none-any.whl → 0.1.11__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
gptdiff/gptdiff.py CHANGED
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env python3
2
+ from pathlib import Path
3
+ import subprocess
4
+ import hashlib
5
+ import re
6
+
2
7
 
3
8
  import openai
4
9
  from openai import OpenAI
@@ -255,12 +260,20 @@ def build_environment(files_dict):
255
260
  return '\n'.join(env)
256
261
 
257
262
  def generate_diff(environment, goal, model=None, temperature=0.7, max_tokens=32000, api_key=None, base_url=None, prepend=None):
258
- """API: Generate diff from environment and goal"""
263
+ """API: Generate a git diff from the environment and goal.
264
+
265
+ If 'prepend' is provided, it should be a path to a file whose content will be
266
+ prepended to the system prompt.
267
+ """
259
268
  if model is None:
260
269
  model = os.getenv('GPTDIFF_MODEL', 'deepseek-reasoner')
261
270
  if prepend:
262
- prepend = load_prepend_file(args.prepend)
263
- print("Including prepend",len(enc.encode(json.dumps(prepend))), "tokens")
271
+ if prepend.startswith("http://") or prepend.startswith("https://"):
272
+ import urllib.request
273
+ with urllib.request.urlopen(prepend) as response:
274
+ prepend = response.read().decode('utf-8')
275
+ else:
276
+ prepend = load_prepend_file(prepend)
264
277
  else:
265
278
  prepend = ""
266
279
 
@@ -343,18 +356,172 @@ def smartapply(diff_text, files, model=None, api_key=None, base_url=None):
343
356
 
344
357
  return files
345
358
 
346
- # Function to apply diff to project files
347
359
  def apply_diff(project_dir, diff_text):
348
- diff_file = Path(project_dir) / "diff.patch"
349
- with open(diff_file, 'w') as f:
350
- f.write(diff_text)
360
+ """
361
+ Applies a unified diff (as generated by git diff) to the files in project_dir
362
+ using pure Python (without calling the external 'patch' command).
351
363
 
352
- result = subprocess.run(["patch", "-p1", "-f", "--remove-empty-files", "--input", str(diff_file)], cwd=project_dir, capture_output=True, text=True)
353
- if result.returncode != 0:
354
- return False
355
- else:
364
+ Handles file modifications, new file creation, and file deletions.
365
+
366
+ Returns:
367
+ True if at least one file was modified (or deleted/created) as a result of the patch,
368
+ False otherwise.
369
+ """
370
+ from pathlib import Path
371
+ import re, hashlib
372
+
373
+ def file_hash(filepath):
374
+ h = hashlib.sha256()
375
+ with open(filepath, "rb") as f:
376
+ h.update(f.read())
377
+ return h.hexdigest()
378
+
379
+ def apply_patch_to_file(file_path, patch):
380
+ """
381
+ Applies a unified diff patch (for a single file) to file_path.
382
+
383
+ Returns True if the patch was applied successfully, False otherwise.
384
+ """
385
+ # Read the original file lines; if the file doesn't exist, treat it as empty.
386
+ if file_path.exists():
387
+ original_lines = file_path.read_text(encoding="utf8").splitlines(keepends=True)
388
+ else:
389
+ original_lines = []
390
+ new_lines = []
391
+ current_index = 0
392
+
393
+ patch_lines = patch.splitlines()
394
+ # Regex for a hunk header, e.g., @@ -3,7 +3,6 @@
395
+ hunk_header_re = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
396
+ i = 0
397
+ while i < len(patch_lines):
398
+ line = patch_lines[i]
399
+ if line.startswith("@@"):
400
+ m = hunk_header_re.match(line)
401
+ if not m:
402
+ print("Invalid hunk header:", line)
403
+ return False
404
+ orig_start = int(m.group(1))
405
+ # orig_len = int(m.group(2)) if m.group(2) else 1 # not used explicitly
406
+ # new_start = int(m.group(3))
407
+ # new_len = int(m.group(4)) if m.group(4) else 1
408
+
409
+ # Copy unchanged lines before the hunk.
410
+ hunk_start_index = orig_start - 1 # diff headers are 1-indexed
411
+ if hunk_start_index > len(original_lines):
412
+ print("Hunk start index beyond file length")
413
+ return False
414
+ new_lines.extend(original_lines[current_index:hunk_start_index])
415
+ current_index = hunk_start_index
416
+
417
+ i += 1
418
+ # Process the hunk lines until the next hunk header.
419
+ while i < len(patch_lines) and not patch_lines[i].startswith("@@"):
420
+ pline = patch_lines[i]
421
+ if pline.startswith(" "):
422
+ # Context line must match exactly.
423
+ expected = pline[1:]
424
+ if current_index >= len(original_lines):
425
+ print("Context line expected but file ended")
426
+ return False
427
+ orig_line = original_lines[current_index].rstrip("\n")
428
+ if orig_line != expected:
429
+ print("Context line mismatch. Expected:", expected, "Got:", orig_line)
430
+ return False
431
+ new_lines.append(original_lines[current_index])
432
+ current_index += 1
433
+ elif pline.startswith("-"):
434
+ # Removal line: verify and skip from original.
435
+ expected = pline[1:]
436
+ if current_index >= len(original_lines):
437
+ print("Removal line expected but file ended")
438
+ return False
439
+ orig_line = original_lines[current_index].rstrip("\n")
440
+ if orig_line != expected:
441
+ print("Removal line mismatch. Expected:", expected, "Got:", orig_line)
442
+ return False
443
+ current_index += 1
444
+ elif pline.startswith("+"):
445
+ # Addition line: add to new_lines.
446
+ new_lines.append(pline[1:] + "\n")
447
+ else:
448
+ print("Unexpected line in hunk:", pline)
449
+ return False
450
+ i += 1
451
+ else:
452
+ # Skip non-hunk header lines.
453
+ i += 1
454
+
455
+ # Append any remaining lines from the original file.
456
+ new_lines.extend(original_lines[current_index:])
457
+ # Ensure parent directories exist before writing the file.
458
+ file_path.parent.mkdir(parents=True, exist_ok=True)
459
+ # Write the new content back to the file.
460
+ file_path.write_text("".join(new_lines), encoding="utf8")
356
461
  return True
357
462
 
463
+ # Parse the diff into per-file patches.
464
+ file_patches = parse_diff_per_file(diff_text)
465
+ if not file_patches:
466
+ print("No file patches found in diff.")
467
+ return False
468
+
469
+ # Record original file hashes.
470
+ original_hashes = {}
471
+ for file_path, _ in file_patches:
472
+ target_file = Path(project_dir) / file_path
473
+ if target_file.exists():
474
+ original_hashes[file_path] = file_hash(target_file)
475
+ else:
476
+ original_hashes[file_path] = None
477
+
478
+ any_change = False
479
+ # Process each file patch.
480
+ for file_path, patch in file_patches:
481
+ target_file = Path(project_dir) / file_path
482
+ if "+++ /dev/null" in patch:
483
+ # Deletion patch: delete the file if it exists.
484
+ if target_file.exists():
485
+ target_file.unlink()
486
+ if not target_file.exists():
487
+ any_change = True
488
+ else:
489
+ print(f"Failed to delete file: {target_file}")
490
+ return False
491
+ else:
492
+ # Modification or new file creation.
493
+ success = apply_patch_to_file(target_file, patch)
494
+ if not success:
495
+ print(f"Failed to apply patch to file: {target_file}")
496
+ return False
497
+
498
+ # Verify that at least one file was changed by comparing hashes.
499
+ for file_path, patch in file_patches:
500
+ target_file = Path(project_dir) / file_path
501
+ if "+++ /dev/null" in patch:
502
+ if not target_file.exists():
503
+ any_change = True
504
+ else:
505
+ print(f"Expected deletion but file still exists: {target_file}")
506
+ return False
507
+ else:
508
+ old_hash = original_hashes.get(file_path)
509
+ if target_file.exists():
510
+ new_hash = file_hash(target_file)
511
+ if old_hash != new_hash:
512
+ any_change = True
513
+ else:
514
+ print(f"No change detected in file: {target_file}")
515
+ else:
516
+ print(f"Expected modification or creation but file is missing: {target_file}")
517
+ return False
518
+
519
+ if not any_change:
520
+ print("Patch applied but no file modifications detected.")
521
+ return False
522
+ return True
523
+
524
+
358
525
  def parse_arguments():
359
526
  parser = argparse.ArgumentParser(description='Generate and optionally apply git diffs using GPT-4.')
360
527
  parser.add_argument('prompt', type=str, help='Prompt that runs on the codebase.')
@@ -399,41 +566,76 @@ def parse_diff_per_file(diff_text):
399
566
  Note:
400
567
  Uses 'b/' prefix detection from git diffs to determine target paths
401
568
  """
402
- diffs = []
403
- file_path = None
404
- current_diff = []
405
- from_path = None
406
-
407
- for line in diff_text.split('\n'):
408
- if line.startswith('diff --git'):
409
- if current_diff and file_path is not None:
410
- diffs.append((file_path, '\n'.join(current_diff)))
411
- current_diff = [line]
412
- file_path = None
413
- from_path = None
414
- parts = line.split()
415
- if len(parts) >= 4:
416
- b_path = parts[3]
417
- file_path = b_path[2:] if b_path.startswith('b/') else b_path
418
- else:
419
- current_diff.append(line)
420
- if line.startswith('--- '):
421
- from_path = line[4:].strip()
422
- elif line.startswith('+++ '):
423
- to_path = line[4:].strip()
424
- if to_path == '/dev/null':
425
- if from_path:
426
- # For deletions, use from_path after stripping 'a/' prefix
427
- file_path = from_path[2:] if from_path.startswith('a/') else from_path
428
- else:
429
- # For normal cases, use to_path after stripping 'b/' prefix
430
- file_path = to_path[2:] if to_path.startswith('b/') else to_path
431
-
432
- # Handle remaining diff content after loop
433
- if current_diff and file_path is not None:
434
- diffs.append((file_path, '\n'.join(current_diff)))
435
-
436
- return diffs
569
+ header_re = re.compile(r'^(?:diff --git\s+)?(a/[^ ]+)\s+(b/[^ ]+)\s*$', re.MULTILINE)
570
+ lines = diff_text.splitlines()
571
+
572
+ # Check if any header line exists.
573
+ if not any(header_re.match(line) for line in lines):
574
+ # Fallback strategy: detect file headers starting with '--- a/' or '-- a/'
575
+ diffs = []
576
+ current_lines = []
577
+ current_file = None
578
+ deletion_mode = False
579
+ header_line_re = re.compile(r'^-{2,3}\s+a/(.+)$')
580
+
581
+ for line in lines:
582
+ if header_line_re.match(line):
583
+ if current_file is not None and current_lines:
584
+ if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
585
+ current_lines.append("+++ /dev/null")
586
+ diffs.append((current_file, "\n".join(current_lines)))
587
+ current_lines = [line]
588
+ deletion_mode = False
589
+ file_from = header_line_re.match(line).group(1).strip()
590
+ current_file = file_from
591
+ else:
592
+ current_lines.append(line)
593
+ if "deleted file mode" in line:
594
+ deletion_mode = True
595
+ if line.startswith("+++ "):
596
+ parts = line.split()
597
+ if len(parts) >= 2:
598
+ file_to = parts[1].strip()
599
+ if file_to != "/dev/null":
600
+ current_file = file_to[2:] if (file_to.startswith("a/") or file_to.startswith("b/")) else file_to
601
+ if current_file is not None and current_lines:
602
+ if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
603
+ current_lines.append("+++ /dev/null")
604
+ diffs.append((current_file, "\n".join(current_lines)))
605
+ return diffs
606
+ else:
607
+ # Use header-based strategy.
608
+ diffs = []
609
+ current_lines = []
610
+ current_file = None
611
+ deletion_mode = False
612
+ for line in lines:
613
+ m = header_re.match(line)
614
+ if m:
615
+ if current_file is not None and current_lines:
616
+ if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
617
+ current_lines.append("+++ /dev/null")
618
+ diffs.append((current_file, "\n".join(current_lines)))
619
+ current_lines = [line]
620
+ deletion_mode = False
621
+ file_from = m.group(1) # e.g. "a/index.html"
622
+ file_to = m.group(2) # e.g. "b/index.html"
623
+ current_file = file_to[2:] if file_to.startswith("b/") else file_to
624
+ else:
625
+ current_lines.append(line)
626
+ if "deleted file mode" in line:
627
+ deletion_mode = True
628
+ if line.startswith("+++ "):
629
+ parts = line.split()
630
+ if len(parts) >= 2:
631
+ file_to = parts[1].strip()
632
+ if file_to != "/dev/null":
633
+ current_file = file_to[2:] if (file_to.startswith("a/") or file_to.startswith("b/")) else file_to
634
+ if current_file is not None and current_lines:
635
+ if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
636
+ current_lines.append("+++ /dev/null")
637
+ diffs.append((current_file, "\n".join(current_lines)))
638
+ return diffs
437
639
 
438
640
  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):
439
641
  parser = FlatXMLParser("think")
@@ -486,14 +688,12 @@ def call_llm_for_apply(file_path, original_content, file_diff, model, api_key=No
486
688
  ... )
487
689
  >>> print(updated)
488
690
  def new(): pass"""
489
-
490
691
  system_prompt = """Please apply the diff to this file. Return the result in a block. Write the entire file.
491
692
 
492
693
  1. Carefully apply all changes from the diff
493
694
  2. Preserve surrounding context that isn't changed
494
695
  3. Only return the final file content, do not add any additional markup and do not add a code block
495
696
  4. You must return the entire file. It overwrites the existing file."""
496
-
497
697
  user_prompt = f"""File: {file_path}
498
698
  File contents:
499
699
  <filecontents>
@@ -504,7 +704,6 @@ Diff to apply:
504
704
  <diff>
505
705
  {file_diff}
506
706
  </diff>"""
507
-
508
707
  if extra_prompt:
509
708
  user_prompt += f"\n\n{extra_prompt}"
510
709
  if model == "gemini-2.0-flash-thinking-exp-01-21":
@@ -513,7 +712,6 @@ Diff to apply:
513
712
  {"role": "system", "content": system_prompt},
514
713
  {"role": "user", "content": user_prompt},
515
714
  ]
516
-
517
715
  if api_key is None:
518
716
  api_key = os.getenv('GPTDIFF_LLM_API_KEY')
519
717
  if base_url is None:
@@ -525,7 +723,6 @@ Diff to apply:
525
723
  temperature=0.0,
526
724
  max_tokens=max_tokens)
527
725
  full_response = response.choices[0].message.content
528
-
529
726
  elapsed = time.time() - start_time
530
727
  minutes, seconds = divmod(int(elapsed), 60)
531
728
  time_str = f"{minutes}m {seconds}s" if minutes else f"{seconds}s"
@@ -602,8 +799,9 @@ def main():
602
799
  args.model = os.getenv('GPTDIFF_MODEL', 'deepseek-reasoner')
603
800
 
604
801
  if not args.call and not args.apply:
802
+ append = "\nInstead of using <diff> tags, use ```diff backticks."
605
803
  with open('prompt.txt', 'w') as f:
606
- f.write(full_prompt)
804
+ f.write(full_prompt+append)
607
805
  print(f"Total tokens: {token_count:5d}")
608
806
  print(f"\033[1;32mNot calling GPT-4.\033[0m") # Green color for success message
609
807
  print('Instead, wrote full prompt to prompt.txt. Use `xclip -selection clipboard < prompt.txt` then paste into chatgpt')
@@ -646,7 +844,6 @@ def main():
646
844
  print("\a") # Terminal bell for completion notification
647
845
  return
648
846
 
649
- # Output result
650
847
  elif args.apply:
651
848
  print("\nAttempting apply with the following diff:")
652
849
  print("\n<diff>")
@@ -728,3 +925,6 @@ def main():
728
925
  print(f"Completion tokens: {completion_tokens}")
729
926
  print(f"Total tokens: {total_tokens}")
730
927
  #print(f"Total cost: ${cost:.4f}")
928
+
929
+ if __name__ == "__main__":
930
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: gptdiff
3
- Version: 0.1.10
3
+ Version: 0.1.11
4
4
  Summary: A tool to generate and apply git diffs using LLMs
5
5
  Author: 255labs
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -26,10 +26,15 @@ Dynamic: requires-dist
26
26
  Dynamic: summary
27
27
 
28
28
  # GPTDiff
29
+ <!--
30
+ GPTDiff: Create and apply diffs using AI.
31
+ This tool leverages natural language instructions to modify project codebases.
32
+ -->
29
33
 
30
- 🚀 **Create and apply diffs with AI** - Modify projects with natural language
34
+ 🚀 **Create and apply diffs with AI**
35
+ Modify your project using plain English.
31
36
 
32
- More docs at [gptdiff.255labs.xyz](gptdiff.255labs.xyz)
37
+ More documentation at [gptdiff.255labs.xyz](gptdiff.255labs.xyz)
33
38
 
34
39
  ### Example Usage of `gptdiff`
35
40
 
@@ -63,7 +68,8 @@ cd myproject
63
68
  gptdiff 'add hover effects to the buttons'
64
69
  ```
65
70
 
66
- Generates a prompt.txt file that you can copy and paste into an LLM
71
+ Generates a prompt.txt file containing the full request.
72
+ Copy and paste its content into your preferred LLM (e.g., ChatGPT) for further experimentation.
67
73
 
68
74
  ### Simple command line agent loops
69
75
 
@@ -147,7 +153,7 @@ First sign up for an API key at https://nano-gpt.com/api and generate your key.
147
153
  export GPTDIFF_LLM_API_KEY='your-api-key'
148
154
  # Optional: For switching API providers
149
155
  export GPTDIFF_MODEL='deepseek-reasoner' # Set default model for all commands
150
- export GPTDIFF_LLM_BASE_URL='https://nano-gpt.com/api/v1/
156
+ export GPTDIFF_LLM_BASE_URL='https://nano-gpt.com/api/v1/'
151
157
  ```
152
158
 
153
159
  #### Windows
@@ -171,7 +177,7 @@ Prevent files being appended to the prompt by adding them to `.gitignore` or `.g
171
177
 
172
178
  ### Command Line Usage
173
179
 
174
- After installing the package, you can use the `gptdiff` command in your terminal. cd into your codebase and run:
180
+ After installing the package, use the `gptdiff` command in your terminal. Change directory into your codebase and run:
175
181
 
176
182
  ```bash
177
183
  gptdiff '<user_prompt>'
@@ -181,13 +187,7 @@ any files that are included in .gitignore are ignored when generating prompt.txt
181
187
 
182
188
  #### Specifying Additional Files
183
189
 
184
- You can specify additional files or directories to include in the prompt by adding them as arguments to the `gptdiff` command. If no additional files or directories are specified, the tool will default to using the current working directory.
185
-
186
- Example usage:
187
-
188
- ```bash
189
- gptdiff 'make this change' src test
190
- ```
190
+ You may supply extra files or directories as arguments to the `gptdiff` command. If omitted, the tool defaults to the current working directory, excluding those matching ignore rules.
191
191
 
192
192
  #### Autopatch Changes
193
193
 
@@ -202,24 +202,20 @@ Preview changes without applying them by omitting the `--apply` flag when using
202
202
  ```bash
203
203
  gptdiff "Modernize database queries" --call
204
204
  ```
205
- <span style="color: #0066cc;">ℹ️ Diff preview generated - review changes before applying</span>
205
+ <span style="color: #0066cc;">i️ Diff preview generated - review changes before applying</span>
206
206
 
207
207
  This often generates incorrect diffs that need to be manually merged.
208
208
 
209
209
  #### Smart Apply
210
210
 
211
- For more reliable patching of complex changes, use `smartapply` which processes each file's diff individually with the LLM:
211
+ For robust handling of complex changes, use `smartapply`. It processes each files diff individually via the LLM, ensuring nuanced conflict resolution.
212
212
 
213
- ```bash
214
- gptdiff 'refactor authentication system' --apply
215
- ```
216
-
217
- ### Completion Notification
213
+ ## Completion Notification
218
214
 
219
215
  Use the `--nobeep` option to disable the default completion beep:
220
216
 
221
217
  ```bash
222
- gptdiff '<user_prompt>' --beep
218
+ gptdiff '<user_prompt>' --nobeep
223
219
  ```
224
220
 
225
221
  ## Local API Documentation
@@ -242,29 +238,25 @@ import os
242
238
 
243
239
  os.environ['GPTDIFF_LLM_API_KEY'] = 'your-api-key'
244
240
 
245
- # Create environment representation
246
- environment = '''
247
- File: main.py
248
- Content:
249
- def old_name():
250
- print("Need renaming")
251
- '''
241
+ # Create files dictionary
242
+ files = {"main.py": "def old_name():\n print('Need renaming')"}
243
+
244
+ # Generate transformation diff using an environment string built from the files dictionary
245
+ environment = ""
246
+ for path, content in files.items():
247
+ environment += f"File: {path}\nContent:\n{content}\n"
252
248
 
253
- # Generate transformation diff
254
249
  diff = generate_diff(
255
250
  environment=environment,
256
251
  goal='Rename function to new_name()',
257
252
  model='deepseek-reasoner'
258
253
  )
259
254
 
260
- # Apply changes safely
261
- updated_environment = smartapply(
262
- diff_text=diff,
263
- environment_str=environment
264
- )
255
+ # Apply changes safely using the files dict
256
+ updated_files = smartapply(diff, files)
265
257
 
266
258
  print("Transformed codebase:")
267
- print(updated_environment)
259
+ print(updated_files["main.py"])
268
260
  ```
269
261
 
270
262
  **Batch Processing Example:**
@@ -0,0 +1,8 @@
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,,
@@ -1,8 +0,0 @@
1
- gptdiff/__init__.py,sha256=yGjgwv7tNvH1ZLPsQyoo1CxpTOl1iCAwwDBp-_17ksQ,89
2
- gptdiff/gptdiff.py,sha256=78_Y1ifKxCdC-e8TdKm3kKDklyV0K8S0fKyDdXhLNQs,27706
3
- gptdiff-0.1.10.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
4
- gptdiff-0.1.10.dist-info/METADATA,sha256=jw4gVLU2Gk7Iuqy-NdkeDlF9loiLfd7825lZxXIbQEY,7602
5
- gptdiff-0.1.10.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
6
- gptdiff-0.1.10.dist-info/entry_points.txt,sha256=0yvXYEVAZFI-p32kQ4-h3qKVWS0a86jsM9FAwF89t9w,49
7
- gptdiff-0.1.10.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
8
- gptdiff-0.1.10.dist-info/RECORD,,