Create pijul.ts for executing commands using the Pijul CLI

[?]
Dec 23, 2020, 10:20 PM
ZGMIJNFVDK7R6AF56FNCA23W5KV3HVBUBPTWMLQADCEPB3MOPELQC

Dependencies

Change contents

  • file addition: pijul.ts (-xw-x--x--)
    [2.104980]
    import * as cp from 'child_process';
    import { EventEmitter } from 'events';
    import * as iconv from 'iconv-lite-umd';
    import { CancellationToken } from 'vscode';
    import { dispose, IDisposable, toDisposable } from './disposableUtils';
    import { onceEvent } from './eventUtils';
    export interface IPijulOptions {
    path: string
    version: string
    }
    /**
    * The Pijul class holds information about the Pijul installation
    * in use by the extension which it uses for executing commands with
    * the Pijul CLI. All interactions with the CLI come through this class.
    */
    export class Pijul {
    readonly path: string;
    readonly version: string;
    private readonly _onOutput = new EventEmitter();
    get onOutput (): EventEmitter { return this._onOutput; }
    constructor (options: IPijulOptions) {
    this.path = options.path;
    this.version = options.version;
    }
    /**
    * Opens an existing Pijul repository at the given location
    * @param repository The path to the root of the repository
    * @param dotPijul The path to the repository's .pijul directory
    */
    open (repository: string, dotPijul: string): Repository {
    return new Repository(this, repository, dotPijul);
    }
    /**
    * Runs the command to initialize a new pijul repository
    * @param repository The location where the new repository will be created
    */
    async init (repository: string): Promise<void> {
    await this.exec(repository, ['init']);
    }
    /**
    * Executes a new Pijul command in the given working directory.
    * @param cwd The current working directory in which the command will be run
    * @param args The arguments that will be passed to the command
    * @param options The options for the spawned child process
    */
    async exec (cwd: string, args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
    options = Object.assign({ cwd }, options);
    return await this._exec(args, options);
    }
    /**
    * Executes a new pijul command with the given arguments and decodes the execution result from a buffer into a string
    * @param args The arguments that will be passed to the command
    * @param options The options for the spawned child process
    */
    private async _exec (args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
    const child = this.spawn(args, options);
    if (options.onSpawn) {
    options.onSpawn(child);
    }
    if (options.input) {
    child.stdin!.end(options.input, 'utf8');
    }
    const bufferResult = await exec(child, options.cancellationToken);
    if (options.log !== false && bufferResult.stderr.length > 0) {
    this.log(`${bufferResult.stderr}\n`);
    }
    let encoding = options.encoding ?? 'utf8';
    encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
    const result: IExecutionResult<string> = {
    exitCode: bufferResult.exitCode,
    stdout: iconv.decode(bufferResult.stdout, encoding),
    stderr: bufferResult.stderr
    };
    if (bufferResult.exitCode) {
    return await Promise.reject<IExecutionResult<string>>(new Error('Error executing pijul command'));
    }
    return result;
    }
    /**
    * Spawn a Pijul process using the given arguments to utilize the Pijul CLI
    * @param args The arguments that will be passed to the child process
    * @param options The options that will be used to spawn the new process
    */
    private spawn (args: string[], options: SpawnOptions = {}): cp.ChildProcess {
    if (!this.path) {
    throw new Error('Pijul could not be found in the system.');
    }
    if (!options.stdio && !options.input) {
    options.stdio = ['ignore', null, null]; // Unless provided, ignore stdin and leave default streams for stdout and stderr
    }
    if (!options.log) {
    this.log(`> pijul ${args.join(' ')}\n`);
    }
    return cp.spawn(this.path, args, options);
    }
    /**
    * Emit a log message as an event
    * @param message The message to be emitted
    */
    private log (message: string): void {
    this._onOutput.emit('log', message);
    }
    }
    /**
    * The repository class represents a single Pijul repository.
    * It holds a reference to the Pijul installation which it uses
    * for interacting with the Pijul CLI. The Repository class exposes
    * functions for all of the Pijul CLI commands which require an
    * existing repository to run.
    */
    export class Repository {
    constructor (
    private readonly _pijul: Pijul,
    private readonly repositoryRoot: string,
    readonly dotPijul: string
    ) {}
    get pijul (): Pijul {
    return this._pijul;
    }
    get root (): string {
    return this.repositoryRoot;
    }
    }
    /**
    * Handles the execution of a child process and the capture of its results, cancelling it if necessary
    * @param child The child process being executed
    * @param cancellationToken A cancellation token that can be used to cancel the child process
    */
    async function exec (child: cp.ChildProcess, cancellationToken?: CancellationToken): Promise<IExecutionResult<Buffer>> {
    if (!child.stdout || !child.stderr) {
    throw new Error('Failed to get stdout or stderr from git process.');
    }
    if (cancellationToken?.isCancellationRequested) {
    throw new Error('Cancelled');
    }
    const disposables: IDisposable[] = [];
    // Create handles for stdout and stderr events
    const once = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void): void => {
    ee.once(name, fn);
    disposables.push(toDisposable(() => ee.removeListener(name, fn)));
    };
    const on = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void): void => {
    ee.on(name, fn);
    disposables.push(toDisposable(() => ee.removeListener(name, fn)));
    };
    // Create a promise representing all the different results of the child process
    let result = Promise.all<any>([
    new Promise<number>((resolve, reject) => {
    once(child, 'error', (err) => (reject(err)));
    once(child, 'exit', resolve);
    }),
    new Promise<Buffer>(resolve => {
    const buffers: Buffer[] = [];
    on(child.stdout!, 'data', (b: Buffer) => buffers.push(b));
    once(child.stdout!, 'close', () => resolve(Buffer.concat(buffers)));
    }),
    new Promise<string>(resolve => {
    const buffers: Buffer[] = [];
    on(child.stderr!, 'data', (b: Buffer) => buffers.push(b));
    once(child.stderr!, 'close', () => resolve(Buffer.concat(buffers).toString('utf8')));
    })
    ]) as Promise<[number, Buffer, string]>;
    if (cancellationToken) {
    const cancellationPromise = new Promise<[number, Buffer, string]>((resolve, reject) => {
    onceEvent(cancellationToken.onCancellationRequested)(() => {
    try {
    child.kill();
    } catch (err) {
    // Do nothing
    }
    reject(new Error('Cancelled'));
    });
    });
    result = Promise.race([result, cancellationPromise]);
    }
    try {
    const [exitCode, stdout, stderr] = await result;
    return { exitCode, stdout, stderr };
    } finally {
    dispose(disposables);
    }
    }
    /**
    * Interface for representing the results of the execution of a child process
    */
    export interface IExecutionResult<T extends string | Buffer> {
    exitCode: number
    stdout: T
    stderr: string
    }
    /**
    * Interface for representing the options for spawning a child process
    */
    export interface SpawnOptions extends cp.SpawnOptions {
    input?: string
    encoding?: string
    log?: boolean
    cancellationToken?: CancellationToken
    onSpawn?: (childProcess: cp.ChildProcess) => void
    }