master
cyhhao 2 years ago
parent 8b10f4da49
commit 26150120d4

@ -1,8 +1,8 @@
import debug from 'debug'; import debug from 'debug'
import { asyncMap } from 'rxjs-async-map'; import { asyncMap } from 'rxjs-async-map'
import { rxToStream, streamToStringRx } from 'rxjs-stream'; import { rxToStream, streamToStringRx } from 'rxjs-stream'
import { filter, map, mergeMap, scan, tap } from 'rxjs/operators'; import { filter, map, mergeMap, scan, tap } from 'rxjs/operators'
import { superpathjoin as join } from 'superpathjoin'; import { superpathjoin as join } from 'superpathjoin'
// TODO Add tests // TODO Add tests
@ -17,73 +17,73 @@ const ONE_LINE_COMMANDS = [
GitCommands.capabilities, GitCommands.capabilities,
GitCommands.option, GitCommands.option,
GitCommands.list, GitCommands.list,
]; ]
const logError = (...args: any) => { const logError = (...args: any) => {
console.error(...args); console.error(...args)
}; }
const log = debug('git-remote-helper'); const log = debug('git-remote-helper')
const logIo = log.extend('io'); const logIo = log.extend('io')
const logInput = logIo.extend('input'); const logInput = logIo.extend('input')
const logOutput = logIo.extend('output'); const logOutput = logIo.extend('output')
export type PushRef = { src: string; dst: string; force: boolean }; export type PushRef = { src: string, dst: string, force: boolean }
export type FetchRef = { ref: string; oid: string }; export type FetchRef = { ref: string, oid: string }
type CommandCapabilities = { type CommandCapabilities = {
command: GitCommands.capabilities; command: GitCommands.capabilities
}; }
type CommandOption = { type CommandOption = {
command: GitCommands.option; command: GitCommands.option
key: string; key: string
value: string; value: string
}; }
type CommandList = { type CommandList = {
command: GitCommands.list; command: GitCommands.list
forPush: boolean; forPush: boolean
}; }
type CommandPush = { type CommandPush = {
command: GitCommands.push; command: GitCommands.push
refs: PushRef[]; refs: PushRef[]
}; }
type CommandFetch = { type CommandFetch = {
command: GitCommands.fetch; command: GitCommands.fetch
refs: FetchRef[]; refs: FetchRef[]
}; }
export type Command = export type Command =
| CommandCapabilities | CommandCapabilities
| CommandOption | CommandOption
| CommandList | CommandList
| CommandPush | CommandPush
| CommandFetch; | CommandFetch
/** /**
* These are parameters which are passed to every api callback * These are parameters which are passed to every api callback
*/ */
export type ApiBaseParams = { export type ApiBaseParams = {
gitdir: string; gitdir: string
/** /**
* The remote name, or the remote URL if a name is not provided. Supplied by * The remote name, or the remote URL if a name is not provided. Supplied by
* the native git client. * the native git client.
*/ */
remoteName: string; remoteName: string
/** /**
* The remote URL passed by the native git client. * The remote URL passed by the native git client.
* *
* NOTE: It will not contain the leading `HELPER::`, only the part after that. * NOTE: It will not contain the leading `HELPER::`, only the part after that.
*/ */
remoteUrl: string; remoteUrl: string
}; }
type HandlePush = ( type HandlePush = (
params: ApiBaseParams & { refs: PushRef[] } params: ApiBaseParams & { refs: PushRef[] }
) => Promise<string>; ) => Promise<string>
type HandleFetch = ( type HandleFetch = (
params: ApiBaseParams & { params: ApiBaseParams & {
refs: FetchRef[]; refs: FetchRef[]
} }
) => Promise<string>; ) => Promise<string>
type ApiBase = { type ApiBase = {
/** /**
@ -92,28 +92,28 @@ type ApiBase = {
* awaited, so you can safely do setup steps here and trust they 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. * finished before any of the other API methods are invoked.
*/ */
init?: (params: ApiBaseParams) => Promise<void>; init?: (params: ApiBaseParams) => Promise<void>
list: ( list: (
params: ApiBaseParams & { params: ApiBaseParams & {
forPush: boolean; forPush: boolean
} }
) => Promise<string>; ) => Promise<string>
handlePush?: HandlePush; handlePush?: HandlePush
handleFetch?: HandleFetch; handleFetch?: HandleFetch
}; }
type ApiPush = ApiBase & { type ApiPush = ApiBase & {
handlePush: HandlePush; handlePush: HandlePush
handleFetch?: undefined; handleFetch?: undefined
}; }
type ApiFetch = ApiBase & { type ApiFetch = ApiBase & {
handlePush?: undefined; handlePush?: undefined
handleFetch: HandleFetch; handleFetch: HandleFetch
}; }
type ApiBoth = ApiBase & { type ApiBoth = ApiBase & {
handlePush: HandlePush; handlePush: HandlePush
handleFetch: HandleFetch; handleFetch: HandleFetch
}; }
type Api = ApiPush | ApiFetch | ApiBoth; type Api = ApiPush | ApiFetch | ApiBoth
const GitRemoteHelper = async ({ const GitRemoteHelper = async ({
env, env,
@ -121,53 +121,53 @@ const GitRemoteHelper = async ({
stdin, stdin,
stdout, stdout,
}: { }: {
env: typeof process.env; env: typeof process.env
stdin: typeof process.stdin; stdin: typeof process.stdin
stdout: typeof process.stdout; stdout: typeof process.stdout
api: Api; api: Api
}) => { }) => {
const inputStream = streamToStringRx(stdin); const inputStream = streamToStringRx(stdin)
const getDir = () => { const getDir = () => {
if (typeof env['GIT_DIR'] !== 'string') { 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 join(__dirname, ".git")
} }
return env['GIT_DIR']; return env['GIT_DIR']
}; }
const gitdir = join(process.cwd(), getDir()); const gitdir = join(process.cwd(), getDir())
const [, , remoteName, remoteUrl] = process.argv; const [, , remoteName, remoteUrl] = process.argv
const capabilitiesResponse = const capabilitiesResponse =
[GitCommands.option, GitCommands.push, GitCommands.fetch] [GitCommands.option, GitCommands.push, GitCommands.fetch]
.filter(option => { .filter(option => {
if (option === GitCommands.option) { if (option === GitCommands.option) {
return true; return true
} else if (option === GitCommands.push) { } else if (option === GitCommands.push) {
return typeof api.handlePush === 'function'; return typeof api.handlePush === 'function'
} else if (option === GitCommands.fetch) { } else if (option === GitCommands.fetch) {
return typeof api.handleFetch === 'function'; return typeof api.handleFetch === 'function'
} else { } else {
throw new Error('Unknown option #GDhBnb'); throw new Error('Unknown option #GDhBnb')
} }
}) })
.join('\n') + '\n\n'; .join('\n') + '\n\n'
log('Startup #p6i3kB', { log('Startup #p6i3kB', {
gitdir, gitdir,
remoteName, remoteName,
remoteUrl, remoteUrl,
capabilitiesResponse, capabilitiesResponse,
}); })
if (typeof api.init === 'function') { if (typeof api.init === 'function') {
await api.init({ gitdir, remoteName, remoteUrl }); await api.init({ gitdir, remoteName, remoteUrl })
} }
const commands = inputStream.pipe( const commands = inputStream.pipe(
tap(line => { tap(line => {
logInput('Got raw input line #gARMUQ', JSON.stringify(line)); logInput('Got raw input line #gARMUQ', JSON.stringify(line))
}), }),
// The `line` can actually contain multiple lines, so we split them out into // The `line` can actually contain multiple lines, so we split them out into
// multiple pieces and recombine them again // multiple pieces and recombine them again
@ -180,10 +180,10 @@ const GitRemoteHelper = async ({
// console.log('') // console.log('')
// console.error('====') // console.error('====')
// console.error(line) // console.error(line)
// log('Scanning #NH7FyX', JSON.stringify({ acc, line })); // log('Scanning #NH7FyX', JSON.stringify({ acc, line }))
// If we emitted the last value, then we ignore all of the current lines // If we emitted the last value, then we ignore all of the current lines
// and start fresh on the next "batch" // and start fresh on the next "batch"
const linesWaitingToBeEmitted = acc.emit ? [] : acc.lines; const linesWaitingToBeEmitted = acc.emit ? [] : acc.lines
// When we hit an empty line, it's always the completion of a command // 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. // block, so we always want to emit the lines we've been collecting.
@ -191,10 +191,10 @@ const GitRemoteHelper = async ({
// here, it gets dropped here. // here, it gets dropped here.
if (line === '') { if (line === '') {
if (linesWaitingToBeEmitted.length === 0) { if (linesWaitingToBeEmitted.length === 0) {
return { emit: false, lines: [] }; return { emit: false, lines: [] }
} }
return { emit: true, lines: linesWaitingToBeEmitted }; return { emit: true, lines: linesWaitingToBeEmitted }
} }
// Some commands emit one line at a time and so do not get buffered // Some commands emit one line at a time and so do not get buffered
@ -204,28 +204,28 @@ const GitRemoteHelper = async ({
logError( logError(
'Got one line command with lines waiting #ompfQK', 'Got one line command with lines waiting #ompfQK',
JSON.stringify({ linesWaitingToBeEmitted }) JSON.stringify({ linesWaitingToBeEmitted })
); )
throw new Error('Got one line command with lines waiting #evVyYv'); throw new Error('Got one line command with lines waiting #evVyYv')
} }
return { emit: true, lines: [line] }; return { emit: true, lines: [line] }
} }
// Otherwise, this line is part of a multi line command, so stick it // Otherwise, this line is part of a multi line command, so stick it
// into the "buffer" and do not emit // into the "buffer" and do not emit
return { emit: false, lines: linesWaitingToBeEmitted.concat(line) }; return { emit: false, lines: linesWaitingToBeEmitted.concat(line) }
}, },
{ emit: false, lines: [] as string[] } { emit: false, lines: [] as string[] }
), ),
tap(acc => { tap(acc => {
log('Scan output #SAAmZ4', acc); log('Scan output #SAAmZ4', acc)
}), }),
filter(acc => acc.emit), filter(acc => acc.emit),
map(emitted => emitted.lines), map(emitted => emitted.lines),
tap(lines => { tap(lines => {
log('Buffer emptied #TRqQFc', JSON.stringify(lines)); log('Buffer emptied #TRqQFc', JSON.stringify(lines))
}) })
); )
// NOTE: Splitting this into 2 pipelines so typescript is happy that it // NOTE: Splitting this into 2 pipelines so typescript is happy that it
// produces a string // produces a string
@ -234,46 +234,46 @@ const GitRemoteHelper = async ({
// Build objects from the sequential lines // Build objects from the sequential lines
map( map(
(lines): Command => { (lines): Command => {
log('Mapping buffered line #pDqtRP', lines); log('Mapping buffered line #pDqtRP', lines)
const command = lines[0]; const command = lines[0]
if (command.startsWith('capabilities')) { if (command.startsWith('capabilities')) {
return { command: GitCommands.capabilities }; return { command: GitCommands.capabilities }
} else if (command.startsWith(GitCommands.list)) { } else if (command.startsWith(GitCommands.list)) {
return { return {
command: GitCommands.list, command: GitCommands.list,
forPush: command.startsWith('list for-push'), forPush: command.startsWith('list for-push'),
}; }
} else if (command.startsWith(GitCommands.option)) { } else if (command.startsWith(GitCommands.option)) {
const [, key, value] = command.split(' '); const [, key, value] = command.split(' ')
return { command: GitCommands.option, key, value }; return { command: GitCommands.option, key, value }
} else if (command.startsWith(GitCommands.fetch)) { } else if (command.startsWith(GitCommands.fetch)) {
// Lines for fetch commands look like: // Lines for fetch commands look like:
// fetch sha1 branchName // fetch sha1 branchName
const refs = lines.map(line => { const refs = lines.map(line => {
const [, oid, ref] = line.split(' '); const [, oid, ref] = line.split(' ')
return { oid, ref }; return { oid, ref }
}); })
return { command: GitCommands.fetch, refs }; return { command: GitCommands.fetch, refs }
} else if (command.startsWith(GitCommands.push)) { } else if (command.startsWith(GitCommands.push)) {
// Lines for push commands look like this (the + means force 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
// push +refs/heads/master:refs/heads/master // push +refs/heads/master:refs/heads/master
const refs = lines.map(line => { const refs = lines.map(line => {
// Strip the leading `push ` from the line // Strip the leading `push ` from the line
const refsAndForce = line.slice(5); const refsAndForce = line.slice(5)
const force = refsAndForce[0] === '+'; const force = refsAndForce[0] === '+'
const refs = force ? refsAndForce.slice(1) : refsAndForce; const refs = force ? refsAndForce.slice(1) : refsAndForce
const [src, dst] = refs.split(':'); const [src, dst] = refs.split(':')
return { src, dst, force }; return { src, dst, force }
}); })
return { command: GitCommands.push, refs }; return { command: GitCommands.push, refs }
} }
throw new Error('Unknown command #Py9QTP'); throw new Error('Unknown command #Py9QTP')
} }
), ),
asyncMap(async command => { asyncMap(async command => {
@ -281,63 +281,63 @@ const GitRemoteHelper = async ({
log( log(
'Returning capabilities #MJMFfj', 'Returning capabilities #MJMFfj',
JSON.stringify({ command, capabilitiesResponse }) JSON.stringify({ command, capabilitiesResponse })
); )
return capabilitiesResponse; return capabilitiesResponse
} else if (command.command === GitCommands.option) { } else if (command.command === GitCommands.option) {
// TODO Figure out how to handle options properly // TODO Figure out how to handle options properly
log( log(
'Reporting option unsupported #WdUrzx', 'Reporting option unsupported #WdUrzx',
JSON.stringify({ command }) JSON.stringify({ command })
); )
return 'unsupported\n'; return 'unsupported\n'
} else if (command.command === GitCommands.list) { } else if (command.command === GitCommands.list) {
const { forPush } = command; const { forPush } = command
try { try {
return api.list({ gitdir, remoteName, remoteUrl, forPush }); return api.list({ gitdir, remoteName, remoteUrl, forPush })
} catch (error) { } catch (error) {
console.error('api.list threw #93ROre'); console.error('api.list threw #93ROre')
// console.error(error); // console.error(error)
throw error; throw error
} }
} else if (command.command === GitCommands.push) { } else if (command.command === GitCommands.push) {
log('Calling api.handlePush() #qpi4Ah'); log('Calling api.handlePush() #qpi4Ah')
const { refs } = command; const { refs } = command
if (typeof api.handlePush === 'undefined') { if (typeof api.handlePush === 'undefined') {
throw new Error('api.handlePush undefined #9eNmmz'); throw new Error('api.handlePush undefined #9eNmmz')
} }
try { try {
// NOTE: Without the await here, the promise is returned immediately, // NOTE: Without the await here, the promise is returned immediately,
// and the catch block never fires. // and the catch block never fires.
return await api.handlePush({ refs, gitdir, remoteName, remoteUrl }); return await api.handlePush({ refs, gitdir, remoteName, remoteUrl })
} catch (error) { } catch (error) {
console.error('api.handlePush threw #9Ei4c4'); console.error('api.handlePush threw #9Ei4c4')
// console.error(error); // console.error(error)
throw error; throw error
} }
} else if (command.command === GitCommands.fetch) { } else if (command.command === GitCommands.fetch) {
const { refs } = command; const { refs } = command
if (typeof api.handleFetch === 'undefined') { if (typeof api.handleFetch === 'undefined') {
throw new Error('api.handleFetch undefined #9eNmmz'); throw new Error('api.handleFetch undefined #9eNmmz')
} }
try { try {
// NOTE: Without the await here, the promise is returned immediately, // NOTE: Without the await here, the promise is returned immediately,
// and the catch block never fires. // and the catch block never fires.
return await api.handleFetch({ refs, gitdir, remoteName, remoteUrl }); return await api.handleFetch({ refs, gitdir, remoteName, remoteUrl })
} catch (error) { } catch (error) {
console.error('api.handleFetch threw #5jxsQQ'); console.error('api.handleFetch threw #5jxsQQ')
// console.error(error); // console.error(error)
throw error; throw error
} }
} }
throw new Error('Unrecognised command #e6nTnS'); throw new Error('Unrecognised command #e6nTnS')
}, 1), }, 1),
tap(x => { tap(x => {
logOutput('Sending response #31EyIs', JSON.stringify(x)); logOutput('Sending response #31EyIs', JSON.stringify(x))
}) })
); )
rxToStream(output).pipe(stdout); rxToStream(output).pipe(stdout)
}; }
export default GitRemoteHelper; export default GitRemoteHelper

@ -1,14 +1,14 @@
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import pathUtil from 'path' import pathUtil from 'path'
import { Ref, Status, Storage } from "./storage"; import { Ref, Status, Storage } from "./storage"
import { superpathjoin as join } from 'superpathjoin'; import { superpathjoin as join } from 'superpathjoin'
const mockPath = process.env.HOME + "/.git3/mock" const mockPath = process.env.HOME + "/.git3/mock"
fs.mkdir(mockPath, { recursive: true }) fs.mkdir(mockPath, { recursive: true })
const log = console.error const log = console.error
log("mock path", mockPath) log("mock path", mockPath)
export class ETHStorage implements Storage { export class ETHStorage implements Storage {
repoURI: string; repoURI: string
constructor(repoURI: string) { constructor(repoURI: string) {
this.repoURI = repoURI this.repoURI = repoURI
@ -57,7 +57,7 @@ export class ETHStorage implements Storage {
} }
async remove(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]> {
let buffer = await fs.readFile(join(mockPath, path)) let buffer = await fs.readFile(join(mockPath, path))

Loading…
Cancel
Save