diff --git a/addons/isl-server/src/Repository.ts b/addons/isl-server/src/Repository.ts index 5eee869ac02bd..d008c9ea11c92 100644 --- a/addons/isl-server/src/Repository.ts +++ b/addons/isl-server/src/Repository.ts @@ -10,7 +10,6 @@ import type {KindOfChange, PollKind} from './WatchForChanges'; import type {TrackEventName} from './analytics/eventNames'; import type {ConfigLevel, ResolveCommandConflictOutput} from './commands'; import type {RepositoryContext} from './serverTypes'; -import type {ExecaError} from 'execa'; import type { CommitInfo, Disposable, @@ -41,6 +40,7 @@ import type { CwdInfo, } from 'isl/src/types'; import type {Comparison} from 'shared/Comparison'; +import type {EjecaOptions, EjecaError} from 'shared/ejeca'; import {Internal} from './Internal'; import {OperationQueue} from './OperationQueue'; @@ -76,10 +76,9 @@ import { import { findPublicAncestor, handleAbortSignalOnProcess, - isExecaError, + isEjecaError, serializeAsyncCall, } from './utils'; -import execa from 'execa'; import { settableConfigNames, allConfigNames, @@ -92,6 +91,7 @@ import {revsetArgsForComparison} from 'shared/Comparison'; import {LRU} from 'shared/LRU'; import {RateLimiter} from 'shared/RateLimiter'; import {TypedEventEmitter} from 'shared/TypedEventEmitter'; +import {ejeca} from 'shared/ejeca'; import {exists} from 'shared/fs'; import {removeLeadingPathSep} from 'shared/pathUtils'; import {notEmpty, randomId, nullthrows} from 'shared/utils'; @@ -689,7 +689,7 @@ export class Repository { throw new Error(`command "${args.join(' ')}" is not allowed`); } - const execution = execa(command, args, options); + const execution = ejeca(command, args, options); // It would be more appropriate to call this in reponse to execution.on('spawn'), but // this seems to be inconsistent about firing in all versions of node. // Just send spawn immediately. Errors during spawn like ENOENT will still be reported by `exit`. @@ -708,7 +708,7 @@ export class Repository { const result = await execution; onProgress('exit', result.exitCode || 0); } catch (err) { - onProgress('exit', isExecaError(err) ? err.exitCode : -1); + onProgress('exit', isEjecaError(err) ? err.exitCode : -1); throw err; } } @@ -789,7 +789,7 @@ export class Repository { this.uncommittedChangesEmitter.emit('change', this.uncommittedChanges); } catch (err) { let error = err; - if (isExecaError(error)) { + if (isEjecaError(error)) { if (error.stderr.includes('checkout is currently in progress')) { this.initialConnectionContext.logger.info( 'Ignoring `sl status` error caused by in-progress checkout', @@ -799,8 +799,8 @@ export class Repository { } this.initialConnectionContext.logger.error('Error fetching files: ', error); - if (isExecaError(error)) { - error = simplifyExecaError(error); + if (isEjecaError(error)) { + error = simplifyEjecaError(error); } // emit an error, but don't save it to this.uncommittedChanges @@ -895,13 +895,13 @@ export class Repository { if (internalError) { error = internalError; } - if (isExecaError(error) && error.stderr.includes('Please check your internet connection')) { + if (isEjecaError(error) && error.stderr.includes('Please check your internet connection')) { error = Error('Network request failed. Please check your internet connection.'); } this.initialConnectionContext.logger.error('Error fetching commits: ', error); - if (isExecaError(error)) { - error = simplifyExecaError(error); + if (isEjecaError(error)) { + error = simplifyEjecaError(error); } this.smartlogCommitsChangesEmitter.emit('change', { @@ -1373,7 +1373,7 @@ export class Repository { /** Which event name to track for this command. If undefined, generic 'RunCommand' is used. */ eventName: TrackEventName | undefined, ctx: RepositoryContext, - options?: execa.Options, + options?: EjecaOptions, timeout?: number, ) { const id = randomId(); @@ -1508,8 +1508,8 @@ function isUnhealthyEdenFs(cwd: string): Promise { } /** - * Extract the actually useful stderr part of the Execa Error, to avoid the long command args being printed first. + * Extract the actually useful stderr part of the Ejeca Error, to avoid the long command args being printed first. * */ -function simplifyExecaError(error: ExecaError): Error { +function simplifyEjecaError(error: EjecaError): Error { return new Error(error.stderr.trim() || error.message); } diff --git a/addons/isl-server/src/ServerToClientAPI.ts b/addons/isl-server/src/ServerToClientAPI.ts index 0421c769140fe..3ef90f056c7c1 100644 --- a/addons/isl-server/src/ServerToClientAPI.ts +++ b/addons/isl-server/src/ServerToClientAPI.ts @@ -11,7 +11,6 @@ import type {ServerSideTracker} from './analytics/serverSideTracker'; import type {Logger} from './logger'; import type {ServerPlatform} from './serverPlatform'; import type {RepositoryContext} from './serverTypes'; -import type {ExecaError} from 'execa'; import type {TypeaheadResult} from 'isl-components/Types'; import type {Serializable} from 'isl/src/serialize'; import type { @@ -29,6 +28,7 @@ import type { CodeReviewProviderSpecificClientToServerMessages, StableLocationData, } from 'isl/src/types'; +import type {EjecaError} from 'shared/ejeca'; import type {ExportStack, ImportedStack} from 'shared/types/stack'; import {generatedFilesDetector} from './GeneratedFiles'; @@ -963,7 +963,7 @@ export default class ServerToClientAPI { url: {value: result.stdout}, }); }) - .catch((err: ExecaError) => { + .catch((err: EjecaError) => { this.logger.error('Failed to get repo url at hash:', err); this.postMessage({ type: 'gotRepoUrlAtHash', diff --git a/addons/isl-server/src/__tests__/Repository.test.ts b/addons/isl-server/src/__tests__/Repository.test.ts index 962754791ee16..9b801e70df027 100644 --- a/addons/isl-server/src/__tests__/Repository.test.ts +++ b/addons/isl-server/src/__tests__/Repository.test.ts @@ -13,20 +13,16 @@ import type {RunnableOperation} from 'isl/src/types'; import {absolutePathForFileInRepo, Repository} from '../Repository'; import {makeServerSideTracker} from '../analytics/serverSideTracker'; import {extractRepoInfoFromUrl, setConfigOverrideForTests} from '../commands'; -import * as execa from 'execa'; import {CommandRunner, type MergeConflicts, type ValidatedRepoInfo} from 'isl/src/types'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import * as ejeca from 'shared/ejeca'; import * as fsUtils from 'shared/fs'; import {clone, mockLogger, nextTick} from 'shared/testUtils'; /* eslint-disable require-await */ -jest.mock('execa', () => { - return jest.fn(); -}); - jest.mock('../WatchForChanges', () => { class MockWatchForChanges { dispose = jest.fn(); @@ -42,12 +38,12 @@ const mockTracker = makeServerSideTracker( jest.fn(), ); -function mockExeca( +function mockEjeca( cmds: Array<[RegExp, (() => {stdout: string} | Error) | {stdout: string} | Error]>, ) { - return jest.spyOn(execa, 'default').mockImplementation(((cmd: string, args: Array) => { + return jest.spyOn(ejeca, 'ejeca').mockImplementation(((cmd: string, args: Array) => { const argStr = cmd + ' ' + args?.join(' '); - const execaOther = { + const ejecaOther = { kill: jest.fn(), on: jest.fn((event, cb) => { // immediately call exit cb to teardown timeout @@ -65,15 +61,15 @@ function mockExeca( if (value instanceof Error) { throw value; } - return {...execaOther, ...value}; + return {...ejecaOther, ...value}; } } - return {...execaOther, stdout: ''}; - }) as unknown as typeof execa.default); + return {...ejecaOther, stdout: ''}; + }) as unknown as typeof ejeca.ejeca); } -function processExitError(code: number, message: string): execa.ExecaError { - const err = new Error(message) as execa.ExecaError; +function processExitError(code: number, message: string): ejeca.EjecaError { + const err = new Error(message) as ejeca.EjecaError; err.exitCode = code; return err; } @@ -94,9 +90,9 @@ describe('Repository', () => { }); it('setting command name', async () => { - const execaSpy = mockExeca([]); + const ejecaSpy = mockEjeca([]); await Repository.getRepoInfo({...ctx, cmd: 'slb'}); - expect(execaSpy).toHaveBeenCalledWith( + expect(ejecaSpy).toHaveBeenCalledWith( 'slb', expect.arrayContaining(['root']), expect.anything(), @@ -106,7 +102,7 @@ describe('Repository', () => { describe('extracting github repo info', () => { beforeEach(() => { setConfigOverrideForTests([['github.pull_request_domain', 'github.com']]); - mockExeca([ + mockEjeca([ [/^sl root --dotdir/, {stdout: '/path/to/myRepo/.sl'}], [/^sl root/, {stdout: '/path/to/myRepo'}], [ @@ -184,7 +180,7 @@ describe('Repository', () => { it('extracting repo info', async () => { setConfigOverrideForTests([]); setPathsDefault('mononoke://0.0.0.0/fbsource'); - mockExeca([ + mockEjeca([ [/^sl root --dotdir/, {stdout: '/path/to/myRepo/.sl'}], [/^sl root/, {stdout: '/path/to/myRepo'}], ]); @@ -203,7 +199,7 @@ describe('Repository', () => { it('handles cwd not exists', async () => { const err = new Error('cwd does not exist') as Error & {code: string}; err.code = 'ENOENT'; - mockExeca([[/^sl root/, err]]); + mockEjeca([[/^sl root/, err]]); const info = (await Repository.getRepoInfo(ctx)) as ValidatedRepoInfo; expect(info).toEqual({ type: 'cwdDoesNotExist', @@ -213,7 +209,7 @@ describe('Repository', () => { it('handles missing executables on windows', async () => { const osSpy = jest.spyOn(os, 'platform').mockImplementation(() => 'win32'); - mockExeca([ + mockEjeca([ [ /^sl root/, processExitError( @@ -235,7 +231,7 @@ describe('Repository', () => { it('prevents setting configs not in the allowlist', async () => { setConfigOverrideForTests([]); setPathsDefault('mononoke://0.0.0.0/fbsource'); - mockExeca([ + mockEjeca([ [/^sl root --dotdir/, {stdout: '/path/to/myRepo/.sl'}], [/^sl root/, {stdout: '/path/to/myRepo'}], ]); @@ -257,9 +253,9 @@ describe('Repository', () => { pullRequestDomain: undefined, }; - let execaSpy: ReturnType; + let ejecaSpy: ReturnType; beforeEach(() => { - execaSpy = mockExeca([]); + ejecaSpy = mockEjeca([]); }); async function runOperation(op: Partial) { @@ -284,7 +280,7 @@ describe('Repository', () => { args: ['commit', '--message', 'hi'], }); - expect(execaSpy).toHaveBeenCalledWith( + expect(ejecaSpy).toHaveBeenCalledWith( 'sl', ['commit', '--message', 'hi', '--noninteractive'], expect.anything(), @@ -296,7 +292,7 @@ describe('Repository', () => { args: ['rebase', '--rev', {type: 'succeedable-revset', revset: 'aaa'}], }); - expect(execaSpy).toHaveBeenCalledWith( + expect(ejecaSpy).toHaveBeenCalledWith( 'sl', ['rebase', '--rev', 'max(successors(aaa))', '--noninteractive'], expect.anything(), @@ -308,7 +304,7 @@ describe('Repository', () => { args: ['rebase', '--rev', {type: 'exact-revset', revset: 'aaa'}], }); - expect(execaSpy).toHaveBeenCalledWith( + expect(ejecaSpy).toHaveBeenCalledWith( 'sl', ['rebase', '--rev', 'aaa', '--noninteractive'], expect.anything(), @@ -320,7 +316,7 @@ describe('Repository', () => { args: ['add', {type: 'repo-relative-file', path: 'path/to/file.txt'}], }); - expect(execaSpy).toHaveBeenCalledWith( + expect(ejecaSpy).toHaveBeenCalledWith( 'sl', ['add', '../repo/path/to/file.txt', '--noninteractive'], expect.anything(), @@ -332,7 +328,7 @@ describe('Repository', () => { args: ['commit', {type: 'config', key: 'ui.allowemptycommit', value: 'True'}], }); - expect(execaSpy).toHaveBeenCalledWith( + expect(ejecaSpy).toHaveBeenCalledWith( 'sl', ['commit', '--config', 'ui.allowemptycommit=True', '--noninteractive'], expect.anything(), @@ -344,7 +340,7 @@ describe('Repository', () => { args: ['debugsh'], }); - expect(execaSpy).not.toHaveBeenCalledWith( + expect(ejecaSpy).not.toHaveBeenCalledWith( 'sl', ['debugsh', '--noninteractive'], expect.anything(), @@ -356,7 +352,7 @@ describe('Repository', () => { args: ['commit', {type: 'config', key: 'foo.bar', value: '1'}], }); - expect(execaSpy).not.toHaveBeenCalledWith( + expect(ejecaSpy).not.toHaveBeenCalledWith( 'sl', expect.arrayContaining(['commit', '--config', 'foo.bar=1']), expect.anything(), @@ -368,7 +364,7 @@ describe('Repository', () => { args: ['commit', '--config', 'foo.bar=1'], }); - expect(execaSpy).not.toHaveBeenCalledWith( + expect(ejecaSpy).not.toHaveBeenCalledWith( 'sl', expect.arrayContaining(['commit', '--config', 'foo.bar=1']), expect.anything(), @@ -394,10 +390,10 @@ www/flib/intern/entity/diff/EntPhabricatorDiffSchema.php it('parses sloc', async () => { const repo = new Repository(repoInfo, ctx); - const execaSpy = mockExeca([[/^sl diff/, () => ({stdout: EXAMPLE_DIFFSTAT})]]); + const ejecaSpy = mockEjeca([[/^sl diff/, () => ({stdout: EXAMPLE_DIFFSTAT})]]); const results = repo.fetchSignificantLinesOfCode(ctx, 'abcdef', ['generated.file']); await expect(results).resolves.toEqual({sloc: 45, strictSloc: 45}); - expect(execaSpy).toHaveBeenCalledWith( + expect(ejecaSpy).toHaveBeenCalledWith( 'sl', expect.arrayContaining([ 'diff', @@ -415,9 +411,9 @@ www/flib/intern/entity/diff/EntPhabricatorDiffSchema.php it('handles empty generated list', async () => { const repo = new Repository(repoInfo, ctx); - const execaSpy = mockExeca([[/^sl diff/, () => ({stdout: EXAMPLE_DIFFSTAT})]]); + const ejecaSpy = mockEjeca([[/^sl diff/, () => ({stdout: EXAMPLE_DIFFSTAT})]]); repo.fetchSignificantLinesOfCode(ctx, 'abcdef', []); - expect(execaSpy).toHaveBeenCalledWith( + expect(ejecaSpy).toHaveBeenCalledWith( 'sl', expect.arrayContaining(['diff', '-B', '-X', '**__generated__**', '-c', 'abcdef']), expect.anything(), @@ -426,11 +422,11 @@ www/flib/intern/entity/diff/EntPhabricatorDiffSchema.php it('handles multiple generated files', async () => { const repo = new Repository(repoInfo, ctx); - const execaSpy = mockExeca([[/^sl diff/, () => ({stdout: EXAMPLE_DIFFSTAT})]]); + const ejecaSpy = mockEjeca([[/^sl diff/, () => ({stdout: EXAMPLE_DIFFSTAT})]]); const generatedFiles = ['generated1.file', 'generated2.file']; repo.fetchSignificantLinesOfCode(ctx, 'abcdef', generatedFiles); await nextTick(); - expect(execaSpy).toHaveBeenCalledWith( + expect(ejecaSpy).toHaveBeenCalledWith( 'sl', expect.arrayContaining([ 'diff', @@ -470,33 +466,33 @@ www/flib/intern/entity/diff/EntPhabricatorDiffSchema.php it('uses correct revset in normal case', async () => { const repo = new Repository(repoInfo, ctx); - const execaSpy = mockExeca([]); + const ejecaSpy = mockEjeca([]); await repo.fetchSmartlogCommits(); expectCalledWithRevset( - execaSpy, + ejecaSpy, 'smartlog(((interestingbookmarks() + heads(draft())) & date(-14)) + .)', ); }); it('updates revset when changing date range', async () => { - const execaSpy = mockExeca([]); + const ejecaSpy = mockEjeca([]); const repo = new Repository(repoInfo, ctx); repo.nextVisibleCommitRangeInDays(); await repo.fetchSmartlogCommits(); expectCalledWithRevset( - execaSpy, + ejecaSpy, 'smartlog(((interestingbookmarks() + heads(draft())) & date(-60)) + .)', ); repo.nextVisibleCommitRangeInDays(); await repo.fetchSmartlogCommits(); - expectCalledWithRevset(execaSpy, 'smartlog((interestingbookmarks() + heads(draft())) + .)'); + expectCalledWithRevset(ejecaSpy, 'smartlog((interestingbookmarks() + heads(draft())) + .)'); }); it('fetches additional revsets', async () => { - const execaSpy = mockExeca([]); + const ejecaSpy = mockEjeca([]); const repo = new Repository(repoInfo, ctx); repo.stableLocations = [ @@ -504,7 +500,7 @@ www/flib/intern/entity/diff/EntPhabricatorDiffSchema.php ]; await repo.fetchSmartlogCommits(); expectCalledWithRevset( - execaSpy, + ejecaSpy, 'smartlog(((interestingbookmarks() + heads(draft())) & date(-14)) + . + present(aaa))', ); @@ -514,7 +510,7 @@ www/flib/intern/entity/diff/EntPhabricatorDiffSchema.php ]; await repo.fetchSmartlogCommits(); expectCalledWithRevset( - execaSpy, + ejecaSpy, 'smartlog(((interestingbookmarks() + heads(draft())) & date(-14)) + . + present(aaa) + present(bbb))', ); @@ -522,7 +518,7 @@ www/flib/intern/entity/diff/EntPhabricatorDiffSchema.php repo.nextVisibleCommitRangeInDays(); await repo.fetchSmartlogCommits(); expectCalledWithRevset( - execaSpy, + ejecaSpy, 'smartlog((interestingbookmarks() + heads(draft())) + . + present(aaa) + present(bbb))', ); }); @@ -610,7 +606,7 @@ ${MARK_OUT} const MOCK_CONFLICT_WITH_FILE1_RESOLVED: ResolveCommandConflictOutput = clone(MOCK_CONFLICT); MOCK_CONFLICT_WITH_FILE1_RESOLVED[0].conflicts.splice(0, 1); - // these mock values are returned by execa / fs mocks + // these mock values are returned by ejeca / fs mocks // default: start in a not-in-conflict state let slMergeDirExists = false; let conflictData: ResolveCommandConflictOutput = NOT_IN_CONFLICT; @@ -629,7 +625,7 @@ ${MARK_OUT} jest.spyOn(fsUtils, 'exists').mockImplementation(() => Promise.resolve(slMergeDirExists)); - mockExeca([ + mockEjeca([ [ /^sl resolve --tool internal:dumpjson --all/, () => ({stdout: JSON.stringify(conflictData)}), @@ -810,7 +806,7 @@ ${MARK_OUT} }); it('handles errors from `sl resolve`', async () => { - mockExeca([ + mockEjeca([ [/^sl resolve --tool internal:dumpjson --all/, new Error('failed to do the thing')], ]); @@ -995,7 +991,7 @@ describe('absolutePathForFileInRepo', () => { describe('getCwdInfo', () => { it('computes cwd path and labels', async () => { - mockExeca([[/^sl root/, {stdout: '/path/to/myRepo'}]]); + mockEjeca([[/^sl root/, {stdout: '/path/to/myRepo'}]]); jest.spyOn(fs.promises, 'realpath').mockImplementation(async (path, _opts) => { return path as string; }); @@ -1014,7 +1010,7 @@ describe('getCwdInfo', () => { }); it('uses realpath', async () => { - mockExeca([[/^sl root/, {stdout: '/data/users/name/myRepo'}]]); + mockEjeca([[/^sl root/, {stdout: '/data/users/name/myRepo'}]]); jest.spyOn(fs.promises, 'realpath').mockImplementation(async (path, _opts) => { return (path as string).replace(/^\/home\/name\//, '/data/users/name/'); }); @@ -1033,7 +1029,7 @@ describe('getCwdInfo', () => { }); it('returns null for non-repos', async () => { - mockExeca([[/^sl root/, new Error('not a repository')]]); + mockEjeca([[/^sl root/, new Error('not a repository')]]); await expect( Repository.getCwdInfo({ cmd: 'sl', diff --git a/addons/isl-server/src/__tests__/analytics.test.ts b/addons/isl-server/src/__tests__/analytics.test.ts index 036bc6585f3f5..9b93d187c614f 100644 --- a/addons/isl-server/src/__tests__/analytics.test.ts +++ b/addons/isl-server/src/__tests__/analytics.test.ts @@ -13,7 +13,7 @@ import type {RepositoryContext} from '../serverTypes'; import {Repository} from '../Repository'; import {makeServerSideTracker} from '../analytics/serverSideTracker'; import {setConfigOverrideForTests} from '../commands'; -import * as execa from 'execa'; +import * as ejeca from 'shared/ejeca'; import {mockLogger} from 'shared/testUtils'; import {defer} from 'shared/utils'; @@ -41,16 +41,12 @@ jest.mock('../WatchForChanges', () => { return {WatchForChanges: MockWatchForChanges}; }); -jest.mock('execa', () => { - return jest.fn(); -}); - -function mockExeca( +function mockEjeca( cmds: Array<[RegExp, (() => {stdout: string} | Error) | {stdout: string} | Error]>, ) { - return jest.spyOn(execa, 'default').mockImplementation(((cmd: string, args: Array) => { + return jest.spyOn(ejeca, 'ejeca').mockImplementation(((cmd: string, args: Array) => { const argStr = cmd + ' ' + args?.join(' '); - const execaOther = { + const ejecaOther = { kill: jest.fn(), on: jest.fn((event, cb) => { // immediately call exit cb to teardown timeout @@ -68,11 +64,11 @@ function mockExeca( if (value instanceof Error) { throw value; } - return {...execaOther, ...value}; + return {...ejecaOther, ...value}; } } - return {...execaOther, stdout: ''}; - }) as unknown as typeof execa.default); + return {...ejecaOther, stdout: ''}; + }) as unknown as typeof ejeca.ejeca); } describe('track', () => { @@ -124,7 +120,7 @@ describe('track', () => { ['path.default', 'https://github.com/facebook/sapling.git'], ['github.pull_request_domain', 'github.com'], ]); - const execaSpy = mockExeca([ + const ejecaSpy = mockEjeca([ [/^sl root --dotdir/, {stdout: '/path/to/myRepo/.sl'}], [/^sl root/, {stdout: '/path/to/myRepo'}], [ @@ -160,7 +156,7 @@ describe('track', () => { mockLogger, ); repo.dispose(); - execaSpy.mockClear(); + ejecaSpy.mockClear(); }); it('uses consistent session id, but different track ids', () => { diff --git a/addons/isl-server/src/commands.ts b/addons/isl-server/src/commands.ts index b6cce19b4fccd..8a9872da1473b 100644 --- a/addons/isl-server/src/commands.ts +++ b/addons/isl-server/src/commands.ts @@ -6,11 +6,12 @@ */ import type {RepositoryContext} from './serverTypes'; +import type {EjecaOptions, EjecaReturn} from 'shared/ejeca'; -import {isExecaError} from './utils'; -import execa from 'execa'; +import {isEjecaError} from './utils'; import {ConflictType, type AbsolutePath, type MergeConflicts} from 'isl/src/types'; import os from 'node:os'; +import {ejeca} from 'shared/ejeca'; export const MAX_FETCHED_FILES_PER_COMMIT = 25; export const MAX_SIMULTANEOUS_CAT_CALLS = 4; @@ -52,12 +53,12 @@ export type ResolveCommandConflictOutput = [ export async function runCommand( ctx: RepositoryContext, args_: Array, - options_?: execa.Options, + options_?: EjecaOptions, timeout: number = READ_COMMAND_TIMEOUT_MS, -): Promise> { +): Promise { const {command, args, options} = getExecParams(ctx.cmd, args_, ctx.cwd, options_); ctx.logger.log('run command: ', ctx.cwd, command, args[0]); - const result = execa(command, args, options); + const result = ejeca(command, args, options); let timedOut = false; let timeoutId: NodeJS.Timeout | undefined; @@ -76,7 +77,7 @@ export async function runCommand( const val = await result; return val; } catch (err: unknown) { - if (isExecaError(err)) { + if (isEjecaError(err)) { if (err.killed) { if (timedOut) { throw new Error('Timed out'); @@ -174,12 +175,12 @@ export function getExecParams( command: string, args_: Array, cwd: string, - options_?: execa.Options, + options_?: EjecaOptions, env?: NodeJS.ProcessEnv | Record, ): { command: string; args: Array; - options: execa.Options; + options: EjecaOptions; } { let args = [...args_, '--noninteractive']; // expandHomeDir is not supported on windows @@ -214,7 +215,7 @@ export function getExecParams( langEnv = 'C.UTF-8'; } newEnv.LANG = langEnv; - const options: execa.Options = { + const options: EjecaOptions = { ...options_, env: newEnv, cwd, diff --git a/addons/isl-server/src/github/queryGraphQL.ts b/addons/isl-server/src/github/queryGraphQL.ts index e9265b5069f96..7d658fb4314a2 100644 --- a/addons/isl-server/src/github/queryGraphQL.ts +++ b/addons/isl-server/src/github/queryGraphQL.ts @@ -6,8 +6,8 @@ */ import {Internal} from '../Internal'; -import {isExecaError} from '../utils'; -import execa from 'execa'; +import {isEjecaError} from '../utils'; +import {ejeca} from 'shared/ejeca'; export default async function queryGraphQL( query: string, @@ -39,7 +39,7 @@ export default async function queryGraphQL( args.push('-f', `query=${query}`); try { - const {stdout} = await execa('gh', args, { + const {stdout} = await ejeca('gh', args, { env: { ...((await Internal.additionalGhEnvVars?.()) ?? {}), }, @@ -52,7 +52,7 @@ export default async function queryGraphQL( return json.data; } catch (error: unknown) { - if (isExecaError(error)) { + if (isEjecaError(error)) { if (error.code === 'ENOENT' || error.code === 'EACCES') { // `gh` not installed on path throw new Error(`GhNotInstalledError: ${(error as Error).stack}`); @@ -76,7 +76,7 @@ export async function isGithubEnterprise(hostname: string): Promise { args.push('--hostname', hostname); try { - await execa('gh', args, { + await ejeca('gh', args, { env: { ...((await Internal.additionalGhEnvVars?.()) ?? {}), }, diff --git a/addons/isl-server/src/utils.ts b/addons/isl-server/src/utils.ts index 7229ec3b15e9b..8a29fe644040c 100644 --- a/addons/isl-server/src/utils.ts +++ b/addons/isl-server/src/utils.ts @@ -5,9 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import type execa from 'execa'; -import type {ExecaChildProcess, ExecaError} from 'execa'; import type {CommitInfo, SmartlogCommits} from 'isl/src/types'; +import type {EjecaError, EjecaReturn, EjecaChildProcess} from 'shared/ejeca'; import os from 'node:os'; import {truncate} from 'shared/utils'; @@ -79,7 +78,7 @@ export function serializeAsyncCall(asyncFun: () => Promise): () => Promise * This is slightly more robust than execa 6.0 and nodejs' `signal` support: * if a process was stopped (by `SIGTSTP` or `SIGSTOP`), it can still be killed. */ -export function handleAbortSignalOnProcess(child: ExecaChildProcess, signal: AbortSignal) { +export function handleAbortSignalOnProcess(child: EjecaChildProcess, signal: AbortSignal) { signal.addEventListener('abort', () => { if (os.platform() == 'win32') { // Signals are ignored on Windows. @@ -130,7 +129,7 @@ export function findPublicAncestor( * Return a JSON object. On error, the JSON object has property "error". */ export function parseExecJson( - exec: Promise>, + exec: Promise, reply: (parsed?: T, error?: string) => void, ) { exec @@ -165,7 +164,7 @@ export function parseExecJson( }); } -export function isExecaError(s: unknown): s is ExecaError & {code?: string} { +export function isEjecaError(s: unknown): s is EjecaError & {code?: string} { return s != null && typeof s === 'object' && 'exitCode' in s; } diff --git a/addons/isl/integrationTests/setup.tsx b/addons/isl/integrationTests/setup.tsx index 1f015d9d08826..46136cd27d591 100644 --- a/addons/isl/integrationTests/setup.tsx +++ b/addons/isl/integrationTests/setup.tsx @@ -7,7 +7,7 @@ import type {MessageBusStatus} from '../src/MessageBus'; import type {Disposable, RepoRelativePath} from '../src/types'; -import type {Options as ExecaOptions} from 'execa'; +import type {EjecaOptions} from 'shared/ejeca'; import type {Logger} from 'isl-server/src/logger'; import type {ServerPlatform} from 'isl-server/src/serverPlatform'; import type {RepositoryContext} from 'isl-server/src/serverTypes'; @@ -151,7 +151,7 @@ export async function initRepo() { tracker: mockTracker, }; - async function sl(args: Array, options?: ExecaOptions) { + async function sl(args: Array, options?: EjecaOptions) { testLogger.log(ctx.cmd, ...args); const result = await runCommand(ctx, args, { ...options, diff --git a/addons/screenshot-tool/src/testRepo.ts b/addons/screenshot-tool/src/testRepo.ts index 5625f7752278f..158d5aff6854e 100644 --- a/addons/screenshot-tool/src/testRepo.ts +++ b/addons/screenshot-tool/src/testRepo.ts @@ -6,7 +6,7 @@ */ import {getCacheDir, sha1} from './utils'; -import {execa} from 'execa'; +import {ejeca} from 'shared/ejeca'; import * as fs from 'node:fs/promises'; import {join} from 'node:path'; import {dirSync} from 'tmp'; @@ -72,7 +72,7 @@ export class TestRepo { async run(args: Array, input = ''): Promise { const env = {...process.env, SL_AUTOMATION: '1', HGPLAIN: '1'}; logger.info('Running', this.command, args.join(' ')); - const child = await execa(this.command, args, { + const child = await ejeca(this.command, args, { cwd: this.repoPath, input: Buffer.from(input), env,