Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send project metadata to the index #1122

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bin-src/trace.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import meow from 'meow';

import { getRepositoryRoot } from '../node-src/git/git';
import { getDependentStoryFiles } from '../node-src/lib/getDependentStoryFiles';
import { isPackageManifestFile } from '../node-src/lib/utils';
import { readStatsFile } from '../node-src/tasks/readStatsFile';
Expand Down Expand Up @@ -91,6 +92,9 @@ export async function main(argv: string[]) {
untraced: flags.untraced,
traceChanged: flags.mode || true,
},
git: {
rootPath: await getRepositoryRoot(),
},
} as any;
const stats = await readStatsFile(flags.statsFile);
const changedFiles = input.map((f) => f.replace(/^\.\//, ''));
Expand Down
167 changes: 167 additions & 0 deletions node-src/git/execGit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { PassThrough, Transform } from 'node:stream';
import { beforeEach } from 'node:test';

import { execaCommand as rawExecaCommand } from 'execa';
import { describe, expect, it, vitest } from 'vitest';

import gitNoCommits from '../ui/messages/errors/gitNoCommits';
import gitNotInitialized from '../ui/messages/errors/gitNotInitialized';
import gitNotInstalled from '../ui/messages/errors/gitNotInstalled';
import { execGitCommand, execGitCommandCountLines, execGitCommandOneLine } from './execGit';

vitest.mock('execa');

const execaCommand = vitest.mocked(rawExecaCommand);
beforeEach(() => {
execaCommand.mockReset();
});

describe('execGitCommand', () => {
it('returns execa output if it works', async () => {
execaCommand.mockResolvedValue({
all: Buffer.from('some output'),
} as any);

expect(await execGitCommand('some command')).toEqual('some output');
});

it('errors if there is no output', async () => {
execaCommand.mockResolvedValue({
all: undefined,
} as any);

await expect(execGitCommand('some command')).rejects.toThrow(/Unexpected missing git/);
});

it('handles missing git error', async () => {
execaCommand.mockRejectedValue(new Error('not a git repository'));

await expect(execGitCommand('some command')).rejects.toThrow(
gitNotInitialized({ command: 'some command' })
);
});

it('handles git not found error', async () => {
execaCommand.mockRejectedValue(new Error('git not found'));

await expect(execGitCommand('some command')).rejects.toThrow(
gitNotInstalled({ command: 'some command' })
);
});

it('handles no commits yet', async () => {
execaCommand.mockRejectedValue(new Error('does not have any commits yet'));

await expect(execGitCommand('some command')).rejects.toThrow(
gitNoCommits({ command: 'some command' })
);
});

it('rethrows arbitrary errors', async () => {
execaCommand.mockRejectedValue(new Error('something random'));
await expect(execGitCommand('some command')).rejects.toThrow('something random');
});
});

function createExecaStreamer() {
let resolver;
let rejecter;
const promiseLike = new Promise((aResolver, aRejecter) => {
resolver = aResolver;
rejecter = aRejecter;
}) as Promise<unknown> & {
stdout: Transform;
kill: () => void;
_rejecter: (err: Error) => void;
};
promiseLike.stdout = new PassThrough();
promiseLike.kill = resolver;
promiseLike._rejecter = rejecter;
return promiseLike;
}

describe('execGitCommandOneLine', () => {
it('returns the first line if the command works', async () => {
const streamer = createExecaStreamer();
execaCommand.mockReturnValue(streamer as any);

const promise = execGitCommandOneLine('some command');

streamer.stdout.write('First line\n');
streamer.stdout.write('Second line\n');

expect(await promise).toEqual('First line');
});

it('returns the output if the command only has one line', async () => {
const streamer = createExecaStreamer();
execaCommand.mockReturnValue(streamer as any);

const promise = execGitCommandOneLine('some command');

streamer.stdout.write('First line\n');
streamer.stdout.end();

expect(await promise).toEqual('First line');
});

it('Return an error if the command has no ouput', async () => {
const streamer = createExecaStreamer();
execaCommand.mockReturnValue(streamer as any);

const promise = execGitCommandOneLine('some command');

streamer.kill();

await expect(promise).rejects.toThrow(/missing git command output/);
});

it('rethrows arbitrary errors', async () => {
const streamer = createExecaStreamer();
execaCommand.mockReturnValue(streamer as any);

const promise = execGitCommandOneLine('some command');

streamer._rejecter(new Error('some error'));

await expect(promise).rejects.toThrow(/some error/);
});
});

describe('execGitCommandCountLines', () => {
it('counts lines, many', async () => {
const streamer = createExecaStreamer();
execaCommand.mockReturnValue(streamer as any);

const promise = execGitCommandCountLines('some command');

streamer.stdout.write('First line\n');
streamer.stdout.write('Second line\n');
streamer.kill();

expect(await promise).toEqual(2);
});

it('counts lines, one', async () => {
const streamer = createExecaStreamer();
execaCommand.mockReturnValue(streamer as any);

const promise = execGitCommandCountLines('some command');

streamer.stdout.write('First line\n');
streamer.kill();

expect(await promise).toEqual(1);
});

it('counts lines, none', async () => {
const streamer = createExecaStreamer();
execaCommand.mockReturnValue(streamer as any);

const promise = execGitCommandCountLines('some command');

streamer.kill();

expect(await promise).toEqual(0);
});
});
120 changes: 120 additions & 0 deletions node-src/git/execGit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { createInterface } from 'node:readline';

import { execaCommand } from 'execa';

import gitNoCommits from '../ui/messages/errors/gitNoCommits';
import gitNotInitialized from '../ui/messages/errors/gitNotInitialized';
import gitNotInstalled from '../ui/messages/errors/gitNotInstalled';

const defaultOptions: Parameters<typeof execaCommand>[1] = {
env: { LANG: 'C', LC_ALL: 'C' }, // make sure we're speaking English
timeout: 20_000, // 20 seconds
all: true, // interleave stdout and stderr
shell: true, // we'll deal with escaping ourselves (for now)
};

/**
* Execute a Git command in the local terminal.
*
* @param command The command to execute.
* @param options Execa options
*
* @returns The result of the command from the terminal.
*/
export async function execGitCommand(
command: string,
options?: Parameters<typeof execaCommand>[1]
) {
try {
const { all } = await execaCommand(command, { ...defaultOptions, ...options });

if (all === undefined) {
throw new Error(`Unexpected missing git command output for command: '${command}'`);
}

return all.toString();
} catch (error) {
const { message } = error;

if (message.includes('not a git repository')) {
throw new Error(gitNotInitialized({ command }));
}

if (message.includes('git not found')) {
throw new Error(gitNotInstalled({ command }));
}

if (message.includes('does not have any commits yet')) {
throw new Error(gitNoCommits({ command }));
}

throw error;
}
}

/**
* Execute a Git command in the local terminal and just get the first line.
*
* @param command The command to execute.
* @param options Execa options
*
* @returns The first line of the command from the terminal.
*/
export async function execGitCommandOneLine(
command: string,
options?: Parameters<typeof execaCommand>[1]
) {
const process = execaCommand(command, { ...defaultOptions, buffer: false, ...options });

return Promise.race([
// This promise will resolve only if there is an error or it times out
(async () => {
await process;

throw new Error(`Unexpected missing git command output for command: '${command}'`);
})(),
// We expect this promise to resolve first
new Promise<string>((resolve, reject) => {
if (!process.stdout) {
return reject(new Error('Unexpected missing stdout'));
}

const rl = createInterface(process.stdout);
rl.once('line', (line) => {
rl.close();
process.kill();

resolve(line);
});
}),
]);
}

/**
* Execute a Git command in the local terminal and count the lines in the result
*
* @param command The command to execute.
* @param options Execa options
*
* @returns The number of lines the command returned
*/
export async function execGitCommandCountLines(
command: string,
options?: Parameters<typeof execaCommand>[1]
) {
const process = execaCommand(command, { ...defaultOptions, buffer: false, ...options });
if (!process.stdout) {
throw new Error('Unexpected missing stdout');
}

let lineCount = 0;
const rl = createInterface(process.stdout);
rl.on('line', () => {
lineCount += 1;
});

// If the process errors, this will throw
await process;

return lineCount;
}
3 changes: 2 additions & 1 deletion node-src/git/getParentCommits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import gql from 'fake-tag';

import { localBuildsSpecifier } from '../lib/localBuildsSpecifier';
import { Context } from '../types';
import { commitExists, execGitCommand } from './git';
import { execGitCommand } from './execGit';
import { commitExists } from './git';

export const FETCH_N_INITIAL_BUILD_COMMITS = 20;

Expand Down
Loading