threadlines 0.2.3 → 0.2.6
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.
- package/dist/commands/check.js +134 -150
- package/dist/git/bitbucket.js +259 -0
- package/dist/git/repo.js +1 -1
- package/dist/types/git.js +5 -0
- package/dist/utils/context.js +42 -0
- package/dist/utils/environment.js +4 -1
- package/package.json +1 -1
- package/dist/git/context.js +0 -55
- package/dist/git/github-diff.js +0 -116
- package/dist/git/gitlab-diff.js +0 -86
- package/dist/git/local-diff.js +0 -53
- package/dist/git/vercel-diff.js +0 -42
- package/dist/utils/git-diff-executor.js +0 -65
- package/dist/utils/metadata.js +0 -290
package/dist/commands/check.js
CHANGED
|
@@ -44,6 +44,7 @@ const config_1 = require("../utils/config");
|
|
|
44
44
|
const environment_1 = require("../utils/environment");
|
|
45
45
|
const github_1 = require("../git/github");
|
|
46
46
|
const gitlab_1 = require("../git/gitlab");
|
|
47
|
+
const bitbucket_1 = require("../git/bitbucket");
|
|
47
48
|
const vercel_1 = require("../git/vercel");
|
|
48
49
|
const local_1 = require("../git/local");
|
|
49
50
|
const diff_1 = require("../git/diff");
|
|
@@ -51,6 +52,24 @@ const fs = __importStar(require("fs"));
|
|
|
51
52
|
const path = __importStar(require("path"));
|
|
52
53
|
const chalk_1 = __importDefault(require("chalk"));
|
|
53
54
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
55
|
+
/**
|
|
56
|
+
* Helper to get context for any environment.
|
|
57
|
+
* This centralizes the environment switch logic.
|
|
58
|
+
*/
|
|
59
|
+
async function getContextForEnvironment(environment, repoRoot, commitSha) {
|
|
60
|
+
switch (environment) {
|
|
61
|
+
case 'github':
|
|
62
|
+
return (0, github_1.getGitHubContext)(repoRoot);
|
|
63
|
+
case 'gitlab':
|
|
64
|
+
return (0, gitlab_1.getGitLabContext)(repoRoot);
|
|
65
|
+
case 'bitbucket':
|
|
66
|
+
return (0, bitbucket_1.getBitbucketContext)(repoRoot);
|
|
67
|
+
case 'vercel':
|
|
68
|
+
return (0, vercel_1.getVercelContext)(repoRoot);
|
|
69
|
+
default:
|
|
70
|
+
return (0, local_1.getLocalContext)(repoRoot, commitSha);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
54
73
|
// Get CLI version from package.json
|
|
55
74
|
const packageJsonPath = path.join(__dirname, '../../package.json');
|
|
56
75
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
@@ -103,9 +122,10 @@ async function checkCommand(options) {
|
|
|
103
122
|
console.log(chalk_1.default.gray(' 3. Make sure .env.local is in your .gitignore'));
|
|
104
123
|
console.log('');
|
|
105
124
|
console.log(chalk_1.default.white(' CI/CD:'));
|
|
106
|
-
console.log(chalk_1.default.gray(' GitHub Actions:
|
|
107
|
-
console.log(chalk_1.default.gray(' GitLab CI:
|
|
108
|
-
console.log(chalk_1.default.gray('
|
|
125
|
+
console.log(chalk_1.default.gray(' GitHub Actions: Settings → Secrets → Add variables'));
|
|
126
|
+
console.log(chalk_1.default.gray(' GitLab CI: Settings → CI/CD → Variables'));
|
|
127
|
+
console.log(chalk_1.default.gray(' Bitbucket Pipelines: Repository settings → Repository variables'));
|
|
128
|
+
console.log(chalk_1.default.gray(' Vercel: Settings → Environment Variables'));
|
|
109
129
|
console.log('');
|
|
110
130
|
console.log(chalk_1.default.gray('Get your credentials at: https://devthreadline.com/settings'));
|
|
111
131
|
process.exit(1);
|
|
@@ -126,155 +146,37 @@ async function checkCommand(options) {
|
|
|
126
146
|
let repoName;
|
|
127
147
|
let branchName;
|
|
128
148
|
let metadata = {};
|
|
129
|
-
//
|
|
149
|
+
// Check for explicit flags
|
|
130
150
|
const explicitFlags = [options.branch, options.commit, options.file, options.folder, options.files].filter(Boolean);
|
|
151
|
+
// Validate mutually exclusive flags
|
|
131
152
|
if (explicitFlags.length > 1) {
|
|
132
153
|
console.error(chalk_1.default.red('❌ Error: Only one review option can be specified at a time'));
|
|
133
154
|
console.log(chalk_1.default.gray(' Options: --branch, --commit, --file, --folder, --files'));
|
|
134
155
|
process.exit(1);
|
|
135
156
|
}
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
console.log(chalk_1.default.gray(`📝 Reading ${options.files.length} file(s)...`));
|
|
147
|
-
gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
|
|
148
|
-
}
|
|
149
|
-
else if (options.branch) {
|
|
150
|
-
console.log(chalk_1.default.gray(`📝 Collecting git changes for branch: ${options.branch}...`));
|
|
151
|
-
gitDiff = await (0, diff_1.getBranchDiff)(repoRoot, options.branch);
|
|
152
|
-
// Get repo/branch using environment-specific approach
|
|
153
|
-
if (environment === 'github') {
|
|
154
|
-
const gitContext = await (0, github_1.getGitHubContext)(repoRoot);
|
|
155
|
-
repoName = gitContext.repoName;
|
|
156
|
-
branchName = gitContext.branchName;
|
|
157
|
-
metadata = {
|
|
158
|
-
commitSha: gitContext.commitSha,
|
|
159
|
-
commitMessage: gitContext.commitMessage,
|
|
160
|
-
commitAuthorName: gitContext.commitAuthor.name,
|
|
161
|
-
commitAuthorEmail: gitContext.commitAuthor.email,
|
|
162
|
-
prTitle: gitContext.prTitle
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
else if (environment === 'gitlab') {
|
|
166
|
-
const gitContext = await (0, gitlab_1.getGitLabContext)(repoRoot);
|
|
167
|
-
repoName = gitContext.repoName;
|
|
168
|
-
branchName = gitContext.branchName;
|
|
169
|
-
metadata = {
|
|
170
|
-
commitSha: gitContext.commitSha,
|
|
171
|
-
commitMessage: gitContext.commitMessage,
|
|
172
|
-
commitAuthorName: gitContext.commitAuthor.name,
|
|
173
|
-
commitAuthorEmail: gitContext.commitAuthor.email,
|
|
174
|
-
prTitle: gitContext.prTitle
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
else if (environment === 'vercel') {
|
|
178
|
-
const gitContext = await (0, vercel_1.getVercelContext)(repoRoot);
|
|
179
|
-
repoName = gitContext.repoName;
|
|
180
|
-
branchName = gitContext.branchName;
|
|
181
|
-
metadata = {
|
|
182
|
-
commitSha: gitContext.commitSha,
|
|
183
|
-
commitMessage: gitContext.commitMessage,
|
|
184
|
-
commitAuthorName: gitContext.commitAuthor.name,
|
|
185
|
-
commitAuthorEmail: gitContext.commitAuthor.email
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
const gitContext = await (0, local_1.getLocalContext)(repoRoot);
|
|
190
|
-
repoName = gitContext.repoName;
|
|
191
|
-
branchName = gitContext.branchName;
|
|
192
|
-
metadata = {
|
|
193
|
-
commitSha: gitContext.commitSha,
|
|
194
|
-
commitMessage: gitContext.commitMessage,
|
|
195
|
-
commitAuthorName: gitContext.commitAuthor.name,
|
|
196
|
-
commitAuthorEmail: gitContext.commitAuthor.email
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
else if (options.commit) {
|
|
201
|
-
console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${options.commit}...`));
|
|
202
|
-
gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
|
|
203
|
-
// Get repo/branch using environment-specific approach
|
|
204
|
-
if (environment === 'github') {
|
|
205
|
-
const gitContext = await (0, github_1.getGitHubContext)(repoRoot);
|
|
206
|
-
repoName = gitContext.repoName;
|
|
207
|
-
branchName = gitContext.branchName;
|
|
208
|
-
metadata = {
|
|
209
|
-
commitSha: gitContext.commitSha,
|
|
210
|
-
commitMessage: gitContext.commitMessage,
|
|
211
|
-
commitAuthorName: gitContext.commitAuthor.name,
|
|
212
|
-
commitAuthorEmail: gitContext.commitAuthor.email,
|
|
213
|
-
prTitle: gitContext.prTitle
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
else if (environment === 'gitlab') {
|
|
217
|
-
const gitContext = await (0, gitlab_1.getGitLabContext)(repoRoot);
|
|
218
|
-
repoName = gitContext.repoName;
|
|
219
|
-
branchName = gitContext.branchName;
|
|
220
|
-
metadata = {
|
|
221
|
-
commitSha: gitContext.commitSha,
|
|
222
|
-
commitMessage: gitContext.commitMessage,
|
|
223
|
-
commitAuthorName: gitContext.commitAuthor.name,
|
|
224
|
-
commitAuthorEmail: gitContext.commitAuthor.email,
|
|
225
|
-
prTitle: gitContext.prTitle
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
else if (environment === 'vercel') {
|
|
229
|
-
const gitContext = await (0, vercel_1.getVercelContext)(repoRoot);
|
|
230
|
-
repoName = gitContext.repoName;
|
|
231
|
-
branchName = gitContext.branchName;
|
|
232
|
-
metadata = {
|
|
233
|
-
commitSha: gitContext.commitSha,
|
|
234
|
-
commitMessage: gitContext.commitMessage,
|
|
235
|
-
commitAuthorName: gitContext.commitAuthor.name,
|
|
236
|
-
commitAuthorEmail: gitContext.commitAuthor.email
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
const gitContext = await (0, local_1.getLocalContext)(repoRoot, options.commit);
|
|
241
|
-
repoName = gitContext.repoName;
|
|
242
|
-
branchName = gitContext.branchName;
|
|
243
|
-
metadata = {
|
|
244
|
-
commitSha: gitContext.commitSha,
|
|
245
|
-
commitMessage: gitContext.commitMessage,
|
|
246
|
-
commitAuthorName: gitContext.commitAuthor.name,
|
|
247
|
-
commitAuthorEmail: gitContext.commitAuthor.email
|
|
248
|
-
};
|
|
157
|
+
// CI environments: auto-detect only, flags are ignored with warning
|
|
158
|
+
// Local: full flag support for developer flexibility
|
|
159
|
+
if ((0, environment_1.isCIEnvironment)(environment)) {
|
|
160
|
+
// Warn if flags are passed in CI - they're meant for local development
|
|
161
|
+
if (explicitFlags.length > 0) {
|
|
162
|
+
const flagName = options.branch ? '--branch' :
|
|
163
|
+
options.commit ? '--commit' :
|
|
164
|
+
options.file ? '--file' :
|
|
165
|
+
options.folder ? '--folder' : '--files';
|
|
166
|
+
console.log(chalk_1.default.yellow(`⚠️ Warning: ${flagName} flag ignored in CI environment. Using auto-detection.\n`));
|
|
249
167
|
}
|
|
250
|
-
|
|
251
|
-
else {
|
|
252
|
-
// Auto-detect: Use environment-specific context collection (completely isolated)
|
|
168
|
+
// CI auto-detect: use environment-specific context
|
|
253
169
|
const envNames = {
|
|
254
170
|
vercel: 'Vercel',
|
|
255
|
-
github: 'GitHub',
|
|
256
|
-
gitlab: 'GitLab',
|
|
257
|
-
|
|
171
|
+
github: 'GitHub Actions',
|
|
172
|
+
gitlab: 'GitLab CI',
|
|
173
|
+
bitbucket: 'Bitbucket Pipelines'
|
|
258
174
|
};
|
|
259
175
|
console.log(chalk_1.default.gray(`📝 Collecting git context for ${envNames[environment]}...`));
|
|
260
|
-
|
|
261
|
-
let envContext;
|
|
262
|
-
if (environment === 'github') {
|
|
263
|
-
envContext = await (0, github_1.getGitHubContext)(repoRoot);
|
|
264
|
-
}
|
|
265
|
-
else if (environment === 'gitlab') {
|
|
266
|
-
envContext = await (0, gitlab_1.getGitLabContext)(repoRoot);
|
|
267
|
-
}
|
|
268
|
-
else if (environment === 'vercel') {
|
|
269
|
-
envContext = await (0, vercel_1.getVercelContext)(repoRoot);
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
envContext = await (0, local_1.getLocalContext)(repoRoot);
|
|
273
|
-
}
|
|
176
|
+
const envContext = await getContextForEnvironment(environment, repoRoot);
|
|
274
177
|
gitDiff = envContext.diff;
|
|
275
178
|
repoName = envContext.repoName;
|
|
276
179
|
branchName = envContext.branchName;
|
|
277
|
-
// Use metadata from environment context
|
|
278
180
|
metadata = {
|
|
279
181
|
commitSha: envContext.commitSha,
|
|
280
182
|
commitMessage: envContext.commitMessage,
|
|
@@ -283,6 +185,63 @@ async function checkCommand(options) {
|
|
|
283
185
|
prTitle: envContext.prTitle
|
|
284
186
|
};
|
|
285
187
|
}
|
|
188
|
+
else {
|
|
189
|
+
// Local environment: support all flags
|
|
190
|
+
if (options.file) {
|
|
191
|
+
console.log(chalk_1.default.gray(`📝 Reading file: ${options.file}...`));
|
|
192
|
+
gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
|
|
193
|
+
}
|
|
194
|
+
else if (options.folder) {
|
|
195
|
+
console.log(chalk_1.default.gray(`📝 Reading folder: ${options.folder}...`));
|
|
196
|
+
gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
|
|
197
|
+
}
|
|
198
|
+
else if (options.files && options.files.length > 0) {
|
|
199
|
+
console.log(chalk_1.default.gray(`📝 Reading ${options.files.length} file(s)...`));
|
|
200
|
+
gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
|
|
201
|
+
}
|
|
202
|
+
else if (options.branch) {
|
|
203
|
+
console.log(chalk_1.default.gray(`📝 Collecting git changes for branch: ${options.branch}...`));
|
|
204
|
+
gitDiff = await (0, diff_1.getBranchDiff)(repoRoot, options.branch);
|
|
205
|
+
// Use local context for metadata
|
|
206
|
+
const localContext = await (0, local_1.getLocalContext)(repoRoot);
|
|
207
|
+
repoName = localContext.repoName;
|
|
208
|
+
branchName = localContext.branchName;
|
|
209
|
+
metadata = {
|
|
210
|
+
commitSha: localContext.commitSha,
|
|
211
|
+
commitMessage: localContext.commitMessage,
|
|
212
|
+
commitAuthorName: localContext.commitAuthor.name,
|
|
213
|
+
commitAuthorEmail: localContext.commitAuthor.email
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
else if (options.commit) {
|
|
217
|
+
console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${options.commit}...`));
|
|
218
|
+
gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
|
|
219
|
+
// Use local context for metadata, passing commit SHA for author lookup
|
|
220
|
+
const localContext = await (0, local_1.getLocalContext)(repoRoot, options.commit);
|
|
221
|
+
repoName = localContext.repoName;
|
|
222
|
+
branchName = localContext.branchName;
|
|
223
|
+
metadata = {
|
|
224
|
+
commitSha: localContext.commitSha,
|
|
225
|
+
commitMessage: localContext.commitMessage,
|
|
226
|
+
commitAuthorName: localContext.commitAuthor.name,
|
|
227
|
+
commitAuthorEmail: localContext.commitAuthor.email
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// Local auto-detect: staged/unstaged changes
|
|
232
|
+
console.log(chalk_1.default.gray('📝 Collecting git context for Local...'));
|
|
233
|
+
const localContext = await (0, local_1.getLocalContext)(repoRoot);
|
|
234
|
+
gitDiff = localContext.diff;
|
|
235
|
+
repoName = localContext.repoName;
|
|
236
|
+
branchName = localContext.branchName;
|
|
237
|
+
metadata = {
|
|
238
|
+
commitSha: localContext.commitSha,
|
|
239
|
+
commitMessage: localContext.commitMessage,
|
|
240
|
+
commitAuthorName: localContext.commitAuthor.name,
|
|
241
|
+
commitAuthorEmail: localContext.commitAuthor.email
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
286
245
|
if (gitDiff.changedFiles.length === 0) {
|
|
287
246
|
console.error(chalk_1.default.bold('ℹ️ No changes detected.'));
|
|
288
247
|
process.exit(0);
|
|
@@ -345,9 +304,9 @@ async function checkCommand(options) {
|
|
|
345
304
|
});
|
|
346
305
|
// 7. Display results (with filtering if --full not specified)
|
|
347
306
|
displayResults(response, options.full || false);
|
|
348
|
-
// Exit with appropriate code
|
|
349
|
-
const
|
|
350
|
-
process.exit(
|
|
307
|
+
// Exit with appropriate code (attention or errors = failure)
|
|
308
|
+
const hasIssues = response.results.some(r => r.status === 'attention' || r.status === 'error');
|
|
309
|
+
process.exit(hasIssues ? 1 : 0);
|
|
351
310
|
}
|
|
352
311
|
catch (error) {
|
|
353
312
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -368,7 +327,10 @@ function displayResults(response, showFull) {
|
|
|
368
327
|
const notRelevant = results.filter((r) => r.status === 'not_relevant').length;
|
|
369
328
|
const compliant = results.filter((r) => r.status === 'compliant').length;
|
|
370
329
|
const attention = results.filter((r) => r.status === 'attention').length;
|
|
330
|
+
const errors = results.filter((r) => r.status === 'error').length;
|
|
371
331
|
const attentionItems = filteredResults.filter((r) => r.status === 'attention');
|
|
332
|
+
// Always show errors regardless of --full flag
|
|
333
|
+
const errorItems = results.filter((r) => r.status === 'error');
|
|
372
334
|
// Build summary parts
|
|
373
335
|
const summaryParts = [];
|
|
374
336
|
if (notRelevant > 0) {
|
|
@@ -383,15 +345,11 @@ function displayResults(response, showFull) {
|
|
|
383
345
|
if (metadata.timedOut > 0) {
|
|
384
346
|
summaryParts.push(`${metadata.timedOut} timed out`);
|
|
385
347
|
}
|
|
386
|
-
if (
|
|
387
|
-
summaryParts.push(`${
|
|
388
|
-
}
|
|
389
|
-
// Display informational message if present (e.g., zero diffs)
|
|
390
|
-
if (message) {
|
|
391
|
-
console.log('\n' + chalk_1.default.blue('ℹ️ ' + message));
|
|
348
|
+
if (errors > 0) {
|
|
349
|
+
summaryParts.push(`${errors} errors`);
|
|
392
350
|
}
|
|
393
351
|
// Show success message with breakdown if no issues
|
|
394
|
-
if (attention === 0 && metadata.timedOut === 0 &&
|
|
352
|
+
if (attention === 0 && metadata.timedOut === 0 && errors === 0) {
|
|
395
353
|
const summary = summaryParts.length > 0 ? ` (${summaryParts.join(', ')})` : '';
|
|
396
354
|
console.log('\n' + chalk_1.default.green(`✓ Threadline check passed${summary}`));
|
|
397
355
|
console.log(chalk_1.default.gray(` ${metadata.totalThreadlines} threadline${metadata.totalThreadlines !== 1 ? 's' : ''} checked\n`));
|
|
@@ -421,8 +379,8 @@ function displayResults(response, showFull) {
|
|
|
421
379
|
if (metadata.timedOut > 0) {
|
|
422
380
|
console.log(chalk_1.default.yellow(` ${metadata.timedOut} timed out`));
|
|
423
381
|
}
|
|
424
|
-
if (
|
|
425
|
-
console.log(chalk_1.default.red(` ${
|
|
382
|
+
if (errors > 0) {
|
|
383
|
+
console.log(chalk_1.default.red(` ${errors} errors`));
|
|
426
384
|
}
|
|
427
385
|
console.log('');
|
|
428
386
|
}
|
|
@@ -448,4 +406,30 @@ function displayResults(response, showFull) {
|
|
|
448
406
|
console.log(''); // Empty line between threadlines
|
|
449
407
|
}
|
|
450
408
|
}
|
|
409
|
+
// Show error items (always shown, regardless of --full flag)
|
|
410
|
+
if (errorItems.length > 0) {
|
|
411
|
+
for (const item of errorItems) {
|
|
412
|
+
console.log(chalk_1.default.red(`[error] ${item.expertId}`));
|
|
413
|
+
// Show error message
|
|
414
|
+
if (item.error) {
|
|
415
|
+
console.log(chalk_1.default.red(` Error: ${item.error.message}`));
|
|
416
|
+
if (item.error.type) {
|
|
417
|
+
console.log(chalk_1.default.red(` Type: ${item.error.type}`));
|
|
418
|
+
}
|
|
419
|
+
if (item.error.code) {
|
|
420
|
+
console.log(chalk_1.default.red(` Code: ${item.error.code}`));
|
|
421
|
+
}
|
|
422
|
+
// Show raw response for debugging
|
|
423
|
+
if (item.error.rawResponse) {
|
|
424
|
+
console.log(chalk_1.default.gray(' Raw response:'));
|
|
425
|
+
console.log(chalk_1.default.gray(JSON.stringify(item.error.rawResponse, null, 2).split('\n').map(line => ' ' + line).join('\n')));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else if (item.reasoning) {
|
|
429
|
+
// Fallback to reasoning if no error object
|
|
430
|
+
console.log(chalk_1.default.red(` ${item.reasoning}`));
|
|
431
|
+
}
|
|
432
|
+
console.log(''); // Empty line between errors
|
|
433
|
+
}
|
|
434
|
+
}
|
|
451
435
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bitbucket Pipelines Environment - Complete Isolation
|
|
4
|
+
*
|
|
5
|
+
* All Bitbucket-specific logic is contained in this file.
|
|
6
|
+
* No dependencies on other environment implementations.
|
|
7
|
+
*
|
|
8
|
+
* Exports a single function: getBitbucketContext() that returns:
|
|
9
|
+
* - diff: GitDiffResult
|
|
10
|
+
* - repoName: string
|
|
11
|
+
* - branchName: string
|
|
12
|
+
* - commitAuthor: { name: string; email: string }
|
|
13
|
+
* - prTitle?: string (PR title - not available in Bitbucket env vars)
|
|
14
|
+
*
|
|
15
|
+
* Implementation Status (all tested 2026-01-18):
|
|
16
|
+
* - ✅ Direct commit to main
|
|
17
|
+
* - ✅ Feature branch push
|
|
18
|
+
* - ✅ PR context
|
|
19
|
+
*/
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.getBitbucketContext = getBitbucketContext;
|
|
25
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
26
|
+
const child_process_1 = require("child_process");
|
|
27
|
+
const diff_1 = require("./diff");
|
|
28
|
+
/**
|
|
29
|
+
* Gets all Bitbucket context in one call
|
|
30
|
+
*/
|
|
31
|
+
async function getBitbucketContext(repoRoot) {
|
|
32
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
33
|
+
// Check if we're in a git repo
|
|
34
|
+
const isRepo = await git.checkIsRepo();
|
|
35
|
+
if (!isRepo) {
|
|
36
|
+
throw new Error('Not a git repository. Threadline requires a git repository.');
|
|
37
|
+
}
|
|
38
|
+
// Get all Bitbucket context
|
|
39
|
+
const diff = await getDiff(repoRoot);
|
|
40
|
+
const repoName = getRepoName();
|
|
41
|
+
const branchName = getBranchName();
|
|
42
|
+
const context = detectContext();
|
|
43
|
+
const commitSha = getCommitSha();
|
|
44
|
+
// Get commit author (from git log - Bitbucket doesn't provide this as env var)
|
|
45
|
+
const commitAuthor = await getCommitAuthor(repoRoot);
|
|
46
|
+
// Get commit message if we have a SHA
|
|
47
|
+
let commitMessage;
|
|
48
|
+
if (commitSha) {
|
|
49
|
+
const message = await (0, diff_1.getCommitMessage)(repoRoot, commitSha);
|
|
50
|
+
if (message) {
|
|
51
|
+
commitMessage = message;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
diff,
|
|
56
|
+
repoName,
|
|
57
|
+
branchName,
|
|
58
|
+
commitSha,
|
|
59
|
+
commitMessage,
|
|
60
|
+
commitAuthor,
|
|
61
|
+
prTitle: undefined, // Bitbucket doesn't expose PR title as env var
|
|
62
|
+
context
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get diff for Bitbucket Pipelines environment
|
|
67
|
+
*
|
|
68
|
+
* Bitbucket Pipelines with depth: full has full git history available,
|
|
69
|
+
* including origin/main. Unlike GitLab, no fetch is needed.
|
|
70
|
+
*
|
|
71
|
+
* Diff Strategy:
|
|
72
|
+
*
|
|
73
|
+
* | Scenario | Target Branch Known? | Diff Command |
|
|
74
|
+
* |---------------------------|---------------------------------------------|-------------------------------------------|
|
|
75
|
+
* | PR | ✅ Yes - BITBUCKET_PR_DESTINATION_BRANCH | origin/${destination}...HEAD |
|
|
76
|
+
* | Feature branch (no PR) | ❌ No - detect main/master | origin/main...HEAD or origin/master...HEAD|
|
|
77
|
+
* | Push to default branch | N/A | HEAD~1...HEAD |
|
|
78
|
+
*
|
|
79
|
+
* Key point: For PRs, Bitbucket provides BITBUCKET_PR_DESTINATION_BRANCH - this is the
|
|
80
|
+
* most relevant comparison point because it's where the code will be merged.
|
|
81
|
+
*
|
|
82
|
+
* For non-PR feature branches, Bitbucket does NOT provide a default branch env var
|
|
83
|
+
* (unlike GitLab's CI_DEFAULT_BRANCH), so we detect by checking if origin/main or
|
|
84
|
+
* origin/master exists.
|
|
85
|
+
*/
|
|
86
|
+
async function getDiff(repoRoot) {
|
|
87
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
88
|
+
const branchName = process.env.BITBUCKET_BRANCH;
|
|
89
|
+
const prId = process.env.BITBUCKET_PR_ID;
|
|
90
|
+
const prDestinationBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
|
|
91
|
+
// Scenario 1: PR context - use the target branch from env var
|
|
92
|
+
if (prId) {
|
|
93
|
+
if (!prDestinationBranch) {
|
|
94
|
+
throw new Error('Bitbucket PR context detected but BITBUCKET_PR_DESTINATION_BRANCH is not set. ' +
|
|
95
|
+
'This should be automatically provided by Bitbucket Pipelines.');
|
|
96
|
+
}
|
|
97
|
+
console.log(` [Bitbucket] PR #${prId}, using origin/${prDestinationBranch}...HEAD`);
|
|
98
|
+
const diff = await git.diff([`origin/${prDestinationBranch}...HEAD`, '-U200']);
|
|
99
|
+
const diffSummary = await git.diffSummary([`origin/${prDestinationBranch}...HEAD`]);
|
|
100
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
101
|
+
return { diff: diff || '', changedFiles };
|
|
102
|
+
}
|
|
103
|
+
// Scenario 2: Non-PR push
|
|
104
|
+
if (!branchName) {
|
|
105
|
+
throw new Error('Bitbucket Pipelines: BITBUCKET_BRANCH environment variable is not set. ' +
|
|
106
|
+
'This should be automatically provided by Bitbucket Pipelines.');
|
|
107
|
+
}
|
|
108
|
+
// Detect the default branch (Bitbucket doesn't provide this as an env var)
|
|
109
|
+
const defaultBranch = await detectDefaultBranch(git);
|
|
110
|
+
// If we're on the default branch, just show the last commit
|
|
111
|
+
if (branchName === defaultBranch) {
|
|
112
|
+
console.log(` [Bitbucket] Push to ${defaultBranch}, using HEAD~1...HEAD`);
|
|
113
|
+
const diff = await git.diff(['HEAD~1...HEAD', '-U200']);
|
|
114
|
+
const diffSummary = await git.diffSummary(['HEAD~1...HEAD']);
|
|
115
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
116
|
+
return { diff: diff || '', changedFiles };
|
|
117
|
+
}
|
|
118
|
+
// Feature branch: compare against default branch
|
|
119
|
+
// This shows all changes the branch introduces, correctly excluding
|
|
120
|
+
// any commits merged in from the default branch
|
|
121
|
+
console.log(` [Bitbucket] Feature branch "${branchName}", using origin/${defaultBranch}...HEAD`);
|
|
122
|
+
const diff = await git.diff([`origin/${defaultBranch}...HEAD`, '-U200']);
|
|
123
|
+
const diffSummary = await git.diffSummary([`origin/${defaultBranch}...HEAD`]);
|
|
124
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
125
|
+
return { diff: diff || '', changedFiles };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Detect the default branch for Bitbucket Pipelines.
|
|
129
|
+
*
|
|
130
|
+
* Bitbucket does NOT provide a default branch env var (unlike GitLab's CI_DEFAULT_BRANCH
|
|
131
|
+
* or GitHub's repository.default_branch in the event JSON).
|
|
132
|
+
*
|
|
133
|
+
* We try 'main' first (most common), then 'master' as fallback.
|
|
134
|
+
* This covers the vast majority of repositories.
|
|
135
|
+
*
|
|
136
|
+
* ---
|
|
137
|
+
* Design Decision: We compare against main instead of just checking the last commit
|
|
138
|
+
*
|
|
139
|
+
* Threadlines assumes that feature branches are intended to eventually merge to the
|
|
140
|
+
* default branch. Comparing against main shows ALL changes the branch introduces,
|
|
141
|
+
* which is what you want to review before merging.
|
|
142
|
+
*
|
|
143
|
+
* Per-commit checking happens during local development.
|
|
144
|
+
* ---
|
|
145
|
+
*/
|
|
146
|
+
async function detectDefaultBranch(git) {
|
|
147
|
+
// Try 'main' first (modern default)
|
|
148
|
+
try {
|
|
149
|
+
await git.revparse(['--verify', 'origin/main']);
|
|
150
|
+
return 'main';
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// origin/main doesn't exist, try master
|
|
154
|
+
}
|
|
155
|
+
// Try 'master' (legacy default)
|
|
156
|
+
try {
|
|
157
|
+
await git.revparse(['--verify', 'origin/master']);
|
|
158
|
+
return 'master';
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// origin/master doesn't exist either
|
|
162
|
+
}
|
|
163
|
+
throw new Error('Bitbucket Pipelines: Cannot determine default branch. ' +
|
|
164
|
+
'Neither origin/main nor origin/master found. ' +
|
|
165
|
+
'For repositories with a different default branch, create a PR to trigger branch comparison.');
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Gets repository name for Bitbucket Pipelines
|
|
169
|
+
*
|
|
170
|
+
* Uses BITBUCKET_REPO_FULL_NAME to construct the repo URL.
|
|
171
|
+
* Example: ngrootscholten/threadline -> https://bitbucket.org/ngrootscholten/threadline.git
|
|
172
|
+
*/
|
|
173
|
+
function getRepoName() {
|
|
174
|
+
const repoFullName = process.env.BITBUCKET_REPO_FULL_NAME;
|
|
175
|
+
if (!repoFullName) {
|
|
176
|
+
throw new Error('Bitbucket Pipelines: BITBUCKET_REPO_FULL_NAME environment variable is not set. ' +
|
|
177
|
+
'This should be automatically provided by Bitbucket Pipelines.');
|
|
178
|
+
}
|
|
179
|
+
return `https://bitbucket.org/${repoFullName}.git`;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Gets branch name for Bitbucket Pipelines
|
|
183
|
+
*/
|
|
184
|
+
function getBranchName() {
|
|
185
|
+
const branchName = process.env.BITBUCKET_BRANCH;
|
|
186
|
+
if (!branchName) {
|
|
187
|
+
throw new Error('Bitbucket Pipelines: BITBUCKET_BRANCH environment variable is not set. ' +
|
|
188
|
+
'This should be automatically provided by Bitbucket Pipelines.');
|
|
189
|
+
}
|
|
190
|
+
return branchName;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Detects Bitbucket context (PR, branch, or commit)
|
|
194
|
+
*/
|
|
195
|
+
function detectContext() {
|
|
196
|
+
// PR context
|
|
197
|
+
const prId = process.env.BITBUCKET_PR_ID;
|
|
198
|
+
const prDestinationBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
|
|
199
|
+
const sourceBranch = process.env.BITBUCKET_BRANCH;
|
|
200
|
+
if (prId && prDestinationBranch && sourceBranch) {
|
|
201
|
+
return {
|
|
202
|
+
type: 'pr',
|
|
203
|
+
prNumber: prId,
|
|
204
|
+
sourceBranch,
|
|
205
|
+
targetBranch: prDestinationBranch
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// Branch context
|
|
209
|
+
if (process.env.BITBUCKET_BRANCH) {
|
|
210
|
+
return {
|
|
211
|
+
type: 'branch',
|
|
212
|
+
branchName: process.env.BITBUCKET_BRANCH
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// Commit context
|
|
216
|
+
if (process.env.BITBUCKET_COMMIT) {
|
|
217
|
+
return {
|
|
218
|
+
type: 'commit',
|
|
219
|
+
commitSha: process.env.BITBUCKET_COMMIT
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
// Fallback to local (shouldn't happen in Bitbucket Pipelines)
|
|
223
|
+
return { type: 'local' };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Gets commit SHA from Bitbucket environment
|
|
227
|
+
*/
|
|
228
|
+
function getCommitSha() {
|
|
229
|
+
return process.env.BITBUCKET_COMMIT;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Gets commit author for Bitbucket Pipelines
|
|
233
|
+
*
|
|
234
|
+
* Bitbucket doesn't provide commit author as an environment variable,
|
|
235
|
+
* so we use git log to get it.
|
|
236
|
+
*
|
|
237
|
+
* This approach is verified by our test script (test-bitbucket-context.ts)
|
|
238
|
+
* which successfully retrieves commit author in all scenarios:
|
|
239
|
+
* - Direct commit to main
|
|
240
|
+
* - Feature branch push
|
|
241
|
+
* - PR pipeline
|
|
242
|
+
* - Merge commit
|
|
243
|
+
*/
|
|
244
|
+
async function getCommitAuthor(repoRoot) {
|
|
245
|
+
// Use raw git commands - this is exactly what the test script uses and we know it works
|
|
246
|
+
try {
|
|
247
|
+
const name = (0, child_process_1.execSync)('git log -1 --format=%an', { encoding: 'utf-8', cwd: repoRoot }).trim();
|
|
248
|
+
const email = (0, child_process_1.execSync)('git log -1 --format=%ae', { encoding: 'utf-8', cwd: repoRoot }).trim();
|
|
249
|
+
if (!name || !email) {
|
|
250
|
+
throw new Error('git log returned empty name or email');
|
|
251
|
+
}
|
|
252
|
+
return { name, email };
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
256
|
+
throw new Error(`Bitbucket Pipelines: Failed to get commit author from git log. ` +
|
|
257
|
+
`Error: ${errorMessage}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
package/dist/git/repo.js
CHANGED
|
@@ -213,7 +213,7 @@ async function getGitLabBranchName(_repoRoot) {
|
|
|
213
213
|
* Uses GITHUB_EVENT_PATH JSON (repository.default_branch) - the most authoritative source
|
|
214
214
|
* provided directly by GitHub Actions.
|
|
215
215
|
*
|
|
216
|
-
* This function is ONLY called from GitHub Actions context (
|
|
216
|
+
* This function is ONLY called from GitHub Actions context (github.ts),
|
|
217
217
|
* so GITHUB_EVENT_PATH should always be available. If it's not, we fail with a clear error.
|
|
218
218
|
*
|
|
219
219
|
* Returns the branch name (e.g., "main", "master") without the "origin/" prefix.
|