Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4741,6 +4741,57 @@ it.layer(layerGitManagerTest)("GitManager", (it) => {
}),
);

it.effect(
"create_pr publishes to the push remote when the tracked upstream already has HEAD",
() =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
const originDir = yield* createBareRemote();
const forkDir = yield* createBareRemote();
yield* runGit(repoDir, ["remote", "add", "origin", originDir]);
yield* runGit(repoDir, ["remote", "add", "fork", forkDir]);
yield* runGit(repoDir, ["checkout", "-b", "topic"]);
NodeFS.writeFileSync(NodePath.join(repoDir, "changes.txt"), "change\n");
yield* runGit(repoDir, ["add", "changes.txt"]);
yield* runGit(repoDir, ["commit", "-m", "Topic change"]);
yield* runGit(repoDir, ["push", "-u", "origin", "topic"]);
yield* configureVisibleRemoteUrlWithLocalRewrite(
repoDir,
"origin",
"git@github.com:pingdotgg/codething-mvp.git",
originDir,
);
yield* configureVisibleRemoteUrlWithLocalRewrite(
repoDir,
"fork",
"git@github.com:octocat/codething-mvp.git",
forkDir,
);
yield* runGit(repoDir, ["config", "remote.pushDefault", "fork"]);

const { manager, providerCalls } = yield* makeManager();
const result = yield* runStackedAction(manager, {
cwd: repoDir,
action: "create_pr",
});

expect(result.push).toMatchObject({ status: "pushed", upstreamBranch: "fork/topic" });
expect(result.pr.status).toBe("created");
expect(
yield* runGit(forkDir, ["--git-dir=.", "log", "-1", "--pretty=%s", "topic"]),
).toMatchObject({
stdout: "Topic change\n",
});
expect(created(providerCalls)).toEqual([
{ baseRefName: "main", headSelector: "octocat:topic" },
]);
expect(yield* runGit(repoDir, ["rev-parse", "--abbrev-ref", "@{upstream}"])).toMatchObject({
stdout: "origin/topic\n",
});
}),
);

