A note-taking app inspired by nb
at main 101 lines 3.2 kB view raw
1import $ from "@david/dax"; 2import { existsSync } from "@std/fs/exists"; 3import { join } from "@std/path"; 4 5import { Notebook } from "./notebook.ts"; 6 7export function hasGit(notebook: Notebook): boolean { 8 return existsSync(join(notebook.root, ".git"), { isDirectory: true }); 9} 10 11export async function hasRemote(notebook: Notebook): Promise<boolean> { 12 if (!hasGit(notebook)) return false; 13 const remotes = await $`git remote`.cwd(notebook.root).text(); 14 return remotes.trim() !== ""; 15} 16 17export async function commit(notebook: Notebook, message: string) { 18 if (!hasGit(notebook)) return; 19 const changed = await $`git status --porcelain`.cwd(notebook.root).text(); 20 if (changed.trim() === "") return; 21 await $`git add .`.cwd(notebook.root); 22 await $`git commit -am ${message}`.cwd(notebook.root); 23} 24 25export async function initRepo(path: string) { 26 await $`git init ${path}`; 27} 28 29function lastSyncTime(notebook: Notebook): Temporal.Instant | undefined { 30 try { 31 return Deno.statSync(join(notebook.root, ".git", "FETCH_HEAD")).mtime 32 ?.toTemporalInstant(); 33 } catch { 34 return; 35 } 36} 37 38export async function syncNotebook(notebook: Notebook, force: boolean = false) { 39 if (!hasGit(notebook)) return; 40 41 const root = notebook.root; 42 const status = await $`git status --porcelain`.cwd(root).text(); 43 if (status.trim() !== "") { 44 await commit(notebook, "Checkpointing untracked changes"); 45 } 46 47 if (!await hasRemote(notebook)) return; 48 49 const last = lastSyncTime(notebook); 50 // FIXME(bmps): Make time configurable 51 if ( 52 force || 53 (last != null && 54 (Temporal.Now.instant().since(last).total("minutes") >= 30)) 55 ) { 56 await $`git pull --rebase`.cwd(root); 57 await $`git push`.cwd(root); 58 } 59} 60 61export async function cloneNotebook(url: string, notebook: Notebook) { 62 await $`git clone ${url} ${notebook.root}`; 63} 64 65export async function creationDateFor( 66 root: string, 67 relpath: string, 68): Promise<Temporal.Instant> { 69 const fullPath = join(root, relpath); 70 if (!existsSync(join(root, ".git"), { isDirectory: true })) { 71 const stat = Deno.statSync(fullPath); 72 return (stat.birthtime ?? stat.mtime ?? new Date()).toTemporalInstant(); 73 } 74 const out = 75 (await $`git --no-pager log --reverse --date=iso-local --format="%aI" -- ${relpath}` 76 .cwd(root).env({ "TZ": "UTC" }).text()).split("\n")[0]; 77 if (!out) { 78 const stat = Deno.statSync(fullPath); 79 return (stat.birthtime ?? stat.mtime ?? new Date()).toTemporalInstant(); 80 } 81 return Temporal.Instant.from(out); 82} 83 84export async function modificationDateFor( 85 root: string, 86 relpath: string, 87): Promise<Temporal.Instant> { 88 const fullPath = join(root, relpath); 89 if (!existsSync(join(root, ".git"), { isDirectory: true })) { 90 const stat = Deno.statSync(fullPath); 91 return (stat.mtime ?? new Date()).toTemporalInstant(); 92 } 93 const out = 94 (await $`git --no-pager log --date=iso-local --format="%aI" -- ${relpath}` 95 .cwd(root).env({ "TZ": "UTC" }).text()).split("\n")[0]; 96 if (!out) { 97 const stat = Deno.statSync(fullPath); 98 return (stat.mtime ?? new Date()).toTemporalInstant(); 99 } 100 return Temporal.Instant.from(out); 101}