mirror of
https://github.com/microsoft/monaco-editor.git
synced 2025-12-22 20:52:56 +01:00
IndexedDB layer for SWITCH
cgen-e8367dc43f144fefaa618c07fa07b3a7
This commit is contained in:
parent
700215ed3f
commit
301bf98891
6 changed files with 411 additions and 9 deletions
41
website/src/website/pages/switch/FileTree.tsx
Normal file
41
website/src/website/pages/switch/FileTree.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface FileNode { path: string; isDir: boolean; children?: FileNode[] }
|
||||||
|
|
||||||
|
export function FileTree({ tree, onOpen }: { tree: FileNode[]; onOpen: (path: string) => void }) {
|
||||||
|
return (
|
||||||
|
<div className="switch-filetree" role="tree">
|
||||||
|
{tree.map((n) => (
|
||||||
|
<TreeNode key={n.path} node={n} onOpen={onOpen} depth={0} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TreeNode({ node, onOpen, depth }: { node: FileNode; onOpen: (path: string) => void; depth: number }) {
|
||||||
|
const [open, setOpen] = React.useState(true);
|
||||||
|
const paddingLeft = 8 + depth * 12;
|
||||||
|
if (node.isDir) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="switch-tree-row" style={{ paddingLeft }} onClick={() => setOpen(!open)}>
|
||||||
|
<span className="switch-tree-chevron">{open ? "▾" : "▸"}</span>
|
||||||
|
<span className="switch-tree-dir">{basename(node.path)}</span>
|
||||||
|
</div>
|
||||||
|
{open && node.children?.map((c) => (
|
||||||
|
<TreeNode key={c.path} node={c} onOpen={onOpen} depth={depth + 1} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="switch-tree-row" style={{ paddingLeft }} role="treeitem" onClick={() => onOpen(node.path)}>
|
||||||
|
<span className="switch-tree-file">{basename(node.path)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function basename(p: string) {
|
||||||
|
const idx = p.lastIndexOf("/");
|
||||||
|
return idx >= 0 ? p.slice(idx + 1) : p;
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,87 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ControlledMonacoEditor } from "../../components/monaco/MonacoEditor";
|
import { ControlledMonacoEditor } from "../../components/monaco/MonacoEditor";
|
||||||
import "../../switch.scss";
|
import "../../switch.scss";
|
||||||
|
import { createRepository, listRepositories } from "../../switch/models";
|
||||||
|
import { pickDirectory, getDirectoryHandle, ensureReadPerm, walk } from "../../switch/fs";
|
||||||
|
import type { RepoRecord } from "../../switch/db";
|
||||||
|
import { FileTree } from "./FileTree";
|
||||||
|
|
||||||
|
interface OpenFile { path: string; content: string; language: string }
|
||||||
|
|
||||||
export function SwitchPage() {
|
export function SwitchPage() {
|
||||||
const [value, setValue] = React.useState<string>(`// Welcome to SWITCH\n// Start coding, create repos and manage issues here.\n`);
|
const [repos, setRepos] = React.useState<RepoRecord[]>([]);
|
||||||
|
const [activeRepo, setActiveRepo] = React.useState<RepoRecord | undefined>();
|
||||||
|
const [tree, setTree] = React.useState<{ path: string; isDir: boolean; children?: any[] }[]>([]);
|
||||||
|
const [openFile, setOpenFile] = React.useState<OpenFile | undefined>();
|
||||||
|
const [editorValue, setEditorValue] = React.useState<string>("// SWITCH: Open a folder to get started\n");
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
refreshRepos();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function refreshRepos() {
|
||||||
|
const list = await listRepositories();
|
||||||
|
setRepos(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onOpenFolder() {
|
||||||
|
const { id: fsId, handle } = await pickDirectory();
|
||||||
|
if (!(await ensureReadPerm(handle))) return;
|
||||||
|
const repo = await createRepository(handle.name || "workspace", fsId);
|
||||||
|
await refreshRepos();
|
||||||
|
setActiveRepo(repo);
|
||||||
|
await loadTree(repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTree(repo: RepoRecord) {
|
||||||
|
if (!repo.fsHandleId) return;
|
||||||
|
const dir = await getDirectoryHandle(repo.fsHandleId);
|
||||||
|
if (!dir) return;
|
||||||
|
if (!(await ensureReadPerm(dir))) return;
|
||||||
|
|
||||||
|
// flatten files then build a simple tree
|
||||||
|
const files: { path: string }[] = [];
|
||||||
|
for await (const f of walk(dir)) files.push({ path: f.path });
|
||||||
|
const root: any = {};
|
||||||
|
for (const f of files) {
|
||||||
|
const parts = f.path.split("/");
|
||||||
|
let cur = root;
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const key = parts.slice(0, i + 1).join("/");
|
||||||
|
const isDir = i < parts.length - 1;
|
||||||
|
cur.children = cur.children || new Map<string, any>();
|
||||||
|
if (!cur.children.has(key)) cur.children.set(key, { path: key, isDir, children: undefined });
|
||||||
|
cur = cur.children.get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const toArray = (node: any): any[] => {
|
||||||
|
if (!node.children) return [];
|
||||||
|
const arr = Array.from(node.children.values());
|
||||||
|
for (const n of arr) if (n.isDir) n.children = toArray(n);
|
||||||
|
arr.sort((a, b) => (a.isDir === b.isDir ? a.path.localeCompare(b.path) : a.isDir ? -1 : 1));
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
setTree(toArray(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSelectRepo(repoId: string) {
|
||||||
|
const repo = repos.find((r) => r.id === repoId);
|
||||||
|
setActiveRepo(repo);
|
||||||
|
if (repo) await loadTree(repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onOpenFile(path: string) {
|
||||||
|
if (!activeRepo?.fsHandleId) return;
|
||||||
|
const dir = await getDirectoryHandle(activeRepo.fsHandleId);
|
||||||
|
if (!dir) return;
|
||||||
|
if (!(await ensureReadPerm(dir))) return;
|
||||||
|
const file = await getFileByPath(dir, path);
|
||||||
|
if (!file) return;
|
||||||
|
const content = await file.text();
|
||||||
|
setOpenFile({ path, content, language: languageFromPath(path) });
|
||||||
|
setEditorValue(content);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="switch-shell">
|
<div className="switch-shell">
|
||||||
<div className="switch-activity-bar" aria-label="Activity Bar">
|
<div className="switch-activity-bar" aria-label="Activity Bar">
|
||||||
|
|
@ -15,34 +93,89 @@ export function SwitchPage() {
|
||||||
<div className="switch-main">
|
<div className="switch-main">
|
||||||
<aside className="switch-sidebar" aria-label="Sidebar">
|
<aside className="switch-sidebar" aria-label="Sidebar">
|
||||||
<div className="switch-pane">
|
<div className="switch-pane">
|
||||||
<div className="switch-pane-header">Explorer</div>
|
<div className="switch-pane-header">Repositories</div>
|
||||||
<div className="switch-pane-body">
|
<div className="switch-pane-body">
|
||||||
<button className="switch-secondary-btn">Open Folder</button>
|
<button className="switch-secondary-btn" onClick={onOpenFolder}>Open Folder</button>
|
||||||
<button className="switch-secondary-btn">New Repository</button>
|
<div>
|
||||||
|
{repos.map((r) => (
|
||||||
|
<div key={r.id}>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="repo" onChange={() => onSelectRepo(r.id)} checked={activeRepo?.id === r.id} /> {r.name}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="switch-pane">
|
<div className="switch-pane">
|
||||||
<div className="switch-pane-header">Repositories</div>
|
<div className="switch-pane-header">Explorer</div>
|
||||||
<div className="switch-pane-body">
|
<div className="switch-pane-body">
|
||||||
<div className="switch-empty">No repositories yet</div>
|
{tree.length === 0 ? (
|
||||||
|
<div className="switch-empty">No files</div>
|
||||||
|
) : (
|
||||||
|
<FileTree tree={tree} onOpen={onOpenFile} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<section className="switch-editor" aria-label="Editor">
|
<section className="switch-editor" aria-label="Editor">
|
||||||
<div className="switch-tabbar">
|
<div className="switch-tabbar">
|
||||||
<div className="switch-tab active">welcome.ts</div>
|
<div className="switch-tab active">{openFile?.path || "welcome.txt"}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="switch-editor-inner">
|
<div className="switch-editor-inner">
|
||||||
<ControlledMonacoEditor value={value} onDidValueChange={setValue} language="typescript" theme="vs-dark" />
|
<ControlledMonacoEditor value={editorValue} onDidValueChange={setEditorValue} language={openFile?.language || "plaintext"} theme="vs-dark" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<footer className="switch-statusbar" aria-label="Status Bar">
|
<footer className="switch-statusbar" aria-label="Status Bar">
|
||||||
<div className="switch-status-item">SWITCH</div>
|
<div className="switch-status-item">SWITCH</div>
|
||||||
<div className="switch-status-item">TypeScript</div>
|
<div className="switch-status-item">{openFile?.language || "Plain Text"}</div>
|
||||||
<div className="switch-status-item">UTF-8</div>
|
<div className="switch-status-item">UTF-8</div>
|
||||||
<div className="switch-status-item">LF</div>
|
<div className="switch-status-item">LF</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getFileByPath(dir: FileSystemDirectoryHandle, path: string): Promise<File | undefined> {
|
||||||
|
const parts = path.split("/");
|
||||||
|
let cur: FileSystemDirectoryHandle = dir;
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const name = parts[i];
|
||||||
|
if (i === parts.length - 1) {
|
||||||
|
const fh = await (cur as any).getFileHandle(name).catch(() => undefined) as FileSystemFileHandle | undefined;
|
||||||
|
return fh ? fh.getFile() : undefined;
|
||||||
|
} else {
|
||||||
|
cur = await (cur as any).getDirectoryHandle(name).catch(() => undefined);
|
||||||
|
if (!cur) return undefined as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function languageFromPath(path: string): string {
|
||||||
|
const ext = path.split(".").pop()?.toLowerCase();
|
||||||
|
switch (ext) {
|
||||||
|
case "ts": return "typescript";
|
||||||
|
case "tsx": return "typescript";
|
||||||
|
case "js": return "javascript";
|
||||||
|
case "jsx": return "javascript";
|
||||||
|
case "json": return "json";
|
||||||
|
case "css": return "css";
|
||||||
|
case "scss": return "scss";
|
||||||
|
case "html": return "html";
|
||||||
|
case "md": return "markdown";
|
||||||
|
case "py": return "python";
|
||||||
|
case "rb": return "ruby";
|
||||||
|
case "go": return "go";
|
||||||
|
case "rs": return "rust";
|
||||||
|
case "java": return "java";
|
||||||
|
case "cs": return "csharp";
|
||||||
|
case "cpp": return "cpp";
|
||||||
|
case "yml":
|
||||||
|
case "yaml": return "yaml";
|
||||||
|
case "xml": return "xml";
|
||||||
|
case "sql": return "sql";
|
||||||
|
default: return "plaintext";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
138
website/src/website/switch/db.ts
Normal file
138
website/src/website/switch/db.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
export type StoreName = "repos" | "branches" | "commits" | "issues" | "fsHandles" | "settings";
|
||||||
|
|
||||||
|
const DB_NAME = "switch-db";
|
||||||
|
const DB_VERSION = 1;
|
||||||
|
|
||||||
|
export interface RepoRecord {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdAt: number;
|
||||||
|
defaultBranch: string;
|
||||||
|
fsHandleId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BranchRecord {
|
||||||
|
id: string;
|
||||||
|
repoId: string;
|
||||||
|
name: string;
|
||||||
|
headCommitId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommitRecord {
|
||||||
|
id: string;
|
||||||
|
repoId: string;
|
||||||
|
message: string;
|
||||||
|
parentIds: string[];
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IssueRecord {
|
||||||
|
id: string;
|
||||||
|
repoId: string;
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
labels: string[];
|
||||||
|
status: "open" | "closed";
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
branchId?: string;
|
||||||
|
filePath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FsHandleRecord {
|
||||||
|
id: string;
|
||||||
|
handle: FileSystemDirectoryHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openDB(): Promise<IDBDatabase> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
||||||
|
req.onupgradeneeded = () => {
|
||||||
|
const db = req.result;
|
||||||
|
if (!db.objectStoreNames.contains("repos")) {
|
||||||
|
const store = db.createObjectStore("repos", { keyPath: "id" });
|
||||||
|
store.createIndex("by_name", "name", { unique: false });
|
||||||
|
}
|
||||||
|
if (!db.objectStoreNames.contains("branches")) {
|
||||||
|
const store = db.createObjectStore("branches", { keyPath: "id" });
|
||||||
|
store.createIndex("by_repo", "repoId", { unique: false });
|
||||||
|
store.createIndex("by_repo_name", ["repoId", "name"], { unique: true });
|
||||||
|
}
|
||||||
|
if (!db.objectStoreNames.contains("commits")) {
|
||||||
|
const store = db.createObjectStore("commits", { keyPath: "id" });
|
||||||
|
store.createIndex("by_repo", "repoId", { unique: false });
|
||||||
|
}
|
||||||
|
if (!db.objectStoreNames.contains("issues")) {
|
||||||
|
const store = db.createObjectStore("issues", { keyPath: "id" });
|
||||||
|
store.createIndex("by_repo", "repoId", { unique: false });
|
||||||
|
}
|
||||||
|
if (!db.objectStoreNames.contains("fsHandles")) {
|
||||||
|
db.createObjectStore("fsHandles", { keyPath: "id" });
|
||||||
|
}
|
||||||
|
if (!db.objectStoreNames.contains("settings")) {
|
||||||
|
db.createObjectStore("settings", { keyPath: "id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tx<T>(storeNames: StoreName[], mode: IDBTransactionMode, fn: (tx: IDBTransaction) => Promise<T>): Promise<T> {
|
||||||
|
const db = await openDB();
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(storeNames, mode);
|
||||||
|
const done = async () => {
|
||||||
|
try {
|
||||||
|
const r = await fn(transaction);
|
||||||
|
resolve(r);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
transaction.oncomplete = () => db.close();
|
||||||
|
transaction.onabort = () => reject(transaction.error);
|
||||||
|
transaction.onerror = () => reject(transaction.error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function put<T>(store: StoreName, value: T): Promise<void> {
|
||||||
|
await tx([store], "readwrite", async (t) => {
|
||||||
|
await requestAsPromise<void>(t.objectStore(store).put(value as any));
|
||||||
|
return undefined as any;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get<T>(store: StoreName, key: IDBValidKey): Promise<T | undefined> {
|
||||||
|
return tx([store], "readonly", async (t) => {
|
||||||
|
return requestAsPromise<T | undefined>(t.objectStore(store).get(key));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function del(store: StoreName, key: IDBValidKey): Promise<void> {
|
||||||
|
await tx([store], "readwrite", async (t) => {
|
||||||
|
await requestAsPromise<void>(t.objectStore(store).delete(key));
|
||||||
|
return undefined as any;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllByIndex<T>(store: StoreName, index: string, query: IDBValidKey | IDBKeyRange): Promise<T[]> {
|
||||||
|
return tx([store], "readonly", async (t) => {
|
||||||
|
const idx = t.objectStore(store).index(index);
|
||||||
|
return requestAsPromise<T[]>(idx.getAll(query));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAll<T>(store: StoreName): Promise<T[]> {
|
||||||
|
return tx([store], "readonly", async (t) => {
|
||||||
|
return requestAsPromise<T[]>(t.objectStore(store).getAll());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestAsPromise<T>(req: IDBRequest<T>): Promise<T> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
req.onsuccess = () => resolve(req.result as T);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
36
website/src/website/switch/fs.ts
Normal file
36
website/src/website/switch/fs.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { put, get, type FsHandleRecord } from "./db";
|
||||||
|
import { nanoid } from "./uid";
|
||||||
|
|
||||||
|
export async function pickDirectory(): Promise<{ id: string; handle: FileSystemDirectoryHandle }> {
|
||||||
|
// @ts-ignore
|
||||||
|
const handle: FileSystemDirectoryHandle = await (window as any).showDirectoryPicker();
|
||||||
|
const id = nanoid();
|
||||||
|
const rec: FsHandleRecord = { id, handle };
|
||||||
|
await put("fsHandles", rec);
|
||||||
|
return { id, handle };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDirectoryHandle(id: string): Promise<FileSystemDirectoryHandle | undefined> {
|
||||||
|
const rec = await get<FsHandleRecord>("fsHandles", id);
|
||||||
|
return rec?.handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureReadPerm(dir: FileSystemDirectoryHandle): Promise<boolean> {
|
||||||
|
const perm = await (dir as any).queryPermission?.({ mode: "read" });
|
||||||
|
if (perm === "granted") return true;
|
||||||
|
const req = await (dir as any).requestPermission?.({ mode: "read" });
|
||||||
|
return req === "granted";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function* walk(dir: FileSystemDirectoryHandle, pathPrefix = ""): AsyncGenerator<{ path: string; file: File }>{
|
||||||
|
// @ts-ignore
|
||||||
|
for await (const [name, entry] of (dir as any).entries()) {
|
||||||
|
const p = pathPrefix ? `${pathPrefix}/${name}` : name;
|
||||||
|
if (entry.kind === "directory") {
|
||||||
|
yield* walk(entry as FileSystemDirectoryHandle, p);
|
||||||
|
} else {
|
||||||
|
const file = await (entry as FileSystemFileHandle).getFile();
|
||||||
|
yield { path: p, file };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
website/src/website/switch/models.ts
Normal file
47
website/src/website/switch/models.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { put, get, getAll, getAllByIndex, del, type RepoRecord, type BranchRecord, type CommitRecord, type IssueRecord } from "./db";
|
||||||
|
import { nanoid } from "./uid";
|
||||||
|
|
||||||
|
export async function createRepository(name: string, fsHandleId?: string): Promise<RepoRecord> {
|
||||||
|
const repo: RepoRecord = { id: nanoid(), name, createdAt: Date.now(), defaultBranch: "main", fsHandleId };
|
||||||
|
await put("repos", repo);
|
||||||
|
const main: BranchRecord = { id: nanoid(), repoId: repo.id, name: repo.defaultBranch, headCommitId: undefined };
|
||||||
|
await put("branches", main);
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listRepositories(): Promise<RepoRecord[]> {
|
||||||
|
return getAll<RepoRecord>("repos");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteRepository(repoId: string): Promise<void> {
|
||||||
|
// naive cascade delete
|
||||||
|
const branches = await getAllByIndex<BranchRecord>("branches", "by_repo", repoId);
|
||||||
|
for (const b of branches) await del("branches", b.id);
|
||||||
|
const commits = await getAllByIndex<CommitRecord>("commits", "by_repo", repoId);
|
||||||
|
for (const c of commits) await del("commits", c.id);
|
||||||
|
const issues = await getAllByIndex<IssueRecord>("issues", "by_repo", repoId);
|
||||||
|
for (const i of issues) await del("issues", i.id);
|
||||||
|
await del("repos", repoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createBranch(repoId: string, name: string, fromCommitId?: string): Promise<BranchRecord> {
|
||||||
|
const existing = await getAllByIndex<BranchRecord>("branches", "by_repo_name", [repoId, name]).catch(() => []);
|
||||||
|
if (existing && existing.length) throw new Error("Branch already exists");
|
||||||
|
const branch: BranchRecord = { id: nanoid(), repoId, name, headCommitId: fromCommitId };
|
||||||
|
await put("branches", branch);
|
||||||
|
return branch;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listBranches(repoId: string): Promise<BranchRecord[]> {
|
||||||
|
return getAllByIndex<BranchRecord>("branches", "by_repo", repoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function commit(repoId: string, message: string, parentIds: string[] = []): Promise<CommitRecord> {
|
||||||
|
const c: CommitRecord = { id: nanoid(), repoId, message, parentIds, timestamp: Date.now() };
|
||||||
|
await put("commits", c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRepo(repoId: string): Promise<RepoRecord | undefined> {
|
||||||
|
return get<RepoRecord>("repos", repoId);
|
||||||
|
}
|
||||||
7
website/src/website/switch/uid.ts
Normal file
7
website/src/website/switch/uid.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export function nanoid(size = 21): string {
|
||||||
|
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-";
|
||||||
|
let id = "";
|
||||||
|
const bytes = crypto.getRandomValues(new Uint8Array(size));
|
||||||
|
for (let i = 0; i < size; i++) id += alphabet[bytes[i] & 63];
|
||||||
|
return id;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue