A note-taking app inspired by nb
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}