korekt-cli 0.9.3 → 0.9.4
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/package.json +1 -1
- package/src/git-logic.js +11 -2
- package/src/git-logic.test.js +175 -1
- package/src/utils.js +21 -0
package/package.json
CHANGED
package/src/git-logic.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { detectCIProvider, getPrUrl } from './utils.js';
|
|
3
|
+
import { detectCIProvider, getPrUrl, getSourceBranchFromCI } from './utils.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Truncate content to a maximum number of lines using "head and tail".
|
|
@@ -319,7 +319,16 @@ export async function runLocalReview(targetBranch = null, ignorePatterns = null)
|
|
|
319
319
|
// 1. Get Repo URL, current branch name, and repository root
|
|
320
320
|
const { stdout: repoUrl } = await execa('git', ['remote', 'get-url', 'origin']);
|
|
321
321
|
const { stdout: sourceBranch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
322
|
-
|
|
322
|
+
let branchName = sourceBranch.trim();
|
|
323
|
+
|
|
324
|
+
// Handle detached HEAD state (common in CI pipelines)
|
|
325
|
+
if (branchName === 'HEAD') {
|
|
326
|
+
const ciBranch = getSourceBranchFromCI();
|
|
327
|
+
if (ciBranch) {
|
|
328
|
+
branchName = ciBranch;
|
|
329
|
+
console.error(chalk.gray(`Detected source branch from CI: ${branchName}`));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
323
332
|
|
|
324
333
|
// Get the repository root directory - we'll run all git commands from there
|
|
325
334
|
const { stdout: repoRoot } = await execa('git', ['rev-parse', '--show-toplevel']);
|
package/src/git-logic.test.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
getContributors,
|
|
10
10
|
} from './git-logic.js';
|
|
11
11
|
import { execa } from 'execa';
|
|
12
|
-
import { detectCIProvider } from './utils.js';
|
|
12
|
+
import { detectCIProvider, getSourceBranchFromCI } from './utils.js';
|
|
13
13
|
|
|
14
14
|
describe('parseNameStatus', () => {
|
|
15
15
|
it('should correctly parse M, A, and D statuses', () => {
|
|
@@ -838,6 +838,7 @@ describe('is_ci flag in payload', () => {
|
|
|
838
838
|
vi.mock('./utils.js', () => ({
|
|
839
839
|
detectCIProvider: vi.fn(),
|
|
840
840
|
getPrUrl: vi.fn().mockReturnValue(null),
|
|
841
|
+
getSourceBranchFromCI: vi.fn().mockReturnValue(null),
|
|
841
842
|
}));
|
|
842
843
|
});
|
|
843
844
|
|
|
@@ -1011,3 +1012,176 @@ describe('is_ci flag in payload', () => {
|
|
|
1011
1012
|
expect(result.is_ci).toBe(false);
|
|
1012
1013
|
});
|
|
1013
1014
|
});
|
|
1015
|
+
|
|
1016
|
+
describe('getSourceBranchFromCI', () => {
|
|
1017
|
+
const originalEnv = process.env;
|
|
1018
|
+
|
|
1019
|
+
beforeEach(() => {
|
|
1020
|
+
vi.doUnmock('./utils.js');
|
|
1021
|
+
process.env = { ...originalEnv };
|
|
1022
|
+
// Clear any CI-related env vars
|
|
1023
|
+
delete process.env.SYSTEM_PULLREQUEST_SOURCEBRANCH;
|
|
1024
|
+
delete process.env.GITHUB_HEAD_REF;
|
|
1025
|
+
delete process.env.BITBUCKET_BRANCH;
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
afterEach(() => {
|
|
1029
|
+
process.env = originalEnv;
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
it('should return null when not in CI environment', async () => {
|
|
1033
|
+
const { getSourceBranchFromCI: fn } = await import('./utils.js');
|
|
1034
|
+
expect(fn()).toBeNull();
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
it('should extract branch name from Azure DevOps with refs/heads/ prefix', async () => {
|
|
1038
|
+
process.env.SYSTEM_PULLREQUEST_SOURCEBRANCH = 'refs/heads/feature/my-branch';
|
|
1039
|
+
const { getSourceBranchFromCI: fn } = await import('./utils.js');
|
|
1040
|
+
expect(fn()).toBe('feature/my-branch');
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
it('should handle Azure DevOps branch without refs/heads/ prefix', async () => {
|
|
1044
|
+
process.env.SYSTEM_PULLREQUEST_SOURCEBRANCH = 'feature/my-branch';
|
|
1045
|
+
const { getSourceBranchFromCI: fn } = await import('./utils.js');
|
|
1046
|
+
expect(fn()).toBe('feature/my-branch');
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it('should return GitHub head ref directly', async () => {
|
|
1050
|
+
process.env.GITHUB_HEAD_REF = 'feature/github-branch';
|
|
1051
|
+
const { getSourceBranchFromCI: fn } = await import('./utils.js');
|
|
1052
|
+
expect(fn()).toBe('feature/github-branch');
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it('should return Bitbucket branch directly', async () => {
|
|
1056
|
+
process.env.BITBUCKET_BRANCH = 'feature/bitbucket-branch';
|
|
1057
|
+
const { getSourceBranchFromCI: fn } = await import('./utils.js');
|
|
1058
|
+
expect(fn()).toBe('feature/bitbucket-branch');
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
it('should prefer Azure DevOps over GitHub when both are set', async () => {
|
|
1062
|
+
process.env.SYSTEM_PULLREQUEST_SOURCEBRANCH = 'refs/heads/azure-branch';
|
|
1063
|
+
process.env.GITHUB_HEAD_REF = 'github-branch';
|
|
1064
|
+
const { getSourceBranchFromCI: fn } = await import('./utils.js');
|
|
1065
|
+
expect(fn()).toBe('azure-branch');
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
describe('runLocalReview - detached HEAD handling', () => {
|
|
1070
|
+
beforeEach(() => {
|
|
1071
|
+
vi.mock('execa');
|
|
1072
|
+
vi.mock('./utils.js', () => ({
|
|
1073
|
+
detectCIProvider: vi.fn().mockReturnValue(null),
|
|
1074
|
+
getPrUrl: vi.fn().mockReturnValue(null),
|
|
1075
|
+
getSourceBranchFromCI: vi.fn(),
|
|
1076
|
+
}));
|
|
1077
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
afterEach(() => {
|
|
1081
|
+
vi.restoreAllMocks();
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
it('should use CI environment variable when git returns HEAD (detached HEAD state)', async () => {
|
|
1085
|
+
// Use the statically imported getSourceBranchFromCI (line 12)
|
|
1086
|
+
// which is automatically mocked by vi.mock() in beforeEach
|
|
1087
|
+
vi.mocked(getSourceBranchFromCI).mockReturnValue('feature/ci-branch');
|
|
1088
|
+
|
|
1089
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
1090
|
+
const command = [cmd, ...args].join(' ');
|
|
1091
|
+
|
|
1092
|
+
if (command.includes('remote get-url origin')) {
|
|
1093
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
1094
|
+
}
|
|
1095
|
+
// Simulate detached HEAD state
|
|
1096
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
1097
|
+
return { stdout: 'HEAD' };
|
|
1098
|
+
}
|
|
1099
|
+
if (command.includes('rev-parse --show-toplevel')) {
|
|
1100
|
+
return { stdout: '/path/to/repo' };
|
|
1101
|
+
}
|
|
1102
|
+
if (command.includes('rev-parse --verify main')) {
|
|
1103
|
+
return { stdout: 'commit-hash' };
|
|
1104
|
+
}
|
|
1105
|
+
if (command === 'git fetch origin main') {
|
|
1106
|
+
return { stdout: '' };
|
|
1107
|
+
}
|
|
1108
|
+
if (command.includes('merge-base origin/main HEAD')) {
|
|
1109
|
+
return { stdout: 'abc123' };
|
|
1110
|
+
}
|
|
1111
|
+
if (command.includes('log --no-merges --pretty=%B---EOC---')) {
|
|
1112
|
+
return { stdout: 'feat: add feature---EOC---' };
|
|
1113
|
+
}
|
|
1114
|
+
if (command.includes('log --no-merges --format=%ae|%an')) {
|
|
1115
|
+
return { stdout: 'user@example.com|User Name' };
|
|
1116
|
+
}
|
|
1117
|
+
if (command.includes('diff --name-status')) {
|
|
1118
|
+
return { stdout: 'M\tfile.js' };
|
|
1119
|
+
}
|
|
1120
|
+
if (command.includes('diff -U15')) {
|
|
1121
|
+
return { stdout: 'diff content' };
|
|
1122
|
+
}
|
|
1123
|
+
if (command.includes('show abc123:file.js')) {
|
|
1124
|
+
return { stdout: 'original content' };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return { stdout: '' };
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
const result = await runLocalReview('main');
|
|
1131
|
+
|
|
1132
|
+
expect(result).toBeDefined();
|
|
1133
|
+
expect(result.source_branch).toBe('feature/ci-branch');
|
|
1134
|
+
expect(getSourceBranchFromCI).toHaveBeenCalled();
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
it('should keep HEAD as source_branch when in detached HEAD but no CI env var', async () => {
|
|
1138
|
+
// getSourceBranchFromCI returns null by default (set in beforeEach mock)
|
|
1139
|
+
// so no need to explicitly set it here
|
|
1140
|
+
|
|
1141
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
1142
|
+
const command = [cmd, ...args].join(' ');
|
|
1143
|
+
|
|
1144
|
+
if (command.includes('remote get-url origin')) {
|
|
1145
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
1146
|
+
}
|
|
1147
|
+
// Simulate detached HEAD state
|
|
1148
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
1149
|
+
return { stdout: 'HEAD' };
|
|
1150
|
+
}
|
|
1151
|
+
if (command.includes('rev-parse --show-toplevel')) {
|
|
1152
|
+
return { stdout: '/path/to/repo' };
|
|
1153
|
+
}
|
|
1154
|
+
if (command.includes('rev-parse --verify main')) {
|
|
1155
|
+
return { stdout: 'commit-hash' };
|
|
1156
|
+
}
|
|
1157
|
+
if (command === 'git fetch origin main') {
|
|
1158
|
+
return { stdout: '' };
|
|
1159
|
+
}
|
|
1160
|
+
if (command.includes('merge-base origin/main HEAD')) {
|
|
1161
|
+
return { stdout: 'abc123' };
|
|
1162
|
+
}
|
|
1163
|
+
if (command.includes('log --no-merges --pretty=%B---EOC---')) {
|
|
1164
|
+
return { stdout: 'feat: add feature---EOC---' };
|
|
1165
|
+
}
|
|
1166
|
+
if (command.includes('log --no-merges --format=%ae|%an')) {
|
|
1167
|
+
return { stdout: 'user@example.com|User Name' };
|
|
1168
|
+
}
|
|
1169
|
+
if (command.includes('diff --name-status')) {
|
|
1170
|
+
return { stdout: 'M\tfile.js' };
|
|
1171
|
+
}
|
|
1172
|
+
if (command.includes('diff -U15')) {
|
|
1173
|
+
return { stdout: 'diff content' };
|
|
1174
|
+
}
|
|
1175
|
+
if (command.includes('show abc123:file.js')) {
|
|
1176
|
+
return { stdout: 'original content' };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return { stdout: '' };
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
const result = await runLocalReview('main');
|
|
1183
|
+
|
|
1184
|
+
expect(result).toBeDefined();
|
|
1185
|
+
expect(result.source_branch).toBe('HEAD');
|
|
1186
|
+
});
|
|
1187
|
+
});
|
package/src/utils.js
CHANGED
|
@@ -15,6 +15,27 @@ export function detectCIProvider() {
|
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Get source branch name from CI environment variables
|
|
20
|
+
* Useful when git is in detached HEAD state (common in CI pipelines)
|
|
21
|
+
* @returns {string|null} Branch name or null if not in CI context
|
|
22
|
+
*/
|
|
23
|
+
export function getSourceBranchFromCI() {
|
|
24
|
+
// Azure DevOps: refs/heads/feature/my-branch -> feature/my-branch
|
|
25
|
+
if (process.env.SYSTEM_PULLREQUEST_SOURCEBRANCH) {
|
|
26
|
+
return process.env.SYSTEM_PULLREQUEST_SOURCEBRANCH.replace(/^refs\/heads\//, '');
|
|
27
|
+
}
|
|
28
|
+
// GitHub Actions: already just the branch name
|
|
29
|
+
if (process.env.GITHUB_HEAD_REF) {
|
|
30
|
+
return process.env.GITHUB_HEAD_REF;
|
|
31
|
+
}
|
|
32
|
+
// Bitbucket Pipelines: already just the branch name
|
|
33
|
+
if (process.env.BITBUCKET_BRANCH) {
|
|
34
|
+
return process.env.BITBUCKET_BRANCH;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
18
39
|
/**
|
|
19
40
|
* Build PR URL from CI environment variables
|
|
20
41
|
* @returns {string|null} Full PR URL or null if not in CI PR context
|