mirror of
https://gitbruv.vercel.app/api/git/bruv/gitbruv.git
synced 2025-12-20 23:24:09 +01:00
speed improvements?
This commit is contained in:
parent
bbbf5f2a24
commit
5fa299cc9a
3 changed files with 158 additions and 62 deletions
|
|
@ -346,8 +346,7 @@ export async function getRepositoryWithStars(owner: string, name: string) {
|
||||||
const repo = await getRepository(owner, name);
|
const repo = await getRepository(owner, name);
|
||||||
if (!repo) return null;
|
if (!repo) return null;
|
||||||
|
|
||||||
const starCount = await getStarCount(repo.id);
|
const [starCount, starred] = await Promise.all([getStarCount(repo.id), isStarredByUser(repo.id)]);
|
||||||
const starred = await isStarredByUser(repo.id);
|
|
||||||
|
|
||||||
return { ...repo, starCount, starred };
|
return { ...repo, starCount, starred };
|
||||||
}
|
}
|
||||||
|
|
@ -365,10 +364,7 @@ export async function getUserRepositoriesWithStars(username: string) {
|
||||||
return reposWithStars;
|
return reposWithStars;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateRepository(
|
export async function updateRepository(repoId: string, data: { name?: string; description?: string; visibility?: "public" | "private" }) {
|
||||||
repoId: string,
|
|
||||||
data: { name?: string; description?: string; visibility?: "public" | "private" }
|
|
||||||
) {
|
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
throw new Error("Unauthorized");
|
throw new Error("Unauthorized");
|
||||||
|
|
@ -445,13 +441,7 @@ export async function getRepoBranches(owner: string, repoName: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRepoCommits(
|
export async function getRepoCommits(owner: string, repoName: string, branch: string, limit: number = 30, skip: number = 0) {
|
||||||
owner: string,
|
|
||||||
repoName: string,
|
|
||||||
branch: string,
|
|
||||||
limit: number = 30,
|
|
||||||
skip: number = 0
|
|
||||||
) {
|
|
||||||
const user = await db.query.users.findFirst({
|
const user = await db.query.users.findFirst({
|
||||||
where: eq(users.username, owner),
|
where: eq(users.username, owner),
|
||||||
});
|
});
|
||||||
|
|
@ -515,11 +505,7 @@ export async function getRepoCommitCount(owner: string, repoName: string, branch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPublicRepositories(
|
export async function getPublicRepositories(sortBy: "stars" | "updated" | "created" = "updated", limit: number = 20, offset: number = 0) {
|
||||||
sortBy: "stars" | "updated" | "created" = "updated",
|
|
||||||
limit: number = 20,
|
|
||||||
offset: number = 0
|
|
||||||
) {
|
|
||||||
const allRepos = await db
|
const allRepos = await db
|
||||||
.select({
|
.select({
|
||||||
id: repositories.id,
|
id: repositories.id,
|
||||||
|
|
@ -538,13 +524,7 @@ export async function getPublicRepositories(
|
||||||
.from(repositories)
|
.from(repositories)
|
||||||
.innerJoin(users, eq(repositories.ownerId, users.id))
|
.innerJoin(users, eq(repositories.ownerId, users.id))
|
||||||
.where(eq(repositories.visibility, "public"))
|
.where(eq(repositories.visibility, "public"))
|
||||||
.orderBy(
|
.orderBy(sortBy === "stars" ? desc(sql`star_count`) : sortBy === "created" ? desc(repositories.createdAt) : desc(repositories.updatedAt))
|
||||||
sortBy === "stars"
|
|
||||||
? desc(sql`star_count`)
|
|
||||||
: sortBy === "created"
|
|
||||||
? desc(repositories.createdAt)
|
|
||||||
: desc(repositories.updatedAt)
|
|
||||||
)
|
|
||||||
.limit(limit + 1)
|
.limit(limit + 1)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
|
|
||||||
|
|
@ -571,3 +551,115 @@ export async function getPublicRepositories(
|
||||||
hasMore,
|
hasMore,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FileEntry = {
|
||||||
|
name: string;
|
||||||
|
type: "blob" | "tree";
|
||||||
|
oid: string;
|
||||||
|
path: string;
|
||||||
|
lastCommit: { message: string; timestamp: number } | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getRepoPageData(owner: string, repoName: string) {
|
||||||
|
const [user, session] = await Promise.all([db.query.users.findFirst({ where: eq(users.username, owner) }), getSession()]);
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
const repo = await db.query.repositories.findFirst({
|
||||||
|
where: and(eq(repositories.ownerId, user.id), eq(repositories.name, repoName)),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!repo) return null;
|
||||||
|
|
||||||
|
if (repo.visibility === "private" && (!session?.user || session.user.id !== repo.ownerId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [starCountResult, starredResult] = await Promise.all([
|
||||||
|
db.select({ count: count() }).from(stars).where(eq(stars.repositoryId, repo.id)),
|
||||||
|
session?.user ? db.query.stars.findFirst({ where: and(eq(stars.userId, session.user.id), eq(stars.repositoryId, repo.id)) }) : Promise.resolve(null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const starCount = starCountResult[0]?.count ?? 0;
|
||||||
|
const starred = !!starredResult;
|
||||||
|
const isOwner = session?.user?.id === repo.ownerId;
|
||||||
|
|
||||||
|
const repoPrefix = getRepoPrefix(user.id, `${repoName}.git`);
|
||||||
|
const fs = createR2Fs(repoPrefix);
|
||||||
|
|
||||||
|
let files: FileEntry[] = [];
|
||||||
|
let isEmpty = true;
|
||||||
|
let readmeContent: string | null = null;
|
||||||
|
let branches: string[] = [];
|
||||||
|
let commitCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [branchList, commits] = await Promise.all([git.listBranches({ fs, gitdir: "/" }), git.log({ fs, gitdir: "/", ref: repo.defaultBranch })]);
|
||||||
|
|
||||||
|
branches = branchList;
|
||||||
|
commitCount = commits.length;
|
||||||
|
|
||||||
|
if (commits.length > 0) {
|
||||||
|
isEmpty = false;
|
||||||
|
const commitOid = commits[0].oid;
|
||||||
|
|
||||||
|
const { tree } = await git.readTree({ fs, gitdir: "/", oid: commitOid });
|
||||||
|
|
||||||
|
const fileEntries = await Promise.all(
|
||||||
|
tree.map(async (entry) => {
|
||||||
|
let lastCommit: { message: string; timestamp: number } | null = null;
|
||||||
|
try {
|
||||||
|
const fileCommits = await git.log({ fs, gitdir: "/", ref: repo.defaultBranch, filepath: entry.path, depth: 1 });
|
||||||
|
if (fileCommits.length > 0) {
|
||||||
|
lastCommit = {
|
||||||
|
message: fileCommits[0].commit.message.split("\n")[0],
|
||||||
|
timestamp: fileCommits[0].commit.committer.timestamp * 1000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return {
|
||||||
|
name: entry.path,
|
||||||
|
type: entry.type as "blob" | "tree",
|
||||||
|
oid: entry.oid,
|
||||||
|
path: entry.path,
|
||||||
|
lastCommit,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
fileEntries.sort((a, b) => {
|
||||||
|
if (a.type === "tree" && b.type !== "tree") return -1;
|
||||||
|
if (a.type !== "tree" && b.type === "tree") return 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
files = fileEntries;
|
||||||
|
|
||||||
|
const readmeEntry = tree.find((e) => e.path.toLowerCase() === "readme.md" && e.type === "blob");
|
||||||
|
if (readmeEntry) {
|
||||||
|
const { blob } = await git.readBlob({ fs, gitdir: "/", oid: readmeEntry.oid });
|
||||||
|
readmeContent = new TextDecoder("utf-8").decode(blob);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { code?: string };
|
||||||
|
if (error.code !== "NotFoundError") {
|
||||||
|
console.error("getRepoPageData error:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
repo: {
|
||||||
|
...repo,
|
||||||
|
owner: { id: user.id, username: user.username, name: user.name, image: user.image },
|
||||||
|
starCount,
|
||||||
|
starred,
|
||||||
|
},
|
||||||
|
files,
|
||||||
|
isEmpty,
|
||||||
|
readmeContent,
|
||||||
|
branches,
|
||||||
|
commitCount,
|
||||||
|
isOwner,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getRepositoryWithStars, getRepoFileTree, getRepoFile, getRepoBranches, getRepoCommitCount } from "@/actions/repositories";
|
import { getRepoPageData } from "@/actions/repositories";
|
||||||
import { getSession } from "@/lib/session";
|
|
||||||
import { FileTree } from "@/components/file-tree";
|
import { FileTree } from "@/components/file-tree";
|
||||||
import { CodeViewer } from "@/components/code-viewer";
|
import { CodeViewer } from "@/components/code-viewer";
|
||||||
import { CloneUrl } from "@/components/clone-url";
|
import { CloneUrl } from "@/components/clone-url";
|
||||||
|
|
@ -15,27 +14,13 @@ import { getPublicServerUrl } from "@/lib/utils";
|
||||||
export default async function RepoPage({ params }: { params: Promise<{ username: string; repo: string }> }) {
|
export default async function RepoPage({ params }: { params: Promise<{ username: string; repo: string }> }) {
|
||||||
const { username, repo: repoName } = await params;
|
const { username, repo: repoName } = await params;
|
||||||
|
|
||||||
const repo = await getRepositoryWithStars(username, repoName);
|
const data = await getRepoPageData(username, repoName);
|
||||||
|
|
||||||
if (!repo) {
|
if (!data) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await getSession();
|
const { repo, files, isEmpty, readmeContent, branches, commitCount, isOwner } = data;
|
||||||
const isOwner = session?.user?.id === repo.ownerId;
|
|
||||||
|
|
||||||
const [fileTree, branches, commitCount] = await Promise.all([
|
|
||||||
getRepoFileTree(username, repoName, repo.defaultBranch),
|
|
||||||
getRepoBranches(username, repoName),
|
|
||||||
getRepoCommitCount(username, repoName, repo.defaultBranch),
|
|
||||||
]);
|
|
||||||
const readmeFile = fileTree?.files.find((f) => f.name.toLowerCase() === "readme.md" && f.type === "blob");
|
|
||||||
|
|
||||||
let readmeContent = null;
|
|
||||||
if (readmeFile) {
|
|
||||||
const file = await getRepoFile(username, repoName, repo.defaultBranch, readmeFile.name);
|
|
||||||
readmeContent = file?.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container px-4 py-6">
|
<div className="container px-4 py-6">
|
||||||
|
|
@ -81,12 +66,7 @@ export default async function RepoPage({ params }: { params: Promise<{ username:
|
||||||
<div className="lg:col-span-3 space-y-6">
|
<div className="lg:col-span-3 space-y-6">
|
||||||
<div className="border border-border rounded-lg overflow-hidden">
|
<div className="border border-border rounded-lg overflow-hidden">
|
||||||
<div className="flex items-center justify-between gap-4 px-4 py-3 bg-card border-b border-border">
|
<div className="flex items-center justify-between gap-4 px-4 py-3 bg-card border-b border-border">
|
||||||
<BranchSelector
|
<BranchSelector branches={branches} currentBranch={repo.defaultBranch} username={username} repoName={repo.name} />
|
||||||
branches={branches}
|
|
||||||
currentBranch={repo.defaultBranch}
|
|
||||||
username={username}
|
|
||||||
repoName={repo.name}
|
|
||||||
/>
|
|
||||||
{commitCount > 0 && (
|
{commitCount > 0 && (
|
||||||
<Link
|
<Link
|
||||||
href={`/${username}/${repo.name}/commits/${repo.defaultBranch}`}
|
href={`/${username}/${repo.name}/commits/${repo.defaultBranch}`}
|
||||||
|
|
@ -99,10 +79,10 @@ export default async function RepoPage({ params }: { params: Promise<{ username:
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{fileTree?.isEmpty ? (
|
{isEmpty ? (
|
||||||
<EmptyRepoGuide username={username} repoName={repo.name} />
|
<EmptyRepoGuide username={username} repoName={repo.name} />
|
||||||
) : (
|
) : (
|
||||||
<FileTree files={fileTree?.files || []} username={username} repoName={repo.name} branch={repo.defaultBranch} />
|
<FileTree files={files} username={username} repoName={repo.name} branch={repo.defaultBranch} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
44
lib/r2-fs.ts
44
lib/r2-fs.ts
|
|
@ -1,4 +1,6 @@
|
||||||
import { r2Get, r2Put, r2Delete, r2Exists, r2List, r2DeletePrefix } from "./r2";
|
import { r2Get, r2Put, r2Delete, r2List, r2DeletePrefix } from "./r2";
|
||||||
|
|
||||||
|
const NOT_FOUND = Symbol("NOT_FOUND");
|
||||||
|
|
||||||
function normalizePath(path: string): string {
|
function normalizePath(path: string): string {
|
||||||
return path.replace(/\/+/g, "/").replace(/^\//, "").replace(/\/$/, "");
|
return path.replace(/\/+/g, "/").replace(/^\//, "").replace(/\/$/, "");
|
||||||
|
|
@ -24,6 +26,8 @@ function createStatResult(type: "file" | "dir", size: number) {
|
||||||
|
|
||||||
export function createR2Fs(repoPrefix: string) {
|
export function createR2Fs(repoPrefix: string) {
|
||||||
const dirMarkerCache = new Set<string>();
|
const dirMarkerCache = new Set<string>();
|
||||||
|
const fileCache = new Map<string, Buffer | typeof NOT_FOUND>();
|
||||||
|
const listCache = new Map<string, string[]>();
|
||||||
|
|
||||||
const getKey = (filepath: string) => {
|
const getKey = (filepath: string) => {
|
||||||
const normalized = normalizePath(filepath);
|
const normalized = normalizePath(filepath);
|
||||||
|
|
@ -36,9 +40,28 @@ export function createR2Fs(repoPrefix: string) {
|
||||||
return `${repoPrefix}/${normalized}`.replace(/\/+/g, "/");
|
return `${repoPrefix}/${normalized}`.replace(/\/+/g, "/");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cachedR2Get = async (key: string): Promise<Buffer | null> => {
|
||||||
|
if (fileCache.has(key)) {
|
||||||
|
const cached = fileCache.get(key);
|
||||||
|
return cached === NOT_FOUND ? null : cached!;
|
||||||
|
}
|
||||||
|
const data = await r2Get(key);
|
||||||
|
fileCache.set(key, data ?? NOT_FOUND);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedR2List = async (prefix: string): Promise<string[]> => {
|
||||||
|
if (listCache.has(prefix)) {
|
||||||
|
return listCache.get(prefix)!;
|
||||||
|
}
|
||||||
|
const keys = await r2List(prefix);
|
||||||
|
listCache.set(prefix, keys);
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
|
|
||||||
const readFile = async (filepath: string, options?: { encoding?: string }): Promise<Buffer | string> => {
|
const readFile = async (filepath: string, options?: { encoding?: string }): Promise<Buffer | string> => {
|
||||||
const key = getKey(filepath);
|
const key = getKey(filepath);
|
||||||
const data = await r2Get(key);
|
const data = await cachedR2Get(key);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
const err = new Error(`ENOENT: no such file or directory, open '${filepath}'`) as NodeJS.ErrnoException;
|
const err = new Error(`ENOENT: no such file or directory, open '${filepath}'`) as NodeJS.ErrnoException;
|
||||||
err.code = "ENOENT";
|
err.code = "ENOENT";
|
||||||
|
|
@ -52,19 +75,21 @@ export function createR2Fs(repoPrefix: string) {
|
||||||
|
|
||||||
const writeFile = async (filepath: string, data: Buffer | string): Promise<void> => {
|
const writeFile = async (filepath: string, data: Buffer | string): Promise<void> => {
|
||||||
const key = getKey(filepath);
|
const key = getKey(filepath);
|
||||||
console.log("[R2FS] writeFile:", key, "size:", typeof data === "string" ? data.length : data.length);
|
const buffer = typeof data === "string" ? Buffer.from(data) : data;
|
||||||
await r2Put(key, typeof data === "string" ? Buffer.from(data) : data);
|
await r2Put(key, buffer);
|
||||||
|
fileCache.set(key, buffer);
|
||||||
};
|
};
|
||||||
|
|
||||||
const unlink = async (filepath: string): Promise<void> => {
|
const unlink = async (filepath: string): Promise<void> => {
|
||||||
const key = getKey(filepath);
|
const key = getKey(filepath);
|
||||||
await r2Delete(key);
|
await r2Delete(key);
|
||||||
|
fileCache.set(key, NOT_FOUND);
|
||||||
};
|
};
|
||||||
|
|
||||||
const readdir = async (filepath: string): Promise<string[]> => {
|
const readdir = async (filepath: string): Promise<string[]> => {
|
||||||
const prefix = getKey(filepath);
|
const prefix = getKey(filepath);
|
||||||
const fullPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
const fullPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
||||||
const keys = await r2List(fullPrefix);
|
const keys = await cachedR2List(fullPrefix);
|
||||||
|
|
||||||
const entries = new Set<string>();
|
const entries = new Set<string>();
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
|
|
@ -98,14 +123,13 @@ export function createR2Fs(repoPrefix: string) {
|
||||||
return createStatResult("dir", 0);
|
return createStatResult("dir", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exists = await r2Exists(key);
|
const data = await cachedR2Get(key);
|
||||||
if (exists) {
|
if (data) {
|
||||||
const data = await r2Get(key);
|
return createStatResult("file", data.length);
|
||||||
return createStatResult("file", data?.length || 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefix = key.endsWith("/") ? key : `${key}/`;
|
const prefix = key.endsWith("/") ? key : `${key}/`;
|
||||||
const children = await r2List(prefix);
|
const children = await cachedR2List(prefix);
|
||||||
if (children.length > 0) {
|
if (children.length > 0) {
|
||||||
return createStatResult("dir", 0);
|
return createStatResult("dir", 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue