Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: support profile parameters in pull & clone command #28

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 17 additions & 119 deletions apps/sparo-lib/src/cli/commands/checkout.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
/**
Expand Down Expand Up @@ -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.
*/
Expand All @@ -107,28 +98,15 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
}
}

const targetProfileNames: Set<string> = new Set();
const currentProfileNames: Set<string> = new Set();
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));
}
// 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) {
const targetProfileNames: Set<string> = new Set([...profiles, ...addProfiles]);
const nonExistProfileNames: string[] = [];
for (const targetProfileName of targetProfileNames) {
/**
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
};
}
}
60 changes: 52 additions & 8 deletions apps/sparo-lib/src/cli/commands/clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -15,6 +16,8 @@ export interface ICloneCommandOptions {
repository: string;
directory?: string;
skipGitConfig?: boolean;
profile?: string[];
addProfile?: string[];
}

@Command()
Expand All @@ -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 {
Expand All @@ -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.';
Expand Down Expand Up @@ -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) {
Expand All @@ -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)) {
EscapeB marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down
3 changes: 3 additions & 0 deletions apps/sparo-lib/src/cli/commands/cmd-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion apps/sparo-lib/src/cli/commands/list-profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading
Loading