Skip to content

Commit 0ce7e56

Browse files
juliusmarmingeJulius Marminge
andauthored
feat(scm): Gitlab (#2462)
Co-authored-by: Julius Marminge <julius@macmini.local>
1 parent 350d76e commit 0ce7e56

24 files changed

Lines changed: 1606 additions & 185 deletions

apps/server/src/git/GitManager.test.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
881881
hasUpstream: false,
882882
aheadCount: 0,
883883
behindCount: 0,
884+
aheadOfDefaultCount: 0,
884885
pr: null,
885886
});
886887
}),
@@ -910,6 +911,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
910911
hasUpstream: false,
911912
aheadCount: 0,
912913
behindCount: 0,
914+
aheadOfDefaultCount: 0,
913915
pr: null,
914916
});
915917
}),
@@ -1672,6 +1674,41 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
16721674
}),
16731675
);
16741676

1677+
it.effect("pushes existing commits without committing dirty worktree changes", () =>
1678+
Effect.gen(function* () {
1679+
const repoDir = yield* makeTempDir("t3code-git-manager-");
1680+
yield* initRepo(repoDir);
1681+
yield* runGit(repoDir, ["checkout", "-b", "feature/push-dirty"]);
1682+
const remoteDir = yield* createBareRemote();
1683+
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
1684+
fs.writeFileSync(path.join(repoDir, "push-dirty.txt"), "push dirty\n");
1685+
yield* runGit(repoDir, ["add", "push-dirty.txt"]);
1686+
yield* runGit(repoDir, ["commit", "-m", "Push dirty branch"]);
1687+
fs.mkdirSync(path.join(repoDir, ".vercel"));
1688+
fs.writeFileSync(path.join(repoDir, ".vercel", "project.json"), "{}\n");
1689+
1690+
const { manager } = yield* makeManager();
1691+
const result = yield* runStackedAction(manager, {
1692+
cwd: repoDir,
1693+
action: "push",
1694+
});
1695+
1696+
expect(result.commit.status).toBe("skipped_not_requested");
1697+
expect(result.push.status).toBe("pushed");
1698+
expect(result.pr.status).toBe("skipped_not_requested");
1699+
expect(
1700+
yield* runGit(repoDir, ["status", "--porcelain"]).pipe(
1701+
Effect.map((output) => output.stdout.trim()),
1702+
),
1703+
).toContain("?? .vercel/");
1704+
expect(
1705+
yield* runGit(remoteDir, ["log", "-1", "--pretty=%s", "feature/push-dirty"]).pipe(
1706+
Effect.map((output) => output.stdout.trim()),
1707+
),
1708+
).toBe("Push dirty branch");
1709+
}),
1710+
);
1711+
16751712
it.effect("create_pr pushes a clean branch before creating the PR when needed", () =>
16761713
Effect.gen(function* () {
16771714
const repoDir = yield* makeTempDir("t3code-git-manager-");
@@ -2418,6 +2455,113 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
24182455
}),
24192456
);
24202457

