소스 검색

Adds open file from remote command

main
Eric Amodio 4 년 전
부모
커밋
5271e14eb2
13개의 변경된 파일427개의 추가작업 그리고 14개의 파일을 삭제
  1. +5
    -0
      package.json
  2. +1
    -0
      src/commands.ts
  3. +1
    -0
      src/commands/common.ts
  4. +68
    -0
      src/commands/openFileFromRemote.ts
  5. +14
    -0
      src/git/gitService.ts
  6. +5
    -0
      src/git/models/repository.ts
  7. +40
    -3
      src/git/remotes/azure-devops.ts
  8. +70
    -3
      src/git/remotes/bitbucket-server.ts
  9. +70
    -3
      src/git/remotes/bitbucket.ts
  10. +10
    -2
      src/git/remotes/custom.ts
  11. +66
    -0
      src/git/remotes/github.ts
  12. +70
    -3
      src/git/remotes/gitlab.ts
  13. +7
    -0
      src/git/remotes/provider.ts

+ 5
- 0
package.json 파일 보기

@ -2528,6 +2528,11 @@
"icon": "$(globe)"
},
{
"command": "gitlens.openFileFromRemote",
"title": "Open File From Remote",
"category": "GitLens"
},
{
"command": "gitlens.openFileInRemote",
"title": "Open File on Remote",
"category": "GitLens",

+ 1
- 0
src/commands.ts 파일 보기

@ -24,6 +24,7 @@ export * from './commands/openBranchesOnRemote';
export * from './commands/openBranchOnRemote';
export * from './commands/openChangedFiles';
export * from './commands/openCommitOnRemote';
export * from './commands/openFileFromRemote';
export * from './commands/openFileOnRemote';
export * from './commands/openFileAtRevision';
export * from './commands/openFileAtRevisionFrom';

+ 1
- 0
src/commands/common.ts 파일 보기

@ -59,6 +59,7 @@ export enum Commands {
OpenBranchesInRemote = 'gitlens.openBranchesInRemote',
OpenBranchInRemote = 'gitlens.openBranchInRemote',
OpenCommitInRemote = 'gitlens.openCommitInRemote',
OpenFileFromRemote = 'gitlens.openFileFromRemote',
OpenFileInRemote = 'gitlens.openFileInRemote',
OpenFileAtRevision = 'gitlens.openFileRevision',
OpenFileAtRevisionFrom = 'gitlens.openFileRevisionFrom',

+ 68
- 0
src/commands/openFileFromRemote.ts 파일 보기

@ -0,0 +1,68 @@
'use strict';
import { env, Range, Uri, window } from 'vscode';
import { command, Command, Commands, openEditor } from './common';
import { Container } from '../container';
@command()
export class OpenFileFromRemoteCommand extends Command {
constructor() {
super(Commands.OpenFileFromRemote);
}
async execute() {
let clipboard: string | undefined = await env.clipboard.readText();
try {
Uri.parse(clipboard, true);
} catch {
clipboard = undefined;
}
const url = await window.showInputBox({
prompt: 'Enter a remote file url to open',
placeHolder: 'Remote file url',
value: clipboard,
ignoreFocusOut: true,
});
if (url == null || url.length === 0) return;
let local = await Container.git.getLocalInfoFromRemoteUri(Uri.parse(url));
if (local == null) {
local = await Container.git.getLocalInfoFromRemoteUri(Uri.parse(url), { validate: false });
if (local == null) {
void window.showWarningMessage('Unable to parse the provided remote url.');
return;
}
const confirm = 'Open File...';
const pick = await window.showWarningMessage(
'Unable to find a workspace folder that matches the provided remote url.',
confirm,
);
if (pick !== confirm) return;
}
let selection;
if (local.startLine) {
if (local.endLine) {
selection = new Range(local.startLine - 1, 0, local.endLine, 0);
} else {
selection = new Range(local.startLine - 1, 0, local.startLine - 1, 0);
}
}
try {
await openEditor(local.uri, { selection: selection, rethrow: true });
} catch {
const uris = await window.showOpenDialog({
title: 'Open local file',
defaultUri: local.uri,
canSelectMany: false,
canSelectFolders: false,
});
if (uris == null || uris.length === 0) return;
await openEditor(uris[0]);
}
}
}

+ 14
- 0
src/git/gitService.ts 파일 보기

@ -2843,6 +2843,20 @@ export class GitService implements Disposable {
return repo;
}
async getLocalInfoFromRemoteUri(
uri: Uri,
options?: { validate?: boolean },
): Promise<{ uri: Uri; startLine?: number; endLine?: number } | undefined> {
for (const repo of await this.getRepositories()) {
for (const remote of await repo.getRemotes()) {
const local = await remote?.provider?.getLocalInfoFromRemoteUri(repo, uri, options);
if (local != null) return local;
}
}
return undefined;
}
async getRepositoryCount(): Promise<number> {
const repositoryTree = await this.getRepositoryTree();
return repositoryTree.count();

+ 5
- 0
src/git/models/repository.ts 파일 보기

@ -580,6 +580,11 @@ export class Repository implements Disposable {
}
}
toAbsoluteUri(path: string, options?: { validate?: boolean }): Uri | undefined {
const uri = Uri.joinPath(GitUri.file(this.path), path);
return !(options?.validate ?? true) || this.containsUri(uri) ? uri : undefined;
}
unstar() {
return this.updateStarred(false);
}

+ 40
- 3
src/git/remotes/azure-devops.ts 파일 보기

@ -1,8 +1,9 @@
'use strict';
import { Range } from 'vscode';
import { RemoteProvider } from './provider';
import { AutolinkReference } from '../../config';
import { Range, Uri } from 'vscode';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
import { AutolinkReference } from '../../config';
import { Repository } from '../models/repository';
import { RemoteProvider } from './provider';
const gitRegex = /\/_git\/?/i;
const legacyDefaultCollectionRegex = /^DefaultCollection\//i;
@ -10,6 +11,9 @@ const orgAndProjectRegex = /^(.*?)\/(.*?)\/(.*)/;
const sshDomainRegex = /^(ssh|vs-ssh)\./i;
const sshPathRegex = /^\/?v\d\//i;
const fileRegex = /path=([^&]+)/i;
const rangeRegex = /line=(\d+)(?:&lineEnd=(\d+))?/;
export class AzureDevOpsRemote extends RemoteProvider {
constructor(domain: string, path: string, protocol?: string, name?: string, legacy: boolean = false) {
if (sshDomainRegex.test(domain)) {
@ -66,6 +70,39 @@ export class AzureDevOpsRemote extends RemoteProvider {
return this._displayPath;
}
// eslint-disable-next-line @typescript-eslint/require-await
async getLocalInfoFromRemoteUri(
repository: Repository,
uri: Uri,
options?: { validate?: boolean },
): Promise<{ uri: Uri; startLine?: number; endLine?: number } | undefined> {
if (uri.authority !== this.domain) return undefined;
// if ((options?.validate ?? true) && !uri.path.startsWith(`/${this.path}/`)) return undefined;
let startLine;
let endLine;
if (uri.query) {
const match = rangeRegex.exec(uri.query);
if (match != null) {
const [, start, end] = match;
if (start) {
startLine = parseInt(start, 10);
if (end) {
endLine = parseInt(end, 10);
}
}
}
}
const match = fileRegex.exec(uri.query);
if (match == null) return undefined;
const [, path] = match;
const absoluteUri = repository.toAbsoluteUri(path, { validate: options?.validate });
return absoluteUri != null ? { uri: absoluteUri, startLine: startLine, endLine: endLine } : undefined;
}
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
}

+ 70
- 3
src/git/remotes/bitbucket-server.ts 파일 보기

@ -1,8 +1,13 @@
'use strict';
import { Range } from 'vscode';
import { RemoteProvider } from './provider';
import { AutolinkReference } from '../../config';
import { Range, Uri } from 'vscode';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
import { AutolinkReference } from '../../config';
import { GitRevision } from '../models/models';
import { Repository } from '../models/repository';
import { RemoteProvider } from './provider';
const fileRegex = /^\/([^/]+)\/([^/]+?)\/src(.+)$/i;
const rangeRegex = /^lines-(\d+)(?::(\d+))?$/;
export class BitbucketServerRemote extends RemoteProvider {
constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) {
@ -43,6 +48,68 @@ export class BitbucketServerRemote extends RemoteProvider {
return this.formatName('Bitbucket Server');
}
async getLocalInfoFromRemoteUri(
repository: Repository,
uri: Uri,
options?: { validate?: boolean },
): Promise<{ uri: Uri; startLine?: number; endLine?: number } | undefined> {
if (uri.authority !== this.domain) return undefined;
if ((options?.validate ?? true) && !uri.path.startsWith(`/${this.path}/`)) return undefined;
let startLine;
let endLine;
if (uri.fragment) {
const match = rangeRegex.exec(uri.fragment);
if (match != null) {
const [, start, end] = match;
if (start) {
startLine = parseInt(start, 10);
if (end) {
endLine = parseInt(end, 10);
}
}
}
}
const match = fileRegex.exec(uri.path);
if (match == null) return undefined;
const [, , , path] = match;
// Check for a permalink
let index = path.indexOf('/', 1);
if (index !== -1) {
const sha = path.substring(1, index);
if (GitRevision.isSha(sha)) {
const uri = repository.toAbsoluteUri(path.substr(index), { validate: options?.validate });
if (uri != null) return { uri: uri, startLine: startLine, endLine: endLine };
}
}
const branches = new Set<string>(
(
await repository.getBranches({
filter: b => b.remote,
})
).map(b => b.getNameWithoutRemote()),
);
// Check for a link with branch (and deal with branch names with /)
let branch;
index = path.length;
do {
index = path.lastIndexOf('/', index - 1);
branch = path.substring(1, index);
if (branches.has(branch)) {
const uri = repository.toAbsoluteUri(path.substr(index), { validate: options?.validate });
if (uri != null) return { uri: uri, startLine: startLine, endLine: endLine };
}
} while (index > 0);
return undefined;
}
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
}

+ 70
- 3
src/git/remotes/bitbucket.ts 파일 보기

@ -1,8 +1,13 @@
'use strict';
import { Range } from 'vscode';
import { RemoteProvider } from './provider';
import { AutolinkReference } from '../../config';
import { Range, Uri } from 'vscode';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
import { AutolinkReference } from '../../config';
import { GitRevision } from '../models/models';
import { Repository } from '../models/repository';
import { RemoteProvider } from './provider';
const fileRegex = /^\/([^/]+)\/([^/]+?)\/src(.+)$/i;
const rangeRegex = /^lines-(\d+)(?::(\d+))?$/;
export class BitbucketRemote extends RemoteProvider {
constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) {
@ -36,6 +41,68 @@ export class BitbucketRemote extends RemoteProvider {
return this.formatName('Bitbucket');
}
async getLocalInfoFromRemoteUri(
repository: Repository,
uri: Uri,
options?: { validate?: boolean },
): Promise<{ uri: Uri; startLine?: number; endLine?: number } | undefined> {
if (uri.authority !== this.domain) return undefined;
if ((options?.validate ?? true) && !uri.path.startsWith(`/${this.path}/`)) return undefined;
let startLine;
let endLine;
if (uri.fragment) {
const match = rangeRegex.exec(uri.fragment);
if (match != null) {
const [, start, end] = match;
if (start) {
startLine = parseInt(start, 10);
if (end) {
endLine = parseInt(end, 10);
}
}
}
}
const match = fileRegex.exec(uri.path);
if (match == null) return undefined;
const [, , , path] = match;
// Check for a permalink
let index = path.indexOf('/', 1);
if (index !== -1) {
const sha = path.substring(1, index);
if (GitRevision.isSha(sha)) {
const uri = repository.toAbsoluteUri(path.substr(index), { validate: options?.validate });
if (uri != null) return { uri: uri, startLine: startLine, endLine: endLine };
}
}
const branches = new Set<string>(
(
await repository.getBranches({
filter: b => b.remote,
})
).map(b => b.getNameWithoutRemote()),
);
// Check for a link with branch (and deal with branch names with /)
let branch;
index = path.length;
do {
index = path.lastIndexOf('/', index - 1);
branch = path.substring(1, index);
if (branches.has(branch)) {
const uri = repository.toAbsoluteUri(path.substr(index), { validate: options?.validate });
if (uri != null) return { uri: uri, startLine: startLine, endLine: endLine };
}
} while (index > 0);
return undefined;
}
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
}

+ 10
- 2
src/git/remotes/custom.ts 파일 보기

@ -1,8 +1,9 @@
'use strict';
import { Range } from 'vscode';
import { Range, Uri } from 'vscode';
import { RemotesUrlsConfig } from '../../configuration';
import { Strings } from '../../system';
import { Repository } from '../models/repository';
import { RemoteProvider } from './provider';
import { Strings } from '../../system';
export class CustomRemote extends RemoteProvider {
private readonly urls: RemotesUrlsConfig;
@ -16,6 +17,13 @@ export class CustomRemote extends RemoteProvider {
return this.formatName('Custom');
}
getLocalInfoFromRemoteUri(
_repository: Repository,
_uri: Uri,
): Promise<{ uri: Uri; startLine?: number; endLine?: number } | undefined> {
return Promise.resolve(undefined);
}
protected getUrlForRepository(): string {
return Strings.interpolate(this.urls.repository, this.getContext());
}

+ 66
- 0
src/git/remotes/github.ts 파일 보기

@ -4,10 +4,14 @@ import { DynamicAutolinkReference } from '../../annotations/autolinks';
import { AutolinkReference } from '../../config';
import { Container } from '../../container';
import { IssueOrPullRequest } from '../models/issue';
import { GitRevision } from '../models/models';
import { PullRequest } from '../models/pullRequest';
import { Repository } from '../models/repository';
import { RemoteProviderWithApi } from './provider';
const issueEnricher3rdParyRegex = /\b(\w+\\?-?\w+(?!\\?-)\/\w+\\?-?\w+(?!\\?-))\\?#([0-9]+)\b/g;
const fileRegex = /^\/([^/]+)\/([^/]+?)\/blob(.+)$/i;
const rangeRegex = /^L(\d+)(?:-L(\d+))?$/;
export class GitHubRemote extends RemoteProviderWithApi<{ token: string }> {
private readonly Buttons = class {
@ -115,6 +119,68 @@ export class GitHubRemote extends RemoteProviderWithApi<{ token: string }> {
return true;
}
async getLocalInfoFromRemoteUri(
repository: Repository,
uri: Uri,
options?: { validate?: boolean },
): Promise<{ uri: Uri; startLine?: number; endLine?: number } | undefined> {
if (uri.authority !== this.domain) return undefined;
if ((options?.validate ?? true) && !uri.path.startsWith(`/${this.path}/`)) return undefined;
let startLine;
let endLine;
if (uri.fragment) {
const match = rangeRegex.exec(uri.fragment);
if (match != null) {
const [, start, end] = match;
if (start) {
startLine = parseInt(start, 10);
if (end) {
endLine = parseInt(end, 10);
}
}
}
}
const match = fileRegex.exec(uri.path);
if (match == null) return undefined;
const [, , , path] = match;
// Check for a permalink
let index = path.indexOf('/', 1);
if (index !== -1) {
const sha = path.substring(1, index);
if (GitRevision.isSha(sha)) {
const uri = repository.toAbsoluteUri(path.substr(index), { validate: options?.validate });
if (uri != null) return { uri: uri, startLine: startLine, endLine: endLine };
}
}
const branches = new Set<string>(
(
await repository.getBranches({
filter: b => b.remote,
})
).map(b => b.getNameWithoutRemote()),
);
// Check for a link with branch (and deal with branch names with /)
let branch;
index = path.length;
do {
index = path.lastIndexOf('/', index - 1);
branch = path.substring(1, index);
if (branches.has(branch)) {
const uri = repository.toAbsoluteUri(path.substr(index), { validate: options?.validate });
if (uri != null) return { uri: uri, startLine: startLine, endLine: endLine };
}
} while (index > 0);
return undefined;
}
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
}

+ 70
- 3
src/git/remotes/gitlab.ts 파일 보기

@ -1,8 +1,13 @@
'use strict';
import { Range } from 'vscode';
import { RemoteProvider } from './provider';
import { AutolinkReference } from '../../config';
import { Range, Uri } from 'vscode';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
import { AutolinkReference } from '../../config';
import { GitRevision } from '../models/models';
import { Repository } from '../models/repository';
import { RemoteProvider } from './provider';
const fileRegex = /^\/([^/]+)\/([^/]+?)\/-\/blob(.+)$/i;
const rangeRegex = /^L(\d+)(?:-(\d+))?$/;
export class GitLabRemote extends RemoteProvider {
constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) {
@ -31,6 +36,68 @@ export class GitLabRemote extends RemoteProvider {
return this.formatName('GitLab');
}
async getLocalInfoFromRemoteUri(
repository: Repository,
uri: Uri,
options?: { validate?: boolean },
): Promise<{ uri: Uri; startLine?: number; endLine?: number } | undefined> {
if (uri.authority !== this.domain) return undefined;
if ((options?.validate ?? true) && !uri.path.startsWith(`/${this.path}/`)) return undefined;
let startLine;
let endLine;
if (uri.fragment) {
const match = rangeRegex.exec(uri.fragment);
if (match != null) {
const [, start, end] = match;
if (start) {
startLine = parseInt(start, 10);
if (end) {
endLine = parseInt(end, 10);
}
}
}
}
const match = fileRegex.exec(uri.path);
if (match == null) return undefined;
const [, , , path] = match;
// Check for a permalink
let index = path.indexOf('/', 1);
if (index !== -1) {
const sha = path.substring(1, index);
if (GitRevision.isSha(sha)) {
const uri = repository.toAbsoluteUri(path.substr(index), { validate: options?.validate });
if (uri != null) return { uri: uri, startLine: startLine, endLine: endLine };
}
}
const branches = new Set<string>(
(
await repository.getBranches({
filter: b => b.remote,
})
).map(b => b.getNameWithoutRemote()),
);
// Check for a link with branch (and deal with branch names with /)
let branch;
index = path.length;
do {
index = path.lastIndexOf('/', index - 1);
branch = path.substring(1, index);
if (branches.has(branch)) {
const uri = repository.toAbsoluteUri(path.substr(index), { validate: options?.validate });
if (uri != null) return { uri: uri, startLine: startLine, endLine: endLine };
}
} while (index > 0);
return undefined;
}
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
}

+ 7
- 0
src/git/remotes/provider.ts 파일 보기

@ -9,6 +9,7 @@ import { Messages } from '../../messages';
import { IssueOrPullRequest } from '../models/issue';
import { GitLogCommit } from '../models/logCommit';
import { PullRequest } from '../models/pullRequest';
import { Repository } from '../models/repository';
import { debug, gate, Promises } from '../../system';
export class CredentialError extends Error {
@ -129,6 +130,12 @@ export abstract class RemoteProvider {
return RemoteProviderWithApi.is(this);
}
abstract getLocalInfoFromRemoteUri(
repository: Repository,
uri: Uri,
options?: { validate?: boolean },
): Promise<{ uri: Uri; startLine?: number; endLine?: number } | undefined>;
open(resource: RemoteResource): Promise<boolean | undefined> {
return this.openUrl(this.url(resource));
}

불러오는 중...
취소
저장