Skip to content

Commit

Permalink
feat: support profile parameters in pull command
Browse files Browse the repository at this point in the history
  • Loading branch information
EscapeB committed Feb 28, 2024
1 parent fe6e9ac commit e8691c3
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 117 deletions.
133 changes: 17 additions & 116 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 @@ -69,14 +64,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 @@ -99,24 +90,15 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
}
}

const targetProfileNames: 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));
}

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 @@ -165,43 +147,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) {
// TODO: policy #1: Can not sparse checkout with uncommitted changes in the cone.
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._gitSparseCheckoutService.resolveSparoProfileAsync(profile, {
localStateUpdateAction: 'add'
});
// for profiles, we use sparse checkout set
await this._gitSparseCheckoutService.checkoutAsync({
selections,
includeFolders,
excludeFolders,
checkoutAction: 'set'
});
}
for (const profile of addProfiles) {
// For each add profile we add it to local state.
const { selections, includeFolders, excludeFolders } =
await this._gitSparseCheckoutService.resolveSparoProfileAsync(profile, {
localStateUpdateAction: 'add'
});
// for add profiles, we use sparse checkout add
await this._gitSparseCheckoutService.checkoutAsync({
selections,
includeFolders,
excludeFolders,
checkoutAction: 'add'
});
}
}
// sync local sparse checkout state with given profiles.
this._sparoProfileService.syncProfileState({
profiles: isNoProfile ? null : profiles,
addProfiles
});
};

public getHelp(): string {
Expand Down Expand Up @@ -247,53 +197,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
};
}
}
2 changes: 2 additions & 0 deletions apps/sparo-lib/src/cli/commands/cmd-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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 @@ -25,6 +26,7 @@ export const COMMAND_LIST: Constructable[] = [
CloneCommand,
CheckoutCommand,
FetchCommand,
PullCommand,

// The commands customized by Sparo require a mirror command to Git
GitCloneCommand,
Expand Down
96 changes: 96 additions & 0 deletions apps/sparo-lib/src/cli/commands/pull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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 { GitRepoInfo } from 'git-repo-info';
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 = 'pull changes from remote branch to local';

@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;
const repoInfo: GitRepoInfo = gitService.getRepoInfo();

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) {
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.
this._sparoProfileService.syncProfileState({
profiles: isNoProfile ? null : profiles,
addProfiles
});
};

public getHelp(): string {
return `pull help`;
}
}
Loading

0 comments on commit e8691c3

Please sign in to comment.