mirror of
https://github.com/tiktok/sparo.git
synced 2024-11-14 19:35:12 -05:00
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:
commit
3a7a42e976
8 changed files with 422 additions and 204 deletions
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
95
apps/sparo-lib/src/cli/commands/pull.ts
Normal file
95
apps/sparo-lib/src/cli/commands/pull.ts
Normal 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`;
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "sparo",
|
||||
"comment": "support profile related parameters in pull & clone command",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "sparo"
|
||||
}
|
Loading…
Reference in a new issue