Merge pull request #83 from tiktok/feat-sparo-completion

[sparo] shell completion feature
This commit is contained in:
Adrian Zhang 2024-08-15 22:30:48 +08:00 committed by GitHub
commit 30a6efef00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 1099 additions and 136 deletions

2
.gitignore vendored
View file

@ -63,6 +63,8 @@ jspm_packages/
.idea/ .idea/
*.iml *.iml
.nvmrc
# Rush temporary files # Rush temporary files
common/deploy/ common/deploy/
common/temp/ common/temp/

View file

@ -19,22 +19,25 @@ export class SparoCommandLine {
private constructor() {} private constructor() {}
public static async launchAsync(launchOptions: ILaunchOptions): Promise<void> { public static async launchAsync(launchOptions: ILaunchOptions): Promise<void> {
if (launchOptions.collectTelemetryAsync) { const isCompletion: boolean = ['completion', '--get-yargs-completions'].includes(process.argv[2]);
const telemetryService: TelemetryService = await getFromContainerAsync(TelemetryService); if (!isCompletion) {
telemetryService.setCollectTelemetryFunction(launchOptions.collectTelemetryAsync); if (launchOptions.collectTelemetryAsync) {
} const telemetryService: TelemetryService = await getFromContainerAsync(TelemetryService);
telemetryService.setCollectTelemetryFunction(launchOptions.collectTelemetryAsync);
}
if (GitVersionCompatibility.reportGitRequiredVersion()) { if (GitVersionCompatibility.reportGitRequiredVersion()) {
process.exit(1); process.exit(1);
} }
SparoStartupBanner.logBanner({ SparoStartupBanner.logBanner({
callerPackageJson: launchOptions.callerPackageJson callerPackageJson: launchOptions.callerPackageJson
}); });
if (launchOptions.additionalSkeletonFolders) { if (launchOptions.additionalSkeletonFolders) {
const gitSparseCheckoutService: GitSparseCheckoutService = const gitSparseCheckoutService: GitSparseCheckoutService =
await getFromContainerAsync(GitSparseCheckoutService); await getFromContainerAsync(GitSparseCheckoutService);
gitSparseCheckoutService.setAdditionalSkeletonFolders(launchOptions.additionalSkeletonFolders); gitSparseCheckoutService.setAdditionalSkeletonFolders(launchOptions.additionalSkeletonFolders);
}
} }
const sparo: SparoCommandLine = new SparoCommandLine(); const sparo: SparoCommandLine = new SparoCommandLine();
@ -79,6 +82,6 @@ export class SparoCommandLine {
} }
private _supportedCommand(commandName: string): boolean { private _supportedCommand(commandName: string): boolean {
return this._commandsMap.has(commandName); return this._commandsMap.has(commandName) || commandName === 'completion';
} }
} }

View file

@ -28,6 +28,18 @@ export class AutoConfigCommand implements ICommand<IAutoConfigCommandOptions> {
type: 'boolean', type: 'boolean',
hidden: true, hidden: true,
default: false default: false
})
.completion('completion', false, (current, argv, done) => {
const longParameters: string[] = [argv.overwrite ? '' : '--overwrite'].filter(Boolean);
if (current.startsWith('--')) {
done(
longParameters.filter((parameter) => {
return parameter.startsWith(current);
})
);
} else {
done([]);
}
}); });
} }
public handler = async ( public handler = async (
@ -52,7 +64,4 @@ export class AutoConfigCommand implements ICommand<IAutoConfigCommandOptions> {
throw e; throw e;
} }
}; };
public getHelp(): string {
return '';
}
} }

View file

@ -17,5 +17,4 @@ export interface ICommand<O extends {}> {
builder: (yargs: Argv<O>) => void; builder: (yargs: Argv<O>) => void;
handler: (args: ArgumentsCamelCase<O>, terminalService: TerminalService) => Promise<void>; handler: (args: ArgumentsCamelCase<O>, terminalService: TerminalService) => Promise<void>;
getHelp: () => string;
} }

View file

