• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

agentic-dev-library / thumbcode / 22324268802

23 Feb 2026 08:48PM UTC coverage: 58.65% (+3.1%) from 55.51%
22324268802

push

github

jbdevprimary
chore: update PRD status, clean ralph session, fix formatting

- Update prd.json to reflect 9/24 stories completed and merged to main
- Remove stale ralph-tui session files (iterations/, session.json)
- Apply Biome auto-formatting fixes to anthropic-client.ts and check-contrast.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

1600 of 3067 branches covered (52.17%)

Branch coverage included in aggregate %.

0 of 2 new or added lines in 1 file covered. (0.0%)

175 existing lines in 29 files now uncovered.

2597 of 4089 relevant lines covered (63.51%)

41.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.63
/packages/core/src/git/GitDiffService.ts
1
/**
2
 * Git Diff Service
3
 *
4
 * Handles diff and status operations: diff, diffWorkingDir, status.
5
 */
6

7
import { Directory, Encoding, Filesystem } from '@capacitor/filesystem';
8
import * as Diff from 'diff';
9
import git from 'isomorphic-git';
10

11
import { fs } from './git-fs';
12
import type { DiffResult, DiffStats, FileDiff, FileStatus, GitResult } from './types';
13

14
/**
15
 * Helper to read file content from a tree at specific commit
16
 */
17
async function readBlobContent(dir: string, oid: string, filepath: string): Promise<string | null> {
18
  try {
206✔
19
    const { blob } = await git.readBlob({
206✔
20
      fs,
21
      dir,
22
      oid,
23
      filepath,
24
    });
25
    return new TextDecoder().decode(blob);
206✔
26
  } catch {
UNCOV
27
    return null;
×
28
  }
29
}
30

31
/**
32
 * Generate unified diff patch between two strings
33
 */
34
function createUnifiedPatch(
35
  filepath: string,
36
  oldContent: string,
37
  newContent: string
38
): { patch: string; additions: number; deletions: number } {
39
  const patch = Diff.createPatch(filepath, oldContent, newContent, 'old', 'new');
106✔
40

41
  // Count additions and deletions
42
  let additions = 0;
106✔
43
  let deletions = 0;
106✔
44
  const lines = patch.split('\n');
106✔
45
  for (const line of lines) {
106✔
46
    if (line.startsWith('+') && !line.startsWith('+++')) {
1,436✔
47
      additions++;
306✔
48
    } else if (line.startsWith('-') && !line.startsWith('---')) {
1,130✔
49
      deletions++;
206✔
50
    }
51
  }
52

53
  return { patch, additions, deletions };
106✔
54
}
55

