|
@ -1,33 +1,43 @@ |
|
|
import type { AuthenticationSession, Disposable, QuickInputButton, Range } from 'vscode'; |
|
|
import type { AuthenticationSession, Disposable, QuickInputButton, Range } from 'vscode'; |
|
|
import { env, ThemeIcon, Uri, window } from 'vscode'; |
|
|
import { env, ThemeIcon, Uri, window } from 'vscode'; |
|
|
import type { Autolink, DynamicAutolinkReference } from '../../annotations/autolinks'; |
|
|
|
|
|
|
|
|
import type { Autolink, DynamicAutolinkReference, MaybeEnrichedAutolink } from '../../annotations/autolinks'; |
|
|
import type { AutolinkReference } from '../../config'; |
|
|
import type { AutolinkReference } from '../../config'; |
|
|
|
|
|
import { GlyphChars } from '../../constants'; |
|
|
import type { Container } from '../../container'; |
|
|
import type { Container } from '../../container'; |
|
|
import type { |
|
|
import type { |
|
|
IntegrationAuthenticationProvider, |
|
|
IntegrationAuthenticationProvider, |
|
|
IntegrationAuthenticationSessionDescriptor, |
|
|
IntegrationAuthenticationSessionDescriptor, |
|
|
} from '../../plus/integrationAuthentication'; |
|
|
} from '../../plus/integrationAuthentication'; |
|
|
|
|
|
import { fromNow } from '../../system/date'; |
|
|
import { log } from '../../system/decorators/log'; |
|
|
import { log } from '../../system/decorators/log'; |
|
|
import { encodeUrl } from '../../system/encoding'; |
|
|
import { encodeUrl } from '../../system/encoding'; |
|
|
import { equalsIgnoreCase } from '../../system/string'; |
|
|
|
|
|
|
|
|
import { equalsIgnoreCase, escapeMarkdown } from '../../system/string'; |
|
|
import { supportedInVSCodeVersion } from '../../system/utils'; |
|
|
import { supportedInVSCodeVersion } from '../../system/utils'; |
|
|
import type { Account } from '../models/author'; |
|
|
import type { Account } from '../models/author'; |
|
|
import type { DefaultBranch } from '../models/defaultBranch'; |
|
|
import type { DefaultBranch } from '../models/defaultBranch'; |
|
|
import type { IssueOrPullRequest, SearchedIssue } from '../models/issue'; |
|
|
import type { IssueOrPullRequest, SearchedIssue } from '../models/issue'; |
|
|
|
|
|
import { getIssueOrPullRequestMarkdownIcon } from '../models/issue'; |
|
|
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../models/pullRequest'; |
|
|
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../models/pullRequest'; |
|
|
import { isSha } from '../models/reference'; |
|
|
import { isSha } from '../models/reference'; |
|
|
import type { Repository } from '../models/repository'; |
|
|
import type { Repository } from '../models/repository'; |
|
|
import type { RepositoryMetadata } from '../models/repositoryMetadata'; |
|
|
import type { RepositoryMetadata } from '../models/repositoryMetadata'; |
|
|
import { ensurePaidPlan, RichRemoteProvider } from './richRemoteProvider'; |
|
|
import { ensurePaidPlan, RichRemoteProvider } from './richRemoteProvider'; |
|
|
|
|
|
|
|
|
const autolinkFullIssuesRegex = /\b(?<repo>[^/\s]+\/[^/\s]+)#(?<num>[0-9]+)\b(?!]\()/g; |
|
|
|
|
|
const autolinkFullMergeRequestsRegex = /\b(?<repo>[^/\s]+\/[^/\s]+)!(?<num>[0-9]+)\b(?!]\()/g; |
|
|
|
|
|
|
|
|
const autolinkFullIssuesRegex = /\b([^/\s]+\/[^/\s]+?)(?:\\)?#([0-9]+)\b(?!]\()/g; |
|
|
|
|
|
const autolinkFullMergeRequestsRegex = /\b([^/\s]+\/[^/\s]+?)(?:\\)?!([0-9]+)\b(?!]\()/g; |
|
|
const fileRegex = /^\/([^/]+)\/([^/]+?)\/-\/blob(.+)$/i; |
|
|
const fileRegex = /^\/([^/]+)\/([^/]+?)\/-\/blob(.+)$/i; |
|
|
const rangeRegex = /^L(\d+)(?:-(\d+))?$/; |
|
|
const rangeRegex = /^L(\d+)(?:-(\d+))?$/; |
|
|
|
|
|
|
|
|
const authProvider = Object.freeze({ id: 'gitlab', scopes: ['read_api', 'read_user', 'read_repository'] }); |
|
|
const authProvider = Object.freeze({ id: 'gitlab', scopes: ['read_api', 'read_user', 'read_repository'] }); |
|
|
|
|
|
|
|
|
export class GitLabRemote extends RichRemoteProvider { |
|
|
|
|
|
|
|
|
type GitLabRepositoryDescriptor = |
|
|
|
|
|
| { |
|
|
|
|
|
owner: string; |
|
|
|
|
|
name: string; |
|
|
|
|
|
} |
|
|
|
|
|
| Record<string, never>; |
|
|
|
|
|
|
|
|
|
|
|
export class GitLabRemote extends RichRemoteProvider<GitLabRepositoryDescriptor> { |
|
|
protected get authProvider() { |
|
|
protected get authProvider() { |
|
|
return authProvider; |
|
|
return authProvider; |
|
|
} |
|
|
} |
|
@ -72,6 +82,9 @@ export class GitLabRemote extends RichRemoteProvider { |
|
|
text: string, |
|
|
text: string, |
|
|
outputFormat: 'html' | 'markdown' | 'plaintext', |
|
|
outputFormat: 'html' | 'markdown' | 'plaintext', |
|
|
tokenMapping: Map<string, string>, |
|
|
tokenMapping: Map<string, string>, |
|
|
|
|
|
enrichedAutolinks?: Map<string, MaybeEnrichedAutolink>, |
|
|
|
|
|
prs?: Set<string>, |
|
|
|
|
|
footnotes?: Map<number, string>, |
|
|
) => { |
|
|
) => { |
|
|
return outputFormat === 'plaintext' |
|
|
return outputFormat === 'plaintext' |
|
|
? text |
|
|
? text |
|
@ -86,29 +99,68 @@ export class GitLabRemote extends RichRemoteProvider { |
|
|
tokenMapping.set(token, `<a href="${url}" title=${title}>${linkText}</a>`); |
|
|
tokenMapping.set(token, `<a href="${url}" title=${title}>${linkText}</a>`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let footnoteIndex: number; |
|
|
|
|
|
|
|
|
|
|
|
const issueResult = enrichedAutolinks?.get(num)?.[0]; |
|
|
|
|
|
if (issueResult?.value != null) { |
|
|
|
|
|
if (issueResult.paused) { |
|
|
|
|
|
if (footnotes != null && !prs?.has(num)) { |
|
|
|
|
|
footnoteIndex = footnotes.size + 1; |
|
|
|
|
|
footnotes.set( |
|
|
|
|
|
footnoteIndex, |
|
|
|
|
|
`[${getIssueOrPullRequestMarkdownIcon()} GitLab Issue ${repo}#${num} $(loading~spin)](${url}${title}")`, |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
const issue = issueResult.value; |
|
|
|
|
|
const issueTitle = escapeMarkdown(issue.title.trim()); |
|
|
|
|
|
if (footnotes != null && !prs?.has(num)) { |
|
|
|
|
|
footnoteIndex = footnotes.size + 1; |
|
|
|
|
|
footnotes.set( |
|
|
|
|
|
footnoteIndex, |
|
|
|
|
|
`[${getIssueOrPullRequestMarkdownIcon( |
|
|
|
|
|
issue, |
|
|
|
|
|
)} **${issueTitle}**](${url}${title})\\\n${GlyphChars.Space.repeat( |
|
|
|
|
|
5, |
|
|
|
|
|
)}${linkText} ${issue.state} ${fromNow( |
|
|
|
|
|
issue.closedDate ?? issue.date, |
|
|
|
|
|
)}`,
|
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} else if (footnotes != null && !prs?.has(num)) { |
|
|
|
|
|
footnoteIndex = footnotes.size + 1; |
|
|
|
|
|
footnotes.set( |
|
|
|
|
|
footnoteIndex, |
|
|
|
|
|
`[${getIssueOrPullRequestMarkdownIcon()} GitLab Issue ${repo}#${num}](${url}${title})`, |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
return token; |
|
|
return token; |
|
|
}); |
|
|
}); |
|
|
}, |
|
|
}, |
|
|
parse: (text: string, autolinks: Map<string, Autolink>) => { |
|
|
parse: (text: string, autolinks: Map<string, Autolink>) => { |
|
|
let repo: string; |
|
|
|
|
|
|
|
|
let ownerAndRepo: string; |
|
|
let num: string; |
|
|
let num: string; |
|
|
|
|
|
|
|
|
let match; |
|
|
let match; |
|
|
do { |
|
|
do { |
|
|
match = autolinkFullIssuesRegex.exec(text); |
|
|
match = autolinkFullIssuesRegex.exec(text); |
|
|
if (match?.groups == null) break; |
|
|
|
|
|
|
|
|
if (match == null) break; |
|
|
|
|
|
|
|
|
({ repo, num } = match.groups); |
|
|
|
|
|
|
|
|
[, ownerAndRepo, num] = match; |
|
|
|
|
|
|
|
|
|
|
|
const [owner, repo] = ownerAndRepo.split('/', 2); |
|
|
autolinks.set(num, { |
|
|
autolinks.set(num, { |
|
|
provider: this, |
|
|
provider: this, |
|
|
id: num, |
|
|
id: num, |
|
|
prefix: `${repo}#`, |
|
|
|
|
|
url: `${this.protocol}://${this.domain}/${repo}/-/issues/${num}`, |
|
|
|
|
|
title: `Open Issue #<num> from ${repo} on ${this.name}`, |
|
|
|
|
|
|
|
|
prefix: `${ownerAndRepo}#`, |
|
|
|
|
|
url: `${this.protocol}://${this.domain}/${ownerAndRepo}/-/issues/${num}`, |
|
|
|
|
|
title: `Open Issue #<num> from ${ownerAndRepo} on ${this.name}`, |
|
|
|
|
|
|
|
|
type: 'issue', |
|
|
type: 'issue', |
|
|
description: `${this.name} Issue ${repo}#${num}`, |
|
|
|
|
|
|
|
|
description: `${this.name} Issue ${ownerAndRepo}#${num}`, |
|
|
|
|
|
descriptor: { owner: owner, name: repo } satisfies GitLabRepositoryDescriptor, |
|
|
}); |
|
|
}); |
|
|
} while (true); |
|
|
} while (true); |
|
|
}, |
|
|
}, |
|
@ -118,6 +170,9 @@ export class GitLabRemote extends RichRemoteProvider { |
|
|
text: string, |
|
|
text: string, |
|
|
outputFormat: 'html' | 'markdown' | 'plaintext', |
|
|
outputFormat: 'html' | 'markdown' | 'plaintext', |
|
|
tokenMapping: Map<string, string>, |
|
|
tokenMapping: Map<string, string>, |
|
|
|
|
|
enrichedAutolinks?: Map<string, MaybeEnrichedAutolink>, |
|
|
|
|
|
prs?: Set<string>, |
|
|
|
|
|
footnotes?: Map<number, string>, |
|
|
) => { |
|
|
) => { |
|
|
return outputFormat === 'plaintext' |
|
|
return outputFormat === 'plaintext' |
|
|
? text |
|
|
? text |
|
@ -136,30 +191,73 @@ export class GitLabRemote extends RichRemoteProvider { |
|
|
tokenMapping.set(token, `<a href="${url}" title=${title}>${linkText}</a>`); |
|
|
tokenMapping.set(token, `<a href="${url}" title=${title}>${linkText}</a>`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let footnoteIndex: number; |
|
|
|
|
|
|
|
|
|
|
|
const issueResult = enrichedAutolinks?.get(num)?.[0]; |
|
|
|
|
|
if (issueResult?.value != null) { |
|
|
|
|
|
if (issueResult.paused) { |
|
|
|
|
|
if (footnotes != null && !prs?.has(num)) { |
|
|
|
|
|
footnoteIndex = footnotes.size + 1; |
|
|
|
|
|
footnotes.set( |
|
|
|
|
|
footnoteIndex, |
|
|
|
|
|
`[${getIssueOrPullRequestMarkdownIcon()} ${ |
|
|
|
|
|
this.name |
|
|
|
|
|
} Merge Request ${repo}!${num} $(loading~spin)](${url}${title}")`,
|
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
const issue = issueResult.value; |
|
|
|
|
|
const issueTitle = escapeMarkdown(issue.title.trim()); |
|
|
|
|
|
if (footnotes != null && !prs?.has(num)) { |
|
|
|
|
|
footnoteIndex = footnotes.size + 1; |
|
|
|
|
|
footnotes.set( |
|
|
|
|
|
footnoteIndex, |
|
|
|
|
|
`[${getIssueOrPullRequestMarkdownIcon( |
|
|
|
|
|
issue, |
|
|
|
|
|
)} **${issueTitle}**](${url}${title})\\\n${GlyphChars.Space.repeat( |
|
|
|
|
|
5, |
|
|
|
|
|
)}${linkText} ${issue.state} ${fromNow( |
|
|
|
|
|
issue.closedDate ?? issue.date, |
|
|
|
|
|
)}`,
|
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} else if (footnotes != null && !prs?.has(num)) { |
|
|
|
|
|
footnoteIndex = footnotes.size + 1; |
|
|
|
|
|
footnotes.set( |
|
|
|
|
|
footnoteIndex, |
|
|
|
|
|
`[${getIssueOrPullRequestMarkdownIcon()} ${ |
|
|
|
|
|
this.name |
|
|
|
|
|
} Merge Request ${repo}!${num}](${url}${title})`,
|
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
return token; |
|
|
return token; |
|
|
}, |
|
|
}, |
|
|
); |
|
|
); |
|
|
}, |
|
|
}, |
|
|
parse: (text: string, autolinks: Map<string, Autolink>) => { |
|
|
parse: (text: string, autolinks: Map<string, Autolink>) => { |
|
|
let repo: string; |
|
|
|
|
|
|
|
|
let ownerAndRepo: string; |
|
|
let num: string; |
|
|
let num: string; |
|
|
|
|
|
|
|
|
let match; |
|
|
let match; |
|
|
do { |
|
|
do { |
|
|
match = autolinkFullMergeRequestsRegex.exec(text); |
|
|
match = autolinkFullMergeRequestsRegex.exec(text); |
|
|
if (match?.groups == null) break; |
|
|
|
|
|
|
|
|
if (match == null) break; |
|
|
|
|
|
|
|
|
({ repo, num } = match.groups); |
|
|
|
|
|
|
|
|
[, ownerAndRepo, num] = match; |
|
|
|
|
|
|
|
|
|
|
|
const [owner, repo] = ownerAndRepo.split('/', 2); |
|
|
autolinks.set(num, { |
|
|
autolinks.set(num, { |
|
|
provider: this, |
|
|
provider: this, |
|
|
id: num, |
|
|
id: num, |
|
|
prefix: `${repo}!`, |
|
|
|
|
|
url: `${this.protocol}://${this.domain}/${repo}/-/merge_requests/${num}`, |
|
|
|
|
|
title: `Open Merge Request !<num> from ${repo} on ${this.name}`, |
|
|
|
|
|
|
|
|
prefix: `${ownerAndRepo}!`, |
|
|
|
|
|
url: `${this.protocol}://${this.domain}/${ownerAndRepo}/-/merge_requests/${num}`, |
|
|
|
|
|
title: `Open Merge Request !<num> from ${ownerAndRepo} on ${this.name}`, |
|
|
|
|
|
|
|
|
type: 'pullrequest', |
|
|
type: 'pullrequest', |
|
|
description: `Merge Request !${num} from ${repo} on ${this.name}`, |
|
|
|
|
|
|
|
|
description: `${this.name} Merge Request !${num} from ${ownerAndRepo}`, |
|
|
|
|
|
|
|
|
|
|
|
descriptor: { owner: owner, name: repo } satisfies GitLabRepositoryDescriptor, |
|
|
}); |
|
|
}); |
|
|
} while (true); |
|
|
} while (true); |
|
|
}, |
|
|
}, |
|
@ -330,8 +428,15 @@ export class GitLabRemote extends RichRemoteProvider { |
|
|
protected override async getProviderIssueOrPullRequest( |
|
|
protected override async getProviderIssueOrPullRequest( |
|
|
{ accessToken }: AuthenticationSession, |
|
|
{ accessToken }: AuthenticationSession, |
|
|
id: string, |
|
|
id: string, |
|
|
|
|
|
descriptor: GitLabRepositoryDescriptor | undefined, |
|
|
): Promise<IssueOrPullRequest | undefined> { |
|
|
): Promise<IssueOrPullRequest | undefined> { |
|
|
const [owner, repo] = this.splitPath(); |
|
|
|
|
|
|
|
|
let owner; |
|
|
|
|
|
let repo; |
|
|
|
|
|
if (descriptor != null) { |
|
|
|
|
|
({ owner, name: repo } = descriptor); |
|
|
|
|
|
} else { |
|
|
|
|
|
[owner, repo] = this.splitPath(); |
|
|
|
|
|
} |
|
|
return (await this.container.gitlab)?.getIssueOrPullRequest(this, accessToken, owner, repo, Number(id), { |
|
|
return (await this.container.gitlab)?.getIssueOrPullRequest(this, accessToken, owner, repo, Number(id), { |
|
|
baseUrl: this.apiBaseUrl, |
|
|
baseUrl: this.apiBaseUrl, |
|
|
}); |
|
|
}); |
|
|