Harvest changes from a worktree. Returns null if clean or on error.
(testName: string)
| 150 | |
| 151 | /** Harvest changes from a worktree. Returns null if clean or on error. */ |
| 152 | harvest(testName: string): HarvestResult | null { |
| 153 | const info = this.active.get(testName); |
| 154 | if (!info) return null; |
| 155 | |
| 156 | try { |
| 157 | // Check if worktree directory still exists (agent may have deleted it) |
| 158 | if (!fs.existsSync(info.path)) { |
| 159 | process.stderr.write(` HARVEST [${testName}]: worktree dir deleted, skipping\n`); |
| 160 | return null; |
| 161 | } |
| 162 | |
| 163 | // Stage everything including untracked files |
| 164 | git(['-C', info.path, 'add', '-A'], info.path, true); |
| 165 | |
| 166 | // Get diff against original SHA (captures both committed and uncommitted changes) |
| 167 | const patch = git(['-C', info.path, 'diff', info.originalSha, '--cached'], info.path, true); |
| 168 | |
| 169 | if (!patch) return null; |
| 170 | |
| 171 | // Get diff stat for human-readable output |
| 172 | const diffStat = git(['-C', info.path, 'diff', info.originalSha, '--cached', '--stat'], info.path, true); |
| 173 | |
| 174 | // Get changed file names |
| 175 | const nameOnly = git(['-C', info.path, 'diff', info.originalSha, '--cached', '--name-only'], info.path, true); |
| 176 | const changedFiles = nameOnly.split('\n').filter(Boolean); |
| 177 | |
| 178 | // Dedup check |
| 179 | const hash = crypto.createHash('sha256').update(patch).digest('hex'); |
| 180 | const dedupIndex = loadDedupIndex(); |
| 181 | const isDuplicate = hash in dedupIndex.hashes; |
| 182 | |
| 183 | let patchPath = ''; |
| 184 | |
| 185 | if (!isDuplicate) { |
| 186 | // Save patch |
| 187 | const harvestDir = path.join(os.homedir(), '.gstack-dev', 'harvests', this.runId); |
| 188 | fs.mkdirSync(harvestDir, { recursive: true }); |
| 189 | patchPath = path.join(harvestDir, `${testName}.patch`); |
| 190 | fs.writeFileSync(patchPath, patch); |
| 191 | |
| 192 | // Update dedup index |
| 193 | dedupIndex.hashes[hash] = this.runId; |
| 194 | saveDedupIndex(dedupIndex); |
| 195 | } |
| 196 | |
| 197 | const result: HarvestResult = { |
| 198 | testName, |
| 199 | worktreePath: info.path, |
| 200 | diffStat, |
| 201 | patchPath, |
| 202 | changedFiles, |
| 203 | isDuplicate, |
| 204 | }; |
| 205 | |
| 206 | this.harvestResults.push(result); |
| 207 | return result; |
| 208 | } catch (err) { |
| 209 | process.stderr.write(` HARVEST [${testName}]: error — ${err}\n`); |
no test coverage detected