56
class GitDiffServiceClass {
57
  /**
58
   * Get file status for the repository
59
   */
60
  async status(dir: string): Promise<GitResult<FileStatus[]>> {
61
    try {
6✔
62
      const matrix = await git.statusMatrix({ fs, dir });
6✔
63

64
      const statuses: FileStatus[] = matrix.map(([filepath, head, workdir, stage]) => {
5✔
65
        let status: FileStatus['status'];
66
        let staged = false;
9✔
67

68
        // Interpret status matrix
69
        // [HEAD, WORKDIR, STAGE]
70
        if (head === 0 && workdir === 2 && stage === 0) {
9✔
71
          status = 'untracked';
1✔
72
        } else if (head === 0 && workdir === 2 && stage === 2) {
8✔
73
          status = 'added';
1✔
74
          staged = true;
1✔
75
        } else if (head === 1 && workdir === 0 && stage === 0) {
7✔
76
          status = 'deleted';
1✔
77
        } else if (head === 1 && workdir === 0 && stage === 3) {
6✔
78
          status = 'deleted';
1✔
79
          staged = true;
1✔
80
        } else if (head === 1 && workdir === 2 && stage === 1) {
5✔
81
          status = 'modified';
1✔
82
        } else if (head === 1 && workdir === 2 && stage === 2) {
4✔
83
          status = 'modified';
1✔
84
          staged = true;
1✔
85
        } else if (head === 1 && workdir === 1 && stage === 1) {
3✔
86
          status = 'unmodified';
2✔
87
        } else {
88
          status = 'modified';
1✔
89
        }
90

91
        return {
9✔
92
          path: filepath,
93
          status,
94
          staged,
95
        };
96
      });
97

98
      // Filter out unmodified files for cleaner output
99
      return {
5✔
100
        success: true,
101
        data: statuses.filter((s) => s.status !== 'unmodified'),
9✔
102
      };
103
    } catch (error) {
104
      return {
1✔
105
        success: false,
106
        error: error instanceof Error ? error.message : 'Failed to get status',
1!
107
      };
108
    }
109
  }
110

111
  /**
112
   * Get diff between two commits using tree walking
113
   * Performs full recursive tree comparison and generates unified diffs
114
   */
115
  async diff(dir: string, commitA: string, commitB: string): Promise<GitResult<DiffResult>> {
116
    try {
5✔
117
      // Resolve refs to commit OIDs
118
      const oidA = await git.resolveRef({ fs, dir, ref: commitA });
5✔
119
      const oidB = await git.resolveRef({ fs, dir, ref: commitB });
4✔
120

121
      // Read commit objects to get tree OIDs
122
      const commitObjA = await git.readCommit({ fs, dir, oid: oidA });
4✔
123
      const commitObjB = await git.readCommit({ fs, dir, oid: oidB });
4✔
124

125
      const treeA = commitObjA.commit.tree;
4✔
126
      const treeB = commitObjB.commit.tree;
4✔
127

128
      // Walk both trees simultaneously to find differences
129
      const files: FileDiff[] = [];
4✔
130
      const filesInA = new Map<string, string>(); // filepath -> blob oid
4✔
131
      const filesInB = new Map<string, string>(); // filepath -> blob oid
4✔
132

133
      // Walk tree A recursively
134
      await this.walkTree(dir, treeA, '', filesInA);
4✔
135
      // Walk tree B recursively
136
      await this.walkTree(dir, treeB, '', filesInB);
4✔
137

138
      // Find all unique files
139
      const allFiles = new Set([...filesInA.keys(), ...filesInB.keys()]);
4✔
140

141
      let totalAdditions = 0;
4✔
142
      let totalDeletions = 0;
4✔
143

144
      // Compare each file
145
      const allFilesArray = Array.from(allFiles);
4✔
146
      const BATCH_SIZE = 20;
4✔
147

148
      for (let i = 0; i < allFilesArray.length; i += BATCH_SIZE) {
4✔
149
        const batch = allFilesArray.slice(i, i + BATCH_SIZE);
8✔
150
        await Promise.all(
8✔
151
          batch.map(async (filepath) => {
152
            const blobA = filesInA.get(filepath);
104✔
153
            const blobB = filesInB.get(filepath);
104✔
154

155
            if (blobA === blobB) {
104✔
156
              // File unchanged
157
              return;
1✔
158
            }
159

160
            let type: FileDiff['type'];
161
            let oldContent = '';
103✔
162
            let newContent = '';
103✔
163

164
            if (!blobA && blobB) {
103✔
165
              // File added in B
166
              type = 'add';
1✔
167
              newContent = (await readBlobContent(dir, oidB, filepath)) || '';
1!
168
            } else if (blobA && !blobB) {
102✔
169
              // File deleted in B
170
              type = 'delete';
1✔
171
              oldContent = (await readBlobContent(dir, oidA, filepath)) || '';
1!
172
            } else {
173
              // File modified
174
              type = 'modify';
101✔
175
              oldContent = (await readBlobContent(dir, oidA, filepath)) || '';
101!
176
              newContent = (await readBlobContent(dir, oidB, filepath)) || '';
101!
177
            }
178

179
            // Generate unified diff
180
            const { patch, additions, deletions } = createUnifiedPatch(
103✔
181
              filepath,
182
              oldContent,
183
              newContent
184
            );
185

186
            totalAdditions += additions;
103✔
187
            totalDeletions += deletions;
103✔
188

189
            files.push({
103✔
190
              path: filepath,
191
              type,
192
              additions,
193
              deletions,
194
              patch,
195
            });
196
          })
197
        );
198
      }
199

200
      const stats: DiffStats = {
8✔
201
        filesChanged: files.length,
202
        additions: totalAdditions,
203
        deletions: totalDeletions,
204
      };
205

206
      return {
8✔
207
        success: true,
208
        data: {
209
          files,
210
          stats,
211
        },
212
      };
213
    } catch (error) {
214
      return {
1✔
215
        success: false,
216
        error: error instanceof Error ? error.message : 'Failed to generate diff',
1!
217
      };
218
    }
219
  }
220

221
  /**
222
   * Recursively walk a git tree and collect all file paths with their blob OIDs
223
   */
224
  private async walkTree(
225
    dir: string,
226
    treeOid: string,
227
    prefix: string,
228
    result: Map<string, string>
229
  ): Promise<void> {
230
    const tree = await git.readTree({ fs, dir, oid: treeOid });
8✔
231

232
    for (const entry of tree.tree) {
8✔
233
      const fullPath = prefix ? `${prefix}/${entry.path}` : entry.path;
206!
234

235
      if (entry.type === 'blob') {
206!
236
        result.set(fullPath, entry.oid);
206✔
UNCOV
237
      } else if (entry.type === 'tree') {
×
238
        // Recursively walk subtrees
UNCOV
239
        await this.walkTree(dir, entry.oid, fullPath, result);
×
240
      }
241
    }
242
  }
243

244
  /**
245
   * Get diff for working directory changes (staged and unstaged)
246
   */
247
  async diffWorkingDir(dir: string): Promise<GitResult<DiffResult>> {
248
    try {
4✔
249
      const matrix = await git.statusMatrix({ fs, dir });
4✔
250
      const headOid = await git.resolveRef({ fs, dir, ref: 'HEAD' });
3✔
251

252
      const results = await Promise.all(
3✔
253
        matrix.map(async ([filepath, head, workdir, _stage]) => {
254
          // Skip unmodified files
255
          if (head === 1 && workdir === 1) return null;
5✔
256

257
          let type: FileDiff['type'];
258
          let oldContent = '';
3✔
259
          let newContent = '';
3✔
260

261
          if (head === 0 && workdir === 2) {
3✔
262
            // New file (untracked or added)
263
            type = 'add';
1✔
264
            try {
1✔
265
              const fileResult = await Filesystem.readFile({
1✔
266
                path: `${dir}/${filepath}`,
267
                directory: Directory.Documents,
268
                encoding: Encoding.UTF8,
269
              });
270
              newContent = fileResult.data as string;
1✔
271
            } catch {
UNCOV
272
              newContent = '';
×
273
            }
274
          } else if (head === 1 && workdir === 0) {
2✔
275
            // Deleted file
276
            type = 'delete';
1✔
277
            oldContent = (await readBlobContent(dir, headOid, filepath)) || '';
1!
278
          } else {
279
            // Modified file
280
            type = 'modify';
1✔
281
            oldContent = (await readBlobContent(dir, headOid, filepath)) || '';
1!
282
            try {
1✔
283
              const fileResult = await Filesystem.readFile({
1✔
284
                path: `${dir}/${filepath}`,
285
                directory: Directory.Documents,
286
                encoding: Encoding.UTF8,
287
              });
288
              newContent = fileResult.data as string;
1✔
289
            } catch {
UNCOV
290
              newContent = '';
×
291
            }
292
          }
293

294
          const { patch, additions, deletions } = createUnifiedPatch(
3✔
295
            filepath,
296
            oldContent,
297
            newContent
298
          );
299

300
          return {
3✔
301
            path: filepath,
302
            type,
303
            additions,
304
            deletions,
305
            patch,
306
          };
307
        })
308
      );
309

310
      const files: FileDiff[] = [];
3✔
311
      let totalAdditions = 0;
3✔
312
      let totalDeletions = 0;
3✔
313

314
      for (const result of results) {
3✔
315
        if (result) {
5✔
316
          files.push(result);
3✔
317
          totalAdditions += result.additions;
3✔
318
          totalDeletions += result.deletions;
3✔
319
        }
320
      }
321

322
      return {
3✔
323
        success: true,
324
        data: {
325
          files,
326
          stats: {
327
            filesChanged: files.length,
328
            additions: totalAdditions,
329
            deletions: totalDeletions,
330
          },
331
        },
332
      };
333
    } catch (error) {
334
      return {
1✔
335
        success: false,
336
        error: error instanceof Error ? error.message : 'Failed to generate working directory diff',
1!
337
      };
338
    }
339
  }
340
}
341

342
export const GitDiffService = new GitDiffServiceClass();
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc