1
0
Fork 0
mirror of https://gitbruv.vercel.app/api/git/bruv/gitbruv.git synced 2025-12-20 23:24:09 +01:00
This commit is contained in:
Ahmet Kilinc 2025-12-20 12:07:05 +00:00
parent 4249f028aa
commit 468781e311
20 changed files with 84 additions and 185 deletions

View file

@ -1,14 +1,10 @@
import { GitBranch } from "lucide-react"; import { GitBranch } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
export default function AuthLayout({ export default function AuthLayout({ children }: { children: React.ReactNode }) {
children,
}: {
children: React.ReactNode;
}) {
return ( return (
<div className="min-h-screen flex flex-col items-center justify-center relative overflow-hidden px-4"> <div className="min-h-screen flex flex-col items-center justify-center relative overflow-hidden px-4">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-accent/15 via-background to-background" /> <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,var(--tw-gradient-stops))] from-accent/15 via-background to-background" />
<div className="absolute inset-0"> <div className="absolute inset-0">
<div className="absolute top-1/4 left-1/4 w-[500px] h-[500px] bg-accent/5 rounded-full blur-[100px]" /> <div className="absolute top-1/4 left-1/4 w-[500px] h-[500px] bg-accent/5 rounded-full blur-[100px]" />
<div className="absolute bottom-1/4 right-1/4 w-[400px] h-[400px] bg-primary/5 rounded-full blur-[100px]" /> <div className="absolute bottom-1/4 right-1/4 w-[400px] h-[400px] bg-primary/5 rounded-full blur-[100px]" />

View file

@ -89,9 +89,7 @@ export default function RegisterPage() {
required required
className="bg-input/50 h-11" className="bg-input/50 h-11"
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">This will be your unique identifier on gitbruv</p>
This will be your unique identifier on gitbruv
</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email">Email address</Label> <Label htmlFor="email">Email address</Label>
@ -117,15 +115,9 @@ export default function RegisterPage() {
minLength={8} minLength={8}
className="bg-input/50 h-11" className="bg-input/50 h-11"
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">Must be at least 8 characters</p>
Must be at least 8 characters
</p>
</div> </div>
<Button <Button type="submit" disabled={loading} className="w-full h-11">
type="submit"
disabled={loading}
className="w-full h-11"
>
{loading ? ( {loading ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
@ -140,10 +132,7 @@ export default function RegisterPage() {
<div className="mt-6 p-4 rounded-xl border border-border text-center"> <div className="mt-6 p-4 rounded-xl border border-border text-center">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Already have an account?{" "} Already have an account?{" "}
<Link <Link href="/login" className="text-accent hover:underline font-medium">
href="/login"
className="text-accent hover:underline font-medium"
>
Sign in Sign in
</Link> </Link>
</p> </p>

View file

@ -6,10 +6,22 @@ import { BranchSelector } from "@/components/branch-selector";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Lock, Globe, GitCommit, ChevronLeft, ChevronRight, Loader2 } from "lucide-react"; import { Lock, Globe, GitCommit, ChevronLeft, ChevronRight } from "lucide-react";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
async function CommitsList({ username, repoName, branch, page, perPage }: { username: string; repoName: string; branch: string; page: number; perPage: number }) { async function CommitsList({
username,
repoName,
branch,
page,
perPage,
}: {
username: string;
repoName: string;
branch: string;
page: number;
perPage: number;
}) {
const skip = (page - 1) * perPage; const skip = (page - 1) * perPage;
const { commits, hasMore } = await getRepoCommits(username, repoName, branch, perPage, skip); const { commits, hasMore } = await getRepoCommits(username, repoName, branch, perPage, skip);

View file

@ -22,11 +22,7 @@ type RepoData = {
ownerId: string; ownerId: string;
}; };
export default function RepoSettingsPage({ export default function RepoSettingsPage({ params }: { params: Promise<{ username: string; repo: string }> }) {
params,
}: {
params: Promise<{ username: string; repo: string }>;
}) {
const { username, repo: repoName } = use(params); const { username, repo: repoName } = use(params);
const router = useRouter(); const router = useRouter();
const { data: session } = useSession(); const { data: session } = useSession();
@ -117,9 +113,7 @@ export default function RepoSettingsPage({
<CardContent className="p-12 text-center"> <CardContent className="p-12 text-center">
<AlertTriangle className="h-12 w-12 mx-auto mb-4 text-muted-foreground" /> <AlertTriangle className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<h2 className="text-xl font-semibold mb-2">Access Denied</h2> <h2 className="text-xl font-semibold mb-2">Access Denied</h2>
<p className="text-muted-foreground mb-6"> <p className="text-muted-foreground mb-6">You don&apos;t have permission to access this page</p>
You don&apos;t have permission to access this page
</p>
<Button asChild> <Button asChild>
<Link href={`/${username}/${repoName}`}>Back to repository</Link> <Link href={`/${username}/${repoName}`}>Back to repository</Link>
</Button> </Button>
@ -175,9 +169,7 @@ export default function RepoSettingsPage({
<div className="space-y-2"> <div className="space-y-2">
<label <label
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${ className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
formData.visibility === "public" formData.visibility === "public" ? "border-accent bg-accent/5" : "border-border hover:border-muted-foreground/50"
? "border-accent bg-accent/5"
: "border-border hover:border-muted-foreground/50"
}`} }`}
> >
<input <input
@ -191,17 +183,13 @@ export default function RepoSettingsPage({
<Globe className="h-5 w-5 text-muted-foreground mt-0.5" /> <Globe className="h-5 w-5 text-muted-foreground mt-0.5" />
<div> <div>
<p className="font-medium">Public</p> <p className="font-medium">Public</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">Anyone can see this repository</p>
Anyone can see this repository
</p>
</div> </div>
</label> </label>
<label <label
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${ className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
formData.visibility === "private" formData.visibility === "private" ? "border-accent bg-accent/5" : "border-border hover:border-muted-foreground/50"
? "border-accent bg-accent/5"
: "border-border hover:border-muted-foreground/50"
}`} }`}
> >
<input <input
@ -215,9 +203,7 @@ export default function RepoSettingsPage({
<Lock className="h-5 w-5 text-muted-foreground mt-0.5" /> <Lock className="h-5 w-5 text-muted-foreground mt-0.5" />
<div> <div>
<p className="font-medium">Private</p> <p className="font-medium">Private</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">Only you can see this repository</p>
Only you can see this repository
</p>
</div> </div>
</label> </label>
</div> </div>
@ -236,17 +222,13 @@ export default function RepoSettingsPage({
<Card className="border-destructive/50"> <Card className="border-destructive/50">
<CardHeader> <CardHeader>
<CardTitle className="text-destructive">Danger Zone</CardTitle> <CardTitle className="text-destructive">Danger Zone</CardTitle>
<CardDescription> <CardDescription>Irreversible actions that can affect your repository</CardDescription>
Irreversible actions that can affect your repository
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex items-center justify-between p-4 rounded-lg border border-destructive/30 bg-destructive/5"> <div className="flex items-center justify-between p-4 rounded-lg border border-destructive/30 bg-destructive/5">
<div> <div>
<p className="font-medium">Delete this repository</p> <p className="font-medium">Delete this repository</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">Once deleted, it cannot be recovered</p>
Once deleted, it cannot be recovered
</p>
</div> </div>
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}> <Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
@ -260,33 +242,23 @@ export default function RepoSettingsPage({
<DialogTitle>Delete repository</DialogTitle> <DialogTitle>Delete repository</DialogTitle>
<DialogDescription> <DialogDescription>
This action cannot be undone. This will permanently delete the{" "} This action cannot be undone. This will permanently delete the{" "}
<strong>{username}/{repo.name}</strong> repository and all of its contents. <strong>
{username}/{repo.name}
</strong>{" "}
repository and all of its contents.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-2 py-4"> <div className="space-y-2 py-4">
<Label htmlFor="confirm"> <Label htmlFor="confirm">
Type <strong>{repo.name}</strong> to confirm Type <strong>{repo.name}</strong> to confirm
</Label> </Label>
<Input <Input id="confirm" value={deleteConfirm} onChange={(e) => setDeleteConfirm(e.target.value)} placeholder={repo.name} />
id="confirm"
value={deleteConfirm}
onChange={(e) => setDeleteConfirm(e.target.value)}
placeholder={repo.name}
/>
</div> </div>
<DialogFooter> <DialogFooter>
<Button <Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleting}>
variant="outline"
onClick={() => setDeleteOpen(false)}
disabled={deleting}
>
Cancel Cancel
</Button> </Button>
<Button <Button variant="destructive" onClick={handleDelete} disabled={deleteConfirm !== repo.name || deleting}>
variant="destructive"
onClick={handleDelete}
disabled={deleteConfirm !== repo.name || deleting}
>
{deleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} {deleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Delete repository Delete repository
</Button> </Button>
@ -299,4 +271,3 @@ export default function RepoSettingsPage({
</div> </div>
); );
} }

View file

@ -5,7 +5,7 @@ import { getRepository, getRepoFileTree, getRepoBranches } from "@/actions/repos
import { FileTree } from "@/components/file-tree"; import { FileTree } from "@/components/file-tree";
import { BranchSelector } from "@/components/branch-selector"; import { BranchSelector } from "@/components/branch-selector";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Lock, Globe, ChevronRight, Home, Loader2 } from "lucide-react"; import { Lock, Globe, ChevronRight, Home } from "lucide-react";
async function TreeContent({ username, repoName, branch, dirPath }: { username: string; repoName: string; branch: string; dirPath: string }) { async function TreeContent({ username, repoName, branch, dirPath }: { username: string; repoName: string; branch: string; dirPath: string }) {
const fileTree = await getRepoFileTree(username, repoName, branch, dirPath); const fileTree = await getRepoFileTree(username, repoName, branch, dirPath);

View file

@ -7,7 +7,7 @@ import { getUserRepositoriesWithStars, getUserStarredRepos } from "@/actions/rep
import { RepoList } from "@/components/repo-list"; import { RepoList } from "@/components/repo-list";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { CalendarDays, GitBranch, MapPin, Link as LinkIcon, Loader2, Star, BookOpen } from "lucide-react"; import { CalendarDays, GitBranch, MapPin, Link as LinkIcon, Star, BookOpen } from "lucide-react";
import { format } from "date-fns"; import { format } from "date-fns";
import Link from "next/link"; import Link from "next/link";
import { GithubIcon, XIcon, LinkedInIcon } from "@/components/icons"; import { GithubIcon, XIcon, LinkedInIcon } from "@/components/icons";
@ -61,13 +61,7 @@ function TabSkeleton() {
); );
} }
export default async function ProfilePage({ export default async function ProfilePage({ params, searchParams }: { params: Promise<{ username: string }>; searchParams: Promise<{ tab?: string }> }) {
params,
searchParams,
}: {
params: Promise<{ username: string }>;
searchParams: Promise<{ tab?: string }>;
}) {
const { username } = await params; const { username } = await params;
const { tab } = await searchParams; const { tab } = await searchParams;

View file

@ -1,11 +1,7 @@
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import { QueryProvider } from "@/lib/query-client"; import { QueryProvider } from "@/lib/query-client";
export default function MainLayout({ export default function MainLayout({ children }: { children: React.ReactNode }) {
children,
}: {
children: React.ReactNode;
}) {
return ( return (
<QueryProvider> <QueryProvider>
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
@ -15,4 +11,3 @@ export default function MainLayout({
</QueryProvider> </QueryProvider>
); );
} }

View file

@ -28,12 +28,9 @@ export default async function HomePage() {
<p className="text-sm text-muted-foreground truncate">@{username}</p> <p className="text-sm text-muted-foreground truncate">@{username}</p>
</div> </div>
</div> </div>
<nav className="mt-4 space-y-1"> <nav className="mt-4 space-y-1">
<Link <Link href={`/${username}`} className="flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm hover:bg-card transition-colors">
href={`/${username}`}
className="flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm hover:bg-card transition-colors"
>
<BookOpen className="h-4 w-4 text-muted-foreground" /> <BookOpen className="h-4 w-4 text-muted-foreground" />
Your repositories Your repositories
</Link> </Link>
@ -57,9 +54,7 @@ export default async function HomePage() {
<GitBranch className="h-8 w-8 text-accent" /> <GitBranch className="h-8 w-8 text-accent" />
</div> </div>
<h3 className="text-lg font-semibold mb-2">No repositories yet</h3> <h3 className="text-lg font-semibold mb-2">No repositories yet</h3>
<p className="text-muted-foreground mb-6 max-w-sm mx-auto"> <p className="text-muted-foreground mb-6 max-w-sm mx-auto">Create your first repository to start building something awesome</p>
Create your first repository to start building something awesome
</p>
<Button asChild size="lg"> <Button asChild size="lg">
<Link href="/new"> <Link href="/new">
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
@ -93,19 +88,14 @@ function LandingPage() {
<h1 className="text-4xl sm:text-5xl lg:text-7xl font-bold tracking-tight mb-6"> <h1 className="text-4xl sm:text-5xl lg:text-7xl font-bold tracking-tight mb-6">
Where the world Where the world
<br /> <br />
<span className="bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent"> <span className="bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent">builds software</span>
builds software
</span>
</h1> </h1>
<p className="text-lg lg:text-xl text-muted-foreground max-w-2xl mx-auto mb-10"> <p className="text-lg lg:text-xl text-muted-foreground max-w-2xl mx-auto mb-10">
Host and review code, manage projects, and build software alongside Host and review code, manage projects, and build software alongside millions of developers. Your code, your way.
millions of developers. Your code, your way.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button size="lg" asChild className="text-base h-12 px-8"> <Button size="lg" asChild className="text-base h-12 px-8">
<Link href="/register"> <Link href="/register">Get started for free</Link>
Get started for free
</Link>
</Button> </Button>
<Button size="lg" variant="outline" asChild className="text-base h-12 px-8"> <Button size="lg" variant="outline" asChild className="text-base h-12 px-8">
<Link href="/login">Sign in</Link> <Link href="/login">Sign in</Link>
@ -118,9 +108,7 @@ function LandingPage() {
<div className="container"> <div className="container">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-3xl font-bold mb-4">Everything you need to ship</h2> <h2 className="text-3xl font-bold mb-4">Everything you need to ship</h2>
<p className="text-muted-foreground max-w-2xl mx-auto"> <p className="text-muted-foreground max-w-2xl mx-auto">Powerful features to help you build, test, and deploy your projects faster</p>
Powerful features to help you build, test, and deploy your projects faster
</p>
</div> </div>
<div className="grid md:grid-cols-3 gap-6"> <div className="grid md:grid-cols-3 gap-6">
<FeatureCard <FeatureCard
@ -128,16 +116,8 @@ function LandingPage() {
title="Collaborative coding" title="Collaborative coding"
description="Build better software together with powerful code review and collaboration tools." description="Build better software together with powerful code review and collaboration tools."
/> />
<FeatureCard <FeatureCard icon={Rocket} title="Ship faster" description="Automate your workflow with CI/CD pipelines and deploy with confidence." />
icon={Rocket} <FeatureCard icon={Users} title="Open source" description="Join the world's largest developer community and contribute to projects." />
title="Ship faster"
description="Automate your workflow with CI/CD pipelines and deploy with confidence."
/>
<FeatureCard
icon={Users}
title="Open source"
description="Join the world's largest developer community and contribute to projects."
/>
</div> </div>
</div> </div>
</section> </section>
@ -145,15 +125,7 @@ function LandingPage() {
); );
} }
function FeatureCard({ function FeatureCard({ icon: Icon, title, description }: { icon: React.ElementType; title: string; description: string }) {
icon: Icon,
title,
description,
}: {
icon: React.ElementType;
title: string;
description: string;
}) {
return ( return (
<div className="group p-6 rounded-xl border border-border bg-card hover:border-accent/50 transition-all duration-300"> <div className="group p-6 rounded-xl border border-border bg-card hover:border-accent/50 transition-all duration-300">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-primary/20 to-accent/20 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform"> <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-primary/20 to-accent/20 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">

View file

@ -4,7 +4,6 @@ import { ProfileForm } from "@/components/settings/profile-form";
import { AvatarUpload } from "@/components/settings/avatar-upload"; import { AvatarUpload } from "@/components/settings/avatar-upload";
import { SocialLinksForm } from "@/components/settings/social-links-form"; import { SocialLinksForm } from "@/components/settings/social-links-form";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
export default async function SettingsPage() { export default async function SettingsPage() {
const user = await getCurrentUser(); const user = await getCurrentUser();
@ -18,9 +17,7 @@ export default async function SettingsPage() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Profile Picture</CardTitle> <CardTitle>Profile Picture</CardTitle>
<CardDescription> <CardDescription>Upload a picture to personalize your profile</CardDescription>
Upload a picture to personalize your profile
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<AvatarUpload currentAvatar={user.avatarUrl} name={user.name} /> <AvatarUpload currentAvatar={user.avatarUrl} name={user.name} />
@ -30,9 +27,7 @@ export default async function SettingsPage() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Profile Information</CardTitle> <CardTitle>Profile Information</CardTitle>
<CardDescription> <CardDescription>Update your profile details visible to other users</CardDescription>
Update your profile details visible to other users
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ProfileForm <ProfileForm
@ -51,9 +46,7 @@ export default async function SettingsPage() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Social Links</CardTitle> <CardTitle>Social Links</CardTitle>
<CardDescription> <CardDescription>Add links to your social profiles</CardDescription>
Add links to your social profiles
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<SocialLinksForm socialLinks={user.socialLinks} /> <SocialLinksForm socialLinks={user.socialLinks} />
@ -62,4 +55,3 @@ export default async function SettingsPage() {
</div> </div>
); );
} }

View file

@ -2,4 +2,3 @@ import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js"; import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth); export const { POST, GET } = toNextJsHandler(auth);

View file

@ -1,22 +1,19 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { r2Get } from "@/lib/r2"; import { r2Get } from "@/lib/r2";
export async function GET( export async function GET(request: NextRequest, { params }: { params: Promise<{ filename: string }> }) {
request: NextRequest,
{ params }: { params: Promise<{ filename: string }> }
) {
const { filename } = await params; const { filename } = await params;
const key = `avatars/${filename}`; const key = `avatars/${filename}`;
const data = await r2Get(key); const data = await r2Get(key);
if (!data) { if (!data) {
return new NextResponse(null, { status: 404 }); return new NextResponse(null, { status: 404 });
} }
const ext = filename.split(".").pop()?.toLowerCase(); const ext = filename.split(".").pop()?.toLowerCase();
let contentType = "image/png"; let contentType = "image/png";
if (ext === "jpg" || ext === "jpeg") { if (ext === "jpg" || ext === "jpeg") {
contentType = "image/jpeg"; contentType = "image/jpeg";
} else if (ext === "gif") { } else if (ext === "gif") {
@ -32,4 +29,3 @@ export async function GET(
}, },
}); });
} }

View file

@ -5,15 +5,12 @@ import { GitBranch, Home } from "lucide-react";
export default function NotFound() { export default function NotFound() {
return ( return (
<div className="min-h-screen flex flex-col items-center justify-center px-4"> <div className="min-h-screen flex flex-col items-center justify-center px-4">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-destructive/10 via-transparent to-transparent" /> <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,var(--tw-gradient-stops))] from-destructive/10 via-transparent to-transparent" />
<div className="relative text-center"> <div className="relative text-center">
<GitBranch className="h-16 w-16 mx-auto mb-6 text-muted-foreground" /> <GitBranch className="h-16 w-16 mx-auto mb-6 text-muted-foreground" />
<h1 className="text-7xl font-bold text-foreground mb-2">404</h1> <h1 className="text-7xl font-bold text-foreground mb-2">404</h1>
<h2 className="text-2xl font-semibold mb-4">Page not found</h2> <h2 className="text-2xl font-semibold mb-4">Page not found</h2>
<p className="text-muted-foreground mb-8 max-w-md"> <p className="text-muted-foreground mb-8 max-w-md">The page you&apos;re looking for doesn&apos;t exist or you don&apos;t have permission to view it.</p>
The page you&apos;re looking for doesn&apos;t exist or you don&apos;t have
permission to view it.
</p>
<Button asChild> <Button asChild>
<Link href="/" className="gap-2"> <Link href="/" className="gap-2">
<Home className="h-4 w-4" /> <Home className="h-4 w-4" />
@ -24,4 +21,3 @@ export default function NotFound() {
</div> </div>
); );
} }

View file

@ -16,8 +16,5 @@ export function QueryProvider({ children }: { children: React.ReactNode }) {
}) })
); );
return ( return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
} }

View file

@ -7,66 +7,69 @@ import os from "os";
export async function syncR2ToLocal(userId: string, repoName: string): Promise<string> { export async function syncR2ToLocal(userId: string, repoName: string): Promise<string> {
const repoPrefix = getRepoPrefix(userId, `${repoName}.git`); const repoPrefix = getRepoPrefix(userId, `${repoName}.git`);
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "gitbruv-")); const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "gitbruv-"));
const keys = await r2List(repoPrefix); const keys = await r2List(repoPrefix);
for (const key of keys) { for (const key of keys) {
const relativePath = key.slice(repoPrefix.length + 1); const relativePath = key.slice(repoPrefix.length + 1);
if (!relativePath) continue; if (!relativePath) continue;
const localPath = path.join(tempDir, relativePath); const localPath = path.join(tempDir, relativePath);
const localDir = path.dirname(localPath); const localDir = path.dirname(localPath);
await fs.mkdir(localDir, { recursive: true }); await fs.mkdir(localDir, { recursive: true });
const data = await r2Get(key); const data = await r2Get(key);
if (data) { if (data) {
await fs.writeFile(localPath, data); await fs.writeFile(localPath, data);
} }
} }
await fs.mkdir(path.join(tempDir, "objects"), { recursive: true }); await fs.mkdir(path.join(tempDir, "objects"), { recursive: true });
await fs.mkdir(path.join(tempDir, "objects/info"), { recursive: true }); await fs.mkdir(path.join(tempDir, "objects/info"), { recursive: true });
await fs.mkdir(path.join(tempDir, "objects/pack"), { recursive: true }); await fs.mkdir(path.join(tempDir, "objects/pack"), { recursive: true });
await fs.mkdir(path.join(tempDir, "refs"), { recursive: true }); await fs.mkdir(path.join(tempDir, "refs"), { recursive: true });
await fs.mkdir(path.join(tempDir, "refs/heads"), { recursive: true }); await fs.mkdir(path.join(tempDir, "refs/heads"), { recursive: true });
await fs.mkdir(path.join(tempDir, "refs/tags"), { recursive: true }); await fs.mkdir(path.join(tempDir, "refs/tags"), { recursive: true });
const headPath = path.join(tempDir, "HEAD"); const headPath = path.join(tempDir, "HEAD");
try { try {
await fs.access(headPath); await fs.access(headPath);
} catch { } catch {
await fs.writeFile(headPath, "ref: refs/heads/main\n"); await fs.writeFile(headPath, "ref: refs/heads/main\n");
} }
const configPath = path.join(tempDir, "config"); const configPath = path.join(tempDir, "config");
try { try {
await fs.access(configPath); await fs.access(configPath);
} catch { } catch {
await fs.writeFile(configPath, `[core] await fs.writeFile(
configPath,
`[core]
\trepositoryformatversion = 0 \trepositoryformatversion = 0
\tfilemode = true \tfilemode = true
\tbare = true \tbare = true
`); `
);
} }
return tempDir; return tempDir;
} }
export async function syncLocalToR2(localDir: string, userId: string, repoName: string): Promise<void> { export async function syncLocalToR2(localDir: string, userId: string, repoName: string): Promise<void> {
const repoPrefix = getRepoPrefix(userId, `${repoName}.git`); const repoPrefix = getRepoPrefix(userId, `${repoName}.git`);
const existingKeys = await r2List(repoPrefix); const existingKeys = await r2List(repoPrefix);
const existingSet = new Set(existingKeys); const existingSet = new Set(existingKeys);
const newKeys = new Set<string>(); const newKeys = new Set<string>();
async function uploadDir(dirPath: string, r2Prefix: string) { async function uploadDir(dirPath: string, r2Prefix: string) {
const entries = await fs.readdir(dirPath, { withFileTypes: true }); const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
const localPath = path.join(dirPath, entry.name); const localPath = path.join(dirPath, entry.name);
const r2Key = `${r2Prefix}/${entry.name}`; const r2Key = `${r2Prefix}/${entry.name}`;
if (entry.isDirectory()) { if (entry.isDirectory()) {
await uploadDir(localPath, r2Key); await uploadDir(localPath, r2Key);
} else if (entry.isFile()) { } else if (entry.isFile()) {
@ -76,9 +79,9 @@ export async function syncLocalToR2(localDir: string, userId: string, repoName:
} }
} }
} }
await uploadDir(localDir, repoPrefix); await uploadDir(localDir, repoPrefix);
for (const key of existingSet) { for (const key of existingSet) {
if (!newKeys.has(key)) { if (!newKeys.has(key)) {
await r2Delete(key); await r2Delete(key);
@ -89,28 +92,21 @@ export async function syncLocalToR2(localDir: string, userId: string, repoName:
export async function cleanupTempDir(tempDir: string): Promise<void> { export async function cleanupTempDir(tempDir: string): Promise<void> {
try { try {
await fs.rm(tempDir, { recursive: true, force: true }); await fs.rm(tempDir, { recursive: true, force: true });
} catch { } catch {}
}
} }
export async function withTempRepo<T>( export async function withTempRepo<T>(userId: string, repoName: string, operation: (tempDir: string) => Promise<T>, syncBack: boolean = false): Promise<T> {
userId: string,
repoName: string,
operation: (tempDir: string) => Promise<T>,
syncBack: boolean = false
): Promise<T> {
const tempDir = await syncR2ToLocal(userId, repoName); const tempDir = await syncR2ToLocal(userId, repoName);
try { try {
const result = await operation(tempDir); const result = await operation(tempDir);
if (syncBack) { if (syncBack) {
await syncLocalToR2(tempDir, userId, repoName); await syncLocalToR2(tempDir, userId, repoName);
} }
return result; return result;
} finally { } finally {
await cleanupTempDir(tempDir); await cleanupTempDir(tempDir);
} }
} }

View file

@ -95,4 +95,3 @@ export async function r2DeletePrefix(prefix: string): Promise<void> {
await r2Delete(key); await r2Delete(key);
} }
} }

View file

@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View file

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View file

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B