Since .lit
already uses testing/LightningFS for the local filesystem we can easily use https://isomorphic-git.org/docs/en/quickstart↗ to manage versioning...
Table of Contents
Initial plan
The initial plan is to just auto commit on all actions, enabling "infinite" undo.
Thereafter that may remain the default but ideally an ergonomic version of the raw git api can be exposed for more advanced users.
Implementation
// initially, because it's on every change
// a commit will mostly be for a single
// file at a time the immediate exception
// being when a file with output files
// is edited, in which case the commit
// includes those files.
export const fn = async () => {
const now = (new Date()).toISOString()
const fs = lit.lfs
const dir = lit.location.root
const git = lit.git
const FILE = 0, WORKDIR = 2, STAGE = 3
const unstaged = row => {
return row[WORKDIR] !== row[STAGE]
}
// get/list unstaged files
const status = await git.statusMatrix({ fs,dir})
const files = status
.filter( unstaged )
.map(row => row[FILE])
// stage everything
await git.add({fs, dir, filepath: '.'})
// message
const message = `Commit ${lit.location.src}
at ${now} includes the following ${files.length} files:
${files.map(f=> "- " + f).join('\n')}`
// return message
// commit
const sha = await git.commit({fs, dir,
message,
author: {
name: 'dotlitbot',
email: 'bot@dotlit.org'
}
})
return `Committed ${sha.slice(0,6)}
${message}`
}
Committed d738da
Auto commit testing/isomorphic_git.lit
at: 2021-04-26T08:25:54.893Z
includes the following 2 files:
- testing/isomorphic_git.lit
- utils/git-commit-all.js
Investigating API
return lit.fs
.readFile('/.git/HEAD', {
encoding: 'utf8'
})
return lit.fs
.readFile('/.git/refs/heads/master', {
encoding: 'utf8'
})
return (async () => {
return await lit.git.init({
fs: lit.lfs,
dir: lit.location.root
})
})()
Status
return (async () => {
return lit.location.src + " : " + await lit.git.status({
fs: lit.lfs,
dir: lit.location.root,
filepath: '.'
})
})()
return (async () => {
return await lit.git.statusMatrix({
fs: lit.lfs,
dir: lit.location.root,
filepaths: ['testing/']
})
})()
const fs = lit.lfs
const dir = lit.location.root
const git = lit.git
const FILE = 0, WORKDIR = 2, STAGE = 3
// list files with unstaged changes
return (async () => {
const filenames = (await git.statusMatrix({ fs,dir}))
.filter(row => row[WORKDIR] !== row[STAGE])
.map(row => row[FILE])
return filenames
})()
Add
const fs = lit.lfs
const dir = lit.location.root
return (async ()=> {
return await lit.git.add({
fs,
dir,
filepath: '.'
})
})()
Commit
const fs = lit.lfs
const dir = lit.location.root
return (async ()=> {
const now = (new Date()).toISOString()
let sha = await lit.git.commit({
fs,
dir,
message: `Auto commit (${now})`,
author: {
name: 'dotlit',
email: 'bit@dotlit.org'
}
})
console.log(sha)
})()
Log
const indentLines = str => str.split('\n').map( line => ` ${line}`).join('\n')
return (async ()=> {
let commits = await lit.git.log({
fs: lit.lfs,
dir: lit.location.root,
depth: 10
})
return "**Log**\n" + commits.map( x => `1. **\`${x.oid.slice(0,6)}\`**
${indentLines(x.commit.message)}`).join('\n')
})()
Log
-
c87e49
Commit /testing/isomorphic_git.lit
at 2021-06-03T13:14:34.272Z includes the following 1 files:
- testing/isomorphic_git.lit
-
46ceec
Auto commit (2021-06-03T13:10:44.622Z)
Diff
const {git, lfs} = lit
return [git.walk, git.TREE]
return lit.git
const commitHash1 = '0c40ed746ebe53cf744d78191d0bbc2941537280'
const commitHash2 = 'b081f51cd27f54cf58915512006838d4eb67716b'
const git = lit.git
return git.walk({
lit.lfs,
lit.location.root,
trees: [git.TREE({ ref: commitHash1 }), git.TREE({ ref: commitHash2 })],
map: async function(filepath, [A, B]) {
 return filepath
})
)}
Http client
Requires a CORS Proxy!
export const proxy = async (returnEndpoint) => {
const setup = (resolve, reject) => {
if (typeof lit === "undefined") reject("No lit");
else if (!window.__runkitCORSProxyEnpoint) {
(async (fn) => {
const rkEmbed = document.createElement("script");
rkEmbed.onload = async (fn) => {
const el = document.createElement("div");
document.body.appendChild(el);
el.setAttribute("style", "height:0;");
RunKit.createNotebook({
element: el,
mode: "endpoint",
onLoad: async (rk) => {
const endpoint = await rk.getEndpointURL();
window.__runkitCORSProxyEnpoint = endpoint;
document.body.removeChild(el);
},
evaluateOnLoad: true,
source: await lit.fs.readFile(
"/testing/runkit-express-cors-proxy.js",
{
encoding: "utf8",
}
),
});
};
rkEmbed.setAttribute("src", "https://embed.runkit.com");
document.body.appendChild(rkEmbed);
})();
} else {
resolve(window.__runkitCORSProxyEnpoint);
}
};
const endpoint = await new Promise(setup).then((e) => e);
if (false && !window.__runkitCORSProxyEnpoint) {
return "Still setting up proxy endpoint";
} else {
if (returnEndpoint) return endpoint;
const getAndReplaceDomain = (originalUrl, newDomain) => {
return newDomain + originalUrl.replace(/^https?:\/\//, "/");
};
const proxyFetch = async (url, opts = {}) => {
const proxyUrl = getAndReplaceDomain(url, endpoint);
return fetch(proxyUrl, opts);
};
return proxyFetch;
}
};
return (async (fn) => {
const http = await import(
"https://unpkg.com/isomorphic-git/http/web/index.js"
);
return lit.git.getRemoteInfo({
http: http.default,
corsProxy: await lit.file.data.plugins.proxy.corsProxy(true),
url: "https://github.com/dotlitdev/dotlit",
});
})();
Web worker
<div>
<input
id="repository"
type="text"
style="width: 50em"
title="Tip: enter a private repo URL to see the credentialManager plugin prompt for a password.",
value="https://github.com/isomorphic-git/isomorphic-git",
/>
<button type="button" id="cloneButton">Clone</button>
</div>
<div>
<progress id="progress" value="0"></progress>
<span id="progress-txt" style="font-family: monospace;"></span>
</div>
<output id="log" style="white-space: pre; font-family: monospace;"></output>
<script src="https://unpkg.com/magic-portal"></script>
<script>
// alert("Running")
const $ = id => document.getElementById(id);
let worker = new Worker("gitworker.js");
const portal = new MagicPortal(worker);
worker.addEventListener("message", ({ data }) => console.log(data));
const mainThread = {
async print(message) {
let text = $("log").textContent;
if (message.endsWith("\r")) {
// overwrite last line
text = text.trim().replace(/.+$/, "");
}
text += message + "\n";
$("log").textContent = text;
},
async progress(evt) {
$("progress-txt").textContent = evt.phase;
$("progress").value = evt.total ? evt.loaded / evt.total : 0.5;
return;
},
async fill(url) {
let username = window.prompt("Username:");
let password = window.prompt("Password:");
return { username, password };
},
async rejected({ url, auth }) {
window.alert("Authentication rejected");
return;
}
};
portal.set("mainThread", mainThread, {
void: ["print", "progress", "rejected"]
});
https://unpkg.com/magic-portal
return new Promise((resolve,reject) => {
let myWorker;
try {
myWorker = new Worker('gitworker.js')
myWorker.onmessage = (ev) => {
if (ev.data === 'done') resolve(ev.data)
else console.log(ev.data)
}
myWorker.onerror = (err) => {
resolve({msg: "worker.onerror: " + err.message + " (" + err.filename + ":" + err.lineno + ")", err, err},)
}
} catch(err) {
resolve({msg: "Caught err", err})
}
})
// alert("Running")
const $ = id => document.getElementById(id);
let worker = new Worker("gitworker.js");
const portal = new MagicPortal(worker);
worker.addEventListener("message", ({ data }) => console.log(data));
const mainThread = {
async print(message) {
console.log(message)
},
async progress(evt) {
console.log(evt.phase, evt.total ? evt.loaded / evt.total : 0.5)
return;
},
async fill(url) {
let username = window.prompt("Username:");
let password = window.prompt("Password:");
return { username, password };
},
async rejected({ url, auth }) {
window.alert("Authentication rejected");
return;
},
async failure({message}) {
alert("Failure: ", message)
return
},
}
portal.set("mainThread", mainThread, {
void: ["print", "progress", "rejected", "failure"]
});
async function doCloneAndStuff() {
console.log("CLONE");
await workerThread.setDir("/testing");
await workerThread.clone({
corsProxy: "https://cors.isomorphic-git.org",
url: "https://GitHub.com/dotlitdev/dotlit"
});
console.log("CLONED!!")
let branches = await workerThread.listBranches({ remote: "origin" });
console.log("BRANCHES:\n" + branches.map(b => ` ${b}`).join("\n"))
let files = await workerThread.listFiles({});
console.log("FILES:\n" + files.map(b => ` ${b}`).join("\n"))
let commits = await workerThread.log({});
console.log("LOG:\n" +
commits
.map(c => ` ${c.oid.slice(0, 7)}: ${c.commit.message}`)
.join("\n"))
}
return (async () => {
const workerThread = await portal.get("workerThread");
window.workerThread = workerThread
window.worker = worker
console.log(workerThread)
console.log("ready")
await doCloneAndStuff()
})();
return workerThread.log({})