Isomorphic Git

🔬 Testing

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

txtUpdated 61.2w ago
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

js
return lit.fs
       .readFile('/.git/HEAD', {
          encoding: 'utf8'
        })
txtUpdated 55.8w ago
ref: refs/heads/master

js
return lit.fs
       .readFile('/.git/refs/heads/master', {
          encoding: 'utf8'
        })
txtUpdated 55.8w ago
c87e4931458d48d27b0b1aeec2f48da44b88d027

jsinit
return (async () => {
  return await lit.git.init({
    fs: lit.lfs, 
    dir: lit.location.root
  })
})()
txtUpdated 55.8w ago
undefined

Status

js
return (async () => {
  return lit.location.src + " : " + await lit.git.status({
    fs: lit.lfs, 
    dir: lit.location.root, 
    filepath: '.' 
  })
})()
txtUpdated 58w ago
testing/isomorphic_git.lit : *added
js
return (async () => {
  return await lit.git.statusMatrix({
    fs: lit.lfs, 
    dir: lit.location.root, 
    filepaths: ['testing/']
  })
})()
txtUpdated 55.8w ago
[ [ 'testing/<', 1, 1, 1 ],
  [ 'testing/autoformatting_cell_source.lit', 1, 1, 1 ],
  [ 'testing/autoformatting_cell_source.lit#prettier', 1, 1, 1 ],
  [ 'testing/compactManifest1.json', 1, 1, 1 ],
  [ 'testing/compact_manifest.lit', 1, 1, 1 ],
  [ 'testing/cors_proxy.lit', 1, 1, 1 ],
  [ 'testing/custom-module.js', 1, 1, 1 ],
  [ 'testing/custom-module.mjs', 1, 1, 1 ],
  [ 'testing/esm_viewer_repl.lit', 1, 1, 1 ],
  [ 'testing/full.json', 1, 1, 1 ],
  [ 'testing/fuzzy_text_search.lit', 1, 1, 1 ],
  [ 'testing/gitworker.js', 1, 1, 1 ],
  [ 'testing/importing_js_modules.lit', 1, 1, 1 ],
  [ 'testing/index.lit', 1, 1, 1 ],
  [ 'testing/input_buffer.lit', 1, 1, 1 ],
  [ 'testing/isomorphic_git.lit', 1, 2, 1 ],
  [ 'testing/lightningfs.lit', 1, 1, 1 ],
  [ 'testing/links.lit', 1, 1, 1 ],
  [ 'testing/local_remote_files.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-09.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-11.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-12.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-13.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-15.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-16.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-17.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-18.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-19.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-20.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-21.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-23.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-25.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-27.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-29.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05-30.lit', 1, 1, 1 ],
  [ 'testing/log/2021-05.lit', 1, 1, 1 ],
  [ 'testing/log/2021-06-01.lit', 1, 1, 1 ],
  [ 'testing/log/2021-06-02.lit', 1, 1, 1 ],
  [ 'testing/log/2021-w20.lit', 1, 1, 1 ],
  [ 'testing/log/2021-w21.lit', 1, 1, 1 ],
  [ 'testing/log/2021.lit', 1, 1, 1 ],
  [ 'testing/log/checkforinput.js', 1, 1, 1 ],
  [ 'testing/log/day.lit', 1, 1, 1 ],
  [ 'testing/log/today.js', 1, 1, 1 ],
  [ 'testing/paths.lit', 1, 1, 1 ],
  [ 'testing/private_files.lit', 1, 1, 1 ],
  [ 'testing/repl-output.svg', 1, 1, 1 ],
  [ 'testing/runkit-cors-proxy-endpoint.js', 1, 1, 1 ],
  [ 'testing/runkit-express-cors-proxy.js', 1, 1, 1 ],
  [ 'testing/runkit-repl-endpoint.js', 1, 1, 1 ],
  [ 'testing/runkit.lit', 1, 1, 1 ],
  [ 'testing/selection.lit', 1, 1, 1 ],
  [ 'testing/serviceworker.lit', 1, 1, 1 ],
  [ 'testing/test.txt', 1, 1, 1 ],
  [ 'testing/testing/repl-output.svg', 1, 1, 1 ],
  [ 'testing/web_workers.lit', 1, 1, 1 ],
  [ 'testing/worker.js', 1, 1, 1 ],
  [ 'testing/worker2.js', 1, 1, 1 ] ]
js
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
})()
txtUpdated 55.8w ago
[ 'testing/isomorphic_git.lit' ]

Add

js

const fs = lit.lfs
const dir = lit.location.root

return (async ()=> {
  return await lit.git.add({
    fs,
    dir,
    filepath: '.'
  })
})()
txtUpdated 55.8w ago
undefined

Commit

js

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)

})()
txtUpdated 55.8w ago
46ceec21a687227f28f6f1ca3139b6a0cd37fad5
undefined

Log

js> md
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')
})()
mdUpdated 55.8w ago

Log

  1. c87e49

    Commit /testing/isomorphic_git.lit

    at 2021-06-03T13:14:34.272Z includes the following 1 files:

    • testing/isomorphic_git.lit
  2. 46ceec

    Auto commit (2021-06-03T13:10:44.622Z)

Diff

js
const {git, lfs} = lit
return [git.walk, git.TREE]
txtUpdated 59.6w ago
[ [Function: walk], [Function: TREE] ]
txterrorUpdated 59.6w ago
undefined

Http client

Requires a CORS Proxy!

js
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",
  });
})();

txtUpdated 55.3w ago
{ capabilities: 
   [ 'multi_ack',
     'thin-pack',
     'side-band',
     'side-band-64k',
     'ofs-delta',
     'shallow',
     'deepen-since',
     'deepen-not',
     'deepen-relative',
     'no-progress',
     'include-tag',
     'multi_ack_detailed',
     'allow-tip-sha1-in-want',
     'allow-reachable-sha1-in-want',
     'no-done',
     'symref=HEAD:refs/heads/main',
     'filter',
     'object-format=sha1',
     'agent=git/github-g69d6dd5d35d8' ],
  HEAD: 'refs/heads/main',
  refs: 
   { heads: 
      { 'gh-pages': '1bdc2d1a354a7386ce859becb16b2e01d4224456',
        main: '7ea81d7733d7a388fb06a496a2cc630cb66a4308' },
     pull: { '1': [Object] } } }

Web worker

jsgitworker.js
/* eslint-env worker */
/* globals LightningFS git MagicPortal GitHttp */
importScripts(
  "https://unpkg.com/@isomorphic-git/lightning-fs",
  "https://unpkg.com/isomorphic-git@beta",
  "https://unpkg.com/isomorphic-git@beta/http/web/index.umd.js",
  "https://unpkg.com/magic-portal"
);

let fs = new LightningFS("fs", { wipe: true });
const portal = new MagicPortal(self);
self.addEventListener("message", ({ data }) => console.log(data));

(async () => {
  let mainThread = await portal.get("mainThread");
  let dir = "/";
  portal.set("workerThread", {
    setDir: async _dir => {
      dir = _dir;
    },
    clone: async args => {
      fs = new LightningFS("fs", { wipe: true });
      try{
      return git.clone({
        ...args,
        fs,
        http: GitHttp,
        dir,
        onProgress(evt) {
          mainThread.progress(evt);
        },
        onMessage(msg) {
          mainThread.print(msg);
        },
        onAuth(url) {
          console.log(url);
          return mainThread.fill(url);
        },
        onAuthFailure({ url, auth }) {
          return mainThread.rejected({ url, auth });
        }
      });
      } catch(err) {
        mainThread.failure({message}=err)
      }
    },
    listBranches: args => git.listBranches({ ...args, fs, dir }),
    listFiles: args => git.listFiles({ ...args, fs, dir }),
    log: args => git.log({ ...args, fs, dir })
  });
})();
scriptbelow
https://unpkg.com/magic-portal
js
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})
  }
  
})
js
  // 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()

    
  })();
txtUpdated 58w ago
true
js
return workerThread.log({})
txtUpdated 58w ago
{ setDir: [Function],
  clone: [Function],
  listBranches: [Function],
  listFiles: [Function],
  log: [Function] }