@ -1,5 +1,6 @@
import * as child_process from 'child_process'; import * as child_process from 'child_process';
import { inject } from 'inversify'; import { inject } from 'inversify';
import { JsonFile } from '@rushstack/node-core-library';
import { Command } from '../../decorator'; import { Command } from '../../decorator';
import { GitService } from '../../services/GitService'; import { GitService } from '../../services/GitService';
import { GitRemoteFetchConfigService } from '../../services/GitRemoteFetchConfigService'; import { GitRemoteFetchConfigService } from '../../services/GitRemoteFetchConfigService';
@ -32,7 +33,7 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
@inject(GitRemoteFetchConfigService) private _gitRemoteFetchConfigService!: GitRemoteFetchConfigService; @inject(GitRemoteFetchConfigService) private _gitRemoteFetchConfigService!: GitRemoteFetchConfigService;
@inject(SparoProfileService) private _sparoProfileService!: SparoProfileService; @inject(SparoProfileService) private _sparoProfileService!: SparoProfileService;
public builder(yargs: Argv<{}>): void { public builder = (yargs: Argv<{}>): void => {
/** /**
* git checkout [-q] [-f] [-m] [<branch>] * git checkout [-q] [-f] [-m] [<branch>]
* git checkout [-q] [-f] [-m] --detach [<branch>] * git checkout [-q] [-f] [-m] --detach [<branch>]
@ -98,8 +99,114 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
default: [], default: [],
description: description:
'Checkout projects downstream from (and including itself and all its dependencies) project <from..>, can be used together with option --profile/--add-profile to form a union selection of the two options. The projects selectors here will never replace what have been checked out by profiles' 'Checkout projects downstream from (and including itself and all its dependencies) project <from..>, can be used together with option --profile/--add-profile to form a union selection of the two options. The projects selectors here will never replace what have been checked out by profiles'
})
.completion('completion', false, (current, argv, done) => {
const isNoProfile: boolean = argv.profile.some(
(profile: string | boolean) => typeof profile === 'boolean' && profile === false
);
const shortParameters: string[] = [argv.b ? '' : '-b', argv.B ? '' : '-B'].filter(Boolean);
const longParameters: string[] = [
isNoProfile ? '' : '--no-profile',
isNoProfile ? '' : '--profile',
isNoProfile ? '' : '--add-profile',
'--to',
'--from'
].filter(Boolean);
if (current === '-') {
done(shortParameters);
} else if (current === '--') {
done(longParameters);
} else if (current === '--profile' || current === '--add-profile') {
const profileNameSet: Set<string> = new Set(this._sparoProfileService.loadProfileNames());
for (const profile of argv.profile) {
if (typeof profile === 'string') {
profileNameSet.delete(profile);
}
}
for (const profile of argv.addProfile) {
if (typeof profile === 'string') {
profileNameSet.delete(profile);
}
}
done(Array.from(profileNameSet));
} else if (current === '--to' || current === '--from') {
let rushJson: { projects?: { packageName: string }[] } = {};
const root: string = this._gitService.getRepoInfo().root;
try {
rushJson = JsonFile.load(`${root}/rush.json`);
} catch (e) {
// no-catch
}
if (Array.isArray(rushJson.projects)) {
const packageNameSet: Set<string> = new Set<string>(
rushJson.projects.map((project) => project.packageName)
);
if (current === '--to') {
for (const packageName of argv.to) {
packageNameSet.delete(packageName);
}
}
if (current === '--from') {
for (const packageName of argv.from) {
packageNameSet.delete(packageName);
}
}
const packageNames: string[] = Array.from(packageNameSet).sort();
if (process.cwd() !== root) {
packageNames.unshift('.');
}
done(packageNames);
}
done([]);
} else if (current.startsWith('--')) {
done(longParameters.filter((parameter) => parameter.startsWith(current)));
} else {
const previous: string = process.argv.slice(-2)[0];
if (previous === '--profile' || previous === '--add-profile') {
const profileNameSet: Set<string> = new Set(this._sparoProfileService.loadProfileNames());
for (const profile of argv.profile) {
if (typeof profile === 'string') {
profileNameSet.delete(profile);
}
}
for (const profile of argv.addProfile) {
if (typeof profile === 'string') {
profileNameSet.delete(profile);
}
}
done(Array.from(profileNameSet).filter((profileName) => profileName.startsWith(current)));
} else if (previous === '--to' || previous === '--from') {
let rushJson: { projects?: { packageName: string }[] } = {};
const root: string = this._gitService.getRepoInfo().root;
try {
rushJson = JsonFile.load(`${root}/rush.json`);
} catch (e) {
// no-catch
}
if (Array.isArray(rushJson.projects)) {
const packageNameSet: Set<string> = new Set<string>(
rushJson.projects.map((project) => project.packageName)
);
if (previous === '--to') {
for (const packageName of argv.to) {
packageNameSet.delete(packageName);
}
}
if (previous === '--from') {
for (const packageName of argv.from) {
packageNameSet.delete(packageName);
}
}
const packageNames: string[] = Array.from(packageNameSet).sort();
done(packageNames.filter((packageName) => packageName.startsWith(current)));
}
done([]);
}
done([]);
}
}); });
} };
public handler = async ( public handler = async (
args: ArgumentsCamelCase<ICheckoutCommandOptions>, args: ArgumentsCamelCase<ICheckoutCommandOptions>,
@ -258,10 +365,6 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
} }
}; };
public getHelp(): string {
return '';
}
private _ensureBranchInLocal(branch: string): boolean { private _ensureBranchInLocal(branch: string): boolean {
// fetch from remote // fetch from remote
const remote: string = this._gitService.getBranchRemote(branch); const remote: string = this._gitService.getBranchRemote(branch);

View file

@ -44,7 +44,4 @@ export class CICheckoutCommand implements ICommand<ICICheckoutCommandOptions> {
from from
}); });
}; };
public getHelp(): string {
return 'sparse help';
}
} }

View file

@ -71,8 +71,4 @@ export class CICloneCommand implements ICommand<ICloneCommandOptions> {
terminal.writeLine(`Remember to run "cd ${directory}"`); terminal.writeLine(`Remember to run "cd ${directory}"`);
}; };
public getHelp(): string {
return `clone help`;
}
} }

View file

@ -22,7 +22,7 @@ export interface ICloneCommandOptions {
@Command() @Command()
export class CloneCommand implements ICommand<ICloneCommandOptions> { export class CloneCommand implements ICommand<ICloneCommandOptions> {
public cmd: string = 'clone <repository> [directory]'; public cmd: string = 'clone <repository> [directory]';
public description: string = ''; public description: string = 'Clone a repository into a new directory';
@inject(GitService) private _gitService!: GitService; @inject(GitService) private _gitService!: GitService;
@inject(GitCloneService) private _gitCloneService!: GitCloneService; @inject(GitCloneService) private _gitCloneService!: GitCloneService;
@ -61,6 +61,17 @@ export class CloneCommand implements ICommand<ICloneCommandOptions> {
return 'You must specify a repository to clone.'; return 'You must specify a repository to clone.';
} }
return true; return true;
})
.completion('completion', false, (current, argv, done) => {
const longParameters: string[] = ['--profile', '--branch', '--full', '--skip-git-config'];
if (current === '--') {
done(longParameters);
} else if (current.startsWith('--')) {
done(longParameters.filter((parameter) => parameter.startsWith(current)));
} else {
done([]);
}
}); });
} }
@ -154,8 +165,4 @@ export class CloneCommand implements ICommand<ICloneCommandOptions> {
terminal.writeLine(' ' + Colorize.cyan('sparo init-profile --profile <profile_name>')); terminal.writeLine(' ' + Colorize.cyan('sparo init-profile --profile <profile_name>'));
} }
}; };
public getHelp(): string {
return `clone help`;
}
} }

View file

@ -22,7 +22,7 @@ export class FetchCommand implements ICommand<IFetchCommandOptions> {
@inject(GitService) private _gitService!: GitService; @inject(GitService) private _gitService!: GitService;
@inject(GitRemoteFetchConfigService) private _gitRemoteFetchConfigService!: GitRemoteFetchConfigService; @inject(GitRemoteFetchConfigService) private _gitRemoteFetchConfigService!: GitRemoteFetchConfigService;
public builder(yargs: Argv<{}>): void { public builder = (yargs: Argv<{}>): void => {
/** /**
* sparo fetch <remote> <branch> [--all] * sparo fetch <remote> <branch> [--all]
*/ */
@ -31,8 +31,31 @@ export class FetchCommand implements ICommand<IFetchCommandOptions> {
.positional('branch', { type: 'string' }) .positional('branch', { type: 'string' })
.string('remote') .string('remote')
.string('branch') .string('branch')
.boolean('all'); .boolean('all')
} .completion('completion', false, (current, argv, done) => {
const longParameters: string[] = [argv.all ? '' : '--all', argv.tags ? '' : '--tags'].filter(Boolean);
if (current === 'fetch') {
done(['origin']);
} else if (current === 'origin') {
const branchNames: string[] = this._gitRemoteFetchConfigService.getBranchNamesFromRemote('origin');
branchNames.unshift('HEAD');
done(branchNames);
} else if (current === '--') {
done(longParameters);
} else if (current.startsWith('--')) {
done(longParameters.filter((parameter) => parameter.startsWith(current)));
} else {
const previous: string = process.argv.slice(-2)[0];
if (previous === 'origin') {
const branchNames: string[] =
this._gitRemoteFetchConfigService.getBranchNamesFromRemote('origin');
branchNames.unshift('HEAD');
done(branchNames.filter((name) => name.startsWith(current)));
}
done([]);
}
});
};
public handler = async ( public handler = async (
args: ArgumentsCamelCase<IFetchCommandOptions>, args: ArgumentsCamelCase<IFetchCommandOptions>,
@ -75,8 +98,4 @@ export class FetchCommand implements ICommand<IFetchCommandOptions> {
restoreSingleBranchCallback?.(); restoreSingleBranchCallback?.();
}; };
public getHelp(): string {
return `fetch help`;
}
} }

View file

@ -27,8 +27,4 @@ export class GitCheckoutCommand implements ICommand<{}> {
terminal.writeDebugLine(`proxy args in git-checkout command: ${JSON.stringify(rawArgs)}`); terminal.writeDebugLine(`proxy args in git-checkout command: ${JSON.stringify(rawArgs)}`);
gitService.executeGitCommand({ args: rawArgs }); gitService.executeGitCommand({ args: rawArgs });
}; };
public getHelp(): string {
return `git-checkout help`;
}
} }

