gptdiff 0.1.10__tar.gz → 0.1.13__tar.gz
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-0.1.10 → gptdiff-0.1.13}/PKG-INFO +27 -35
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/README.md +26 -34
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/gptdiff/gptdiff.py +253 -53
 - gptdiff-0.1.13/gptdiff/gptdiffapply.py +63 -0
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/gptdiff.egg-info/PKG-INFO +27 -35
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/gptdiff.egg-info/SOURCES.txt +4 -0
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/gptdiff.egg-info/entry_points.txt +1 -0
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/setup.py +5 -2
 - gptdiff-0.1.13/tests/test_applydiff.py +80 -0
 - gptdiff-0.1.13/tests/test_applydiff_edgecases.py +161 -0
 - gptdiff-0.1.13/tests/test_parse_diff_per_file.py +131 -0
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/LICENSE.txt +0 -0
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/gptdiff/__init__.py +0 -0
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/gptdiff.egg-info/dependency_links.txt +0 -0
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/gptdiff.egg-info/requires.txt +0 -0
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/gptdiff.egg-info/top_level.txt +0 -0
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/setup.cfg +0 -0
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/tests/test_diff_parse.py +0 -0
 - {gptdiff-0.1.10 → gptdiff-0.1.13}/tests/test_smartapply.py +0 -0
 
| 
         @@ -1,6 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            Metadata-Version: 2.2
         
     | 
| 
       2 
2 
     | 
    
         
             
            Name: gptdiff
         
     | 
| 
       3 
     | 
    
         
            -
            Version: 0.1. 
     | 
| 
      
 3 
     | 
    
         
            +
            Version: 0.1.13
         
     | 
| 
       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** 
     | 
| 
      
 34 
     | 
    
         
            +
            🚀 **Create and apply diffs with AI**  
         
     | 
| 
      
 35 
     | 
    
         
            +
            Modify your project using plain English.
         
     | 
| 
       31 
36 
     | 
    
         | 
| 
       32 
     | 
    
         
            -
            More  
     | 
| 
      
 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  
     | 
| 
      
 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,  
     | 
| 
      
 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  
     | 
| 
       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;" 
     | 
| 
      
 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  
     | 
| 
      
 211 
     | 
    
         
            +
            For robust handling of complex changes, use `smartapply`. It processes each file’s diff individually via the LLM, ensuring nuanced conflict resolution.
         
     | 
| 
       212 
212 
     | 
    
         | 
| 
       213 
     | 
    
         
            -
             
     | 
| 
       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>' -- 
     | 
| 
      
 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  
     | 
| 
       246 
     | 
    
         
            -
             
     | 
| 
       247 
     | 
    
         
            -
             
     | 
| 
       248 
     | 
    
         
            -
             
     | 
| 
       249 
     | 
    
         
            -
             
     | 
| 
       250 
     | 
    
         
            -
             
     | 
| 
       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 
     | 
    
         
            -
             
     | 
