mirror of
https://github.com/tiktok/sparo.git
synced 2024-11-14 19:35:12 -05:00
Merge pull request #60 from tiktok/feat-sparo-checkout
sparo checkout can handle branch, tag, commit SHA and file path
This commit is contained in:
commit
dbfe9c5094
7 changed files with 148 additions and 27 deletions
|
@ -16,6 +16,8 @@ export interface ICheckoutCommandOptions {
|
|||
addProfile?: string[];
|
||||
}
|
||||
|
||||
type ICheckoutTargetKind = 'branch' | 'tag' | 'commit' | 'filePath';
|
||||
|
||||
@Command()
|
||||
export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
|
||||
public cmd: string = 'checkout [branch] [start-point]';
|
||||
|
@ -74,7 +76,28 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
|
|||
): Promise<void> => {
|
||||
const { _gitService: gitService } = this;
|
||||
terminalService.terminal.writeDebugLine(`got args in checkout command: ${JSON.stringify(args)}`);
|
||||
const { b, B, branch, startPoint } = args;
|
||||
const { b, B, startPoint } = args;
|
||||
|
||||
let branch: string | undefined = args.branch;
|
||||
|
||||
/**
|
||||
* Special case: "sparo checkout -"
|
||||
*
|
||||
* "git checkout -" is a shortcut that checks out the previously checked out branch.
|
||||
* yargs can not handle this, patch this case here.
|
||||
*/
|
||||
if (!branch) {
|
||||
const checkoutIndex: number = process.argv.findIndex((value: string) => value === 'checkout');
|
||||
if (checkoutIndex >= 0 && process.argv[checkoutIndex + 1] === '-') {
|
||||
branch = '-';
|
||||
// FIXME: supports "sparo checkout -"
|
||||
throw new Error(
|
||||
`Git's "-" token is not yet supported. If this feature is important for your work, please let us know by creating a GitHub issue.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let checkoutTargetKind: ICheckoutTargetKind = 'branch';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -91,9 +114,25 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
|
|||
throw new Error(`Failed to get branch ${operationBranch}`);
|
||||
} else {
|
||||
if (operationBranch !== currentBranch) {
|
||||
const isSynced: boolean = this._ensureBranchInLocal(operationBranch);
|
||||
if (!isSynced) {
|
||||
throw new Error(`Failed to sync ${operationBranch} from remote server`);
|
||||
// 1. First, Sparo needs to see the branch matches any branch name
|
||||
const isBranchSynced: boolean = this._ensureBranchInLocal(operationBranch);
|
||||
if (isBranchSynced) {
|
||||
checkoutTargetKind = 'branch';
|
||||
} else {
|
||||
// 2. If not, try tag names
|
||||
const isTagSynced: boolean = this._ensureTagInLocal(operationBranch);
|
||||
if (isTagSynced) {
|
||||
checkoutTargetKind = 'tag';
|
||||
} else {
|
||||
// 3. If not, try commit SHA
|
||||
const isCommitSHA: boolean = this._gitService.getObjectType(operationBranch) === 'commit';
|
||||
if (isCommitSHA) {
|
||||
checkoutTargetKind = 'commit';
|
||||
} else {
|
||||
// 4. Otherwise, treat it as file path
|
||||
checkoutTargetKind = 'filePath';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -104,8 +143,11 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
|
|||
profilesFromArg: args.profile
|
||||
});
|
||||
|
||||
// check wether profiles exist in local or operation branch
|
||||
if (!isNoProfile) {
|
||||
// Check wether profiles exist in local or operation branch
|
||||
// Skip check in the following cases:
|
||||
// 1. No profile
|
||||
// 2. The target kind is file path
|
||||
if (!isNoProfile && checkoutTargetKind !== 'filePath') {
|
||||
const targetProfileNames: Set<string> = new Set([...profiles, ...addProfiles]);
|
||||
const nonExistProfileNames: string[] = [];
|
||||
for (const targetProfileName of targetProfileNames) {
|
||||
|
@ -147,6 +189,13 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
|
|||
if (startPoint) {
|
||||
checkoutArgs.push(startPoint);
|
||||
}
|
||||
if (Array.isArray(args['--'])) {
|
||||
checkoutArgs.push('--');
|
||||
checkoutArgs.push(...args['--']);
|
||||
} else if (process.argv.includes('--')) {
|
||||
// "sparo checkout --" works
|
||||
checkoutArgs.push('--');
|
||||
}
|
||||
const result: child_process.SpawnSyncReturns<string> = gitService.executeGitCommand({
|
||||
args: checkoutArgs
|
||||
});
|
||||
|
@ -167,6 +216,26 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
|
|||
}
|
||||
|
||||
private _ensureBranchInLocal(branch: string): boolean {
|
||||
// fetch from remote
|
||||
const remote: string = this._gitService.getBranchRemote(branch);
|
||||
|
||||
const fetchResult: child_process.SpawnSyncReturns<string> = this._gitService.executeGitCommand({
|
||||
args: [
|
||||
'fetch',
|
||||
remote,
|
||||
`+refs/heads/${branch}:refs/remotes/${remote}/${branch}`,
|
||||
// Follows the recommended config fetch.showForcedUpdates false
|
||||
'--no-show-forced-updates'
|
||||
]
|
||||
});
|
||||
|
||||
if (fetchResult.status === 0) {
|
||||
// create local branch from remote branch
|
||||
this._gitService.executeGitCommand({
|
||||
args: ['branch', branch, `${remote}/${branch}`]
|
||||
});
|
||||
}
|
||||
|
||||
const branchExistsInLocal: boolean = Boolean(
|
||||
this._gitService
|
||||
.executeGitCommandAndCaptureOutput({
|
||||
|
@ -174,27 +243,8 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
|
|||
})
|
||||
.trim()
|
||||
);
|
||||
if (!branchExistsInLocal) {
|
||||
// fetch from remote
|
||||
const remote: string = this._gitService.getBranchRemote(branch);
|
||||
|
||||
const fetchResult: child_process.SpawnSyncReturns<string> = this._gitService.executeGitCommand({
|
||||
args: ['fetch', remote, `refs/heads/${branch}:refs/remotes/${remote}/${branch}`]
|
||||
});
|
||||
if (fetchResult.status !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// create local branch from remote branch
|
||||
const createBranchResult: child_process.SpawnSyncReturns<string> = this._gitService.executeGitCommand({
|
||||
args: ['branch', branch, `${remote}/${branch}`]
|
||||
});
|
||||
if (createBranchResult.status !== 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return branchExistsInLocal;
|
||||
}
|
||||
|
||||
private _getCurrentBranch(): string {
|
||||
|
@ -205,4 +255,22 @@ export class CheckoutCommand implements ICommand<ICheckoutCommandOptions> {
|
|||
.trim();
|
||||
return currentBranch;
|
||||
}
|
||||
|
||||
private _ensureTagInLocal(tag: string): boolean {
|
||||
// fetch from remote
|
||||
const remote: string = 'origin';
|
||||
|
||||
this._gitService.executeGitCommand({
|
||||
args: ['fetch', remote, 'tag', tag, '--force']
|
||||
});
|
||||
|
||||
const tagExistsInLocal: boolean = Boolean(
|
||||
this._gitService
|
||||
.executeGitCommandAndCaptureOutput({
|
||||
args: ['tag', '--list', tag]
|
||||
})
|
||||
.trim()
|
||||
);
|
||||
return tagExistsInLocal;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ export { Sparo, type ILaunchOptions } from './api/Sparo';
|
|||
|
||||
export { getFromContainerAsync } from './di/container';
|
||||
|
||||
export { GitService, type IExecuteGitCommandParams } from './services/GitService';
|
||||
export { GitService, type IExecuteGitCommandParams, type IObjectType } from './services/GitService';
|
||||
export { TerminalService, type ITerminal } from './services/TerminalService';
|
||||
|
||||
export type { ICollectTelemetryFunction, ITelemetryData } from './services/TelemetryService';
|
||||
|
|
|
@ -26,6 +26,9 @@ export class ArgvService {
|
|||
* which prints help text instead of proxy args to git commit.
|
||||
*/
|
||||
this._parsed = await this.yargsArgv
|
||||
.parserConfiguration({
|
||||
'populate--': true
|
||||
})
|
||||
// --debug
|
||||
.boolean('debug')
|
||||
// --verbose
|
||||
|
|
|
@ -15,6 +15,11 @@ export interface IExecuteGitCommandParams {
|
|||
workingDirectory?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export type IObjectType = 'blob' | 'tag' | 'commit' | 'tree';
|
||||
|
||||
/**
|
||||
* Help class for git operations
|
||||
*
|
||||
|
@ -390,6 +395,18 @@ Please specify a directory on the command line
|
|||
return Boolean(result);
|
||||
}
|
||||
|
||||
public getObjectType(object: string): IObjectType | undefined {
|
||||
let objectType: IObjectType | undefined;
|
||||
try {
|
||||
objectType = this.executeGitCommandAndCaptureOutput({
|
||||
args: ['cat-file', '-t', object]
|
||||
}).trim() as IObjectType;
|
||||
} catch (e) {
|
||||
// query an unknown type
|
||||
}
|
||||
return objectType;
|
||||
}
|
||||
|
||||
private _processResult(result: child_process.SpawnSyncReturns<string>): void {
|
||||
if (result.error) {
|
||||
result.error.message += '\n' + (result.stderr ? result.stderr.toString() + '\n' : '');
|
||||
|
|
|
@ -23,4 +23,22 @@ describe(GitService.name, () => {
|
|||
'%252E%252E%252Fsparo'
|
||||
);
|
||||
});
|
||||
|
||||
it('should get commit object type', async () => {
|
||||
const gitService = await getFromContainerAsync(GitService);
|
||||
// Shallow clone is used in CI builds, so getting the current commit SHA of HEAD
|
||||
const commitSHA: string = gitService
|
||||
.executeGitCommandAndCaptureOutput({
|
||||
args: ['rev-parse', 'HEAD']
|
||||
})
|
||||
.trim();
|
||||
expect(commitSHA).toBeTruthy();
|
||||
const objectType = gitService.getObjectType(commitSHA);
|
||||
expect(objectType).toBe('commit');
|
||||
});
|
||||
it('should get unknown object type', async () => {
|
||||
const gitService = await getFromContainerAsync(GitService);
|
||||
const objectType = gitService.getObjectType('foo');
|
||||
expect(objectType).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "sparo",
|
||||
"comment": "Checkout can handle branch, tag, commit SHA and file path",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "sparo"
|
||||
}
|
|
@ -40,6 +40,8 @@ export class GitService {
|
|||
// (undocumented)
|
||||
getIsSparseCheckoutMode(): boolean | undefined;
|
||||
// (undocumented)
|
||||
getObjectType(object: string): IObjectType | undefined;
|
||||
// (undocumented)
|
||||
getRepoInfo(): GitRepoInfo;
|
||||
get gitPath(): string | undefined;
|
||||
// (undocumented)
|
||||
|
@ -83,6 +85,9 @@ export interface ILaunchOptions {
|
|||
collectTelemetryAsync?: ICollectTelemetryFunction;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type IObjectType = 'blob' | 'tag' | 'commit' | 'tree';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface ITelemetryData {
|
||||
readonly args: string[];
|
||||
|
|
Loading…
Reference in a new issue