threadlines 0.2.4 → 0.2.7
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 +83 -139
- package/dist/git/bitbucket.js +185 -0
- package/dist/git/diff.js +0 -196
- package/dist/git/github.js +25 -52
- package/dist/git/gitlab.js +21 -48
- package/dist/git/local.js +2 -2
- package/dist/git/vercel.js +2 -2
- package/dist/index.js +2 -4
- package/dist/types/git.js +5 -0
- package/dist/utils/context.js +4 -142
- 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/repo.js +0 -253
- 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,36 @@ async function checkCommand(options) {
|
|
|
126
146
|
let repoName;
|
|
127
147
|
let branchName;
|
|
128
148
|
let metadata = {};
|
|
149
|
+
// Check for explicit flags
|
|
150
|
+
const explicitFlags = [options.commit, options.file, options.folder, options.files].filter(Boolean);
|
|
129
151
|
// Validate mutually exclusive flags
|
|
130
|
-
const explicitFlags = [options.branch, options.commit, options.file, options.folder, options.files].filter(Boolean);
|
|
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
|
-
console.log(chalk_1.default.gray(' Options: --
|
|
154
|
+
console.log(chalk_1.default.gray(' Options: --commit, --file, --folder, --files'));
|
|
134
155
|
process.exit(1);
|
|
135
156
|
}
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
else if (options.files && options.files.length > 0) {
|
|
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
|
-
};
|
|
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.commit ? '--commit' :
|
|
163
|
+
options.file ? '--file' :
|
|
164
|
+
options.folder ? '--folder' : '--files';
|
|
165
|
+
console.log(chalk_1.default.yellow(`⚠️ Warning: ${flagName} flag ignored in CI environment. Using auto-detection.\n`));
|
|
238
166
|
}
|
|
239
|
-
|
|
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
|
-
};
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
// Auto-detect: Use environment-specific context collection (completely isolated)
|
|
167
|
+
// CI auto-detect: use environment-specific context
|
|
253
168
|
const envNames = {
|
|
254
169
|
vercel: 'Vercel',
|
|
255
|
-
github: 'GitHub',
|
|
256
|
-
gitlab: 'GitLab',
|
|
257
|
-
|
|
170
|
+
github: 'GitHub Actions',
|
|
171
|
+
gitlab: 'GitLab CI',
|
|
172
|
+
bitbucket: 'Bitbucket Pipelines'
|
|
258
173
|
};
|
|
259
174
|
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
|
-
}
|
|
175
|
+
const envContext = await getContextForEnvironment(environment, repoRoot);
|
|
274
176
|
gitDiff = envContext.diff;
|
|
275
177
|
repoName = envContext.repoName;
|
|
276
178
|
branchName = envContext.branchName;
|
|
277
|
-
// Use metadata from environment context
|
|
278
179
|
metadata = {
|
|
279
180
|
commitSha: envContext.commitSha,
|
|
280
181
|
commitMessage: envContext.commitMessage,
|
|
@@ -283,6 +184,49 @@ async function checkCommand(options) {
|
|
|
283
184
|
prTitle: envContext.prTitle
|
|
284
185
|
};
|
|
285
186
|
}
|
|
187
|
+
else {
|
|
188
|
+
// Local environment: support all flags
|
|
189
|
+
if (options.file) {
|
|
190
|
+
console.log(chalk_1.default.gray(`📝 Reading file: ${options.file}...`));
|
|
191
|
+
gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
|
|
192
|
+
}
|
|
193
|
+
else if (options.folder) {
|
|
194
|
+
console.log(chalk_1.default.gray(`📝 Reading folder: ${options.folder}...`));
|
|
195
|
+
gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
|
|
196
|
+
}
|
|
197
|
+
else if (options.files && options.files.length > 0) {
|
|
198
|
+
console.log(chalk_1.default.gray(`📝 Reading ${options.files.length} file(s)...`));
|
|
199
|
+
gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
|
|
200
|
+
}
|
|
201
|
+
else if (options.commit) {
|
|
202
|
+
console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${options.commit}...`));
|
|
203
|
+
gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
|
|
204
|
+
// Use local context for metadata, passing commit SHA for author lookup
|
|
205
|
+
const localContext = await (0, local_1.getLocalContext)(repoRoot, options.commit);
|
|
206
|
+
repoName = localContext.repoName;
|
|
207
|
+
branchName = localContext.branchName;
|
|
208
|
+
metadata = {
|
|
209
|
+
commitSha: localContext.commitSha,
|
|
210
|
+
commitMessage: localContext.commitMessage,
|
|
211
|
+
commitAuthorName: localContext.commitAuthor.name,
|
|
212
|
+
commitAuthorEmail: localContext.commitAuthor.email
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// Local auto-detect: staged/unstaged changes
|
|
217
|
+
console.log(chalk_1.default.gray('📝 Collecting git context for Local...'));
|
|
218
|
+
const localContext = await (0, local_1.getLocalContext)(repoRoot);
|
|
219
|
+
gitDiff = localContext.diff;
|
|
220
|
+
repoName = localContext.repoName;
|
|
221
|
+
branchName = localContext.branchName;
|
|
222
|
+
metadata = {
|
|
223
|
+
commitSha: localContext.commitSha,
|
|
224
|
+
commitMessage: localContext.commitMessage,
|
|
225
|
+
commitAuthorName: localContext.commitAuthor.name,
|
|
226
|
+
commitAuthorEmail: localContext.commitAuthor.email
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
286
230
|
if (gitDiff.changedFiles.length === 0) {
|
|
287
231
|
console.error(chalk_1.default.bold('ℹ️ No changes detected.'));
|
|
288
232
|
process.exit(0);
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bitbucket Pipelines Environment
|
|
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
|
+
* Strategy:
|
|
69
|
+
* - PR context: Compare source branch vs target branch (full PR diff)
|
|
70
|
+
* - Any push (main or feature branch): Compare last commit only (HEAD~1...HEAD)
|
|
71
|
+
*
|
|
72
|
+
* Note: Bitbucket Pipelines with depth: full has full git history available.
|
|
73
|
+
*/
|
|
74
|
+
async function getDiff(repoRoot) {
|
|
75
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
76
|
+
const prId = process.env.BITBUCKET_PR_ID;
|
|
77
|
+
const prDestinationBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
|
|
78
|
+
// PR Context: Compare source vs target branch
|
|
79
|
+
if (prId) {
|
|
80
|
+
if (!prDestinationBranch) {
|
|
81
|
+
throw new Error('Bitbucket PR context detected but BITBUCKET_PR_DESTINATION_BRANCH is not set. ' +
|
|
82
|
+
'This should be automatically provided by Bitbucket Pipelines.');
|
|
83
|
+
}
|
|
84
|
+
console.log(` [Bitbucket] PR #${prId}, using origin/${prDestinationBranch}...HEAD`);
|
|
85
|
+
const diff = await git.diff([`origin/${prDestinationBranch}...HEAD`, '-U200']);
|
|
86
|
+
const diffSummary = await git.diffSummary([`origin/${prDestinationBranch}...HEAD`]);
|
|
87
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
88
|
+
return { diff: diff || '', changedFiles };
|
|
89
|
+
}
|
|
90
|
+
// Any push (main or feature branch): Review last commit only
|
|
91
|
+
const diff = await git.diff(['HEAD~1...HEAD', '-U200']);
|
|
92
|
+
const diffSummary = await git.diffSummary(['HEAD~1...HEAD']);
|
|
93
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
94
|
+
return { diff: diff || '', changedFiles };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Gets repository name for Bitbucket Pipelines
|
|
98
|
+
*
|
|
99
|
+
* Uses BITBUCKET_REPO_FULL_NAME to construct the repo URL.
|
|
100
|
+
* Example: ngrootscholten/threadline -> https://bitbucket.org/ngrootscholten/threadline.git
|
|
101
|
+
*/
|
|
102
|
+
function getRepoName() {
|
|
103
|
+
const repoFullName = process.env.BITBUCKET_REPO_FULL_NAME;
|
|
104
|
+
if (!repoFullName) {
|
|
105
|
+
throw new Error('Bitbucket Pipelines: BITBUCKET_REPO_FULL_NAME environment variable is not set. ' +
|
|
106
|
+
'This should be automatically provided by Bitbucket Pipelines.');
|
|
107
|
+
}
|
|
108
|
+
return `https://bitbucket.org/${repoFullName}.git`;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Gets branch name for Bitbucket Pipelines
|
|
112
|
+
*/
|
|
113
|
+
function getBranchName() {
|
|
114
|
+
const branchName = process.env.BITBUCKET_BRANCH;
|
|
115
|
+
if (!branchName) {
|
|
116
|
+
throw new Error('Bitbucket Pipelines: BITBUCKET_BRANCH environment variable is not set. ' +
|
|
117
|
+
'This should be automatically provided by Bitbucket Pipelines.');
|
|
118
|
+
}
|
|
119
|
+
return branchName;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Detects Bitbucket context (PR or commit)
|
|
123
|
+
*
|
|
124
|
+
* - PR context: When BITBUCKET_PR_ID is set
|
|
125
|
+
* - Commit context: Any push (main or feature branch) - reviews single commit
|
|
126
|
+
*/
|
|
127
|
+
function detectContext() {
|
|
128
|
+
// PR context
|
|
129
|
+
const prId = process.env.BITBUCKET_PR_ID;
|
|
130
|
+
const prDestinationBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
|
|
131
|
+
const sourceBranch = process.env.BITBUCKET_BRANCH;
|
|
132
|
+
if (prId && prDestinationBranch && sourceBranch) {
|
|
133
|
+
return {
|
|
134
|
+
type: 'pr',
|
|
135
|
+
prNumber: prId,
|
|
136
|
+
sourceBranch,
|
|
137
|
+
targetBranch: prDestinationBranch
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Any push (main or feature branch) → commit context
|
|
141
|
+
if (process.env.BITBUCKET_COMMIT) {
|
|
142
|
+
return {
|
|
143
|
+
type: 'commit',
|
|
144
|
+
commitSha: process.env.BITBUCKET_COMMIT
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
throw new Error('Bitbucket Pipelines: Could not detect context. ' +
|
|
148
|
+
'Expected BITBUCKET_PR_ID or BITBUCKET_COMMIT to be set. ' +
|
|
149
|
+
'This should be automatically provided by Bitbucket Pipelines.');
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Gets commit SHA from Bitbucket environment
|
|
153
|
+
*/
|
|
154
|
+
function getCommitSha() {
|
|
155
|
+
return process.env.BITBUCKET_COMMIT;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Gets commit author for Bitbucket Pipelines
|
|
159
|
+
*
|
|
160
|
+
* Bitbucket doesn't provide commit author as an environment variable,
|
|
161
|
+
* so we use git log to get it.
|
|
162
|
+
*
|
|
163
|
+
* This approach is verified by our test script (test-bitbucket-context.ts)
|
|
164
|
+
* which successfully retrieves commit author in all scenarios:
|
|
165
|
+
* - Direct commit to main
|
|
166
|
+
* - Feature branch push
|
|
167
|
+
* - PR pipeline
|
|
168
|
+
* - Merge commit
|
|
169
|
+
*/
|
|
170
|
+
async function getCommitAuthor(repoRoot) {
|
|
171
|
+
// Use raw git commands - this is exactly what the test script uses and we know it works
|
|
172
|
+
try {
|
|
173
|
+
const name = (0, child_process_1.execSync)('git log -1 --format=%an', { encoding: 'utf-8', cwd: repoRoot }).trim();
|
|
174
|
+
const email = (0, child_process_1.execSync)('git log -1 --format=%ae', { encoding: 'utf-8', cwd: repoRoot }).trim();
|
|
175
|
+
if (!name || !email) {
|
|
176
|
+
throw new Error('git log returned empty name or email');
|
|
177
|
+
}
|
|
178
|
+
return { name, email };
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
182
|
+
throw new Error(`Bitbucket Pipelines: Failed to get commit author from git log. ` +
|
|
183
|
+
`Error: ${errorMessage}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
package/dist/git/diff.js
CHANGED
|
@@ -3,185 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getBranchDiff = getBranchDiff;
|
|
7
6
|
exports.getCommitMessage = getCommitMessage;
|
|
8
7
|
exports.getCommitAuthor = getCommitAuthor;
|
|
9
8
|
exports.getCommitDiff = getCommitDiff;
|
|
10
|
-
exports.getPRMRDiff = getPRMRDiff;
|
|
11
9
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
12
|
-
/**
|
|
13
|
-
* Get diff for a specific branch (all commits vs base branch)
|
|
14
|
-
* Uses git merge-base to find common ancestor, then diffs from there
|
|
15
|
-
*/
|
|
16
|
-
async function getBranchDiff(repoRoot, branchName, baseBranch) {
|
|
17
|
-
const git = (0, simple_git_1.default)(repoRoot);
|
|
18
|
-
// Check if we're in a git repo
|
|
19
|
-
const isRepo = await git.checkIsRepo();
|
|
20
|
-
if (!isRepo) {
|
|
21
|
-
throw new Error('Not a git repository. Threadline requires a git repository.');
|
|
22
|
-
}
|
|
23
|
-
// Determine base branch
|
|
24
|
-
let base;
|
|
25
|
-
if (baseBranch) {
|
|
26
|
-
// Use provided base branch
|
|
27
|
-
base = baseBranch;
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
// Check if the branch itself is a base branch (main/master)
|
|
31
|
-
const baseBranchNames = ['main', 'master'];
|
|
32
|
-
const isBaseBranch = baseBranchNames.includes(branchName.toLowerCase());
|
|
33
|
-
if (isBaseBranch) {
|
|
34
|
-
// For main/master branch, compare against previous commit (HEAD~1)
|
|
35
|
-
// This checks what changed in the most recent commit
|
|
36
|
-
try {
|
|
37
|
-
const previousCommit = await git.revparse(['HEAD~1']);
|
|
38
|
-
// Use commit-based diff instead
|
|
39
|
-
const diff = await git.diff([`${previousCommit}..HEAD`, '-U200']);
|
|
40
|
-
const diffSummary = await git.diffSummary([`${previousCommit}..HEAD`]);
|
|
41
|
-
const changedFiles = diffSummary.files.map(f => f.file);
|
|
42
|
-
return {
|
|
43
|
-
diff: diff || '',
|
|
44
|
-
changedFiles
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
// If no previous commit, return empty (first commit)
|
|
49
|
-
const errorMessage = error instanceof Error ? error.message : 'HEAD~1 does not exist';
|
|
50
|
-
console.log(`[DEBUG] No previous commit found (first commit or error): ${errorMessage}`);
|
|
51
|
-
return {
|
|
52
|
-
diff: '',
|
|
53
|
-
changedFiles: []
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
// Try to detect base branch: upstream, default branch, or common names
|
|
58
|
-
base = await detectBaseBranch(git, branchName);
|
|
59
|
-
}
|
|
60
|
-
// Helper function to detect base branch
|
|
61
|
-
// Returns the branch name to use in git commands (may be local or remote)
|
|
62
|
-
// In CI environments, prioritizes remote refs since local branches often don't exist
|
|
63
|
-
// Note: Vercel is excluded here because it uses commit context, not branch context
|
|
64
|
-
async function detectBaseBranch(git, branchName) {
|
|
65
|
-
const isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI);
|
|
66
|
-
// Strategy 1: Try upstream tracking branch (most reliable if set)
|
|
67
|
-
try {
|
|
68
|
-
const upstream = await git.revparse(['--abbrev-ref', '--symbolic-full-name', `${branchName}@{u}`]);
|
|
69
|
-
const upstreamBranch = upstream.replace(/^origin\//, '');
|
|
70
|
-
// Don't use the branch itself as its base
|
|
71
|
-
if (upstreamBranch !== branchName) {
|
|
72
|
-
// In CI, prefer remote refs since local branches often don't exist
|
|
73
|
-
if (isCI) {
|
|
74
|
-
console.log(`[DEBUG] CI environment detected, using upstream tracking branch (remote): ${upstream}`);
|
|
75
|
-
return upstream;
|
|
76
|
-
}
|
|
77
|
-
// In local dev, check if local branch exists
|
|
78
|
-
try {
|
|
79
|
-
await git.revparse([upstreamBranch]);
|
|
80
|
-
console.log(`[DEBUG] Using upstream tracking branch (local): ${upstreamBranch}`);
|
|
81
|
-
return upstreamBranch;
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
console.log(`[DEBUG] Upstream tracking branch exists but local branch '${upstreamBranch}' not found, using remote: ${upstream}`);
|
|
85
|
-
return upstream;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
console.log(`[DEBUG] Upstream tracking branch '${upstreamBranch}' is the same as current branch, skipping`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
const errorMessage = error instanceof Error ? error.message : 'no upstream configured';
|
|
94
|
-
console.log(`[DEBUG] Upstream tracking branch not set for '${branchName}': ${errorMessage}`);
|
|
95
|
-
}
|
|
96
|
-
// Strategy 2: Try default branch from origin/HEAD (reliable if configured)
|
|
97
|
-
try {
|
|
98
|
-
const defaultBranch = await git.revparse(['--abbrev-ref', 'refs/remotes/origin/HEAD']);
|
|
99
|
-
const defaultBranchName = defaultBranch.replace(/^origin\//, '');
|
|
100
|
-
// Don't use the branch itself as its base
|
|
101
|
-
if (defaultBranchName !== branchName) {
|
|
102
|
-
// In CI, prefer remote refs
|
|
103
|
-
if (isCI) {
|
|
104
|
-
console.log(`[DEBUG] CI environment detected, using default branch (remote): ${defaultBranch}`);
|
|
105
|
-
return defaultBranch;
|
|
106
|
-
}
|
|
107
|
-
// In local dev, check if local branch exists
|
|
108
|
-
try {
|
|
109
|
-
await git.revparse([defaultBranchName]);
|
|
110
|
-
console.log(`[DEBUG] Using default branch (local): ${defaultBranchName}`);
|
|
111
|
-
return defaultBranchName;
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
console.log(`[DEBUG] Default branch exists but local branch '${defaultBranchName}' not found, using remote: ${defaultBranch}`);
|
|
115
|
-
return defaultBranch;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
console.log(`[DEBUG] Default branch '${defaultBranchName}' is the same as current branch, skipping`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
catch (error) {
|
|
123
|
-
const errorMessage = error instanceof Error ? error.message : 'not found';
|
|
124
|
-
console.log(`[DEBUG] Default branch (refs/remotes/origin/HEAD) not configured: ${errorMessage}`);
|
|
125
|
-
}
|
|
126
|
-
// Strategy 3: Try common branch names by checking remote refs first, then local branches
|
|
127
|
-
// This works reliably in CI with fetch-depth: 0, and also works locally
|
|
128
|
-
const commonBases = ['main', 'master', 'develop'];
|
|
129
|
-
for (const candidate of commonBases) {
|
|
130
|
-
if (candidate.toLowerCase() === branchName.toLowerCase()) {
|
|
131
|
-
continue; // Skip if it's the same branch
|
|
132
|
-
}
|
|
133
|
-
// Try remote ref first
|
|
134
|
-
try {
|
|
135
|
-
await git.revparse([`origin/${candidate}`]);
|
|
136
|
-
// In CI, prefer remote refs since local branches often don't exist
|
|
137
|
-
if (isCI) {
|
|
138
|
-
console.log(`[DEBUG] CI environment detected, using common branch name (remote): origin/${candidate}`);
|
|
139
|
-
return `origin/${candidate}`;
|
|
140
|
-
}
|
|
141
|
-
// In local dev, check if local branch exists
|
|
142
|
-
try {
|
|
143
|
-
await git.revparse([candidate]);
|
|
144
|
-
console.log(`[DEBUG] Using common branch name (local): ${candidate}`);
|
|
145
|
-
return candidate;
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
console.log(`[DEBUG] Common branch '${candidate}' exists remotely but not locally, using remote: origin/${candidate}`);
|
|
149
|
-
return `origin/${candidate}`;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
const errorMessage = error instanceof Error ? error.message : 'does not exist';
|
|
154
|
-
console.log(`[DEBUG] Remote branch 'origin/${candidate}' not found: ${errorMessage}`);
|
|
155
|
-
// If remote doesn't exist, also try local branch (especially for CI like Vercel)
|
|
156
|
-
try {
|
|
157
|
-
await git.revparse([candidate]);
|
|
158
|
-
console.log(`[DEBUG] Remote 'origin/${candidate}' not available, but local branch '${candidate}' found - using local`);
|
|
159
|
-
return candidate;
|
|
160
|
-
}
|
|
161
|
-
catch (localError) {
|
|
162
|
-
const localErrorMessage = localError instanceof Error ? localError.message : 'does not exist';
|
|
163
|
-
console.log(`[DEBUG] Local branch '${candidate}' also not found: ${localErrorMessage}`);
|
|
164
|
-
// Continue to next candidate
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
// All strategies failed - provide clear error with context
|
|
169
|
-
throw new Error(`Could not determine base branch for '${branchName}'. ` +
|
|
170
|
-
`Tried: upstream tracking, default branch (origin/HEAD), and common names (main, master, develop). ` +
|
|
171
|
-
`Please specify base branch with --base flag or configure upstream tracking with: ` +
|
|
172
|
-
`git branch --set-upstream-to=origin/main ${branchName}`);
|
|
173
|
-
}
|
|
174
|
-
// Get diff between base and branch (cumulative diff of all commits)
|
|
175
|
-
// Format: git diff base...branch (three-dot notation finds common ancestor)
|
|
176
|
-
const diff = await git.diff([`${base}...${branchName}`, '-U200']);
|
|
177
|
-
// Get list of changed files
|
|
178
|
-
const diffSummary = await git.diffSummary([`${base}...${branchName}`]);
|
|
179
|
-
const changedFiles = diffSummary.files.map(f => f.file);
|
|
180
|
-
return {
|
|
181
|
-
diff: diff || '',
|
|
182
|
-
changedFiles
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
10
|
/**
|
|
186
11
|
* Get commit message for a specific commit SHA
|
|
187
12
|
* Returns full commit message (subject + body) or null if commit not found
|
|
@@ -273,24 +98,3 @@ async function getCommitDiff(repoRoot, sha) {
|
|
|
273
98
|
changedFiles
|
|
274
99
|
};
|
|
275
100
|
}
|
|
276
|
-
/**
|
|
277
|
-
* Get diff for PR/MR (source branch vs target branch)
|
|
278
|
-
*/
|
|
279
|
-
async function getPRMRDiff(repoRoot, sourceBranch, targetBranch) {
|
|
280
|
-
const git = (0, simple_git_1.default)(repoRoot);
|
|
281
|
-
// Check if we're in a git repo
|
|
282
|
-
const isRepo = await git.checkIsRepo();
|
|
283
|
-
if (!isRepo) {
|
|
284
|
-
throw new Error('Not a git repository. Threadline requires a git repository.');
|
|
285
|
-
}
|
|
286
|
-
// Get diff between target and source (cumulative diff)
|
|
287
|
-
// Format: git diff target...source (three-dot notation finds common ancestor)
|
|
288
|
-
const diff = await git.diff([`${targetBranch}...${sourceBranch}`, '-U200']);
|
|
289
|
-
// Get list of changed files
|
|
290
|
-
const diffSummary = await git.diffSummary([`${targetBranch}...${sourceBranch}`]);
|
|
291
|
-
const changedFiles = diffSummary.files.map(f => f.file);
|
|
292
|
-
return {
|
|
293
|
-
diff: diff || '',
|
|
294
|
-
changedFiles
|
|
295
|
-
};
|
|
296
|
-
}
|