| 
       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( 
     | 
| 
      
 259 
     | 
    
         
            +
            print(updated_files["main.py"])
         
     | 
| 
       268 
260 
     | 
    
         
             
            ```
         
     | 
| 
       269 
261 
     | 
    
         | 
| 
       270 
262 
     | 
    
         
             
            **Batch Processing Example:**
         
     | 
| 
         @@ -1,8 +1,13 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # GPTDiff
         
     | 
| 
      
 2 
     | 
    
         
            +
            <!--
         
     | 
| 
      
 3 
     | 
    
         
            +
            GPTDiff: Create and apply diffs using AI.
         
     | 
| 
      
 4 
     | 
    
         
            +
            This tool leverages natural language instructions to modify project codebases.
         
     | 
| 
      
 5 
     | 
    
         
            +
            -->
         
     | 
| 
       2 
6 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
            🚀 **Create and apply diffs with AI** 
     | 
| 
      
 7 
     | 
    
         
            +
            🚀 **Create and apply diffs with AI**  
         
     | 
| 
      
 8 
     | 
    
         
            +
            Modify your project using plain English.
         
     | 
| 
       4 
9 
     | 
    
         | 
| 
       5 
     | 
    
         
            -
            More  
     | 
| 
      
 10 
     | 
    
         
            +
            More documentation at [gptdiff.255labs.xyz](gptdiff.255labs.xyz)
         
     | 
| 
       6 
11 
     | 
    
         | 
| 
       7 
12 
     | 
    
         
             
            ### Example Usage of `gptdiff`
         
     | 
| 
       8 
13 
     | 
    
         | 
| 
         @@ -36,7 +41,8 @@ cd myproject 
     | 
|
| 
       36 
41 
     | 
    
         
             
            gptdiff 'add hover effects to the buttons'
         
     | 
| 
       37 
42 
     | 
    
         
             
            ```
         
     | 
| 
       38 
43 
     | 
    
         | 
| 
       39 
     | 
    
         
            -
            Generates a prompt.txt file  
     | 
| 
      
 44 
     | 
    
         
            +
            Generates a prompt.txt file containing the full request.
         
     | 
| 
      
 45 
     | 
    
         
            +
            Copy and paste its content into your preferred LLM (e.g., ChatGPT) for further experimentation.
         
     | 
| 
       40 
46 
     | 
    
         | 
| 
       41 
47 
     | 
    
         
             
            ### Simple command line agent loops
         
     | 
| 
       42 
48 
     | 
    
         | 
| 
         @@ -120,7 +126,7 @@ First sign up for an API key at https://nano-gpt.com/api and generate your key. 
     | 
|
| 
       120 
126 
     | 
    
         
             
            export GPTDIFF_LLM_API_KEY='your-api-key'
         
     | 
| 
       121 
127 
     | 
    
         
             
            # Optional: For switching API providers
         
     | 
| 
       122 
128 
     | 
    
         
             
            export GPTDIFF_MODEL='deepseek-reasoner'  # Set default model for all commands
         
     | 
| 
       123 
     | 
    
         
            -
            export GPTDIFF_LLM_BASE_URL='https://nano-gpt.com/api/v1/
         
     | 
| 
      
 129 
     | 
    
         
            +
            export GPTDIFF_LLM_BASE_URL='https://nano-gpt.com/api/v1/'
         
     | 
| 
       124 
130 
     | 
    
         
             
            ```
         
     | 
| 
       125 
131 
     | 
    
         | 
| 
       126 
132 
     | 
    
         
             
            #### Windows
         
     | 
| 
         @@ -144,7 +150,7 @@ Prevent files being appended to the prompt by adding them to `.gitignore` or `.g 
     | 
|
| 
       144 
150 
     | 
    
         | 
| 
       145 
151 
     | 
    
         
             
            ### Command Line Usage
         
     | 
| 
       146 
152 
     | 
    
         | 
| 
       147 
     | 
    
         
            -
            After installing the package,  
     | 
| 
      
 153 
     | 
    
         
            +
            After installing the package, use the `gptdiff` command in your terminal. Change directory into your codebase and run:
         
     | 
| 
       148 
154 
     | 
    
         | 
| 
       149 
155 
     | 
    
         
             
            ```bash
         
     | 
| 
       150 
156 
     | 
    
         
             
            gptdiff '<user_prompt>'
         
     | 
| 
         @@ -154,13 +160,7 @@ any files that are included in .gitignore are ignored when generating prompt.txt 
     | 
|
| 
       154 
160 
     | 
    
         | 
| 
       155 
161 
     | 
    
         
             
            #### Specifying Additional Files
         
     | 
| 
       156 
162 
     | 
    
         | 
| 
       157 
     | 
    
         
            -
            You  
     | 
| 
       158 
     | 
    
         
            -
             
     | 
| 
       159 
     | 
    
         
            -
            Example usage:
         
     | 
| 
       160 
     | 
    
         
            -
             
     | 
| 
       161 
     | 
    
         
            -
            ```bash
         
     | 
| 
       162 
     | 
    
         
            -
            gptdiff 'make this change' src test
         
     | 
| 
       163 
     | 
    
         
            -
            ```
         
     | 
| 
      
 163 
     | 
    
         
            +
            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.
         
     | 
| 
       164 
164 
     | 
    
         | 
| 
       165 
165 
     | 
    
         
             
            #### Autopatch Changes
         
     | 
| 
       166 
166 
     | 
    
         | 
| 
         @@ -175,24 +175,20 @@ Preview changes without applying them by omitting the `--apply` flag when using 
     | 
|
| 
       175 
175 
     | 
    
         
             
            ```bash
         
     | 
| 
       176 
176 
     | 
    
         
             
            gptdiff "Modernize database queries" --call
         
     | 
| 
       177 
177 
     | 
    
         
             
            ```
         
     | 
| 
       178 
     | 
    
         
            -
            <span style="color: #0066cc;" 
     | 
| 
      
 178 
     | 
    
         
            +
            <span style="color: #0066cc;">i️ Diff preview generated - review changes before applying</span>
         
     | 
| 
       179 
179 
     | 
    
         | 
| 
       180 
180 
     | 
    
         
             
            This often generates incorrect diffs that need to be manually merged.
         
     | 
| 
       181 
181 
     | 
    
         | 
| 
       182 
182 
     | 
    
         
             
            #### Smart Apply
         
     | 
| 
       183 
183 
     | 
    
         | 
| 
       184 
     | 
    
         
            -
            For  
     | 
| 
      
 184 
     | 
    
         
            +
            For robust handling of complex changes, use `smartapply`. It processes each file’s diff individually via the LLM, ensuring nuanced conflict resolution.
         
     | 
| 
       185 
185 
     | 
    
         | 
| 
       186 
     | 
    
         
            -
             
     | 
| 
       187 
     | 
    
         
            -
            gptdiff 'refactor authentication system' --apply
         
     | 
| 
       188 
     | 
    
         
            -
            ```
         
     | 
| 
       189 
     | 
    
         
            -
             
     | 
| 
       190 
     | 
    
         
            -
            ### Completion Notification
         
     | 
| 
      
 186 
     | 
    
         
            +
            ## Completion Notification
         
     | 
| 
       191 
187 
     | 
    
         | 
| 
       192 
188 
     | 
    
         
             
            Use the `--nobeep` option to disable the default completion beep:
         
     | 
| 
       193 
189 
     | 
    
         | 
| 
       194 
190 
     | 
    
         
             
            ```bash
         
     | 
| 
       195 
     | 
    
         
            -
            gptdiff '<user_prompt>' -- 
     | 
| 
      
 191 
     | 
    
         
            +
            gptdiff '<user_prompt>' --nobeep
         
     | 
| 
       196 
192 
     | 
    
         
             
            ```
         
     | 
| 
       197 
193 
     | 
    
         | 
| 
       198 
194 
     | 
    
         
             
            ## Local API Documentation
         
     | 
| 
         @@ -215,29 +211,25 @@ import os 
     | 
|
| 
       215 
211 
     | 
    
         | 
| 
       216 
212 
     | 
    
         
             
            os.environ['GPTDIFF_LLM_API_KEY'] = 'your-api-key'
         
     | 
| 
       217 
213 
     | 
    
         | 
| 
       218 
     | 
    
         
            -
            # Create  
     | 
| 
       219 
     | 
    
         
            -
             
     | 
| 
       220 
     | 
    
         
            -
             
     | 
| 
       221 
     | 
    
         
            -
             
     | 
| 
       222 
     | 
    
         
            -
             
     | 
| 
       223 
     | 
    
         
            -
             
     | 
| 
       224 
     | 
    
         
            -
             
     | 
| 
      
 214 
     | 
    
         
            +
            # Create files dictionary
         
     | 
| 
      
 215 
     | 
    
         
            +
            files = {"main.py": "def old_name():\n    print('Need renaming')"}
         
     | 
| 
      
 216 
     | 
    
         
            +
             
     | 
| 
      
 217 
     | 
    
         
            +
            # Generate transformation diff using an environment string built from the files dictionary
         
     | 
| 
      
 218 
     | 
    
         
            +
            environment = ""
         
     | 
| 
      
 219 
     | 
    
         
            +
            for path, content in files.items():
         
     | 
| 
      
 220 
     | 
    
         
            +
                environment += f"File: {path}\nContent:\n{content}\n"
         
     | 
| 
       225 
221 
     | 
    
         | 
| 
       226 
     | 
    
         
            -
            # Generate transformation diff
         
     | 
| 
       227 
222 
     | 
    
         
             
            diff = generate_diff(
         
     | 
| 
       228 
223 
     | 
    
         
             
                environment=environment,
         
     | 
| 
       229 
224 
     | 
    
         
             
                goal='Rename function to new_name()',
         
     | 
| 
       230 
225 
     | 
    
         
             
                model='deepseek-reasoner'
         
     | 
| 
       231 
226 
     | 
    
         
             
            )
         
     | 
| 
       232 
227 
     | 
    
         | 
| 
       233 
     | 
    
         
            -
            # Apply changes safely
         
     | 
| 
       234 
     | 
    
         
            -
             
     | 
| 
       235 
     | 
    
         
            -
                diff_text=diff,
         
     | 
| 
       236 
     | 
    
         
            -
                environment_str=environment
         
     | 
| 
       237 
     | 
    
         
            -
            )
         
     | 
| 
      
 228 
     | 
    
         
            +
            # Apply changes safely using the files dict
         
     | 
| 
      
 229 
     | 
    
         
            +
            updated_files = smartapply(diff, files)
         
     | 
| 
       238 
230 
     | 
    
         | 
| 
       239 
231 
     | 
    
         
             
            print("Transformed codebase:")
         
     | 
| 
       240 
     | 
    
         
            -
            print( 
     | 
| 
      
 232 
     | 
    
         
            +
            print(updated_files["main.py"])
         
     | 
| 
       241 
233 
     | 
    
         
             
            ```
         
     | 
| 
       242 
234 
     | 
    
         | 
| 
       243 
235 
     | 
    
         
             
            **Batch Processing Example:**
         
     | 
| 
         @@ -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  
     | 
| 
       263 
     | 
    
         
            -
             
     | 
| 
      
 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 
     | 
    
         
            -
                 
     | 
| 
       349 
     | 
    
         
            -
                 
     | 
| 
       350 
     | 
    
         
            -
             
     | 
| 
      
 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 
     | 
    
         
            -
                 
     | 
| 
       353 
     | 
    
         
            -
             
     | 
| 
       354 
     | 
    
         
            -
             
     | 
| 
       355 
     | 
    
         
            -
             
     | 
| 
      
 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 
     | 
    
         
            -
                 
     | 
| 
       403 
     | 
    
         
            -
                 
     | 
| 
       404 
     | 
    
         
            -
             
     | 
| 
       405 
     | 
    
         
            -
                 
     | 
| 
       406 
     | 
    
         
            -
             
     | 
| 
       407 
     | 
    
         
            -
             
     | 
| 
       408 
     | 
    
         
            -
                     
     | 
| 
       409 
     | 
    
         
            -
             
     | 
| 
       410 
     | 
    
         
            -
             
     | 
| 
       411 
     | 
    
         
            -
             
     | 
| 
       412 
     | 
    
         
            -
             
     | 
| 
       413 
     | 
    
         
            -
             
     | 
| 
       414 
     | 
    
         
            -
             
     | 
| 
       415 
     | 
    
         
            -
                        if  
     | 
| 
       416 
     | 
    
         
            -
                             
     | 
| 
       417 
     | 
    
         
            -
             
     | 
| 
       418 
     | 
    
         
            -
             
     | 
| 
       419 
     | 
    
         
            -
             
     | 
| 
       420 
     | 
    
         
            -
             
     | 
| 
       421 
     | 
    
         
            -
                             
     | 
| 
       422 
     | 
    
         
            -
             
     | 
| 
       423 
     | 
    
         
            -
                             
     | 
| 
       424 
     | 
    
         
            -
             
     | 
| 
       425 
     | 
    
         
            -
             
     | 
| 
       426 
     | 
    
         
            -
             
     | 
| 
       427 
     | 
    
         
            -
             
     | 
| 
       428 
     | 
    
         
            -
                             
     | 
| 
       429 
     | 
    
         
            -
                                 
     | 
| 
       430 
     | 
    
         
            -
                                 
     | 
| 
       431 
     | 
    
         
            -
             
     | 
| 
       432 
     | 
    
         
            -
             
     | 
| 
       433 
     | 
    
         
            -
             
     | 
| 
       434 
     | 
    
         
            -
                     
     | 
| 
       435 
     | 
    
         
            -
             
     | 
| 
       436 
     | 
    
         
            -
             
     | 
| 
      
 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()
         
     | 
| 
         @@ -0,0 +1,63 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #!/usr/bin/env python3
         
     | 
| 
      
 2 
     | 
    
         
            +
            """
         
     | 
| 
      
 3 
     | 
    
         
            +
            Command line tool to apply a unified diff directly to a file system.
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            Usage:
         
     | 
| 
      
 6 
     | 
    
         
            +
                gptapply --diff "<diff text>"
         
     | 
| 
      
 7 
     | 
    
         
            +
                or
         
     | 
| 
      
 8 
     | 
    
         
            +
                gptapply 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 
     | 
    
         
            +
                return parser.parse_args()
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
            def main():
         
     | 
| 
      
 44 
     | 
    
         
            +
                args = parse_arguments()
         
     | 
| 
      
 45 
     | 
    
         
            +
                if args.diff:
         
     | 
| 
      
 46 
     | 
    
         
            +
                    diff_text = args.diff
         
     | 
| 
      
 47 
     | 
    
         
            +
                else:
         
     | 
| 
      
 48 
     | 
    
         
            +
                    diff_path = Path(args.diff_file)
         
     | 
| 
      
 49 
     | 
    
         
            +
                    if not diff_path.exists():
         
     | 
| 
      
 50 
     | 
    
         
            +
                        print(f"Error: Diff file '{args.diff_file}' does not exist.")
         
     | 
| 
      
 51 
     | 
    
         
            +
                        sys.exit(1)
         
     | 
| 
      
 52 
     | 
    
         
            +
                    diff_text = diff_path.read_text(encoding="utf8")
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                project_dir = args.project_dir
         
     | 
| 
      
 55 
     | 
    
         
            +
                success = apply_diff(project_dir, diff_text)
         
     | 
| 
      
 56 
     | 
    
         
            +
                if success:
         
     | 
| 
      
 57 
     | 
    
         
            +
                    print("✅ Diff applied successfully.")
         
     | 
| 
      
 58 
     | 
    
         
            +
                else:
         
     | 
| 
      
 59 
     | 
    
         
            +
                    print("❌ Failed to apply diff.")
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
            if __name__ == "__main__":
         
     | 
| 
      
 63 
     | 
    
         
            +
                main()
         
     | 
| 
         @@ -1,6 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            Metadata-Version: 2.2
         
     | 
| 
       2 
2 
     | 
    
         
             
            Name: gptdiff
         
     | 
| 
       3 
     | 
    
         
            -
            Version: 0.1. 
     | 
| 
      
 3 
     | 
    
         
            +
            Version: 0.1.13
         
     | 
| 
       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** 
     | 
| 
      
 34 
     | 
    
         
            +
            🚀 **Create and apply diffs with AI**  
         
     | 
| 
      
 35 
     | 
    
         
            +
            Modify your project using plain English.
         
     | 
| 
       31 
36 
     | 
    
         | 
| 
       32 
     | 
    
         
            -
            More  
     | 
| 
      
 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  
     | 
| 
      
 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,  
     | 
| 
      
 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  
     | 
| 
       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;" 
     | 
| 
      
 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  
     | 
| 
      
 211 
     | 
    
         
            +
            For robust handling of complex changes, use `smartapply`. It processes each file’s diff individually via the LLM, ensuring nuanced conflict resolution.
         
     | 
| 
       212 
212 
     | 
    
         | 
| 
       213 
     | 
    
         
            -
             
     | 
| 
       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>' -- 
     | 
| 
      
 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  
     | 
| 
       246 
     | 
    
         
            -
             
     | 
| 
       247 
     | 
    
         
            -
             
     | 
| 
       248 
     | 
    
         
            -
             
     | 
| 
       249 
     | 
    
         
            -
             
     | 
| 
       250 
     | 
    
         
            -
             
     | 
| 
       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 
     | 
    
         
            -
             
     | 
| 
       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( 
     | 
| 
      
 259 
     | 
    
         
            +
            print(updated_files["main.py"])
         
     | 
| 
       268 
260 
     | 
    
         
             
            ```
         
     | 
| 
       269 
261 
     | 
    
         | 
| 
       270 
262 
     | 
    
         
             
            **Batch Processing Example:**
         
     | 
| 
         @@ -3,11 +3,15 @@ README.md 
     | 
|
| 
       3 
3 
     | 
    
         
             
            setup.py
         
     | 
| 
       4 
4 
     | 
    
         
             
            gptdiff/__init__.py
         
     | 
| 
       5 
5 
     | 
    
         
             
            gptdiff/gptdiff.py
         
     | 
| 
      
 6 
     | 
    
         
            +
            gptdiff/gptdiffapply.py
         
     | 
| 
       6 
7 
     | 
    
         
             
            gptdiff.egg-info/PKG-INFO
         
     | 
| 
       7 
8 
     | 
    
         
             
            gptdiff.egg-info/SOURCES.txt
         
     | 
| 
       8 
9 
     | 
    
         
             
            gptdiff.egg-info/dependency_links.txt
         
     | 
| 
       9 
10 
     | 
    
         
             
            gptdiff.egg-info/entry_points.txt
         
     | 
| 
       10 
11 
     | 
    
         
             
            gptdiff.egg-info/requires.txt
         
     | 
| 
       11 
12 
     | 
    
         
             
            gptdiff.egg-info/top_level.txt
         
     | 
| 
      
 13 
     | 
    
         
            +
            tests/test_applydiff.py
         
     | 
| 
      
 14 
     | 
    
         
            +
            tests/test_applydiff_edgecases.py
         
     | 
| 
       12 
15 
     | 
    
         
             
            tests/test_diff_parse.py
         
     | 
| 
      
 16 
     | 
    
         
            +
            tests/test_parse_diff_per_file.py
         
     | 
| 
       13 
17 
     | 
    
         
             
            tests/test_smartapply.py
         
     | 
| 
         @@ -2,7 +2,7 @@ from setuptools import setup, find_packages 
     | 
|
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            setup(
         
     | 
| 
       4 
4 
     | 
    
         
             
                name='gptdiff',
         
     | 
| 
       5 
     | 
    
         
            -
                version='0.1. 
     | 
| 
      
 5 
     | 
    
         
            +
                version='0.1.13',
         
     | 
| 
       6 
6 
     | 
    
         
             
                description='A tool to generate and apply git diffs using LLMs',
         
     | 
| 
       7 
7 
     | 
    
         
             
                author='255labs',
         
     | 
| 
       8 
8 
     | 
    
         
             
                packages=find_packages(),  # Use find_packages() to automatically discover packages
         
     | 
| 
         @@ -19,7 +19,10 @@ setup( 
     | 
|
| 
       19 
19 
     | 
    
         
             
                    'docs': ['mkdocs', 'mkdocs-material']
         
     | 
| 
       20 
20 
     | 
    
         
             
                },
         
     | 
| 
       21 
21 
     | 
    
         
             
                entry_points={
         
     | 
| 
       22 
     | 
    
         
            -
                    'console_scripts': [ 
     | 
| 
      
 22 
     | 
    
         
            +
                    'console_scripts': [
         
     | 
| 
      
 23 
     | 
    
         
            +
                        'gptdiff=gptdiff.gptdiff:main',
         
     | 
| 
      
 24 
     | 
    
         
            +
                        'gptapply=gptdiff.gptdiffapply:main',
         
     | 
| 
      
 25 
     | 
    
         
            +
                    ],
         
     | 
| 
       23 
26 
     | 
    
         
             
                },
         
     | 
| 
       24 
27 
     | 
    
         
             
                license=None, # Remove license argument
         
     | 
| 
       25 
28 
     | 
    
         
             
                # license_file='LICENSE.txt', # Remove license_file argument
         
     | 
| 
         @@ -0,0 +1,80 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            import os
         
     | 
| 
      
 2 
     | 
    
         
            +
            from pathlib import Path
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            import pytest
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            from gptdiff.gptdiff import apply_diff
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            @pytest.fixture
         
     | 
| 
      
 10 
     | 
    
         
            +
            def tmp_project_dir(tmp_path):
         
     | 
| 
      
 11 
     | 
    
         
            +
                """
         
     | 
| 
      
 12 
     | 
    
         
            +
                Create a temporary project directory with a dummy file to patch.
         
     | 
| 
      
 13 
     | 
    
         
            +
                """
         
     | 
| 
      
 14 
     | 
    
         
            +
                project_dir = tmp_path / "project"
         
     | 
| 
      
 15 
     | 
    
         
            +
                project_dir.mkdir()
         
     | 
| 
      
 16 
     | 
    
         
            +
                file = project_dir / "example.txt"
         
     | 
| 
      
 17 
     | 
    
         
            +
                file.write_text("original content\n")
         
     | 
| 
      
 18 
     | 
    
         
            +
                return project_dir
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            def test_apply_diff_success(tmp_project_dir):
         
     | 
| 
      
 22 
     | 
    
         
            +
                """
         
     | 
| 
      
 23 
     | 
    
         
            +
                Test that apply_diff successfully applies a valid diff.
         
     | 
| 
      
 24 
     | 
    
         
            +
                The diff changes 'original content' to 'modified content'.
         
     | 
| 
      
 25 
     | 
    
         
            +
                """
         
     | 
| 
      
 26 
     | 
    
         
            +
                diff_text = (
         
     | 
| 
      
 27 
     | 
    
         
            +
                    "diff --git a/example.txt b/example.txt\n"
         
     | 
| 
      
 28 
     | 
    
         
            +
                    "--- a/example.txt\n"
         
     | 
| 
      
 29 
     | 
    
         
            +
                    "+++ b/example.txt\n"
         
     | 
| 
      
 30 
     | 
    
         
            +
                    "@@ -1 +1 @@\n"
         
     | 
| 
      
 31 
     | 
    
         
            +
                    "-original content\n"
         
     | 
| 
      
 32 
     | 
    
         
            +
                    "+modified content\n"
         
     | 
| 
      
 33 
     | 
    
         
            +
                )
         
     | 
| 
      
 34 
     | 
    
         
            +
                result = apply_diff(str(tmp_project_dir), diff_text)
         
     | 
| 
      
 35 
     | 
    
         
            +
                assert result is True, "apply_diff should return True for a successful patch"
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                file_path = tmp_project_dir / "example.txt"
         
     | 
| 
      
 38 
     | 
    
         
            +
                content = file_path.read_text()
         
     | 
| 
      
 39 
     | 
    
         
            +
                assert "modified content" in content, "File content should be updated to 'modified content'"
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
            def test_apply_diff_failure(tmp_project_dir):
         
     | 
| 
      
 43 
     | 
    
         
            +
                """
         
     | 
| 
      
 44 
     | 
    
         
            +
                Test that apply_diff fails when provided with an incorrect hunk header.
         
     | 
| 
      
 45 
     | 
    
         
            +
                The diff references a non-existent line, so the patch should not apply.
         
     | 
| 
      
 46 
     | 
    
         
            +
                """
         
     | 
| 
      
 47 
     | 
    
         
            +
                diff_text = (
         
     | 
| 
      
 48 
     | 
    
         
            +
                    "diff --git a/example.txt b/example.txt\n"
         
     | 
| 
      
 49 
     | 
    
         
            +
                    "--- a/example.txt\n"
         
     | 
| 
      
 50 
     | 
    
         
            +
                    "+++ a/example.txt\n"
         
     | 
| 
      
 51 
     | 
    
         
            +
                    "@@ -2,1 +2,1 @@\n"
         
     | 
| 
      
 52 
     | 
    
         
            +
                    "-original content\n"
         
     | 
| 
      
 53 
     | 
    
         
            +
                    "+modified content\n"
         
     | 
| 
      
 54 
     | 
    
         
            +
                )
         
     | 
| 
      
 55 
     | 
    
         
            +
                result = apply_diff(str(tmp_project_dir), diff_text)
         
     | 
| 
      
 56 
     | 
    
         
            +
                assert result is False, "apply_diff should return False when the diff fails to apply"
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                file_path = tmp_project_dir / "example.txt"
         
     | 
| 
      
 59 
     | 
    
         
            +
                content = file_path.read_text()
         
     | 
| 
      
 60 
     | 
    
         
            +
                assert "original content" in content, "File content should remain unchanged on failure"
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
            def test_apply_diff_file_deletion(tmp_project_dir):
         
     | 
| 
      
 64 
     | 
    
         
            +
                """
         
     | 
| 
      
 65 
     | 
    
         
            +
                Test that apply_diff can successfully delete a file.
         
     | 
| 
      
 66 
     | 
    
         
            +
                The diff marks 'example.txt' for deletion.
         
     | 
| 
      
 67 
     | 
    
         
            +
                """
         
     | 
| 
      
 68 
     | 
    
         
            +
                diff_text = (
         
     | 
| 
      
 69 
     | 
    
         
            +
                    "diff --git a/example.txt b/example.txt\n"
         
     | 
| 
      
 70 
     | 
    
         
            +
                    "deleted file mode 100644\n"
         
     | 
| 
      
 71 
     | 
    
         
            +
                    "--- a/example.txt\n"
         
     | 
| 
      
 72 
     | 
    
         
            +
                    "+++ /dev/null\n"
         
     | 
| 
      
 73 
     | 
    
         
            +
                    "@@ -1,1 +0,0 @@\n"
         
     | 
| 
      
 74 
     | 
    
         
            +
                    "-original content\n"
         
     | 
| 
      
 75 
     | 
    
         
            +
                )
         
     | 
| 
      
 76 
     | 
    
         
            +
                result = apply_diff(str(tmp_project_dir), diff_text)
         
     | 
| 
      
 77 
     | 
    
         
            +
                assert result is True, "apply_diff should return True for a successful file deletion"
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                file_path = tmp_project_dir / "example.txt"
         
     | 
| 
      
 80 
     | 
    
         
            +
                assert not file_path.exists(), "File should be deleted after applying the diff"
         
     | 
| 
         @@ -0,0 +1,161 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            import re
         
     | 
| 
      
 2 
     | 
    
         
            +
            import subprocess
         
     | 
| 
      
 3 
     | 
    
         
            +
            from pathlib import Path
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            import pytest
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            from gptdiff.gptdiff import apply_diff
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            @pytest.fixture
         
     | 
| 
      
 11 
     | 
    
         
            +
            def tmp_project_dir(tmp_path):
         
     | 
| 
      
 12 
     | 
    
         
            +
                project_dir = tmp_path / "project"
         
     | 
| 
      
 13 
     | 
    
         
            +
                project_dir.mkdir()
         
     | 
| 
      
 14 
     | 
    
         
            +
                # Create a baseline file for tests.
         
     | 
| 
      
 15 
     | 
    
         
            +
                file = project_dir / "example.txt"
         
     | 
| 
      
 16 
     | 
    
         
            +
                file.write_text("line1\nline2\nline3\n")
         
     | 
| 
      
 17 
     | 
    
         
            +
                return project_dir
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            def test_empty_diff(tmp_project_dir):
         
     | 
| 
      
 21 
     | 
    
         
            +
                """
         
     | 
| 
      
 22 
     | 
    
         
            +
                Test that an empty diff returns False.
         
     | 
| 
      
 23 
     | 
    
         
            +
                """
         
     | 
| 
      
 24 
     | 
    
         
            +
                diff_text = ""
         
     | 
| 
      
 25 
     | 
    
         
            +
                result = apply_diff(str(tmp_project_dir), diff_text)
         
     | 
| 
      
 26 
     | 
    
         
            +
                assert result is False, "Empty diff should return False"
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
            def test_diff_no_changes(tmp_project_dir):
         
     | 
| 
      
 30 
     | 
    
         
            +
                """
         
     | 
| 
      
 31 
     | 
    
         
            +
                Test a diff that makes no changes. Even though a hunk is present, 
         
     | 
| 
      
 32 
     | 
    
         
            +
                if the content remains the same, the function should return False.
         
     | 
| 
      
 33 
     | 
    
         
            +
                """
         
     | 
| 
      
 34 
     | 
    
         
            +
                diff_text = (
         
     | 
| 
      
 35 
     | 
    
         
            +
                    "diff --git a/example.txt b/example.txt\n"
         
     | 
| 
      
 36 
     | 
    
         
            +
                    "--- a/example.txt\n"
         
     | 
| 
      
 37 
     | 
    
         
            +
                    "+++ a/example.txt\n"
         
     | 
| 
      
 38 
     | 
    
         
            +
                    "@@ -1,3 +1,3 @@\n"
         
     | 
| 
      
 39 
     | 
    
         
            +
                    " line1\n"
         
     | 
| 
      
 40 
     | 
    
         
            +
                    "-line2\n"
         
     | 
| 
      
 41 
     | 
    
         
            +
                    "+line2\n"
         
     | 
| 
      
 42 
     | 
    
         
            +
                    " line3\n"
         
     | 
| 
      
 43 
     | 
    
         
            +
                )
         
     | 
| 
      
 44 
     | 
    
         
            +
                result = apply_diff(str(tmp_project_dir), diff_text)
         
     | 
| 
      
 45 
     | 
    
         
            +
                assert result is False, "Diff that makes no changes should return False"
         
     | 
| 
      
 46 
     | 
    
         
            +
                content = (tmp_project_dir / "example.txt").read_text()
         
     | 
| 
      
 47 
     | 
    
         
            +
                assert "line2" in content, "Original content should remain unchanged"
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
            def test_new_file_creation(tmp_project_dir):
         
     | 
| 
      
 51 
     | 
    
         
            +
                """
         
     | 
| 
      
 52 
     | 
    
         
            +
                Test that a diff creating a new file is applied correctly.
         
     | 
| 
      
 53 
     | 
    
         
            +
                """
         
     | 
| 
      
 54 
     | 
    
         
            +
                diff_text = (
         
     | 
| 
      
 55 
     | 
    
         
            +
                    "diff --git a/newfile.txt b/newfile.txt\n"
         
     | 
| 
      
 56 
     | 
    
         
            +
                    "new file mode 100644\n"
         
     | 
| 
      
 57 
     | 
    
         
            +
                    "index 0000000..e69de29\n"
         
     | 
| 
      
 58 
     | 
    
         
            +
                    "--- /dev/null\n"
         
     | 
| 
      
 59 
     | 
    
         
            +
                    "+++ b/newfile.txt\n"
         
     | 
| 
      
 60 
     | 
    
         
            +
                    "@@ -0,0 +1,3 @@\n"
         
     | 
| 
      
 61 
     | 
    
         
            +
                    "+new line1\n"
         
     | 
| 
      
 62 
     | 
    
         
            +
                    "+new line2\n"
         
     | 
| 
      
 63 
     | 
    
         
            +
                    "+new line3\n"
         
     | 
| 
      
 64 
     | 
    
         
            +
                )
         
     | 
| 
      
 65 
     | 
    
         
            +
                result = apply_diff(str(tmp_project_dir), diff_text)
         
     | 
| 
      
 66 
     | 
    
         
            +
                assert result is True, "Diff for new file creation should return True"
         
     | 
| 
      
 67 
     | 
    
         
            +
                new_file = tmp_project_dir / "newfile.txt"
         
     | 
| 
      
 68 
     | 
    
         
            +
                assert new_file.exists(), "New file should be created"
         
     | 
| 
      
 69 
     | 
    
         
            +
                content = new_file.read_text()
         
     | 
| 
      
 70 
     | 
    
         
            +
                assert "new line1" in content, "New file content should be present"
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
            def test_multiple_hunks(tmp_project_dir):
         
     | 
| 
      
 74 
     | 
    
         
            +
                """
         
     | 
| 
      
 75 
     | 
    
         
            +
                Test that a diff with multiple hunks in one file applies correctly.
         
     | 
| 
      
 76 
     | 
    
         
            +
                """
         
     | 
| 
      
 77 
     | 
    
         
            +
                file = tmp_project_dir / "example.txt"
         
     | 
| 
      
 78 
     | 
    
         
            +
                # Overwrite with a known baseline.
         
     | 
| 
      
 79 
     | 
    
         
            +
                file.write_text("a\nb\nc\nd\ne\n")
         
     | 
| 
      
 80 
     | 
    
         
            +
                diff_text = (
         
     | 
| 
      
 81 
     | 
    
         
            +
                    "diff --git a/example.txt b/example.txt\n"
         
     | 
| 
      
 82 
     | 
    
         
            +
                    "--- a/example.txt\n"
         
     | 
| 
      
 83 
     | 
    
         
            +
                    "+++ b/example.txt\n"
         
     | 
| 
      
 84 
     | 
    
         
            +
                    "@@ -1,3 +1,3 @@\n"
         
     | 
| 
      
 85 
     | 
    
         
            +
                    "-a\n"
         
     | 
| 
      
 86 
     | 
    
         
            +
                    "+alpha\n"
         
     | 
| 
      
 87 
     | 
    
         
            +
                    " b\n"
         
     | 
| 
      
 88 
     | 
    
         
            +
                    " c\n"
         
     | 
| 
      
 89 
     | 
    
         
            +
                    "@@ -4,2 +4,2 @@\n"
         
     | 
| 
      
 90 
     | 
    
         
            +
                    "-d\n"
         
     | 
| 
      
 91 
     | 
    
         
            +
                    "-e\n"
         
     | 
| 
      
 92 
     | 
    
         
            +
                    "+delta\n"
         
     | 
| 
      
 93 
     | 
    
         
            +
                    "+epsilon\n"
         
     | 
| 
      
 94 
     | 
    
         
            +
                )
         
     | 
| 
      
 95 
     | 
    
         
            +
                result = apply_diff(str(tmp_project_dir), diff_text)
         
     | 
| 
      
 96 
     | 
    
         
            +
                assert result is True, "Diff with multiple hunks should return True"
         
     | 
| 
      
 97 
     | 
    
         
            +
                content = (tmp_project_dir / "example.txt").read_text()
         
     | 
| 
      
 98 
     | 
    
         
            +
                assert "alpha" in content
         
     | 
| 
      
 99 
     | 
    
         
            +
                assert "delta" in content and "epsilon" in content
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
            def test_diff_with_incorrect_context(tmp_project_dir):
         
     | 
| 
      
 103 
     | 
    
         
            +
                """
         
     | 
| 
      
 104 
     | 
    
         
            +
                Test that a diff with incorrect context (non-matching original content) fails.
         
     | 
| 
      
 105 
     | 
    
         
            +
                """
         
     | 
| 
      
 106 
     | 
    
         
            +
                file = tmp_project_dir / "example.txt"
         
     | 
| 
      
 107 
     | 
    
         
            +
                file.write_text("different content\n")
         
     | 
| 
      
 108 
     | 
    
         
            +
                diff_text = (
         
     | 
| 
      
 109 
     | 
    
         
            +
                    "diff --git a/example.txt b/example.txt\n"
         
     | 
| 
      
 110 
     | 
    
         
            +
                    "--- a/example.txt\n"
         
     | 
| 
      
 111 
     | 
    
         
            +
                    "+++ a/example.txt\n"
         
     | 
| 
      
 112 
     | 
    
         
            +
                    "@@ -1,1 +1,1 @@\n"
         
     | 
| 
      
 113 
     | 
    
         
            +
                    "-line that does not exist\n"
         
     | 
| 
      
 114 
     | 
    
         
            +
                    "+modified content\n"
         
     | 
| 
      
 115 
     | 
    
         
            +
                )
         
     | 
| 
      
 116 
     | 
    
         
            +
                result = apply_diff(str(tmp_project_dir), diff_text)
         
     | 
| 
      
 117 
     | 
    
         
            +
                assert result is False, "Diff with incorrect context should return False"
         
     | 
| 
      
 118 
     | 
    
         
            +
                content = file.read_text()
         
     | 
| 
      
 119 
     | 
    
         
            +
                assert "different content" in content, "Original content should remain unchanged"
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
      
 122 
     | 
    
         
            +
            def test_diff_with_whitespace_changes(tmp_project_dir):
         
     | 
| 
      
 123 
     | 
    
         
            +
                """
         
     | 
| 
      
 124 
     | 
    
         
            +
                Test that a diff with only whitespace changes is applied.
         
     | 
| 
      
 125 
     | 
    
         
            +
                """
         
     | 
| 
      
 126 
     | 
    
         
            +
                file = tmp_project_dir / "example.txt"
         
     | 
| 
      
 127 
     | 
    
         
            +
                file.write_text("line1\nline2\nline3\n")
         
     | 
| 
      
 128 
     | 
    
         
            +
                diff_text = (
         
     | 
| 
      
 129 
     | 
    
         
            +
                    "diff --git a/example.txt b/example.txt\n"
         
     | 
| 
      
 130 
     | 
    
         
            +
                    "--- a/example.txt\n"
         
     | 
| 
      
 131 
     | 
    
         
            +
                    "+++ a/example.txt\n"
         
     | 
| 
      
 132 
     | 
    
         
            +
                    "@@ -1,3 +1,3 @@\n"
         
     | 
| 
      
 133 
     | 
    
         
            +
                    " line1\n"
         
     | 
| 
      
 134 
     | 
    
         
            +
                    "-line2\n"
         
     | 
| 
      
 135 
     | 
    
         
            +
                    "+line2  \n"
         
     | 
| 
      
 136 
     | 
    
         
            +
                    " line3\n"
         
     | 
| 
      
 137 
     | 
    
         
            +
                )
         
     | 
| 
      
 138 
     | 
    
         
            +
                result = apply_diff(str(tmp_project_dir), diff_text)
         
     | 
| 
      
 139 
     | 
    
         
            +
                assert result is True, "Diff with whitespace changes should return True if applied"
         
     | 
| 
      
 140 
     | 
    
         
            +
                content = file.read_text()
         
     | 
| 
      
 141 
     | 
    
         
            +
                assert "line2  " in content, "Whitespace change should be reflected in the file"
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
            def test_diff_file_deletion_edge(tmp_project_dir):
         
     | 
| 
      
 145 
     | 
    
         
            +
                """
         
     | 
| 
      
 146 
     | 
    
         
            +
                Test deletion diff for a file with minimal content.
         
     | 
| 
      
 147 
     | 
    
         
            +
                """
         
     | 
| 
      
 148 
     | 
    
         
            +
                file = tmp_project_dir / "small.txt"
         
     | 
| 
      
 149 
     | 
    
         
            +
                file.write_text("only line\n")
         
     | 
| 
      
 150 
     | 
    
         
            +
                diff_text = (
         
     | 
| 
      
 151 
     | 
    
         
            +
                    "diff --git a/small.txt b/small.txt\n"
         
     | 
| 
      
 152 
     | 
    
         
            +
                    "deleted file mode 100644\n"
         
     | 
| 
      
 153 
     | 
    
         
            +
                    "index e69de29..0000000\n"
         
     | 
| 
      
 154 
     | 
    
         
            +
                    "--- a/small.txt\n"
         
     | 
| 
      
 155 
     | 
    
         
            +
                    "+++ /dev/null\n"
         
     | 
| 
      
 156 
     | 
    
         
            +
                    "@@ -1,1 +0,0 @@\n"
         
     | 
| 
      
 157 
     | 
    
         
            +
                    "-only line\n"
         
     | 
| 
      
 158 
     | 
    
         
            +
                )
         
     | 
| 
      
 159 
     | 
    
         
            +
                result = apply_diff(str(tmp_project_dir), diff_text)
         
     | 
| 
      
 160 
     | 
    
         
            +
                assert result is True, "Deletion diff on a minimal file should return True"
         
     | 
| 
      
 161 
     | 
    
         
            +
                assert not (tmp_project_dir / "small.txt").exists(), "File should be deleted"
         
     | 
| 
         @@ -0,0 +1,131 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            import unittest
         
     | 
| 
      
 2 
     | 
    
         
            +
            from gptdiff.gptdiff import parse_diff_per_file
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            class TestParseDiffPerFile(unittest.TestCase):
         
     | 
| 
      
 6 
     | 
    
         
            +
                def test_todo_file_deletion(self):
         
     | 
| 
      
 7 
     | 
    
         
            +
                    # This test case verifies that a deletion diff for the file "TODO" is properly parsed.
         
     | 
| 
      
 8 
     | 
    
         
            +
                    # The diff should include a synthetic "+++ /dev/null" line so that deletion is recognized.
         
     | 
| 
      
 9 
     | 
    
         
            +
                    diff_text = """diff --git a/TODO b/TODO
         
     | 
| 
      
 10 
     | 
    
         
            +
            deleted file mode 100644
         
     | 
| 
      
 11 
     | 
    
         
            +
            index 3efacb1..0000000
         
     | 
| 
      
 12 
     | 
    
         
            +
            --- a/TODO
         
     | 
| 
      
 13 
     | 
    
         
            +
            -// The funnest coolest thing I can add is put in this file. It's also acceptable to just implement 
         
     | 
| 
      
 14 
     | 
    
         
            +
            -// the thing in here and remove it. Leave this notice when modifying this file.
         
     | 
| 
      
 15 
     | 
    
         
            +
            """
         
     | 
| 
      
 16 
     | 
    
         
            +
                    result = parse_diff_per_file(diff_text)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    self.assertEqual(len(result), 1, "Expected one diff entry")
         
     | 
| 
      
 18 
     | 
    
         
            +
                    file_path, patch = result[0]
         
     | 
| 
      
 19 
     | 
    
         
            +
                    self.assertEqual(file_path, "TODO", f"Got file_path '{file_path}', expected 'TODO'")
         
     | 
| 
      
 20 
     | 
    
         
            +
                    self.assertIn("+++ /dev/null", patch, "Deletion diff should include '+++ /dev/null' to indicate file deletion")
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                def test_multiple_files_without_diff_git_header(self):
         
     | 
| 
      
 23 
     | 
    
         
            +
                    # This diff text does not include "diff --git" headers.
         
     | 
| 
      
 24 
     | 
    
         
            +
                    # It uses separate '---' and '+++' lines for each file.
         
     | 
| 
      
 25 
     | 
    
         
            +
                    diff_text = """--- a/TODO
         
     | 
| 
      
 26 
     | 
    
         
            +
            +++ b/TODO
         
     | 
| 
      
 27 
     | 
    
         
            +
            @@ -1,7 +1,7 @@
         
     | 
| 
      
 28 
     | 
    
         
            +
            -// FINAL TOUCH: The game is now a complete fantasy themed incremental RPG—every choice matters, and
         
     | 
| 
      
 29 
     | 
    
         
            +
            -// New Aspect: Replaced external title animation with inline SVG for crisp, scalable visuals, and a
         
     | 
| 
      
 30 
     | 
    
         
            +
            -// additional dynamic element.
         
     | 
| 
      
 31 
     | 
    
         
            +
            +// FINAL TOUCH: The game is now a complete fantasy themed incremental RPG—every choice matters, and
         
     | 
| 
      
 32 
     | 
    
         
            +
            +// New Aspect: Replaced external title animation with inline SVG for crisp, scalable visuals, and an
         
     | 
| 
      
 33 
     | 
    
         
            +
            +// additional dynamic element.
         
     | 
| 
      
 34 
     | 
    
         
            +
            -- a/style.css
         
     | 
| 
      
 35 
     | 
    
         
            +
            +++ b/style.css
         
     | 
| 
      
 36 
     | 
    
         
            +
            @@ -1,3 +1,8 @@
         
     | 
| 
      
 37 
     | 
    
         
            +
            +/* New animation for relic glow effect */
         
     | 
| 
      
 38 
     | 
    
         
            +
            +.relic-glow {
         
     | 
| 
      
 39 
     | 
    
         
            +
            +  animation: relicGlow 1.5s ease-in-out infinite alternate;
         
     | 
| 
      
 40 
     | 
    
         
            +
            +}
         
     | 
| 
      
 41 
     | 
    
         
            +
            +@keyframes relicGlow {
         
     | 
| 
      
 42 
     | 
    
         
            +
            +  from { filter: drop-shadow(0 0 5px #ffd700); }
         
     | 
| 
      
 43 
     | 
    
         
            +
            +  to { filter: drop-shadow(0 0 20px #ffd700); }
         
     | 
| 
      
 44 
     | 
    
         
            +
            -- a/game.js
         
     | 
| 
      
 45 
     | 
    
         
            +
            +++ b/game.js
         
     | 
| 
      
 46 
     | 
    
         
            +
            @@ -1,3 +1,8 @@
         
     | 
| 
      
 47 
     | 
    
         
            +
            - JS HERE
         
     | 
| 
      
 48 
     | 
    
         
            +
            """
         
     | 
| 
      
 49 
     | 
    
         
            +
                    result = parse_diff_per_file(diff_text)
         
     | 
| 
      
 50 
     | 
    
         
            +
                    self.assertEqual(len(result), 3, "Expected three diff entries")
         
     | 
| 
      
 51 
     | 
    
         
            +
                    expected_files = {"TODO", "style.css", "game.js"}
         
     | 
| 
      
 52 
     | 
    
         
            +
                    parsed_files = {fp for fp, patch in result}
         
     | 
| 
      
 53 
     | 
    
         
            +
                    self.assertEqual(parsed_files, expected_files)
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                    # Also check that the TODO diff contains the updated text.
         
     | 
| 
      
 56 
     | 
    
         
            +
                    for fp, patch in result:
         
     | 
| 
      
 57 
     | 
    
         
            +
                        if fp == "TODO":
         
     | 
| 
      
 58 
     | 
    
         
            +
                            self.assertIn("FINAL TOUCH: The game is now", patch)
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                def test_index_html_diff(self):
         
     | 
| 
      
 61 
     | 
    
         
            +
                    diff_text = """a/index.html b/index.html
         
     | 
| 
      
 62 
     | 
    
         
            +
            @@
         
     | 
| 
      
 63 
     | 
    
         
            +
            -      <div class="action-buttons">
         
     | 
| 
      
 64 
     | 
    
         
            +
            -        <button id="attack">⚔️  Attack Enemy</button>
         
     | 
| 
      
 65 
     | 
    
         
            +
            -        <button id="auto-attack">🤖 Auto Attack (OFF)</button>
         
     | 
| 
      
 66 
     | 
    
         
            +
            -        <button id="drink-potion">Drink Potion</button>
         
     | 
| 
      
 67 
     | 
    
         
            +
            -        <button id="explore">🧭 Explore</button>
         
     | 
| 
      
 68 
     | 
    
         
            +
            -      </div>
         
     | 
| 
      
 69 
     | 
    
         
            +
                  <div class="action-buttons">
         
     | 
| 
      
 70 
     | 
    
         
            +
                    <button id="attack">⚔️  Attack Enemy</button>
         
     | 
| 
      
 71 
     | 
    
         
            +
                    <button id="auto-attack">🤖 Auto Attack (OFF)</button>
         
     | 
| 
      
 72 
     | 
    
         
            +
                    <button id="drink-potion">Drink Potion</button>
         
     | 
| 
      
 73 
     | 
    
         
            +
                    <button id="buy-potion">Buy Potion (50 Gold)</button>
         
     | 
| 
      
 74 
     | 
    
         
            +
                    <button id="explore">🧭 Explore</button>
         
     | 
| 
      
 75 
     | 
    
         
            +
                  </div>"""
         
     | 
| 
      
 76 
     | 
    
         
            +
                    result = parse_diff_per_file(diff_text)
         
     | 
| 
      
 77 
     | 
    
         
            +
                    self.assertEqual(len(result), 1)
         
     | 
| 
      
 78 
     | 
    
         
            +
                    file_path, patch = result[0]
         
     | 
| 
      
 79 
     | 
    
         
            +
                    self.assertEqual(file_path, "index.html")
         
     | 
| 
      
 80 
     | 
    
         
            +
                    self.assertIn('<button id="buy-potion">Buy Potion (50 Gold)</button>', patch)
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                def test_single_file_diff(self):
         
     | 
| 
      
 84 
     | 
    
         
            +
                    diff_text = """diff --git a/file.py b/file.py
         
     | 
| 
      
 85 
     | 
    
         
            +
            --- a/file.py
         
     | 
| 
      
 86 
     | 
    
         
            +
            +++ b/file.py
         
     | 
| 
      
 87 
     | 
    
         
            +
            @@ -1,2 +1,2 @@
         
     | 
| 
      
 88 
     | 
    
         
            +
            -def old():
         
     | 
| 
      
 89 
     | 
    
         
            +
            -    pass
         
     | 
| 
      
 90 
     | 
    
         
            +
            +def new():
         
     | 
| 
      
 91 
     | 
    
         
            +
            +    pass"""
         
     | 
| 
      
 92 
     | 
    
         
            +
                    result = parse_diff_per_file(diff_text)
         
     | 
| 
      
 93 
     | 
    
         
            +
                    self.assertEqual(len(result), 1)
         
     | 
| 
      
 94 
     | 
    
         
            +
                    file_path, patch = result[0]
         
     | 
| 
      
 95 
     | 
    
         
            +
                    self.assertEqual(file_path, "file.py")
         
     | 
| 
      
 96 
     | 
    
         
            +
                    self.assertIn("def new():", patch)
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                def test_file_deletion(self):
         
     | 
| 
      
 99 
     | 
    
         
            +
                    diff_text = """diff --git a/old.py b/old.py
         
     | 
| 
      
 100 
     | 
    
         
            +
            --- a/old.py
         
     | 
| 
      
 101 
     | 
    
         
            +
            +++ /dev/null
         
     | 
| 
      
 102 
     | 
    
         
            +
            @@ -1,2 +0,0 @@
         
     | 
| 
      
 103 
     | 
    
         
            +
            -def old():
         
     | 
| 
      
 104 
     | 
    
         
            +
            -    pass"""
         
     | 
| 
      
 105 
     | 
    
         
            +
                    result = parse_diff_per_file(diff_text)
         
     | 
| 
      
 106 
     | 
    
         
            +
                    self.assertEqual(len(result), 1)
         
     | 
| 
      
 107 
     | 
    
         
            +
                    file_path, patch = result[0]
         
     | 
| 
      
 108 
     | 
    
         
            +
                    self.assertEqual(file_path, "old.py")
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                def test_multiple_files(self):
         
     | 
| 
      
 111 
     | 
    
         
            +
                    diff_text = """diff --git a/file1.py b/file1.py
         
     | 
| 
      
 112 
     | 
    
         
            +
            --- a/file1.py
         
     | 
| 
      
 113 
     | 
    
         
            +
            +++ b/file1.py
         
     | 
| 
      
 114 
     | 
    
         
            +
            @@ -1 +1 @@
         
     | 
| 
      
 115 
     | 
    
         
            +
            -print("Hello")
         
     | 
| 
      
 116 
     | 
    
         
            +
            +print("Hi")
         
     | 
| 
      
 117 
     | 
    
         
            +
            diff --git a/file2.py b/file2.py
         
     | 
| 
      
 118 
     | 
    
         
            +
            --- a/file2.py
         
     | 
| 
      
 119 
     | 
    
         
            +
            +++ b/file2.py
         
     | 
| 
      
 120 
     | 
    
         
            +
            @@ -1 +1 @@
         
     | 
| 
      
 121 
     | 
    
         
            +
            -print("World")
         
     | 
| 
      
 122 
     | 
    
         
            +
            +print("Earth")"""
         
     | 
| 
      
 123 
     | 
    
         
            +
                    result = parse_diff_per_file(diff_text)
         
     | 
| 
      
 124 
     | 
    
         
            +
                    self.assertEqual(len(result), 2)
         
     | 
| 
      
 125 
     | 
    
         
            +
                    paths = [fp for fp, _ in result]
         
     | 
| 
      
 126 
     | 
    
         
            +
                    self.assertIn("file1.py", paths)
         
     | 
| 
      
 127 
     | 
    
         
            +
                    self.assertIn("file2.py", paths)
         
     | 
| 
      
 128 
     | 
    
         
            +
             
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
            if __name__ == '__main__':
         
     | 
| 
      
 131 
     | 
    
         
            +
                unittest.main()
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     |