Browse Source

Support deep links that provide repo path (#2711)

main
Ramin Tadayon 1 year ago
committed by GitHub
parent
commit
52c288eb2e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 73 additions and 31 deletions
  1. +18
    -3
      src/uris/deepLinks/deepLink.ts
  2. +55
    -28
      src/uris/deepLinks/deepLinkService.ts

+ 18
- 3
src/uris/deepLinks/deepLink.ts View File

@ -46,7 +46,8 @@ export function refTypeToDeepLinkType(refType: GitReference['refType']): DeepLin
export interface DeepLink { export interface DeepLink {
type: DeepLinkType; type: DeepLinkType;
repoId: string; repoId: string;
remoteUrl: string;
remoteUrl?: string;
repoPath?: string;
targetId?: string; targetId?: string;
} }
@ -57,14 +58,23 @@ export function parseDeepLinkUri(uri: Uri): DeepLink | undefined {
const [, type, prefix, repoId, target, ...targetId] = uri.path.split('/'); const [, type, prefix, repoId, target, ...targetId] = uri.path.split('/');
if (type !== UriTypes.DeepLink || prefix !== DeepLinkType.Repository) return undefined; if (type !== UriTypes.DeepLink || prefix !== DeepLinkType.Repository) return undefined;
const remoteUrl = new URLSearchParams(uri.query).get('url');
if (!remoteUrl) return undefined;
const urlParams = new URLSearchParams(uri.query);
let remoteUrl = urlParams.get('url') ?? undefined;
if (remoteUrl != null) {
remoteUrl = decodeURIComponent(remoteUrl);
}
let repoPath = urlParams.get('path') ?? undefined;
if (repoPath != null) {
repoPath = decodeURIComponent(repoPath);
}
if (!remoteUrl && !repoPath) return undefined;
if (target == null) { if (target == null) {
return { return {
type: DeepLinkType.Repository, type: DeepLinkType.Repository,
repoId: repoId, repoId: repoId,
remoteUrl: remoteUrl, remoteUrl: remoteUrl,
repoPath: repoPath,
}; };
} }
@ -72,6 +82,7 @@ export function parseDeepLinkUri(uri: Uri): DeepLink | undefined {
type: target as DeepLinkType, type: target as DeepLinkType,
repoId: repoId, repoId: repoId,
remoteUrl: remoteUrl, remoteUrl: remoteUrl,
repoPath: repoPath,
targetId: targetId.join('/'), targetId: targetId.join('/'),
}; };
} }
@ -98,6 +109,7 @@ export const enum DeepLinkServiceAction {
DeepLinkErrored, DeepLinkErrored,
OpenRepo, OpenRepo,
RepoMatchedWithId, RepoMatchedWithId,
RepoMatchedWithPath,
RepoMatchedWithRemoteUrl, RepoMatchedWithRemoteUrl,
RepoMatchFailed, RepoMatchFailed,
RepoAdded, RepoAdded,
@ -123,6 +135,7 @@ export interface DeepLinkServiceContext {
repo?: Repository | undefined; repo?: Repository | undefined;
remoteUrl?: string | undefined; remoteUrl?: string | undefined;
remote?: GitRemote | undefined; remote?: GitRemote | undefined;
repoPath?: string | undefined;
targetId?: string | undefined; targetId?: string | undefined;
targetType?: DeepLinkType | undefined; targetType?: DeepLinkType | undefined;
targetSha?: string | undefined; targetSha?: string | undefined;
@ -135,6 +148,7 @@ export const deepLinkStateTransitionTable: { [state: string]: { [action: string]
[DeepLinkServiceState.RepoMatch]: { [DeepLinkServiceState.RepoMatch]: {
[DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle, [DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle,
[DeepLinkServiceAction.RepoMatchedWithId]: DeepLinkServiceState.RemoteMatch, [DeepLinkServiceAction.RepoMatchedWithId]: DeepLinkServiceState.RemoteMatch,
[DeepLinkServiceAction.RepoMatchedWithPath]: DeepLinkServiceState.TargetMatch,
[DeepLinkServiceAction.RepoMatchedWithRemoteUrl]: DeepLinkServiceState.TargetMatch, [DeepLinkServiceAction.RepoMatchedWithRemoteUrl]: DeepLinkServiceState.TargetMatch,
[DeepLinkServiceAction.RepoMatchFailed]: DeepLinkServiceState.CloneOrAddRepo, [DeepLinkServiceAction.RepoMatchFailed]: DeepLinkServiceState.CloneOrAddRepo,
}, },
@ -152,6 +166,7 @@ export const deepLinkStateTransitionTable: { [state: string]: { [action: string]
}, },
[DeepLinkServiceState.AddedRepoMatch]: { [DeepLinkServiceState.AddedRepoMatch]: {
[DeepLinkServiceAction.RepoMatchedWithId]: DeepLinkServiceState.RemoteMatch, [DeepLinkServiceAction.RepoMatchedWithId]: DeepLinkServiceState.RemoteMatch,
[DeepLinkServiceAction.RepoMatchedWithPath]: DeepLinkServiceState.TargetMatch,
[DeepLinkServiceAction.RepoMatchedWithRemoteUrl]: DeepLinkServiceState.TargetMatch, [DeepLinkServiceAction.RepoMatchedWithRemoteUrl]: DeepLinkServiceState.TargetMatch,
[DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle, [DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle,
}, },

+ 55
- 28
src/uris/deepLinks/deepLinkService.ts View File

@ -12,6 +12,7 @@ import { executeCommand } from '../../system/command';
import { configuration } from '../../system/configuration'; import { configuration } from '../../system/configuration';
import { once } from '../../system/event'; import { once } from '../../system/event';
import { Logger } from '../../system/logger'; import { Logger } from '../../system/logger';
import { normalizePath } from '../../system/path';
import { openWorkspace, OpenWorkspaceLocation } from '../../system/utils'; import { openWorkspace, OpenWorkspaceLocation } from '../../system/utils';
import type { DeepLink, DeepLinkProgress, DeepLinkServiceContext } from './deepLink'; import type { DeepLink, DeepLinkProgress, DeepLinkServiceContext } from './deepLink';
import { import {
@ -43,7 +44,7 @@ export class DeepLinkService implements Disposable {
if (link == null) return; if (link == null) return;
if (this._context.state === DeepLinkServiceState.Idle) { if (this._context.state === DeepLinkServiceState.Idle) {
if (!link.repoId || !link.type || !link.remoteUrl) {
if (!link.type || (!link.repoId && !link.remoteUrl && !link.repoPath)) {
void window.showErrorMessage('Unable to resolve link'); void window.showErrorMessage('Unable to resolve link');
Logger.warn(`Unable to resolve link - missing basic properties: ${uri.toString()}`); Logger.warn(`Unable to resolve link - missing basic properties: ${uri.toString()}`);
return; return;
@ -87,6 +88,7 @@ export class DeepLinkService implements Disposable {
repo: undefined, repo: undefined,
remoteUrl: undefined, remoteUrl: undefined,
remote: undefined, remote: undefined,
repoPath: undefined,
targetId: undefined, targetId: undefined,
targetType: undefined, targetType: undefined,
targetSha: undefined, targetSha: undefined,
@ -100,6 +102,7 @@ export class DeepLinkService implements Disposable {
targetType: link.type, targetType: link.type,
url: url, url: url,
remoteUrl: link.remoteUrl, remoteUrl: link.remoteUrl,
repoPath: link.repoPath,
targetId: link.targetId, targetId: link.targetId,
}; };
} }
@ -133,13 +136,13 @@ export class DeepLinkService implements Disposable {
private async getShaForTarget(): Promise<string | undefined> { private async getShaForTarget(): Promise<string | undefined> {
const { repo, remote, targetType, targetId } = this._context; const { repo, remote, targetType, targetId } = this._context;
if (!repo || !remote || targetType === DeepLinkType.Repository || !targetId) {
if (!repo || targetType === DeepLinkType.Repository || !targetId) {
return undefined; return undefined;
} }
if (targetType === DeepLinkType.Branch) { if (targetType === DeepLinkType.Branch) {
// Form the target branch name using the remote name and branch name // Form the target branch name using the remote name and branch name
const branchName = `${remote.name}/${targetId}`;
const branchName = remote != null ? `${remote.name}/${targetId}` : targetId;
let branch = await repo.getBranch(branchName); let branch = await repo.getBranch(branchName);
if (branch) { if (branch) {
return branch.sha; return branch.sha;
@ -175,13 +178,20 @@ export class DeepLinkService implements Disposable {
} }
private async showOpenTypePrompt(): Promise<DeepLinkRepoOpenType | undefined> { private async showOpenTypePrompt(): Promise<DeepLinkRepoOpenType | undefined> {
const options: { title: string; action?: DeepLinkRepoOpenType; isCloseAffordance?: boolean }[] = [
{ title: 'Open Folder', action: DeepLinkRepoOpenType.Folder },
{ title: 'Open Workspace', action: DeepLinkRepoOpenType.Workspace },
];
if (this._context.remoteUrl != null) {
options.push({ title: 'Clone', action: DeepLinkRepoOpenType.Clone });
}
options.push({ title: 'Cancel', isCloseAffordance: true });
const openTypeResult = await window.showInformationMessage( const openTypeResult = await window.showInformationMessage(
'No matching repository found. Please choose an option.', 'No matching repository found. Please choose an option.',
{ modal: true }, { modal: true },
{ title: 'Open Folder', action: DeepLinkRepoOpenType.Folder },
{ title: 'Open Workspace', action: DeepLinkRepoOpenType.Workspace },
{ title: 'Clone', action: DeepLinkRepoOpenType.Clone },
{ title: 'Cancel', isCloseAffordance: true },
...options,
); );
return openTypeResult?.action; return openTypeResult?.action;
@ -292,7 +302,7 @@ export class DeepLinkService implements Disposable {
while (true) { while (true) {
this._context.state = deepLinkStateTransitionTable[this._context.state][action]; this._context.state = deepLinkStateTransitionTable[this._context.state][action];
const { state, repoId, repo, url, remoteUrl, remote, targetSha, targetType } = this._context;
const { state, repoId, repo, url, remoteUrl, remote, repoPath, targetSha, targetType } = this._context;
this._onDeepLinkProgressUpdated.fire(deepLinkStateToProgress[state]); this._onDeepLinkProgressUpdated.fire(deepLinkStateToProgress[state]);
switch (state) { switch (state) {
case DeepLinkServiceState.Idle: case DeepLinkServiceState.Idle:
@ -306,31 +316,48 @@ export class DeepLinkService implements Disposable {
return; return;
case DeepLinkServiceState.RepoMatch: case DeepLinkServiceState.RepoMatch:
case DeepLinkServiceState.AddedRepoMatch: case DeepLinkServiceState.AddedRepoMatch:
if (!repoId || !remoteUrl) {
if (!repoId && !remoteUrl && !repoPath) {
action = DeepLinkServiceAction.DeepLinkErrored; action = DeepLinkServiceAction.DeepLinkErrored;
message = 'No repository id or remote url was provided.';
message = 'No repository id, remote url or path was provided.';
break; break;
} }
[, remoteDomain, remotePath] = parseGitRemoteUrl(remoteUrl);
if (remoteUrl != null) {
[, remoteDomain, remotePath] = parseGitRemoteUrl(remoteUrl);
}
// Try to match a repo using the remote URL first, since that saves us some steps. // Try to match a repo using the remote URL first, since that saves us some steps.
// As a fallback, try to match using the repo id. // As a fallback, try to match using the repo id.
for (const repo of this.container.git.repositories) { for (const repo of this.container.git.repositories) {
// eslint-disable-next-line no-loop-func
matchingRemotes = await repo.getRemotes({ filter: r => r.matches(remoteDomain, remotePath) });
if (matchingRemotes.length > 0) {
if (
repoPath != null &&
normalizePath(repo.path.toLowerCase()) === normalizePath(repoPath.toLowerCase())
) {
this._context.repo = repo; this._context.repo = repo;
this._context.remote = matchingRemotes[0];
action = DeepLinkServiceAction.RepoMatchedWithRemoteUrl;
action = DeepLinkServiceAction.RepoMatchedWithPath;
break; break;
} }
// Repo ID can be any valid SHA in the repo, though standard practice is to use the
// first commit SHA.
if (await this.container.git.validateReference(repo.path, repoId)) {
this._context.repo = repo;
action = DeepLinkServiceAction.RepoMatchedWithId;
break;
if (remoteDomain != null && remotePath != null) {
matchingRemotes = await repo.getRemotes({
// eslint-disable-next-line no-loop-func
filter: r => r.matches(remoteDomain, remotePath),
});
if (matchingRemotes.length > 0) {
this._context.repo = repo;
this._context.remote = matchingRemotes[0];
action = DeepLinkServiceAction.RepoMatchedWithRemoteUrl;
break;
}
}
if (repoId != null && repoId !== '-') {
// Repo ID can be any valid SHA in the repo, though standard practice is to use the
// first commit SHA.
if (await this.container.git.validateReference(repo.path, repoId)) {
this._context.repo = repo;
action = DeepLinkServiceAction.RepoMatchedWithId;
break;
}
} }
} }
@ -346,9 +373,9 @@ export class DeepLinkService implements Disposable {
break; break;
case DeepLinkServiceState.CloneOrAddRepo: case DeepLinkServiceState.CloneOrAddRepo:
if (!repoId || !remoteUrl) {
if (!repoId && !remoteUrl && !repoPath) {
action = DeepLinkServiceAction.DeepLinkErrored; action = DeepLinkServiceAction.DeepLinkErrored;
message = 'Missing repository id or remote url.';
message = 'Missing repository id, remote url and path.';
break; break;
} }
@ -387,7 +414,7 @@ export class DeepLinkService implements Disposable {
break; break;
} }
if (repoOpenUri != null && repoOpenType === DeepLinkRepoOpenType.Clone) {
if (repoOpenUri != null && remoteUrl != null && repoOpenType === DeepLinkRepoOpenType.Clone) {
// clone the repository, then set repoOpenUri to the repo path // clone the repository, then set repoOpenUri to the repo path
try { try {
repoClonePath = await window.withProgress( repoClonePath = await window.withProgress(
@ -495,9 +522,9 @@ export class DeepLinkService implements Disposable {
case DeepLinkServiceState.TargetMatch: case DeepLinkServiceState.TargetMatch:
case DeepLinkServiceState.FetchedTargetMatch: case DeepLinkServiceState.FetchedTargetMatch:
if (!repo || !remote || !targetType) {
if (!repo || !targetType) {
action = DeepLinkServiceAction.DeepLinkErrored; action = DeepLinkServiceAction.DeepLinkErrored;
message = 'Missing repository, remote, or target type.';
message = 'Missing repository or target type.';
break; break;
} }
@ -508,7 +535,7 @@ export class DeepLinkService implements Disposable {
this._context.targetSha = await this.getShaForTarget(); this._context.targetSha = await this.getShaForTarget();
if (!this._context.targetSha) { if (!this._context.targetSha) {
if (state === DeepLinkServiceState.TargetMatch) {
if (state === DeepLinkServiceState.TargetMatch && remote != null) {
action = DeepLinkServiceAction.TargetMatchFailed; action = DeepLinkServiceAction.TargetMatchFailed;
} else { } else {
action = DeepLinkServiceAction.DeepLinkErrored; action = DeepLinkServiceAction.DeepLinkErrored;

Loading…
Cancel
Save