threadlines 0.2.6 → 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 +5 -20
- package/dist/git/bitbucket.js +17 -91
- 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/utils/context.js +4 -184
- package/package.json +1 -1
- package/dist/git/repo.js +0 -253
package/dist/commands/check.js
CHANGED
|
@@ -147,11 +147,11 @@ async function checkCommand(options) {
|
|
|
147
147
|
let branchName;
|
|
148
148
|
let metadata = {};
|
|
149
149
|
// Check for explicit flags
|
|
150
|
-
const explicitFlags = [options.
|
|
150
|
+
const explicitFlags = [options.commit, options.file, options.folder, options.files].filter(Boolean);
|
|
151
151
|
// Validate mutually exclusive flags
|
|
152
152
|
if (explicitFlags.length > 1) {
|
|
153
153
|
console.error(chalk_1.default.red('❌ Error: Only one review option can be specified at a time'));
|
|
154
|
-
console.log(chalk_1.default.gray(' Options: --
|
|
154
|
+
console.log(chalk_1.default.gray(' Options: --commit, --file, --folder, --files'));
|
|
155
155
|
process.exit(1);
|
|
156
156
|
}
|
|
157
157
|
// CI environments: auto-detect only, flags are ignored with warning
|
|
@@ -159,10 +159,9 @@ async function checkCommand(options) {
|
|
|
159
159
|
if ((0, environment_1.isCIEnvironment)(environment)) {
|
|
160
160
|
// Warn if flags are passed in CI - they're meant for local development
|
|
161
161
|
if (explicitFlags.length > 0) {
|
|
162
|
-
const flagName = options.
|
|
163
|
-
options.
|
|
164
|
-
options.
|
|
165
|
-
options.folder ? '--folder' : '--files';
|
|
162
|
+
const flagName = options.commit ? '--commit' :
|
|
163
|
+
options.file ? '--file' :
|
|
164
|
+
options.folder ? '--folder' : '--files';
|
|
166
165
|
console.log(chalk_1.default.yellow(`⚠️ Warning: ${flagName} flag ignored in CI environment. Using auto-detection.\n`));
|
|
167
166
|
}
|
|
168
167
|
// CI auto-detect: use environment-specific context
|
|
@@ -199,20 +198,6 @@ async function checkCommand(options) {
|
|
|
199
198
|
console.log(chalk_1.default.gray(`📝 Reading ${options.files.length} file(s)...`));
|
|
200
199
|
gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
|
|
201
200
|
}
|
|
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
201
|
else if (options.commit) {
|
|
217
202
|
console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${options.commit}...`));
|
|
218
203
|
gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
|
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.
|
|
@@ -65,30 +65,17 @@ async function getBitbucketContext(repoRoot) {
|
|
|
65
65
|
/**
|
|
66
66
|
* Get diff for Bitbucket Pipelines environment
|
|
67
67
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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)
|
|
70
71
|
*
|
|
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.
|
|
72
|
+
* Note: Bitbucket Pipelines with depth: full has full git history available.
|
|
85
73
|
*/
|
|
86
74
|
async function getDiff(repoRoot) {
|
|
87
75
|
const git = (0, simple_git_1.default)(repoRoot);
|
|
88
|
-
const branchName = process.env.BITBUCKET_BRANCH;
|
|
89
76
|
const prId = process.env.BITBUCKET_PR_ID;
|
|
90
77
|
const prDestinationBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
|
|
91
|
-
//
|
|
78
|
+
// PR Context: Compare source vs target branch
|
|
92
79
|
if (prId) {
|
|
93
80
|
if (!prDestinationBranch) {
|
|
94
81
|
throw new Error('Bitbucket PR context detected but BITBUCKET_PR_DESTINATION_BRANCH is not set. ' +
|
|
@@ -100,70 +87,12 @@ async function getDiff(repoRoot) {
|
|
|
100
87
|
const changedFiles = diffSummary.files.map(f => f.file);
|
|
101
88
|
return { diff: diff || '', changedFiles };
|
|
102
89
|
}
|
|
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`]);
|
|
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']);
|
|
124
93
|
const changedFiles = diffSummary.files.map(f => f.file);
|
|
125
94
|
return { diff: diff || '', changedFiles };
|
|
126
95
|
}
|
|
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
96
|
/**
|
|
168
97
|
* Gets repository name for Bitbucket Pipelines
|
|
169
98
|
*
|
|
@@ -190,7 +119,10 @@ function getBranchName() {
|
|
|
190
119
|
return branchName;
|
|
191
120
|
}
|
|
192
121
|
/**
|
|
193
|
-
* Detects Bitbucket context (PR
|
|
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
|
|
194
126
|
*/
|
|
195
127
|
function detectContext() {
|
|
196
128
|
// PR context
|
|
@@ -205,22 +137,16 @@ function detectContext() {
|
|
|
205
137
|
targetBranch: prDestinationBranch
|
|
206
138
|
};
|
|
207
139
|
}
|
|
208
|
-
//
|
|
209
|
-
if (process.env.BITBUCKET_BRANCH) {
|
|
210
|
-
return {
|
|
211
|
-
type: 'branch',
|
|
212
|
-
branchName: process.env.BITBUCKET_BRANCH
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
// Commit context
|
|
140
|
+
// Any push (main or feature branch) → commit context
|
|
216
141
|
if (process.env.BITBUCKET_COMMIT) {
|
|
217
142
|
return {
|
|
218
143
|
type: 'commit',
|
|
219
144
|
commitSha: process.env.BITBUCKET_COMMIT
|
|
220
145
|
};
|
|
221
146
|
}
|
|
222
|
-
|
|
223
|
-
|
|
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.');
|
|
224
150
|
}
|
|
225
151
|
/**
|
|
226
152
|
* 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/github.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* GitHub Actions Environment
|
|
3
|
+
* GitHub Actions Environment
|
|
4
4
|
*
|
|
5
5
|
* All GitHub-specific logic is contained in this file.
|
|
6
6
|
* No dependencies on other environment implementations.
|
|
@@ -52,10 +52,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
52
52
|
exports.getGitHubContext = getGitHubContext;
|
|
53
53
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
54
54
|
const fs = __importStar(require("fs"));
|
|
55
|
-
const repo_1 = require("./repo");
|
|
56
55
|
const diff_1 = require("./diff");
|
|
57
56
|
/**
|
|
58
|
-
* Gets all GitHub context
|
|
57
|
+
* Gets all GitHub context
|
|
59
58
|
*/
|
|
60
59
|
async function getGitHubContext(repoRoot) {
|
|
61
60
|
const git = (0, simple_git_1.default)(repoRoot);
|
|
@@ -70,7 +69,6 @@ async function getGitHubContext(repoRoot) {
|
|
|
70
69
|
const branchName = await getBranchName();
|
|
71
70
|
const context = detectContext();
|
|
72
71
|
const commitSha = getCommitSha(context);
|
|
73
|
-
// Get commit author (fails loudly if unavailable)
|
|
74
72
|
// Note: commitSha parameter not needed - GitHub reads from GITHUB_EVENT_PATH JSON
|
|
75
73
|
const commitAuthor = await getCommitAuthor();
|
|
76
74
|
// Get commit message if we have a SHA
|
|
@@ -96,17 +94,17 @@ async function getGitHubContext(repoRoot) {
|
|
|
96
94
|
}
|
|
97
95
|
/**
|
|
98
96
|
* Gets diff for GitHub Actions CI environment
|
|
97
|
+
*
|
|
98
|
+
* Strategy:
|
|
99
|
+
* - PR context: Compare source branch vs target branch (full PR diff)
|
|
100
|
+
* - Any push (main or feature branch): Compare last commit only (HEAD~1...HEAD)
|
|
99
101
|
*/
|
|
100
102
|
async function getDiff(repoRoot) {
|
|
101
103
|
const git = (0, simple_git_1.default)(repoRoot);
|
|
102
|
-
const defaultBranch = await (0, repo_1.getDefaultBranchName)(repoRoot);
|
|
103
|
-
// Determine context from GitHub environment variables
|
|
104
104
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
105
105
|
const baseRef = process.env.GITHUB_BASE_REF;
|
|
106
106
|
const headRef = process.env.GITHUB_HEAD_REF;
|
|
107
|
-
|
|
108
|
-
const commitSha = process.env.GITHUB_SHA;
|
|
109
|
-
// Scenario 1: PR Context
|
|
107
|
+
// PR Context: Compare source vs target branch
|
|
110
108
|
if (eventName === 'pull_request') {
|
|
111
109
|
if (!baseRef || !headRef) {
|
|
112
110
|
throw new Error('GitHub PR context detected but GITHUB_BASE_REF or GITHUB_HEAD_REF is missing. ' +
|
|
@@ -120,36 +118,14 @@ async function getDiff(repoRoot) {
|
|
|
120
118
|
changedFiles
|
|
121
119
|
};
|
|
122
120
|
}
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
changedFiles
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
136
|
-
throw new Error(`Could not get diff for default branch '${defaultBranch}'. ` +
|
|
137
|
-
`This might be the first commit on the branch. Error: ${errorMessage}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// Scenario 3: Feature Branch Push
|
|
141
|
-
if (refName) {
|
|
142
|
-
const diff = await git.diff([`origin/${defaultBranch}...origin/${refName}`, '-U200']);
|
|
143
|
-
const diffSummary = await git.diffSummary([`origin/${defaultBranch}...origin/${refName}`]);
|
|
144
|
-
const changedFiles = diffSummary.files.map(f => f.file);
|
|
145
|
-
return {
|
|
146
|
-
diff: diff || '',
|
|
147
|
-
changedFiles
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
throw new Error('GitHub Actions environment detected but no valid context found. ' +
|
|
151
|
-
'Expected GITHUB_EVENT_NAME="pull_request" (with GITHUB_BASE_REF/GITHUB_HEAD_REF) ' +
|
|
152
|
-
'or GITHUB_REF_NAME for branch context.');
|
|
121
|
+
// Any push (main or feature branch): Review last commit only
|
|
122
|
+
const diff = await git.diff(['HEAD~1...HEAD', '-U200']);
|
|
123
|
+
const diffSummary = await git.diffSummary(['HEAD~1...HEAD']);
|
|
124
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
125
|
+
return {
|
|
126
|
+
diff: diff || '',
|
|
127
|
+
changedFiles
|
|
128
|
+
};
|
|
153
129
|
}
|
|
154
130
|
/**
|
|
155
131
|
* Gets repository name for GitHub Actions
|
|
@@ -175,10 +151,13 @@ async function getBranchName() {
|
|
|
175
151
|
return refName;
|
|
176
152
|
}
|
|
177
153
|
/**
|
|
178
|
-
* Detects GitHub context (PR
|
|
154
|
+
* Detects GitHub context (PR or commit)
|
|
155
|
+
*
|
|
156
|
+
* - PR context: When GITHUB_EVENT_NAME is 'pull_request'
|
|
157
|
+
* - Commit context: Any push (main or feature branch) - reviews single commit
|
|
179
158
|
*/
|
|
180
159
|
function detectContext() {
|
|
181
|
-
//
|
|
160
|
+
// PR context
|
|
182
161
|
if (process.env.GITHUB_EVENT_NAME === 'pull_request') {
|
|
183
162
|
const targetBranch = process.env.GITHUB_BASE_REF;
|
|
184
163
|
const sourceBranch = process.env.GITHUB_HEAD_REF;
|
|
@@ -192,22 +171,16 @@ function detectContext() {
|
|
|
192
171
|
};
|
|
193
172
|
}
|
|
194
173
|
}
|
|
195
|
-
//
|
|
196
|
-
if (process.env.GITHUB_REF_NAME) {
|
|
197
|
-
return {
|
|
198
|
-
type: 'branch',
|
|
199
|
-
branchName: process.env.GITHUB_REF_NAME
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
// 3. Check for commit context
|
|
174
|
+
// Any push (main or feature branch) → commit context
|
|
203
175
|
if (process.env.GITHUB_SHA) {
|
|
204
176
|
return {
|
|
205
177
|
type: 'commit',
|
|
206
178
|
commitSha: process.env.GITHUB_SHA
|
|
207
179
|
};
|
|
208
180
|
}
|
|
209
|
-
|
|
210
|
-
|
|
181
|
+
throw new Error('GitHub Actions: Could not detect context. ' +
|
|
182
|
+
'Expected GITHUB_EVENT_NAME="pull_request" or GITHUB_SHA to be set. ' +
|
|
183
|
+
'This should be automatically provided by GitHub Actions.');
|
|
211
184
|
}
|
|
212
185
|
/**
|
|
213
186
|
* Gets commit SHA from context
|
|
@@ -216,7 +189,7 @@ function getCommitSha(context) {
|
|
|
216
189
|
if (context.type === 'commit') {
|
|
217
190
|
return context.commitSha;
|
|
218
191
|
}
|
|
219
|
-
if (context.type === '
|
|
192
|
+
if (context.type === 'pr') {
|
|
220
193
|
return process.env.GITHUB_SHA;
|
|
221
194
|
}
|
|
222
195
|
return undefined;
|
package/dist/git/gitlab.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* GitLab CI Environment
|
|
3
|
+
* GitLab CI Environment
|
|
4
4
|
*
|
|
5
5
|
* All GitLab-specific logic is contained in this file.
|
|
6
6
|
* No dependencies on other environment implementations.
|
|
@@ -20,7 +20,7 @@ exports.getGitLabContext = getGitLabContext;
|
|
|
20
20
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
21
21
|
const diff_1 = require("./diff");
|
|
22
22
|
/**
|
|
23
|
-
* Gets all GitLab context
|
|
23
|
+
* Gets all GitLab context
|
|
24
24
|
*/
|
|
25
25
|
async function getGitLabContext(repoRoot) {
|
|
26
26
|
const git = (0, simple_git_1.default)(repoRoot);
|
|
@@ -61,29 +61,19 @@ async function getGitLabContext(repoRoot) {
|
|
|
61
61
|
/**
|
|
62
62
|
* Get diff for GitLab CI environment
|
|
63
63
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
64
|
+
* Strategy:
|
|
65
|
+
* - MR context: Fetch target branch, compare source vs target (full MR diff)
|
|
66
|
+
* - Any push (main or feature branch): Compare last commit only (HEAD~1...HEAD)
|
|
66
67
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* 1. MR Context (CI_MERGE_REQUEST_IID is set):
|
|
70
|
-
* - Fetch target branch, then diff target vs source
|
|
71
|
-
*
|
|
72
|
-
* 2. Feature Branch Push (CI_COMMIT_REF_NAME != CI_DEFAULT_BRANCH):
|
|
73
|
-
* - Fetch default branch, then diff default vs feature
|
|
74
|
-
*
|
|
75
|
-
* 3. Default Branch Push (CI_COMMIT_REF_NAME == CI_DEFAULT_BRANCH):
|
|
76
|
-
* - Use HEAD~1...HEAD (last commit only, no fetch needed)
|
|
68
|
+
* Note: GitLab CI does a shallow clone, so we fetch the target branch for MR context.
|
|
69
|
+
* For regular pushes, HEAD~1...HEAD works without additional fetching.
|
|
77
70
|
*/
|
|
78
71
|
async function getDiff(repoRoot) {
|
|
79
72
|
const git = (0, simple_git_1.default)(repoRoot);
|
|
80
|
-
// Get GitLab CI environment variables
|
|
81
73
|
const mrIid = process.env.CI_MERGE_REQUEST_IID;
|
|
82
74
|
const targetBranch = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
83
75
|
const sourceBranch = process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
|
|
84
|
-
|
|
85
|
-
const defaultBranch = process.env.CI_DEFAULT_BRANCH || 'main';
|
|
86
|
-
// Scenario 1: MR Context
|
|
76
|
+
// MR Context: Fetch target branch and compare
|
|
87
77
|
if (mrIid) {
|
|
88
78
|
if (!targetBranch || !sourceBranch) {
|
|
89
79
|
throw new Error('GitLab MR context detected but CI_MERGE_REQUEST_TARGET_BRANCH_NAME or ' +
|
|
@@ -97,23 +87,9 @@ async function getDiff(repoRoot) {
|
|
|
97
87
|
const changedFiles = diffSummary.files.map(f => f.file);
|
|
98
88
|
return { diff: diff || '', changedFiles };
|
|
99
89
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
// Scenario 3: Default Branch Push
|
|
105
|
-
if (refName === defaultBranch) {
|
|
106
|
-
console.log(` [GitLab] Push to default branch (${defaultBranch}), using HEAD~1...HEAD`);
|
|
107
|
-
const diff = await git.diff(['HEAD~1...HEAD', '-U200']);
|
|
108
|
-
const diffSummary = await git.diffSummary(['HEAD~1...HEAD']);
|
|
109
|
-
const changedFiles = diffSummary.files.map(f => f.file);
|
|
110
|
-
return { diff: diff || '', changedFiles };
|
|
111
|
-
}
|
|
112
|
-
// Scenario 2: Feature Branch Push
|
|
113
|
-
console.log(` [GitLab] Feature branch push, fetching default branch: origin/${defaultBranch}`);
|
|
114
|
-
await git.fetch(['origin', `${defaultBranch}:refs/remotes/origin/${defaultBranch}`, '--depth=1']);
|
|
115
|
-
const diff = await git.diff([`origin/${defaultBranch}...origin/${refName}`, '-U200']);
|
|
116
|
-
const diffSummary = await git.diffSummary([`origin/${defaultBranch}...origin/${refName}`]);
|
|
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']);
|
|
117
93
|
const changedFiles = diffSummary.files.map(f => f.file);
|
|
118
94
|
return { diff: diff || '', changedFiles };
|
|
119
95
|
}
|
|
@@ -140,10 +116,13 @@ async function getBranchName() {
|
|
|
140
116
|
return refName;
|
|
141
117
|
}
|
|
142
118
|
/**
|
|
143
|
-
* Detects GitLab context (MR
|
|
119
|
+
* Detects GitLab context (MR or commit)
|
|
120
|
+
*
|
|
121
|
+
* - MR context: When CI_MERGE_REQUEST_IID is set
|
|
122
|
+
* - Commit context: Any push (main or feature branch) - reviews single commit
|
|
144
123
|
*/
|
|
145
124
|
function detectContext() {
|
|
146
|
-
//
|
|
125
|
+
// MR context
|
|
147
126
|
const mrIid = process.env.CI_MERGE_REQUEST_IID;
|
|
148
127
|
if (mrIid) {
|
|
149
128
|
const targetBranch = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
@@ -157,22 +136,16 @@ function detectContext() {
|
|
|
157
136
|
};
|
|
158
137
|
}
|
|
159
138
|
}
|
|
160
|
-
//
|
|
161
|
-
if (process.env.CI_COMMIT_REF_NAME) {
|
|
162
|
-
return {
|
|
163
|
-
type: 'branch',
|
|
164
|
-
branchName: process.env.CI_COMMIT_REF_NAME
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
// 3. Check for commit context
|
|
139
|
+
// Any push (main or feature branch) → commit context
|
|
168
140
|
if (process.env.CI_COMMIT_SHA) {
|
|
169
141
|
return {
|
|
170
142
|
type: 'commit',
|
|
171
143
|
commitSha: process.env.CI_COMMIT_SHA
|
|
172
144
|
};
|
|
173
145
|
}
|
|
174
|
-
|
|
175
|
-
|
|
146
|
+
throw new Error('GitLab CI: Could not detect context. ' +
|
|
147
|
+
'Expected CI_MERGE_REQUEST_IID or CI_COMMIT_SHA to be set. ' +
|
|
148
|
+
'This should be automatically provided by GitLab CI.');
|
|
176
149
|
}
|
|
177
150
|
/**
|
|
178
151
|
* Gets commit SHA from context
|
|
@@ -181,7 +154,7 @@ function getCommitSha(context) {
|
|
|
181
154
|
if (context.type === 'commit') {
|
|
182
155
|
return context.commitSha;
|
|
183
156
|
}
|
|
184
|
-
if (context.type === '
|
|
157
|
+
if (context.type === 'mr') {
|
|
185
158
|
return process.env.CI_COMMIT_SHA;
|
|
186
159
|
}
|
|
187
160
|
return undefined;
|
package/dist/git/local.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Local Environment
|
|
3
|
+
* Local Environment
|
|
4
4
|
*
|
|
5
5
|
* All Local-specific logic is contained in this file.
|
|
6
6
|
* No dependencies on other environment implementations.
|
|
@@ -19,7 +19,7 @@ exports.getLocalContext = getLocalContext;
|
|
|
19
19
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
20
20
|
const diff_1 = require("./diff");
|
|
21
21
|
/**
|
|
22
|
-
* Gets all Local context
|
|
22
|
+
* Gets all Local context
|
|
23
23
|
*/
|
|
24
24
|
async function getLocalContext(repoRoot, commitSha) {
|
|
25
25
|
const git = (0, simple_git_1.default)(repoRoot);
|
package/dist/git/vercel.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Vercel Environment
|
|
3
|
+
* Vercel Environment
|
|
4
4
|
*
|
|
5
5
|
* All Vercel-specific logic is contained in this file.
|
|
6
6
|
* No dependencies on other environment implementations.
|
|
@@ -20,7 +20,7 @@ const simple_git_1 = __importDefault(require("simple-git"));
|
|
|
20
20
|
const child_process_1 = require("child_process");
|
|
21
21
|
const diff_1 = require("./diff");
|
|
22
22
|
/**
|
|
23
|
-
* Gets all Vercel context
|
|
23
|
+
* Gets all Vercel context
|
|
24
24
|
*/
|
|
25
25
|
async function getVercelContext(repoRoot) {
|
|
26
26
|
const git = (0, simple_git_1.default)(repoRoot);
|
package/dist/index.js
CHANGED
|
@@ -63,7 +63,6 @@ program
|
|
|
63
63
|
.description('Check code against your threadlines')
|
|
64
64
|
.option('--api-url <url>', 'Threadline server URL', process.env.THREADLINE_API_URL || 'https://devthreadline.com')
|
|
65
65
|
.option('--full', 'Show all results (compliant, attention, not_relevant). Default: only attention items')
|
|
66
|
-
.option('--branch <name>', 'Review all commits in branch vs base (e.g., --branch feature/new-feature)')
|
|
67
66
|
.option('--commit <ref>', 'Review specific commit. Accepts commit SHA or git reference (e.g., HEAD, HEAD~1, abc123). Example: --commit HEAD')
|
|
68
67
|
.option('--file <path>', 'Review entire file (all lines as additions)')
|
|
69
68
|
.option('--folder <path>', 'Review all files in folder recursively')
|
|
@@ -72,13 +71,12 @@ program
|
|
|
72
71
|
Examples:
|
|
73
72
|
$ threadlines check # Check staged/unstaged changes (local dev)
|
|
74
73
|
$ threadlines check --commit HEAD # Check latest commit locally
|
|
75
|
-
$ threadlines check --branch main # Check all commits in branch vs base
|
|
76
74
|
$ threadlines check --file src/api.ts # Check entire file
|
|
77
75
|
$ threadlines check --full # Show all results (not just attention items)
|
|
78
76
|
|
|
79
77
|
Auto-detection in CI:
|
|
80
|
-
-
|
|
81
|
-
-
|
|
78
|
+
- PR/MR context → reviews all changes in the PR/MR
|
|
79
|
+
- Push to any branch → reviews the commit being pushed
|
|
82
80
|
- Local development → reviews staged/unstaged changes
|
|
83
81
|
`)
|
|
84
82
|
.action(check_1.checkCommand);
|
package/dist/utils/context.js
CHANGED
|
@@ -1,190 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Review Context
|
|
3
|
+
* Review Context Types
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* - PR/MR:
|
|
7
|
-
* -
|
|
8
|
-
* - Commit: Single commit changes
|
|
5
|
+
* Type definitions for the different code review contexts:
|
|
6
|
+
* - PR/MR: Comparing source branch vs target branch (branch-level diff)
|
|
7
|
+
* - Commit: Single commit changes (any push without PR/MR)
|
|
9
8
|
* - Local: Staged/unstaged changes in working directory
|
|
10
|
-
*
|
|
11
|
-
* Context detection is environment-specific - each CI platform
|
|
12
|
-
* provides different environment variables.
|
|
13
9
|
*/
|
|
14
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.detectContext = detectContext;
|
|
16
|
-
/**
|
|
17
|
-
* Detects the review context based on the environment.
|
|
18
|
-
*
|
|
19
|
-
* Each environment has different environment variables available,
|
|
20
|
-
* so detection logic is environment-specific.
|
|
21
|
-
*/
|
|
22
|
-
function detectContext(environment) {
|
|
23
|
-
switch (environment) {
|
|
24
|
-
case 'github':
|
|
25
|
-
return detectGitHubContext();
|
|
26
|
-
case 'gitlab':
|
|
27
|
-
return detectGitLabContext();
|
|
28
|
-
case 'bitbucket':
|
|
29
|
-
return detectBitbucketContext();
|
|
30
|
-
case 'vercel':
|
|
31
|
-
return detectVercelContext();
|
|
32
|
-
case 'local':
|
|
33
|
-
return { type: 'local' };
|
|
34
|
-
default:
|
|
35
|
-
return { type: 'local' };
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* GitHub Actions context detection
|
|
40
|
-
*
|
|
41
|
-
* Environment Variables:
|
|
42
|
-
* - PR: GITHUB_EVENT_NAME='pull_request', GITHUB_BASE_REF, GITHUB_HEAD_REF, GITHUB_EVENT_NUMBER
|
|
43
|
-
* - Branch: GITHUB_REF_NAME
|
|
44
|
-
* - Commit: GITHUB_SHA
|
|
45
|
-
*/
|
|
46
|
-
function detectGitHubContext() {
|
|
47
|
-
// 1. Check for PR context
|
|
48
|
-
if (process.env.GITHUB_EVENT_NAME === 'pull_request') {
|
|
49
|
-
const targetBranch = process.env.GITHUB_BASE_REF;
|
|
50
|
-
const sourceBranch = process.env.GITHUB_HEAD_REF;
|
|
51
|
-
const prNumber = process.env.GITHUB_EVENT_PULL_REQUEST_NUMBER || process.env.GITHUB_EVENT_NUMBER;
|
|
52
|
-
if (targetBranch && sourceBranch && prNumber) {
|
|
53
|
-
return {
|
|
54
|
-
type: 'pr',
|
|
55
|
-
prNumber,
|
|
56
|
-
sourceBranch,
|
|
57
|
-
targetBranch
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// 2. Check for branch context
|
|
62
|
-
if (process.env.GITHUB_REF_NAME) {
|
|
63
|
-
return {
|
|
64
|
-
type: 'branch',
|
|
65
|
-
branchName: process.env.GITHUB_REF_NAME
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
// 3. Check for commit context
|
|
69
|
-
if (process.env.GITHUB_SHA) {
|
|
70
|
-
return {
|
|
71
|
-
type: 'commit',
|
|
72
|
-
commitSha: process.env.GITHUB_SHA
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
// 4. Fallback to local
|
|
76
|
-
return { type: 'local' };
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* GitLab CI context detection
|
|
80
|
-
*
|
|
81
|
-
* Environment Variables:
|
|
82
|
-
* - MR: CI_MERGE_REQUEST_IID, CI_MERGE_REQUEST_TARGET_BRANCH_NAME, CI_MERGE_REQUEST_SOURCE_BRANCH_NAME, CI_MERGE_REQUEST_TITLE
|
|
83
|
-
* - Branch: CI_COMMIT_REF_NAME
|
|
84
|
-
* - Commit: CI_COMMIT_SHA
|
|
85
|
-
*/
|
|
86
|
-
function detectGitLabContext() {
|
|
87
|
-
// 1. Check for MR context
|
|
88
|
-
if (process.env.CI_MERGE_REQUEST_IID) {
|
|
89
|
-
const targetBranch = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
90
|
-
const sourceBranch = process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
|
|
91
|
-
const mrNumber = process.env.CI_MERGE_REQUEST_IID;
|
|
92
|
-
const mrTitle = process.env.CI_MERGE_REQUEST_TITLE;
|
|
93
|
-
if (targetBranch && sourceBranch && mrNumber) {
|
|
94
|
-
return {
|
|
95
|
-
type: 'mr',
|
|
96
|
-
mrNumber,
|
|
97
|
-
sourceBranch,
|
|
98
|
-
targetBranch,
|
|
99
|
-
prTitle: mrTitle || undefined
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// 2. Check for branch context
|
|
104
|
-
if (process.env.CI_COMMIT_REF_NAME) {
|
|
105
|
-
return {
|
|
106
|
-
type: 'branch',
|
|
107
|
-
branchName: process.env.CI_COMMIT_REF_NAME
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
// 3. Check for commit context
|
|
111
|
-
if (process.env.CI_COMMIT_SHA) {
|
|
112
|
-
return {
|
|
113
|
-
type: 'commit',
|
|
114
|
-
commitSha: process.env.CI_COMMIT_SHA
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
// 4. Fallback to local
|
|
118
|
-
return { type: 'local' };
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Vercel context detection
|
|
122
|
-
*
|
|
123
|
-
* Environment Variables:
|
|
124
|
-
* - Branch: VERCEL_GIT_COMMIT_REF
|
|
125
|
-
* - Commit: VERCEL_GIT_COMMIT_SHA
|
|
126
|
-
*
|
|
127
|
-
* Vercel Limitation:
|
|
128
|
-
* Vercel performs shallow clones of the repository, typically fetching only the
|
|
129
|
-
* specific commit being deployed. The git repository in Vercel's build environment
|
|
130
|
-
* does not contain the full git history or remote branch references (e.g., origin/main).
|
|
131
|
-
* This means branch-based diff operations (comparing feature branch against base branch)
|
|
132
|
-
* are not possible because the base branch refs are not available in the repository.
|
|
133
|
-
*
|
|
134
|
-
* Solution:
|
|
135
|
-
* We hardcode commit context for Vercel, using VERCEL_GIT_COMMIT_SHA to get a
|
|
136
|
-
* commit-based diff (comparing the commit against its parent). This works within
|
|
137
|
-
* Vercel's constraints since we only need the commit SHA, not branch references.
|
|
138
|
-
*/
|
|
139
|
-
function detectVercelContext() {
|
|
140
|
-
// Hardcode commit context for Vercel due to shallow clone limitations
|
|
141
|
-
// Vercel's git repository doesn't have base branch refs available
|
|
142
|
-
if (process.env.VERCEL_GIT_COMMIT_SHA) {
|
|
143
|
-
return {
|
|
144
|
-
type: 'commit',
|
|
145
|
-
commitSha: process.env.VERCEL_GIT_COMMIT_SHA
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
// Fallback to local
|
|
149
|
-
return { type: 'local' };
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Bitbucket Pipelines context detection
|
|
153
|
-
*
|
|
154
|
-
* Environment Variables (all tested 2026-01-18):
|
|
155
|
-
* - Branch: BITBUCKET_BRANCH
|
|
156
|
-
* - Commit: BITBUCKET_COMMIT
|
|
157
|
-
* - PR: BITBUCKET_PR_ID, BITBUCKET_PR_DESTINATION_BRANCH
|
|
158
|
-
*
|
|
159
|
-
* Note: Bitbucket does not provide PR title as an environment variable.
|
|
160
|
-
*/
|
|
161
|
-
function detectBitbucketContext() {
|
|
162
|
-
// PR context
|
|
163
|
-
const prId = process.env.BITBUCKET_PR_ID;
|
|
164
|
-
const prDestinationBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
|
|
165
|
-
const sourceBranch = process.env.BITBUCKET_BRANCH;
|
|
166
|
-
if (prId && prDestinationBranch && sourceBranch) {
|
|
167
|
-
return {
|
|
168
|
-
type: 'pr',
|
|
169
|
-
prNumber: prId,
|
|
170
|
-
sourceBranch,
|
|
171
|
-
targetBranch: prDestinationBranch
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
// Branch context
|
|
175
|
-
if (process.env.BITBUCKET_BRANCH) {
|
|
176
|
-
return {
|
|
177
|
-
type: 'branch',
|
|
178
|
-
branchName: process.env.BITBUCKET_BRANCH
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
// Commit context
|
|
182
|
-
if (process.env.BITBUCKET_COMMIT) {
|
|
183
|
-
return {
|
|
184
|
-
type: 'commit',
|
|
185
|
-
commitSha: process.env.BITBUCKET_COMMIT
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
// Fallback to local
|
|
189
|
-
return { type: 'local' };
|
|
190
|
-
}
|
package/package.json
CHANGED
package/dist/git/repo.js
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.getGitHubRepoName = getGitHubRepoName;
|
|
40
|
-
exports.getVercelRepoName = getVercelRepoName;
|
|
41
|
-
exports.getLocalRepoName = getLocalRepoName;
|
|
42
|
-
exports.getGitHubBranchName = getGitHubBranchName;
|
|
43
|
-
exports.getVercelBranchName = getVercelBranchName;
|
|
44
|
-
exports.getLocalBranchName = getLocalBranchName;
|
|
45
|
-
exports.getGitLabRepoName = getGitLabRepoName;
|
|
46
|
-
exports.getGitLabBranchName = getGitLabBranchName;
|
|
47
|
-
exports.getDefaultBranchName = getDefaultBranchName;
|
|
48
|
-
const simple_git_1 = __importDefault(require("simple-git"));
|
|
49
|
-
const fs = __importStar(require("fs"));
|
|
50
|
-
const path = __importStar(require("path"));
|
|
51
|
-
/**
|
|
52
|
-
* GitHub Actions: Get repository name
|
|
53
|
-
*
|
|
54
|
-
* Uses GITHUB_REPOSITORY environment variable (format: "owner/repo").
|
|
55
|
-
* This is the ONLY method for GitHub - no fallbacks, no alternatives.
|
|
56
|
-
*/
|
|
57
|
-
async function getGitHubRepoName(_repoRoot) {
|
|
58
|
-
const githubRepo = process.env.GITHUB_REPOSITORY;
|
|
59
|
-
if (!githubRepo) {
|
|
60
|
-
throw new Error('GitHub Actions: GITHUB_REPOSITORY environment variable is not set. ' +
|
|
61
|
-
'This should be automatically provided by GitHub Actions.');
|
|
62
|
-
}
|
|
63
|
-
const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com';
|
|
64
|
-
return `${serverUrl}/${githubRepo}.git`;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Vercel: Get repository name
|
|
68
|
-
*
|
|
69
|
-
* Uses VERCEL_GIT_REPO_OWNER and VERCEL_GIT_REPO_SLUG environment variables.
|
|
70
|
-
* This is the ONLY method for Vercel - no fallbacks, no alternatives.
|
|
71
|
-
*/
|
|
72
|
-
async function getVercelRepoName(_repoRoot) {
|
|
73
|
-
const owner = process.env.VERCEL_GIT_REPO_OWNER;
|
|
74
|
-
const slug = process.env.VERCEL_GIT_REPO_SLUG;
|
|
75
|
-
if (!owner || !slug) {
|
|
76
|
-
throw new Error('Vercel: VERCEL_GIT_REPO_OWNER or VERCEL_GIT_REPO_SLUG environment variable is not set. ' +
|
|
77
|
-
'This should be automatically provided by Vercel CI.');
|
|
78
|
-
}
|
|
79
|
-
return `https://github.com/${owner}/${slug}.git`;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Local: Get repository name
|
|
83
|
-
*
|
|
84
|
-
* Uses git command to get origin remote URL.
|
|
85
|
-
* This is the ONLY method for local - no fallbacks, no alternatives.
|
|
86
|
-
* Git should always be available in local development.
|
|
87
|
-
*/
|
|
88
|
-
async function getLocalRepoName(repoRoot) {
|
|
89
|
-
const git = (0, simple_git_1.default)(repoRoot);
|
|
90
|
-
// Check if we're in a git repo
|
|
91
|
-
const isRepo = await git.checkIsRepo();
|
|
92
|
-
if (!isRepo) {
|
|
93
|
-
throw new Error('Local: Not a git repository. Threadline requires a git repository.');
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
const remotes = await git.getRemotes(true);
|
|
97
|
-
const origin = remotes.find(r => r.name === 'origin');
|
|
98
|
-
if (!origin || !origin.refs?.fetch) {
|
|
99
|
-
throw new Error('Local: No origin remote found. ' +
|
|
100
|
-
'Please configure an origin remote: git remote add origin <url>');
|
|
101
|
-
}
|
|
102
|
-
return origin.refs.fetch;
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
// If it's already our error, re-throw it
|
|
106
|
-
if (error instanceof Error && error.message.includes('Local:')) {
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
// Otherwise, wrap it
|
|
110
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
111
|
-
throw new Error(`Local: Failed to get repository name from git: ${errorMessage}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* GitHub Actions: Get branch name
|
|
116
|
-
*
|
|
117
|
-
* Uses GITHUB_REF_NAME environment variable.
|
|
118
|
-
* This is the ONLY method for GitHub - no fallbacks, no alternatives.
|
|
119
|
-
*/
|
|
120
|
-
async function getGitHubBranchName(_repoRoot) {
|
|
121
|
-
const refName = process.env.GITHUB_REF_NAME;
|
|
122
|
-
if (!refName) {
|
|
123
|
-
throw new Error('GitHub Actions: GITHUB_REF_NAME environment variable is not set. ' +
|
|
124
|
-
'This should be automatically provided by GitHub Actions.');
|
|
125
|
-
}
|
|
126
|
-
return refName;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Vercel: Get branch name
|
|
130
|
-
*
|
|
131
|
-
* Uses VERCEL_GIT_COMMIT_REF environment variable.
|
|
132
|
-
* This is the ONLY method for Vercel - no fallbacks, no alternatives.
|
|
133
|
-
*/
|
|
134
|
-
async function getVercelBranchName(_repoRoot) {
|
|
135
|
-
const branchName = process.env.VERCEL_GIT_COMMIT_REF;
|
|
136
|
-
if (!branchName) {
|
|
137
|
-
throw new Error('Vercel: VERCEL_GIT_COMMIT_REF environment variable is not set. ' +
|
|
138
|
-
'This should be automatically provided by Vercel CI.');
|
|
139
|
-
}
|
|
140
|
-
return branchName;
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Local: Get branch name
|
|
144
|
-
*
|
|
145
|
-
* Uses git command to get current branch name.
|
|
146
|
-
* This is the ONLY method for local - no fallbacks, no alternatives.
|
|
147
|
-
* Git should always be available in local development.
|
|
148
|
-
*/
|
|
149
|
-
async function getLocalBranchName(repoRoot) {
|
|
150
|
-
const git = (0, simple_git_1.default)(repoRoot);
|
|
151
|
-
// Check if we're in a git repo
|
|
152
|
-
const isRepo = await git.checkIsRepo();
|
|
153
|
-
if (!isRepo) {
|
|
154
|
-
throw new Error('Local: Not a git repository. Threadline requires a git repository.');
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
const branchName = await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
158
|
-
if (!branchName || branchName.trim() === '') {
|
|
159
|
-
throw new Error('Local: Could not determine branch name. ' +
|
|
160
|
-
'This might be a brand new repository with no commits. ' +
|
|
161
|
-
'Make at least one commit before running threadlines check.');
|
|
162
|
-
}
|
|
163
|
-
// Handle detached HEAD state
|
|
164
|
-
if (branchName === 'HEAD') {
|
|
165
|
-
throw new Error('Local: Currently in detached HEAD state. ' +
|
|
166
|
-
'Please checkout a branch before running threadlines check.');
|
|
167
|
-
}
|
|
168
|
-
return branchName.trim();
|
|
169
|
-
}
|
|
170
|
-
catch (error) {
|
|
171
|
-
// If it's already our error, re-throw it
|
|
172
|
-
if (error instanceof Error && error.message.includes('Local:')) {
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
// Otherwise, wrap it
|
|
176
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
177
|
-
throw new Error(`Local: Failed to get branch name from git: ${errorMessage}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* GitLab CI: Get repository name
|
|
182
|
-
*
|
|
183
|
-
* Uses CI_PROJECT_URL environment variable.
|
|
184
|
-
* This is the ONLY method for GitLab - no fallbacks, no alternatives.
|
|
185
|
-
*/
|
|
186
|
-
async function getGitLabRepoName(_repoRoot) {
|
|
187
|
-
const projectUrl = process.env.CI_PROJECT_URL;
|
|
188
|
-
if (!projectUrl) {
|
|
189
|
-
throw new Error('GitLab CI: CI_PROJECT_URL environment variable is not set. ' +
|
|
190
|
-
'This should be automatically provided by GitLab CI.');
|
|
191
|
-
}
|
|
192
|
-
// CI_PROJECT_URL is like "https://gitlab.com/owner/repo"
|
|
193
|
-
// Add .git suffix for consistency with other environments
|
|
194
|
-
return `${projectUrl}.git`;
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* GitLab CI: Get branch name
|
|
198
|
-
*
|
|
199
|
-
* Uses CI_COMMIT_REF_NAME environment variable.
|
|
200
|
-
* This is the ONLY method for GitLab - no fallbacks, no alternatives.
|
|
201
|
-
*/
|
|
202
|
-
async function getGitLabBranchName(_repoRoot) {
|
|
203
|
-
const refName = process.env.CI_COMMIT_REF_NAME;
|
|
204
|
-
if (!refName) {
|
|
205
|
-
throw new Error('GitLab CI: CI_COMMIT_REF_NAME environment variable is not set. ' +
|
|
206
|
-
'This should be automatically provided by GitLab CI.');
|
|
207
|
-
}
|
|
208
|
-
return refName;
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Detects the default branch name of the repository for GitHub Actions.
|
|
212
|
-
*
|
|
213
|
-
* Uses GITHUB_EVENT_PATH JSON (repository.default_branch) - the most authoritative source
|
|
214
|
-
* provided directly by GitHub Actions.
|
|
215
|
-
*
|
|
216
|
-
* This function is ONLY called from GitHub Actions context (github.ts),
|
|
217
|
-
* so GITHUB_EVENT_PATH should always be available. If it's not, we fail with a clear error.
|
|
218
|
-
*
|
|
219
|
-
* Returns the branch name (e.g., "main", "master") without the "origin/" prefix.
|
|
220
|
-
* Throws an error if the default branch cannot be detected.
|
|
221
|
-
*/
|
|
222
|
-
async function getDefaultBranchName(_repoRoot) {
|
|
223
|
-
// GitHub Actions provides GITHUB_EVENT_PATH which contains repository.default_branch
|
|
224
|
-
const githubEventPath = process.env.GITHUB_EVENT_PATH;
|
|
225
|
-
if (!githubEventPath) {
|
|
226
|
-
throw new Error('GITHUB_EVENT_PATH environment variable is not set. ' +
|
|
227
|
-
'This should be automatically provided by GitHub Actions. ' +
|
|
228
|
-
'This function should only be called in GitHub Actions context.');
|
|
229
|
-
}
|
|
230
|
-
try {
|
|
231
|
-
const eventPath = path.resolve(githubEventPath);
|
|
232
|
-
if (!fs.existsSync(eventPath)) {
|
|
233
|
-
throw new Error(`GITHUB_EVENT_PATH file does not exist: ${eventPath}`);
|
|
234
|
-
}
|
|
235
|
-
const eventJson = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
|
|
236
|
-
const defaultBranch = eventJson.repository?.default_branch;
|
|
237
|
-
if (!defaultBranch || typeof defaultBranch !== 'string') {
|
|
238
|
-
throw new Error('Could not find repository.default_branch in GITHUB_EVENT_PATH JSON. ' +
|
|
239
|
-
'This should be automatically provided by GitHub Actions.');
|
|
240
|
-
}
|
|
241
|
-
return defaultBranch;
|
|
242
|
-
}
|
|
243
|
-
catch (error) {
|
|
244
|
-
// If it's already our error, re-throw it
|
|
245
|
-
if (error instanceof Error && (error.message.includes('GITHUB_EVENT_PATH') || error.message.includes('default_branch'))) {
|
|
246
|
-
throw error;
|
|
247
|
-
}
|
|
248
|
-
// Otherwise, wrap it
|
|
249
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
250
|
-
throw new Error(`Failed to read or parse GITHUB_EVENT_PATH: ${errorMessage}. ` +
|
|
251
|
-
'This should be automatically provided by GitHub Actions.');
|
|
252
|
-
}
|
|
253
|
-
}
|