UNPKG

@7i7o/git-remote-proland

Version:
224 lines (186 loc) 6.9 kB
import { existsSync, mkdirSync } from 'fs'; import { spawn } from 'child_process'; import readline from 'readline'; import { Writable } from 'stream'; import { downloadProtocolLandRepo, uploadProtocolLandRepo, } from './protocolLandSync'; import path from 'path'; import type { Repo } from '../types'; import { PL_TMP_PATH, clearCache, getWallet, log, ownerOrContributor, setCacheDirty, unsetCacheDirty, waitFor, } from './common'; // string to check if objects were pushed const OBJECTS_PUSHED = 'unpack ok'; export type RemoteHelperParams = { remoteName: string; remoteUrl: string; gitdir: string; }; export const remoteHelper = async (params: RemoteHelperParams) => { const { remoteUrl, gitdir } = params; // get tmp path for remote (throws if can't create a tmp path) const tmpPath = getTmpPath(gitdir); // get repoId from remoteUrl (remove the `protocol.land://` from it) const repoId = `${remoteUrl.replace(/.*:\/\//, '')}`; // download protocol land repo latest version to tmpRemotePath const repo = await downloadProtocolLandRepo(repoId, tmpPath); // construct bare repo path const bareRemotePath = path.join(tmpPath, repo.dataTxId); // start communication with git talkToGit(bareRemotePath, repo, tmpPath); }; function getTmpPath(gitdir: string) { const tmpPath = path.join(gitdir, PL_TMP_PATH); // Check if the tmp folder exists, and create it if it doesn't if (!existsSync(tmpPath)) { mkdirSync(tmpPath, { recursive: true }); if (!existsSync(tmpPath)) throw new Error(`Failed to create the directory: ${tmpPath}`); } return tmpPath; } function talkToGit(bareRemotePath: string, repo: Repo, tmpPath: string) { // create a readline interface to read lines const rl = readline.createInterface({ input: process.stdin, output: new Writable({ write(chunk, encoding, callback) { callback(); }, }), // Passing a null stream for output }); // Main communication loop async function readLinesUntilEmpty() { const promptForLine = () => new Promise<string>((resolve) => rl.question('', resolve)); while (true) { const line = (await promptForLine()).trim(); // if empty line -> Exit if (line === '') { rl.close(); process.exit(0); } const [command, arg] = line.split(' '); switch (command) { case 'capabilities': console.log('connect'); console.log(''); break; case 'connect': console.log(''); // spawn git utility 'arg' with the remoteUrl as an argument await spawnPipedGitCommand( arg as string, bareRemotePath, repo, tmpPath ); break; } } } readLinesUntilEmpty(); } const spawnPipedGitCommand = async ( gitCommand: string, remoteUrl: string, repo: Repo, tmpPath: string ) => { // if pushing if (gitCommand === 'git-receive-pack') { const wallet = getWallet({ warn: false }); // if missing wallet, exit without running gitCommand (getWallet prints a message) if (!wallet) process.exit(0); const ownerOrContrib = await ownerOrContributor(repo, wallet, { pushing: true, }); // if not owner or contriburtor, exit without running gitCommand (ownerOrContributor prints a message) if (!ownerOrContrib) process.exit(0); } else { // not pushing // getWallet warns if wallet is not found const wallet = getWallet({ warn: true }); if (wallet) { // has a wallet defined, but // warn user if not owner or contributor (fn already prints a message) ownerOrContributor(repo, wallet); } } // define flag to check if objects have been pushed let objectsUpdated = false; // spawn the gitCommand and pipe all stdio const gitProcess = spawn(gitCommand, [remoteUrl as string], { stdio: ['pipe', 'pipe', 'pipe'], // Pipe for stdin, stdout, and stderr }); // Pipe Data: // stdin: process -> gitProcess // stdout: gitProcess -> process // stderr: gitProcess -> process process.stdin.pipe(gitProcess.stdin); gitProcess.stdout.pipe(process.stdout); gitProcess.stderr.pipe(process.stderr); // parse stdout to check if objects have been updated (avoid uploading after an empty push) gitProcess.stdout.on('data', (data) => { if (data.toString().includes(OBJECTS_PUSHED)) objectsUpdated = true; }); // Handle process exit gitProcess.on('exit', async (code) => { // if error, show message and exit if (code !== 0) { log( `git command '${gitCommand}' exited with error. Exit code: ${code}`, { color: 'red', } ); process.exit(code ? code : 1); } // if pushed to tmp bare remote ok AND objects were updated, then upload repo to protocol land if (gitCommand === 'git-receive-pack' && objectsUpdated) { log( `Push to temp remote finished successfully, now syncing with Protocol Land ...` ); // mark cache as inconsistent setCacheDirty(tmpPath, repo.dataTxId); const pathToPack = path.join(remoteUrl, '..', '..', '..'); waitFor(1000); const success = await uploadProtocolLandRepo( pathToPack, repo, tmpPath ); // We clear the cached remote: // If upload succeeded, there's a new txId for the repo // If upload failed, the cached remote has an inconsistent state clearCache(tmpPath, { keepFolders: ['cache'] }); // remove inconsistent cache mark unsetCacheDirty(tmpPath, repo.dataTxId); if (success) log(`Successfully pushed repo '${repo.id}' to Protocol Land`, { color: 'green', }); else { log(`Failed to push repo '${repo.id}' to Protocol Land`, { color: 'red', }); log( 'Please run `git pull` first to clean the cache and integrate your changes', { color: 'red', } ); process.exit(1); } } }); };