git-cat-file
Version:
A pure-JavaScript implementation of `git cat-file -p` for Node.js.
176 lines (142 loc) • 5.33 kB
text/typescript
/**
* https://github.com/kawanet/git-cat-file
*/
import {promises as fs} from "fs"
import type {GCF} from "../types/git-cat-file.d.ts"
import {shortCache} from "./cache.ts"
import {Commit} from "./commit.ts"
import type {ObjStore} from "./obj-store.ts"
import {Tag} from "./tag.ts"
interface RefCommit {
ref: string
commit: string
}
const isObjectId = (oid: string) => (oid && /^[0-9a-f]{40}$/i.test(oid))
export class Ref {
protected readonly root: string
constructor(root: string) {
this.root = root
}
async findCommitId(revision: string, store: ObjStore): Promise<string> {
if (!revision || !/[^.\/]/.test(revision)) {
throw new Error(`Invalid revision: ${revision}`)
}
let ancestry: string[]
revision = revision.replace(/([~^]\d*)+$/, match => {
ancestry = match.split(/([~^]\d*)/).filter(v => v)
return ""
})
if (revision === "@") {
revision = "HEAD"
}
const id = await this.findId(revision)
if (id && !ancestry) return id
const obj = await this.getRawCommit(id || revision, store)
if (!obj) return
if (obj && !ancestry) return obj.oid
// https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection#_ancestry_references
// HEAD^
// HEAD~2
let commit: GCF.Commit = new Commit(obj, store)
for (const gen of ancestry) {
const mark = gen[0]
const num = gen.substring(1)
const count = (num === "0") ? 0 : (+num || 1)
const tilde = (mark === "~") ? count : 1
const caret = (mark === "^") ? count : 1
for (let i = 0; i < tilde; i++) {
const parents = await commit.getParents()
if (!parents) return
if (caret) commit = parents[caret - 1]
if (!commit) return
}
}
return commit.getId()
}
private async findId(revision: string): Promise<string> {
while (revision) {
revision = await this.searchRef(revision)
if (!revision) break
const ref = revision.replace(/^ref:\s*/, "")
if (/^refs\//.test(ref)) {
revision = await this.findRef(ref)
}
if (isObjectId(revision)) {
return revision // commit
}
}
}
private async getRawCommit(revision: string, store: ObjStore): Promise<GCF.IObject> {
const object_id = await store.findObjectId(revision)
if (!object_id) return // not found
let obj = await store.getObject(object_id)
if (obj?.type === "tag") {
const tag = new Tag(obj, store)
const object_id = tag.getMeta("object")
obj = await store.getObject(object_id)
}
if (obj.type === "commit") return obj
}
private readPackedRefIndex = shortCache(async () => {
const index: {[ref: string]: string} = {}
const list = await this.readPackedRefList()
for (const ref of list) {
index[ref.ref] = ref.commit
}
return index
})
private async readPackedRefList(): Promise<RefCommit[]> {
const list: RefCommit[] = []
const text: string = await this.readTextFile(`packed-refs`).catch((): null => null)
if (!text) return list
const lines = text.split(/\r?\n/).filter(s => /^\w/.test(s))
for (const line of lines) {
const [commit, ref] = splitBySpace(line)
list.push({commit, ref})
}
return list
}
private async searchRef(ref: string): Promise<string> {
// .git/HEAD
if (/HEAD$/.test(ref)) {
const commit = await this.readFirstLine(ref).catch((): null => null)
if (commit) return commit
}
// .git/refs/heads/main
{
const commit = await this.findRef(`refs/heads/${ref}`).catch((): null => null)
if (commit) return commit
}
// .git/refs/tags/xxxx
{
const commit = await this.findRef(`refs/tags/${ref}`).catch((): null => null)
if (commit) return commit
}
// .git/refs/remotes/xxxx
{
const commit = await this.findRef(`refs/remotes/${ref}`).catch((): null => null)
if (commit) return commit
}
}
private async findRef(name: string): Promise<string> {
// loose ref
const ref = await this.readFirstLine(name)
if (ref) return ref
// packed ref
const index = await this.readPackedRefIndex()
if (index[name]) return index[name]
}
private async readFirstLine(name: string): Promise<string> {
const text: string = await this.readTextFile(name).catch((): null => null)
if (text) return text.split(/\r?\n/).filter(s => /^[^#\s]/.test(s)).shift()
}
private readTextFile = shortCache((name: string): Promise<string> => {
const path = `${this.root}/${name}`
// console.warn(`readFile: ${path}`);
return fs.readFile(path, "utf-8")
})
}
function splitBySpace(line: string): string[] {
const sp = line.indexOf(" ")
return [line.slice(0, sp), line.slice(sp + 1)]
}