threadlines 0.2.22 → 0.2.23

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 +49 -8
  2. package/package.json +1 -1
package/dist/git/diff.js CHANGED
@@ -145,11 +145,22 @@ async function getCommitAuthor(repoRoot, sha) {
145
145
  * Get diff for a PR/MR context in CI environments.
146
146
  *
147
147
  * This is a shared implementation for CI environments that do shallow clones.
148
- * It fetches the target branch on-demand and compares it against HEAD.
148
+ * Uses three-dots logic (merge base) to show only the developer's changes,
149
+ * avoiding drift from main moving forward.
149
150
  *
150
151
  * Strategy:
151
152
  * 1. Fetch target branch: origin/${targetBranch}:refs/remotes/origin/${targetBranch}
152
- * 2. Diff: origin/${targetBranch}..HEAD (two dots = direct comparison)
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)
156
+ *
157
+ * Why three dots (merge base) instead of two dots (direct comparison)?
158
+ * - Two dots: Shows all differences between target branch tip and HEAD
159
+ * - Includes changes that happened in main since branching (drift)
160
+ * - Can show files the developer didn't touch
161
+ * - Three dots: Shows only changes from merge base to HEAD
162
+ * - Shows only what the developer actually changed
163
+ * - Industry standard for "change detection" in PR/MR reviews
153
164
  *
154
165
  * Why HEAD instead of origin/${sourceBranch}?
155
166
  * - CI shallow clones only have HEAD available by default
@@ -175,12 +186,42 @@ async function getPRDiff(repoRoot, targetBranch, logger) {
175
186
  `This is required for PR/MR diff comparison. ` +
176
187
  `Error: ${fetchError instanceof Error ? fetchError.message : 'Unknown error'}`);
177
188
  }
178
- // Use two dots (..) for direct comparison (same as GitHub)
179
- // Two dots: shows all changes in HEAD that aren't in origin/${targetBranch}
180
- // Three dots: requires finding merge base which can fail with shallow clones
181
- logger?.debug(`Comparing origin/${targetBranch}..HEAD`);
182
- const diff = await git.diff([`origin/${targetBranch}..HEAD`, '-U200']);
183
- const diffSummary = await git.diffSummary([`origin/${targetBranch}..HEAD`]);
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;
193
+ 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
+ }
201
+ }
202
+ catch (error) {
203
+ 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'}`);
219
+ }
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`]);
184
225
  const changedFiles = diffSummary.files.map(f => f.file);
185
226
  return {
186
227
  diff: diff || '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadlines",
3
- "version": "0.2.22",
3
+ "version": "0.2.23",
4
4
  "description": "Threadlines CLI - AI-powered linter based on your natural language documentation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {