edsger 0.52.0 → 0.53.0
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.
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execFileSync
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
2
|
import { getGitHubConfig } from '../../api/github.js';
|
|
3
3
|
import { getIssue } from '../../api/issues/index.js';
|
|
4
4
|
import { getPullRequests, } from '../../services/pull-requests.js';
|
|
5
5
|
import { branchExists, remoteBranchExists, } from '../../utils/git-branch-manager.js';
|
|
6
6
|
import { buildCredentialArgs } from '../../utils/git-push.js';
|
|
7
|
-
import { getRepoForkInfo, } from '../../utils/github-repo-info.js';
|
|
7
|
+
import { getRepoForkInfo, resolveDefaultBranch, } from '../../utils/github-repo-info.js';
|
|
8
8
|
import { logError, logInfo } from '../../utils/logger.js';
|
|
9
9
|
/**
|
|
10
10
|
* Get the dev branch name for an issue
|
|
@@ -16,14 +16,16 @@ function getDevBranchName(issueId) {
|
|
|
16
16
|
* Get the HEAD SHA of a branch
|
|
17
17
|
*/
|
|
18
18
|
function getBranchHeadSha(branchName) {
|
|
19
|
-
return
|
|
19
|
+
return execFileSync('git', ['rev-parse', branchName], {
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
}).trim();
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Get diff stat between two refs
|
|
23
25
|
*/
|
|
24
26
|
function getDiffStat(baseRef, headRef) {
|
|
25
27
|
try {
|
|
26
|
-
return
|
|
28
|
+
return execFileSync('git', ['diff', '--stat', `${baseRef}...${headRef}`], {
|
|
27
29
|
encoding: 'utf-8',
|
|
28
30
|
}).trim();
|
|
29
31
|
}
|
|
@@ -51,9 +53,7 @@ function parseGitStatus(status) {
|
|
|
51
53
|
*/
|
|
52
54
|
function getChangedFiles(baseRef, headRef) {
|
|
53
55
|
try {
|
|
54
|
-
const output =
|
|
55
|
-
encoding: 'utf-8',
|
|
56
|
-
}).trim();
|
|
56
|
+
const output = execFileSync('git', ['diff', '--name-status', `${baseRef}...${headRef}`], { encoding: 'utf-8' }).trim();
|
|
57
57
|
if (!output) {
|
|
58
58
|
return [];
|
|
59
59
|
}
|
|
@@ -77,7 +77,7 @@ function getChangedFiles(baseRef, headRef) {
|
|
|
77
77
|
*/
|
|
78
78
|
function isAncestor(commitSha, ref) {
|
|
79
79
|
try {
|
|
80
|
-
|
|
80
|
+
execFileSync('git', ['merge-base', '--is-ancestor', commitSha, ref], {
|
|
81
81
|
encoding: 'utf-8',
|
|
82
82
|
stdio: 'pipe',
|
|
83
83
|
});
|
|
@@ -121,25 +121,50 @@ export async function fetchPRExecutionContext(issueId, verbose) {
|
|
|
121
121
|
getPullRequests({ issueId, verbose }),
|
|
122
122
|
getGitHubConfig(issueId, verbose),
|
|
123
123
|
]);
|
|
124
|
-
//
|
|
124
|
+
// Detect fork status + default branch (used by the fast-forward step below)
|
|
125
|
+
let forkInfo = { isFork: false };
|
|
126
|
+
if (githubConfig.token && githubConfig.owner && githubConfig.repo) {
|
|
127
|
+
try {
|
|
128
|
+
forkInfo = await getRepoForkInfo(githubConfig.token, githubConfig.owner, githubConfig.repo);
|
|
129
|
+
if (verbose) {
|
|
130
|
+
logInfo(forkInfo.isFork
|
|
131
|
+
? `📌 Repository is a fork of ${forkInfo.upstream?.owner}/${forkInfo.upstream?.repo}`
|
|
132
|
+
: `📌 Repository is not a fork`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (verbose) {
|
|
137
|
+
logError(`Failed to detect fork status: ${error instanceof Error ? error.message : String(error)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Resolve the repo's default branch (e.g. main, master, develop). Falls back
|
|
142
|
+
// through GitHub API → local symbolic-ref → literal 'main'.
|
|
143
|
+
const defaultBranch = resolveDefaultBranch(forkInfo.defaultBranch);
|
|
144
|
+
// Fetch latest remote refs and fast-forward the local default branch.
|
|
145
|
+
// Use execFileSync (not execSync) so defaultBranch — which can flow in from
|
|
146
|
+
// the GitHub API's default_branch field — is never passed through a shell.
|
|
125
147
|
try {
|
|
126
148
|
const credArgs = buildCredentialArgs(githubConfig.token);
|
|
127
149
|
execFileSync('git', [...credArgs, 'fetch', 'origin'], {
|
|
128
150
|
encoding: 'utf-8',
|
|
129
151
|
stdio: 'pipe',
|
|
130
152
|
});
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
execFileSync('git', ['checkout', defaultBranch], {
|
|
154
|
+
encoding: 'utf-8',
|
|
155
|
+
stdio: 'pipe',
|
|
156
|
+
});
|
|
157
|
+
execFileSync('git', ['merge', '--ff-only', `origin/${defaultBranch}`], {
|
|
133
158
|
encoding: 'utf-8',
|
|
134
159
|
stdio: 'pipe',
|
|
135
160
|
});
|
|
136
161
|
if (verbose) {
|
|
137
|
-
logInfo(
|
|
162
|
+
logInfo(`✅ Local ${defaultBranch} synced with origin/${defaultBranch}`);
|
|
138
163
|
}
|
|
139
164
|
}
|
|
140
165
|
catch (error) {
|
|
141
166
|
if (verbose) {
|
|
142
|
-
logInfo(`⚠️ Could not sync
|
|
167
|
+
logInfo(`⚠️ Could not sync ${defaultBranch} with origin: ${error instanceof Error ? error.message : String(error)}`);
|
|
143
168
|
}
|
|
144
169
|
}
|
|
145
170
|
// Verify dev branch exists
|
|
@@ -168,23 +193,6 @@ export async function fetchPRExecutionContext(issueId, verbose) {
|
|
|
168
193
|
if (!githubConfig.configured) {
|
|
169
194
|
throw new Error(`GitHub is not configured. ${githubConfig.message || 'Please configure GitHub integration.'}`);
|
|
170
195
|
}
|
|
171
|
-
// Detect fork status
|
|
172
|
-
let forkInfo = { isFork: false };
|
|
173
|
-
if (githubConfig.token && githubConfig.owner && githubConfig.repo) {
|
|
174
|
-
try {
|
|
175
|
-
forkInfo = await getRepoForkInfo(githubConfig.token, githubConfig.owner, githubConfig.repo);
|
|
176
|
-
if (verbose) {
|
|
177
|
-
logInfo(forkInfo.isFork
|
|
178
|
-
? `📌 Repository is a fork of ${forkInfo.upstream?.owner}/${forkInfo.upstream?.repo}`
|
|
179
|
-
: `📌 Repository is not a fork`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
if (verbose) {
|
|
184
|
-
logError(`Failed to detect fork status: ${error instanceof Error ? error.message : String(error)}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
196
|
// Determine sync mode
|
|
189
197
|
const devRef = localExists ? devBranchName : `origin/${devBranchName}`;
|
|
190
198
|
const devBranchHeadSha = getBranchHeadSha(devRef);
|
|
@@ -202,8 +210,8 @@ export async function fetchPRExecutionContext(issueId, verbose) {
|
|
|
202
210
|
lastSyncedCommit = null;
|
|
203
211
|
isIncrementalSync = false;
|
|
204
212
|
}
|
|
205
|
-
// Get diff info: for incremental, diff from last sync; for first run, diff from
|
|
206
|
-
const diffBase = isIncrementalSync && lastSyncedCommit ? lastSyncedCommit :
|
|
213
|
+
// Get diff info: for incremental, diff from last sync; for first run, diff from default branch
|
|
214
|
+
const diffBase = isIncrementalSync && lastSyncedCommit ? lastSyncedCommit : defaultBranch;
|
|
207
215
|
const diffStat = getDiffStat(diffBase, devRef);
|
|
208
216
|
const changedFiles = getChangedFiles(diffBase, devRef);
|
|
209
217
|
if (verbose) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFileSync
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
2
|
import { getGitHubConfig } from '../../api/github.js';
|
|
3
3
|
import { getIssue } from '../../api/issues/index.js';
|
|
4
4
|
import { getProduct } from '../../api/products.js';
|
|
@@ -6,7 +6,7 @@ import { getBranches } from '../../services/branches.js';
|
|
|
6
6
|
import { getPullRequests, } from '../../services/pull-requests.js';
|
|
7
7
|
import { branchExists, remoteBranchExists, } from '../../utils/git-branch-manager.js';
|
|
8
8
|
import { buildCredentialArgs } from '../../utils/git-push.js';
|
|
9
|
-
import { getRepoForkInfo, } from '../../utils/github-repo-info.js';
|
|
9
|
+
import { getRepoForkInfo, resolveDefaultBranch, } from '../../utils/github-repo-info.js';
|
|
10
10
|
import { logError, logInfo } from '../../utils/logger.js';
|
|
11
11
|
/**
|
|
12
12
|
* Get the dev branch name for an issue
|
|
@@ -18,14 +18,16 @@ function getDevBranchName(issueId) {
|
|
|
18
18
|
* Get the HEAD SHA of a branch
|
|
19
19
|
*/
|
|
20
20
|
function getBranchHeadSha(branchName) {
|
|
21
|
-
return
|
|
21
|
+
return execFileSync('git', ['rev-parse', branchName], {
|
|
22
|
+
encoding: 'utf-8',
|
|
23
|
+
}).trim();
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* Get diff stat between two refs
|
|
25
27
|
*/
|
|
26
28
|
function getDiffStat(baseRef, headRef) {
|
|
27
29
|
try {
|
|
28
|
-
return
|
|
30
|
+
return execFileSync('git', ['diff', '--stat', `${baseRef}...${headRef}`], {
|
|
29
31
|
encoding: 'utf-8',
|
|
30
32
|
}).trim();
|
|
31
33
|
}
|
|
@@ -38,9 +40,7 @@ function getDiffStat(baseRef, headRef) {
|
|
|
38
40
|
*/
|
|
39
41
|
function getChangedFiles(baseRef, headRef) {
|
|
40
42
|
try {
|
|
41
|
-
const output =
|
|
42
|
-
encoding: 'utf-8',
|
|
43
|
-
}).trim();
|
|
43
|
+
const output = execFileSync('git', ['diff', '--name-only', `${baseRef}...${headRef}`], { encoding: 'utf-8' }).trim();
|
|
44
44
|
if (!output) {
|
|
45
45
|
return [];
|
|
46
46
|
}
|
|
@@ -53,18 +53,18 @@ function getChangedFiles(baseRef, headRef) {
|
|
|
53
53
|
/**
|
|
54
54
|
* Determine the diff base ref for incremental re-runs
|
|
55
55
|
* If existing PRs have last_synced_commit, use the earliest one
|
|
56
|
-
* Otherwise use origin
|
|
56
|
+
* Otherwise use origin/<defaultBranch> (remote-tracking ref, always up-to-date after fetch)
|
|
57
57
|
*/
|
|
58
|
-
function determineDiffBaseRef(existingPRs, replaceExisting) {
|
|
58
|
+
function determineDiffBaseRef(existingPRs, defaultBranchRef, replaceExisting) {
|
|
59
59
|
if (replaceExisting || existingPRs.length === 0) {
|
|
60
|
-
return
|
|
60
|
+
return defaultBranchRef;
|
|
61
61
|
}
|
|
62
62
|
// Find the minimum last_synced_commit (earliest sync point)
|
|
63
63
|
const syncedCommits = existingPRs
|
|
64
64
|
.map((pr) => pr.last_synced_commit)
|
|
65
65
|
.filter((c) => c !== null);
|
|
66
66
|
if (syncedCommits.length === 0) {
|
|
67
|
-
return
|
|
67
|
+
return defaultBranchRef;
|
|
68
68
|
}
|
|
69
69
|
// All PRs should have been synced to the same commit
|
|
70
70
|
// Use the first one (they should all be equal after a successful sync)
|
|
@@ -131,12 +131,17 @@ export async function fetchPRSplittingContext(issueId, verbose, replaceExisting)
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
// Resolve the repo's default branch (handles non-main defaults and forks).
|
|
135
|
+
// Prefer the GitHub API value (already fetched in forkInfo above); fall back
|
|
136
|
+
// to the local symbolic-ref, then to literal 'main'.
|
|
137
|
+
const defaultBranch = resolveDefaultBranch(forkInfo.defaultBranch);
|
|
138
|
+
const defaultBranchRef = `origin/${defaultBranch}`;
|
|
134
139
|
// Determine diff range
|
|
135
140
|
const devRef = localExists ? devBranchName : `origin/${devBranchName}`;
|
|
136
|
-
const baseRef = determineDiffBaseRef(existingPullRequests, replaceExisting);
|
|
141
|
+
const baseRef = determineDiffBaseRef(existingPullRequests, defaultBranchRef, replaceExisting);
|
|
137
142
|
const devBranchHeadSha = getBranchHeadSha(devRef);
|
|
138
143
|
// Check if there are new changes since last sync
|
|
139
|
-
if (baseRef !==
|
|
144
|
+
if (baseRef !== defaultBranchRef && baseRef === devBranchHeadSha) {
|
|
140
145
|
if (verbose) {
|
|
141
146
|
logInfo(`No new changes since last sync (HEAD: ${devBranchHeadSha})`);
|
|
142
147
|
}
|
|
@@ -7,8 +7,20 @@ export interface RepoForkInfo {
|
|
|
7
7
|
owner: string;
|
|
8
8
|
repo: string;
|
|
9
9
|
};
|
|
10
|
+
defaultBranch?: string;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
|
-
* Detect if a repository is a fork and get upstream info
|
|
13
|
+
* Detect if a repository is a fork and get upstream + default branch info
|
|
13
14
|
*/
|
|
14
15
|
export declare function getRepoForkInfo(token: string, owner: string, repo: string): Promise<RepoForkInfo>;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the repo's default branch with a multi-tier fallback:
|
|
18
|
+
* 1. `apiValue` from GitHub API (most authoritative)
|
|
19
|
+
* 2. local `git symbolic-ref refs/remotes/origin/HEAD`
|
|
20
|
+
* 3. literal `'main'`
|
|
21
|
+
*
|
|
22
|
+
* Use this anywhere we need to diff/merge against the upstream default branch.
|
|
23
|
+
* The hardcoded `origin/main` assumption breaks for repos whose default is
|
|
24
|
+
* `master`, `develop`, etc., or for forks that inherited a non-`main` default.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveDefaultBranch(apiValue?: string): string;
|
|
@@ -2,18 +2,44 @@
|
|
|
2
2
|
* GitHub repository information utilities
|
|
3
3
|
*/
|
|
4
4
|
import { Octokit } from '@octokit/rest';
|
|
5
|
+
import { execFileSync } from 'child_process';
|
|
5
6
|
/**
|
|
6
|
-
* Detect if a repository is a fork and get upstream info
|
|
7
|
+
* Detect if a repository is a fork and get upstream + default branch info
|
|
7
8
|
*/
|
|
8
9
|
export async function getRepoForkInfo(token, owner, repo) {
|
|
9
10
|
const octokit = new Octokit({ auth: token });
|
|
10
11
|
const { data } = await octokit.repos.get({ owner, repo });
|
|
12
|
+
const result = {
|
|
13
|
+
isFork: false,
|
|
14
|
+
defaultBranch: data.default_branch,
|
|
15
|
+
};
|
|
11
16
|
if (data.fork && data.parent) {
|
|
12
17
|
const [upstreamOwner, upstreamRepo] = data.parent.full_name.split('/');
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
result.isFork = true;
|
|
19
|
+
result.upstream = { owner: upstreamOwner, repo: upstreamRepo };
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the repo's default branch with a multi-tier fallback:
|
|
25
|
+
* 1. `apiValue` from GitHub API (most authoritative)
|
|
26
|
+
* 2. local `git symbolic-ref refs/remotes/origin/HEAD`
|
|
27
|
+
* 3. literal `'main'`
|
|
28
|
+
*
|
|
29
|
+
* Use this anywhere we need to diff/merge against the upstream default branch.
|
|
30
|
+
* The hardcoded `origin/main` assumption breaks for repos whose default is
|
|
31
|
+
* `master`, `develop`, etc., or for forks that inherited a non-`main` default.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveDefaultBranch(apiValue) {
|
|
34
|
+
if (apiValue) {
|
|
35
|
+
return apiValue;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const ref = execFileSync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
39
|
+
const stripped = ref.replace(/^origin\//, '');
|
|
40
|
+
return stripped || 'main';
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return 'main';
|
|
17
44
|
}
|
|
18
|
-
return { isFork: false };
|
|
19
45
|
}
|