@pando/git-remote-pando
Version:
git-remote-helper for pando
464 lines (463 loc) • 18.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const debug_1 = __importDefault(require("debug"));
const eth_provider_1 = __importDefault(require("eth-provider"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const jsonfile_1 = __importDefault(require("jsonfile"));
const level_1 = __importDefault(require("level"));
const ora_1 = __importDefault(require("ora"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const shelljs_1 = __importDefault(require("shelljs"));
const truffle_contract_1 = __importDefault(require("truffle-contract"));
const web3_1 = __importDefault(require("web3"));
const git_1 = __importDefault(require("./lib/git"));
const ipld_1 = __importDefault(require("./lib/ipld"));
const line_1 = __importDefault(require("./lib/line"));
const _timeout = async (duration) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, duration);
});
};
class Helper {
constructor(name = '_', url) {
// name and url
this.name = name;
this.url = url;
// address and path shortcuts
this.address = this.url.split('://')[1];
this.path = path_1.default.resolve(process.env.GIT_DIR);
// config
this.config = this._config();
// lib
this.debug = debug_1.default('pando');
this.line = new line_1.default();
this.git = new git_1.default(this);
this.ipld = new ipld_1.default(this);
}
// OK
async initialize() {
// create dirs
fs_extra_1.default.ensureDirSync(path_1.default.join(this.path, 'refs', 'remotes', this.name));
fs_extra_1.default.ensureDirSync(path_1.default.join(this.path, 'pando', 'refs'));
// load db
this._db = level_1.default(path_1.default.join(this.path, 'pando', 'refs', this.address));
// initialize web3
this._provider = await this._ethConnect();
this._web3 = new web3_1.default(this._provider);
// initialize repo contract
this._repo = { artifact: undefined, contract: undefined };
// tslint:disable-next-line:no-submodule-imports
this._repo.artifact = truffle_contract_1.default(require('@pando/repository/abi/PandoRepository.json'));
this._repo.artifact.setProvider(this._provider);
this._repo.artifact.defaults({ from: this.config.ethereum.account });
this._repo.artifact.autoGas = true;
this._repo.artifact.gasMultiplier = 3;
this._repo.contract = await this._repo.artifact.at(this.address);
// initialize kernel contract
this._kernel = { artifact: undefined, contract: undefined };
// tslint:disable-next-line:no-submodule-imports
this._kernel.artifact = truffle_contract_1.default(require('@pando/repository/abi/Kernel.json'));
this._kernel.artifact.setProvider(this._provider);
this._kernel.artifact.defaults({ from: this.config.ethereum.account });
this._kernel.artifact.autoGas = true;
this._kernel.artifact.gasMultiplier = 3;
this._kernel.contract = await this._kernel.artifact.at(await this._repo.contract.kernel());
}
// OK
async run() {
while (true) {
const cmd = await this.line.next();
switch (this._cmd(cmd)) {
case 'capabilities':
await this._handleCapabilities();
break;
case 'list for-push':
await this._handleList({ forPush: true });
break;
case 'list':
await this._handleList();
break;
case 'fetch':
await this._handleFetch(cmd);
break;
case 'push':
await this._handlePush(cmd);
break;
case 'end':
this._exit();
case 'unknown':
throw new Error('Unknown command: ' + cmd);
}
}
}
/***** commands handling methods *****/
// OK
async _handleCapabilities() {
this.debug('cmd', 'capabilities');
// this._send('option')
this._send('fetch');
this._send('push');
this._send('');
}
// OK
async _handleList({ forPush = false } = {}) {
forPush ? this.debug('cmd', 'list', 'for-push') : this.debug('cmd, list');
const refs = await this._fetchRefs();
// tslint:disable-next-line:forin
for (const ref in refs) {
this._send(this.ipld.cidToSha(refs[ref]) + ' ' + ref);
}
// force HEAD to master and update once pando handle symbolic refs
this._send('@refs/heads/master' + ' ' + 'HEAD');
this._send('');
}
// OK
async _handleFetch(line) {
this.debug('cmd', line);
while (true) {
const [cmd, oid, name] = line.split(' ');
await this._fetch(oid, name);
line = await this.line.next();
if (line === '')
break;
}
this._send('');
}
// OK
async _handlePush(line) {
this.debug('cmd', line);
while (true) {
const [src, dst] = line.split(' ')[1].split(':');
if (src === '') {
await this._delete(dst);
}
else {
await this._push(src, dst);
}
line = await this.line.next();
if (line === '')
break;
}
this._send('');
}
/***** refs db methods *****/
// OK
async _fetchRefs() {
this.debug('fetching remote refs from chain');
let start;
let block;
let events;
const ops = [];
const updates = {};
const spinner = ora_1.default('Fetching remote refs from chain [this may take a while]').start();
try {
start = await this._db.get('@block');
}
catch (err) {
if (err.message === 'Key not found in database [@block]') {
start = 0;
}
else {
throw err;
}
}
try {
block = await this._web3.eth.getBlockNumber();
events = await this._repo.contract.getPastEvents('UpdateRef', {
fromBlock: start,
toBlock: 'latest',
});
for (const event of events) {
updates[event.args.ref] = event.args.hash;
}
// tslint:disable-next-line:forin
for (const ref in updates) {
ops.push({ type: 'put', key: ref, value: updates[ref] });
}
ops.push({ type: 'put', key: '@block', value: block });
await this._db.batch(ops);
spinner.succeed('Remote refs fetched from chain');
return this._getRefs();
}
catch (err) {
spinner.fail('Failed to fetch remote refs from chain');
throw err;
}
}
// OK
async _getRefs() {
this.debug('reading refs from local db');
const refs = {};
return new Promise((resolve, reject) => {
this._db
.createReadStream()
.on('data', ref => {
if (ref.key !== '@block')
refs[ref.key] = ref.value;
})
.on('error', err => {
reject(err);
})
.on('end', () => {
//
})
.on('close', () => {
resolve(refs);
});
});
}
/***** core methods *****/
// OK
async _fetch(oid, ref) {
this.debug('fetching', ref, oid, this.ipld.shaToCid(oid));
await this.git.download(oid);
}
// OK
async _push(src, dst) {
this.debug('pushing', src, 'to', dst);
return new Promise(async (resolve, reject) => {
let spinner;
let head;
let txHash;
let mapping = {};
const puts = [];
const pins = [];
try {
const refs = await this._getRefs();
const remote = refs[dst];
const srcBranch = src.split('/').pop();
const dstBranch = dst.split('/').pop();
const revListCmd = remote ? `git rev-list --left-only ${srcBranch}...${this.name}/${dstBranch}` : 'git rev-list --all';
const commits = shelljs_1.default
.exec(revListCmd, { silent: true })
.stdout.split('\n')
.slice(0, -1);
// checking permissions
try {
spinner = ora_1.default(`Checking permissions over ${this.address}`).start();
if (process.env.PANDO_OPEN_PR) {
if (await this._kernel.contract.hasPermission(this.config.ethereum.account, this.address, await this._repo.contract.OPEN_PR_ROLE(), '0x0')) {
spinner.succeed(`You have open PR permission over ${this.address}`);
}
else {
spinner.fail(`You do not have open PR permission over ${this.address}`);
this._die();
}
}
else {
if (await this._kernel.contract.hasPermission(this.config.ethereum.account, this.address, await this._repo.contract.PUSH_ROLE(), '0x0')) {
spinner.succeed(`You have push permission over ${this.address}`);
}
else {
spinner.fail(`You do not have push permission over ${this.address}. Try to run 'git pando pr open' to open a push request.`);
this._die();
}
}
}
catch (err) {
spinner.fail(`Failed to check permissions over ${this.address}: ` + err.message);
this._die();
}
// collect git objects
try {
spinner = ora_1.default('Collecting git objects [this may take a while]').start();
for (const commit of commits) {
const _mapping = await this.git.collect(commit, mapping);
mapping = Object.assign({}, mapping, _mapping);
}
head = this.ipld.shaToCid(commits[0]);
// tslint:disable-next-line:forin
for (const entry in mapping) {
puts.push(this.ipld.put(mapping[entry]));
}
spinner.succeed('Git objects collected');
}
catch (err) {
spinner.fail('Failed to collect git objects: ' + err.message);
this._die();
}
// upload git objects
try {
spinner = ora_1.default('Uploading git objects to IPFS').start();
await Promise.all(puts);
spinner.succeed('Git objects uploaded to IPFS');
}
catch (err) {
spinner.fail('Failed to upload git objects to IPFS: ' + err.message);
this._die();
}
// pin git objects
try {
// tslint:disable-next-line:forin
for (const entry in mapping) {
pins.push(this.ipld.pin(entry));
}
spinner = ora_1.default('Pinning git objects to IPFS').start();
await Promise.all(pins);
spinner.succeed('Git objects pinned to IPFS');
}
catch (err) {
spinner.fail('Failed to pin git objects to IPFS: ' + err.message);
this._die();
}
// register on chain
if (!process.env.PANDO_OPEN_PR) {
try {
spinner = ora_1.default(`Registering ref ${dst} ${head} on-chain`).start();
this._repo.contract
.push(dst, head)
.on('error', err => {
if (!err.message.includes('Failed to subscribe to new newBlockHeaders to confirm the transaction receipts')) {
spinner.fail(`Failed to register ref ${dst} ${head} on-chain: ` + err.message);
this._die();
}
})
.on('transactionHash', hash => {
txHash = hash;
spinner.text = `Registering ref ${dst} ${head} on-chain through tx ${txHash}`;
})
.then(receipt => {
spinner.succeed(`Ref ${dst} ${head} registered on-chain through tx ${txHash}`);
this._send(`ok ${dst}`);
resolve();
})
.catch(err => {
spinner.fail(`Failed to register ref ${dst} ${head} on-chain: ` + err.message);
this._die();
});
}
catch (err) {
spinner.fail(`Failed to register ref ${dst} ${head} on-chain: ` + err.message);
this._die();
}
}
else {
try {
spinner = ora_1.default(`Opening PR for ${dst} ${head} on-chain`).start();
this._repo.contract
.openPR(process.env.PANDO_PR_TITLE, process.env.PANDO_PR_DESCRIPTION || '', dst, head)
.on('error', err => {
if (!err.message.includes('Failed to subscribe to new newBlockHeaders to confirm the transaction receipts')) {
spinner.fail(`Failed to open PR for ${dst} ${head} on-chain: ` + err.message);
this._die();
}
})
.on('transactionHash', hash => {
txHash = hash;
spinner.text = `Opening PR for ${dst} ${head} on-chain through tx ${txHash}`;
})
.then(receipt => {
const id = receipt.logs.filter(l => l.event === 'OpenPR')[0].args.id;
spinner.succeed(`PR #${id} for ${dst} ${head} opened on-chain through tx ${txHash}`);
this._send(`error ${dst} Relax! This is because you do not have push permission: PR #${id} has been opened for you instead`);
resolve();
})
.catch(err => {
spinner.fail(`Failed to open PR for ${dst} ${head} on-chain: ` + err.message);
this._die();
});
}
catch (err) {
spinner.fail(`Failed to open PR for ${dst} ${head} on-chain: ` + err.message);
this._die();
}
}
}
catch (err) {
this._die(err.message);
}
});
}
// TODO
async _delete(dst) {
this.debug('deleting', dst);
}
/***** utility methods *****/
// OK
async _ethConnect() {
this.debug('connecting to gateway', this.config.ethereum.gateway);
const provider = eth_provider_1.default(this.config.ethereum.gateway);
const spinner = ora_1.default('Connecting to Ethereum').start();
while (true) {
try {
const accounts = await provider.send('eth_accounts');
spinner.stop();
for (const account of accounts) {
if (account === this.config.ethereum.account)
return provider;
}
this._die("Failed to access your Ethereum account. Update your gateway configuration or run 'git pando config' to select another account.");
}
catch (err) {
if (err.code === 4100 || err.code === 4001) {
spinner.text = `Error connecting to Ethereum. ${err.message}.`;
await _timeout(2000);
}
else {
spinner.stop();
this._die('Failed to connect to Ethereum. Make sure your Ethereum gateway is running.');
}
}
}
}
// OK
_config() {
const LOCAL = path_1.default.join(this.path, 'pando', '.pandorc');
const GLOBAL = path_1.default.join(os_1.default.homedir(), '.pandorc');
if (fs_extra_1.default.pathExistsSync(LOCAL))
return jsonfile_1.default.readFileSync(LOCAL);
if (fs_extra_1.default.pathExistsSync(GLOBAL))
return jsonfile_1.default.readFileSync(GLOBAL);
this._die("No configuration file found. Run 'git-pando config' and come back to us.");
}
// OK
_cmd(line) {
if (line === 'capabilities') {
return 'capabilities';
}
else if (line === 'list for-push') {
return 'list for-push';
}
else if (line === 'list') {
return 'list';
}
else if (line.startsWith('option')) {
return 'option';
}
else if (line.startsWith('fetch')) {
return 'fetch';
}
else if (line.startsWith('push')) {
return 'push';
}
else if (line === '') {
return 'end';
}
else {
return 'unknown';
}
}
// OK
_send(message) {
// tslint:disable-next-line:no-console
console.log(message);
}
// OK
_die(message) {
// tslint:disable-next-line:no-console
if (message)
console.error(message);
process.exit(1);
}
// OK
_exit() {
process.exit(0);
}
}
exports.default = Helper;