threadlines 0.2.24 → 0.2.25

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.
Files changed (2) hide show
  1. package/dist/git/diff.js +32 -36
  2. package/package.json +1 -1
package/dist/git/diff.js CHANGED
@@ -11,6 +11,7 @@ exports.getPRDiff = getPRDiff;
11
11
  exports.getCommitDiff = getCommitDiff;
12
12
  const simple_git_1 = __importDefault(require("simple-git"));
13
13
  const child_process_1 = require("child_process");
14
+ const logger_1 = require("../utils/logger");
14
15
  // =============================================================================
15
16
  // CORE GIT OPERATIONS
16
17
  // These functions use raw git commands and work reliably across all CI environments.
@@ -146,13 +147,17 @@ async function getCommitAuthor(repoRoot, sha) {
146
147
  *
147
148
  * This is a shared implementation for CI environments that do shallow clones.
148
149
  * Uses three-dots logic (merge base) to show only the developer's changes,
149
- * avoiding drift from main moving forward.
150
+ * with graceful fallback to two dots for shallow clones.
150
151
  *
151
152
  * Strategy:
152
153
  * 1. Fetch target branch: origin/${targetBranch}:refs/remotes/origin/${targetBranch}
153
- * 2. Find merge base (common ancestor) using git merge-base (plumbing command)
154
- * 3. Fetch merge base commit (always fetch, assume not available)
155
- * 4. Diff: ${mergeBase}..HEAD (shows only developer's changes, not changes from main)
154
+ * 2. Try three dots: git diff origin/${targetBranch}...HEAD (merge base comparison)
155
+ * - Shows only developer's changes (avoids drift from main moving forward)
156
+ * - Works when we have enough history (full clones or GitHub's merge commits)
157
+ * 3. Fallback to two dots: git diff origin/${targetBranch}..HEAD (direct comparison)
158
+ * - Used when shallow clone prevents merge base calculation
159
+ * - May include drift from main, but provides working diff instead of crashing
160
+ * - Warning is logged to inform user of potential drift
156
161
  *
157
162
  * Why three dots (merge base) instead of two dots (direct comparison)?
158
163
  * - Two dots: Shows all differences between target branch tip and HEAD
@@ -186,43 +191,34 @@ async function getPRDiff(repoRoot, targetBranch, logger) {
186
191
  `This is required for PR/MR diff comparison. ` +
187
192
  `Error: ${fetchError instanceof Error ? fetchError.message : 'Unknown error'}`);
188
193
  }
189
- // Find merge base (common ancestor) using plumbing command
190
- // Three dots logic: compare against merge base to show only developer's changes
191
- // This avoids drift from main moving forward (two dots would include those changes)
192
- let mergeBase;
194
+ // Try three dots (merge base) first - shows only developer's changes
195
+ // Falls back to two dots (direct comparison) if shallow clone prevents merge base calculation
196
+ let diff;
197
+ let changedFiles;
193
198
  try {
194
- mergeBase = (0, child_process_1.execSync)(`git merge-base origin/${targetBranch} HEAD`, {
195
- encoding: 'utf-8',
196
- cwd: repoRoot
197
- }).trim();
198
- if (!mergeBase || mergeBase.length !== 40) {
199
- throw new Error(`Invalid merge base SHA: "${mergeBase}"`);
200
- }
199
+ // Step 1: Try the "Perfect" Diff (Three Dots)
200
+ // This isolates developer changes by comparing against merge base
201
+ // Works when we have enough history (full clones or GitHub's merge commits)
202
+ logger?.debug(`Attempting three-dots diff (merge base): origin/${targetBranch}...HEAD`);
203
+ diff = await git.diff([`origin/${targetBranch}...HEAD`, '-U200']);
204
+ const diffSummary = await git.diffSummary([`origin/${targetBranch}...HEAD`]);
205
+ changedFiles = diffSummary.files.map(f => f.file);
201
206
  }
202
207
  catch (error) {
208
+ // Step 2: Fallback to "Risky" Diff (Two Dots)
209
+ // If three dots fails, it means we're in a shallow clone without enough history
210
+ // We accept the "two dot" risk (may include drift from main) rather than crashing
203
211
  const errorMessage = error instanceof Error ? error.message : String(error);
204
- throw new Error(`Failed to find merge base between origin/${targetBranch} and HEAD. ` +
205
- `This is required to show only the developer's changes (avoids drift from main). ` +
206
- `Error: ${errorMessage}`);
207
- }
208
- // Always fetch merge base (assume it's not available, fetch is fast/no-op if it is)
209
- // This ensures we have the merge base available for diff comparison
210
- logger?.debug(`Fetching merge base: ${mergeBase}`);
211
- try {
212
- await git.fetch(['origin', mergeBase, '--depth=1']);
213
- }
214
- catch (fetchError) {
215
- throw new Error(`Failed to fetch merge base ${mergeBase} from origin. ` +
216
- `This is required for PR/MR diff comparison in shallow clones. ` +
217
- `Ensure 'origin' remote is configured and accessible. ` +
218
- `Error: ${fetchError instanceof Error ? fetchError.message : 'Unknown error'}`);
212
+ logger_1.logger.warn(`Shallow clone detected: Cannot calculate merge base. ` +
213
+ `Diff may include unrelated changes from ${targetBranch} that occurred after branching. ` +
214
+ `Using direct comparison (two dots) as fallback.`);
215
+ logger?.debug(`Fallback error: ${errorMessage}`);
216
+ // Use two dots (direct comparison) - shows all differences between tips
217
+ logger?.debug(`Using two-dots diff (direct comparison): origin/${targetBranch}..HEAD`);
218
+ diff = await git.diff([`origin/${targetBranch}..HEAD`, '-U200']);
219
+ const diffSummary = await git.diffSummary([`origin/${targetBranch}..HEAD`]);
220
+ changedFiles = diffSummary.files.map(f => f.file);
219
221
  }
220
- // Use merge base for diff (three dots logic: shows only developer's changes)
221
- // git diff is plumbing command, ignores shallow boundaries
222
- logger?.debug(`Comparing ${mergeBase}..HEAD (merge base vs PR branch)`);
223
- const diff = await git.diff([`${mergeBase}..HEAD`, '-U200']);
224
- const diffSummary = await git.diffSummary([`${mergeBase}..HEAD`]);
225
- const changedFiles = diffSummary.files.map(f => f.file);
226
222
  return {
227
223
  diff: diff || '',
228
224
  changedFiles
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadlines",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "Threadlines CLI - AI-powered linter based on your natural language documentation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {