|
1 | 1 | import * as vscode from 'vscode' |
2 | 2 | import { exec } from 'child_process' |
3 | 3 | import { Logger } from '@shared/utils/logger' |
| 4 | +import * as crypto from 'crypto' |
| 5 | +import * as fs from 'fs/promises' |
| 6 | +import * as os from 'os' |
| 7 | +import * as path from 'path' |
4 | 8 |
|
5 | 9 | const execAsync = (command: string, options: any): Promise<string> => { |
6 | 10 | return new Promise((resolve, reject) => { |
@@ -55,64 +59,107 @@ export const get_git_diff = async ( |
55 | 59 | workspace_folder: vscode.WorkspaceFolder |
56 | 60 | ): Promise<string | null> => { |
57 | 61 | try { |
58 | | - let diff = '' |
59 | | - try { |
60 | | - // Add --binary to handle binary files correctly. |
61 | | - // execAsync will reject if there are differences (exit code 1), so we catch it. |
62 | | - diff = await execAsync('git diff --binary HEAD', { |
63 | | - cwd: workspace_folder.uri.fsPath, |
| 62 | + const cwd = workspace_folder.uri.fsPath |
| 63 | + const tmp_dir = os.tmpdir() |
| 64 | + |
| 65 | + // Get list of changed tracked files |
| 66 | + const tracked_files_output = await execAsync( |
| 67 | + 'git diff -z --name-only HEAD', |
| 68 | + { |
| 69 | + cwd, |
64 | 70 | encoding: 'utf8', |
65 | 71 | maxBuffer: 50 * 1024 * 1024 // 50MB max |
66 | | - }) |
67 | | - } catch (e: any) { |
68 | | - if (e.status == 1 && e.stdout) { |
69 | | - // Exit code 1 means there are differences, which is not an error for us. |
70 | | - diff = e.stdout.toString() |
71 | | - } else { |
72 | | - // Other exit codes indicate a real error. |
73 | | - throw e |
74 | 72 | } |
75 | | - } |
| 73 | + ) |
| 74 | + const tracked_files = tracked_files_output |
| 75 | + .split('\0') |
| 76 | + .filter((f) => f.length > 0) |
76 | 77 |
|
77 | | - const untracked_files = ( |
78 | | - await execAsync('git ls-files --others --exclude-standard', { |
79 | | - cwd: workspace_folder.uri.fsPath, |
| 78 | + // Get list of untracked files |
| 79 | + const untracked_files_output = await execAsync( |
| 80 | + 'git ls-files -z --others --exclude-standard', |
| 81 | + { |
| 82 | + cwd, |
80 | 83 | encoding: 'utf8' |
81 | | - }) |
| 84 | + } |
82 | 85 | ) |
83 | | - .trim() |
84 | | - .split('\n') |
| 86 | + const untracked_files = untracked_files_output |
| 87 | + .split('\0') |
85 | 88 | .filter((f) => f.length > 0) |
86 | 89 |
|
87 | | - let untracked_diff = '' |
88 | | - for (const file of untracked_files) { |
| 90 | + const all_files = [ |
| 91 | + ...tracked_files.map((f) => ({ file: f, is_untracked: false })), |
| 92 | + ...untracked_files.map((f) => ({ file: f, is_untracked: true })) |
| 93 | + ] |
| 94 | + |
| 95 | + let total_diff = '' |
| 96 | + |
| 97 | + for (const { file, is_untracked } of all_files) { |
| 98 | + const absolute_path = path.join(cwd, file) |
| 99 | + let mtime = 0 |
| 100 | + |
89 | 101 | try { |
90 | | - // Use git diff to create a patch for the new file, which handles binary files correctly. |
91 | | - // execAsync will throw if there are differences, but the diff will be in stdout. |
92 | | - const file_diff = await execAsync( |
93 | | - `git diff --no-index --binary /dev/null "${file}"`, |
94 | | - { |
95 | | - cwd: workspace_folder.uri.fsPath, |
96 | | - encoding: 'utf8', |
97 | | - maxBuffer: 50 * 1024 * 1024 |
98 | | - } |
99 | | - ) |
100 | | - untracked_diff += file_diff |
| 102 | + const stats = await fs.stat(absolute_path) |
| 103 | + mtime = Math.floor(stats.mtimeMs) |
| 104 | + } catch { |
| 105 | + // File might be deleted or inaccessible |
| 106 | + } |
| 107 | + |
| 108 | + let diff_chunk = '' |
| 109 | + let cache_path = '' |
| 110 | + |
| 111 | + if (mtime > 0) { |
| 112 | + const path_hash = crypto |
| 113 | + .createHash('md5') |
| 114 | + .update(absolute_path) |
| 115 | + .digest('hex') |
| 116 | + const cache_filename = `cwc-checkpoint-cache-${path_hash}-${mtime}.txt` |
| 117 | + cache_path = path.join(tmp_dir, cache_filename) |
| 118 | + |
| 119 | + try { |
| 120 | + const cached_diff = await fs.readFile(cache_path, 'utf8') |
| 121 | + total_diff += cached_diff |
| 122 | + continue |
| 123 | + } catch { |
| 124 | + // Cache miss |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + try { |
| 129 | + const cmd = is_untracked |
| 130 | + ? `git diff --no-index --binary /dev/null "${file}"` |
| 131 | + : `git diff --binary HEAD -- "${file}"` |
| 132 | + |
| 133 | + diff_chunk = await execAsync(cmd, { |
| 134 | + cwd, |
| 135 | + encoding: 'utf8', |
| 136 | + maxBuffer: 50 * 1024 * 1024 |
| 137 | + }) |
101 | 138 | } catch (err: any) { |
102 | | - if (err.status == 1 && err.stdout) { |
103 | | - // The diff output is in stdout of the error object |
104 | | - untracked_diff += err.stdout.toString() |
| 139 | + if (err.status === 1 && err.stdout) { |
| 140 | + diff_chunk = err.stdout.toString() |
105 | 141 | } else { |
106 | 142 | Logger.warn({ |
107 | 143 | function_name: 'get_git_diff', |
108 | | - message: `Could not create diff for untracked file: ${file}`, |
| 144 | + message: `Could not create diff for file: ${file}`, |
109 | 145 | data: err |
110 | 146 | }) |
111 | 147 | } |
112 | 148 | } |
| 149 | + |
| 150 | + if (diff_chunk) { |
| 151 | + total_diff += diff_chunk |
| 152 | + if (mtime > 0 && cache_path) { |
| 153 | + try { |
| 154 | + await fs.writeFile(cache_path, diff_chunk, 'utf8') |
| 155 | + } catch (e) { |
| 156 | + // Ignore cache write errors |
| 157 | + } |
| 158 | + } |
| 159 | + } |
113 | 160 | } |
114 | 161 |
|
115 | | - return diff + untracked_diff |
| 162 | + return total_diff |
116 | 163 | } catch (error) { |
117 | 164 | Logger.error({ |
118 | 165 | function_name: 'get_git_diff', |
|
0 commit comments