threadlines 0.2.4 → 0.2.7

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