View file

@ -27,8 +27,4 @@ export class GitCloneCommand implements ICommand<{}> {
terminal.writeDebugLine(`proxy args in git-clone command: ${JSON.stringify(rawArgs)}`); terminal.writeDebugLine(`proxy args in git-clone command: ${JSON.stringify(rawArgs)}`);
gitService.executeGitCommand({ args: rawArgs }); gitService.executeGitCommand({ args: rawArgs });
}; };
public getHelp(): string {
return `git-clone help`;
}
} }

View file

@ -28,8 +28,4 @@ export class GitFetchCommand implements ICommand<{}> {
terminal.writeDebugLine(`proxy args in git-fetch command: ${JSON.stringify(rawArgs)}`); terminal.writeDebugLine(`proxy args in git-fetch command: ${JSON.stringify(rawArgs)}`);
gitService.executeGitCommand({ args: rawArgs }); gitService.executeGitCommand({ args: rawArgs });
}; };
public getHelp(): string {
return `git-fetch help`;
}
} }

View file

@ -27,7 +27,4 @@ export class GitPullCommand implements ICommand<{}> {
terminal.writeDebugLine(`proxy args in git-pull command: ${JSON.stringify(rawArgs)}`); terminal.writeDebugLine(`proxy args in git-pull command: ${JSON.stringify(rawArgs)}`);
gitService.executeGitCommand({ args: rawArgs }); gitService.executeGitCommand({ args: rawArgs });
}; };
public getHelp(): string {
return `git-pull help`;
}
} }

View file

@ -28,7 +28,18 @@ export class InitProfileCommand implements ICommand<IInitProjectCommandOptions>
description: 'The name of the profile to initialize.' description: 'The name of the profile to initialize.'
}) })
.demandOption(['profile']) .demandOption(['profile'])
.usage('Usage: $0 init-profile --profile <profile>'); .usage('Usage: $0 init-profile --profile <profile>')
.completion('completion', false, (current, argv, done) => {
const longParameters: string[] = [argv.profile ? '' : '--profile'].filter(Boolean);
if (current === '--') {
done(longParameters);
} else if (current.startsWith('--')) {
done(longParameters.filter((parameter) => parameter.startsWith(current)));
} else {
done([]);
}
});
} }
public handler = async ( public handler = async (
@ -66,8 +77,4 @@ export class InitProfileCommand implements ICommand<IInitProjectCommandOptions>
this._terminalService.terminal.writeLine(); this._terminalService.terminal.writeLine();
this._terminalService.terminal.writeLine(' ' + Colorize.cyan(destinationPath)); this._terminalService.terminal.writeLine(' ' + Colorize.cyan(destinationPath));
}; };
public getHelp(): string {
return 'init-profile help';
}
} }

View file

@ -1,9 +1,10 @@
import childProcess from 'child_process'; import childProcess from 'child_process';
import { Sort } from '@rushstack/node-core-library'; import { JsonFile, Sort } from '@rushstack/node-core-library';
import { inject } from 'inversify'; import { inject } from 'inversify';
import { SparoProfileService } from '../../services/SparoProfileService'; import { SparoProfileService } from '../../services/SparoProfileService';
import { ICommand } from './base'; import { ICommand } from './base';
import { Command } from '../../decorator'; import { Command } from '../../decorator';
import { GitService } from '../../services/GitService';
import { GitSparseCheckoutService } from '../../services/GitSparseCheckoutService'; import { GitSparseCheckoutService } from '../../services/GitSparseCheckoutService';
import type { Argv, ArgumentsCamelCase } from 'yargs'; import type { Argv, ArgumentsCamelCase } from 'yargs';
@ -24,14 +25,58 @@ export class ListProfilesCommand implements ICommand<IListProfilesCommandOptions
public description: string = public description: string =
'List all available profiles or query profiles that contain the specified project name'; 'List all available profiles or query profiles that contain the specified project name';
@inject(SparoProfileService) private _sparoProfileService!: SparoProfileService; @inject(SparoProfileService) private _sparoProfileService!: SparoProfileService;
@inject(GitService) private _gitService!: GitService;
@inject(GitSparseCheckoutService) private _gitSparseCheckoutService!: GitSparseCheckoutService; @inject(GitSparseCheckoutService) private _gitSparseCheckoutService!: GitSparseCheckoutService;
public builder(yargs: Argv<IListProfilesCommandOptions>): void { public builder = (yargs: Argv<IListProfilesCommandOptions>): void => {
yargs.option('project', { yargs
type: 'string', .option('project', {
description: 'List all profiles contains this specified project name' type: 'string',
}); description: 'List all profiles contains this specified project name'
} })
.completion('completion', false, (current, argv, done) => {
const longParameters: string[] = [argv.project ? '' : '--project'].filter(Boolean);
if (current === '--') {
done(longParameters);
} else if (current === '--project') {
let rushJson: { projects?: { packageName: string }[] } = {};
const root: string = this._gitService.getRepoInfo().root;
try {
rushJson = JsonFile.load(`${root}/rush.json`);
} catch (e) {
// no-catch
}
if (Array.isArray(rushJson.projects)) {
const packageNameSet: Set<string> = new Set<string>(
rushJson.projects.map((project) => project.packageName)
);
const packageNames: string[] = Array.from(packageNameSet).sort();
done(packageNames);
}
} else if (current.startsWith('--')) {
done(longParameters.filter((parameter) => parameter.startsWith(current)));
} else {
const previous: string = process.argv.slice(-2)[0];
if (previous === '--project') {
let rushJson: { projects?: { packageName: string }[] } = {};
const root: string = this._gitService.getRepoInfo().root;
try {
rushJson = JsonFile.load(`${root}/rush.json`);
} catch (e) {
// no-catch
}
if (Array.isArray(rushJson.projects)) {
const packageNameSet: Set<string> = new Set<string>(
rushJson.projects.map((project) => project.packageName)
);
const packageNames: string[] = Array.from(packageNameSet).sort();
done(packageNames.filter((packageName) => packageName.startsWith(current)));
}
}
done([]);
}
});
};
public handler = async ( public handler = async (
args: ArgumentsCamelCase<IListProfilesCommandOptions>, args: ArgumentsCamelCase<IListProfilesCommandOptions>,
terminalService: TerminalService terminalService: TerminalService
@ -85,7 +130,4 @@ export class ListProfilesCommand implements ICommand<IListProfilesCommandOptions
} }
} }
}; };
public getHelp(): string {
return 'list-profiles help';
}
} }

View file

