threadlines 0.2.6 → 0.2.9
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/api/client.js +2 -17
- package/dist/commands/check.js +26 -41
- package/dist/commands/init.js +10 -1
- package/dist/git/bitbucket.js +23 -92
- package/dist/git/diff.js +0 -196
- package/dist/git/file.js +2 -1
- package/dist/git/github.js +32 -52
- package/dist/git/gitlab.js +23 -49
- package/dist/git/local.js +2 -2
- package/dist/git/vercel.js +2 -2
- package/dist/index.js +18 -7
- package/dist/utils/config-file.js +118 -0
- package/dist/utils/config.js +98 -57
- package/dist/utils/context.js +4 -184
- package/dist/utils/logger.js +65 -0
- package/dist/validators/experts.js +2 -1
- package/package.json +1 -1
- package/dist/git/repo.js +0 -253
package/dist/api/client.js
CHANGED
|
@@ -16,23 +16,8 @@ class ReviewAPIClient {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
async review(request) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return response.data;
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
if (error && typeof error === 'object' && 'response' in error) {
|
|
25
|
-
const axiosError = error;
|
|
26
|
-
throw new Error(`API error: ${axiosError.response.status} - ${axiosError.response.data?.message || axiosError.message || 'Unknown error'}`);
|
|
27
|
-
}
|
|
28
|
-
else if (error && typeof error === 'object' && 'request' in error) {
|
|
29
|
-
throw new Error(`Network error: Could not reach Threadline server at ${this.client.defaults.baseURL}`);
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
33
|
-
throw new Error(`Request error: ${errorMessage}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
19
|
+
const response = await this.client.post('/api/threadline-check', request);
|
|
20
|
+
return response.data;
|
|
36
21
|
}
|
|
37
22
|
}
|
|
38
23
|
exports.ReviewAPIClient = ReviewAPIClient;
|
package/dist/commands/check.js
CHANGED
|
@@ -48,6 +48,8 @@ const bitbucket_1 = require("../git/bitbucket");
|
|
|
48
48
|
const vercel_1 = require("../git/vercel");
|
|
49
49
|
const local_1 = require("../git/local");
|
|
50
50
|
const diff_1 = require("../git/diff");
|
|
51
|
+
const config_file_1 = require("../utils/config-file");
|
|
52
|
+
const logger_1 = require("../utils/logger");
|
|
51
53
|
const fs = __importStar(require("fs"));
|
|
52
54
|
const path = __importStar(require("path"));
|
|
53
55
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -77,6 +79,8 @@ const CLI_VERSION = packageJson.version;
|
|
|
77
79
|
async function checkCommand(options) {
|
|
78
80
|
const cwd = process.cwd();
|
|
79
81
|
const repoRoot = cwd; // Keep for backward compatibility with rest of function
|
|
82
|
+
// Load configuration
|
|
83
|
+
const config = await (0, config_file_1.loadConfig)(cwd);
|
|
80
84
|
console.log(chalk_1.default.blue(`🔍 Threadline CLI v${CLI_VERSION}: Checking code against your threadlines...\n`));
|
|
81
85
|
// Get git root for consistent file paths across monorepo
|
|
82
86
|
const git = (0, simple_git_1.default)(cwd);
|
|
@@ -84,13 +88,13 @@ async function checkCommand(options) {
|
|
|
84
88
|
try {
|
|
85
89
|
const isRepo = await git.checkIsRepo();
|
|
86
90
|
if (!isRepo) {
|
|
87
|
-
|
|
91
|
+
logger_1.logger.error('Not a git repository. Threadline requires a git repository.');
|
|
88
92
|
process.exit(1);
|
|
89
93
|
}
|
|
90
94
|
gitRoot = (await git.revparse(['--show-toplevel'])).trim();
|
|
91
95
|
}
|
|
92
96
|
catch {
|
|
93
|
-
|
|
97
|
+
logger_1.logger.error('Failed to get git root. Make sure you are in a git repository.');
|
|
94
98
|
process.exit(1);
|
|
95
99
|
}
|
|
96
100
|
// Pre-flight check: Validate ALL required environment variables at once
|
|
@@ -103,9 +107,9 @@ async function checkCommand(options) {
|
|
|
103
107
|
if (!account || account.startsWith('$'))
|
|
104
108
|
missingVars.push('THREADLINE_ACCOUNT');
|
|
105
109
|
if (missingVars.length > 0) {
|
|
106
|
-
|
|
110
|
+
logger_1.logger.error('Missing required environment variables:');
|
|
107
111
|
for (const varName of missingVars) {
|
|
108
|
-
|
|
112
|
+
logger_1.logger.error(` • ${varName}`);
|
|
109
113
|
}
|
|
110
114
|
console.log('');
|
|
111
115
|
console.log(chalk_1.default.yellow('To fix this:'));
|
|
@@ -132,7 +136,7 @@ async function checkCommand(options) {
|
|
|
132
136
|
}
|
|
133
137
|
try {
|
|
134
138
|
// 1. Find and validate threadlines
|
|
135
|
-
|
|
139
|
+
logger_1.logger.info('Finding threadlines...');
|
|
136
140
|
const threadlines = await (0, experts_1.findThreadlines)(cwd, gitRoot);
|
|
137
141
|
console.log(chalk_1.default.green(`✓ Found ${threadlines.length} threadline(s)\n`));
|
|
138
142
|
if (threadlines.length === 0) {
|
|
@@ -147,11 +151,11 @@ async function checkCommand(options) {
|
|
|
147
151
|
let branchName;
|
|
148
152
|
let metadata = {};
|
|
149
153
|
// Check for explicit flags
|
|
150
|
-
const explicitFlags = [options.
|
|
154
|
+
const explicitFlags = [options.commit, options.file, options.folder, options.files].filter(Boolean);
|
|
151
155
|
// Validate mutually exclusive flags
|
|
152
156
|
if (explicitFlags.length > 1) {
|
|
153
|
-
|
|
154
|
-
console.log(chalk_1.default.gray(' Options: --
|
|
157
|
+
logger_1.logger.error('Only one review option can be specified at a time');
|
|
158
|
+
console.log(chalk_1.default.gray(' Options: --commit, --file, --folder, --files'));
|
|
155
159
|
process.exit(1);
|
|
156
160
|
}
|
|
157
161
|
// CI environments: auto-detect only, flags are ignored with warning
|
|
@@ -159,11 +163,10 @@ async function checkCommand(options) {
|
|
|
159
163
|
if ((0, environment_1.isCIEnvironment)(environment)) {
|
|
160
164
|
// Warn if flags are passed in CI - they're meant for local development
|
|
161
165
|
if (explicitFlags.length > 0) {
|
|
162
|
-
const flagName = options.
|
|
163
|
-
options.
|
|
164
|
-
options.
|
|
165
|
-
|
|
166
|
-
console.log(chalk_1.default.yellow(`⚠️ Warning: ${flagName} flag ignored in CI environment. Using auto-detection.\n`));
|
|
166
|
+
const flagName = options.commit ? '--commit' :
|
|
167
|
+
options.file ? '--file' :
|
|
168
|
+
options.folder ? '--folder' : '--files';
|
|
169
|
+
logger_1.logger.warn(`${flagName} flag ignored in CI environment. Using auto-detection.`);
|
|
167
170
|
}
|
|
168
171
|
// CI auto-detect: use environment-specific context
|
|
169
172
|
const envNames = {
|
|
@@ -172,7 +175,7 @@ async function checkCommand(options) {
|
|
|
172
175
|
gitlab: 'GitLab CI',
|
|
173
176
|
bitbucket: 'Bitbucket Pipelines'
|
|
174
177
|
};
|
|
175
|
-
|
|
178
|
+
logger_1.logger.info(`Collecting git context for ${envNames[environment]}...`);
|
|
176
179
|
const envContext = await getContextForEnvironment(environment, repoRoot);
|
|
177
180
|
gitDiff = envContext.diff;
|
|
178
181
|
repoName = envContext.repoName;
|
|
@@ -188,33 +191,19 @@ async function checkCommand(options) {
|
|
|
188
191
|
else {
|
|
189
192
|
// Local environment: support all flags
|
|
190
193
|
if (options.file) {
|
|
191
|
-
|
|
194
|
+
logger_1.logger.info(`Reading file: ${options.file}...`);
|
|
192
195
|
gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
|
|
193
196
|
}
|
|
194
197
|
else if (options.folder) {
|
|
195
|
-
|
|
198
|
+
logger_1.logger.info(`Reading folder: ${options.folder}...`);
|
|
196
199
|
gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
|
|
197
200
|
}
|
|
198
201
|
else if (options.files && options.files.length > 0) {
|
|
199
|
-
|
|
202
|
+
logger_1.logger.info(`Reading ${options.files.length} file(s)...`);
|
|
200
203
|
gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
|
|
201
204
|
}
|
|
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
205
|
else if (options.commit) {
|
|
217
|
-
|
|
206
|
+
logger_1.logger.info(`Collecting git changes for commit: ${options.commit}...`);
|
|
218
207
|
gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
|
|
219
208
|
// Use local context for metadata, passing commit SHA for author lookup
|
|
220
209
|
const localContext = await (0, local_1.getLocalContext)(repoRoot, options.commit);
|
|
@@ -229,7 +218,7 @@ async function checkCommand(options) {
|
|
|
229
218
|
}
|
|
230
219
|
else {
|
|
231
220
|
// Local auto-detect: staged/unstaged changes
|
|
232
|
-
|
|
221
|
+
logger_1.logger.info('Collecting git context for Local...');
|
|
233
222
|
const localContext = await (0, local_1.getLocalContext)(repoRoot);
|
|
234
223
|
gitDiff = localContext.diff;
|
|
235
224
|
repoName = localContext.repoName;
|
|
@@ -279,13 +268,9 @@ async function checkCommand(options) {
|
|
|
279
268
|
contextContent
|
|
280
269
|
};
|
|
281
270
|
});
|
|
282
|
-
// 5.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
'https://devthreadline.com';
|
|
286
|
-
// 6. Call review API
|
|
287
|
-
console.log(chalk_1.default.gray('🤖 Running threadline checks...'));
|
|
288
|
-
const client = new client_1.ReviewAPIClient(apiUrl);
|
|
271
|
+
// 5. Call review API
|
|
272
|
+
logger_1.logger.info('Running threadline checks...');
|
|
273
|
+
const client = new client_1.ReviewAPIClient(config.api_url);
|
|
289
274
|
const response = await client.review({
|
|
290
275
|
threadlines: threadlinesWithContext,
|
|
291
276
|
diff: gitDiff.diff,
|
|
@@ -310,7 +295,7 @@ async function checkCommand(options) {
|
|
|
310
295
|
}
|
|
311
296
|
catch (error) {
|
|
312
297
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
313
|
-
|
|
298
|
+
logger_1.logger.error(errorMessage);
|
|
314
299
|
process.exit(1);
|
|
315
300
|
}
|
|
316
301
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -40,6 +40,8 @@ exports.initCommand = initCommand;
|
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const chalk_1 = __importDefault(require("chalk"));
|
|
43
|
+
const logger_1 = require("../utils/logger");
|
|
44
|
+
const config_file_1 = require("../utils/config-file");
|
|
43
45
|
const TEMPLATE = `---
|
|
44
46
|
id: example-threadline
|
|
45
47
|
version: 1.0.0
|
|
@@ -75,12 +77,19 @@ async function initCommand() {
|
|
|
75
77
|
const repoRoot = process.cwd();
|
|
76
78
|
const threadlinesDir = path.join(repoRoot, 'threadlines');
|
|
77
79
|
const exampleFile = path.join(threadlinesDir, 'example.md');
|
|
80
|
+
const configFile = path.join(repoRoot, '.threadlinerc');
|
|
78
81
|
try {
|
|
79
82
|
// Create threadlines directory if it doesn't exist
|
|
80
83
|
if (!fs.existsSync(threadlinesDir)) {
|
|
81
84
|
fs.mkdirSync(threadlinesDir, { recursive: true });
|
|
82
85
|
console.log(chalk_1.default.green(`✓ Created /threadlines directory`));
|
|
83
86
|
}
|
|
87
|
+
// Create .threadlinerc if it doesn't exist
|
|
88
|
+
if (!fs.existsSync(configFile)) {
|
|
89
|
+
const configContent = JSON.stringify(config_file_1.DEFAULT_CONFIG, null, 2);
|
|
90
|
+
fs.writeFileSync(configFile, configContent, 'utf-8');
|
|
91
|
+
console.log(chalk_1.default.green(`✓ Created .threadlinerc`));
|
|
92
|
+
}
|
|
84
93
|
// Check if example file already exists
|
|
85
94
|
if (fs.existsSync(exampleFile)) {
|
|
86
95
|
console.log(chalk_1.default.yellow(`⚠️ ${exampleFile} already exists`));
|
|
@@ -109,7 +118,7 @@ async function initCommand() {
|
|
|
109
118
|
}
|
|
110
119
|
catch (error) {
|
|
111
120
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
112
|
-
|
|
121
|
+
logger_1.logger.error(errorMessage);
|
|
113
122
|
process.exit(1);
|
|
114
123
|
}
|
|
115
124
|
}
|
package/dist/git/bitbucket.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Bitbucket Pipelines Environment
|
|
3
|
+
* Bitbucket Pipelines Environment
|
|
4
4
|
*
|
|
5
5
|
* All Bitbucket-specific logic is contained in this file.
|
|
6
6
|
* No dependencies on other environment implementations.
|
|
@@ -25,6 +25,7 @@ exports.getBitbucketContext = getBitbucketContext;
|
|
|
25
25
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
26
26
|
const child_process_1 = require("child_process");
|
|
27
27
|
const diff_1 = require("./diff");
|
|
28
|
+
const logger_1 = require("../utils/logger");
|
|
28
29
|
/**
|
|
29
30
|
* Gets all Bitbucket context in one call
|
|
30
31
|
*/
|
|
@@ -65,105 +66,38 @@ async function getBitbucketContext(repoRoot) {
|
|
|
65
66
|
/**
|
|
66
67
|
* Get diff for Bitbucket Pipelines environment
|
|
67
68
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
69
|
+
* Strategy:
|
|
70
|
+
* - PR context: Fetch destination branch on-demand, compare source vs target (full PR diff)
|
|
71
|
+
* - Any push (main or feature branch): Compare last commit only (HEAD~1...HEAD)
|
|
70
72
|
*
|
|
71
|
-
*
|
|
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.
|
|
73
|
+
* Note: We fetch the destination branch on-demand so this works with shallow clones.
|
|
74
|
+
* Users don't need `depth: full` in their bitbucket-pipelines.yml.
|
|
85
75
|
*/
|
|
86
76
|
async function getDiff(repoRoot) {
|
|
87
77
|
const git = (0, simple_git_1.default)(repoRoot);
|
|
88
|
-
const branchName = process.env.BITBUCKET_BRANCH;
|
|
89
78
|
const prId = process.env.BITBUCKET_PR_ID;
|
|
90
79
|
const prDestinationBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
|
|
91
|
-
//
|
|
80
|
+
// PR Context: Fetch destination branch and compare
|
|
92
81
|
if (prId) {
|
|
93
82
|
if (!prDestinationBranch) {
|
|
94
83
|
throw new Error('Bitbucket PR context detected but BITBUCKET_PR_DESTINATION_BRANCH is not set. ' +
|
|
95
84
|
'This should be automatically provided by Bitbucket Pipelines.');
|
|
96
85
|
}
|
|
97
|
-
|
|
86
|
+
// Fetch destination branch on-demand (works with shallow clones)
|
|
87
|
+
logger_1.logger.debug(`Fetching destination branch: origin/${prDestinationBranch}`);
|
|
88
|
+
await git.fetch(['origin', `${prDestinationBranch}:refs/remotes/origin/${prDestinationBranch}`, '--depth=1']);
|
|
89
|
+
logger_1.logger.debug(`PR #${prId}, using origin/${prDestinationBranch}...HEAD`);
|
|
98
90
|
const diff = await git.diff([`origin/${prDestinationBranch}...HEAD`, '-U200']);
|
|
99
91
|
const diffSummary = await git.diffSummary([`origin/${prDestinationBranch}...HEAD`]);
|
|
100
92
|
const changedFiles = diffSummary.files.map(f => f.file);
|
|
101
93
|
return { diff: diff || '', changedFiles };
|
|
102
94
|
}
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
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`]);
|
|
95
|
+
// Any push (main or feature branch): Review last commit only
|
|
96
|
+
const diff = await git.diff(['HEAD~1...HEAD', '-U200']);
|
|
97
|
+
const diffSummary = await git.diffSummary(['HEAD~1...HEAD']);
|
|
124
98
|
const changedFiles = diffSummary.files.map(f => f.file);
|
|
125
99
|
return { diff: diff || '', changedFiles };
|
|
126
100
|
}
|
|
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
101
|
/**
|
|
168
102
|
* Gets repository name for Bitbucket Pipelines
|
|
169
103
|
*
|
|
@@ -190,7 +124,10 @@ function getBranchName() {
|
|
|
190
124
|
return branchName;
|
|
191
125
|
}
|
|
192
126
|
/**
|
|
193
|
-
* Detects Bitbucket context (PR
|
|
127
|
+
* Detects Bitbucket context (PR or commit)
|
|
128
|
+
*
|
|
129
|
+
* - PR context: When BITBUCKET_PR_ID is set
|
|
130
|
+
* - Commit context: Any push (main or feature branch) - reviews single commit
|
|
194
131
|
*/
|
|
195
132
|
function detectContext() {
|
|
196
133
|
// PR context
|
|
@@ -205,22 +142,16 @@ function detectContext() {
|
|
|
205
142
|
targetBranch: prDestinationBranch
|
|
206
143
|
};
|
|
207
144
|
}
|
|
208
|
-
//
|
|
209
|
-
if (process.env.BITBUCKET_BRANCH) {
|
|
210
|
-
return {
|
|
211
|
-
type: 'branch',
|
|
212
|
-
branchName: process.env.BITBUCKET_BRANCH
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
// Commit context
|
|
145
|
+
// Any push (main or feature branch) → commit context
|
|
216
146
|
if (process.env.BITBUCKET_COMMIT) {
|
|
217
147
|
return {
|
|
218
148
|
type: 'commit',
|
|
219
149
|
commitSha: process.env.BITBUCKET_COMMIT
|
|
220
150
|
};
|
|
221
151
|
}
|
|
222
|
-
|
|
223
|
-
|
|
152
|
+
throw new Error('Bitbucket Pipelines: Could not detect context. ' +
|
|
153
|
+
'Expected BITBUCKET_PR_ID or BITBUCKET_COMMIT to be set. ' +
|
|
154
|
+
'This should be automatically provided by Bitbucket Pipelines.');
|
|
224
155
|
}
|
|
225
156
|
/**
|
|
226
157
|
* Gets commit SHA from Bitbucket environment
|
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
|
-
}
|
package/dist/git/file.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.getMultipleFilesContent = getMultipleFilesContent;
|
|
|
39
39
|
const fs = __importStar(require("fs"));
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const glob_1 = require("glob");
|
|
42
|
+
const logger_1 = require("../utils/logger");
|
|
42
43
|
/**
|
|
43
44
|
* Read content of a single file and create artificial diff (all lines as additions)
|
|
44
45
|
*/
|
|
@@ -116,7 +117,7 @@ async function getFolderContent(repoRoot, folderPath) {
|
|
|
116
117
|
catch (error) {
|
|
117
118
|
// Skip files that can't be read (permissions, etc.)
|
|
118
119
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
119
|
-
|
|
120
|
+
logger_1.logger.warn(`Could not read file '${filePath}': ${errorMessage}`);
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
return {
|