it.effect("rejects push/pr actions from detached HEAD", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
Expand Down
30 changes: 25 additions & 5 deletions apps/server/src/git/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2376,6 +2376,7 @@ export const make = Effect.gen(function* () {
fallbackBranch: string | null,
threadId: ThreadId | undefined,
emit: GitActionProgressEmitter,
pushRemoteRef?: { readonly remoteName: string; readonly upstreamRef: string },
) {
const provider = yield* sourceControlProvider(cwd);
const terms = sourceControlDefinitions.get(provider.kind).changeRequest;
Expand All @@ -2396,10 +2397,12 @@ export const make = Effect.gen(function* () {
});
}

const headContext = yield* resolveBranchHeadContext(cwd, {
branch,
upstreamRef: details.upstreamRef,
});
// A configured push remote receives the commits while the branch keeps
// tracking its upstream, so the PR head is the push remote's branch.
const headContext = yield* resolveBranchHeadContext(
cwd,
pushRemoteRef ? { branch, ...pushRemoteRef } : { branch, upstreamRef: details.upstreamRef },
);

const existing = yield* findOpenPr(cwd, headContext);
if (existing) {
Expand Down Expand Up @@ -3402,12 +3405,26 @@ export const make = Effect.gen(function* () {
> {
const initialStatus = yield* gitCore.statusDetails(input.cwd);
const wantsCommit = isCommitAction(input.action);
const upToDateBranch =
input.action === "create_pr" &&
initialStatus.hasUpstream &&
initialStatus.aheadCount === 0
? initialStatus.branch
: null;
// A current upstream says nothing about a separate push remote, which may lack HEAD.
const pushTarget = upToDateBranch
? yield* gitCore
.resolvePushTarget(input.cwd, { branch: upToDateBranch, hasUpstream: true })
.pipe(Effect.orElseSucceed(() => null))
: null;
const wantsPush =
input.action === "push" ||
input.action === "commit_push" ||
input.action === "commit_push_pr" ||
(input.action === "create_pr" &&
(!initialStatus.hasUpstream || initialStatus.aheadCount > 0));
(!initialStatus.hasUpstream ||
initialStatus.aheadCount > 0 ||
pushTarget?.viaPushRemote === true));
const wantsPr = input.action === "create_pr" || input.action === "commit_push_pr";

if (input.featureBranch && !wantsCommit) {
Expand Down Expand Up @@ -3560,6 +3577,9 @@ export const make = Effect.gen(function* () {
currentBranch,
input.threadId,
progress.emit,
"pushRemote" in push && push.pushRemote && push.upstreamBranch
? { remoteName: push.pushRemote, upstreamRef: push.upstreamBranch }
: undefined,
),
),
)
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,16 @@ export interface GitPushResult {
status: "pushed" | "skipped_up_to_date";
branch: string;
upstreamBranch?: string | undefined;
/** Set when `upstreamBranch` is on a configured push remote instead of the tracked upstream. */
pushRemote?: string | undefined;
setUpstream?: boolean | undefined;
}

export interface GitPushTarget {
remoteName: string;
branchName: string;
/** The tracked upstream is current, yet pushes go to this configured push remote. */
viaPushRemote: boolean;
}

export interface GitRangeContext {
Expand Down
146 changes: 146 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7951,6 +7951,152 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
assert.notEqual(originMain.exitCode, 0);
}),
);

it.effect.each(["branch.topic.pushRemote", "remote.pushDefault"] as const)(
"pushes a branch tracking its upstream head to the %s remote",
(configKey) =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const upstreamRemote = yield* makeTmpDir("git-upstream-remote-");
const forkRemote = yield* makeTmpDir("git-fork-remote-");
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* git(upstreamRemote, ["init", "--bare"]);
yield* git(forkRemote, ["init", "--bare"]);
yield* git(cwd, ["remote", "add", "upstream", upstreamRemote]);
yield* git(cwd, ["remote", "add", "fork", forkRemote]);
yield* git(cwd, ["checkout", "-b", "topic"]);
yield* git(cwd, ["push", "upstream", "topic"]);
yield* git(cwd, ["push", "fork", "topic"]);
yield* git(cwd, ["branch", "--set-upstream-to=upstream/topic"]);
yield* git(cwd, ["config", configKey, "fork"]);
const upstreamTopicSha = yield* git(cwd, ["rev-parse", "HEAD"]);
yield* writeTextFile(cwd, "fork.txt", "fork\n");
yield* git(cwd, ["add", "-A"]);
yield* driver.commit(cwd, "Add fork update", "");

const pushed = yield* driver.pushCurrentBranch(cwd, null);

assert.deepInclude(pushed, {
status: "pushed",
branch: "topic",
upstreamBranch: "fork/topic",
pushRemote: "fork",
setUpstream: false,
});
assert.equal(
yield* git(forkRemote, ["--git-dir=.", "log", "-1", "--pretty=%s", "topic"]),
"Add fork update",
);
assert.equal(
yield* git(upstreamRemote, ["--git-dir=.", "rev-parse", "topic"]),
upstreamTopicSha,
);
assert.equal(
yield* git(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"]),
"upstream/topic",
);
}),
);

it.effect.each(["deleted", "rewound"] as const)(
"pushes an up-to-date tracked branch to the push remote when its branch there is %s",
(forkState) =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const upstreamRemote = yield* makeTmpDir("git-upstream-remote-");
const forkRemote = yield* makeTmpDir("git-fork-remote-");
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* git(upstreamRemote, ["init", "--bare"]);
yield* git(forkRemote, ["init", "--bare"]);
yield* git(cwd, ["remote", "add", "upstream", upstreamRemote]);
yield* git(cwd, ["remote", "add", "fork", forkRemote]);
yield* git(cwd, ["checkout", "-b", "topic"]);
yield* writeTextFile(cwd, "upstream.txt", "upstream\n");
yield* git(cwd, ["add", "-A"]);
yield* driver.commit(cwd, "Add upstream update", "");
yield* git(cwd, ["push", "-u", "upstream", "topic"]);
yield* git(cwd, ["config", "remote.pushDefault", "fork"]);
// Another checkout changes the fork after this one last fetched it,
// leaving a local fork/topic that still contains HEAD.
yield* git(cwd, ["push", "fork", "topic"]);
yield* git(
forkRemote,
forkState === "deleted"
? ["--git-dir=.", "update-ref", "-d", "refs/heads/topic"]
: ["--git-dir=.", "update-ref", "refs/heads/topic", "topic~1"],
);

const pushed = yield* driver.pushCurrentBranch(cwd, null);

assert.deepInclude(pushed, {
status: "pushed",
upstreamBranch: "fork/topic",
pushRemote: "fork",
});
assert.equal(
yield* git(forkRemote, ["--git-dir=.", "log", "-1", "--pretty=%s", "topic"]),
"Add upstream update",
);
assert.equal(
yield* git(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"]),
"upstream/topic",
);
}),
);

it.effect("skips an up-to-date tracked branch the push remote already has", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const upstreamRemote = yield* makeTmpDir("git-upstream-remote-");
const forkRemote = yield* makeTmpDir("git-fork-remote-");
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* git(upstreamRemote, ["init", "--bare"]);
yield* git(forkRemote, ["init", "--bare"]);
yield* git(cwd, ["remote", "add", "upstream", upstreamRemote]);
yield* git(cwd, ["remote", "add", "fork", forkRemote]);
yield* git(cwd, ["checkout", "-b", "topic"]);
yield* git(cwd, ["push", "-u", "upstream", "topic"]);
yield* git(cwd, ["push", "fork", "topic"]);
yield* git(cwd, ["config", "remote.pushDefault", "fork"]);

const pushed = yield* driver.pushCurrentBranch(cwd, null);

assert.deepInclude(pushed, {
status: "skipped_up_to_date",
upstreamBranch: "fork/topic",
pushRemote: "fork",
});
}),
);

it.effect("skips an up-to-date branch whose differently named upstream is its base", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const upstreamRemote = yield* makeTmpDir("git-upstream-remote-");
const forkRemote = yield* makeTmpDir("git-fork-remote-");
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* git(upstreamRemote, ["init", "--bare"]);
yield* git(forkRemote, ["init", "--bare"]);
yield* git(cwd, ["remote", "add", "upstream", upstreamRemote]);
yield* git(cwd, ["remote", "add", "fork", forkRemote]);
yield* git(cwd, ["push", "upstream", "HEAD:refs/heads/release"]);
yield* git(cwd, ["fetch", "upstream"]);
yield* git(cwd, ["checkout", "-b", "topic", "--track", "upstream/release"]);
yield* git(cwd, ["config", "remote.pushDefault", "fork"]);

const pushed = yield* driver.pushCurrentBranch(cwd, null);

assert.deepInclude(pushed, {
status: "skipped_up_to_date",
upstreamBranch: "upstream/release",
});
assert.equal(yield* git(forkRemote, ["--git-dir=.", "branch", "--list", "topic"]), "");
}),
);
});
});

Expand Down
Loading
Loading