Merge pull request #28 from EscapeB/feat/support_profile_parameters_in_pull

feat: support profile parameters in pull & clone command
This commit is contained in:
EscapeB 2024-03-05 10:59:28 +08:00 committed by GitHub
commit 3a7a42e976
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 422 additions and 204 deletions

View file

@ -1,14 +1,12 @@
import * as child_process from 'child_process';
import { inject } from 'inversify';
import { Command } from '../../decorator';
import type { ICommand } from './base';
import { type ArgumentsCamelCase, type Argv } from 'yargs';
import { GitService } from '../../services/GitService';
import { TerminalService } from '../../services/TerminalService';
import { ILocalStateProfiles, LocalState } from '../../logic/LocalState';
import { SparoProfileService } from '../../services/SparoProfileService';
import { GitSparseCheckoutService } from '../../services/GitSparseCheckoutService';
import type { ICommand } from './base';
import type { ArgumentsCamelCase, Argv } from 'yargs';
export interface ICheckoutCommandOptions {
profile: string[];
branch?: string;
@ -26,9 +24,6 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
@inject(GitService) private _gitService!: GitService;
@inject(SparoProfileService) private _sparoProfileService!: SparoProfileService;
@inject(GitSparseCheckoutService) private _gitSparseCheckoutService!: GitSparseCheckoutService;
@inject(LocalState) private _localState!: LocalState;
@inject(TerminalService) private _terminalService!: TerminalService;
public builder(yargs: Argv<{}>): void {
/**
@ -77,14 +72,10 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
args: ArgumentsCamelCase<ICheckoutCommandOptions>,
terminalService: TerminalService
): Promise<void> => {
const { _gitService: gitService, _localState: localState } = this;
const { _gitService: gitService } = this;
terminalService.terminal.writeDebugLine(`got args in checkout command: ${JSON.stringify(args)}`);
const { b, B, branch, startPoint } = args;
const { isNoProfile, profiles, addProfiles } = this._processProfilesFromArg({
addProfilesFromArg: args.addProfile ?? [],
profilesFromArg: args.profile
});
/**
* Since we set up single branch by default and branch can be missing in local, we are going to fetch the branch from remote server here.
*/
@ -107,28 +98,15 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
}
}
const targetProfileNames: Set<string> = new Set();
const currentProfileNames: Set<string> = new Set();
// preprocess profile related args
const { isNoProfile, profiles, addProfiles } = await this._sparoProfileService.preprocessProfileArgs({
addProfilesFromArg: args.addProfile ?? [],
profilesFromArg: args.profile
});
// check wether profiles exist in local or operation branch
if (!isNoProfile) {
// Get target profile.
// 1. If profile specified from CLI parameter, preferential use it.
// 2. If none profile specified, read from existing profile from local state as default.
// 3. If add profile was specified from CLI parameter, add them to result of 1 or 2.
const localStateProfiles: ILocalStateProfiles | undefined = await localState.getProfiles();
if (profiles.size) {
profiles.forEach((p) => targetProfileNames.add(p));
} else if (localStateProfiles) {
Object.keys(localStateProfiles).forEach((p) => {
targetProfileNames.add(p);
currentProfileNames.add(p);
});
}
if (addProfiles.size) {
addProfiles.forEach((p) => targetProfileNames.add(p));
}
const targetProfileNames: Set<string> = new Set([...profiles, ...addProfiles]);
const nonExistProfileNames: string[] = [];
for (const targetProfileName of targetProfileNames) {
/**
@ -177,42 +155,11 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
throw new Error(`git checkout failed`);
}
// checkout profiles
localState.reset();
if (isNoProfile) {
// if no profile specified, purge to skeleton
await this._gitSparseCheckoutService.purgeAsync();
} else if (targetProfileNames.size) {
let isCurrentSubsetOfTarget: boolean = true;
for (const currentProfileName of currentProfileNames) {
if (!targetProfileNames.has(currentProfileName)) {
isCurrentSubsetOfTarget = false;
break;
}
}
// In most case, sparo need to reset the sparse checkout cone.
// Only when the current profiles are subset of target profiles, we can skip this step.
if (!isCurrentSubsetOfTarget) {
await this._gitSparseCheckoutService.purgeAsync();
}
// TODO: policy #1: Can not sparse checkout with uncommitted changes in the cone.
for (const profile of targetProfileNames) {
// Since we have run localState.reset() before, for each profile we just add it to local state.
const { selections, includeFolders, excludeFolders } =
await this._gitSparseCheckoutService.resolveSparoProfileAsync(profile, {
localStateUpdateAction: 'add'
});
await this._gitSparseCheckoutService.checkoutAsync({
selections,
includeFolders,
excludeFolders,
checkoutAction: 'add'
});
}
}
// sync local sparse checkout state with given profiles.
await this._sparoProfileService.syncProfileState({
profiles: isNoProfile ? undefined : profiles,
addProfiles
});
};
public getHelp(): string {
@ -258,53 +205,4 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
.trim();
return currentBranch;
}
private _processProfilesFromArg({
profilesFromArg,
addProfilesFromArg
}: {
profilesFromArg: string[];
addProfilesFromArg: string[];
}): {
isNoProfile: boolean;
profiles: Set<string>;
addProfiles: Set<string>;
} {
/**
* --profile is defined as array type parameter, specifying --no-profile is resolved to false by yargs.
*
* @example --no-profile -> [false]
* @example --no-profile --profile foo -> [false, "foo"]
* @example --profile foo --no-profile -> ["foo", false]
*/
let isNoProfile: boolean = false;
const profiles: Set<string> = new Set();
for (const profile of profilesFromArg) {
if (typeof profile === 'boolean' && profile === false) {
isNoProfile = true;
continue;
}
profiles.add(profile);
}
/**
* --add-profile is defined as array type parameter
* @example --no-profile --add-profile foo -> throw error
* @example --profile bar --add-profile foo -> current profiles = bar + foo
* @example --add-profile foo -> current profiles = current profiles + foo
*/
const addProfiles: Set<string> = new Set(addProfilesFromArg.filter((p) => typeof p === 'string'));
if (isNoProfile && (profiles.size || addProfiles.size)) {
throw new Error(`The "--no-profile" parameter cannot be combined with "--profile" or "--add-profile"`);
}
return {
isNoProfile,
profiles,
addProfiles
};
}
}

