threadlines 0.2.14 → 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.
@@ -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 (from git log - Bitbucket doesn't provide this as env var)
47
- const commitAuthor = await getCommitAuthor(repoRoot);
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
- const output = (0, child_process_1.execSync)(command, {
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
- return null;
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
@@ -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,
@@ -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 (fails loudly if unavailable)
41
- const commitAuthor = await getCommitAuthor();
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: Fetch target branch, compare source vs target (full MR diff)
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
- const sourceBranch = process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
79
- // MR Context: Fetch target branch and compare
79
+ // MR Context: Use shared getPRDiff() implementation
80
80
  if (mrIid) {
81
- if (!targetBranch || !sourceBranch) {
82
- throw new Error('GitLab MR context detected but CI_MERGE_REQUEST_TARGET_BRANCH_NAME or ' +
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
- logger_1.logger.debug(`Fetching target branch: origin/${targetBranch}`);
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
- const gitAuthor = await (0, diff_1.getCommitAuthor)(repoRoot, commitSha);
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
  }
@@ -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 (fails loudly if unavailable)
40
- const commitAuthor = await getCommitAuthorForVercel(repoRoot, commitSha);
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
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadlines",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "Threadlines CLI - AI-powered linter based on your natural language documentation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {