Merge pull request #65 from tiktok/feat-fetch-all

Feat fetch all
This commit is contained in:
Cheng Liu 2024-04-15 16:32:09 -07:00 committed by GitHub
commit 2539ea136a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 181 additions and 5 deletions

View file

@ -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> {

View file

@ -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> {

View file

@ -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]
});
}
}

View file

@ -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 });
}
}
}

View file

@ -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][] = [

View 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);
}
};
}

View file

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "sparo",
"comment": "Sparo fetch all remote branches when \"sparo fetch--all\"",
"type": "minor"
}
],
"packageName": "sparo"
}

View file

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "sparo",
"comment": "Track the branch which specifies to sparo checkout",
"type": "minor"
}
],
"packageName": "sparo"
}

View file

@ -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

View file

@ -9,6 +9,7 @@ words:
- GVFS
- humanish
- inversify
- Positionals
- rushstack
- Sparo
- tiktok