mirror of
https://github.com/tiktok/sparo.git
synced 2024-11-27 09:36:04 -05:00
commit
2539ea136a
10 changed files with 181 additions and 5 deletions
|
@ -7,6 +7,7 @@ import { ArgvService } from '../services/ArgvService';
|
|||
import { GitVersionCompatibility } from '../logic/GitVersionCompatibility';
|
||||
import { TelemetryService } from '../services/TelemetryService';
|
||||
import { GitSparseCheckoutService } from '../services/GitSparseCheckoutService';
|
||||
import { GracefulShutdownService } from '../services/GracefulShutdownService';
|
||||
import { getCommandName } from './commands/util';
|
||||
import { SparoStartupBanner } from './SparoStartupBanner';
|
||||
import type { ILaunchOptions } from '../api/Sparo';
|
||||
|
@ -51,6 +52,10 @@ export class SparoCICommandLine {
|
|||
this._commandsMap.add(getCommandName(cmdInstance.cmd));
|
||||
})
|
||||
);
|
||||
|
||||
const gracefulShutdownService: GracefulShutdownService =
|
||||
await getFromContainerAsync(GracefulShutdownService);
|
||||
gracefulShutdownService.setup();
|
||||
}
|
||||
|
||||
public async runAsync(): Promise<void> {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { ICommand } from './commands/base';
|
|||
import { GitVersionCompatibility } from '../logic/GitVersionCompatibility';
|
||||
import { TelemetryService } from '../services/TelemetryService';
|
||||
import { GitSparseCheckoutService } from '../services/GitSparseCheckoutService';
|
||||
import { GracefulShutdownService } from '../services/GracefulShutdownService';
|
||||
import { getCommandName } from './commands/util';
|
||||
import { SparoStartupBanner } from './SparoStartupBanner';
|
||||
import type { ILaunchOptions } from '../api/Sparo';
|
||||
|
@ -52,6 +53,10 @@ export class SparoCommandLine {
|
|||
this._commandsMap.add(getCommandName(cmdInstance.cmd));
|
||||
})
|
||||
);
|
||||
|
||||
const gracefulShutdownService: GracefulShutdownService =
|
||||
await getFromContainerAsync(GracefulShutdownService);
|
||||
gracefulShutdownService.setup();
|
||||
}
|
||||
|
||||
public async runAsync(): Promise<void> {
|
||||
|
|
|
@ -237,6 +237,8 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
|
|||
this._gitService.executeGitCommand({
|
||||
args: ['branch', branch, `${remote}/${branch}`]
|
||||
});
|
||||
|
||||
this._addRemoteBranchIfNotExists(remote, branch);
|
||||
}
|
||||
|
||||
const branchExistsInLocal: boolean = Boolean(
|
||||
|
@ -276,4 +278,21 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
|
|||
);
|
||||
return tagExistsInLocal;
|
||||
}
|
||||
|
||||
private _addRemoteBranchIfNotExists(remote: string, branch: string): void {
|
||||
const result: string | undefined = this._gitService.getGitConfig(`remote.${remote}.fetch`, {
|
||||
array: true
|
||||
});
|
||||
const remoteFetchGitConfig: string[] | undefined = result?.split('\n').filter(Boolean);
|
||||
|
||||
// Prevents adding the same remote branch multiple times
|
||||
const targetConfig: string = `+refs/heads/${branch}:refs/remotes/${remote}/${branch}`;
|
||||
if (remoteFetchGitConfig?.some((value: string) => value === targetConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._gitService.executeGitCommand({
|
||||
args: ['remote', 'set-branches', '--add', remote, branch]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { inject } from 'inversify';
|
||||
import { Command } from '../../decorator';
|
||||
import { GitService } from '../../services/GitService';
|
||||
import { GracefulShutdownService } from '../../services/GracefulShutdownService';
|
||||
|
||||
import type { Argv, ArgumentsCamelCase } from 'yargs';
|
||||
import type { GitRepoInfo } from 'git-repo-info';
|
||||
|
@ -19,6 +20,7 @@ export class FetchCommand implements ICommand<IFetchCommandOptions> {
|
|||
public description: string = 'fetch remote branch to local';
|
||||
|
||||
@inject(GitService) private _gitService!: GitService;
|
||||
@inject(GracefulShutdownService) private _gracefulShutdownService!: GracefulShutdownService;
|
||||
public builder(yargs: Argv<{}>): void {
|
||||
/**
|
||||
* sparo fetch <remote> <branch> [--all]
|
||||
|
@ -28,7 +30,7 @@ export class FetchCommand implements ICommand<IFetchCommandOptions> {
|
|||
.positional('branch', { type: 'string' })
|
||||
.string('remote')
|
||||
.string('branch')
|
||||
.boolean('full');
|
||||
.boolean('all');
|
||||
}
|
||||
|
||||
public handler = async (
|
||||
|
@ -44,16 +46,64 @@ export class FetchCommand implements ICommand<IFetchCommandOptions> {
|
|||
const { all, branch = defaultBranch, remote = this._gitService.getBranchRemote(branch) } = args;
|
||||
const fetchArgs: string[] = ['fetch'];
|
||||
|
||||
let restoreSingleBranchCallback: (() => void) | undefined;
|
||||
if (all) {
|
||||
// Temporary revert single branch fetch if necessary
|
||||
restoreSingleBranchCallback = this._revertSingleBranchIfNecessary(remote);
|
||||
|
||||
fetchArgs.push('--all');
|
||||
} else {
|
||||
fetchArgs.push(remote, branch);
|
||||
}
|
||||
|
||||
gitService.executeGitCommand({ args: fetchArgs });
|
||||
|
||||
restoreSingleBranchCallback?.();
|
||||
};
|
||||
|
||||
public getHelp(): string {
|
||||
return `fetch help`;
|
||||
}
|
||||
|
||||
private _revertSingleBranchIfNecessary = (remote: string): (() => void) | undefined => {
|
||||
let remoteFetchGitConfig: string[] | undefined = this._getRemoteFetchGitConfig(remote);
|
||||
let callback: (() => void) | undefined;
|
||||
if (remoteFetchGitConfig) {
|
||||
this._setAllBranchFetch(remote);
|
||||
|
||||
callback = () => {
|
||||
if (remoteFetchGitConfig) {
|
||||
this._restoreSingleBranchFetch(remote, remoteFetchGitConfig);
|
||||
|
||||
// Avoid memory leaking
|
||||
remoteFetchGitConfig = undefined;
|
||||
this._gracefulShutdownService.unregisterCallback(callback);
|
||||
}
|
||||
};
|
||||
|
||||
this._gracefulShutdownService.registerCallback(callback);
|
||||
}
|
||||
|
||||
return callback;
|
||||
};
|
||||
|
||||
private _getRemoteFetchGitConfig(remote: string): string[] | undefined {
|
||||
const result: string | undefined = this._gitService.getGitConfig(`remote.${remote}.fetch`, {
|
||||
array: true
|
||||
});
|
||||
return result?.split('\n').filter(Boolean);
|
||||
}
|
||||
|
||||
private _setAllBranchFetch(remote: string): void {
|
||||
this._gitService.setGitConfig(`remote.${remote}.fetch`, `+refs/heads/*:refs/remotes/${remote}/*`, {
|
||||
replaceAll: true
|
||||
});
|
||||
}
|
||||
|
||||
private _restoreSingleBranchFetch(remote: string, remoteFetchGitConfig: string[]): void {
|
||||
this._gitService.unsetGitConfig(`remote.${remote}.fetch`);
|
||||
for (const value of remoteFetchGitConfig) {
|
||||
this._gitService.setGitConfig(`remote.${remote}.fetch`, value, { add: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,17 +38,23 @@ export class GitService {
|
|||
public setGitConfig(
|
||||
k: string,
|
||||
v: string | number | boolean,
|
||||
option?: { dryRun?: boolean; global?: boolean }
|
||||
option?: { dryRun?: boolean; global?: boolean; replaceAll?: boolean; add?: boolean }
|
||||
): void {
|
||||
const gitPath: string = this.getGitPathOrThrow();
|
||||
const currentWorkingDirectory: string = this.getRepoInfo().root;
|
||||
const { dryRun = false, global = false } = option ?? {};
|
||||
const { dryRun = false, global = false, replaceAll = false, add = false } = option ?? {};
|
||||
const args: string[] = [];
|
||||
|
||||
args.push('config');
|
||||
if (global) {
|
||||
args.push('--global');
|
||||
}
|
||||
if (add) {
|
||||
args.push('--add');
|
||||
}
|
||||
if (replaceAll) {
|
||||
args.push('--replace-all');
|
||||
}
|
||||
|
||||
args.push(k, String(v));
|
||||
|
||||
|
@ -64,15 +70,21 @@ export class GitService {
|
|||
}
|
||||
}
|
||||
|
||||
public getGitConfig(k: string, option?: { dryRun?: boolean; global?: boolean }): string | undefined {
|
||||
public getGitConfig(
|
||||
k: string,
|
||||
option?: { dryRun?: boolean; global?: boolean; array?: boolean }
|
||||
): string | undefined {
|
||||
const gitPath: string = this.getGitPathOrThrow();
|
||||
const currentWorkingDirectory: string = this.getRepoInfo().root;
|
||||
const { dryRun = false, global = false } = option ?? {};
|
||||
const { dryRun = false, global = false, array = false } = option ?? {};
|
||||
const args: string[] = [];
|
||||
args.push('config');
|
||||
if (global) {
|
||||
args.push('--global');
|
||||
}
|
||||
if (array) {
|
||||
args.push('--get-all');
|
||||
}
|
||||
args.push(k);
|
||||
this._terminalService.terminal.writeDebugLine(`get git config with args ${JSON.stringify(args)}`);
|
||||
if (!dryRun) {
|
||||
|
@ -93,6 +105,25 @@ export class GitService {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
public unsetGitConfig(k: string): void {
|
||||
const gitPath: string = this.getGitPathOrThrow();
|
||||
const currentWorkingDirectory: string = this.getRepoInfo().root;
|
||||
const args: string[] = [];
|
||||
|
||||
args.push('config');
|
||||
args.push('--unset-all');
|
||||
args.push(k);
|
||||
|
||||
this._terminalService.terminal.writeDebugLine(`unset git config with args ${JSON.stringify(args)}`);
|
||||
const { status, stderr } = Executable.spawnSync(gitPath, args, {
|
||||
currentWorkingDirectory,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
if (status !== 0) {
|
||||
throw new Error(`Error while unsetting git config: ${stderr}`);
|
||||
}
|
||||
}
|
||||
|
||||
public setRecommendConfig(option?: { overwrite?: boolean; dryRun?: boolean }): void {
|
||||
const { overwrite = false, dryRun = false } = option ?? {};
|
||||
const recommendedConfigs: [string, string, number][] = [
|
||||
|
|
40
apps/sparo-lib/src/services/GracefulShutdownService.ts
Normal file
40
apps/sparo-lib/src/services/GracefulShutdownService.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Service } from '../decorator';
|
||||
|
||||
type ICallback = () => void;
|
||||
|
||||
/**
|
||||
* Helper class for managing graceful shutdown callbacks
|
||||
*
|
||||
* Example:
|
||||
* When running "sparo fetch --all", the command will temporarily modify git configs.
|
||||
* It's essential to register a restore callback via graceful shutdown service to
|
||||
* prevent inconsistent git configs status if user presses CTRL + C to terminate the
|
||||
* process in the middle of running.
|
||||
*/
|
||||
@Service()
|
||||
export class GracefulShutdownService {
|
||||
private _callbacks: Set<ICallback> = new Set<ICallback>();
|
||||
|
||||
public setup = (): void => {
|
||||
process.on('SIGINT', () => this._handleSignal());
|
||||
};
|
||||
|
||||
public registerCallback = (cb: ICallback): void => {
|
||||
this._callbacks.add(cb);
|
||||
};
|
||||
|
||||
public unregisterCallback = (cb?: ICallback): void => {
|
||||
if (!cb) {
|
||||
return;
|
||||
}
|
||||
this._callbacks.delete(cb);
|
||||
};
|
||||
|
||||
private _handleSignal = (): void => {
|
||||
// Keep the implementation simple to run each callbacks synchronously.
|
||||
for (const cb of Array.from(this._callbacks)) {
|
||||
cb();
|
||||
this.unregisterCallback(cb);
|
||||
}
|
||||
};
|
||||
}
|
10
common/changes/sparo/feat-fetch-all_2024-04-12-21-23.json
Normal file
10
common/changes/sparo/feat-fetch-all_2024-04-12-21-23.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "sparo",
|
||||
"comment": "Sparo fetch all remote branches when \"sparo fetch--all\"",
|
||||
"type": "minor"
|
||||
}
|
||||
],
|
||||
"packageName": "sparo"
|
||||
}
|
10
common/changes/sparo/feat-fetch-all_2024-04-12-22-38.json
Normal file
10
common/changes/sparo/feat-fetch-all_2024-04-12-22-38.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "sparo",
|
||||
"comment": "Track the branch which specifies to sparo checkout",
|
||||
"type": "minor"
|
||||
}
|
||||
],
|
||||
"packageName": "sparo"
|
||||
}
|
|
@ -28,6 +28,7 @@ export class GitService {
|
|||
getGitConfig(k: string, option?: {
|
||||
dryRun?: boolean;
|
||||
global?: boolean;
|
||||
array?: boolean;
|
||||
}): string | undefined;
|
||||
// (undocumented)
|
||||
getGitEmail(): string | undefined;
|
||||
|
@ -50,12 +51,16 @@ export class GitService {
|
|||
setGitConfig(k: string, v: string | number | boolean, option?: {
|
||||
dryRun?: boolean;
|
||||
global?: boolean;
|
||||
replaceAll?: boolean;
|
||||
add?: boolean;
|
||||
}): void;
|
||||
// (undocumented)
|
||||
setRecommendConfig(option?: {
|
||||
overwrite?: boolean;
|
||||
dryRun?: boolean;
|
||||
}): void;
|
||||
// (undocumented)
|
||||
unsetGitConfig(k: string): void;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
|
|
|
@ -9,6 +9,7 @@ words:
|
|||
- GVFS
|
||||
- humanish
|
||||
- inversify
|
||||
- Positionals
|
||||
- rushstack
|
||||
- Sparo
|
||||
- tiktok
|
||||
|
|
Loading…
Reference in a new issue