From 075c58b29f2c0296dc1a38f9125f04ce831b76b0 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Mon, 26 Jun 2023 17:01:12 +0900 Subject: [PATCH] Adds support for comparison links Allows for branches and/or tags as comparison targets --- src/uris/deepLinks/deepLink.ts | 35 ++++++++- src/uris/deepLinks/deepLinkService.ts | 143 ++++++++++++++++++++++++++-------- 2 files changed, 143 insertions(+), 35 deletions(-) diff --git a/src/uris/deepLinks/deepLink.ts b/src/uris/deepLinks/deepLink.ts index 0b6aa17..2635c5a 100644 --- a/src/uris/deepLinks/deepLink.ts +++ b/src/uris/deepLinks/deepLink.ts @@ -10,6 +10,7 @@ export const enum UriTypes { export enum DeepLinkType { Branch = 'b', Commit = 'c', + Comparison = 'compare', Repository = 'r', Tag = 't', } @@ -20,6 +21,8 @@ export function deepLinkTypeToString(type: DeepLinkType): string { return 'Branch'; case DeepLinkType.Commit: return 'Commit'; + case DeepLinkType.Comparison: + return 'Comparison'; case DeepLinkType.Repository: return 'Repository'; case DeepLinkType.Tag: @@ -49,13 +52,14 @@ export interface DeepLink { remoteUrl?: string; repoPath?: string; targetId?: string; + secondaryTargetId?: string; } export function parseDeepLinkUri(uri: Uri): DeepLink | undefined { // The link target id is everything after the link target. // For example, if the uri is /link/r/{repoId}/b/{branchName}?url={remoteUrl}, // the link target id is {branchName} - const [, type, prefix, repoId, target, ...targetId] = uri.path.split('/'); + const [, type, prefix, repoId, target, ...rest] = uri.path.split('/'); if (type !== UriTypes.DeepLink || prefix !== DeepLinkType.Repository) return undefined; const urlParams = new URLSearchParams(uri.query); @@ -78,12 +82,28 @@ export function parseDeepLinkUri(uri: Uri): DeepLink | undefined { }; } + if (rest == null || rest.length === 0) return undefined; + + let targetId: string; + let secondaryTargetId: string | undefined; + const joined = rest.join('/'); + + if (target === DeepLinkType.Comparison) { + const split = joined.split(/(\.\.\.|\.\.)/); + if (split.length !== 3) return undefined; + targetId = split[0]; + secondaryTargetId = split[2]; + } else { + targetId = joined; + } + return { type: target as DeepLinkType, repoId: repoId, remoteUrl: remoteUrl, repoPath: repoPath, - targetId: targetId.join('/'), + targetId: targetId, + secondaryTargetId: secondaryTargetId, }; } @@ -99,6 +119,7 @@ export const enum DeepLinkServiceState { Fetch, FetchedTargetMatch, OpenGraph, + OpenComparison, } export const enum DeepLinkServiceAction { @@ -118,6 +139,7 @@ export const enum DeepLinkServiceAction { RemoteMatchFailed, RemoteAdded, TargetMatched, + TargetsMatched, TargetMatchFailed, TargetFetched, } @@ -137,8 +159,10 @@ export interface DeepLinkServiceContext { remote?: GitRemote | undefined; repoPath?: string | undefined; targetId?: string | undefined; + secondaryTargetId?: string | undefined; targetType?: DeepLinkType | undefined; targetSha?: string | undefined; + secondaryTargetSha?: string | undefined; } export const deepLinkStateTransitionTable: { [state: string]: { [action: string]: DeepLinkServiceState } } = { @@ -183,6 +207,7 @@ export const deepLinkStateTransitionTable: { [state: string]: { [action: string] [DeepLinkServiceState.TargetMatch]: { [DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle, [DeepLinkServiceAction.TargetMatched]: DeepLinkServiceState.OpenGraph, + [DeepLinkServiceAction.TargetsMatched]: DeepLinkServiceState.OpenComparison, [DeepLinkServiceAction.TargetMatchFailed]: DeepLinkServiceState.Fetch, }, [DeepLinkServiceState.Fetch]: { @@ -192,12 +217,17 @@ export const deepLinkStateTransitionTable: { [state: string]: { [action: string] }, [DeepLinkServiceState.FetchedTargetMatch]: { [DeepLinkServiceAction.TargetMatched]: DeepLinkServiceState.OpenGraph, + [DeepLinkServiceAction.TargetsMatched]: DeepLinkServiceState.OpenComparison, [DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle, }, [DeepLinkServiceState.OpenGraph]: { [DeepLinkServiceAction.DeepLinkResolved]: DeepLinkServiceState.Idle, [DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle, }, + [DeepLinkServiceState.OpenComparison]: { + [DeepLinkServiceAction.DeepLinkResolved]: DeepLinkServiceState.Idle, + [DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle, + }, }; export interface DeepLinkProgress { @@ -217,4 +247,5 @@ export const deepLinkStateToProgress: { [state: string]: DeepLinkProgress } = { [DeepLinkServiceState.Fetch]: { message: 'Fetching...', increment: 80 }, [DeepLinkServiceState.FetchedTargetMatch]: { message: 'Finding a matching target...', increment: 90 }, [DeepLinkServiceState.OpenGraph]: { message: 'Opening graph...', increment: 95 }, + [DeepLinkServiceState.OpenComparison]: { message: 'Opening comparison...', increment: 95 }, }; diff --git a/src/uris/deepLinks/deepLinkService.ts b/src/uris/deepLinks/deepLinkService.ts index 0d9ec1f..299d6eb 100644 --- a/src/uris/deepLinks/deepLinkService.ts +++ b/src/uris/deepLinks/deepLinkService.ts @@ -62,6 +62,12 @@ export class DeepLinkService implements Disposable { return; } + if (link.type === DeepLinkType.Comparison && !link.secondaryTargetId) { + void window.showErrorMessage('Unable to resolve link'); + Logger.warn(`Unable to resolve link - no secondary target id provided: ${uri.toString()}`); + return; + } + this.setContextFromDeepLink(link, uri.toString()); await this.processDeepLink(); @@ -90,6 +96,7 @@ export class DeepLinkService implements Disposable { remote: undefined, repoPath: undefined, targetId: undefined, + secondaryTargetId: undefined, targetType: undefined, targetSha: undefined, }; @@ -104,6 +111,7 @@ export class DeepLinkService implements Disposable { remoteUrl: link.remoteUrl, repoPath: link.repoPath, targetId: link.targetId, + secondaryTargetId: link.secondaryTargetId, }; } @@ -134,44 +142,83 @@ export class DeepLinkService implements Disposable { }); } - private async getShaForTarget(): Promise { - const { repo, remote, targetType, targetId } = this._context; - if (!repo || targetType === DeepLinkType.Repository || !targetId) { - return undefined; + private async getShaForBranch(targetId: string): Promise { + const { repo, remote } = this._context; + if (!repo) return undefined; + + // Form the target branch name using the remote name and branch name + const branchName = remote != null ? `${remote.name}/${targetId}` : targetId; + let branch = await repo.getBranch(branchName); + if (branch?.sha != null) { + return branch.sha; } - if (targetType === DeepLinkType.Branch) { - // Form the target branch name using the remote name and branch name - const branchName = remote != null ? `${remote.name}/${targetId}` : targetId; - let branch = await repo.getBranch(branchName); - if (branch) { - return branch.sha; - } + // If it doesn't exist on the target remote, it may still exist locally. + branch = await repo.getBranch(targetId); + if (branch?.sha != null) { + return branch.sha; + } - // If it doesn't exist on the target remote, it may still exist locally. - branch = await repo.getBranch(targetId); - if (branch) { - return branch.sha; - } + return undefined; + } - return undefined; + private async getShaForTag(targetId: string): Promise { + const { repo } = this._context; + if (!repo) return undefined; + const tag = await repo.getTag(targetId); + if (tag?.sha != null) { + return tag.sha; } - if (targetType === DeepLinkType.Tag) { - const tag = await repo.getTag(targetId); - if (tag) { - return tag.sha; - } + return undefined; + } + + private async getShaForCommit(targetId: string): Promise { + const { repo } = this._context; + if (!repo) return undefined; + if (await this.container.git.validateReference(repo.path, targetId)) { + return targetId; + } + + return undefined; + } + + private async getShasForComparison( + targetId: string, + secondaryTargetId: string, + ): Promise<[string, string] | undefined> { + // try treating each id as a commit sha first, then a branch if that fails, then a tag if that fails + const sha1 = + (await this.getShaForCommit(targetId)) ?? + (await this.getShaForBranch(targetId)) ?? + (await this.getShaForTag(targetId)); + if (sha1 == null) return undefined; + const sha2 = + (await this.getShaForCommit(secondaryTargetId)) ?? + (await this.getShaForBranch(secondaryTargetId)) ?? + (await this.getShaForTag(secondaryTargetId)); + if (sha2 == null) return undefined; + return [sha1, sha2]; + } - return undefined; + private async getShasForTargets(): Promise { + const { repo, targetType, targetId, secondaryTargetId } = this._context; + if (repo == null || targetType === DeepLinkType.Repository || targetId == null) return undefined; + if (targetType === DeepLinkType.Branch) { + return this.getShaForBranch(targetId); + } + + if (targetType === DeepLinkType.Tag) { + return this.getShaForTag(targetId); } if (targetType === DeepLinkType.Commit) { - if (await this.container.git.validateReference(repo.path, targetId)) { - return targetId; - } + return this.getShaForCommit(targetId); + } - return undefined; + if (targetType === DeepLinkType.Comparison) { + if (secondaryTargetId == null) return undefined; + return this.getShasForComparison(targetId, secondaryTargetId); } return undefined; @@ -222,7 +269,7 @@ export class DeepLinkService implements Disposable { private async showFetchPrompt(): Promise { const fetchResult = await window.showInformationMessage( - "The link target couldn't be found. Would you like to fetch from the remote?", + "The link target(s) couldn't be found. Would you like to fetch from the remote?", { modal: true }, { title: 'Fetch', action: true }, { title: 'Cancel', isCloseAffordance: true }, @@ -302,7 +349,8 @@ export class DeepLinkService implements Disposable { while (true) { this._context.state = deepLinkStateTransitionTable[this._context.state][action]; - const { state, repoId, repo, url, remoteUrl, remote, repoPath, targetSha, targetType } = this._context; + const { state, repoId, repo, url, remoteUrl, remote, repoPath, targetSha, secondaryTargetSha, targetType } = + this._context; this._onDeepLinkProgressUpdated.fire(deepLinkStateToProgress[state]); switch (state) { case DeepLinkServiceState.Idle: @@ -533,18 +581,30 @@ export class DeepLinkService implements Disposable { break; } - this._context.targetSha = await this.getShaForTarget(); - if (!this._context.targetSha) { + if (targetType === DeepLinkType.Comparison) { + [this._context.targetSha, this._context.secondaryTargetSha] = + (await this.getShasForTargets()) ?? []; + } else { + this._context.targetSha = (await this.getShasForTargets()) as string | undefined; + } + + if ( + this._context.targetSha == null || + (this._context.secondaryTargetSha == null && targetType === DeepLinkType.Comparison) + ) { if (state === DeepLinkServiceState.TargetMatch && remote != null) { action = DeepLinkServiceAction.TargetMatchFailed; } else { action = DeepLinkServiceAction.DeepLinkErrored; - message = 'No matching target found.'; + message = `No matching ${targetSha == null ? 'target' : 'secondary target'} found.`; } break; } - action = DeepLinkServiceAction.TargetMatched; + action = + targetType === DeepLinkType.Comparison + ? DeepLinkServiceAction.TargetsMatched + : DeepLinkServiceAction.TargetMatched; break; case DeepLinkServiceState.Fetch: @@ -596,6 +656,23 @@ export class DeepLinkService implements Disposable { action = DeepLinkServiceAction.DeepLinkResolved; break; + case DeepLinkServiceState.OpenComparison: + if (!repo) { + action = DeepLinkServiceAction.DeepLinkErrored; + message = 'Missing repository.'; + break; + } + + if (!targetSha || !secondaryTargetSha) { + action = DeepLinkServiceAction.DeepLinkErrored; + message = 'Missing target or secondary target.'; + break; + } + + await this.container.searchAndCompareView.compare(repo.path, targetSha, secondaryTargetSha); + action = DeepLinkServiceAction.DeepLinkResolved; + break; + default: action = DeepLinkServiceAction.DeepLinkErrored; message = 'Unknown state.';