2458+
it.effect(
2459+
"restores same-repository upstream tracking after local PR checkout without a remote ref",
2460+
() =>
2461+
Effect.gen(function* () {
2462+
const repoDir = yield* makeTempDir("t3code-git-manager-");
2463+
yield* initRepo(repoDir);
2464+
const remoteDir = yield* createBareRemote();
2465+
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
2466+
yield* runGit(repoDir, ["push", "-u", "origin", "main"]);
2467+
yield* runGit(repoDir, ["checkout", "-b", "feature/pr-local-upstream"]);
2468+
fs.writeFileSync(path.join(repoDir, "upstream.txt"), "upstream\n");
2469+
yield* runGit(repoDir, ["add", "upstream.txt"]);
2470+
yield* runGit(repoDir, ["commit", "-m", "Local upstream PR branch"]);
2471+
yield* runGit(repoDir, ["push", "-u", "origin", "feature/pr-local-upstream"]);
2472+
yield* runGit(repoDir, ["checkout", "main"]);
2473+
yield* runGit(repoDir, ["branch", "-D", "feature/pr-local-upstream"]);
2474+
yield* runGit(repoDir, [
2475+
"update-ref",
2476+
"-d",
2477+
"refs/remotes/origin/feature/pr-local-upstream",
2478+
]);
2479+
2480+
const { manager } = yield* makeManager({
2481+
ghScenario: {
2482+
pullRequest: {
2483+
number: 65,
2484+
title: "Local upstream PR",
2485+
url: "https://github.com/pingdotgg/codething-mvp/pull/65",
2486+
baseRefName: "main",
2487+
headRefName: "feature/pr-local-upstream",
2488+
state: "open",
2489+
isCrossRepository: false,
2490+
headRepositoryNameWithOwner: "pingdotgg/codething-mvp",
2491+
headRepositoryOwnerLogin: "pingdotgg",
2492+
},
2493+
repositoryCloneUrls: {
2494+
"pingdotgg/codething-mvp": {
2495+
url: remoteDir,
2496+
sshUrl: remoteDir,
2497+
},
2498+
},
2499+
},
2500+
});
2501+
2502+
const result = yield* preparePullRequestThread(manager, {
2503+
cwd: repoDir,
2504+
reference: "65",
2505+
mode: "local",
2506+
});
2507+
2508+
expect(result.worktreePath).toBeNull();
2509+
expect(result.branch).toBe("feature/pr-local-upstream");
2510+
expect(
2511+
(yield* runGit(repoDir, ["rev-parse", "--abbrev-ref", "@{upstream}"])).stdout.trim(),
2512+
).toBe("origin/feature/pr-local-upstream");
2513+
}),
2514+
);
2515+
2516+
it.effect(
2517+
"restores same-repository upstream tracking when provider omits head repository metadata",
2518+
() =>
2519+
Effect.gen(function* () {
2520+
const repoDir = yield* makeTempDir("t3code-git-manager-");
2521+
yield* initRepo(repoDir);
2522+
const remoteDir = yield* createBareRemote();
2523+
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
2524+
yield* runGit(repoDir, ["push", "-u", "origin", "main"]);
2525+
yield* runGit(repoDir, ["checkout", "-b", "feature/pr-local-no-head-repo"]);
2526+
fs.writeFileSync(path.join(repoDir, "no-head-repo.txt"), "upstream\n");
2527+
yield* runGit(repoDir, ["add", "no-head-repo.txt"]);
2528+
yield* runGit(repoDir, ["commit", "-m", "Local PR branch without repo metadata"]);
2529+
yield* runGit(repoDir, ["push", "-u", "origin", "feature/pr-local-no-head-repo"]);
2530+
yield* runGit(repoDir, ["checkout", "main"]);
2531+
yield* runGit(repoDir, ["branch", "-D", "feature/pr-local-no-head-repo"]);
2532+
yield* runGit(repoDir, [
2533+
"update-ref",
2534+
"-d",
2535+
"refs/remotes/origin/feature/pr-local-no-head-repo",
2536+
]);
2537+
2538+
const { manager } = yield* makeManager({
2539+
ghScenario: {
2540+
pullRequest: {
2541+
number: 66,
2542+
title: "Local upstream PR without repo metadata",
2543+
url: "https://github.com/pingdotgg/codething-mvp/pull/66",
2544+
baseRefName: "main",
2545+
headRefName: "feature/pr-local-no-head-repo",
2546+
state: "open",
2547+
},
2548+
},
2549+
});
2550+
2551+
const result = yield* preparePullRequestThread(manager, {
2552+
cwd: repoDir,
2553+
reference: "66",
2554+
mode: "local",
2555+
});
2556+
2557+
expect(result.worktreePath).toBeNull();
2558+
expect(result.branch).toBe("feature/pr-local-no-head-repo");
2559+
expect(
2560+
(yield* runGit(repoDir, ["rev-parse", "--abbrev-ref", "@{upstream}"])).stdout.trim(),
2561+
).toBe("origin/feature/pr-local-no-head-repo");
2562+
}),
2563+
);
2564+
24212565
it.effect("prepares pull request threads in worktree mode on the PR head branch", () =>
24222566
Effect.gen(function* () {
24232567
const repoDir = yield* makeTempDir("t3code-git-manager-");

apps/server/src/git/GitManager.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,22 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
568568
localBranch = pullRequest.headBranch,
569569
) {
570570
const repositoryNameWithOwner = resolveHeadRepositoryNameWithOwner(pullRequest) ?? "";
571+
if (repositoryNameWithOwner.length === 0 && pullRequest.isCrossRepository !== true) {
572+
const remoteName = yield* gitCore.resolvePrimaryRemoteName(cwd);
573+
yield* gitCore.fetchRemoteTrackingBranch({
574+
cwd,
575+
remoteName,
576+
remoteBranch: pullRequest.headBranch,
577+
});
578+
yield* gitCore.setBranchUpstream({
579+
cwd,
580+
branch: localBranch,
581+
remoteName,
582+
remoteBranch: pullRequest.headBranch,
583+
});
584+
return;
585+
}
586+
571587
if (repositoryNameWithOwner.length === 0) {
572588
return;
573589
}
@@ -588,6 +604,11 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
588604
url: remoteUrl,
589605
});
590606

607+
yield* gitCore.fetchRemoteTrackingBranch({
608+
cwd,
609+
remoteName,
610+
remoteBranch: pullRequest.headBranch,
611+
});
591612
yield* gitCore.setBranchUpstream({
592613
cwd,
593614
branch: localBranch,
@@ -690,6 +711,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
690711
hasUpstream: false,
691712
aheadCount: 0,
692713
behindCount: 0,
714+
aheadOfDefaultCount: 0,
693715
} satisfies GitStatusDetails;
694716
const readLocalStatus = Effect.fn("readLocalStatus")(function* (cwd: string) {
695717
const details = yield* gitCore
@@ -748,6 +770,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
748770
hasUpstream: details.hasUpstream,
749771
aheadCount: details.aheadCount,
750772
behindCount: details.behindCount,
773+
aheadOfDefaultCount: details.aheadOfDefaultCount,
751774
pr,
752775
} satisfies VcsStatusRemoteResult;
753776
});
@@ -1593,12 +1616,6 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
15931616
"Feature-branch checkout is only supported for commit actions.",
15941617
);
15951618
}
1596-
if (input.action === "push" && initialStatus.hasWorkingTreeChanges) {
1597-
return yield* gitManagerError(
1598-
"runStackedAction",
1599-
"Commit or stash local changes before pushing.",
1600-
);
1601-
}
16021619
if (input.action === "create_pr" && initialStatus.hasWorkingTreeChanges) {
16031620
return yield* gitManagerError(
16041621
"runStackedAction",

apps/server/src/git/GitWorkflowService.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ describe("GitWorkflowService", () => {
6565
hasUpstream: false,
6666
aheadCount: 0,
6767
behindCount: 0,
68+
aheadOfDefaultCount: 0,
6869
pr: null,
6970
});
7071
}).pipe(

apps/server/src/git/GitWorkflowService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ function nonRepositoryStatus(): VcsStatusResult {
112112
hasUpstream: false,
113113
aheadCount: 0,
114114
behindCount: 0,
115+
aheadOfDefaultCount: 0,
115116
pr: null,
116117
};
117118
}

apps/server/src/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { OpenCodeRuntimeLive } from "./provider/opencodeRuntime.ts";
2626
import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery.ts";
2727
import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore.ts";
2828
import * as GitHubCli from "./sourceControl/GitHubCli.ts";
29+
import * as GitLabCli from "./sourceControl/GitLabCli.ts";
2930
import * as TextGeneration from "./textGeneration/TextGeneration.ts";
3031
import { ProviderInstanceRegistryHydrationLive } from "./provider/Layers/ProviderInstanceRegistryHydration.ts";
3132
import { TerminalManagerLive } from "./terminal/Layers/Manager.ts";
@@ -165,7 +166,7 @@ const GitManagerLayerLive = GitManager.layer.pipe(
165166
Layer.provideMerge(GitVcsDriver.layer),
166167
Layer.provideMerge(
167168
SourceControlProviderRegistry.layer.pipe(
168-
Layer.provide(GitHubCli.layer),
169+
Layer.provide(Layer.mergeAll(GitHubCli.layer, GitLabCli.layer)),
169170
Layer.provideMerge(VcsDriverRegistryLayerLive),
170171
),
171172
),

0 commit comments

Comments
 (0)