extremely wip tangled spa

that's a lot of skibidi

aylac.top 2a971fb3 447b68c2

verified
+1 -1
src/index.tsx
···
preload={preloadRepoCommit}
/>
<Route
-
path="/compare/:rev2/:rev1"
+
path="/compare/:base/:compare"
component={RepoCompare}
preload={preloadRepoCompare}
/>
+43 -6
src/routes/repo/blob.tsx
···
useNavigate,
useParams,
} from "@solidjs/router";
-
import { createMemo, createResource } from "solid-js";
+
import { createMemo, createResource, onMount, Show } from "solid-js";
import { CodeBlock } from "../../elements/code_block";
+
import { formatBytes } from "../../util/bytes";
import { getLanguage } from "../../util/get_language";
import { figureOutDid } from "../../util/handle";
+
import { Header } from "./components/header";
+
import { PathBar } from "./components/pathbar";
import { useDid } from "./context";
-
import { PathBar } from "./generic";
-
import { Header } from "./main";
-
import { getRepoBlob, getRepoDefaultBranch } from "./main.data";
+
import { getRepoBlob, getRepoBlobUrl, getRepoDefaultBranch } from "./main.data";
export async function preloadRepoBlob({ params }: { params: Params }) {
const did = await figureOutDid(params.user!);
···
})(),
);
+
const blobUrl = createAsync(async () => {
+
const d = did();
+
if (!d) return;
+
return await getRepoBlobUrl(d, params.repo, params.ref, params.path);
+
});
+
const codeBlock = createMemo(() => {
if (!blob()?.content) return;
return (
···
const ref = () => params.ref || defaultBranch();
+
onMount(() => {
+
if (window.location.hash) {
+
const element = document.getElementById(window.location.hash.slice(1));
+
if (element)
+
element.scrollIntoView({ behavior: "instant", block: "start" });
+
}
+
});
+
return (
<div class="mx-auto max-w-5xl">
<Header user={params.user} repo={params.repo} />
-
<div class="flex flex-col rounded bg-white pt-1 pb-2 dark:bg-gray-800">
-
<div class="mx-4 flex flex-row border-gray-300 border-b py-2 dark:border-gray-700">
+
<div class="flex flex-col rounded bg-white py-2 dark:bg-gray-800">
+
<div class="mx-5 flex flex-col justify-between gap-1 border-gray-300 border-b py-2 md:flex-row md:items-center dark:border-gray-700">
<PathBar
user={params.user}
repo={params.repo}
···
path={params.path}
is_file={true}
/>
+
<div class="text-gray-500 text-xs dark:text-gray-400">
+
<Show when={blobUrl() && ref() && blob()}>
+
<span>
+
{"at "}
+
<a
+
href={`/${params.user}/${params.repo}/tree/${ref()!}`}
+
class="text-black hover:text-gray-700 hover:underline dark:text-white hover:dark:text-gray-300"
+
>
+
{ref()!}
+
</a>
+
</span>
+
<span class="select-none px-1 before:content-['\00B7']" />
+
<span>{formatBytes(blob()!.size!)}</span>
+
<span class="select-none px-1 before:content-['\00B7']" />
+
<a
+
href={blobUrl()}
+
class="text-black hover:text-gray-700 hover:underline dark:text-white hover:dark:text-gray-300"
+
>
+
view raw
+
</a>
+
</Show>
+
</div>
</div>
<div class="flex flex-col p-4">{codeBlock()}</div>
</div>
+19 -6
src/routes/repo/diff/commit.tsx
···
import { figureOutDid } from "../../../util/handle";
import { toRelativeTime } from "../../../util/time";
import type { Commit, DID } from "../../../util/types";
+
import { Header } from "../components/header";
import { useDid } from "../context";
-
import { Header } from "../main";
import { getRepoCommit } from "../main.data";
import { buildTree, type TreeNode } from "./data";
import { DiffView, Sidebar } from "./generic";
···
},
);
+
const commitDiffViewInput = createMemo(() => {
+
if (!commit()?.diff) return;
+
return commit()!.diff.diff.map((diff) => ({
+
oldName: diff.name.old,
+
newName: diff.name.new,
+
textFragments: diff.text_fragments,
+
}));
+
});
+
const sidebar = createMemo(() => {
if (!commit()?.diff.diff)
return { name: "", fullPath: "", type: "directory" } as TreeNode;
···
commit={commit()!}
/>
</Show>
-
<Show when={sidebar()?.children && commit()}>
-
<div class="flex flex-row gap-1">
-
<Sidebar sidebar={sidebar()} commit={commit()!} />
-
<div class="min-w-0 flex-1 flex-col gap-1 p-1 pl-0">
-
<DiffView commit={commit()!} />
+
<Show when={sidebar()?.children && commit() && commitDiffViewInput()}>
+
<div class="flex flex-col gap-1 md:flex-row">
+
<Sidebar
+
sidebar={sidebar()}
+
insertions={commit()!.diff.stat.insertions}
+
deletions={commit()!.diff.stat.deletions}
+
/>
+
<div class="min-w-0 flex-1 flex-col gap-1 p-1 max-md:pt-0 md:pl-0">
+
<DiffView diff={commitDiffViewInput()!} />
</div>
</div>
</Show>
+65 -53
src/routes/repo/diff/compare.tsx
···
import { type Params, useParams } from "@solidjs/router";
import { createMemo, createResource, onMount, Show } from "solid-js";
import { figureOutDid } from "../../../util/handle";
-
import type { DID } from "../../../util/types";
+
import type { Compare, DID } from "../../../util/types";
+
import { Header } from "../components/header";
import { useDid } from "../context";
-
import { Header } from "../main";
-
import { getRepoCompare } from "../main.data";
+
import { getRepoCommit, getRepoCompare } from "../main.data";
import { buildTree, type TreeNode } from "./data";
-
import { DiffView, RenderTree } from "./generic";
+
import { DiffView, Sidebar } from "./generic";
export async function preloadRepoCompare({ params }: { params: Params }) {
-
const did = await figureOutDid(params.user);
+
const did = await figureOutDid(params.user!);
if (!did) return;
-
getRepoCompare(did, params.repo, params.rev1, params.rev2);
-
}
-
-
function ComparisonHeader(props: {
-
user: string;
-
repo: string;
-
message: { title: string; content: string };
-
commit: Commit;
-
}) {
-
return (
-
<div>
-
<Header user={props.user} repo={props.repo} />
-
<div class="mx-1 flex flex-col gap-2 rounded bg-white p-4 dark:bg-gray-800">
-
<div>{props.message.title}</div>
-
<Show when={props.message.content}>
-
<div class="text-xs">{props.message.content}</div>
-
</Show>
-
</div>
-
</div>
-
);
+
getRepoCommit(did, params.repo!, params.ref!);
}
export default function RepoCompare() {
-
const params = useParams();
+
const params = useParams() as {
+
user: string;
+
repo: string;
+
base: string;
+
compare: string;
+
};
const did = useDid();
-
const [commit] = createResource(
+
const [compare] = createResource(
() => {
const d = did();
if (!d) return;
-
return [d, params.repo, params.ref] as const;
+
return [d, params.repo, params.base, params.compare] as const;
},
-
async ([did, repo, ref]) => {
-
const res = await getRepoCompare(did as DID, repo, ref);
+
async ([did, repo, base, compare]) => {
+
const res = await getRepoCompare(did as DID, repo, base, compare);
if (!res.ok) return;
-
return res.data as Commit;
+
return res.data as Compare;
},
);
+
const compareDiffViewInput = createMemo(() => {
+
if (!compare()?.combined_patch) return;
+
return compare()!.combined_patch!.map((diff) => ({
+
oldName: diff.OldName,
+
newName: diff.NewName,
+
textFragments: diff.TextFragments || [],
+
}));
+
});
+
const sidebar = createMemo(() => {
-
if (!commit()?.diff.diff)
+
if (!compare()?.combined_patch)
return { name: "", fullPath: "", type: "directory" } as TreeNode;
-
return buildTree(commit()!.diff.diff.map((v) => v.name.new));
+
const set = new Set(
+
compare()!.combined_patch!.flatMap((file) => [
+
file.NewName,
+
file.OldName,
+
]),
+
);
+
set.delete("");
+
return buildTree(set);
});
-
const message = createMemo(() => {
-
const c = commit();
-
if (!c) return;
-
-
const titleEnd = c.diff.commit.message.indexOf("\n");
-
return {
-
title: c.diff.commit.message.slice(0, titleEnd),
-
content: c.diff.commit.message.slice(titleEnd + 1),
-
};
+
const compareNumbers = createMemo(() => {
+
if (!compare()?.combined_patch) return { insertions: 0, deletions: 0 };
+
let [insertions, deletions] = [0, 0];
+
compare()!.combined_patch!.forEach((file) => {
+
if (!file.TextFragments) return;
+
file.TextFragments.forEach((fragment) => {
+
insertions += fragment.LinesAdded;
+
deletions += fragment.LinesDeleted;
+
});
+
});
+
return { insertions, deletions };
});
onMount(() => {
···
return (
<div class="mx-auto max-w-10xl">
-
<Show when={commit() && message()}>
-
<CommitHeader
-
user={params.user}
-
repo={params.repo}
-
message={message()!}
-
commit={commit()!}
-
/>
+
<Show when={compare()}>
+
<CommitHeader user={params.user} repo={params.repo} />
</Show>
-
<Show when={sidebar()?.children && commit()}>
-
<div class="flex flex-row gap-1">
-
<div class="min-w-0 flex-1 flex-col gap-1 p-1 pl-0">
-
<DiffView commit={commit()!} />
+
<Show when={sidebar()?.children && compare() && compareDiffViewInput()}>
+
<div class="flex flex-col gap-1 md:flex-row">
+
<Sidebar
+
sidebar={sidebar()}
+
insertions={compareNumbers().insertions}
+
deletions={compareNumbers().deletions}
+
/>
+
<div class="min-w-0 flex-1 flex-col gap-1 p-1 max-md:pt-0 md:pl-0">
+
<DiffView diff={compareDiffViewInput()!} />
</div>
</div>
</Show>
</div>
);
}
+
+
function CommitHeader(props: { user: string; repo: string }) {
+
return (
+
<div>
+
<Header user={props.user} repo={props.repo} />
+
</div>
+
);
+
}
+1 -1
src/routes/repo/diff/data.ts
···
children: { [key: string]: BuildNode };
};
-
export function buildTree(paths: string[]): TreeNode {
+
export function buildTree(paths: string[] | Set<string>): TreeNode {
// sorry i used claude for this
const root: { [key: string]: BuildNode } = {};
+33 -25
src/routes/repo/diff/generic.tsx
···
import { createSignal, For, Match, Show, Switch } from "solid-js";
-
import type { Commit, DiffTextFragment } from "../../../util/types";
+
import type { DiffTextFragment } from "../../../util/types";
import type { TreeNode } from "./data";
export function RenderTree(props: { tree: TreeNode; skip?: boolean }) {
···
);
}
-
export function DiffView(props: { commit: Commit }) {
+
export function DiffView(props: {
+
diff: {
+
oldName: string;
+
newName: string;
+
textFragments: DiffTextFragment[];
+
}[];
+
}) {
return (
-
<For each={props.commit.diff.diff}>
+
<For each={props.diff}>
{(diff) => {
const [show, setShow] = createSignal(true);
-
const [addedLines, removedLines] = diff.text_fragments
-
? diff.text_fragments.reduce(
+
const [addedLines, removedLines] = diff.textFragments
+
? diff.textFragments.reduce(
(acc, v) => [acc[0] + v.NewLines, acc[1] + v.LinesDeleted],
[0, 0],
)
···
<div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${removedLines}`}</div>
</Show>
</div>
-
<Show
-
when={diff.name.old !== "" && diff.name.new !== diff.name.old}
-
>
-
<div class="select-text">{diff.name.old}</div>
+
<Show when={diff.oldName !== "" && diff.newName !== diff.oldName}>
+
<div class="select-text">{diff.oldName}</div>
<div class="iconify gravity-ui--arrow-right" />
</Show>
-
<div class="select-text">{diff.name.new}</div>
+
<div class="select-text">{diff.newName}</div>
</button>
);
-
if (!diff.text_fragments)
+
if (!diff.textFragments)
return (
<div
-
id={`file-${encodeURI(diff.name.new)}`}
+
id={`file-${encodeURI(diff.newName)}`}
class="not-last:mb-1 flex w-full flex-col rounded border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
>
{header}
···
</div>
);
-
const lastFrag = diff.text_fragments[diff.text_fragments.length - 1];
+
const lastFrag = diff.textFragments[diff.textFragments.length - 1];
const numberSize = Math.max(
2,
(
···
return (
<div
-
id={`file-${diff.name.new}`}
+
id={`file-${diff.newName}`}
class="not-last:mb-1 flex w-full flex-col rounded border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
>
{header}
···
class={`select-text overflow-x-auto rounded-b bg-white dark:bg-gray-800 ${show() ? "" : "hidden"}`}
>
<div class="min-w-max">
-
<For each={diff.text_fragments}>
+
<For each={diff.textFragments}>
{(frag, i) => (
<Fragment
-
file={diff.name.new}
+
file={diff.newName}
data={frag}
index={i()}
numberSize={numberSize}
···
);
}
-
export function Sidebar(props: { commit: Commit; sidebar: TreeNode }) {
+
export function Sidebar(props: {
+
insertions: number;
+
deletions: number;
+
sidebar: TreeNode;
+
}) {
return (
-
<div class="sticky top-0 flex max-h-screen w-50 overflow-y-auto p-1 pr-0">
+
<div class="flex overflow-y-auto p-1 max-md:pb-0 md:sticky md:top-0 md:max-h-screen md:w-50 md:pr-0">
<div class="flex min-h-max w-full grow cursor-default flex-col rounded border border-gray-200 bg-white p-1 dark:border-gray-700 dark:bg-gray-800">
-
<div class="flex flex-row items-center justify-between gap-1 p-1">
+
<div class="flex flex-row items-center justify-between p-1">
<div class="font-bold">CHANGED FILES</div>
-
<div class="flex h-6 select-text flex-row items-center overflow-hidden rounded font-mono text-xs *:h-full *:content-center *:px-1">
-
<Show when={props.commit.diff.stat.insertions > 0}>
-
<div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${props.commit.diff.stat.insertions}`}</div>
+
<div class="flex h-6 select-text flex-row items-center overflow-clip rounded font-mono text-xs *:h-full *:content-center *:px-1">
+
<Show when={props.insertions > 0}>
+
<div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${props.insertions}`}</div>
</Show>
-
<Show when={props.commit.diff.stat.deletions > 0}>
-
<div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${props.commit.diff.stat.deletions}`}</div>
+
<Show when={props.deletions > 0}>
+
<div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${props.deletions}`}</div>
</Show>
</div>
</div>
-
<div class="max-w-full overflow-x-auto text-nowrap">
+
<div class="flex max-w-full flex-col overflow-x-auto text-nowrap">
<RenderTree tree={props.sidebar} skip={true} />
</div>
</div>
+1 -1
src/routes/repo/generic.tsx src/routes/repo/components/pathbar.tsx
···
});
return (
-
<div class="flex flex-row gap-1">
+
<div class="flex flex-row gap-1 overflow-x-auto">
<a
href={`/${props.user}/${props.repo}/tree/${props.gitref}`}
class="text-gray-600 hover:text-gray-700 hover:underline dark:text-gray-400 hover:dark:text-gray-300"
+11 -2
src/routes/repo/main.data.ts
···
}, "RepoDefaultBranch");
export const getRepoTree = query(
-
async (user: DID, repo: string, ref: string, path?: string) => {
+
async (user: DID, repo: string, ref: string, path: string) => {
const rpc = await getKnotRpc(user, repo);
return await rpc.get("sh.tangled.repo.tree", {
···
return await rpc.get("sh.tangled.repo.blob", {
params: {
repo: `${user}/${repo}`,
-
ref: ref,
+
ref,
path,
},
});
},
"RepoBlob",
+
);
+
+
export const getRepoBlobUrl = query(
+
async (user: DID, repo: string, ref: string, path: string) => {
+
const knot = await getRepoKnot(user, repo);
+
+
return `${knot}/xrpc/sh.tangled.repo.blob?repo=${user}/${repo}&ref=${ref}&path=${path}&raw=true`;
+
},
+
"RepoBlobUrl",
);
export const getRepoLog = query(
+4 -4
src/routes/repo/main.tsx src/routes/repo/components/header.tsx
···
import { type Accessor, createMemo, createResource, Show } from "solid-js";
-
import { getUserRepoByRkey } from "../../util/get_repo";
-
import { figureOutHandle } from "../../util/handle";
-
import type { DID } from "../../util/types";
-
import { useRepoInfo } from "./context";
+
import { getUserRepoByRkey } from "../../../util/get_repo";
+
import { figureOutHandle } from "../../../util/handle";
+
import type { DID } from "../../../util/types";
+
import { useRepoInfo } from "../context";
function HeaderItem(props: {
path: string;
+12 -12
src/routes/repo/tree.tsx
···
import { SolidMarkdown } from "solid-markdown";
import { languageColors } from "../../util/get_language";
import type { Branches, RepoLog, Tags } from "../../util/types";
+
import { Header } from "./components/header";
import { useDid } from "./context";
-
import { Header } from "./main";
import {
getRepoBranches,
getRepoDefaultBranch,
···
import "../../styles/markdown.css";
import { figureOutDid } from "../../util/handle";
import { toRelativeTime } from "../../util/time";
-
import { PathBar } from "./generic";
+
import { PathBar } from "./components/pathbar";
export async function preloadRepoTree({ params }: { params: Params }) {
const did = await figureOutDid(params.user!);
if (!did) return;
-
getRepoTree(did, params.repo!, params.ref!, params.path);
+
getRepoTree(did, params.repo!, params.ref!, params.path!);
}
export default function RepoTree() {
···
<Show when={languages()} fallback={<div class="h-4" />}>
<LanguageLine languages={languages()!} />
</Show>
-
<div class="flex flex-row px-4 py-2">
+
<div class="flex flex-row px-5 py-2">
<Show when={branches() && tags() && ref() && defaultBranch()}>
<select
-
class="w-40 border border-gray-200 p-1 dark:border-gray-700"
+
class="w-40 border border-gray-200 bg-white p-1 dark:border-gray-700 dark:bg-gray-800"
onInput={(e) =>
navigate(
`/${params.user}/${params.repo}/tree/${e.target.value}`,
···
</Show>
</div>
<div class="flex flex-row py-2">
-
<div class="mr-1 flex w-1/2 flex-col border-gray-300 border-r pr-2 pl-4 dark:border-gray-700">
+
<div class="flex flex-1 flex-col border-gray-300 px-5 md:mr-1 md:w-1/2 md:border-r md:pr-3 dark:border-gray-700">
<Show when={defaultBranch() && tree() && sortedFiles()}>
<FileDirectory
user={params.user}
···
/>
</Show>
</div>
-
<div class="ml-1 flex w-1/2 flex-col pr-4 pl-2">
+
<div class="flex w-1/2 flex-col pr-5 pl-2 max-md:hidden">
<Show when={defaultBranch() && sortedFiles() && logs()}>
<LogData
user={params.user}
···
</div>
</Match>
<Match when={params.path && ref()}>
-
<div class="mx-4 flex flex-row overflow-x-auto border-gray-300 border-b pt-3 pb-2 dark:border-gray-700">
+
<div class="mx-5 flex flex-row border-gray-300 border-b pt-4 pb-2 md:items-center dark:border-gray-700">
<PathBar
user={params.user}
repo={params.repo}
···
is_file={false}
/>
</div>
-
<div class="flex flex-row px-4 py-2">
+
<div class="flex flex-row px-5 py-2">
<Show when={defaultBranch() && tree() && sortedFiles()}>
<FileDirectory
user={params.user}
···
<div class="flex flex-row justify-between">
<a
href={`/${props.user}/${props.repo}/${is_file ? "blob" : "tree"}/${props.tree.ref || props.defaultBranch}/${props.tree.parent ? `${props.tree.parent}/` : ""}${file.name}`}
-
class="flex min-w-1/3 flex-row items-center gap-1.5 truncate p-1 hover:underline"
+
class="flex flex-1 @max-xl:grow flex-row items-center gap-1.5 truncate p-1 hover:underline"
>
<div
class={`iconify ${is_file ? "gravity-ui--file" : "gravity-ui--folder-fill"}`}
···
<Show when={last_commit_date}>
<a
href={`/${props.user}/${props.repo}/commit/${file.last_commit!.hash}`}
-
class="mr-2 @max-xl:hidden min-w-0 flex-1 grow truncate text-ellipsis text-left text-gray-500 hover:text-gray-700 hover:underline dark:text-gray-500 hover:dark:text-gray-200"
+
class="mr-2 @max-xl:hidden min-w-0 flex-1 grow truncate text-ellipsis text-left text-gray-500 hover:text-gray-700 hover:underline dark:text-gray-400 hover:dark:text-gray-200"
>
{file.last_commit!.message}
</a>
···
dateStyle: "full",
timeStyle: "short",
})}
-
class="shrink-0 whitespace-nowrap text-gray-500 text-xs hover:text-gray-700 hover:underline dark:text-gray-500 hover:dark:text-gray-200"
+
class="shrink-0 whitespace-nowrap text-gray-500 text-xs hover:text-gray-700 hover:underline dark:text-gray-400 hover:dark:text-gray-200"
>
{toRelativeTime(last_commit_date!)}
</a>
+16
src/util/bytes.ts
···
+
export function formatBytes(bytes: number): string {
+
if (bytes === 0) return "0 B";
+
+
const k = 1000; // Use 1000 for "kB", use 1024 if you want "KiB" math
+
const sizes = ["B", "kB", "MB", "GB", "TB", "PB"];
+
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
+
const value = bytes / k ** i;
+
+
// 1. toFixed(1) ensures we don't have crazy long decimals (1.33333 -> "1.3")
+
// 2. parseFloat() strips useless zeros ("1.0" -> 1, "1.5" -> 1.5)
+
// 3. i === 0 check ensures simple Bytes are always integers
+
const formattedValue = i === 0 ? value : parseFloat(value.toFixed(1));
+
+
return `${formattedValue} ${sizes[i]}`;
+
}
+48
src/util/types.ts
···
};
};
+
export type Compare = {
+
rev1: string;
+
rev2: string;
+
format_patch?: {
+
Files?: {
+
OldName: string;
+
NewName: string;
+
IsNew: boolean;
+
IsDelete: boolean;
+
IsCopy: boolean;
+
IsRename: boolean;
+
OldMode: number;
+
NewMode: number;
+
OldOIDPrefix: string;
+
NewOIDPrefix: string;
+
Score: number;
+
TextFragments?: DiffTextFragment[];
+
}[];
+
SHA: string;
+
Author: { Name: string; Email: string };
+
AuthorDate: string;
+
Commiter: string | null;
+
CommiterDate: string;
+
Title: string;
+
Body: string;
+
SubjectPrefix: string;
+
BodyAppendix: string;
+
}[];
+
combined_patch?: {
+
OldName: string;
+
NewName: string;
+
IsNew: boolean;
+
IsDelete: boolean;
+
IsCopy: boolean;
+
IsRename: boolean;
+
OldMode: number;
+
NewMode: number;
+
OldOIDPrefix: string;
+
NewOIDPrefix: string;
+
Score: number;
+
TextFragments?: DiffTextFragment[];
+
IsBinary: boolean;
+
BinaryFragment: string | null;
+
ReverseBinaryFragment: string | null;
+
}[];
+
combined_patch_raw: string;
+
};
+
export type Branches = {
branches: {
reference: {