threadlines 0.2.13 → 0.2.15
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 +167 -185
- package/dist/git/bitbucket.js +3 -32
- package/dist/git/diff.js +77 -15
- package/dist/git/file.js +17 -11
- package/dist/git/github.js +1 -5
- package/dist/git/gitlab.js +8 -36
- package/dist/git/local.js +2 -9
- package/dist/git/vercel.js +3 -33
- package/package.json +1 -1
package/dist/commands/check.js
CHANGED
|
@@ -47,7 +47,6 @@ const gitlab_1 = require("../git/gitlab");
|
|
|
47
47
|
const bitbucket_1 = require("../git/bitbucket");
|
|
48
48
|
const vercel_1 = require("../git/vercel");
|
|
49
49
|
const local_1 = require("../git/local");
|
|
50
|
-
const diff_1 = require("../git/diff");
|
|
51
50
|
const config_file_1 = require("../utils/config-file");
|
|
52
51
|
const logger_1 = require("../utils/logger");
|
|
53
52
|
const fs = __importStar(require("fs"));
|
|
@@ -68,8 +67,11 @@ async function getContextForEnvironment(environment, repoRoot, commitSha) {
|
|
|
68
67
|
return (0, bitbucket_1.getBitbucketContext)(repoRoot);
|
|
69
68
|
case 'vercel':
|
|
70
69
|
return (0, vercel_1.getVercelContext)(repoRoot);
|
|
71
|
-
|
|
70
|
+
case 'local':
|
|
72
71
|
return (0, local_1.getLocalContext)(repoRoot, commitSha);
|
|
72
|
+
default:
|
|
73
|
+
// TypeScript exhaustiveness check - should never happen
|
|
74
|
+
throw new Error(`Unrecognized environment: ${environment}`);
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
// Get CLI version from package.json
|
|
@@ -93,18 +95,20 @@ async function checkCommand(options) {
|
|
|
93
95
|
}
|
|
94
96
|
gitRoot = (await git.revparse(['--show-toplevel'])).trim();
|
|
95
97
|
}
|
|
96
|
-
catch {
|
|
97
|
-
|
|
98
|
+
catch (error) {
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
logger_1.logger.error(`Failed to get git root: ${message}`);
|
|
98
101
|
process.exit(1);
|
|
99
102
|
}
|
|
100
103
|
// Pre-flight check: Validate ALL required environment variables at once
|
|
101
104
|
const apiKey = (0, config_1.getThreadlineApiKey)();
|
|
102
105
|
const account = (0, config_1.getThreadlineAccount)();
|
|
103
106
|
const missingVars = [];
|
|
104
|
-
// Check for undefined, empty string, or literal unexpanded variable
|
|
105
|
-
if
|
|
107
|
+
// Check for undefined, empty string, or literal unexpanded variable
|
|
108
|
+
// GitLab CI keeps variables as literal "$VAR" if not defined in CI/CD settings
|
|
109
|
+
if (!apiKey || apiKey === '$THREADLINE_API_KEY')
|
|
106
110
|
missingVars.push('THREADLINE_API_KEY');
|
|
107
|
-
if (!account || account
|
|
111
|
+
if (!account || account === '$THREADLINE_ACCOUNT')
|
|
108
112
|
missingVars.push('THREADLINE_ACCOUNT');
|
|
109
113
|
if (missingVars.length > 0) {
|
|
110
114
|
logger_1.logger.error('Missing required environment variables:');
|
|
@@ -134,189 +138,171 @@ async function checkCommand(options) {
|
|
|
134
138
|
console.log(chalk_1.default.gray('Get your credentials at: https://devthreadline.com/settings'));
|
|
135
139
|
process.exit(1);
|
|
136
140
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
141
|
+
// 1. Find and validate threadlines
|
|
142
|
+
logger_1.logger.info('Finding threadlines...');
|
|
143
|
+
const threadlines = await (0, experts_1.findThreadlines)(cwd, gitRoot);
|
|
144
|
+
console.log(chalk_1.default.green(`✓ Found ${threadlines.length} threadline(s)\n`));
|
|
145
|
+
if (threadlines.length === 0) {
|
|
146
|
+
console.log(chalk_1.default.yellow('⚠️ No valid threadlines found.'));
|
|
147
|
+
console.log(chalk_1.default.gray(' Run `npx threadlines init` to create your first threadline.'));
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
// 2. Detect environment and context
|
|
151
|
+
const environment = (0, environment_1.detectEnvironment)();
|
|
152
|
+
let gitDiff;
|
|
153
|
+
let repoName;
|
|
154
|
+
let branchName;
|
|
155
|
+
let reviewContext;
|
|
156
|
+
let metadata = {};
|
|
157
|
+
// Check for explicit flags
|
|
158
|
+
const explicitFlags = [options.commit, options.file, options.folder, options.files].filter(Boolean);
|
|
159
|
+
// Validate mutually exclusive flags
|
|
160
|
+
if (explicitFlags.length > 1) {
|
|
161
|
+
logger_1.logger.error('Only one review option can be specified at a time');
|
|
162
|
+
console.log(chalk_1.default.gray(' Options: --commit, --file, --folder, --files'));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
// CI environments: auto-detect only, flags are ignored with warning
|
|
166
|
+
// Local: full flag support for developer flexibility
|
|
167
|
+
if ((0, environment_1.isCIEnvironment)(environment)) {
|
|
168
|
+
// Warn if flags are passed in CI - they're meant for local development
|
|
169
|
+
if (explicitFlags.length > 0) {
|
|
170
|
+
const flagName = options.commit ? '--commit' :
|
|
171
|
+
options.file ? '--file' :
|
|
172
|
+
options.folder ? '--folder' : '--files';
|
|
173
|
+
logger_1.logger.warn(`${flagName} flag ignored in CI environment. Using auto-detection.`);
|
|
161
174
|
}
|
|
162
|
-
// CI
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
175
|
+
// CI auto-detect: use environment-specific context
|
|
176
|
+
const envNames = {
|
|
177
|
+
vercel: 'Vercel',
|
|
178
|
+
github: 'GitHub Actions',
|
|
179
|
+
gitlab: 'GitLab CI',
|
|
180
|
+
bitbucket: 'Bitbucket Pipelines'
|
|
181
|
+
};
|
|
182
|
+
logger_1.logger.info(`Collecting git context for ${envNames[environment]}...`);
|
|
183
|
+
const envContext = await getContextForEnvironment(environment, repoRoot);
|
|
184
|
+
gitDiff = envContext.diff;
|
|
185
|
+
repoName = envContext.repoName;
|
|
186
|
+
branchName = envContext.branchName;
|
|
187
|
+
reviewContext = envContext.reviewContext; // Get from CI context
|
|
188
|
+
metadata = {
|
|
189
|
+
commitSha: envContext.commitSha,
|
|
190
|
+
commitMessage: envContext.commitMessage,
|
|
191
|
+
commitAuthorName: envContext.commitAuthor.name,
|
|
192
|
+
commitAuthorEmail: envContext.commitAuthor.email,
|
|
193
|
+
prTitle: envContext.prTitle
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// Local environment: all flags share the same metadata
|
|
198
|
+
// 1. Get context and metadata (pass commit SHA if provided)
|
|
199
|
+
logger_1.logger.info('Collecting local context...');
|
|
200
|
+
const localContext = await (0, local_1.getLocalContext)(repoRoot, options.commit);
|
|
201
|
+
repoName = localContext.repoName;
|
|
202
|
+
branchName = localContext.branchName;
|
|
203
|
+
metadata = {
|
|
204
|
+
commitSha: localContext.commitSha,
|
|
205
|
+
commitMessage: localContext.commitMessage,
|
|
206
|
+
commitAuthorName: localContext.commitAuthor.name,
|
|
207
|
+
commitAuthorEmail: localContext.commitAuthor.email
|
|
208
|
+
};
|
|
209
|
+
// 2. Get diff (override with specific content if flag provided)
|
|
210
|
+
if (options.file) {
|
|
211
|
+
reviewContext = 'file';
|
|
212
|
+
logger_1.logger.info(`Reading file: ${options.file}...`);
|
|
213
|
+
gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
|
|
192
214
|
}
|
|
193
|
-
else {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
reviewContext = 'file';
|
|
198
|
-
}
|
|
199
|
-
else if (options.folder) {
|
|
200
|
-
reviewContext = 'folder';
|
|
201
|
-
}
|
|
202
|
-
else if (options.files && options.files.length > 0) {
|
|
203
|
-
reviewContext = 'files';
|
|
204
|
-
}
|
|
205
|
-
else if (options.commit) {
|
|
206
|
-
reviewContext = 'commit';
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
reviewContext = 'local';
|
|
210
|
-
}
|
|
211
|
-
if (options.file) {
|
|
212
|
-
logger_1.logger.info(`Reading file: ${options.file}...`);
|
|
213
|
-
gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
|
|
214
|
-
}
|
|
215
|
-
else if (options.folder) {
|
|
216
|
-
logger_1.logger.info(`Reading folder: ${options.folder}...`);
|
|
217
|
-
gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
|
|
218
|
-
}
|
|
219
|
-
else if (options.files && options.files.length > 0) {
|
|
220
|
-
logger_1.logger.info(`Reading ${options.files.length} file(s)...`);
|
|
221
|
-
gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
|
|
222
|
-
}
|
|
223
|
-
else if (options.commit) {
|
|
224
|
-
logger_1.logger.info(`Collecting git changes for commit: ${options.commit}...`);
|
|
225
|
-
gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
|
|
226
|
-
// Use local context for metadata, passing commit SHA for author lookup
|
|
227
|
-
const localContext = await (0, local_1.getLocalContext)(repoRoot, options.commit);
|
|
228
|
-
repoName = localContext.repoName;
|
|
229
|
-
branchName = localContext.branchName;
|
|
230
|
-
metadata = {
|
|
231
|
-
commitSha: localContext.commitSha,
|
|
232
|
-
commitMessage: localContext.commitMessage,
|
|
233
|
-
commitAuthorName: localContext.commitAuthor.name,
|
|
234
|
-
commitAuthorEmail: localContext.commitAuthor.email
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
// Local auto-detect: staged/unstaged changes
|
|
239
|
-
logger_1.logger.info('Collecting git context for Local...');
|
|
240
|
-
const localContext = await (0, local_1.getLocalContext)(repoRoot);
|
|
241
|
-
gitDiff = localContext.diff;
|
|
242
|
-
repoName = localContext.repoName;
|
|
243
|
-
branchName = localContext.branchName;
|
|
244
|
-
metadata = {
|
|
245
|
-
commitSha: localContext.commitSha,
|
|
246
|
-
commitMessage: localContext.commitMessage,
|
|
247
|
-
commitAuthorName: localContext.commitAuthor.name,
|
|
248
|
-
commitAuthorEmail: localContext.commitAuthor.email
|
|
249
|
-
};
|
|
250
|
-
}
|
|
215
|
+
else if (options.folder) {
|
|
216
|
+
reviewContext = 'folder';
|
|
217
|
+
logger_1.logger.info(`Reading folder: ${options.folder}...`);
|
|
218
|
+
gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
|
|
251
219
|
}
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
220
|
+
else if (options.files && options.files.length > 0) {
|
|
221
|
+
reviewContext = 'files';
|
|
222
|
+
logger_1.logger.info(`Reading ${options.files.length} file(s)...`);
|
|
223
|
+
gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
|
|
255
224
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
console.log('');
|
|
261
|
-
console.log(chalk_1.default.bold('Results:\n'));
|
|
262
|
-
console.log(chalk_1.default.gray(`${threadlines.length} threadlines checked`));
|
|
263
|
-
console.log(chalk_1.default.gray(` ${threadlines.length} not relevant`));
|
|
264
|
-
console.log('');
|
|
265
|
-
process.exit(0);
|
|
225
|
+
else {
|
|
226
|
+
// Default: use diff from localContext (handles commit and staged/unstaged)
|
|
227
|
+
reviewContext = options.commit ? 'commit' : 'local';
|
|
228
|
+
gitDiff = localContext.diff;
|
|
266
229
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
230
|
+
}
|
|
231
|
+
if (gitDiff.changedFiles.length === 0) {
|
|
232
|
+
console.error(chalk_1.default.bold('ℹ️ No changes detected.'));
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
// Check for zero diff (files changed but no actual code changes)
|
|
236
|
+
if (!gitDiff.diff || gitDiff.diff.trim() === '') {
|
|
237
|
+
console.log(chalk_1.default.blue('ℹ️ No code changes detected. Diff contains zero lines added or removed.'));
|
|
238
|
+
console.log(chalk_1.default.gray(` ${gitDiff.changedFiles.length} file(s) changed but no content modifications detected.`));
|
|
239
|
+
console.log('');
|
|
240
|
+
console.log(chalk_1.default.bold('Results:\n'));
|
|
241
|
+
console.log(chalk_1.default.gray(`${threadlines.length} threadlines checked`));
|
|
242
|
+
console.log(chalk_1.default.gray(` ${threadlines.length} not relevant`));
|
|
243
|
+
console.log('');
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
console.log(chalk_1.default.green(`✓ Found ${gitDiff.changedFiles.length} changed file(s)\n`));
|
|
247
|
+
// Log the files being sent
|
|
248
|
+
for (const file of gitDiff.changedFiles) {
|
|
249
|
+
logger_1.logger.info(` → ${file}`);
|
|
250
|
+
}
|
|
251
|
+
// 4. Read context files for each threadline
|
|
252
|
+
const threadlinesWithContext = threadlines.map(threadline => {
|
|
253
|
+
const contextContent = {};
|
|
254
|
+
if (threadline.contextFiles) {
|
|
255
|
+
for (const contextFile of threadline.contextFiles) {
|
|
256
|
+
const fullPath = path.join(repoRoot, contextFile);
|
|
257
|
+
if (fs.existsSync(fullPath)) {
|
|
258
|
+
try {
|
|
275
259
|
contextContent[contextFile] = fs.readFileSync(fullPath, 'utf-8');
|
|
276
260
|
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
263
|
+
throw new Error(`Failed to read context file '${contextFile}' for threadline '${threadline.id}': ${message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
throw new Error(`Context file not found for threadline '${threadline.id}': ${contextFile}`);
|
|
277
268
|
}
|
|
278
269
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
catch (error) {
|
|
316
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
317
|
-
logger_1.logger.error(errorMessage);
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
id: threadline.id,
|
|
273
|
+
version: threadline.version,
|
|
274
|
+
patterns: threadline.patterns,
|
|
275
|
+
content: threadline.content,
|
|
276
|
+
filePath: threadline.filePath,
|
|
277
|
+
contextFiles: threadline.contextFiles,
|
|
278
|
+
contextContent
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
// 5. Call review API
|
|
282
|
+
logger_1.logger.info('Running threadline checks...');
|
|
283
|
+
const client = new client_1.ReviewAPIClient(config.api_url);
|
|
284
|
+
const response = await client.review({
|
|
285
|
+
threadlines: threadlinesWithContext,
|
|
286
|
+
diff: gitDiff.diff,
|
|
287
|
+
files: gitDiff.changedFiles,
|
|
288
|
+
apiKey: apiKey,
|
|
289
|
+
account: account,
|
|
290
|
+
repoName: repoName,
|
|
291
|
+
branchName: branchName,
|
|
292
|
+
commitSha: metadata.commitSha,
|
|
293
|
+
commitMessage: metadata.commitMessage,
|
|
294
|
+
commitAuthorName: metadata.commitAuthorName,
|
|
295
|
+
commitAuthorEmail: metadata.commitAuthorEmail,
|
|
296
|
+
prTitle: metadata.prTitle,
|
|
297
|
+
environment: environment,
|
|
298
|
+
cliVersion: CLI_VERSION,
|
|
299
|
+
reviewContext: reviewContext
|
|
300
|
+
});
|
|
301
|
+
// 7. Display results (with filtering if --full not specified)
|
|
302
|
+
displayResults(response, options.full || false);
|
|
303
|
+
// Exit with appropriate code (attention or errors = failure)
|
|
304
|
+
const hasIssues = response.results.some(r => r.status === 'attention' || r.status === 'error');
|
|
305
|
+
process.exit(hasIssues ? 1 : 0);
|
|
320
306
|
}
|
|
321
307
|
function displayResults(response, showFull) {
|
|
322
308
|
const { results, metadata, message } = response;
|
|
@@ -429,10 +415,6 @@ function displayResults(response, showFull) {
|
|
|
429
415
|
console.log(chalk_1.default.gray(JSON.stringify(item.error.rawResponse, null, 2).split('\n').map(line => ' ' + line).join('\n')));
|
|
430
416
|
}
|
|
431
417
|
}
|
|
432
|
-
else if (item.reasoning) {
|
|
433
|
-
// Fallback to reasoning if no error object
|
|
434
|
-
console.log(chalk_1.default.red(` ${item.reasoning}`));
|
|
435
|
-
}
|
|
436
418
|
console.log(''); // Empty line between errors
|
|
437
419
|
}
|
|
438
420
|
}
|
package/dist/git/bitbucket.js
CHANGED
|
@@ -23,7 +23,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.getBitbucketContext = getBitbucketContext;
|
|
25
25
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
26
|
-
const child_process_1 = require("child_process");
|
|
27
26
|
const diff_1 = require("./diff");
|
|
28
27
|
const logger_1 = require("../utils/logger");
|
|
29
28
|
/**
|
|
@@ -43,8 +42,9 @@ async function getBitbucketContext(repoRoot) {
|
|
|
43
42
|
const context = detectContext();
|
|
44
43
|
const reviewContext = detectReviewContext();
|
|
45
44
|
const commitSha = getCommitSha();
|
|
46
|
-
// Get commit author (
|
|
47
|
-
|
|
45
|
+
// Get commit author using shared function (git log)
|
|
46
|
+
// getCommitAuthor throws on failure with descriptive error
|
|
47
|
+
const commitAuthor = await (0, diff_1.getCommitAuthor)(repoRoot);
|
|
48
48
|
// Get commit message if we have a SHA
|
|
49
49
|
let commitMessage;
|
|
50
50
|
if (commitSha) {
|
|
@@ -172,32 +172,3 @@ function detectReviewContext() {
|
|
|
172
172
|
function getCommitSha() {
|
|
173
173
|
return process.env.BITBUCKET_COMMIT;
|
|
174
174
|
}
|
|
175
|
-
/**
|
|
176
|
-
* Gets commit author for Bitbucket Pipelines
|
|
177
|
-
*
|
|
178
|
-
* Bitbucket doesn't provide commit author as an environment variable,
|
|
179
|
-
* so we use git log to get it.
|
|
180
|
-
*
|
|
181
|
-
* This approach is verified by our test script (test-bitbucket-context.ts)
|
|
182
|
-
* which successfully retrieves commit author in all scenarios:
|
|
183
|
-
* - Direct commit to main
|
|
184
|
-
* - Feature branch push
|
|
185
|
-
* - PR pipeline
|
|
186
|
-
* - Merge commit
|
|
187
|
-
*/
|
|
188
|
-
async function getCommitAuthor(repoRoot) {
|
|
189
|
-
// Use raw git commands - this is exactly what the test script uses and we know it works
|
|
190
|
-
try {
|
|
191
|
-
const name = (0, child_process_1.execSync)('git log -1 --format=%an', { encoding: 'utf-8', cwd: repoRoot }).trim();
|
|
192
|
-
const email = (0, child_process_1.execSync)('git log -1 --format=%ae', { encoding: 'utf-8', cwd: repoRoot }).trim();
|
|
193
|
-
if (!name || !email) {
|
|
194
|
-
throw new Error('git log returned empty name or email');
|
|
195
|
-
}
|
|
196
|
-
return { name, email };
|
|
197
|
-
}
|
|
198
|
-
catch (error) {
|
|
199
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
200
|
-
throw new Error(`Bitbucket Pipelines: Failed to get commit author from git log. ` +
|
|
201
|
-
`Error: ${errorMessage}`);
|
|
202
|
-
}
|
|
203
|
-
}
|
package/dist/git/diff.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getCommitMessage = getCommitMessage;
|
|
7
7
|
exports.getCommitAuthor = getCommitAuthor;
|
|
8
|
+
exports.getPRDiff = getPRDiff;
|
|
8
9
|
exports.getCommitDiff = getCommitDiff;
|
|
9
10
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
10
11
|
const child_process_1 = require("child_process");
|
|
@@ -29,31 +30,92 @@ async function getCommitMessage(repoRoot, sha) {
|
|
|
29
30
|
*
|
|
30
31
|
* Uses raw git log command to extract author information.
|
|
31
32
|
* Works in all environments where git is available.
|
|
33
|
+
*
|
|
34
|
+
* Throws on error - git commits always have authors, so failure indicates
|
|
35
|
+
* an invalid SHA or repository issue that should surface immediately.
|
|
36
|
+
*
|
|
37
|
+
* Used by: GitHub, GitLab, Bitbucket, Vercel, Local (all CI environments)
|
|
32
38
|
*/
|
|
33
39
|
async function getCommitAuthor(repoRoot, sha) {
|
|
40
|
+
const commitRef = sha || 'HEAD';
|
|
41
|
+
let output;
|
|
34
42
|
try {
|
|
35
43
|
// Use raw git command (same as test scripts) - more reliable than simple-git API
|
|
36
|
-
const commitRef = sha || 'HEAD';
|
|
37
44
|
const command = `git log -1 --format="%an <%ae>" ${commitRef}`;
|
|
38
|
-
|
|
45
|
+
output = (0, child_process_1.execSync)(command, {
|
|
39
46
|
encoding: 'utf-8',
|
|
40
47
|
cwd: repoRoot
|
|
41
48
|
}).trim();
|
|
42
|
-
// Parse output: "Name <email>"
|
|
43
|
-
const match = output.match(/^(.+?)\s*<(.+?)>$/);
|
|
44
|
-
if (!match) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
const name = match[1].trim();
|
|
48
|
-
const email = match[2].trim();
|
|
49
|
-
if (!name || !email) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
return { name, email };
|
|
53
49
|
}
|
|
54
|
-
catch {
|
|
55
|
-
|
|
50
|
+
catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
throw new Error(`Failed to get commit author for ${commitRef}: ${message}`);
|
|
53
|
+
}
|
|
54
|
+
// Parse output: "Name <email>"
|
|
55
|
+
const match = output.match(/^(.+?)\s*<(.+?)>$/);
|
|
56
|
+
if (!match) {
|
|
57
|
+
throw new Error(`Failed to parse commit author for ${commitRef}. ` +
|
|
58
|
+
`Expected format "Name <email>", got: "${output}"`);
|
|
59
|
+
}
|
|
60
|
+
const name = match[1].trim();
|
|
61
|
+
const email = match[2].trim();
|
|
62
|
+
if (!name || !email) {
|
|
63
|
+
throw new Error(`Commit author for ${commitRef} has empty name or email. ` +
|
|
64
|
+
`Got name="${name}", email="${email}"`);
|
|
65
|
+
}
|
|
66
|
+
return { name, email };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get diff for a PR/MR context in CI environments.
|
|
70
|
+
*
|
|
71
|
+
* This is a shared implementation for CI environments that do shallow clones.
|
|
72
|
+
* It fetches the target branch on-demand and compares it against HEAD.
|
|
73
|
+
*
|
|
74
|
+
* Strategy:
|
|
75
|
+
* 1. Fetch target branch: origin/${targetBranch}:refs/remotes/origin/${targetBranch}
|
|
76
|
+
* 2. Diff: origin/${targetBranch}..HEAD (two dots = direct comparison)
|
|
77
|
+
*
|
|
78
|
+
* Why HEAD instead of origin/${sourceBranch}?
|
|
79
|
+
* - CI shallow clones only have HEAD available by default
|
|
80
|
+
* - origin/${sourceBranch} doesn't exist until explicitly fetched
|
|
81
|
+
* - HEAD IS the source branch in PR/MR pipelines
|
|
82
|
+
*
|
|
83
|
+
* Currently used by:
|
|
84
|
+
* - GitLab CI (gitlab.ts)
|
|
85
|
+
*
|
|
86
|
+
* Future plan:
|
|
87
|
+
* - Azure DevOps will use this when added
|
|
88
|
+
* - Once proven stable in multiple environments, consider migrating
|
|
89
|
+
* GitHub (github.ts) and Bitbucket (bitbucket.ts) to use this shared
|
|
90
|
+
* implementation instead of their inline versions.
|
|
91
|
+
*
|
|
92
|
+
* @param repoRoot - Path to the repository root
|
|
93
|
+
* @param targetBranch - The branch being merged INTO (e.g., "main", "develop")
|
|
94
|
+
* @param logger - Optional logger for debug output
|
|
95
|
+
*/
|
|
96
|
+
async function getPRDiff(repoRoot, targetBranch, logger) {
|
|
97
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
98
|
+
// Fetch target branch on-demand (works with shallow clones)
|
|
99
|
+
logger?.debug(`Fetching target branch: origin/${targetBranch}`);
|
|
100
|
+
try {
|
|
101
|
+
await git.fetch(['origin', `${targetBranch}:refs/remotes/origin/${targetBranch}`, '--depth=1']);
|
|
102
|
+
}
|
|
103
|
+
catch (fetchError) {
|
|
104
|
+
throw new Error(`Failed to fetch target branch origin/${targetBranch}. ` +
|
|
105
|
+
`This is required for PR/MR diff comparison. ` +
|
|
106
|
+
`Error: ${fetchError instanceof Error ? fetchError.message : 'Unknown error'}`);
|
|
56
107
|
}
|
|
108
|
+
// Use two dots (..) for direct comparison (same as GitHub)
|
|
109
|
+
// Two dots: shows all changes in HEAD that aren't in origin/${targetBranch}
|
|
110
|
+
// Three dots: requires finding merge base which can fail with shallow clones
|
|
111
|
+
logger?.debug(`Comparing origin/${targetBranch}..HEAD`);
|
|
112
|
+
const diff = await git.diff([`origin/${targetBranch}..HEAD`, '-U200']);
|
|
113
|
+
const diffSummary = await git.diffSummary([`origin/${targetBranch}..HEAD`]);
|
|
114
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
115
|
+
return {
|
|
116
|
+
diff: diff || '',
|
|
117
|
+
changedFiles
|
|
118
|
+
};
|
|
57
119
|
}
|
|
58
120
|
/**
|
|
59
121
|
* Get diff for a specific commit
|
package/dist/git/file.js
CHANGED
|
@@ -56,15 +56,17 @@ async function getFileContent(repoRoot, filePath) {
|
|
|
56
56
|
}
|
|
57
57
|
// Read file content
|
|
58
58
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
59
|
+
// Normalize path to forward slashes for cross-platform consistency (git uses forward slashes)
|
|
60
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
59
61
|
// Create artificial diff (all lines as additions)
|
|
60
62
|
const lines = content.split('\n');
|
|
61
63
|
const diff = lines.map((line) => `+${line}`).join('\n');
|
|
62
|
-
// Add diff header
|
|
63
|
-
const diffHeader =
|
|
64
|
+
// Add git diff header (matches format expected by server's filterDiffByFiles)
|
|
65
|
+
const diffHeader = `diff --git a/${normalizedPath} b/${normalizedPath}\n--- /dev/null\n+++ b/${normalizedPath}\n@@ -0,0 +1,${lines.length} @@\n`;
|
|
64
66
|
const fullDiff = diffHeader + diff;
|
|
65
67
|
return {
|
|
66
68
|
diff: fullDiff,
|
|
67
|
-
changedFiles: [
|
|
69
|
+
changedFiles: [normalizedPath]
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
/**
|
|
@@ -81,8 +83,8 @@ async function getFolderContent(repoRoot, folderPath) {
|
|
|
81
83
|
if (!stats.isDirectory()) {
|
|
82
84
|
throw new Error(`Path '${folderPath}' is not a folder`);
|
|
83
85
|
}
|
|
84
|
-
// Find all files recursively
|
|
85
|
-
const pattern = path.join(fullPath, '**', '*');
|
|
86
|
+
// Find all files recursively (normalize to forward slashes for glob on Windows)
|
|
87
|
+
const pattern = path.join(fullPath, '**', '*').replace(/\\/g, '/');
|
|
86
88
|
const files = await (0, glob_1.glob)(pattern, {
|
|
87
89
|
cwd: repoRoot,
|
|
88
90
|
absolute: false,
|
|
@@ -108,11 +110,13 @@ async function getFolderContent(repoRoot, folderPath) {
|
|
|
108
110
|
try {
|
|
109
111
|
const content = fs.readFileSync(path.resolve(repoRoot, filePath), 'utf-8');
|
|
110
112
|
const lines = content.split('\n');
|
|
111
|
-
//
|
|
113
|
+
// Normalize path to forward slashes for cross-platform consistency
|
|
114
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
115
|
+
// Create artificial diff for this file (git diff format)
|
|
112
116
|
const fileDiff = lines.map((line) => `+${line}`).join('\n');
|
|
113
|
-
const diffHeader =
|
|
117
|
+
const diffHeader = `diff --git a/${normalizedPath} b/${normalizedPath}\n--- /dev/null\n+++ b/${normalizedPath}\n@@ -0,0 +1,${lines.length} @@\n`;
|
|
114
118
|
diffs.push(diffHeader + fileDiff);
|
|
115
|
-
changedFiles.push(
|
|
119
|
+
changedFiles.push(normalizedPath);
|
|
116
120
|
}
|
|
117
121
|
catch (error) {
|
|
118
122
|
// Skip files that can't be read (permissions, etc.)
|
|
@@ -148,11 +152,13 @@ async function getMultipleFilesContent(repoRoot, filePaths) {
|
|
|
148
152
|
// Read file content
|
|
149
153
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
150
154
|
const lines = content.split('\n');
|
|
151
|
-
//
|
|
155
|
+
// Normalize path to forward slashes for cross-platform consistency
|
|
156
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
157
|
+
// Create artificial diff for this file (git diff format)
|
|
152
158
|
const fileDiff = lines.map((line) => `+${line}`).join('\n');
|
|
153
|
-
const diffHeader =
|
|
159
|
+
const diffHeader = `diff --git a/${normalizedPath} b/${normalizedPath}\n--- /dev/null\n+++ b/${normalizedPath}\n@@ -0,0 +1,${lines.length} @@\n`;
|
|
154
160
|
diffs.push(diffHeader + fileDiff);
|
|
155
|
-
changedFiles.push(
|
|
161
|
+
changedFiles.push(normalizedPath);
|
|
156
162
|
}
|
|
157
163
|
return {
|
|
158
164
|
diff: diffs.join('\n'),
|
package/dist/git/github.js
CHANGED
|
@@ -43,6 +43,7 @@ async function getGitHubContext(repoRoot) {
|
|
|
43
43
|
'This should be automatically provided by GitHub Actions.');
|
|
44
44
|
}
|
|
45
45
|
// Get commit author using git commands (same approach as Bitbucket/Local)
|
|
46
|
+
// getCommitAuthor throws on failure with descriptive error
|
|
46
47
|
const commitAuthor = await (0, diff_1.getCommitAuthor)(repoRoot, commitSha);
|
|
47
48
|
// Get commit message if we have a SHA
|
|
48
49
|
let commitMessage;
|
|
@@ -54,11 +55,6 @@ async function getGitHubContext(repoRoot) {
|
|
|
54
55
|
}
|
|
55
56
|
// Get PR title if in PR context
|
|
56
57
|
const prTitle = getPRTitle(context);
|
|
57
|
-
// Validate commit author was found
|
|
58
|
-
if (!commitAuthor) {
|
|
59
|
-
throw new Error(`GitHub Actions: Failed to get commit author from git log for commit ${commitSha || 'HEAD'}. ` +
|
|
60
|
-
'This should be automatically available in the git repository.');
|
|
61
|
-
}
|
|
62
58
|
return {
|
|
63
59
|
diff,
|
|
64
60
|
repoName,
|
package/dist/git/gitlab.js
CHANGED
|
@@ -37,8 +37,9 @@ async function getGitLabContext(repoRoot) {
|
|
|
37
37
|
const context = detectContext();
|
|
38
38
|
const reviewContext = detectReviewContext();
|
|
39
39
|
const commitSha = getCommitSha(context);
|
|
40
|
-
// Get commit author
|
|
41
|
-
|
|
40
|
+
// Get commit author using shared function (git log)
|
|
41
|
+
// getCommitAuthor throws on failure with descriptive error
|
|
42
|
+
const commitAuthor = await (0, diff_1.getCommitAuthor)(repoRoot);
|
|
42
43
|
// Get commit message if we have a SHA
|
|
43
44
|
let commitMessage;
|
|
44
45
|
if (commitSha) {
|
|
@@ -65,7 +66,7 @@ async function getGitLabContext(repoRoot) {
|
|
|
65
66
|
* Get diff for GitLab CI environment
|
|
66
67
|
*
|
|
67
68
|
* Strategy:
|
|
68
|
-
* - MR context:
|
|
69
|
+
* - MR context: Uses shared getPRDiff() - fetches target branch, compares against HEAD
|
|
69
70
|
* - Any push (main or feature branch): Compare last commit only (HEAD~1...HEAD)
|
|
70
71
|
*
|
|
71
72
|
* Note: GitLab CI does a shallow clone, so we fetch the target branch for MR context.
|
|
@@ -75,20 +76,13 @@ async function getDiff(repoRoot) {
|
|
|
75
76
|
const git = (0, simple_git_1.default)(repoRoot);
|
|
76
77
|
const mrIid = process.env.CI_MERGE_REQUEST_IID;
|
|
77
78
|
const targetBranch = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
78
|
-
|
|
79
|
-
// MR Context: Fetch target branch and compare
|
|
79
|
+
// MR Context: Use shared getPRDiff() implementation
|
|
80
80
|
if (mrIid) {
|
|
81
|
-
if (!targetBranch
|
|
82
|
-
throw new Error('GitLab MR context detected but CI_MERGE_REQUEST_TARGET_BRANCH_NAME
|
|
83
|
-
'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME is missing. ' +
|
|
81
|
+
if (!targetBranch) {
|
|
82
|
+
throw new Error('GitLab MR context detected but CI_MERGE_REQUEST_TARGET_BRANCH_NAME is missing. ' +
|
|
84
83
|
'This should be automatically provided by GitLab CI.');
|
|
85
84
|
}
|
|
86
|
-
|
|
87
|
-
await git.fetch(['origin', `${targetBranch}:refs/remotes/origin/${targetBranch}`, '--depth=1']);
|
|
88
|
-
const diff = await git.diff([`origin/${targetBranch}...origin/${sourceBranch}`, '-U200']);
|
|
89
|
-
const diffSummary = await git.diffSummary([`origin/${targetBranch}...origin/${sourceBranch}`]);
|
|
90
|
-
const changedFiles = diffSummary.files.map(f => f.file);
|
|
91
|
-
return { diff: diff || '', changedFiles };
|
|
85
|
+
return (0, diff_1.getPRDiff)(repoRoot, targetBranch, logger_1.logger);
|
|
92
86
|
}
|
|
93
87
|
// Any push (main or feature branch): Review last commit only
|
|
94
88
|
const diff = await git.diff(['HEAD~1...HEAD', '-U200']);
|
|
@@ -173,28 +167,6 @@ function getCommitSha(context) {
|
|
|
173
167
|
}
|
|
174
168
|
return undefined;
|
|
175
169
|
}
|
|
176
|
-
/**
|
|
177
|
-
* Gets commit author for GitLab CI
|
|
178
|
-
* Uses CI_COMMIT_AUTHOR environment variable (most reliable)
|
|
179
|
-
*/
|
|
180
|
-
async function getCommitAuthor() {
|
|
181
|
-
const commitAuthor = process.env.CI_COMMIT_AUTHOR;
|
|
182
|
-
if (!commitAuthor) {
|
|
183
|
-
throw new Error('GitLab CI: CI_COMMIT_AUTHOR environment variable is not set. ' +
|
|
184
|
-
'This should be automatically provided by GitLab CI.');
|
|
185
|
-
}
|
|
186
|
-
// Parse "name <email>" format
|
|
187
|
-
const match = commitAuthor.match(/^(.+?)\s*<(.+?)>$/);
|
|
188
|
-
if (!match) {
|
|
189
|
-
throw new Error(`GitLab CI: CI_COMMIT_AUTHOR format is invalid. ` +
|
|
190
|
-
`Expected format: "name <email>", got: "${commitAuthor}". ` +
|
|
191
|
-
`This should be automatically provided by GitLab CI in the correct format.`);
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
name: match[1].trim(),
|
|
195
|
-
email: match[2].trim()
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
170
|
/**
|
|
199
171
|
* Gets MR title for GitLab CI
|
|
200
172
|
*/
|
package/dist/git/local.js
CHANGED
|
@@ -177,15 +177,8 @@ async function getCommitAuthorFromConfig(repoRoot) {
|
|
|
177
177
|
}
|
|
178
178
|
/**
|
|
179
179
|
* Gets commit author from git log (for specific commits)
|
|
180
|
+
* getCommitAuthor throws on failure with descriptive error
|
|
180
181
|
*/
|
|
181
182
|
async function getCommitAuthorFromGit(repoRoot, commitSha) {
|
|
182
|
-
|
|
183
|
-
if (!gitAuthor || !gitAuthor.email) {
|
|
184
|
-
throw new Error(`Local: Failed to get commit author from git log for commit ${commitSha}. ` +
|
|
185
|
-
'This should be available in your local git repository.');
|
|
186
|
-
}
|
|
187
|
-
return {
|
|
188
|
-
name: gitAuthor.name,
|
|
189
|
-
email: gitAuthor.email
|
|
190
|
-
};
|
|
183
|
+
return (0, diff_1.getCommitAuthor)(repoRoot, commitSha);
|
|
191
184
|
}
|
package/dist/git/vercel.js
CHANGED
|
@@ -17,7 +17,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.getVercelContext = getVercelContext;
|
|
19
19
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
20
|
-
const child_process_1 = require("child_process");
|
|
21
20
|
const diff_1 = require("./diff");
|
|
22
21
|
/**
|
|
23
22
|
* Gets all Vercel context
|
|
@@ -36,8 +35,9 @@ async function getVercelContext(repoRoot) {
|
|
|
36
35
|
const commitSha = getCommitSha();
|
|
37
36
|
const context = { type: 'commit', commitSha };
|
|
38
37
|
const reviewContext = 'commit';
|
|
39
|
-
// Get commit author
|
|
40
|
-
|
|
38
|
+
// Get commit author using shared function (git log)
|
|
39
|
+
// getCommitAuthor throws on failure with descriptive error
|
|
40
|
+
const commitAuthor = await (0, diff_1.getCommitAuthor)(repoRoot, commitSha);
|
|
41
41
|
// Get commit message
|
|
42
42
|
let commitMessage;
|
|
43
43
|
const message = await (0, diff_1.getCommitMessage)(repoRoot, commitSha);
|
|
@@ -116,33 +116,3 @@ function getCommitSha() {
|
|
|
116
116
|
}
|
|
117
117
|
return commitSha;
|
|
118
118
|
}
|
|
119
|
-
/**
|
|
120
|
-
* Gets commit author for Vercel
|
|
121
|
-
* Uses VERCEL_GIT_COMMIT_AUTHOR_NAME for name, raw git log command for email
|
|
122
|
-
*
|
|
123
|
-
* Uses raw `git log` command (same as test script) instead of simple-git library
|
|
124
|
-
* because simple-git's log method may not work correctly in Vercel's shallow clone.
|
|
125
|
-
*/
|
|
126
|
-
async function getCommitAuthorForVercel(repoRoot, commitSha) {
|
|
127
|
-
const authorName = process.env.VERCEL_GIT_COMMIT_AUTHOR_NAME;
|
|
128
|
-
if (!authorName) {
|
|
129
|
-
throw new Error('Vercel: VERCEL_GIT_COMMIT_AUTHOR_NAME environment variable is not set. ' +
|
|
130
|
-
'This should be automatically provided by Vercel.');
|
|
131
|
-
}
|
|
132
|
-
// Use raw git log command (same approach as test script) - more reliable than simple-git
|
|
133
|
-
try {
|
|
134
|
-
const email = (0, child_process_1.execSync)(`git log ${commitSha} -1 --format=%ae`, { encoding: 'utf-8', cwd: repoRoot }).trim();
|
|
135
|
-
if (!email) {
|
|
136
|
-
throw new Error('Email is empty');
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
name: authorName.trim(),
|
|
140
|
-
email: email.trim()
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
throw new Error(`Vercel: Failed to get commit author email from git log for commit ${commitSha}. ` +
|
|
145
|
-
`This should be available in Vercel's build environment. ` +
|
|
146
|
-
`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
147
|
-
}
|
|
148
|
-
}
|