merge-steward 0.5.3 → 0.5.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.
@@ -1,5 +1,5 @@
1
1
  import type { GitOperations, SpeculativeBranchBuilder } from "../interfaces.ts";
2
- import type { MergeResult, RebaseResult } from "../types.ts";
2
+ import type { MergeResult } from "../types.ts";
3
3
  export declare class ShellGitOperations implements GitOperations, SpeculativeBranchBuilder {
4
4
  private readonly clonePath;
5
5
  private readonly repoFullName;
@@ -9,7 +9,7 @@ export declare class ShellGitOperations implements GitOperations, SpeculativeBra
9
9
  fetch(remote?: string): Promise<void>;
10
10
  headSha(branch: string): Promise<string>;
11
11
  isAncestor(ancestor: string, descendant: string): Promise<boolean>;
12
- rebase(branch: string, onto: string): Promise<RebaseResult>;
12
+ mergeBaseInto(branch: string, base: string): Promise<MergeResult>;
13
13
  push(branch: string, force?: boolean): Promise<void>;
14
14
  buildSpeculative(prBranch: string, baseBranch: string, specName: string): Promise<MergeResult>;
15
15
  deleteSpeculative(specName: string): Promise<void>;
@@ -41,24 +41,22 @@ export class ShellGitOperations {
41
41
  return false;
42
42
  throw new Error(`git merge-base --is-ancestor failed: ${result.stderr || result.stdout}`);
43
43
  }
44
- async rebase(branch, onto) {
44
+ async mergeBaseInto(branch, base) {
45
45
  const remoteBranchRef = `refs/remotes/origin/${branch}`;
46
46
  const remoteBranchExists = await this.git(["show-ref", "--verify", remoteBranchRef], { allowNonZero: true });
47
47
  if (remoteBranchExists.exitCode === 0) {
48
- // Always start from the freshly fetched remote branch tip so stale local branches
49
- // cannot reintroduce old commits into queue processing.
50
48
  await this.git(["checkout", "-B", branch, `origin/${branch}`]);
51
49
  }
52
50
  else {
53
51
  await this.git(["checkout", branch]);
54
52
  }
55
- const result = await this.git(["rebase", onto], { allowNonZero: true });
53
+ const result = await this.git(["merge", "--no-ff", "--no-edit", base], { allowNonZero: true });
56
54
  if (result.exitCode !== 0) {
57
- await this.git(["rebase", "--abort"], { allowNonZero: true });
55
+ await this.git(["merge", "--abort"], { allowNonZero: true });
58
56
  return { success: false, conflictFiles: parseConflicts(result.stderr) };
59
57
  }
60
58
  const newSha = await this.headSha("HEAD");
61
- return { success: true, newHeadSha: newSha };
59
+ return { success: true, sha: newSha };
62
60
  }
63
61
  async push(branch, force = false) {
64
62
  const args = ["push"];
@@ -1,4 +1,4 @@
1
- import type { CIStatus, CheckResult, IncidentRecord, MergeResult, PRStatus, QueueEntry, RebaseResult } from "./types.ts";
1
+ import type { CIStatus, CheckResult, IncidentRecord, MergeResult, PRStatus, QueueEntry } from "./types.ts";
2
2
  /**
3
3
  * Git operations needed by the reconciler. The sim (GitSim) implements
4
4
  * additional methods for test harness setup, but the reconciler only uses these.
@@ -7,7 +7,7 @@ export interface GitOperations {
7
7
  fetch(remote?: string): Promise<void>;
8
8
  headSha(branch: string): Promise<string>;
9
9
  isAncestor(ancestor: string, descendant: string): Promise<boolean>;
10
- rebase(branch: string, onto: string): Promise<RebaseResult>;
10
+ mergeBaseInto(branch: string, base: string): Promise<MergeResult>;
11
11
  push(branch: string, force?: boolean): Promise<void>;
12
12
  }
13
13
  /**
@@ -71,7 +71,7 @@ export async function reconcile(ctx) {
71
71
  }
72
72
  }
73
73
  }
74
- // ─── Head entry: fetch + gate + rebase ──────────────────────────
74
+ // ─── Head entry: fetch + gate + branch refresh ──────────────────
75
75
  async function prepareHead(ctx, entry) {
76
76
  emit(ctx, entry, "fetch_started");
77
77
  await ctx.git.fetch();
@@ -116,7 +116,7 @@ async function prepareHead(ctx, entry) {
116
116
  emit(ctx, entry, "retry_gated", { baseSha, detail: "base unchanged since last conflict" });
117
117
  return;
118
118
  }
119
- await performRebase(ctx, entry, baseSha);
119
+ await performBranchRefresh(ctx, entry, baseSha);
120
120
  }
121
121
  function describeMainBroken(failingChecks, pendingChecks) {
122
122
  const parts = [];
@@ -135,9 +135,9 @@ function summarizeCheckNames(checks, limit = 3) {
135
135
  }
136
136
  return `${names.slice(0, limit).join(", ")} +${names.length - limit} more`;
137
137
  }
138
- async function performRebase(ctx, entry, baseSha) {
138
+ async function performBranchRefresh(ctx, entry, baseSha) {
139
139
  emit(ctx, entry, "rebase_started", { baseSha });
140
- const result = await ctx.git.rebase(entry.branch, ref(ctx, ctx.baseBranch));
140
+ const result = await ctx.git.mergeBaseInto(entry.branch, ref(ctx, ctx.baseBranch));
141
141
  if (!result.success) {
142
142
  emit(ctx, entry, "rebase_conflict", { baseSha, conflictFiles: result.conflictFiles });
143
143
  if (isBudgetExhausted(entry)) {
@@ -153,28 +153,17 @@ async function performRebase(ctx, entry, baseSha) {
153
153
  }
154
154
  return;
155
155
  }
156
- const headSha = result.newHeadSha ?? entry.headSha;
156
+ const headSha = result.sha ?? entry.headSha;
157
157
  await ctx.git.fetch();
158
158
  const latestRemoteHead = await ctx.git.headSha(ref(ctx, entry.branch));
159
- const candidateKeepsLatestRemote = await ctx.git.isAncestor(latestRemoteHead, headSha);
160
- if (!candidateKeepsLatestRemote) {
161
- const detail = latestRemoteHead === entry.headSha
162
- ? `candidate diverged from remote head: expected ${entry.headSha.slice(0, 8)}, ` +
163
- `latest ${latestRemoteHead.slice(0, 8)}, candidate ${headSha.slice(0, 8)}`
164
- : `remote advanced during rebase: expected ${entry.headSha.slice(0, 8)}, ` +
165
- `latest ${latestRemoteHead.slice(0, 8)}, candidate ${headSha.slice(0, 8)}`;
159
+ if (latestRemoteHead !== entry.headSha) {
160
+ const detail = `remote advanced during refresh: expected ${entry.headSha.slice(0, 8)}, ` +
161
+ `latest ${latestRemoteHead.slice(0, 8)}, candidate ${headSha.slice(0, 8)}`;
166
162
  emit(ctx, entry, "branch_mismatch", { detail });
167
- if (latestRemoteHead !== entry.headSha) {
168
- ctx.store.updateHead(entry.id, latestRemoteHead);
169
- }
170
- else {
171
- ctx.store.transition(entry.id, "queued", {
172
- ...CLEAN_CI,
173
- }, `stale local branch diverged from ${latestRemoteHead.slice(0, 8)}`);
174
- }
163
+ ctx.store.updateHead(entry.id, latestRemoteHead);
175
164
  return;
176
165
  }
177
- await ctx.git.push(entry.branch, true);
166
+ await ctx.git.push(entry.branch, false);
178
167
  emit(ctx, entry, "rebase_succeeded", { baseSha });
179
168
  // Build speculative branch for downstream entries.
180
169
  let specBranch = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "merge-steward",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Serial merge queue for GitHub — rebase, CI-gate, and merge PRs one at a time",
5
5
  "type": "module",
6
6
  "repository": {