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, execSync } from 'child_process';
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 execSync(`git rev-parse ${branchName}`, { encoding: 'utf-8' }).trim();
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 execSync(`git diff --stat ${baseRef}...${headRef}`, {
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 = execSync(`git diff --name-status ${baseRef}...${headRef}`, {
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
- execSync(`git merge-base --is-ancestor ${commitSha} ${ref}`, {
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
- // Fetch latest remote refs and fast-forward local main
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
- execSync('git checkout main', { encoding: 'utf-8', stdio: 'pipe' });
132
- execSync('git merge --ff-only origin/main', {
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('✅ Local main synced with origin/main');
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 main with origin: ${error instanceof Error ? error.message : String(error)}`);
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 main
206
- const diffBase = isIncrementalSync && lastSyncedCommit ? lastSyncedCommit : 'main';
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, execSync } from 'child_process';
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 execSync(`git rev-parse ${branchName}`, { encoding: 'utf-8' }).trim();
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 execSync(`git diff --stat ${baseRef}...${headRef}`, {
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 = execSync(`git diff --name-only ${baseRef}...${headRef}`, {
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/main (remote-tracking ref, always up-to-date after fetch)
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 'origin/main';
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 'origin/main';
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 !== 'origin/main' && baseRef === devBranchHeadSha) {
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
- return {
14
- isFork: true,
15
- upstream: { owner: upstreamOwner, repo: upstreamRepo },
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"