CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | /**
* Fork route — copy a repository into your account.
*/
import { Hono } from "hono";
import { eq, and } from "drizzle-orm";
import { db } from "../db";
import { repositories, users, activityFeed } from "../db/schema";
import { softAuth, requireAuth } from "../middleware/auth";
import type { AuthEnv } from "../middleware/auth";
import { getRepoPath, repoExists, initBareRepo } from "../git/repository";
import { config } from "../lib/config";
import { join } from "path";
const fork = new Hono<AuthEnv>();
fork.use("*", softAuth);
// Fork a repository
fork.post("/:owner/:repo/fork", requireAuth, async (c) => {
const { owner: ownerName, repo: repoName } = c.req.param();
const user = c.get("user")!;
// Can't fork your own repo
if (ownerName === user.username) {
return c.redirect(`/${ownerName}/${repoName}`);
}
// Check source exists
if (!(await repoExists(ownerName, repoName))) {
return c.redirect(`/${ownerName}/${repoName}`);
}
// Check if already forked
if (await repoExists(user.username, repoName)) {
return c.redirect(`/${user.username}/${repoName}`);
}
// Get source repo from DB
const [sourceOwner] = await db
.select()
.from(users)
.where(eq(users.username, ownerName))
.limit(1);
if (!sourceOwner) return c.redirect(`/${ownerName}/${repoName}`);
const [sourceRepo] = await db
.select()
.from(repositories)
.where(
and(
eq(repositories.ownerId, sourceOwner.id),
eq(repositories.name, repoName)
)
)
.limit(1);
if (!sourceRepo) return c.redirect(`/${ownerName}/${repoName}`);
// Clone the bare repo
const sourcePath = getRepoPath(ownerName, repoName);
const destPath = join(config.gitReposPath, user.username, `${repoName}.git`);
const proc = Bun.spawn(["git", "clone", "--bare", sourcePath, destPath], {
stdout: "pipe",
stderr: "pipe",
});
await proc.exited;
// Insert into DB
const [newRepo] = await db
.insert(repositories)
.values({
name: repoName,
ownerId: user.id,
description: sourceRepo.description
? `Fork of ${ownerName}/${repoName} — ${sourceRepo.description}`
: `Fork of ${ownerName}/${repoName}`,
isPrivate: false,
defaultBranch: sourceRepo.defaultBranch,
diskPath: destPath,
forkedFromId: sourceRepo.id,
})
.returning();
// Bootstrap the fork with green-by-default settings, protection, labels
if (newRepo) {
const { bootstrapRepository } = await import("../lib/repo-bootstrap");
await bootstrapRepository({
repositoryId: newRepo.id,
ownerUserId: user.id,
defaultBranch: sourceRepo.defaultBranch,
skipWelcomeIssue: true, // forks don't need a welcome issue
});
}
// Update fork count
await db
.update(repositories)
.set({ forkCount: sourceRepo.forkCount + 1 })
.where(eq(repositories.id, sourceRepo.id));
// Log activity
try {
await db.insert(activityFeed).values({
repositoryId: sourceRepo.id,
userId: user.id,
action: "fork",
metadata: JSON.stringify({ forkOwner: user.username }),
});
} catch {
// best effort
}
return c.redirect(`/${user.username}/${repoName}`);
});
export default fork;
|