master
cyhhao 2 years ago
parent bb36aa8dbd
commit e3e7e83ac9

@ -1,22 +1,19 @@
import util from 'util' import childProcess, { spawnSync } from 'child_process'
import childProcess from 'child_process'
const exec = util.promisify(childProcess.exec);
import * as zlib from "zlib"; import * as zlib from "zlib";
export class GitUtils { export class GitUtils {
static async commandOK(...args: string[]): Promise<boolean> { static EMPTY_TREE_HASH: string = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
try {
await exec(`git ${args.join(" ")}`) static commandOK(...args: string[]): boolean {
return true let res = childProcess.spawnSync("git", args, { encoding: "utf8" })
} return res.status == 0
catch (e) {
return false
}
} }
static async commandOutput(...args: string[]): Promise<string> { static commandOutput(...args: string[]): string {
try { try {
const { stdout } = await exec(`git ${args.join(" ")}`, { encoding: "utf8" }) // const { stdout } = exec(`git ${args.join(" ")}`, { encoding: "utf8" })
const { stdout } = spawnSync("git", args, { encoding: "utf8" })
return stdout return stdout
} }
catch (e) { catch (e) {
@ -24,9 +21,9 @@ export class GitUtils {
} }
} }
static async commandRaw(...args: string[]): Promise<Buffer> { static commandRaw(...args: string[]): Buffer {
try { try {
const { stdout } = await exec(`git ${args.join(" ")}`, { encoding: "buffer" }) const { stdout } = spawnSync("git", args, { encoding: "buffer" })
return stdout return stdout
} }
catch (e) { catch (e) {
@ -34,25 +31,25 @@ export class GitUtils {
} }
} }
static async objectExists(sha: string): Promise<boolean> { static objectExists(sha: string): boolean {
return await this.commandOK("cat-file", "-e", sha) return this.commandOK("cat-file", "-e", sha)
} }
static async objectKind(sha: string): Promise<string> { static objectKind(sha: string): string {
return await this.commandOutput("cat-file", "-t", sha) return this.commandOutput("cat-file", "-t", sha)
} }
static async objectData(sha: string, kind: string): Promise<Buffer> { static objectData(sha: string, kind: string | null = null): Buffer {
if (kind) { if (kind) {
return await this.commandRaw("cat-file", kind, sha) return this.commandRaw("cat-file", kind, sha)
} else { } else {
return await this.commandRaw("cat-file", "-p", sha) return this.commandRaw("cat-file", "-p", sha)
} }
} }
static async encodeObject(sha: string): Promise<Buffer> { static encodeObject(sha: string): Buffer {
let kind = await this.objectKind(sha) let kind = this.objectKind(sha)
let size = await this.commandOutput("cat-file", "-s", sha) let size = this.commandOutput("cat-file", "-s", sha)
let contents = await this.objectData(sha, kind) let contents = this.objectData(sha, kind)
const data = Buffer.concat([ const data = Buffer.concat([
Buffer.from(kind, "utf8"), Buffer.from(kind, "utf8"),
Buffer.from(" "), Buffer.from(" "),
@ -64,30 +61,87 @@ export class GitUtils {
return compressed return compressed
} }
static async isAncestor(ancestor: string, ref: string): Promise<boolean> { static isAncestor(ancestor: string, ref: string): boolean {
return await this.commandOK("merge-base", "--is-ancestor", ancestor, ref) return this.commandOK("merge-base", "--is-ancestor", ancestor, ref)
} }
static async refValue(ref: string): Promise<string> { static refValue(ref: string): string {
let sha = await this.commandOutput("rev-parse", ref) let sha = this.commandOutput("rev-parse", ref)
return sha.trim() return sha.trim()
} }
static async listObjects(ref: string, excludeList: string[]): Promise<string[]> { static listObjects(ref: string, excludeList: string[]): string[] {
let exclude: string[] = [] let exclude: string[] = []
for (let obj of excludeList) { for (let obj of excludeList) {
if (!await this.objectExists(obj)) { if (this.objectExists(obj)) {
exclude.push(`^${obj}`) exclude.push(`^${obj}`)
} }
} }
const objects = await this.commandOutput("rev-list", "--objects", ref, ...exclude); const objects = this.commandOutput("rev-list", "--objects", ref, ...exclude);
if (!objects) { if (!objects) {
return []; return [];
} }
return objects.split("\n").map((item) => item.split(" ")[0]).filter(item => item) return objects.split("\n").map((item) => item.split(" ")[0]).filter(item => item)
} }
static async symbolicRef(ref: string): Promise<string> { static symbolicRef(ref: string): string {
let path = await this.commandOutput("symbolic-ref", ref) let path = this.commandOutput("symbolic-ref", ref)
return path.trim() return path.trim()
} }
static writeObject(kind: string, contents: Buffer): string {
let res = spawnSync("git", ["hash-object", "-w", "--stdin", "-t", kind], { input: contents, encoding: "buffer" })
if (res.status != 0) {
throw new Error("Failed to write object")
}
else {
return res.stdout.toString("utf8").trim()
}
}
static historyExists(sha: string): boolean {
return this.commandOK("rev-list", "--objects", sha)
}
static referencedObjects(sha: string): string[] {
let kind = this.objectKind(sha)
if (kind == "blob") {
// blob objects do not reference any other objects
return []
}
let data = this.objectData(sha).toString("utf8").trim()
if (kind == "tag") {
// tag objects reference a single object
let obj = data.split("\n", 1)[0].split(" ")[1]
return [obj]
}
else if (kind == "commit") {
// commit objects reference a tree and zero or more parents
let lines = data.split("\n")
let tree = lines[0].split(" ")[1]
let objs = [tree]
for (let line of lines) {
if (line.startsWith("parent ")) {
objs.push(line.split(" ")[1])
}
else {
break
}
}
return objs
}
else if (kind == "tree") {
// tree objects reference zero or more trees and blobs, or submodules
if (!data) {
// empty tree
return []
}
let lines = data.split("\n")
// submodules have the mode '160000' and the kind 'commit', we filter them out because
// there is nothing to download and this causes errors
return lines.filter(line => !line.startsWith("160000 commit ")).map(line => line.split(" ")[2])
}
else {
throw new Error(`unexpected git object type: ${kind}`)
}
}
} }

@ -9,6 +9,7 @@ class Git {
remoteUrl: string remoteUrl: string
storage: Storage storage: Storage
refs: Map<string, string> = new Map(); refs: Map<string, string> = new Map();
pushed: Map<string, string> = new Map();
constructor(info: ApiBaseParams, storage: Storage) { constructor(info: ApiBaseParams, storage: Storage) {
this.gitdir = info.gitdir this.gitdir = info.gitdir
@ -16,6 +17,7 @@ class Git {
this.remoteUrl = info.remoteUrl this.remoteUrl = info.remoteUrl
this.storage = storage this.storage = storage
this.refs = new Map() this.refs = new Map()
this.pushed = new Map()
} }
async do_list(forPush: boolean) { async do_list(forPush: boolean) {
@ -36,8 +38,41 @@ class Git {
} }
async do_fetch(refs: { ref: string; oid: string }[]) { async do_fetch(refs: { ref: string; oid: string }[]) {
for (let ref of refs) {
await this.fetch(ref.oid)
}
}
async fetch(oid: string) {
let downloaded = new Set<string>()
let pending = new Set<string>()
let queue = [oid]
while (queue.length > 0 || pending.size > 0) {
if (queue.length > 0) {
let sha = queue.pop() || ""
if (downloaded.has(sha) || pending.has(sha)) continue
if (GitUtils.objectExists(sha)) {
if (sha == GitUtils.EMPTY_TREE_HASH) {
GitUtils.writeObject("tree", Buffer.from(""))
}
if (!GitUtils.historyExists(sha)) {
log("missing part of history from", sha)
queue.push(...GitUtils.referencedObjects(sha))
}
else {
log("already downloaded", sha)
}
}
else {
pending.add(sha)
}
}
else {
} }
}
}
async do_push(refs: { async do_push(refs: {
src: string; src: string;
@ -48,14 +83,21 @@ class Git {
// let remoteHead = null // let remoteHead = null
for (let ref of refs) { for (let ref of refs) {
if (ref.src == "") { if (ref.src == "") {
this.storage.delete(ref.dst) if (this.refs.get("HEAD") == ref.dst) {
return `error ${ref.dst} refusing to delete the current branch: ${ref.dst}` + "\n\n"
}
log("deleting ref", ref.dst)
this.storage.removeRef(ref.dst)
this.refs.delete(ref.dst)
this.pushed.delete(ref.dst)
} else { } else {
outLines.push(await this.push(ref.src, ref.dst) + "\n") outLines.push(await this.push(ref.src, ref.dst) + "\n")
} }
} }
if (this.refs.size == 0) { if (this.refs.size == 0) {
// first push // first push
let symbolicRef = await GitUtils.symbolicRef("HEAD") let symbolicRef = GitUtils.symbolicRef("HEAD")
await this.wirteRef(symbolicRef, "HEAD", true) await this.wirteRef(symbolicRef, "HEAD", true)
} }
log("outLines", outLines) log("outLines", outLines)
@ -70,14 +112,16 @@ class Git {
force = true force = true
} }
let present = Array.from(this.refs.values()) let present = Array.from(this.refs.values())
let objects = await GitUtils.listObjects(src, present) present.push(...Array.from(this.pushed.values()))
let objects = GitUtils.listObjects(src, present)
log("listObjects", objects) log("listObjects", objects)
for (let obj of objects) { for (let obj of objects) {
await this.putObject(obj) await this.putObject(obj)
} }
let sha = await GitUtils.refValue(src) let sha = GitUtils.refValue(src)
let err = await this.wirteRef(sha, dst, force) let err = await this.wirteRef(sha, dst, force)
if (!err) { if (!err) {
this.pushed.set(dst, sha)
return `ok ${dst}` return `ok ${dst}`
} else { } else {
return `error ${dst} ${err}` return `error ${dst} ${err}`
@ -85,10 +129,9 @@ class Git {
} }
async wirteRef(newSha: string, dst: string, force: boolean): Promise<string | null> { async wirteRef(newSha: string, dst: string, force: boolean): Promise<string | null> {
let sha = this.refs.get(dst) let sha = this.refs.get(dst)
if (sha) { if (sha) {
if (!await GitUtils.objectExists(sha)) { if (!GitUtils.objectExists(sha)) {
return "fetch first" return "fetch first"
} }
let isFastForward = GitUtils.isAncestor(sha, newSha) let isFastForward = GitUtils.isAncestor(sha, newSha)
@ -108,7 +151,7 @@ class Git {
} }
async putObject(sha: string) { async putObject(sha: string) {
let data = await GitUtils.encodeObject(sha) let data = GitUtils.encodeObject(sha)
let path = this.objectPath(sha) let path = this.objectPath(sha)
log("writing...", path, sha) log("writing...", path, sha)
let status = await this.storage.upload(path, data) let status = await this.storage.upload(path, data)
@ -120,6 +163,7 @@ class Git {
const suffix = name.slice(2); const suffix = name.slice(2);
return join("objects", prefix, suffix); return join("objects", prefix, suffix);
} }
async read_symbolic_ref(path: string) { async read_symbolic_ref(path: string) {
path = join(this.gitdir, path) path = join(this.gitdir, path)
log("fetching symbolic ref: ", path) log("fetching symbolic ref: ", path)

@ -30,7 +30,7 @@ GitRemoteHelper({
remoteUrl: string; remoteUrl: string;
forPush: boolean; forPush: boolean;
}) => { }) => {
log('list log', p) log('list', p)
let out = await git.do_list(p.forPush) let out = await git.do_list(p.forPush)
log("list out:\n", out) log("list out:\n", out)

@ -48,7 +48,7 @@ export class ETHStorage implements Storage {
await fs.writeFile(stPath, JSON.stringify(dict)) await fs.writeFile(stPath, JSON.stringify(dict))
return Status.SUCCEED return Status.SUCCEED
} }
async delRef(path: string): Promise<Status> { async removeRef(path: string): Promise<Status> {
let stPath = join(mockPath, "refs.json") let stPath = join(mockPath, "refs.json")
let refsJson = await fs.readFile(stPath) let refsJson = await fs.readFile(stPath)
let dict = JSON.parse(refsJson.toString()) let dict = JSON.parse(refsJson.toString())
@ -56,7 +56,7 @@ export class ETHStorage implements Storage {
return Status.SUCCEED return Status.SUCCEED
} }
async delete(path: string): Promise<Status> { async remove(path: string): Promise<Status> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
async download(path: string): Promise<[Status, Buffer]> { async download(path: string): Promise<[Status, Buffer]> {

@ -16,9 +16,9 @@ export interface Storage {
download(path: string): Promise<[Status, Buffer]> download(path: string): Promise<[Status, Buffer]>
upload(path: string, file: Buffer): Promise<Status> upload(path: string, file: Buffer): Promise<Status>
delete(path: string): Promise<Status> remove(path: string): Promise<Status>
listRefs(): Promise<Ref[]> listRefs(): Promise<Ref[]>
setRef(path: string, sha: string): Promise<Status> setRef(path: string, sha: string): Promise<Status>
delRef(path: string): Promise<Status> removeRef(path: string): Promise<Status>
} }
Loading…
Cancel
Save