@ -37,6 +37,55 @@ export class PullCommand implements ICommand<IPullCommandOptions> {
}) })
.array('profile') .array('profile')
.default('profile', []) .default('profile', [])
.option('get-yargs-completions', {
hidden: true,
type: 'boolean'
})
.completion('completion', false, (current, argv, done) => {
const isNoProfile: boolean = argv.profile.some(
(profile: string | boolean) => typeof profile === 'boolean' && profile === false
);
const longParameters: string[] = [
isNoProfile ? '' : '--profile',
isNoProfile ? '' : '--no-profile'
].filter(Boolean);
if (current === 'pull') {
done(['origin']);
} else if (current === 'origin') {
const branchNames: string[] = this._gitRemoteFetchConfigService.getBranchNamesFromRemote('origin');
branchNames.unshift('HEAD');
done(branchNames);
} else if (current === '--') {
done(longParameters);
} else if (current === '--profile') {
const profileNameSet: Set<string> = new Set(this._sparoProfileService.loadProfileNames());
for (const profile of argv.profile) {
if (typeof profile === 'string') {
profileNameSet.delete(profile);
}
}
done(Array.from(profileNameSet));
} else if (current.startsWith('--')) {
done(longParameters.filter((parameter) => parameter.startsWith(current)));
} else {
const previous: string = process.argv.slice(-2)[0];
if (previous === '--profile') {
const profileNameSet: Set<string> = new Set(this._sparoProfileService.loadProfileNames());
for (const profile of argv.profile) {
if (typeof profile === 'string') {
profileNameSet.delete(profile);
}
}
done(Array.from(profileNameSet).filter((profileName) => profileName.startsWith(current)));
} else if (previous === 'origin') {
const branchNames: string[] =
this._gitRemoteFetchConfigService.getBranchNamesFromRemote('origin');
branchNames.unshift('HEAD');
done(branchNames.filter((name) => name.startsWith(current)));
}
done([]);
}
})
.parserConfiguration({ 'unknown-options-as-args': true }) .parserConfiguration({ 'unknown-options-as-args': true })
.usage( .usage(
'$0 pull [options] [repository] [refsepc...] [--profile <profile_name> | --no-profile]' + '$0 pull [options] [repository] [refsepc...] [--profile <profile_name> | --no-profile]' +
@ -105,8 +154,4 @@ export class PullCommand implements ICommand<IPullCommandOptions> {
isProfileRestoreFromLocal isProfileRestoreFromLocal
}); });
}; };
public getHelp(): string {
return `pull help`;
}
} }

View file

@ -1,7 +1,11 @@
import * as path from 'path';
import { inject } from 'inversify'; import { inject } from 'inversify';
import { FileSystem, FolderItem } from '@rushstack/node-core-library';
import { Service } from '../decorator'; import { Service } from '../decorator';
import yargs, { type MiddlewareFunction, type Argv } from 'yargs'; import yargs, { type MiddlewareFunction, type Argv, type AsyncCompletionFunction } from 'yargs';
import { TerminalService } from './TerminalService'; import { TerminalService } from './TerminalService';
import { CommandService, type ICommandInfo } from './CommandService';
import { getFromContainer } from '../di/container';
@Service() @Service()
export class ArgvService { export class ArgvService {
@ -34,6 +38,7 @@ export class ArgvService {
// --verbose // --verbose
.boolean('verbose') .boolean('verbose')
.middleware([this._terminalMiddleware]) .middleware([this._terminalMiddleware])
.completion('completion', false, this._completionFunction)
.parseAsync(); .parseAsync();
} }
@ -63,4 +68,123 @@ export class ArgvService {
this._terminalService.setIsVerbose(verbose); this._terminalService.setIsVerbose(verbose);
} }
}; };
/**
* To test completion, run
*
* sparo --get-yargs-completions sparo <command> ...
*/
private _completionFunction: AsyncCompletionFunction = async (current, argv, done): Promise<void> => {
const nativeGitCommands: ICommandInfo[] = [
{
name: 'add',
description: 'add file contents to the index'
},
{ name: 'branch', description: 'list, create, or delete branches' },
{ name: 'checkout', description: 'checkout a branch or paths to the working tree' },
{ name: 'clone', description: 'clone a repository into a new directory' },
{ name: 'commit', description: 'record changes to the repository' },
{ name: 'diff', description: 'show changes between commits, commit and working tree, etc' },
{ name: 'fetch', description: 'download objects and refs from another repository' },
{ name: 'log', description: 'show commit logs' },
{ name: 'merge', description: 'join two or more development histories together' },
{ name: 'pull', description: 'fetch from and merge with another repository or a local branch' },
{ name: 'push', description: 'update remote refs along with associated objects' },
{ name: 'rebase', description: 'forward-port local commits to the updated upstream head' },
{ name: 'reset', description: 'reset current HEAD to the specified state' },
{ name: 'restore', description: 'restore working tree files' },
{ name: 'status', description: 'show the working tree status' }
];
const commandService: CommandService = getFromContainer(CommandService);
const { commandInfos: sparoCommandInfos } = commandService;
const finalCommands: ICommandInfo[] = Array.from(sparoCommandInfos.values());
for (const nativeGitCommand of nativeGitCommands) {
if (sparoCommandInfos.has(nativeGitCommand.name)) {
continue;
}
finalCommands.push(nativeGitCommand);
}
const finalCommandNameSet: Set<string> = new Set<string>(finalCommands.map((x) => x.name));
const userInputCmdName: string = argv._[1] || '';
if (current.includes('sparo')) {
// top level completion
done(finalCommands.map(({ name, description }) => `${name}:${description}`));
} else if (finalCommandNameSet.has(userInputCmdName)) {
switch (current) {
case 'add': {
done(this._getFileCompletions());
break;
}
case 'commit':
case 'branch':
case 'diff':
case 'log':
case 'merge':
case 'rebase':
case 'restore':
case 'status': {
// TODO: completion for the following commands seem of less use, implement on demand
done([]);
break;
}
default: {
break;
}
}
switch (userInputCmdName) {
case 'add': {
done(this._getFileCompletions(current));
break;
}
case 'rebase': {
const shortParameters: string[] = [argv.i || argv.interactive ? '' : '-i'].filter(Boolean);
const longParameters: string[] = [
argv.continue ? '' : '--continue',
argv.skip ? '' : '--skip',
argv.abort ? '' : '--abort',
argv.i || argv.interactive ? '' : '--interactive'
].filter(Boolean);
if (current === '-') {
done(shortParameters);
} else if (current === '--') {
done(longParameters);
} else if (current.startsWith('--')) {
done(longParameters.filter((parameter) => parameter.startsWith(current)));
}
break;
}
default: {
break;
}
}
} else if (current) {
done(
finalCommands
.filter(({ name }) => name.startsWith(current))
.map(({ name, description }) => `${name}:${description}`)
);
}
done([]);
};
private _getFileCompletions(partial: string = ''): string[] {
const dir: string = partial.endsWith('/') ? partial : path.dirname(partial);
const base: string = partial.endsWith('/') ? '' : path.basename(partial);
try {
const items: FolderItem[] = FileSystem.readFolderItems(dir);
return items
.filter((item) => item.name.startsWith(base))
.map((item) => {
if (item.isDirectory()) {
return path.join(dir, item.name, '/');
}
return path.join(dir, item.name);
});
} catch (e) {
// Return empty result if encounter issues
return [];
}
}
} }

View file

