mirror of git3://git3.w3q/git3-cli
parent
cebf21c72e
commit
e829f38935
@ -0,0 +1,339 @@
|
|||||||
|
import debug from 'debug';
|
||||||
|
import { asyncMap } from 'rxjs-async-map';
|
||||||
|
import { rxToStream, streamToStringRx } from 'rxjs-stream';
|
||||||
|
import { filter, map, mergeMap, scan, tap } from 'rxjs/operators';
|
||||||
|
import { superpathjoin as join } from 'superpathjoin';
|
||||||
|
|
||||||
|
// TODO Add tests
|
||||||
|
|
||||||
|
enum GitCommands {
|
||||||
|
capabilities = 'capabilities',
|
||||||
|
option = 'option',
|
||||||
|
list = 'list',
|
||||||
|
push = 'push',
|
||||||
|
fetch = 'fetch',
|
||||||
|
}
|
||||||
|
const ONE_LINE_COMMANDS = [
|
||||||
|
GitCommands.capabilities,
|
||||||
|
GitCommands.option,
|
||||||
|
GitCommands.list,
|
||||||
|
];
|
||||||
|
|
||||||
|
const logError = (...args: any) => {
|
||||||
|
console.error(...args);
|
||||||
|
};
|
||||||
|
const log = debug('git-remote-helper');
|
||||||
|
const logIo = log.extend('io');
|
||||||
|
const logInput = logIo.extend('input');
|
||||||
|
const logOutput = logIo.extend('output');
|
||||||
|
|
||||||
|
export type PushRef = { src: string; dst: string; force: boolean };
|
||||||
|
export type FetchRef = { ref: string; oid: string };
|
||||||
|
|
||||||
|
type CommandCapabilities = {
|
||||||
|
command: GitCommands.capabilities;
|
||||||
|
};
|
||||||
|
type CommandOption = {
|
||||||
|
command: GitCommands.option;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
type CommandList = {
|
||||||
|
command: GitCommands.list;
|
||||||
|
forPush: boolean;
|
||||||
|
};
|
||||||
|
type CommandPush = {
|
||||||
|
command: GitCommands.push;
|
||||||
|
refs: PushRef[];
|
||||||
|
};
|
||||||
|
type CommandFetch = {
|
||||||
|
command: GitCommands.fetch;
|
||||||
|
refs: FetchRef[];
|
||||||
|
};
|
||||||
|
export type Command =
|
||||||
|
| CommandCapabilities
|
||||||
|
| CommandOption
|
||||||
|
| CommandList
|
||||||
|
| CommandPush
|
||||||
|
| CommandFetch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are parameters which are passed to every api callback
|
||||||
|
*/
|
||||||
|
export type ApiBaseParams = {
|
||||||
|
gitdir: string;
|
||||||
|
/**
|
||||||
|
* The remote name, or the remote URL if a name is not provided. Supplied by
|
||||||
|
* the native git client.
|
||||||
|
*/
|
||||||
|
remoteName: string;
|
||||||
|
/**
|
||||||
|
* The remote URL passed by the native git client.
|
||||||
|
*
|
||||||
|
* NOTE: It will not contain the leading `HELPER::`, only the part after that.
|
||||||
|
*/
|
||||||
|
remoteUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
type HandlePush = (
|
||||||
|
params: ApiBaseParams & { refs: PushRef[] }
|
||||||
|
) => Promise<string>;
|
||||||
|
type HandleFetch = (
|
||||||
|
params: ApiBaseParams & {
|
||||||
|
refs: FetchRef[];
|
||||||
|
}
|
||||||
|
) => Promise<string>;
|
||||||
|
|
||||||
|
type ApiBase = {
|
||||||
|
/**
|
||||||
|
* Optional init() hook which will be called each time that the
|
||||||
|
* git-remote-helper is invoked before any other APIs are called. It will be
|
||||||
|
* awaited, so you can safely do setup steps here and trust they will be
|
||||||
|
* finished before any of the other API methods are invoked.
|
||||||
|
*/
|
||||||
|
init?: (params: ApiBaseParams) => Promise<void>;
|
||||||
|
list: (
|
||||||
|
params: ApiBaseParams & {
|
||||||
|
forPush: boolean;
|
||||||
|
}
|
||||||
|
) => Promise<string>;
|
||||||
|
handlePush?: HandlePush;
|
||||||
|
handleFetch?: HandleFetch;
|
||||||
|
};
|
||||||
|
type ApiPush = ApiBase & {
|
||||||
|
handlePush: HandlePush;
|
||||||
|
handleFetch?: undefined;
|
||||||
|
};
|
||||||
|
type ApiFetch = ApiBase & {
|
||||||
|
handlePush?: undefined;
|
||||||
|
handleFetch: HandleFetch;
|
||||||
|
};
|
||||||
|
type ApiBoth = ApiBase & {
|
||||||
|
handlePush: HandlePush;
|
||||||
|
handleFetch: HandleFetch;
|
||||||
|
};
|
||||||
|
type Api = ApiPush | ApiFetch | ApiBoth;
|
||||||
|
|
||||||
|
const GitRemoteHelper = async ({
|
||||||
|
env,
|
||||||
|
api,
|
||||||
|
stdin,
|
||||||
|
stdout,
|
||||||
|
}: {
|
||||||
|
env: typeof process.env;
|
||||||
|
stdin: typeof process.stdin;
|
||||||
|
stdout: typeof process.stdout;
|
||||||
|
api: Api;
|
||||||
|
}) => {
|
||||||
|
const inputStream = streamToStringRx(stdin);
|
||||||
|
|
||||||
|
const getDir = () => {
|
||||||
|
if (typeof env['GIT_DIR'] !== 'string') {
|
||||||
|
throw new Error('Missing GIT_DIR env #tVJpoU');
|
||||||
|
}
|
||||||
|
return env['GIT_DIR'];
|
||||||
|
};
|
||||||
|
const gitdir = join(process.cwd(), getDir());
|
||||||
|
|
||||||
|
const [, , remoteName, remoteUrl] = process.argv;
|
||||||
|
|
||||||
|
const capabilitiesResponse =
|
||||||
|
[GitCommands.option, GitCommands.push, GitCommands.fetch]
|
||||||
|
.filter(option => {
|
||||||
|
if (option === GitCommands.option) {
|
||||||
|
return true;
|
||||||
|
} else if (option === GitCommands.push) {
|
||||||
|
return typeof api.handlePush === 'function';
|
||||||
|
} else if (option === GitCommands.fetch) {
|
||||||
|
return typeof api.handleFetch === 'function';
|
||||||
|
} else {
|
||||||
|
throw new Error('Unknown option #GDhBnb');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('\n') + '\n\n';
|
||||||
|
|
||||||
|
log('Startup #p6i3kB', {
|
||||||
|
gitdir,
|
||||||
|
remoteName,
|
||||||
|
remoteUrl,
|
||||||
|
capabilitiesResponse,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof api.init === 'function') {
|
||||||
|
await api.init({ gitdir, remoteName, remoteUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
const commands = inputStream.pipe(
|
||||||
|
tap(line => {
|
||||||
|
logInput('Got raw input line #gARMUQ', JSON.stringify(line));
|
||||||
|
}),
|
||||||
|
// The `line` can actually contain multiple lines, so we split them out into
|
||||||
|
// multiple pieces and recombine them again
|
||||||
|
map(line => line.split('\n')),
|
||||||
|
mergeMap(lineGroup => lineGroup),
|
||||||
|
// Commands include a trailing newline which we don't need
|
||||||
|
map(line => line.trimEnd()),
|
||||||
|
scan(
|
||||||
|
(acc, line) => {
|
||||||
|
log('Scanning #NH7FyX', JSON.stringify({ acc, line }));
|
||||||
|
// If we emitted the last value, then we ignore all of the current lines
|
||||||
|
// and start fresh on the next "batch"
|
||||||
|
const linesWaitingToBeEmitted = acc.emit ? [] : acc.lines;
|
||||||
|
|
||||||
|
// When we hit an empty line, it's always the completion of a command
|
||||||
|
// block, so we always want to emit the lines we've been collecting.
|
||||||
|
// NOTE: We do not add the blank line onto the existing array of lines
|
||||||
|
// here, it gets dropped here.
|
||||||
|
if (line === '') {
|
||||||
|
if (linesWaitingToBeEmitted.length === 0) {
|
||||||
|
return { emit: false, lines: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { emit: true, lines: linesWaitingToBeEmitted };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some commands emit one line at a time and so do not get buffered
|
||||||
|
if (ONE_LINE_COMMANDS.find(command => line.startsWith(command))) {
|
||||||
|
// If we have other lines waiting for emission, something went wrong
|
||||||
|
if (linesWaitingToBeEmitted.length > 0) {
|
||||||
|
logError(
|
||||||
|
'Got one line command with lines waiting #ompfQK',
|
||||||
|
JSON.stringify({ linesWaitingToBeEmitted })
|
||||||
|
);
|
||||||
|
throw new Error('Got one line command with lines waiting #evVyYv');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { emit: true, lines: [line] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, this line is part of a multi line command, so stick it
|
||||||
|
// into the "buffer" and do not emit
|
||||||
|
return { emit: false, lines: linesWaitingToBeEmitted.concat(line) };
|
||||||
|
},
|
||||||
|
{ emit: false, lines: [] as string[] }
|
||||||
|
),
|
||||||
|
tap(acc => {
|
||||||
|
log('Scan output #SAAmZ4', acc);
|
||||||
|
}),
|
||||||
|
filter(acc => acc.emit),
|
||||||
|
map(emitted => emitted.lines),
|
||||||
|
tap(lines => {
|
||||||
|
log('Buffer emptied #TRqQFc', JSON.stringify(lines));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// NOTE: Splitting this into 2 pipelines so typescript is happy that it
|
||||||
|
// produces a string
|
||||||
|
const output = commands.pipe(
|
||||||
|
// filter(lines => lines.length > 0),
|
||||||
|
// Build objects from the sequential lines
|
||||||
|
map(
|
||||||
|
(lines): Command => {
|
||||||
|
log('Mapping buffered line #pDqtRP', lines);
|
||||||
|
|
||||||
|
const command = lines[0];
|
||||||
|
|
||||||
|
if (command.startsWith('capabilities')) {
|
||||||
|
return { command: GitCommands.capabilities };
|
||||||
|
} else if (command.startsWith(GitCommands.list)) {
|
||||||
|
return {
|
||||||
|
command: GitCommands.list,
|
||||||
|
forPush: command.startsWith('list for-push'),
|
||||||
|
};
|
||||||
|
} else if (command.startsWith(GitCommands.option)) {
|
||||||
|
const [, key, value] = command.split(' ');
|
||||||
|
return { command: GitCommands.option, key, value };
|
||||||
|
} else if (command.startsWith(GitCommands.fetch)) {
|
||||||
|
// Lines for fetch commands look like:
|
||||||
|
// fetch sha1 branchName
|
||||||
|
const refs = lines.map(line => {
|
||||||
|
const [, oid, ref] = line.split(' ');
|
||||||
|
return { oid, ref };
|
||||||
|
});
|
||||||
|
|
||||||
|
return { command: GitCommands.fetch, refs };
|
||||||
|
} else if (command.startsWith(GitCommands.push)) {
|
||||||
|
// Lines for push commands look like this (the + means force push):
|
||||||
|
// push refs/heads/master:refs/heads/master
|
||||||
|
// push +refs/heads/master:refs/heads/master
|
||||||
|
const refs = lines.map(line => {
|
||||||
|
// Strip the leading `push ` from the line
|
||||||
|
const refsAndForce = line.slice(5);
|
||||||
|
const force = refsAndForce[0] === '+';
|
||||||
|
const refs = force ? refsAndForce.slice(1) : refsAndForce;
|
||||||
|
const [src, dst] = refs.split(':');
|
||||||
|
return { src, dst, force };
|
||||||
|
});
|
||||||
|
|
||||||
|
return { command: GitCommands.push, refs };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unknown command #Py9QTP');
|
||||||
|
}
|
||||||
|
),
|
||||||
|
asyncMap(async command => {
|
||||||
|
if (command.command === GitCommands.capabilities) {
|
||||||
|
log(
|
||||||
|
'Returning capabilities #MJMFfj',
|
||||||
|
JSON.stringify({ command, capabilitiesResponse })
|
||||||
|
);
|
||||||
|
return capabilitiesResponse;
|
||||||
|
} else if (command.command === GitCommands.option) {
|
||||||
|
// TODO Figure out how to handle options properly
|
||||||
|
log(
|
||||||
|
'Reporting option unsupported #WdUrzx',
|
||||||
|
JSON.stringify({ command })
|
||||||
|
);
|
||||||
|
return 'unsupported\n';
|
||||||
|
} else if (command.command === GitCommands.list) {
|
||||||
|
const { forPush } = command;
|
||||||
|
try {
|
||||||
|
return api.list({ gitdir, remoteName, remoteUrl, forPush });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('api.list threw #93ROre');
|
||||||
|
// console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else if (command.command === GitCommands.push) {
|
||||||
|
log('Calling api.handlePush() #qpi4Ah');
|
||||||
|
const { refs } = command;
|
||||||
|
if (typeof api.handlePush === 'undefined') {
|
||||||
|
throw new Error('api.handlePush undefined #9eNmmz');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// NOTE: Without the await here, the promise is returned immediately,
|
||||||
|
// and the catch block never fires.
|
||||||
|
return await api.handlePush({ refs, gitdir, remoteName, remoteUrl });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('api.handlePush threw #9Ei4c4');
|
||||||
|
// console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else if (command.command === GitCommands.fetch) {
|
||||||
|
const { refs } = command;
|
||||||
|
if (typeof api.handleFetch === 'undefined') {
|
||||||
|
throw new Error('api.handleFetch undefined #9eNmmz');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// NOTE: Without the await here, the promise is returned immediately,
|
||||||
|
// and the catch block never fires.
|
||||||
|
return await api.handleFetch({ refs, gitdir, remoteName, remoteUrl });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('api.handleFetch threw #5jxsQQ');
|
||||||
|
// console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unrecognised command #e6nTnS');
|
||||||
|
}, 1),
|
||||||
|
tap(x => {
|
||||||
|
logOutput('Sending response #31EyIs', JSON.stringify(x));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
rxToStream(output).pipe(stdout);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GitRemoteHelper;
|
@ -0,0 +1,102 @@
|
|||||||
|
import { log } from './log';
|
||||||
|
import { superpathjoin as join } from 'superpathjoin';
|
||||||
|
import { ApiBaseParams } from './git-remote-helper';
|
||||||
|
import { Storage } from '../storage/storage';
|
||||||
|
class Git {
|
||||||
|
gitdir: string
|
||||||
|
remoteName: string
|
||||||
|
remoteUrl: string
|
||||||
|
storage: Storage
|
||||||
|
|
||||||
|
constructor(info: ApiBaseParams, storage: Storage) {
|
||||||
|
this.gitdir = info.gitdir
|
||||||
|
this.remoteName = info.remoteName
|
||||||
|
this.remoteUrl = info.remoteUrl
|
||||||
|
this.storage = storage
|
||||||
|
}
|
||||||
|
|
||||||
|
async do_list(forPush: boolean) {
|
||||||
|
let outLines: string[] = []
|
||||||
|
let refs = this.get_refs(forPush)
|
||||||
|
for (let ref of refs) {
|
||||||
|
outLines.push(`${ref.sha} ${ref.ref}`)
|
||||||
|
}
|
||||||
|
if (!forPush) {
|
||||||
|
// head = self.read_symbolic_ref("HEAD")
|
||||||
|
// if head:
|
||||||
|
// _write("@%s HEAD" % head[1])
|
||||||
|
// else:
|
||||||
|
// self._trace("no default branch on remote", Level.INFO)
|
||||||
|
// convert to typescript
|
||||||
|
|
||||||
|
let head = await this.read_symbolic_ref("HEAD")
|
||||||
|
if (head) {
|
||||||
|
outLines.push(`@${head[1]} HEAD`)
|
||||||
|
} else {
|
||||||
|
log("no default branch on remote")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
async read_symbolic_ref(path: string) {
|
||||||
|
path = join(this.gitdir, path)
|
||||||
|
log("fetching symbolic ref: ", path)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// const [status, resp] = await this.storage.download(path);
|
||||||
|
// let ref = resp.content.toString("utf8");
|
||||||
|
// ref = ref.slice("ref: ".length).trim();
|
||||||
|
// const rev = meta.rev;
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_refs(forPush: boolean): { sha: string, ref: string }[] {
|
||||||
|
// try {
|
||||||
|
// const loc = join(this.gitdir, "refs")
|
||||||
|
// let res = this._connection.files_list_folder(loc, recursive = true)
|
||||||
|
// let files = res.entries;
|
||||||
|
// while (res.has_more) {
|
||||||
|
// res = this._connection.files_list_folder_continue(res.cursor);
|
||||||
|
// files.extend(res.entries);
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// if (e instanceof Error) {
|
||||||
|
// throw e;
|
||||||
|
// }
|
||||||
|
// if (forPush) {
|
||||||
|
// // this._first_push = true;
|
||||||
|
// } else {
|
||||||
|
// log("repository is empty")
|
||||||
|
// }
|
||||||
|
// return [];
|
||||||
|
// }
|
||||||
|
// files = files.filter((i) => i instanceof dropbox.files.FileMetadata);
|
||||||
|
// const paths = files.map((i) => i.path_lower);
|
||||||
|
// if (!paths.length) {
|
||||||
|
// return [];
|
||||||
|
// }
|
||||||
|
// let revs: string[] = [];
|
||||||
|
// let data: Uint8Array[] = [];
|
||||||
|
// for (let [rev, datum] of this._get_files(paths)) {
|
||||||
|
// revs.push(rev);
|
||||||
|
// data.push(datum);
|
||||||
|
// }
|
||||||
|
// const refs = [];
|
||||||
|
// for (let [path, rev, datum] of zip(paths, revs, data)) {
|
||||||
|
// const name = this._ref_name_from_path(path);
|
||||||
|
// const sha = datum.decode("utf8").strip();
|
||||||
|
// this._refs[name] = [rev, sha];
|
||||||
|
// refs.push([sha, name]);
|
||||||
|
// }
|
||||||
|
// return refs;
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Git
|
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const log = (...msg: any[]) => {
|
||||||
|
console.error(...msg)
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import { Level } from "level";
|
||||||
|
import { Ref, Status, Storage } from "./storage";
|
||||||
|
const db = new Level('mock')
|
||||||
|
|
||||||
|
export class ETHStorage implements Storage {
|
||||||
|
repoURI: string;
|
||||||
|
|
||||||
|
constructor(repoURI: string) {
|
||||||
|
this.repoURI = repoURI
|
||||||
|
}
|
||||||
|
|
||||||
|
listRefs(): Promise<Ref[]> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
addRefs(refs: Ref[]): Promise<Status> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
delRefs(refs: Ref[]): Promise<Status> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(path: string): Promise<void> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
async download(path: string): Promise<[Status, Buffer]> {
|
||||||
|
const prefix = "file:"
|
||||||
|
let value = await db.get(prefix + path)
|
||||||
|
return [Status.SUCCEED, Buffer.from(value)]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async upload(path: string, file: Buffer): Promise<Status> {
|
||||||
|
const prefix = "file:"
|
||||||
|
await db.put(prefix + path, file.toString())
|
||||||
|
return Status.SUCCEED
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
export enum Status {
|
||||||
|
SUCCEED = "SUCCEED",
|
||||||
|
TIMEOUT = "TIMEOUT",
|
||||||
|
FAILED = "FAILED",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Ref = {
|
||||||
|
ref: string
|
||||||
|
sha: string
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Storage {
|
||||||
|
repoURI: string
|
||||||
|
|
||||||
|
download(path: string): Promise<[Status, Buffer]>
|
||||||
|
upload(path: string, file: Buffer): Promise<Status>
|
||||||
|
delete(path: string): Promise<void>
|
||||||
|
listRefs(): Promise<Ref[]>
|
||||||
|
addRefs(refs: Ref[]): Promise<Status>
|
||||||
|
delRefs(refs: Ref[]): Promise<Status>
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in new issue