authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-01 13:35:57-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-01 20:06:01-08:00
logd0ab35931e90972d4217f319645a36601a1748bf
treedc2f3312e78e3d6013ea06f756da0a55ae2d0492
parent882684a80cb7a74beafc8291e4949e266544c9ec

fix: resolve no-upstream base from origin tracking branches

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 53 insertions(+), 3 deletions(-)

apps/server/src/git/Layers/GitCore.test.ts+26
...@@ -1161,6 +1161,32 @@ it.layer(TestLayer)("git integration", (it) => {...@@ -1161,6 +1161,32 @@ it.layer(TestLayer)("git integration", (it) => {
1161 }),1161 }),
1162 );1162 );
11631163
1164 it.effect(
1165 "computes ahead count against origin/default when local default branch is missing",
1166 () =>
1167 Effect.gen(function* () {
1168 const remote = yield* makeTmpDir();
1169 const source = yield* makeTmpDir();
1170 yield* git(remote, ["init", "--bare"]);
1171
1172 yield* initRepoWithCommit(source);
1173 yield* git(source, ["remote", "add", "origin", remote]);
1174 yield* git(source, ["push", "-u", "origin", "main"]);
1175 yield* git(source, ["checkout", "-b", "feature/remote-base-only"]);
1176 yield* writeTextFile(path.join(source, "feature.txt"), "ahead of origin/main\n");
1177 yield* git(source, ["add", "feature.txt"]);
1178 yield* git(source, ["commit", "-m", "feature commit"]);
1179 yield* git(source, ["branch", "-D", "main"]);
1180
1181 const core = yield* GitCore;
1182 const details = yield* core.statusDetails(source);
1183 expect(details.branch).toBe("feature/remote-base-only");
1184 expect(details.hasUpstream).toBe(false);
1185 expect(details.aheadCount).toBe(1);
1186 expect(details.behindCount).toBe(0);
1187 }),
1188 );
1189
1164 it.effect("skips push when no upstream is configured and branch is not ahead of base", () =>1190 it.effect("skips push when no upstream is configured and branch is not ahead of base", () =>
1165 Effect.gen(function* () {1191 Effect.gen(function* () {
1166 const tmp = yield* makeTmpDir();1192 const tmp = yield* makeTmpDir();
apps/server/src/git/Layers/GitCore.ts+27-3
...@@ -368,6 +368,19 @@ const makeGitCore = Effect.gen(function* () {...@@ -368,6 +368,19 @@ const makeGitCore = Effect.gen(function* () {
368 allowNonZeroExit: true,368 allowNonZeroExit: true,
369 }).pipe(Effect.map((result) => result.code === 0));369 }).pipe(Effect.map((result) => result.code === 0));
370370
371 const remoteBranchExists = (
372 cwd: string,
373 branch: string,
374 ): Effect.Effect<boolean, GitCommandError> =>
375 executeGit(
376 "GitCore.remoteBranchExists",
377 cwd,
378 ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`],
379 {
380 allowNonZeroExit: true,
381 },
382 ).pipe(Effect.map((result) => result.code === 0));
383
371 const resolveBaseBranchForNoUpstream = (384 const resolveBaseBranchForNoUpstream = (
372 cwd: string,385 cwd: string,
373 branch: string,386 branch: string,
...@@ -388,12 +401,23 @@ const makeGitCore = Effect.gen(function* () {...@@ -388,12 +401,23 @@ const makeGitCore = Effect.gen(function* () {
388 ];401 ];
389402
390 for (const candidate of candidates) {403 for (const candidate of candidates) {
391 if (!candidate || candidate === branch) {404 if (!candidate) {
405 continue;
406 }
407
408 const normalizedCandidate = candidate.startsWith("origin/")
409 ? candidate.slice("origin/".length)
410 : candidate;
411 if (normalizedCandidate.length === 0 || normalizedCandidate === branch) {
392 continue;412 continue;
393 }413 }
394414
395 if (yield* branchExists(cwd, candidate)) {415 if (yield* branchExists(cwd, normalizedCandidate)) {
396 return candidate;416 return normalizedCandidate;
417 }
418
419 if (yield* remoteBranchExists(cwd, normalizedCandidate)) {
420 return `origin/${normalizedCandidate}`;
397 }421 }
398 }422 }
399423