@ -1,7 +1,6 @@
import { inject } from 'inversify'; import { inject } from 'inversify';
import type { Argv } from 'yargs'; import type { Argv } from 'yargs';
import type { ICommand } from '../cli/commands/base'; import type { ICommand } from '../cli/commands/base';
import { HelpTextService } from './HelpTextService';
import { TerminalService } from './TerminalService'; import { TerminalService } from './TerminalService';
import { Service } from '../decorator'; import { Service } from '../decorator';
import { ArgvService } from './ArgvService'; import { ArgvService } from './ArgvService';
@ -11,23 +10,31 @@ import { getCommandName } from '../cli/commands/util';
export interface ICommandServiceParams { export interface ICommandServiceParams {
yargs: Argv<{}>; yargs: Argv<{}>;
helpTextService: HelpTextService;
terminalService: TerminalService; terminalService: TerminalService;
} }
export interface ICommandInfo {
name: string;
description: string;
}
@Service() @Service()
export class CommandService { export class CommandService {
@inject(ArgvService) private _yargs!: ArgvService; @inject(ArgvService) private _yargs!: ArgvService;
@inject(HelpTextService) private _helpTextService!: HelpTextService;
@inject(TerminalService) private _terminalService!: TerminalService; @inject(TerminalService) private _terminalService!: TerminalService;
@inject(TelemetryService) private _telemetryService!: TelemetryService; @inject(TelemetryService) private _telemetryService!: TelemetryService;
private _hasInternalError: boolean = false; private _hasInternalError: boolean = false;
private _commandInfos: Map<string, ICommandInfo> = new Map<string, ICommandInfo>();
public register<O extends {}>(command: ICommand<O>): void { public register<O extends {}>(command: ICommand<O>): void {
const { cmd, description, builder, handler, getHelp } = command; const { cmd, description, builder, handler } = command;
const { _terminalService: terminalService } = this; const { _terminalService: terminalService } = this;
const { terminal } = terminalService; const { terminal } = terminalService;
const commandName: string = getCommandName(cmd); const commandName: string = getCommandName(cmd);
this._commandInfos.set(commandName, {
name: commandName,
description
});
this._yargs.yargsArgv.command<O>( this._yargs.yargsArgv.command<O>(
cmd, cmd,
description, description,
@ -63,10 +70,13 @@ export class CommandService {
} }
} }
); );
this._helpTextService.set(commandName, getHelp());
} }
public setHasInternalError(): void { public setHasInternalError(): void {
this._hasInternalError = true; this._hasInternalError = true;
} }
public get commandInfos(): Map<string, ICommandInfo> {
return this._commandInfos;
}
} }

View file

