Browse Source

Improves "best" remote detection

Improves autolink targets -- refs: #2425
main
Eric Amodio 1 year ago
parent
commit
d10f86753c
11 changed files with 271 additions and 19 deletions
  1. +5
    -0
      src/annotations/autolinks.ts
  2. +10
    -1
      src/git/gitProviderService.ts
  3. +12
    -0
      src/git/models/repositoryMetadata.ts
  4. +18
    -8
      src/git/remotes/github.ts
  5. +20
    -8
      src/git/remotes/gitlab.ts
  6. +4
    -0
      src/git/remotes/remoteProvider.ts
  7. +34
    -1
      src/git/remotes/richRemoteProvider.ts
  8. +88
    -0
      src/plus/github/github.ts
  9. +59
    -1
      src/plus/gitlab/gitlab.ts
  10. +16
    -0
      src/plus/gitlab/models.ts
  11. +5
    -0
      src/views/nodes/commitNode.ts

+ 5
- 0
src/annotations/autolinks.ts View File

@ -257,6 +257,11 @@ export class Autolinks implements Disposable {
}
if (remotes != null && remotes.length !== 0) {
remotes = [...remotes].sort((a, b) => {
const aConnected = a.provider?.maybeConnected;
const bConnected = b.provider?.maybeConnected;
return aConnected !== bConnected ? (aConnected ? -1 : bConnected ? 1 : 0) : 0;
});
for (const r of remotes) {
if (r.provider == null) continue;

+ 10
- 1
src/git/gitProviderService.ts View File

@ -2166,7 +2166,16 @@ export class GitProviderService implements Disposable {
}
// Don't choose a remote unless its weighted above
const matchedWeight = weightedRemotes.get(r.name) ?? -1;
let matchedWeight = weightedRemotes.get(r.name) ?? -1;
const p = r.provider;
if (p.hasRichIntegration() && p.maybeConnected) {
const m = await p.getRepositoryMetadata();
if (m?.isFork === false) {
matchedWeight += 101;
}
}
if (matchedWeight > weight) {
bestRemote = r;
weight = matchedWeight;

+ 12
- 0
src/git/models/repositoryMetadata.ts View File

@ -0,0 +1,12 @@
import type { RemoteProviderReference } from './remoteProvider';
export interface RepositoryMetadata {
provider: RemoteProviderReference;
owner: string;
name: string;
isFork: boolean;
parent?: {
owner: string;
name: string;
};
}

+ 18
- 8
src/git/remotes/github.ts View File

@ -18,6 +18,7 @@ import type { IssueOrPullRequest, SearchedIssue } from '../models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../models/pullRequest';
import { isSha } from '../models/reference';
import type { Repository } from '../models/repository';
import type { RepositoryMetadata } from '../models/repositoryMetadata';
import { ensurePaidPlan, RichRemoteProvider } from './richRemoteProvider';
const autolinkFullIssuesRegex = /\b(?<repo>[^/\s]+\/[^/\s]+)#(?<num>[0-9]+)\b(?!]\()/g;
@ -259,7 +260,7 @@ export class GitHubRemote extends RichRemoteProvider {
return `${this.encodeUrl(`${this.baseUrl}?path=${fileName}`)}${line}`;
}
protected async getProviderAccountForCommit(
protected override async getProviderAccountForCommit(
{ accessToken }: AuthenticationSession,
ref: string,
options?: {
@ -273,7 +274,7 @@ export class GitHubRemote extends RichRemoteProvider {
});
}
protected async getProviderAccountForEmail(
protected override async getProviderAccountForEmail(
{ accessToken }: AuthenticationSession,
email: string,
options?: {
@ -287,7 +288,7 @@ export class GitHubRemote extends RichRemoteProvider {
});
}
protected async getProviderDefaultBranch({
protected override async getProviderDefaultBranch({
accessToken,
}: AuthenticationSession): Promise<DefaultBranch | undefined> {
const [owner, repo] = this.splitPath();
@ -296,7 +297,7 @@ export class GitHubRemote extends RichRemoteProvider {
});
}
protected async getProviderIssueOrPullRequest(
protected override async getProviderIssueOrPullRequest(
{ accessToken }: AuthenticationSession,
id: string,
): Promise<IssueOrPullRequest | undefined> {
@ -306,7 +307,7 @@ export class GitHubRemote extends RichRemoteProvider {
});
}
protected async getProviderPullRequestForBranch(
protected override async getProviderPullRequestForBranch(
{ accessToken }: AuthenticationSession,
branch: string,
options?: {
@ -326,7 +327,7 @@ export class GitHubRemote extends RichRemoteProvider {
});
}
protected async getProviderPullRequestForCommit(
protected override async getProviderPullRequestForCommit(
{ accessToken }: AuthenticationSession,
ref: string,
): Promise<PullRequest | undefined> {
@ -336,7 +337,16 @@ export class GitHubRemote extends RichRemoteProvider {
});
}
protected async searchProviderMyPullRequests({
protected override async getProviderRepositoryMetadata({
accessToken,
}: AuthenticationSession): Promise<RepositoryMetadata | undefined> {
const [owner, repo] = this.splitPath();
return (await this.container.github)?.getRepositoryMetadata(this, accessToken, owner, repo, {
baseUrl: this.apiBaseUrl,
});
}
protected override async searchProviderMyPullRequests({
accessToken,
}: AuthenticationSession): Promise<SearchedPullRequest[] | undefined> {
return (await this.container.github)?.searchMyPullRequests(this, accessToken, {
@ -345,7 +355,7 @@ export class GitHubRemote extends RichRemoteProvider {
});
}
protected async searchProviderMyIssues({
protected override async searchProviderMyIssues({
accessToken,
}: AuthenticationSession): Promise<SearchedIssue[] | undefined> {
return (await this.container.github)?.searchMyIssues(this, accessToken, {

+ 20
- 8
src/git/remotes/gitlab.ts View File

@ -18,6 +18,7 @@ import type { IssueOrPullRequest, SearchedIssue } from '../models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../models/pullRequest';
import { isSha } from '../models/reference';
import type { Repository } from '../models/repository';
import type { RepositoryMetadata } from '../models/repositoryMetadata';
import { ensurePaidPlan, RichRemoteProvider } from './richRemoteProvider';
const autolinkFullIssuesRegex = /\b(?<repo>[^/\s]+\/[^/\s]+)#(?<num>[0-9]+)\b(?!]\()/g;
@ -290,7 +291,7 @@ export class GitLabRemote extends RichRemoteProvider {
return `${this.encodeUrl(`${this.baseUrl}?path=${fileName}`)}${line}`;
}
protected async getProviderAccountForCommit(
protected override async getProviderAccountForCommit(
{ accessToken }: AuthenticationSession,
ref: string,
options?: {
@ -304,7 +305,7 @@ export class GitLabRemote extends RichRemoteProvider {
});
}
protected async getProviderAccountForEmail(
protected override async getProviderAccountForEmail(
{ accessToken }: AuthenticationSession,
email: string,
options?: {
@ -318,7 +319,7 @@ export class GitLabRemote extends RichRemoteProvider {
});
}
protected async getProviderDefaultBranch({
protected override async getProviderDefaultBranch({
accessToken,
}: AuthenticationSession): Promise<DefaultBranch | undefined> {
const [owner, repo] = this.splitPath();
@ -327,7 +328,7 @@ export class GitLabRemote extends RichRemoteProvider {
});
}
protected async getProviderIssueOrPullRequest(
protected override async getProviderIssueOrPullRequest(
{ accessToken }: AuthenticationSession,
id: string,
): Promise<IssueOrPullRequest | undefined> {
@ -337,7 +338,7 @@ export class GitLabRemote extends RichRemoteProvider {
});
}
protected async getProviderPullRequestForBranch(
protected override async getProviderPullRequestForBranch(
{ accessToken }: AuthenticationSession,
branch: string,
options?: {
@ -357,7 +358,7 @@ export class GitLabRemote extends RichRemoteProvider {
});
}
protected async getProviderPullRequestForCommit(
protected override async getProviderPullRequestForCommit(
{ accessToken }: AuthenticationSession,
ref: string,
): Promise<PullRequest | undefined> {
@ -367,13 +368,24 @@ export class GitLabRemote extends RichRemoteProvider {
});
}
protected async searchProviderMyPullRequests(
protected override async getProviderRepositoryMetadata({
accessToken,
}: AuthenticationSession): Promise<RepositoryMetadata | undefined> {
const [owner, repo] = this.splitPath();
return (await this.container.gitlab)?.getRepositoryMetadata(this, accessToken, owner, repo, {
baseUrl: this.apiBaseUrl,
});
}
protected override async searchProviderMyPullRequests(
_session: AuthenticationSession,
): Promise<SearchedPullRequest[] | undefined> {
return Promise.resolve(undefined);
}
protected async searchProviderMyIssues(_session: AuthenticationSession): Promise<SearchedIssue[] | undefined> {
protected override async searchProviderMyIssues(
_session: AuthenticationSession,
): Promise<SearchedIssue[] | undefined> {
return Promise.resolve(undefined);
}
}

+ 4
- 0
src/git/remotes/remoteProvider.ts View File

@ -59,6 +59,10 @@ export abstract class RemoteProvider implements RemoteProviderReference {
return this.type === 'rich';
}
get maybeConnected(): boolean | undefined {
return false;
}
abstract getLocalInfoFromRemoteUri(
repository: Repository,
uri: Uri,

+ 34
- 1
src/git/remotes/richRemoteProvider.ts View File

@ -17,6 +17,7 @@ import type { Account } from '../models/author';
import type { DefaultBranch } from '../models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../models/pullRequest';
import type { RepositoryMetadata } from '../models/repositoryMetadata';
import { RemoteProvider } from './remoteProvider';
// TODO@eamodio revisit how once authenticated, all remotes are always connected, even after a restart
@ -73,7 +74,7 @@ export abstract class RichRemoteProvider extends RemoteProvider {
return `connected:${this.key}`;
}
get maybeConnected(): boolean | undefined {
override get maybeConnected(): boolean | undefined {
return this._session === undefined ? undefined : this._session !== null;
}
@ -145,6 +146,7 @@ export abstract class RichRemoteProvider extends RemoteProvider {
}
this.resetRequestExceptionCount();
this._repoMetadata = undefined;
this._prsByCommit.clear();
this._session = null;
@ -286,6 +288,37 @@ export abstract class RichRemoteProvider extends RemoteProvider {
accessToken,
}: AuthenticationSession): Promise<DefaultBranch | undefined>;
private _repoMetadata: RepositoryMetadata | undefined;
@gate()
@debug()
async getRepositoryMetadata(): Promise<RepositoryMetadata | undefined> {
if (this._repoMetadata != null) return this._repoMetadata;
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
try {
const metadata = await this.getProviderRepositoryMetadata(this._session!);
this._repoMetadata = metadata;
this.resetRequestExceptionCount();
return metadata;
} catch (ex) {
Logger.error(ex, scope);
if (ex instanceof AuthenticationError || ex instanceof ProviderRequestClientError) {
this.trackRequestException();
}
return undefined;
}
}
protected abstract getProviderRepositoryMetadata({
accessToken,
}: AuthenticationSession): Promise<RepositoryMetadata | undefined>;
@gate()
@debug()
async searchMyPullRequests(): Promise<SearchedPullRequest[] | undefined> {

+ 88
- 0
src/plus/github/github.ts View File

@ -23,6 +23,7 @@ import type { DefaultBranch } from '../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../git/models/issue';
import type { PullRequest, SearchedPullRequest } from '../../git/models/pullRequest';
import { isSha } from '../../git/models/reference';
import type { RepositoryMetadata } from '../../git/models/repositoryMetadata';
import type { GitUser } from '../../git/models/user';
import { getGitHubNoReplyAddressParts } from '../../git/remotes/github';
import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider';
@ -736,6 +737,93 @@ export class GitHubApi implements Disposable {
}
}
@debug<GitHubApi['getRepositoryMetadata']>({ args: { 0: p => p.name, 1: '<token>' } })
async getRepositoryMetadata(
provider: RichRemoteProvider,
token: string,
owner: string,
repo: string,
options?: {
baseUrl?: string;
},
): Promise<RepositoryMetadata | undefined> {
const scope = getLogScope();
interface QueryResult {
repository:
| {
owner: {
login: string;
};
name: string;
parent:
| {
owner: {
login: string;
};
name: string;
}
| null
| undefined;
}
| null
| undefined;
}
try {
const query = `query getRepositoryMetadata(
$owner: String!
$repo: String!
) {
repository(name: $repo, owner: $owner) {
owner {
login
}
name
parent {
owner {
login
}
name
}
}
}`;
const rsp = await this.graphql<QueryResult>(
provider,
token,
query,
{
...options,
owner: owner,
repo: repo,
},
scope,
);
const r = rsp?.repository ?? undefined;
if (r == null) return undefined;
return {
provider: provider,
owner: r.owner.login,
name: r.name,
isFork: r.parent != null,
parent:
r.parent != null
? {
owner: r.parent.owner.login,
name: r.parent.name,
}
: undefined,
};
} catch (ex) {
if (ex instanceof ProviderRequestNotFoundError) return undefined;
throw this.handleException(ex, provider, scope);
}
}
@debug<GitHubApi['getBlame']>({ args: { 0: '<token>' } })
async getBlame(token: string, owner: string, repo: string, ref: string, path: string): Promise<GitHubBlame> {
const scope = getLogScope();

+ 59
- 1
src/plus/gitlab/gitlab.ts View File

@ -19,6 +19,7 @@ import type { DefaultBranch } from '../../git/models/defaultBranch';
import type { IssueOrPullRequest } from '../../git/models/issue';
import { IssueOrPullRequestType } from '../../git/models/issue';
import { PullRequest } from '../../git/models/pullRequest';
import type { RepositoryMetadata } from '../../git/models/repositoryMetadata';
import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider';
import {
showIntegrationRequestFailed500WarningMessage,
@ -32,7 +33,14 @@ import type { LogScope } from '../../system/logger.scope';
import { getLogScope, setLogScopeExit } from '../../system/logger.scope';
import { Stopwatch } from '../../system/stopwatch';
import { equalsIgnoreCase } from '../../system/string';
import type { GitLabCommit, GitLabIssue, GitLabMergeRequest, GitLabMergeRequestREST, GitLabUser } from './models';
import type {
GitLabCommit,
GitLabIssue,
GitLabMergeRequest,
GitLabMergeRequestREST,
GitLabProjectREST,
GitLabUser,
} from './models';
import { fromGitLabMergeRequestREST, fromGitLabMergeRequestState, GitLabMergeRequestState } from './models';
export class GitLabApi implements Disposable {
@ -533,6 +541,56 @@ export class GitLabApi implements Disposable {
}
}
@debug<GitLabApi['getRepositoryMetadata']>({ args: { 0: p => p.name, 1: '<token>' } })
async getRepositoryMetadata(
provider: RichRemoteProvider,
token: string,
owner: string,
repo: string,
options?: {
baseUrl?: string;
},
): Promise<RepositoryMetadata | undefined> {
const scope = getLogScope();
const projectId = await this.getProjectId(provider, token, owner, repo, options?.baseUrl);
if (!projectId) return undefined;
try {
const proj = await this.request<GitLabProjectREST>(
provider,
token,
options?.baseUrl,
`v4/projects/${projectId}`,
{
method: 'GET',
// ...options,
},
scope,
);
if (proj == null) return undefined;
return {
provider: provider,
owner: proj.namespace.full_path,
name: proj.path,
isFork: proj.forked_from_project != null,
parent:
proj.forked_from_project != null
? {
owner: proj.forked_from_project.namespace.full_path,
name: proj.forked_from_project.path,
}
: undefined,
} satisfies RepositoryMetadata;
} catch (ex) {
if (ex instanceof ProviderRequestNotFoundError) return undefined;
throw this.handleException(ex, provider, scope);
}
}
private async findUser(
provider: RichRemoteProvider,
token: string,

+ 16
- 0
src/plus/gitlab/models.ts View File

@ -118,3 +118,19 @@ export function fromGitLabMergeRequestREST(pr: GitLabMergeRequestREST, provider:
pr.merged_at == null ? undefined : new Date(pr.merged_at),
);
}
export interface GitLabProjectREST {
namespace: {
path: string;
full_path: string;
};
path: string;
forked_from_project?: {
namespace: {
path: string;
full_path: string;
};
path: string;
};
}

+ 5
- 0
src/views/nodes/commitNode.ts View File

@ -243,6 +243,11 @@ export class CommitNode extends ViewRefNode
const remotes = await this.view.container.git.getRemotesWithProviders(this.commit.repoPath, { sort: true });
const remote = await this.view.container.git.getBestRemoteWithRichProvider(remotes);
// If we have a "best" remote, move it to the front of the list
if (remote != null) {
remotes.sort((a, b) => (a === remote ? -1 : b === remote ? 1 : 0));
}
if (this.commit.message == null) {
await this.commit.ensureFullDetails();
}

||||||
x
 
000:0
Loading…
Cancel
Save