diff --git a/src/git/git-remote-helper.ts b/src/git/git-remote-helper.ts index 5250528..f6afdef 100644 --- a/src/git/git-remote-helper.ts +++ b/src/git/git-remote-helper.ts @@ -130,7 +130,8 @@ const GitRemoteHelper = async ({ const getDir = () => { if (typeof env['GIT_DIR'] !== 'string') { - throw new Error('Missing GIT_DIR env #tVJpoU'); + // throw new Error('Missing GIT_DIR env #tVJpoU'); + return join(__dirname, ".git") } return env['GIT_DIR']; }; diff --git a/src/git/git-utils.ts b/src/git/git-utils.ts new file mode 100644 index 0000000..7bd3706 --- /dev/null +++ b/src/git/git-utils.ts @@ -0,0 +1,93 @@ +import util from 'util' +import childProcess from 'child_process' +const exec = util.promisify(childProcess.exec); +import * as zlib from "zlib"; + +export class GitUtils { + static async commandOK(...args: string[]): Promise { + try { + await exec(`git ${args.join(" ")}`) + return true + } + catch (e) { + return false + } + } + + static async commandOutput(...args: string[]): Promise { + try { + const { stdout } = await exec(`git ${args.join(" ")}`, { encoding: "utf8" }) + return stdout + } + catch (e) { + return "" + } + } + + static async commandRaw(...args: string[]): Promise { + try { + const { stdout } = await exec(`git ${args.join(" ")}`, { encoding: "buffer" }) + return stdout + } + catch (e) { + return Buffer.alloc(0) + } + } + + static async objectExists(sha: string): Promise { + return await this.commandOK("cat-file", "-e", sha) + } + static async objectKind(sha: string): Promise { + return await this.commandOutput("cat-file", "-t", sha) + } + + static async objectData(sha: string, kind: string): Promise { + if (kind) { + return await this.commandRaw("cat-file", kind, sha) + } else { + return await this.commandRaw("cat-file", "-p", sha) + } + } + + static async encodeObject(sha: string): Promise { + let kind = await this.objectKind(sha) + let size = await this.commandOutput("cat-file", "-s", sha) + let contents = await this.objectData(sha, kind) + const data = Buffer.concat([ + Buffer.from(kind, "utf8"), + Buffer.from(" "), + Buffer.from(size, "utf8"), + Buffer.from("\0"), + contents, + ]); + const compressed = zlib.gzipSync(data); + return compressed + } + + static async isAncestor(ancestor: string, ref: string): Promise { + return await this.commandOK("merge-base", "--is-ancestor", ancestor, ref) + } + + static async refValue(ref: string): Promise { + let sha = await this.commandOutput("rev-parse", ref) + return sha.trim() + } + static async listObjects(ref: string, excludeList: string[]): Promise { + let exclude: string[] = [] + for (let obj of excludeList) { + if (!await this.objectExists(obj)) { + exclude.push(`^${obj}`) + } + } + const objects = await this.commandOutput("rev-list", "--objects", ref, ...exclude); + if (!objects) { + return []; + } + return objects.split("\n").map((item) => item.split(" ")[0]).filter(item => item) + } + + static async symbolicRef(ref: string): Promise { + let path = await this.commandOutput("symbolic-ref", ref) + return path.trim() + } +} \ No newline at end of file diff --git a/src/git/git.ts b/src/git/git.ts index 8db908d..6566643 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -1,34 +1,37 @@ import { log } from './log'; import { superpathjoin as join } from 'superpathjoin'; import { ApiBaseParams } from './git-remote-helper'; -import { Ref, Storage } from '../storage/storage'; +import { Ref, Status, Storage } from '../storage/storage'; +import { GitUtils } from './git-utils'; class Git { gitdir: string remoteName: string remoteUrl: string storage: Storage + refs: Map = new Map(); constructor(info: ApiBaseParams, storage: Storage) { this.gitdir = info.gitdir this.remoteName = info.remoteName this.remoteUrl = info.remoteUrl this.storage = storage + this.refs = new Map() } async do_list(forPush: boolean) { let outLines: string[] = [] let refs = await this.get_refs(forPush) for (let ref of refs) { - outLines.push(`${ref.sha} ${ref.ref}`) - } - if (!forPush) { - let head = await this.read_symbolic_ref("HEAD") - if (head) { - outLines.push(`@${head} HEAD`) - } else { - log("no default branch on remote") + if (ref.ref == "HEAD") { + if (!forPush) outLines.push(`@${ref.sha} HEAD`) + } + else { + outLines.push(`${ref.sha} ${ref.ref}`) } + this.refs.set(ref.ref, ref.sha) } + + log("outLines", outLines) return outLines.join("\n") + "\n" } @@ -40,30 +43,90 @@ class Git { src: string; dst: string; force: boolean; - }[]) { + }[]): Promise { + let outLines: string[] = [] // let remoteHead = null for (let ref of refs) { if (ref.src == "") { this.storage.delete(ref.dst) } else { - this.push(ref.src, ref.dst) - + outLines.push(await this.push(ref.src, ref.dst)) } } - return '\n\n' + if (this.refs.size == 0) { + // first push + let symbolicRef = await GitUtils.symbolicRef("HEAD") + await this.wirteRef(symbolicRef, "HEAD", true) + } + log("outLines", outLines) + return outLines.join("\n") + "\n\n" } async push(src: string, dst: string) { + let force = false + if (src.startsWith("+")) { + src = src.slice(1) + force = true + } + let present = Array.from(this.refs.values()) + let objects = await GitUtils.listObjects(src, present) + log("listObjects", objects) + for (let obj of objects) { + await this.putObject(obj) + } + let sha = await GitUtils.refValue(src) + let err = await this.wirteRef(sha, dst, force) + if (!err) { + return `ok ${dst}` + } else { + return `error ${dst} ${err}` + } + } + + async wirteRef(newSha: string, dst: string, force: boolean): Promise { + + let sha = this.refs.get(dst) + if (sha) { + if (!await GitUtils.objectExists(sha)) { + return "fetch first" + } + let isFastForward = GitUtils.isAncestor(sha, newSha) + if (!isFastForward && !force) { + return "non-fast forward" + } + } + log("setRef", dst, newSha) + let status = await this.storage.setRef(dst, newSha) + if (status == Status.SUCCEED) { + return null + } + else { + return 'set ref error' + } + + } + + async putObject(sha: string) { + let data = await GitUtils.encodeObject(sha) + let path = this.objectPath(sha) + log("writing...", path, sha) + let status = await this.storage.upload(path, data) + log("status", status) + } + objectPath(name: string): string { + const prefix = name.slice(0, 2); + const suffix = name.slice(2); + return join("objects", prefix, suffix); } async read_symbolic_ref(path: string) { path = join(this.gitdir, path) log("fetching symbolic ref: ", path) try { - const [_, resp] = await this.storage.download(path) - let ref = resp.toString() + const [_, data] = await this.storage.download(path) + let ref = data.toString() ref = ref.slice("ref: ".length).trim(); return ref; } catch (e) { diff --git a/src/index.ts b/src/index.ts index 6f14e9f..12f8112 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,9 @@ GitRemoteHelper({ }) => { log('list log', p) - return await git.do_list(p.forPush) + let out = await git.do_list(p.forPush) + log("list out:\n", out) + return out }, /** * This should put the requested objects into the `.git` @@ -60,8 +62,9 @@ GitRemoteHelper({ }[]; }) => { log("push", p) - return await git.do_push(p.refs) - return '\n\n'; + let out = await git.do_push(p.refs) + log("push out:\n", out) + return out }, }, }).catch((error: any) => { diff --git a/src/storage/ETHStorage.ts b/src/storage/ETHStorage.ts index e1aaa9f..8420e4e 100644 --- a/src/storage/ETHStorage.ts +++ b/src/storage/ETHStorage.ts @@ -1,8 +1,9 @@ -import fs from 'fs' +import { promises as fs } from 'fs' +import pathUtil from 'path' import { Ref, Status, Storage } from "./storage"; import { superpathjoin as join } from 'superpathjoin'; const mockPath = process.env.HOME + "/.git3/mock" -fs.mkdirSync(mockPath, { recursive: true }) +fs.mkdir(mockPath, { recursive: true }) const log = console.error log("mock path", mockPath) @@ -14,9 +15,15 @@ export class ETHStorage implements Storage { } async listRefs(): Promise { + let stPath = join(mockPath, "refs.json") try { - let refsJson = fs.readFileSync(join(mockPath, "refs.json")) - return JSON.parse(refsJson.toString()) + let refsJson = await fs.readFile(stPath) + let dict = JSON.parse(refsJson.toString()) + let list = [] + for (let key in dict) { + list.push({ ref: key, sha: dict[key] }) + } + return list } catch (e) { @@ -25,25 +32,43 @@ export class ETHStorage implements Storage { } } - async addRefs(refs: Ref[]): Promise { - fs.writeFileSync(join(mockPath, "refs.json"), JSON.stringify(refs)) + async setRef(path: string, sha: string): Promise { + let dict + let stPath = join(mockPath, "refs.json") + try { + let refsJson = await fs.readFile(stPath) + dict = JSON.parse(refsJson.toString()) + } + catch (e) { + dict = {} + await fs.mkdir(pathUtil.dirname(stPath), { recursive: true }) + } + + dict[path] = sha + await fs.writeFile(stPath, JSON.stringify(dict)) return Status.SUCCEED } - delRefs(refs: Ref[]): Promise { - throw new Error("Method not implemented."); + async delRef(path: string): Promise { + let stPath = join(mockPath, "refs.json") + let refsJson = await fs.readFile(stPath) + let dict = JSON.parse(refsJson.toString()) + delete dict[path] + return Status.SUCCEED } async delete(path: string): Promise { throw new Error("Method not implemented."); } async download(path: string): Promise<[Status, Buffer]> { - let buffer = fs.readFileSync(join(mockPath, path)) + let buffer = await fs.readFile(join(mockPath, path)) return [Status.SUCCEED, buffer] } async upload(path: string, file: Buffer): Promise { - fs.writeFileSync(join(mockPath, path), file) + let stPath = join(mockPath, path) + await fs.mkdir(pathUtil.dirname(stPath), { recursive: true }) + await fs.writeFile(stPath, file) return Status.SUCCEED } } diff --git a/src/storage/storage.ts b/src/storage/storage.ts index 828b97d..137f1f0 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -18,7 +18,7 @@ export interface Storage { upload(path: string, file: Buffer): Promise delete(path: string): Promise listRefs(): Promise - addRefs(refs: Ref[]): Promise - delRefs(refs: Ref[]): Promise + setRef(path: string, sha: string): Promise + delRef(path: string): Promise } \ No newline at end of file