@ -128,6 +128,10 @@ export class GitRemoteFetchConfigService {
/** /**
* Reads remote.origin.fetch from git configuration. It returns a mapping * Reads remote.origin.fetch from git configuration. It returns a mapping
*
* Map {
* 'master' => Set { '+refs/heads/master:refs/remotes/origin/master' }
* }
*/ */
public getBranchesInfoFromRemoteFetchConfig(remoteFetchConfig: string[]): Map<string, Set<string>> { public getBranchesInfoFromRemoteFetchConfig(remoteFetchConfig: string[]): Map<string, Set<string>> {
const branchRegExp: RegExp = /^(?:\+)?refs\/heads\/([^:]+):/; const branchRegExp: RegExp = /^(?:\+)?refs\/heads\/([^:]+):/;
@ -149,6 +153,17 @@ export class GitRemoteFetchConfigService {
return branchToValues; return branchToValues;
} }
/**
* This function is used for completion
*/
public getBranchNamesFromRemote(remote: string): string[] {
const remoteFetchConfig: string[] | undefined = this._getRemoteFetchInGitConfig(remote);
if (!remoteFetchConfig) {
return [];
}
return Array.from(this.getBranchesInfoFromRemoteFetchConfig(remoteFetchConfig).keys());
}
private _getRemoteFetchInGitConfig(remote: string): string[] | undefined { private _getRemoteFetchInGitConfig(remote: string): string[] | undefined {
const result: string | undefined = this._gitService.getGitConfig(`remote.${remote}.fetch`, { const result: string | undefined = this._gitService.getGitConfig(`remote.${remote}.fetch`, {
array: true array: true

View file

@ -1,18 +0,0 @@
import { inject } from 'inversify';
import { Service } from '../decorator';
import { TerminalService } from './TerminalService';
export interface IHelpTextParams {
terminalService: TerminalService;
}
@Service()
export class HelpTextService {
@inject(TerminalService) private _terminalService!: TerminalService;
public helpTextMap: Map<string, string> = new Map<string, string>();
public set(name: string, text: string): void {
this._terminalService.terminal.writeVerboseLine(`set help text "${name}" to "${text}"`);
this.helpTextMap.set(name, text);
}
}

View file

@ -30,6 +30,20 @@ export class SparoProfileService {
@inject(LocalState) private _localState!: LocalState; @inject(LocalState) private _localState!: LocalState;
@inject(GitSparseCheckoutService) private _gitSparseCheckoutService!: GitSparseCheckoutService; @inject(GitSparseCheckoutService) private _gitSparseCheckoutService!: GitSparseCheckoutService;
/**
* This function is used for completion
*/
public loadProfileNames(): string[] {
const sparoProfileFolder: string = this._sparoProfileFolder;
const sparoProfilePaths: string[] = FileSystem.readFolderItemNames(sparoProfileFolder, {
absolutePaths: true
});
return sparoProfilePaths
.filter((profilePath) => profilePath.endsWith('.json'))
.map((profilePath) => SparoProfileService._getProfileName(profilePath))
.sort();
}
public async loadProfilesAsync(): Promise<void> { public async loadProfilesAsync(): Promise<void> {
if (!this._loadPromise) { if (!this._loadPromise) {
this._loadPromise = (async () => { this._loadPromise = (async () => {

View file

@ -0,0 +1,19 @@
// This is a workaround for https://github.com/eslint/eslint/issues/3458
require('@rushstack/heft-node-rig/profiles/default/includes/eslint/patch/modern-module-resolution');
// This is a workaround for https://github.com/microsoft/rushstack/issues/3021
require('@rushstack/heft-node-rig/profiles/default/includes/eslint/patch/custom-config-package-names');
module.exports = {
extends: [
'@rushstack/heft-node-rig/profiles/default/includes/eslint/profile/node-trusted-tool',
'@rushstack/heft-node-rig/profiles/default/includes/eslint/mixins/friendly-locals'
],
parserOptions: { tsconfigRootDir: __dirname },
overrides: [
{
files: ['*.ts', '*.tsx'],
rules: {}
}
]
};

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) TikTok Pte. Ltd.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,11 @@
# sparo-completion-test
Building this project tests sparo command completion outputs
# Details
`lib/start-test.js` is run after building the project. This scripts generate the output text files under `temp/etc`. In local builds, those files are copied to `etc` folder. During a CI build, the files under these two folders are compared and the CI build fails if they are different. This ensures that files under `etc` folder must be up to date in the PR, and people who review the PR must approve any changes.
# How to fix the build errors
Run `rush build -t sparo-completion-test` to regenerate files under `etc` folder and commit them into Git.

View file

@ -0,0 +1,24 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json",
"extends": "@rushstack/heft-node-rig/profiles/default/config/heft.json",
"phasesByName": {
"build": {
"tasksByName": {
"post-compile": {
"taskDependencies": ["typescript"],
"taskPlugin": {
"pluginName": "run-script-plugin",
"pluginPackage": "@rushstack/heft",
"options": {
"scriptPath": "lib/start-test.js"
}
}
}
}
}
}
}

View file

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@rushstack/heft-node-rig"
}

View file

@ -0,0 +1,3 @@
Running "sparo --get-yargs-completions sparo add __fixture__/":
__fixture__/dir-a/
__fixture__/file.txt

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo add __fixture__/dir-a/file":
__fixture__/dir-a/file-under-a.txt

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo add __fixture__":
__fixture__/

View file

@ -0,0 +1 @@
Running "sparo --get-yargs-completions sparo auto-config":

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo auto-config --":
--overwrite

View file

@ -0,0 +1,4 @@
Running "sparo --get-yargs-completions sparo checkout --add-profile":
my-team
sparo-development
sparo-website

View file

@ -0,0 +1,3 @@
Running "sparo --get-yargs-completions sparo checkout --add-profile spa":
sparo-development
sparo-website

View file

@ -0,0 +1 @@
Running "sparo --get-yargs-completions sparo checkout":

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo checkout --from web":
website

View file

@ -0,0 +1,6 @@
Running "sparo --get-yargs-completions sparo checkout --":
--no-profile
--profile
--add-profile
--to
--from

View file

@ -0,0 +1,3 @@
Running "sparo --get-yargs-completions sparo checkout --no-profile --":
--to
--from

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo checkout --pro":
--profile

View file

@ -0,0 +1,4 @@
Running "sparo --get-yargs-completions sparo checkout --profile":
my-team
sparo-development
sparo-website

View file

@ -0,0 +1,3 @@
Running "sparo --get-yargs-completions sparo checkout --profile spa":
sparo-development
sparo-website

View file

@ -0,0 +1,3 @@
Running "sparo --get-yargs-completions sparo checkout --add-profile sparo-website --add-profile":
my-team
sparo-development

View file

@ -0,0 +1,3 @@
Running "sparo --get-yargs-completions sparo checkout --profile sparo-website --profile":
my-team
sparo-development

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo checkout --to web":
website

View file

@ -0,0 +1 @@
Running "sparo --get-yargs-completions sparo clone":

View file

@ -0,0 +1,5 @@
Running "sparo --get-yargs-completions sparo clone --":
--profile
--branch
--full
--skip-git-config

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo clone --pro":
--profile

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo clone --profile":
--profile

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo fetch --al":
--all

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo fetch":
origin

View file

@ -0,0 +1,3 @@
Running "sparo --get-yargs-completions sparo fetch --":
--all
--tags

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo fetch --all --":
--tags

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo fetch --tags --":
--all

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo fetch origin HEA":
HEAD

View file

@ -0,0 +1 @@
Running "sparo --get-yargs-completions sparo init-profile":

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo init-profile --":
--profile

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo init-profile --pro":
--profile

View file

@ -0,0 +1 @@
Running "sparo --get-yargs-completions sparo list-profiles":

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo list-profiles --":
--project

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo list-profiles --pro":
--project

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo list-profiles --project web":
website

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo pull":
origin

View file

@ -0,0 +1,3 @@
Running "sparo --get-yargs-completions sparo pull --":
--profile
--no-profile

View file

@ -0,0 +1 @@
Running "sparo --get-yargs-completions sparo pull --no-profile --":

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo pull --pro":
--profile

View file

@ -0,0 +1,4 @@
Running "sparo --get-yargs-completions sparo pull --profile":
my-team
sparo-development
sparo-website

View file

@ -0,0 +1,3 @@
Running "sparo --get-yargs-completions sparo pull --profile spa":
sparo-development
sparo-website

View file

@ -0,0 +1,3 @@
Running "sparo --get-yargs-completions sparo pull --profile sparo-website --profile":
my-team
sparo-development

View file

@ -0,0 +1,5 @@
Running "sparo --get-yargs-completions sparo rebase --":
--continue
--skip
--abort
--interactive

View file

@ -0,0 +1,4 @@
Running "sparo --get-yargs-completions sparo rebase --abort --":
--continue
--skip
--interactive

View file

@ -0,0 +1,4 @@
Running "sparo --get-yargs-completions sparo rebase --continue --":
--skip
--abort
--interactive

View file

@ -0,0 +1,4 @@
Running "sparo --get-yargs-completions sparo rebase -i --":
--continue
--skip
--abort

View file

@ -0,0 +1,4 @@
Running "sparo --get-yargs-completions sparo rebase --interactive --":
--continue
--skip
--abort

View file

@ -0,0 +1,4 @@
Running "sparo --get-yargs-completions sparo rebase --skip --":
--continue
--abort
--interactive

View file

@ -0,0 +1,2 @@
Running "sparo --get-yargs-completions sparo rebase -":
-i

View file

@ -0,0 +1 @@
Running "sparo --get-yargs-completions sparo rebase -i -":

View file

@ -0,0 +1 @@
Running "sparo --get-yargs-completions sparo rebase --interactive -":

View file

@ -0,0 +1,23 @@
Running "sparo --get-yargs-completions sparo":
auto-config:Automatic setup optimized git config
list-profiles:List all available profiles or query profiles that contain the specified project name
init-profile:Initialize a new profile.
clone:Clone a repository into a new directory
checkout:Updates files in the working tree to match the version in the index or the specified tree. If no pathspec was given, git checkout will also update HEAD to set the specified branch as the current branch.
fetch:fetch remote branch to local
pull:Incorporates changes from a remote repository into the current branch.
git-clone:original git clone command
git-checkout:original git checkout command
git-fetch:original git fetch command
git-pull:original git pull command
add:add file contents to the index
branch:list, create, or delete branches
commit:record changes to the repository
diff:show changes between commits, commit and working tree, etc
log:show commit logs
merge:join two or more development histories together
push:update remote refs along with associated objects
rebase:forward-port local commits to the updated upstream head
reset:reset current HEAD to the specified state
restore:restore working tree files
status:show the working tree status

View file

@ -0,0 +1,24 @@
{
"name": "sparo-completion-test",
"description": "Building this project tests Sparo command-line outputs",
"version": "1.0.0",
"private": true,
"scripts": {
"_phase:build": "heft run --only build -- --clean",
"build": "heft build --clean"
},
"dependencies": {
"@rushstack/node-core-library": "~3.64.2",
"build-test-utilities": "workspace:*"
},
"devDependencies": {
"@rushstack/heft": "0.64.3",
"@rushstack/heft-node-rig": "2.4.5",
"@types/heft-jest": "1.0.6",
"@types/node": "20.11.16",
"eslint": "8.56.0",
"typescript": "~5.3.3"
},
"keywords": [],
"license": "MIT"
}

View file

@ -0,0 +1,318 @@
import {
ICommandDefinition,
executeCommandsAndCollectOutputs,
updateOrCompareOutputs
} from 'build-test-utilities';
import type { IRunScriptOptions } from '@rushstack/heft';
/**
* This build test is highly inspired by the build test for api-extractor in rushstack.
*/
export async function runAsync(runScriptOptions: IRunScriptOptions): Promise<void> {
const {
heftTaskSession: {
logger,
parameters: { production }
},
heftConfiguration: { buildFolderPath }
} = runScriptOptions;
const prefixArgs: string[] = ['--get-yargs-completions', 'sparo'];
const commandDefinitions: ICommandDefinition[] = [
{
kind: 'sparo-command',
name: 'sparo-top-level-completion',
args: prefixArgs.concat([])
},
// auto-config
{
kind: 'sparo-command',
name: 'auto-config-completion',
args: prefixArgs.concat(['auto-config'])
},
{
kind: 'sparo-command',
name: 'auto-config-long-parameters-completion',
args: prefixArgs.concat(['auto-config', '--'])
},
// list-profiles
{
kind: 'sparo-command',
name: 'list-profiles-completion',
args: prefixArgs.concat(['list-profiles'])
},
{
kind: 'sparo-command',
name: 'list-profiles-long-parameters-completion',
args: prefixArgs.concat(['list-profiles', '--'])
},
{
kind: 'sparo-command',
name: 'list-profiles-pro-completion',
args: prefixArgs.concat(['list-profiles', '--pro'])
},
{
kind: 'sparo-command',
name: 'list-profiles-project-web-completion',
args: prefixArgs.concat(['list-profiles', '--project', 'web'])
},
// init-profile
{
kind: 'sparo-command',
name: 'init-profile-completion',
args: prefixArgs.concat(['init-profile'])
},
{
kind: 'sparo-command',
name: 'init-profile-long-parameters-completion',
args: prefixArgs.concat(['init-profile', '--'])
},
{
kind: 'sparo-command',
name: 'init-profile-pro-completion',
args: prefixArgs.concat(['init-profile', '--pro'])
},
// clone
{
kind: 'sparo-command',
name: 'clone-completion',
args: prefixArgs.concat(['clone'])
},
{
kind: 'sparo-command',
name: 'clone-long-parameters-completion',
args: prefixArgs.concat(['clone', '--'])
},
{
kind: 'sparo-command',
name: 'clone-long-parameters-completion',
args: prefixArgs.concat(['clone', '--'])
},
{
kind: 'sparo-command',
name: 'clone-pro-completion',
args: prefixArgs.concat(['clone', '--pro'])
},
{
kind: 'sparo-command',
name: 'clone-profile-completion',
args: prefixArgs.concat(['clone', '--profile'])
},
// checkout
{
kind: 'sparo-command',
name: 'checkout-completion',
args: prefixArgs.concat(['checkout'])
},
{
kind: 'sparo-command',
name: 'checkout-long-parameters-completion',
args: prefixArgs.concat(['checkout', '--'])
},
{
kind: 'sparo-command',
name: 'checkout-pro-completion',
args: prefixArgs.concat(['checkout', '--pro'])
},
{
kind: 'sparo-command',
name: 'checkout-profile-completion',
args: prefixArgs.concat(['checkout', '--profile'])
},
{
kind: 'sparo-command',
name: 'checkout-profile-spa-completion',
args: prefixArgs.concat(['checkout', '--profile', 'spa'])
},
{
kind: 'sparo-command',
name: 'checkout-second-profile-completion',
args: prefixArgs.concat(['checkout', '--profile', 'sparo-website', '--profile'])
},
{
kind: 'sparo-command',
name: 'checkout-add-profile-completion',
args: prefixArgs.concat(['checkout', '--add-profile'])
},
{
kind: 'sparo-command',
name: 'checkout-add-profile-spa-completion',
args: prefixArgs.concat(['checkout', '--add-profile', 'spa'])
},
{
kind: 'sparo-command',
name: 'checkout-second-add-profile-completion',
args: prefixArgs.concat(['checkout', '--add-profile', 'sparo-website', '--add-profile'])
},
{
kind: 'sparo-command',
name: 'checkout-no-profile-completion',
args: prefixArgs.concat(['checkout', '--no-profile', '--'])
},
{
kind: 'sparo-command',
name: 'checkout-to-web-completion',
args: prefixArgs.concat(['checkout', '--to', 'web'])
},
{
kind: 'sparo-command',
name: 'checkout-from-web-completion',
args: prefixArgs.concat(['checkout', '--from', 'web'])
},
// fetch
{
kind: 'sparo-command',
name: 'fetch-completion',
args: prefixArgs.concat(['fetch'])
},
{
kind: 'sparo-command',
name: 'fetch-long-parameters-completion',
args: prefixArgs.concat(['fetch', '--'])
},
{
kind: 'sparo-command',
name: 'fetch-long-parameters-with-all-completion',
args: prefixArgs.concat(['fetch', '--all', '--'])
},
{
kind: 'sparo-command',
name: 'fetch-long-parameters-with-tags-completion',
args: prefixArgs.concat(['fetch', '--tags', '--'])
},
{
kind: 'sparo-command',
name: 'fetch-al-completion',
args: prefixArgs.concat(['fetch', '--al'])
},
{
kind: 'sparo-command',
name: 'fetch-origin-HEA-completion',
args: prefixArgs.concat(['fetch', 'origin', 'HEA'])
},
// pull
{
kind: 'sparo-command',
name: 'pull-completion',
args: prefixArgs.concat(['pull'])
},
{
kind: 'sparo-command',
name: 'pull-long-parameters-completion',
args: prefixArgs.concat(['pull', '--'])
},
{
kind: 'sparo-command',
name: 'pull-pro-completion',
args: prefixArgs.concat(['pull', '--pro'])
},
{
kind: 'sparo-command',
name: 'pull-profile-completion',
args: prefixArgs.concat(['pull', '--profile'])
},
{
kind: 'sparo-command',
name: 'pull-profile-spa-completion',
args: prefixArgs.concat(['pull', '--profile', 'spa'])
},
{
kind: 'sparo-command',
name: 'pull-second-profile-completion',
args: prefixArgs.concat(['pull', '--profile', 'sparo-website', '--profile'])
},
{
kind: 'sparo-command',
name: 'pull-no-profile-completion',
args: prefixArgs.concat(['pull', '--no-profile', '--'])
},
// add
{
kind: 'sparo-command',
name: 'add-partial-completion',
args: prefixArgs.concat(['add', '__fixture__']),
processStdout: replaceBackslashes
},
{
kind: 'sparo-command',
name: 'add-folder-completion',
args: prefixArgs.concat(['add', '__fixture__/']),
processStdout: replaceBackslashes
},
{
kind: 'sparo-command',
name: 'add-folder-partial-completion',
args: prefixArgs.concat(['add', '__fixture__/dir-a/file']),
processStdout: replaceBackslashes
},
// branch
// commit
// diff
// log
// merge
// rebase
{
kind: 'sparo-command',
name: 'rebase-short-parameters-completion',
args: prefixArgs.concat(['rebase', '-'])
},
{
kind: 'sparo-command',
name: 'rebase-short-with-i-completion',
args: prefixArgs.concat(['rebase', '-i', '-'])
},
{
kind: 'sparo-command',
name: 'rebase-short-with-interactive-completion',
args: prefixArgs.concat(['rebase', '--interactive', '-'])
},
{
kind: 'sparo-command',
name: 'rebase-long-parameters-completion',
args: prefixArgs.concat(['rebase', '--'])
},
{
kind: 'sparo-command',
name: 'rebase-long-with-continue-completion',
args: prefixArgs.concat(['rebase', '--continue', '--'])
},
{
kind: 'sparo-command',
name: 'rebase-long-with-skip-completion',
args: prefixArgs.concat(['rebase', '--skip', '--'])
},
{
kind: 'sparo-command',
name: 'rebase-long-with-abort-completion',
args: prefixArgs.concat(['rebase', '--abort', '--'])
},
{
kind: 'sparo-command',
name: 'rebase-long-with-interactive-completion',
args: prefixArgs.concat(['rebase', '--interactive', '--'])
},
{
kind: 'sparo-command',
name: 'rebase-long-with-i-completion',
args: prefixArgs.concat(['rebase', '-i', '--'])
}
// restore
// status
];
await executeCommandsAndCollectOutputs({
buildFolderPath,
commandDefinitions
});
await updateOrCompareOutputs({
buildFolderPath,
logger,
production
});
}
function replaceBackslashes(text: string): string {
return text.replace(/\\/g, '/');
}

View file

@ -0,0 +1,8 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "./node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json",
"compilerOptions": {
"types": ["node", "heft-jest"]
}
}

View file

@ -6,6 +6,8 @@ Git version is __VERSION__
sparo clone <repository> [directory] sparo clone <repository> [directory]
Clone a repository into a new directory
Positionals: Positionals:
repository The remote repository to clone from. [string] [required] repository The remote repository to clone from. [string] [required]
directory The name of a new directory to clone into. The "humanish" part of directory The name of a new directory to clone into. The "humanish" part of

View file

@ -12,7 +12,7 @@ Commands:
profiles that contain the specified profiles that contain the specified
project name project name
sparo init-profile Initialize a new profile. sparo init-profile Initialize a new profile.
sparo clone <repository> [directory] sparo clone <repository> [directory] Clone a repository into a new directory
sparo checkout [branch] [start-point] Updates files in the working tree to sparo checkout [branch] [start-point] Updates files in the working tree to
match the version in the index or the match the version in the index or the
specified tree. If no pathspec was specified tree. If no pathspec was

View file

@ -27,6 +27,11 @@ export interface ISparoCommandDefinition {
* The working directory * The working directory
*/ */
currentWorkingDirectory?: string; currentWorkingDirectory?: string;
/**
* Process stdout. Use case: Unify path separator to /
*/
processStdout?: (output: string) => string;
} }
export interface ICustomCallbackDefinition { export interface ICustomCallbackDefinition {
@ -64,7 +69,7 @@ export async function executeCommandsAndCollectOutputs({
const { kind } = commandListDefinition; const { kind } = commandListDefinition;
switch (commandListDefinition.kind) { switch (commandListDefinition.kind) {
case 'sparo-command': { case 'sparo-command': {
const { name, args, currentWorkingDirectory } = commandListDefinition; const { name, args, currentWorkingDirectory, processStdout } = commandListDefinition;
const subProcess: ChildProcess = Executable.spawn(sparoBinPath, args, { const subProcess: ChildProcess = Executable.spawn(sparoBinPath, args, {
stdio: 'pipe', stdio: 'pipe',
currentWorkingDirectory, currentWorkingDirectory,
@ -78,8 +83,11 @@ export async function executeCommandsAndCollectOutputs({
let stdout: string = ''; let stdout: string = '';
let stderr: string = ''; let stderr: string = '';
subProcess.stdout?.on('data', (data: Buffer) => { subProcess.stdout?.on('data', (data: Buffer) => {
const text: string = data.toString(); let text: string = data.toString();
console.log(text); console.log(text);
if (processStdout) {
text = processStdout(text);
}
stdout += text; stdout += text;
}); });
@ -111,6 +119,7 @@ export async function executeCommandsAndCollectOutputs({
outputPath, outputPath,
`Running "sparo ${args.join(' ')}":\n${processSparoOutput( `Running "sparo ${args.join(' ')}":\n${processSparoOutput(
stdout, stdout,
// process.cwd() -> project folder
currentWorkingDirectory || process.cwd() currentWorkingDirectory || process.cwd()
)}` )}`
); );

View file

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "sparo",
"comment": "Supports shell completion",
"type": "none"
}
],
"packageName": "sparo"
}

View file

@ -149,6 +149,34 @@ importers:
specifier: ~5.3.3 specifier: ~5.3.3
version: 5.3.3 version: 5.3.3
../../build-tests/sparo-completion-test:
dependencies:
'@rushstack/node-core-library':
specifier: ~3.64.2
version: 3.64.2(@types/node@20.11.16)
build-test-utilities:
specifier: workspace:*
version: link:../test-utilities
devDependencies:
'@rushstack/heft':
specifier: 0.64.3
version: 0.64.3(@types/node@20.11.16)
'@rushstack/heft-node-rig':
specifier: 2.4.5
version: 2.4.5(@rushstack/heft@0.64.3)(@types/node@20.11.16)
'@types/heft-jest':
specifier: 1.0.6
version: 1.0.6
'@types/node':
specifier: 20.11.16
version: 20.11.16
eslint:
specifier: 8.56.0
version: 8.56.0
typescript:
specifier: ~5.3.3
version: 5.3.3
../../build-tests/sparo-output-test: ../../build-tests/sparo-output-test:
dependencies: dependencies:
'@rushstack/node-core-library': '@rushstack/node-core-library':

View file

@ -13,5 +13,6 @@ words:
- rushstack - rushstack
- Sparo - Sparo
- tiktok - tiktok
- yargs
ignoreWords: [] ignoreWords: []
import: [] import: []

View file

@ -270,37 +270,6 @@
"postRushx": [] "postRushx": []
}, },
/**
* Installation variants allow you to maintain a parallel set of configuration files that can be
* used to build the entire monorepo with an alternate set of dependencies. For example, suppose
* you upgrade all your projects to use a new release of an important framework, but during a transition period
* you intend to maintain compatibility with the old release. In this situation, you probably want your
* CI validation to build the entire repo twice: once with the old release, and once with the new release.
*
* Rush "installation variants" correspond to sets of config files located under this folder:
*
* common/config/rush/variants/<variant_name>
*
* The variant folder can contain an alternate common-versions.json file. Its "preferredVersions" field can be used
* to select older versions of dependencies (within a loose SemVer range specified in your package.json files).
* To install a variant, run "rush install --variant <variant_name>".
*
* For more details and instructions, see this article: https://rushjs.io/pages/advanced/installation_variants/
*/
"variants": [
// {
// /**
// * The folder name for this variant.
// */
// "variantName": "old-sdk",
//
// /**
// * An informative description
// */
// "description": "Build this repo using the previous release of the SDK"
// }
],
/** /**
* Rush can collect anonymous telemetry about everyday developer activity such as * Rush can collect anonymous telemetry about everyday developer activity such as
* success/failure of installs, builds, and other operations. You can use this to identify * success/failure of installs, builds, and other operations. You can use this to identify
@ -436,6 +405,10 @@
// } // }
// Build tests // Build tests
{
"packageName": "sparo-completion-test",
"projectFolder": "build-tests/sparo-completion-test"
},
{ {
"packageName": "sparo-output-test", "packageName": "sparo-output-test",
"projectFolder": "build-tests/sparo-output-test" "projectFolder": "build-tests/sparo-output-test"