Skip to content

Commit

Permalink
Merge pull request #28 from EscapeB/feat/support_profile_parameters_i…
Browse files Browse the repository at this point in the history
…n_pull

feat: support profile parameters in pull & clone command
  • Loading branch information
EscapeB authored Mar 5, 2024
2 parents eb3eda4 + 7076806 commit 3a7a42e
Show file tree
Hide file tree
Showing 8 changed files with 422 additions and 204 deletions.
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)) {
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

0 comments on commit 3a7a42e

Please sign in to comment.