View file

@ -7,6 +7,7 @@ import { GitService } from '../../services/GitService';
import { GitSparseCheckoutService } from '../../services/GitSparseCheckoutService';
import { GitCloneService, ICloneOptions } from '../../services/GitCloneService';
import { Stopwatch } from '../../logic/Stopwatch';
import { SparoProfileService } from '../../services/SparoProfileService';
import type { ICommand } from './base';
import type { TerminalService } from '../../services/TerminalService';
@ -15,6 +16,8 @@ export interface ICloneCommandOptions {
repository: string;
directory?: string;
skipGitConfig?: boolean;
profile?: string[];
addProfile?: string[];
}
@Command()
@ -24,6 +27,8 @@ export class CloneCommand implements ICommand<ICloneCommandOptions> {
@inject(GitService) private _gitService!: GitService;
@inject(GitCloneService) private _gitCloneService!: GitCloneService;
@inject(SparoProfileService) private _sparoProfileService!: SparoProfileService;
@inject(GitSparseCheckoutService) private _GitSparseCheckoutService!: GitSparseCheckoutService;
public builder(yargs: Argv<{}>): void {
@ -50,6 +55,10 @@ export class CloneCommand implements ICommand<ICloneCommandOptions> {
describe: 'Specify a branch to clone',
type: 'string'
})
.array('profile')
.default('profile', [])
.array('add-profile')
.default('add-profile', [])
.check((argv) => {
if (!argv.repository) {
return 'You must specify a repository to clone.';
@ -83,7 +92,37 @@ export class CloneCommand implements ICommand<ICloneCommandOptions> {
process.chdir(directory);
await this._GitSparseCheckoutService.checkoutSkeletonAsync();
const { profiles, addProfiles, isNoProfile } = await this._sparoProfileService.preprocessProfileArgs({
profilesFromArg: args.profile ?? [],
addProfilesFromArg: args.addProfile ?? []
});
await this._GitSparseCheckoutService.ensureSkeletonExistAndUpdated();
// check whether profile exist in local branch
if (!isNoProfile) {
const targetProfileNames: Set<string> = new Set([...profiles, ...addProfiles]);
const nonExistProfileNames: string[] = [];
for (const targetProfileName of targetProfileNames) {
if (!this._sparoProfileService.hasProfileInFS(targetProfileName)) {
nonExistProfileNames.push(targetProfileName);
}
}
if (nonExistProfileNames.length) {
throw new Error(
`Clone failed. The following profile(s) are missing in cloned repo: ${Array.from(
targetProfileNames
).join(', ')}`
);
}
}
// sync local sparse checkout state with given profiles.
await this._sparoProfileService.syncProfileState({
profiles: isNoProfile ? undefined : profiles,
addProfiles
});
// set recommended git config
if (!args.skipGitConfig) {
@ -100,13 +139,18 @@ export class CloneCommand implements ICommand<ICloneCommandOptions> {
terminal.writeLine(`Don't forget to change your shell path:`);
terminal.writeLine(' ' + Colorize.cyan(`cd ${directory}`));
terminal.writeLine();
terminal.writeLine('Your next step is to choose a Sparo profile for checkout.');
terminal.writeLine('To see available profiles in this repo:');
terminal.writeLine(' ' + Colorize.cyan('sparo list-profiles'));
terminal.writeLine('To checkout a profile:');
terminal.writeLine(' ' + Colorize.cyan('sparo checkout --profile <profile_name>'));
terminal.writeLine('To create a new profile:');
terminal.writeLine(' ' + Colorize.cyan('sparo init-profile --profile <profile_name>'));
if (isNoProfile || (profiles.size === 0 && addProfiles.size === 0)) {
terminal.writeLine('Your next step is to choose a Sparo profile for checkout.');
terminal.writeLine('To see available profiles in this repo:');
terminal.writeLine(' ' + Colorize.cyan('sparo list-profiles'));
terminal.writeLine('To checkout and set profile:');
terminal.writeLine(' ' + Colorize.cyan('sparo checkout --profile <profile_name>'));
terminal.writeLine('To checkout and add profile:');
terminal.writeLine(' ' + Colorize.cyan('sparo checkout --add-profile <profile_name>'));
terminal.writeLine('To create a new profile:');
terminal.writeLine(' ' + Colorize.cyan('sparo init-profile --profile <profile_name>'));
}
};
public getHelp(): string {

View file

@ -11,6 +11,7 @@ import { GitCheckoutCommand } from './git-checkout';
import { GitFetchCommand } from './git-fetch';
import { GitPullCommand } from './git-pull';
import { InitProfileCommand } from './init-profile';
// import { PullCommand } from './pull';
// When adding new Sparo subcommands, remember to update this doc page:
// https://github.com/tiktok/sparo/blob/main/apps/website/docs/pages/commands/overview.md
@ -22,6 +23,8 @@ export const COMMAND_LIST: Constructable[] = [
CloneCommand,
CheckoutCommand,
FetchCommand,
// Should be introduced after sparo merge|rebase
// PullCommand,
// The commands customized by Sparo require a mirror command to Git
GitCloneCommand,

View file

@ -40,7 +40,7 @@ export class ListProfilesCommand implements ICommand<IListProfilesCommandOptions
terminalService.terminal.writeLine();
// ensure sparse profiles folder
this._gitSparseCheckoutService.initializeRepository();
this._gitSparseCheckoutService.ensureSkeletonExistAndUpdated();
const sparoProfiles: Map<string, SparoProfile> = await this._sparoProfileService.getProfilesAsync();

View file

@ -0,0 +1,95 @@
import { inject } from 'inversify';
import { Command } from '../../decorator';
import { GitService } from '../../services/GitService';
import { SparoProfileService } from '../../services/SparoProfileService';
import type { Argv, ArgumentsCamelCase } from 'yargs';
import type { ICommand } from './base';
import type { TerminalService } from '../../services/TerminalService';
export interface IPullCommandOptions {
branch?: string;
remote?: string;
profile?: string[];
addProfile?: string[];
}
@Command()
export class PullCommand implements ICommand<IPullCommandOptions> {
public cmd: string = 'pull [remote] [branch]';
public description: string = 'Incorporates changes from a remote repository into the current branch.';
@inject(GitService) private _gitService!: GitService;
@inject(SparoProfileService) private _sparoProfileService!: SparoProfileService;
public builder(yargs: Argv<{}>): void {
/**
* sparo pull [remote] [branch] --profile <profile_name> --add-profile <profile_name> --no-profile
*/
yargs
.positional('remote', { type: 'string' })
.positional('branch', { type: 'string' })
.string('remote')
.string('branch')
.boolean('full')
.array('profile')
.default('profile', [])
.array('add-profile')
.default('add-profile', []);
}
public handler = async (
args: ArgumentsCamelCase<IPullCommandOptions>,
terminalService: TerminalService
): Promise<void> => {
const { _gitService: gitService, _sparoProfileService: sparoProfileService } = this;
const { terminal } = terminalService;
terminal.writeDebugLine(`got args in pull command: ${JSON.stringify(args)}`);
const pullArgs: string[] = ['pull'];
const { branch, remote } = args;
if (branch && remote) {
pullArgs.push(remote, branch);
}
const { isNoProfile, profiles, addProfiles } = await sparoProfileService.preprocessProfileArgs({
profilesFromArg: args.profile ?? [],
addProfilesFromArg: args.addProfile ?? []
});
// invoke native git pull command
gitService.executeGitCommand({ args: pullArgs });
// check whether profile exist in local branch
if (!isNoProfile) {
const targetProfileNames: Set<string> = new Set([...profiles, ...addProfiles]);
const nonExistProfileNames: string[] = [];
for (const targetProfileName of targetProfileNames) {
if (!this._sparoProfileService.hasProfileInFS(targetProfileName)) {
nonExistProfileNames.push(targetProfileName);
}
}
if (nonExistProfileNames.length) {
const { branch } = gitService.getRepoInfo();
throw new Error(
`Pull failed. The following profile(s) are missing in local branch "${branch}": ${Array.from(
targetProfileNames
).join(', ')}`
);
}
}
// sync local sparse checkout state with given profiles.
await this._sparoProfileService.syncProfileState({
profiles: isNoProfile ? undefined : profiles,
addProfiles
});
};
public getHelp(): string {
return `pull help`;
}
}

View file

@ -2,14 +2,12 @@ import * as path from 'path';
import * as child_process from 'child_process';
import { inject } from 'inversify';
import { Service } from '../decorator';
import { LocalState, LocalStateUpdateAction } from '../logic/LocalState';
import { type ISelection, SparoProfile } from '../logic/SparoProfile';
import { GitService } from './GitService';
import { SparoProfileService } from './SparoProfileService';
import { TerminalService } from './TerminalService';
import { Executable, FileSystem, JsonFile, JsonSyntax } from '@rushstack/node-core-library';
import { Stopwatch } from '../logic/Stopwatch';
import type { ISelection } from '../logic/SparoProfile';
export interface IRushSparseCheckoutOptions {
selections?: ISelection[];
includeFolders?: string[];
@ -24,81 +22,42 @@ export interface IRushProject {
projectFolder: string;
}
export interface IResolveSparoProfileOptions {
localStateUpdateAction: LocalStateUpdateAction;
}
@Service()
export class GitSparseCheckoutService {
@inject(SparoProfileService) private _sparoProfileService!: SparoProfileService;
@inject(GitService) private _gitService!: GitService;
@inject(LocalState) private _localState!: LocalState;
@inject(TerminalService) private _terminalService!: TerminalService;
private _rushConfigLoaded: boolean = false;
private _rushProjects: IRushProject[] = [];
private _packageNames: Set<string> = new Set<string>();
private _isSkeletonInitializedAndUpdated: boolean = false;
private _finalSkeletonPaths: string[] = [];
public initializeRepository(): void {
this._terminalService.terminal.writeLine('Checking out core files...');
public ensureSkeletonExistAndUpdated(): void {
/**
* Every time sparo cli was invoked, _isInitialized will be reset to false and try to local and update skeleton if needed.
* But it is not necessary to run initializeRepository() multiple times during a given command execution,
* because there is no code changes and the result will be the same each time.
*
* @todo
* Store isInitialized in local file, similar to LocalState, and check whether need to update skeleton
* by checking if there is any code changes in rush.json, autoinstaller, or projects' package json
*/
if (this._isSkeletonInitializedAndUpdated) {
return;
}
if ('true' !== this._gitService.getGitConfig('core.sparsecheckout')?.trim()) {
throw new Error('Sparse checkout is not enabled in this repo.');
}
this._loadRushConfiguration();
this._prepareMonorepoSkeleton();
this.initializeAndUpdateSkeleton();
}
public async resolveSparoProfileAsync(
profile: string,
options: IResolveSparoProfileOptions
): Promise<{
selections: ISelection[];
includeFolders: string[];
excludeFolders: string[];
}> {
this.initializeRepository();
const sparoProfile: SparoProfile | undefined = await this._sparoProfileService.getProfileAsync(profile);
if (!sparoProfile) {
const availableProfiles: string[] = Array.from(
(await this._sparoProfileService.getProfilesAsync()).keys()
);
throw new Error(
`Parse sparse profile "${profile}" error. ${
availableProfiles.length !== 0
? `Available profiles are:
${availableProfiles.join(',')}
`
: 'No profiles now'
}`
);
}
const repositoryRoot: string | null = this._gitService.getRepoInfo().root;
if (!repositoryRoot) {
throw new Error(`Running outside of the git repository folder`);
}
const { selections, includeFolders, excludeFolders } = sparoProfile;
const { localStateUpdateAction } = options;
await this._localState.setProfiles(
{
[profile]: {
selections,
includeFolders,
excludeFolders
}
},
localStateUpdateAction
);
return {
selections,
includeFolders,
excludeFolders
};
public initializeAndUpdateSkeleton(): void {
this._terminalService.terminal.writeLine('Checking out and updating core files...');
this._loadRushConfiguration();
this._prepareMonorepoSkeleton();
this._isSkeletonInitializedAndUpdated = true;
}
public checkoutSkeletonAsync = async (): Promise<void> => {
@ -127,6 +86,10 @@ ${availableProfiles.join(',')}
await this._rushSparseCheckoutAsync({ checkoutAction: 'purge' });
}
/**
*
* @param options
*/
private async _rushSparseCheckoutAsync(options: IRushSparseCheckoutOptions): Promise<void> {
const {
to,
@ -151,12 +114,7 @@ ${availableProfiles.join(',')}
throw new Error(`git repo not found. You should run this tool inside a git repo`);
}
{
const stopwatch: Stopwatch = Stopwatch.start();
this.initializeRepository();
terminal.writeVerboseLine(`Initialize repo sparse checkout. (${stopwatch.toString()})`);
stopwatch.stop();
}
this.ensureSkeletonExistAndUpdated();
const fromSelectors: Set<string> = new Set();
const toSelectors: Set<string> = new Set();
@ -210,7 +168,7 @@ ${availableProfiles.join(',')}
if (toSelectors.size !== 0 || fromSelectors.size !== 0) {
const stopwatch: Stopwatch = Stopwatch.start();
targetFolders = this._getTargetFoldersByRushList({ toSelectors, fromSelectors });
terminal.writeLine(`Run rush list command. (${stopwatch.toString()})`);
terminal.writeVerboseLine(`Run rush list command. (${stopwatch.toString()})`);
stopwatch.stop();
} else {
terminal.writeDebugLine('Skip rush list regarding the absence of from selectors and to selectors');
@ -225,20 +183,38 @@ ${availableProfiles.join(',')}
{
const stopwatch: Stopwatch = Stopwatch.start();
/**
* Perform different logic based on checkoutAction
*
* "purge" : reset repo to skeleton, will remove other paths in checkout paths list
*
* "skeleton" : checkout skeleton in repo, will only add skeleton paths to checkout paths list
*
* "set" : set checkout paths list by invoking "git sparse-checkout set", will implicitly add skeleton paths to this list.
*
* "add" : add a list of paths to checkout list by invoking "git sparse-checkout add"
*/
switch (checkoutAction) {
case 'purge':
case 'skeleton':
// re-apply the initial paths for setting up sparse repo state
this._prepareMonorepoSkeleton({ restore: checkoutAction === 'purge' });
this._prepareMonorepoSkeleton({
restore: checkoutAction === 'purge'
});
break;
case 'add':
case 'set':
if (targetFolders.length === 0) {
terminal.writeDebugLine(`Skip sparse checkout regarding no target folders`);
} else {
// if action is set, we need to combine targetFolder with _finalSkeletonPaths
if (checkoutAction === 'set') {
targetFolders.push(...this._finalSkeletonPaths);
}
terminal.writeLine(
`Performing sparse checkout ${checkoutAction} for these folders: \n${targetFolders.join('\n ')}`
`Performing sparse checkout ${checkoutAction} for these folders: \n${targetFolders.join('\n')}`
);
this._sparseCheckoutPaths(targetFolders, {
action: checkoutAction
});
@ -289,9 +265,9 @@ ${availableProfiles.join(',')}
private _prepareMonorepoSkeleton(options: { restore?: boolean } = {}): void {
const { restore } = options;
const finalSkeletonPaths: string[] = this._getSkeletonPaths();
this._terminalService.terminal.writeLine('Checking out skeleton...');
this._sparseCheckoutPaths(finalSkeletonPaths, {
this._finalSkeletonPaths = this._getSkeletonPaths();
this._terminalService.terminal.writeDebugLine(`Skeleton paths: ${this._finalSkeletonPaths.join(', ')}`);
this._sparseCheckoutPaths(this._finalSkeletonPaths, {
action: restore ? 'set' : 'add'
});
}

View file

@ -2,14 +2,21 @@ import { FileSystem, Async } from '@rushstack/node-core-library';
import path from 'path';
import { inject } from 'inversify';
import { Service } from '../decorator';
import { SparoProfile } from '../logic/SparoProfile';
import { SparoProfile, ISelection } from '../logic/SparoProfile';
import { TerminalService } from './TerminalService';
import { GitService } from './GitService';
import { GitSparseCheckoutService } from './GitSparseCheckoutService';
import { LocalState, ILocalStateProfiles, type LocalStateUpdateAction } from '../logic/LocalState';
export interface ISparoProfileServiceParams {
terminalService: TerminalService;
sparoProfileFolder: string;
}
export interface IResolveSparoProfileOptions {
localStateUpdateAction: LocalStateUpdateAction;
}
const defaultSparoProfileFolder: string = 'common/sparo-profiles';
@Service()
@ -19,6 +26,8 @@ export class SparoProfileService {
@inject(GitService) private _gitService!: GitService;
@inject(TerminalService) private _terminalService!: TerminalService;
@inject(LocalState) private _localState!: LocalState;
@inject(GitSparseCheckoutService) private _gitSparseCheckoutService!: GitSparseCheckoutService;
public async loadProfilesAsync(): Promise<void> {
if (!this._loadPromise) {
@ -105,4 +114,187 @@ export class SparoProfileService {
}
return last;
}
public async resolveSparoProfileAsync(
profile: string,
options: IResolveSparoProfileOptions
): Promise<{
selections: ISelection[];
includeFolders: string[];
excludeFolders: string[];
}> {
this._gitSparseCheckoutService.ensureSkeletonExistAndUpdated();
const sparoProfile: SparoProfile | undefined = await this.getProfileAsync(profile);
if (!sparoProfile) {
const availableProfiles: string[] = Array.from((await this.getProfilesAsync()).keys());
throw new Error(
`Parse sparse profile "${profile}" error. ${
availableProfiles.length !== 0
? `Available profiles are:
${availableProfiles.join(',')}
`
: 'No profiles now'
}`
);
}
const repositoryRoot: string | null = this._gitService.getRepoInfo().root;
if (!repositoryRoot) {
throw new Error(`Running outside of the git repository folder`);
}
const { selections, includeFolders, excludeFolders } = sparoProfile;
const { localStateUpdateAction } = options;
await this._localState.setProfiles(
{
[profile]: {
selections,
includeFolders,
excludeFolders
}
},
localStateUpdateAction
);
return {
selections,
includeFolders,
excludeFolders
};
}
/**
* preprocess profile related args from CLI parameter
*/
public async preprocessProfileArgs({
profilesFromArg,
addProfilesFromArg
}: {
profilesFromArg: string[];
addProfilesFromArg: string[];
}): Promise<{
isNoProfile: boolean;
profiles: Set<string>;
addProfiles: Set<string>;
}> {
let isNoProfile: boolean = false;
/**
* --profile is defined as array type parameter, specifying --no-profile is resolved to false by yargs.
*
* @example --no-profile -> [false]
* @example --no-profile --profile foo -> [false, "foo"]
* @example --profile foo --no-profile -> ["foo", false]
*/
const profiles: Set<string> = new Set();
for (const profile of profilesFromArg) {
if (typeof profile === 'boolean' && profile === false) {
isNoProfile = true;
continue;
}
profiles.add(profile);
}
/**
* --add-profile is defined as array type parameter
* @example --no-profile --add-profile foo -> throw error
* @example --profile bar --add-profile foo -> current profiles = bar + foo
* @example --add-profile foo -> current profiles = current profiles + foo
*/
const addProfiles: Set<string> = new Set(addProfilesFromArg.filter((p) => typeof p === 'string'));
if (isNoProfile && (profiles.size || addProfiles.size)) {
throw new Error(`The "--no-profile" parameter cannot be combined with "--profile" or "--add-profile"`);
}
//
if (!isNoProfile && profiles.size === 0) {
// Get target profile.
// 1. If profile specified from CLI parameter, preferential use it.
// 2. If none profile specified, read from existing profile from local state as default.
const localStateProfiles: ILocalStateProfiles | undefined = await this._localState.getProfiles();
if (localStateProfiles) {
Object.keys(localStateProfiles).forEach((p) => profiles.add(p));
}
}
return {
isNoProfile,
profiles,
addProfiles
};
}
/**
* sync local sparse checkout state with specified profiles
*/
public async syncProfileState({
profiles,
addProfiles
}: {
profiles?: Set<string>;
addProfiles?: Set<string>;
}): Promise<void> {
this._localState.reset();
this._terminalService.terminal.writeLine(
`Syncing local sparse checkout state with following specified profiles:\n${Array.from([
...(profiles ?? []),
...(addProfiles ?? [])
]).join('\n')}`
);
this._terminalService.terminal.writeLine();
if (!profiles || profiles.size === 0) {
// If no profile was specified, purge local state to skeleton
await this._gitSparseCheckoutService.purgeAsync();
} else {
const allProfilesIncludeFolders: string[] = [],
allProfilesExcludeFolders: string[] = [],
allProfilesSelections: ISelection[] = [];
for (const profile of profiles) {
// Since we have run localState.reset() before, for each profile we just add it to local state.
const { selections, includeFolders, excludeFolders } = await this.resolveSparoProfileAsync(profile, {
localStateUpdateAction: 'add'
});
// combine all profiles' selections and include/exclude folder
allProfilesSelections.push(...selections);
allProfilesIncludeFolders.push(...includeFolders);
allProfilesExcludeFolders.push(...excludeFolders);
}
// sparse-checkout set once for all profiles together
await this._gitSparseCheckoutService.checkoutAsync({
selections: allProfilesSelections,
includeFolders: allProfilesIncludeFolders,
excludeFolders: allProfilesExcludeFolders,
checkoutAction: 'set'
});
}
if (addProfiles?.size) {
// If add profiles is specified, using `git sparse-checkout add` to add folders in add profiles
const allAddProfilesSelections: ISelection[] = [],
allAddProfilesIncludeFolders: string[] = [],
allAddProfilesExcludeFolders: string[] = [];
for (const profile of addProfiles) {
// For each add profile we add it to local state.
const { selections, includeFolders, excludeFolders } = await this.resolveSparoProfileAsync(profile, {
localStateUpdateAction: 'add'
});
// combine all add profiles' selections and include/exclude folder
allAddProfilesSelections.push(...selections);
allAddProfilesIncludeFolders.push(...includeFolders);
allAddProfilesExcludeFolders.push(...excludeFolders);
}
/**
* Note:
* Although we could run sparse-checkout add multiple times,
* we combine all add operations and execute once for better performance.
*/
await this._gitSparseCheckoutService.checkoutAsync({
selections: allAddProfilesSelections,
includeFolders: allAddProfilesIncludeFolders,
excludeFolders: allAddProfilesExcludeFolders,
checkoutAction: 'add'
});
}
}
}

View file

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "sparo",
"comment": "support profile related parameters in pull & clone command",
"type": "none"
}
],
"packageName": "sparo"
}