chore: import upstream snapshot with attribution
Basic checks / Compilation, Unit and Integration Tests (push) Has been cancelled
Basic checks / Hygiene and Layering (push) Has been cancelled
Basic checks / Warm up node modules cache (push) Has been cancelled
Check Format and Lint / lint-and-format (push) Has been cancelled
Basic checks / Compilation, Unit and Integration Tests (push) Has been cancelled
Basic checks / Hygiene and Layering (push) Has been cancelled
Basic checks / Warm up node modules cache (push) Has been cancelled
Check Format and Lint / lint-and-format (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AuthenticationSession, authentication, window } from 'vscode';
|
||||
import { Agent, globalAgent } from 'https';
|
||||
import { graphql } from '@octokit/graphql/dist-types/types';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { httpsOverHttp } from 'tunnel';
|
||||
import { URL } from 'url';
|
||||
|
||||
export class AuthenticationError extends Error { }
|
||||
|
||||
function getAgent(url: string | undefined = process.env.HTTPS_PROXY): Agent {
|
||||
if (!url) {
|
||||
return globalAgent;
|
||||
}
|
||||
|
||||
try {
|
||||
const { hostname, port, username, password } = new URL(url);
|
||||
const auth = username && password && `${username}:${password}`;
|
||||
return httpsOverHttp({ proxy: { host: hostname, port, proxyAuth: auth } });
|
||||
} catch (e) {
|
||||
window.showErrorMessage(`HTTPS_PROXY environment variable ignored: ${e.message}`);
|
||||
return globalAgent;
|
||||
}
|
||||
}
|
||||
|
||||
const scopes = ['repo', 'workflow', 'user:email', 'read:user'];
|
||||
|
||||
export async function getSession(): Promise<AuthenticationSession> {
|
||||
return await authentication.getSession('github', scopes, { createIfNone: true });
|
||||
}
|
||||
|
||||
let _octokit: Promise<Octokit> | undefined;
|
||||
|
||||
export function getOctokit(): Promise<Octokit> {
|
||||
if (!_octokit) {
|
||||
_octokit = getSession().then(async session => {
|
||||
const token = session.accessToken;
|
||||
const agent = getAgent();
|
||||
|
||||
const { Octokit } = await import('@octokit/rest');
|
||||
|
||||
return new Octokit({
|
||||
request: { agent },
|
||||
userAgent: 'GitHub VSCode',
|
||||
auth: `token ${token}`
|
||||
});
|
||||
}).then(null, async err => {
|
||||
_octokit = undefined;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
return _octokit;
|
||||
}
|
||||
|
||||
let _octokitGraphql: Promise<graphql> | undefined;
|
||||
|
||||
export async function getOctokitGraphql(): Promise<graphql> {
|
||||
if (!_octokitGraphql) {
|
||||
try {
|
||||
const session = await authentication.getSession('github', scopes, { silent: true });
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError('No GitHub authentication session available.');
|
||||
}
|
||||
|
||||
const token = session.accessToken;
|
||||
const { graphql } = await import('@octokit/graphql');
|
||||
|
||||
return graphql.defaults({
|
||||
headers: {
|
||||
authorization: `token ${token}`
|
||||
},
|
||||
request: {
|
||||
agent: getAgent()
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
_octokitGraphql = undefined;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return _octokitGraphql;
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { authentication, EventEmitter, LogOutputChannel, Memento, Uri, workspace } from 'vscode';
|
||||
import { Repository as GitHubRepository, RepositoryRuleset } from '@octokit/graphql-schema';
|
||||
import { AuthenticationError, getOctokitGraphql } from './auth';
|
||||
import { API, BranchProtection, BranchProtectionProvider, BranchProtectionRule, Repository } from './typings/git';
|
||||
import { DisposableStore, getRepositoryFromUrl } from './util';
|
||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||
|
||||
const REPOSITORY_QUERY = `
|
||||
query repositoryPermissions($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
defaultBranchRef {
|
||||
name
|
||||
},
|
||||
viewerPermission
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const REPOSITORY_RULESETS_QUERY = `
|
||||
query repositoryRulesets($owner: String!, $repo: String!, $cursor: String, $limit: Int = 100) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
rulesets(includeParents: true, first: $limit, after: $cursor) {
|
||||
nodes {
|
||||
name
|
||||
enforcement
|
||||
rules(type: PULL_REQUEST) {
|
||||
totalCount
|
||||
}
|
||||
conditions {
|
||||
refName {
|
||||
include
|
||||
exclude
|
||||
}
|
||||
}
|
||||
target
|
||||
},
|
||||
pageInfo {
|
||||
endCursor,
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export class GithubBranchProtectionProviderManager {
|
||||
|
||||
private readonly disposables = new DisposableStore();
|
||||
private readonly providerDisposables = new DisposableStore();
|
||||
|
||||
private _enabled = false;
|
||||
private set enabled(enabled: boolean) {
|
||||
if (this._enabled === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
for (const repository of this.gitAPI.repositories) {
|
||||
this.providerDisposables.add(this.gitAPI.registerBranchProtectionProvider(repository.rootUri, new GithubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter)));
|
||||
}
|
||||
} else {
|
||||
this.providerDisposables.dispose();
|
||||
}
|
||||
|
||||
this._enabled = enabled;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly gitAPI: API,
|
||||
private readonly globalState: Memento,
|
||||
private readonly logger: LogOutputChannel,
|
||||
private readonly telemetryReporter: TelemetryReporter) {
|
||||
this.disposables.add(this.gitAPI.onDidOpenRepository(repository => {
|
||||
if (this._enabled) {
|
||||
this.providerDisposables.add(gitAPI.registerBranchProtectionProvider(repository.rootUri, new GithubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter)));
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.add(workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('github.branchProtection')) {
|
||||
this.updateEnablement();
|
||||
}
|
||||
}));
|
||||
|
||||
this.updateEnablement();
|
||||
}
|
||||
|
||||
private updateEnablement(): void {
|
||||
const config = workspace.getConfiguration('github', null);
|
||||
this.enabled = config.get<boolean>('branchProtection', true) === true;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.enabled = false;
|
||||
this.disposables.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class GithubBranchProtectionProvider implements BranchProtectionProvider {
|
||||
private readonly _onDidChangeBranchProtection = new EventEmitter<Uri>();
|
||||
onDidChangeBranchProtection = this._onDidChangeBranchProtection.event;
|
||||
|
||||
private branchProtection: BranchProtection[];
|
||||
private readonly globalStateKey = `branchProtection:${this.repository.rootUri.toString()}`;
|
||||
|
||||
constructor(
|
||||
private readonly repository: Repository,
|
||||
private readonly globalState: Memento,
|
||||
private readonly logger: LogOutputChannel,
|
||||
private readonly telemetryReporter: TelemetryReporter) {
|
||||
// Restore branch protection from global state
|
||||
this.branchProtection = this.globalState.get<BranchProtection[]>(this.globalStateKey, []);
|
||||
|
||||
repository.status().then(() => {
|
||||
authentication.onDidChangeSessions(e => {
|
||||
if (e.provider.id === 'github') {
|
||||
this.updateRepositoryBranchProtection();
|
||||
}
|
||||
});
|
||||
this.updateRepositoryBranchProtection();
|
||||
});
|
||||
}
|
||||
|
||||
provideBranchProtection(): BranchProtection[] {
|
||||
return this.branchProtection;
|
||||
}
|
||||
|
||||
private async getRepositoryDetails(owner: string, repo: string): Promise<GitHubRepository> {
|
||||
const graphql = await getOctokitGraphql();
|
||||
const { repository } = await graphql<{ repository: GitHubRepository }>(REPOSITORY_QUERY, { owner, repo });
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
private async getRepositoryRulesets(owner: string, repo: string): Promise<RepositoryRuleset[]> {
|
||||
const rulesets: RepositoryRuleset[] = [];
|
||||
|
||||
let cursor: string | undefined = undefined;
|
||||
const graphql = await getOctokitGraphql();
|
||||
|
||||
while (true) {
|
||||
const { repository } = await graphql<{ repository: GitHubRepository }>(REPOSITORY_RULESETS_QUERY, { owner, repo, cursor });
|
||||
|
||||
rulesets.push(...(repository.rulesets?.nodes ?? [])
|
||||
// Active branch ruleset that contains the pull request required rule
|
||||
.filter(node => node && node.target === 'BRANCH' && node.enforcement === 'ACTIVE' && (node.rules?.totalCount ?? 0) > 0) as RepositoryRuleset[]);
|
||||
|
||||
if (repository.rulesets?.pageInfo.hasNextPage) {
|
||||
cursor = repository.rulesets.pageInfo.endCursor as string | undefined;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return rulesets;
|
||||
}
|
||||
|
||||
private async updateRepositoryBranchProtection(): Promise<void> {
|
||||
const branchProtection: BranchProtection[] = [];
|
||||
|
||||
try {
|
||||
for (const remote of this.repository.state.remotes) {
|
||||
const repository = getRepositoryFromUrl(remote.pushUrl ?? remote.fetchUrl ?? '');
|
||||
|
||||
if (!repository) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Repository details
|
||||
this.logger.trace(`Fetching repository details for "${repository.owner}/${repository.repo}".`);
|
||||
const repositoryDetails = await this.getRepositoryDetails(repository.owner, repository.repo);
|
||||
|
||||
// Check repository write permission
|
||||
if (repositoryDetails.viewerPermission !== 'ADMIN' && repositoryDetails.viewerPermission !== 'MAINTAIN' && repositoryDetails.viewerPermission !== 'WRITE') {
|
||||
this.logger.trace(`Skipping branch protection for "${repository.owner}/${repository.repo}" due to missing repository write permission.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get repository rulesets
|
||||
const branchProtectionRules: BranchProtectionRule[] = [];
|
||||
const repositoryRulesets = await this.getRepositoryRulesets(repository.owner, repository.repo);
|
||||
|
||||
for (const ruleset of repositoryRulesets) {
|
||||
branchProtectionRules.push({
|
||||
include: (ruleset.conditions.refName?.include ?? []).map(r => this.parseRulesetRefName(repositoryDetails, r)),
|
||||
exclude: (ruleset.conditions.refName?.exclude ?? []).map(r => this.parseRulesetRefName(repositoryDetails, r))
|
||||
});
|
||||
}
|
||||
|
||||
branchProtection.push({ remote: remote.name, rules: branchProtectionRules });
|
||||
}
|
||||
|
||||
this.branchProtection = branchProtection;
|
||||
this._onDidChangeBranchProtection.fire(this.repository.rootUri);
|
||||
|
||||
// Save branch protection to global state
|
||||
await this.globalState.update(this.globalStateKey, branchProtection);
|
||||
this.logger.trace(`Branch protection for "${this.repository.rootUri.toString()}": ${JSON.stringify(branchProtection)}.`);
|
||||
|
||||
/* __GDPR__
|
||||
"branchProtection" : {
|
||||
"owner": "lszomoru",
|
||||
"rulesetCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of repository rulesets" }
|
||||
}
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('branchProtection', undefined, { rulesetCount: this.branchProtection.length });
|
||||
} catch (err) {
|
||||
this.logger.warn(`Failed to update repository branch protection: ${err.message}`);
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
// A GitHub authentication session could be missing if the user has not yet
|
||||
// signed in with their GitHub account or they have signed out. If there is
|
||||
// branch protection information we have to clear it.
|
||||
if (this.branchProtection.length !== 0) {
|
||||
this.branchProtection = branchProtection;
|
||||
this._onDidChangeBranchProtection.fire(this.repository.rootUri);
|
||||
|
||||
await this.globalState.update(this.globalStateKey, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseRulesetRefName(repository: GitHubRepository, refName: string): string {
|
||||
if (refName.startsWith('refs/heads/')) {
|
||||
return refName.substring(11);
|
||||
}
|
||||
|
||||
switch (refName) {
|
||||
case '~ALL':
|
||||
return '**/*';
|
||||
case '~DEFAULT_BRANCH':
|
||||
return repository.defaultBranchRef!.name;
|
||||
default:
|
||||
return refName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken, CanonicalUriProvider, CanonicalUriRequestOptions, Disposable, ProviderResult, Uri, workspace } from 'vscode';
|
||||
import { API } from './typings/git';
|
||||
|
||||
const SUPPORTED_SCHEMES = ['ssh', 'https', 'file'];
|
||||
|
||||
export class GitHubCanonicalUriProvider implements CanonicalUriProvider {
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
constructor(private gitApi: API) {
|
||||
this.disposables.push(...SUPPORTED_SCHEMES.map((scheme) => workspace.registerCanonicalUriProvider(scheme, this)));
|
||||
}
|
||||
|
||||
dispose() { this.disposables.forEach((disposable) => disposable.dispose()); }
|
||||
|
||||
provideCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, _token: CancellationToken): ProviderResult<Uri> {
|
||||
if (options.targetScheme !== 'https') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (uri.scheme) {
|
||||
case 'file': {
|
||||
const repository = this.gitApi.getRepository(uri);
|
||||
const remote = repository?.state.remotes.find((remote) => remote.name === repository.state.HEAD?.remote)?.pushUrl?.replace(/^(git@[^\/:]+)(:)/i, 'ssh://$1/');
|
||||
if (remote) {
|
||||
return toHttpsGitHubRemote(uri);
|
||||
}
|
||||
}
|
||||
default:
|
||||
return toHttpsGitHubRemote(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toHttpsGitHubRemote(uri: Uri) {
|
||||
if (uri.scheme === 'ssh' && uri.authority === 'git@github.com') {
|
||||
// if this is a git@github.com URI, return the HTTPS equivalent
|
||||
const [owner, repo] = (uri.path.endsWith('.git') ? uri.path.slice(0, -4) : uri.path).split('/').filter((segment) => segment.length > 0);
|
||||
return Uri.parse(`https://github.com/${owner}/${repo}`);
|
||||
}
|
||||
if (uri.scheme === 'https' && uri.authority === 'github.com') {
|
||||
return uri;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { API as GitAPI } from './typings/git';
|
||||
import { publishRepository } from './publish';
|
||||
import { DisposableStore } from './util';
|
||||
import { LinkContext, getLink, getVscodeDevHost } from './links';
|
||||
|
||||
async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) {
|
||||
try {
|
||||
const permalink = await getLink(gitAPI, useSelection, true, getVscodeDevHost(), 'headlink', context, includeRange);
|
||||
if (permalink) {
|
||||
return vscode.env.clipboard.writeText(permalink);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!(err instanceof vscode.CancellationError)) {
|
||||
vscode.window.showErrorMessage(err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openVscodeDevLink(gitAPI: GitAPI): Promise<vscode.Uri | undefined> {
|
||||
try {
|
||||
const headlink = await getLink(gitAPI, true, false, getVscodeDevHost(), 'headlink');
|
||||
return headlink ? vscode.Uri.parse(headlink) : undefined;
|
||||
} catch (err) {
|
||||
if (!(err instanceof vscode.CancellationError)) {
|
||||
vscode.window.showErrorMessage(err.message);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.publish', async () => {
|
||||
try {
|
||||
publishRepository(gitAPI);
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(err.message);
|
||||
}
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLink', async (context: LinkContext) => {
|
||||
return copyVscodeDevLink(gitAPI, true, context);
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLinkFile', async (context: LinkContext) => {
|
||||
return copyVscodeDevLink(gitAPI, false, context);
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLinkWithoutRange', async (context: LinkContext) => {
|
||||
return copyVscodeDevLink(gitAPI, true, context, false);
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.openOnVscodeDev', async () => {
|
||||
return openVscodeDevLink(gitAPI);
|
||||
}));
|
||||
|
||||
return disposables;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CredentialsProvider, Credentials, API as GitAPI } from './typings/git';
|
||||
import { workspace, Uri, Disposable } from 'vscode';
|
||||
import { getSession } from './auth';
|
||||
|
||||
const EmptyDisposable: Disposable = { dispose() { } };
|
||||
|
||||
class GitHubCredentialProvider implements CredentialsProvider {
|
||||
|
||||
async getCredentials(host: Uri): Promise<Credentials | undefined> {
|
||||
if (!/github\.com/i.test(host.authority)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
return { username: session.account.id, password: session.accessToken };
|
||||
}
|
||||
}
|
||||
|
||||
export class GithubCredentialProviderManager {
|
||||
|
||||
private providerDisposable: Disposable = EmptyDisposable;
|
||||
private readonly disposable: Disposable;
|
||||
|
||||
private _enabled = false;
|
||||
private set enabled(enabled: boolean) {
|
||||
if (this._enabled === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._enabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
this.providerDisposable = this.gitAPI.registerCredentialsProvider(new GitHubCredentialProvider());
|
||||
} else {
|
||||
this.providerDisposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private gitAPI: GitAPI) {
|
||||
this.disposable = workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('github')) {
|
||||
this.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private refresh(): void {
|
||||
const config = workspace.getConfiguration('github', null);
|
||||
const enabled = config.get<boolean>('gitAuthentication', true);
|
||||
this.enabled = !!enabled;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.enabled = false;
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { commands, Disposable, ExtensionContext, extensions, l10n, LogLevel, LogOutputChannel, window } from 'vscode';
|
||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||
import { GithubRemoteSourceProvider } from './remoteSourceProvider';
|
||||
import { API, GitExtension } from './typings/git';
|
||||
import { registerCommands } from './commands';
|
||||
import { GithubCredentialProviderManager } from './credentialProvider';
|
||||
import { DisposableStore, repositoryHasGitHubRemote } from './util';
|
||||
import { GithubPushErrorHandler } from './pushErrorHandler';
|
||||
import { GitBaseExtension } from './typings/git-base';
|
||||
import { GithubRemoteSourcePublisher } from './remoteSourcePublisher';
|
||||
import { GithubBranchProtectionProviderManager } from './branchProtection';
|
||||
import { GitHubCanonicalUriProvider } from './canonicalUriProvider';
|
||||
import { VscodeDevShareProvider } from './shareProviders';
|
||||
|
||||
export function activate(context: ExtensionContext): void {
|
||||
const disposables: Disposable[] = [];
|
||||
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
|
||||
|
||||
const logger = window.createOutputChannel('GitHub', { log: true });
|
||||
disposables.push(logger);
|
||||
|
||||
const onDidChangeLogLevel = (logLevel: LogLevel) => {
|
||||
logger.appendLine(l10n.t('Log level: {0}', LogLevel[logLevel]));
|
||||
};
|
||||
disposables.push(logger.onDidChangeLogLevel(onDidChangeLogLevel));
|
||||
onDidChangeLogLevel(logger.logLevel);
|
||||
|
||||
const { aiKey } = require('../package.json') as { aiKey: string };
|
||||
const telemetryReporter = new TelemetryReporter(aiKey);
|
||||
disposables.push(telemetryReporter);
|
||||
|
||||
disposables.push(initializeGitBaseExtension());
|
||||
disposables.push(initializeGitExtension(context, telemetryReporter, logger));
|
||||
}
|
||||
|
||||
function initializeGitBaseExtension(): Disposable {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
const initialize = () => {
|
||||
try {
|
||||
const gitBaseAPI = gitBaseExtension.getAPI(1);
|
||||
|
||||
disposables.add(gitBaseAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider()));
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Could not initialize GitHub extension');
|
||||
console.warn(err);
|
||||
}
|
||||
};
|
||||
|
||||
const onDidChangeGitBaseExtensionEnablement = (enabled: boolean) => {
|
||||
if (!enabled) {
|
||||
disposables.dispose();
|
||||
} else {
|
||||
initialize();
|
||||
}
|
||||
};
|
||||
|
||||
const gitBaseExtension = extensions.getExtension<GitBaseExtension>('vscode.git-base')!.exports;
|
||||
disposables.add(gitBaseExtension.onDidChangeEnablement(onDidChangeGitBaseExtensionEnablement));
|
||||
onDidChangeGitBaseExtensionEnablement(gitBaseExtension.enabled);
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
function setGitHubContext(gitAPI: API, disposables: DisposableStore) {
|
||||
if (gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
|
||||
commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
|
||||
} else {
|
||||
const openRepoDisposable = gitAPI.onDidOpenRepository(async e => {
|
||||
await e.status();
|
||||
if (repositoryHasGitHubRemote(e)) {
|
||||
commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
|
||||
openRepoDisposable.dispose();
|
||||
}
|
||||
});
|
||||
disposables.add(openRepoDisposable);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeGitExtension(context: ExtensionContext, telemetryReporter: TelemetryReporter, logger: LogOutputChannel): Disposable {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
let gitExtension = extensions.getExtension<GitExtension>('vscode.git');
|
||||
|
||||
const initialize = () => {
|
||||
gitExtension!.activate()
|
||||
.then(extension => {
|
||||
const onDidChangeGitExtensionEnablement = (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
const gitAPI = extension.getAPI(1);
|
||||
|
||||
disposables.add(registerCommands(gitAPI));
|
||||
disposables.add(new GithubCredentialProviderManager(gitAPI));
|
||||
disposables.add(new GithubBranchProtectionProviderManager(gitAPI, context.globalState, logger, telemetryReporter));
|
||||
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler(telemetryReporter)));
|
||||
disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI)));
|
||||
disposables.add(new GitHubCanonicalUriProvider(gitAPI));
|
||||
disposables.add(new VscodeDevShareProvider(gitAPI));
|
||||
setGitHubContext(gitAPI, disposables);
|
||||
|
||||
commands.executeCommand('setContext', 'git-base.gitEnabled', true);
|
||||
} else {
|
||||
disposables.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
disposables.add(extension.onDidChangeEnablement(onDidChangeGitExtensionEnablement));
|
||||
onDidChangeGitExtensionEnablement(extension.enabled);
|
||||
});
|
||||
};
|
||||
|
||||
if (gitExtension) {
|
||||
initialize();
|
||||
} else {
|
||||
const listener = extensions.onDidChange(() => {
|
||||
if (!gitExtension && extensions.getExtension<GitExtension>('vscode.git')) {
|
||||
gitExtension = extensions.getExtension<GitExtension>('vscode.git');
|
||||
initialize();
|
||||
listener.dispose();
|
||||
}
|
||||
});
|
||||
disposables.add(listener);
|
||||
}
|
||||
|
||||
return disposables;
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { API as GitAPI, RefType, Repository } from './typings/git';
|
||||
import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util';
|
||||
|
||||
export function isFileInRepo(repository: Repository, file: vscode.Uri): boolean {
|
||||
return file.path.toLowerCase() === repository.rootUri.path.toLowerCase() ||
|
||||
(file.path.toLowerCase().startsWith(repository.rootUri.path.toLowerCase()) &&
|
||||
file.path.substring(repository.rootUri.path.length).startsWith('/'));
|
||||
}
|
||||
|
||||
export function getRepositoryForFile(gitAPI: GitAPI, file: vscode.Uri): Repository | undefined {
|
||||
for (const repository of gitAPI.repositories) {
|
||||
if (isFileInRepo(repository, file)) {
|
||||
return repository;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
enum LinkType {
|
||||
File = 1,
|
||||
Notebook = 2
|
||||
}
|
||||
|
||||
interface IFilePosition {
|
||||
type: LinkType.File;
|
||||
uri: vscode.Uri;
|
||||
range: vscode.Range | undefined;
|
||||
}
|
||||
|
||||
interface INotebookPosition {
|
||||
type: LinkType.Notebook;
|
||||
uri: vscode.Uri;
|
||||
cellIndex: number;
|
||||
range: vscode.Range | undefined;
|
||||
}
|
||||
|
||||
interface EditorLineNumberContext {
|
||||
uri: vscode.Uri;
|
||||
lineNumber: number;
|
||||
}
|
||||
export type LinkContext = vscode.Uri | EditorLineNumberContext | undefined;
|
||||
|
||||
function extractContext(context: LinkContext): { fileUri: vscode.Uri | undefined; lineNumber: number | undefined } {
|
||||
if (context instanceof vscode.Uri) {
|
||||
return { fileUri: context, lineNumber: undefined };
|
||||
} else if (context !== undefined && 'lineNumber' in context && 'uri' in context) {
|
||||
return { fileUri: context.uri, lineNumber: context.lineNumber };
|
||||
} else {
|
||||
return { fileUri: undefined, lineNumber: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
function getFileAndPosition(context: LinkContext): IFilePosition | INotebookPosition | undefined {
|
||||
let range: vscode.Range | undefined;
|
||||
|
||||
const { fileUri, lineNumber } = extractContext(context);
|
||||
const uri = fileUri ?? vscode.window.activeTextEditor?.document.uri;
|
||||
|
||||
if (uri) {
|
||||
if (uri.scheme === 'vscode-notebook-cell' && vscode.window.activeNotebookEditor?.notebook.uri.fsPath === uri.fsPath) {
|
||||
// if the active editor is a notebook editor and the focus is inside any a cell text editor
|
||||
// generate deep link for text selection for the notebook cell.
|
||||
const cell = vscode.window.activeNotebookEditor.notebook.getCells().find(cell => cell.document.uri.fragment === uri?.fragment);
|
||||
const cellIndex = cell?.index ?? vscode.window.activeNotebookEditor.selection.start;
|
||||
|
||||
const range = getRangeOrSelection(lineNumber);
|
||||
return { type: LinkType.Notebook, uri, cellIndex, range };
|
||||
} else {
|
||||
// the active editor is a text editor
|
||||
range = getRangeOrSelection(lineNumber);
|
||||
return { type: LinkType.File, uri, range };
|
||||
}
|
||||
}
|
||||
|
||||
if (vscode.window.activeNotebookEditor) {
|
||||
// if the active editor is a notebook editor but the focus is not inside any cell text editor, generate deep link for the cell selection in the notebook document.
|
||||
return { type: LinkType.Notebook, uri: vscode.window.activeNotebookEditor.notebook.uri, cellIndex: vscode.window.activeNotebookEditor.selection.start, range: undefined };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getRangeOrSelection(lineNumber: number | undefined) {
|
||||
return lineNumber !== undefined && (!vscode.window.activeTextEditor || vscode.window.activeTextEditor.selection.isEmpty || !vscode.window.activeTextEditor.selection.contains(new vscode.Position(lineNumber - 1, 0)))
|
||||
? new vscode.Range(lineNumber - 1, 0, lineNumber - 1, 1)
|
||||
: vscode.window.activeTextEditor?.selection;
|
||||
}
|
||||
|
||||
export function rangeString(range: vscode.Range | undefined) {
|
||||
if (!range) {
|
||||
return '';
|
||||
}
|
||||
let hash = `#L${range.start.line + 1}`;
|
||||
if (range.start.line !== range.end.line) {
|
||||
hash += `-L${range.end.line + 1}`;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function notebookCellRangeString(index: number | undefined, range: vscode.Range | undefined) {
|
||||
if (index === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!range) {
|
||||
return `#C${index + 1}`;
|
||||
}
|
||||
|
||||
let hash = `#C${index + 1}:L${range.start.line + 1}`;
|
||||
if (range.start.line !== range.end.line) {
|
||||
hash += `-L${range.end.line + 1}`;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function encodeURIComponentExceptSlashes(path: string) {
|
||||
// There may be special characters like # and whitespace in the path.
|
||||
// These characters are not escaped by encodeURI(), so it is not sufficient to
|
||||
// feed the full URI to encodeURI().
|
||||
// Additonally, if we feed the full path into encodeURIComponent(),
|
||||
// this will also encode the path separators, leading to an invalid path.
|
||||
// Therefore, split on the path separator and encode each segment individually.
|
||||
return path.split('/').map((segment) => encodeURIComponent(segment)).join('/');
|
||||
}
|
||||
|
||||
export async function getLink(gitAPI: GitAPI, useSelection: boolean, shouldEnsurePublished: boolean, hostPrefix?: string, linkType: 'permalink' | 'headlink' = 'permalink', context?: LinkContext, useRange?: boolean): Promise<string | undefined> {
|
||||
hostPrefix = hostPrefix ?? 'https://github.com';
|
||||
const fileAndPosition = getFileAndPosition(context);
|
||||
const fileUri = fileAndPosition?.uri;
|
||||
|
||||
// Use the first repo if we cannot determine a repo from the uri.
|
||||
const githubRepository = gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo));
|
||||
const gitRepo = (fileUri ? getRepositoryForFile(gitAPI, fileUri) : githubRepository) ?? githubRepository;
|
||||
if (!gitRepo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldEnsurePublished && fileUri) {
|
||||
await ensurePublished(gitRepo, fileUri);
|
||||
}
|
||||
|
||||
let repo: { owner: string; repo: string } | undefined;
|
||||
gitRepo.state.remotes.find(remote => {
|
||||
if (remote.fetchUrl) {
|
||||
const foundRepo = getRepositoryFromUrl(remote.fetchUrl);
|
||||
if (foundRepo && (remote.name === gitRepo.state.HEAD?.upstream?.remote)) {
|
||||
repo = foundRepo;
|
||||
return;
|
||||
} else if (foundRepo && !repo) {
|
||||
repo = foundRepo;
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
if (!repo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blobSegment = gitRepo.state.HEAD ? (`/blob/${linkType === 'headlink' && gitRepo.state.HEAD.name ? encodeURIComponentExceptSlashes(gitRepo.state.HEAD.name) : gitRepo.state.HEAD?.commit}`) : '';
|
||||
const uriWithoutFileSegments = `${hostPrefix}/${repo.owner}/${repo.repo}${blobSegment}`;
|
||||
if (!fileUri) {
|
||||
return uriWithoutFileSegments;
|
||||
}
|
||||
|
||||
const encodedFilePath = encodeURIComponentExceptSlashes(fileUri.path.substring(gitRepo.rootUri.path.length));
|
||||
const fileSegments = fileAndPosition.type === LinkType.File
|
||||
? (useSelection ? `${encodedFilePath}${useRange ? rangeString(fileAndPosition.range) : ''}` : '')
|
||||
: (useSelection ? `${encodedFilePath}${useRange ? notebookCellRangeString(fileAndPosition.cellIndex, fileAndPosition.range) : ''}` : '');
|
||||
|
||||
return `${uriWithoutFileSegments}${fileSegments}`;
|
||||
}
|
||||
|
||||
export function getBranchLink(url: string, branch: string, hostPrefix: string = 'https://github.com') {
|
||||
const repo = getRepositoryFromUrl(url);
|
||||
if (!repo) {
|
||||
throw new Error('Invalid repository URL provided');
|
||||
}
|
||||
|
||||
branch = encodeURIComponentExceptSlashes(branch);
|
||||
return `${hostPrefix}/${repo.owner}/${repo.repo}/tree/${branch}`;
|
||||
}
|
||||
|
||||
export function getVscodeDevHost(): string {
|
||||
return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`;
|
||||
}
|
||||
|
||||
export async function ensurePublished(repository: Repository, file: vscode.Uri) {
|
||||
await repository.status();
|
||||
|
||||
if ((repository.state.HEAD?.type === RefType.Head || repository.state.HEAD?.type === RefType.Tag)
|
||||
// If HEAD is not published, make sure it is
|
||||
&& !repository?.state.HEAD?.upstream
|
||||
) {
|
||||
const publishBranch = vscode.l10n.t('Publish Branch & Copy Link');
|
||||
const selection = await vscode.window.showInformationMessage(
|
||||
vscode.l10n.t('The current branch is not published to the remote. Would you like to publish your branch before copying a link?'),
|
||||
{ modal: true },
|
||||
publishBranch
|
||||
);
|
||||
if (selection !== publishBranch) {
|
||||
throw new vscode.CancellationError();
|
||||
}
|
||||
|
||||
await vscode.commands.executeCommand('git.publish');
|
||||
}
|
||||
|
||||
const uncommittedChanges = [...repository.state.workingTreeChanges, ...repository.state.indexChanges];
|
||||
if (uncommittedChanges.find((c) => c.uri.toString() === file.toString()) && !repository.state.HEAD?.ahead && !repository.state.HEAD?.behind) {
|
||||
const commitChanges = vscode.l10n.t('Commit Changes');
|
||||
const copyAnyway = vscode.l10n.t('Copy Anyway');
|
||||
const selection = await vscode.window.showWarningMessage(
|
||||
vscode.l10n.t('The current file has uncommitted changes. Please commit your changes before copying a link.'),
|
||||
{ modal: true },
|
||||
commitChanges,
|
||||
copyAnyway
|
||||
);
|
||||
|
||||
if (selection !== copyAnyway) {
|
||||
// Focus the SCM view
|
||||
vscode.commands.executeCommand('workbench.view.scm');
|
||||
throw new vscode.CancellationError();
|
||||
}
|
||||
} else if (repository.state.HEAD?.ahead) {
|
||||
const pushCommits = vscode.l10n.t('Push Commits & Copy Link');
|
||||
const selection = await vscode.window.showInformationMessage(
|
||||
vscode.l10n.t('The current branch has unpublished commits. Would you like to push your commits before copying a link?'),
|
||||
{ modal: true },
|
||||
pushCommits
|
||||
);
|
||||
if (selection !== pushCommits) {
|
||||
throw new vscode.CancellationError();
|
||||
}
|
||||
|
||||
await repository.push();
|
||||
} else if (repository.state.HEAD?.behind) {
|
||||
const pull = vscode.l10n.t('Pull Changes & Copy Link');
|
||||
const selection = await vscode.window.showInformationMessage(
|
||||
vscode.l10n.t('The current branch is not up to date. Would you like to pull before copying a link?'),
|
||||
{ modal: true },
|
||||
pull
|
||||
);
|
||||
if (selection !== pull) {
|
||||
throw new vscode.CancellationError();
|
||||
}
|
||||
|
||||
await repository.pull();
|
||||
}
|
||||
|
||||
await repository.status();
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { API as GitAPI, Repository } from './typings/git';
|
||||
import { getOctokit } from './auth';
|
||||
import { TextEncoder } from 'util';
|
||||
import { basename } from 'path';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { isInCodespaces } from './pushErrorHandler';
|
||||
|
||||
function sanitizeRepositoryName(value: string): string {
|
||||
return value.trim().replace(/[^a-z0-9_.]/ig, '-');
|
||||
}
|
||||
|
||||
function getPick<T extends vscode.QuickPickItem>(quickpick: vscode.QuickPick<T>): Promise<T | undefined> {
|
||||
return Promise.race<T | undefined>([
|
||||
new Promise<T>(c => quickpick.onDidAccept(() => quickpick.selectedItems.length > 0 && c(quickpick.selectedItems[0]))),
|
||||
new Promise<undefined>(c => quickpick.onDidHide(() => c(undefined)))
|
||||
]);
|
||||
}
|
||||
|
||||
export async function publishRepository(gitAPI: GitAPI, repository?: Repository): Promise<void> {
|
||||
if (!vscode.workspace.workspaceFolders?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let folder: vscode.Uri;
|
||||
|
||||
if (repository) {
|
||||
folder = repository.rootUri;
|
||||
} else if (gitAPI.repositories.length === 1) {
|
||||
repository = gitAPI.repositories[0];
|
||||
folder = repository.rootUri;
|
||||
} else if (vscode.workspace.workspaceFolders.length === 1) {
|
||||
folder = vscode.workspace.workspaceFolders[0].uri;
|
||||
} else {
|
||||
const picks = vscode.workspace.workspaceFolders.map(folder => ({ label: folder.name, folder }));
|
||||
const placeHolder = vscode.l10n.t('Pick a folder to publish to GitHub');
|
||||
const pick = await vscode.window.showQuickPick(picks, { placeHolder });
|
||||
|
||||
if (!pick) {
|
||||
return;
|
||||
}
|
||||
|
||||
folder = pick.folder.uri;
|
||||
}
|
||||
|
||||
let quickpick = vscode.window.createQuickPick<vscode.QuickPickItem & { repo?: string; auth?: 'https' | 'ssh'; isPrivate?: boolean }>();
|
||||
quickpick.ignoreFocusOut = true;
|
||||
|
||||
quickpick.placeholder = 'Repository Name';
|
||||
quickpick.value = basename(folder.fsPath);
|
||||
quickpick.show();
|
||||
quickpick.busy = true;
|
||||
|
||||
let owner: string;
|
||||
let octokit: Octokit;
|
||||
try {
|
||||
octokit = await getOctokit();
|
||||
const user = await octokit.users.getAuthenticated({});
|
||||
owner = user.data.login;
|
||||
} catch (e) {
|
||||
// User has cancelled sign in
|
||||
quickpick.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
quickpick.busy = false;
|
||||
|
||||
let repo: string | undefined;
|
||||
let isPrivate: boolean;
|
||||
|
||||
const onDidChangeValue = async () => {
|
||||
const sanitizedRepo = sanitizeRepositoryName(quickpick.value);
|
||||
|
||||
if (!sanitizedRepo) {
|
||||
quickpick.items = [];
|
||||
} else {
|
||||
quickpick.items = [
|
||||
{ label: `$(repo) Publish to GitHub private repository`, description: `$(github) ${owner}/${sanitizedRepo}`, alwaysShow: true, repo: sanitizedRepo, isPrivate: true },
|
||||
{ label: `$(repo) Publish to GitHub public repository`, description: `$(github) ${owner}/${sanitizedRepo}`, alwaysShow: true, repo: sanitizedRepo, isPrivate: false },
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
onDidChangeValue();
|
||||
|
||||
while (true) {
|
||||
const listener = quickpick.onDidChangeValue(onDidChangeValue);
|
||||
const pick = await getPick(quickpick);
|
||||
listener.dispose();
|
||||
|
||||
repo = pick?.repo;
|
||||
isPrivate = pick?.isPrivate ?? true;
|
||||
|
||||
if (repo) {
|
||||
try {
|
||||
quickpick.busy = true;
|
||||
await octokit.repos.get({ owner, repo: repo });
|
||||
quickpick.items = [{ label: `$(error) GitHub repository already exists`, description: `$(github) ${owner}/${repo}`, alwaysShow: true }];
|
||||
} catch {
|
||||
break;
|
||||
} finally {
|
||||
quickpick.busy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quickpick.dispose();
|
||||
|
||||
if (!repo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repository) {
|
||||
const gitignore = vscode.Uri.joinPath(folder, '.gitignore');
|
||||
let shouldGenerateGitignore = false;
|
||||
|
||||
try {
|
||||
await vscode.workspace.fs.stat(gitignore);
|
||||
} catch (err) {
|
||||
shouldGenerateGitignore = true;
|
||||
}
|
||||
|
||||
if (shouldGenerateGitignore) {
|
||||
quickpick = vscode.window.createQuickPick();
|
||||
quickpick.placeholder = vscode.l10n.t('Select which files should be included in the repository.');
|
||||
quickpick.canSelectMany = true;
|
||||
quickpick.show();
|
||||
|
||||
try {
|
||||
quickpick.busy = true;
|
||||
|
||||
const children = (await vscode.workspace.fs.readDirectory(folder))
|
||||
.map(([name]) => name)
|
||||
.filter(name => name !== '.git');
|
||||
|
||||
quickpick.items = children.map(name => ({ label: name }));
|
||||
quickpick.selectedItems = quickpick.items;
|
||||
quickpick.busy = false;
|
||||
|
||||
const result = await Promise.race([
|
||||
new Promise<readonly vscode.QuickPickItem[]>(c => quickpick.onDidAccept(() => c(quickpick.selectedItems))),
|
||||
new Promise<undefined>(c => quickpick.onDidHide(() => c(undefined)))
|
||||
]);
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ignored = new Set(children);
|
||||
result.forEach(c => ignored.delete(c.label));
|
||||
|
||||
if (ignored.size > 0) {
|
||||
const raw = [...ignored].map(i => `/${i}`).join('\n');
|
||||
const encoder = new TextEncoder();
|
||||
await vscode.workspace.fs.writeFile(gitignore, encoder.encode(raw));
|
||||
}
|
||||
} finally {
|
||||
quickpick.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const githubRepository = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: false, title: 'Publish to GitHub' }, async progress => {
|
||||
progress.report({
|
||||
message: isPrivate
|
||||
? vscode.l10n.t('Publishing to a private GitHub repository')
|
||||
: vscode.l10n.t('Publishing to a public GitHub repository'),
|
||||
increment: 25
|
||||
});
|
||||
|
||||
type CreateRepositoryResponseData = Awaited<ReturnType<typeof octokit.repos.createForAuthenticatedUser>>['data'];
|
||||
let createdGithubRepository: CreateRepositoryResponseData | undefined = undefined;
|
||||
|
||||
if (isInCodespaces()) {
|
||||
createdGithubRepository = await vscode.commands.executeCommand<CreateRepositoryResponseData>('github.codespaces.publish', { name: repo!, isPrivate });
|
||||
} else {
|
||||
const res = await octokit.repos.createForAuthenticatedUser({
|
||||
name: repo!,
|
||||
private: isPrivate
|
||||
});
|
||||
createdGithubRepository = res.data;
|
||||
}
|
||||
|
||||
if (createdGithubRepository) {
|
||||
progress.report({ message: vscode.l10n.t('Creating first commit'), increment: 25 });
|
||||
|
||||
if (!repository) {
|
||||
repository = await gitAPI.init(folder, { defaultBranch: createdGithubRepository.default_branch }) || undefined;
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
await repository.commit('first commit', { all: true, postCommitCommand: null });
|
||||
}
|
||||
|
||||
progress.report({ message: vscode.l10n.t('Uploading files'), increment: 25 });
|
||||
|
||||
const branch = await repository.getBranch('HEAD');
|
||||
const protocol = vscode.workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol');
|
||||
const remoteUrl = protocol === 'https' ? createdGithubRepository.clone_url : createdGithubRepository.ssh_url;
|
||||
await repository.addRemote('origin', remoteUrl);
|
||||
await repository.push('origin', branch.name, true);
|
||||
}
|
||||
|
||||
return createdGithubRepository;
|
||||
});
|
||||
|
||||
if (!githubRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
const openOnGitHub = vscode.l10n.t('Open on GitHub');
|
||||
vscode.window.showInformationMessage(vscode.l10n.t('Successfully published the "{0}" repository to GitHub.', `${owner}/${repo}`), openOnGitHub).then(action => {
|
||||
if (action === openOnGitHub) {
|
||||
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(githubRepository.html_url));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TextDecoder } from 'util';
|
||||
import { commands, env, ProgressLocation, Uri, window, workspace, QuickPickOptions, FileType, l10n, Disposable, TextDocumentContentProvider } from 'vscode';
|
||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||
import { getOctokit } from './auth';
|
||||
import { GitErrorCodes, PushErrorHandler, Remote, Repository } from './typings/git';
|
||||
import * as path from 'path';
|
||||
|
||||
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
|
||||
|
||||
export function isInCodespaces(): boolean {
|
||||
return env.remoteName === 'codespaces';
|
||||
}
|
||||
|
||||
const PR_TEMPLATE_FILES = [
|
||||
{ dir: '.', files: ['pull_request_template.md', 'PULL_REQUEST_TEMPLATE.md'] },
|
||||
{ dir: 'docs', files: ['pull_request_template.md', 'PULL_REQUEST_TEMPLATE.md'] },
|
||||
{ dir: '.github', files: ['PULL_REQUEST_TEMPLATE.md', 'PULL_REQUEST_TEMPLATE.md'] }
|
||||
];
|
||||
|
||||
const PR_TEMPLATE_DIRECTORY_NAMES = [
|
||||
'PULL_REQUEST_TEMPLATE',
|
||||
'docs/PULL_REQUEST_TEMPLATE',
|
||||
'.github/PULL_REQUEST_TEMPLATE'
|
||||
];
|
||||
|
||||
async function assertMarkdownFiles(dir: Uri, files: string[]): Promise<Uri[]> {
|
||||
const dirFiles = await workspace.fs.readDirectory(dir);
|
||||
return dirFiles
|
||||
.filter(([name, type]) => Boolean(type & FileType.File) && files.indexOf(name) !== -1)
|
||||
.map(([name]) => Uri.joinPath(dir, name));
|
||||
}
|
||||
|
||||
async function findMarkdownFilesInDir(uri: Uri): Promise<Uri[]> {
|
||||
const files = await workspace.fs.readDirectory(uri);
|
||||
return files
|
||||
.filter(([name, type]) => Boolean(type & FileType.File) && path.extname(name) === '.md')
|
||||
.map(([name]) => Uri.joinPath(uri, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* PR templates can be:
|
||||
* - In the root, `docs`, or `.github` folders, called `pull_request_template.md` or `PULL_REQUEST_TEMPLATE.md`
|
||||
* - Or, in a `PULL_REQUEST_TEMPLATE` directory directly below the root, `docs`, or `.github` folders, called `*.md`
|
||||
*
|
||||
* NOTE This method is a modified copy of a method with same name at microsoft/vscode-pull-request-github repository:
|
||||
* https://github.com/microsoft/vscode-pull-request-github/blob/0a0c3c6c21c0b9c2f4d5ffbc3f8c6a825472e9e6/src/github/folderRepositoryManager.ts#L1061
|
||||
*
|
||||
*/
|
||||
export async function findPullRequestTemplates(repositoryRootUri: Uri): Promise<Uri[]> {
|
||||
const results = await Promise.allSettled([
|
||||
...PR_TEMPLATE_FILES.map(x => assertMarkdownFiles(Uri.joinPath(repositoryRootUri, x.dir), x.files)),
|
||||
...PR_TEMPLATE_DIRECTORY_NAMES.map(x => findMarkdownFilesInDir(Uri.joinPath(repositoryRootUri, x)))
|
||||
]);
|
||||
|
||||
return results.flatMap(x => x.status === 'fulfilled' && x.value || []);
|
||||
}
|
||||
|
||||
export async function pickPullRequestTemplate(repositoryRootUri: Uri, templates: Uri[]): Promise<Uri | undefined> {
|
||||
const quickPickItemFromUri = (x: Uri) => ({ label: path.relative(repositoryRootUri.path, x.path), template: x });
|
||||
const quickPickItems = [
|
||||
{
|
||||
label: l10n.t('No template'),
|
||||
picked: true,
|
||||
template: undefined,
|
||||
},
|
||||
...templates.map(quickPickItemFromUri)
|
||||
];
|
||||
const quickPickOptions: QuickPickOptions = {
|
||||
placeHolder: l10n.t('Select the Pull Request template'),
|
||||
ignoreFocusOut: true
|
||||
};
|
||||
const pickedTemplate = await window.showQuickPick(quickPickItems, quickPickOptions);
|
||||
return pickedTemplate?.template;
|
||||
}
|
||||
|
||||
class CommandErrorOutputTextDocumentContentProvider implements TextDocumentContentProvider {
|
||||
|
||||
private items = new Map<string, string>();
|
||||
|
||||
set(uri: Uri, contents: string): void {
|
||||
this.items.set(uri.path, contents);
|
||||
}
|
||||
|
||||
delete(uri: Uri): void {
|
||||
this.items.delete(uri.path);
|
||||
}
|
||||
|
||||
provideTextDocumentContent(uri: Uri): string | undefined {
|
||||
return this.items.get(uri.path);
|
||||
}
|
||||
}
|
||||
|
||||
export class GithubPushErrorHandler implements PushErrorHandler {
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
private commandErrors = new CommandErrorOutputTextDocumentContentProvider();
|
||||
|
||||
constructor(private readonly telemetryReporter: TelemetryReporter) {
|
||||
this.disposables.push(workspace.registerTextDocumentContentProvider('github-output', this.commandErrors));
|
||||
}
|
||||
|
||||
async handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { stderr: string; gitErrorCode: GitErrorCodes }): Promise<boolean> {
|
||||
if (error.gitErrorCode !== GitErrorCodes.PermissionDenied && error.gitErrorCode !== GitErrorCodes.PushRejected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const remoteUrl = remote.pushUrl || (isInCodespaces() ? remote.fetchUrl : undefined);
|
||||
if (!remoteUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const match = /^(?:https:\/\/github\.com\/|git@github\.com:)([^\/]+)\/([^\/.]+)/i.exec(remoteUrl);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/^:/.test(refspec)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [, owner, repo] = match;
|
||||
|
||||
if (error.gitErrorCode === GitErrorCodes.PermissionDenied) {
|
||||
await this.handlePermissionDeniedError(repository, remote, refspec, owner, repo);
|
||||
|
||||
/* __GDPR__
|
||||
"pushErrorHandler" : {
|
||||
"owner": "lszomoru",
|
||||
"handler": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('pushErrorHandler', { handler: 'PermissionDenied' });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Push protection
|
||||
if (/GH009: Secrets detected!/i.test(error.stderr)) {
|
||||
await this.handlePushProtectionError(owner, repo, error.stderr);
|
||||
|
||||
/* __GDPR__
|
||||
"pushErrorHandler" : {
|
||||
"owner": "lszomoru",
|
||||
"handler": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('pushErrorHandler', { handler: 'PushRejected.PushProtection' });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* __GDPR__
|
||||
"pushErrorHandler" : {
|
||||
"owner": "lszomoru",
|
||||
"handler": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('pushErrorHandler', { handler: 'None' });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async handlePermissionDeniedError(repository: Repository, remote: Remote, refspec: string, owner: string, repo: string): Promise<void> {
|
||||
const yes = l10n.t('Create Fork');
|
||||
const no = l10n.t('No');
|
||||
const askFork = l10n.t('You don\'t have permissions to push to "{0}/{1}" on GitHub. Would you like to create a fork and push to it instead?', owner, repo);
|
||||
|
||||
const answer = await window.showWarningMessage(askFork, { modal: true }, yes, no);
|
||||
if (answer !== yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = /^([^:]*):([^:]*)$/.exec(refspec);
|
||||
const localName = match ? match[1] : refspec;
|
||||
let remoteName = match ? match[2] : refspec;
|
||||
|
||||
const [octokit, ghRepository] = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: l10n.t('Create GitHub fork') }, async progress => {
|
||||
progress.report({ message: l10n.t('Forking "{0}/{1}"...', owner, repo), increment: 33 });
|
||||
|
||||
const octokit = await getOctokit();
|
||||
|
||||
type CreateForkResponseData = Awaited<ReturnType<typeof octokit.repos.createFork>>['data'];
|
||||
|
||||
// Issue: what if the repo already exists?
|
||||
let ghRepository: CreateForkResponseData;
|
||||
try {
|
||||
if (isInCodespaces()) {
|
||||
// Call into the codespaces extension to fork the repository
|
||||
const resp = await commands.executeCommand<{ repository: CreateForkResponseData; ref: string }>('github.codespaces.forkRepository');
|
||||
if (!resp) {
|
||||
throw new Error('Unable to fork respository');
|
||||
}
|
||||
|
||||
ghRepository = resp.repository;
|
||||
|
||||
if (resp.ref) {
|
||||
let ref = resp.ref;
|
||||
if (ref.startsWith('refs/heads/')) {
|
||||
ref = ref.substr(11);
|
||||
}
|
||||
|
||||
remoteName = ref;
|
||||
}
|
||||
} else {
|
||||
const resp = await octokit.repos.createFork({ owner, repo });
|
||||
ghRepository = resp.data;
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
progress.report({ message: l10n.t('Pushing changes...'), increment: 33 });
|
||||
|
||||
// Issue: what if there's already an `upstream` repo?
|
||||
await repository.renameRemote(remote.name, 'upstream');
|
||||
|
||||
// Issue: what if there's already another `origin` repo?
|
||||
const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol');
|
||||
const remoteUrl = protocol === 'https' ? ghRepository.clone_url : ghRepository.ssh_url;
|
||||
await repository.addRemote('origin', remoteUrl);
|
||||
|
||||
try {
|
||||
await repository.fetch('origin', remoteName);
|
||||
await repository.setBranchUpstream(localName, `origin/${remoteName}`);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
await repository.push('origin', localName, true);
|
||||
|
||||
return [octokit, ghRepository] as const;
|
||||
});
|
||||
|
||||
// yield
|
||||
(async () => {
|
||||
const openOnGitHub = l10n.t('Open on GitHub');
|
||||
const createPR = l10n.t('Create PR');
|
||||
const action = await window.showInformationMessage(l10n.t('The fork "{0}" was successfully created on GitHub.', ghRepository.full_name), openOnGitHub, createPR);
|
||||
|
||||
if (action === openOnGitHub) {
|
||||
await commands.executeCommand('vscode.open', Uri.parse(ghRepository.html_url));
|
||||
} else if (action === createPR) {
|
||||
const pr = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: l10n.t('Creating GitHub Pull Request...') }, async _ => {
|
||||
let title = `Update ${remoteName}`;
|
||||
const head = repository.state.HEAD?.name;
|
||||
|
||||
let body: string | undefined;
|
||||
|
||||
if (head) {
|
||||
const commit = await repository.getCommit(head);
|
||||
title = commit.message.split('\n')[0];
|
||||
body = commit.message.slice(title.length + 1).trim();
|
||||
}
|
||||
|
||||
const templates = await findPullRequestTemplates(repository.rootUri);
|
||||
if (templates.length > 0) {
|
||||
templates.sort((a, b) => a.path.localeCompare(b.path));
|
||||
|
||||
const template = await pickPullRequestTemplate(repository.rootUri, templates);
|
||||
|
||||
if (template) {
|
||||
body = new TextDecoder('utf-8').decode(await workspace.fs.readFile(template));
|
||||
}
|
||||
}
|
||||
|
||||
const { data: pr } = await octokit.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
body,
|
||||
head: `${ghRepository.owner.login}:${remoteName}`,
|
||||
base: ghRepository.default_branch
|
||||
});
|
||||
|
||||
await repository.setConfig(`branch.${localName}.remote`, 'upstream');
|
||||
await repository.setConfig(`branch.${localName}.merge`, `refs/heads/${remoteName}`);
|
||||
await repository.setConfig(`branch.${localName}.github-pr-owner-number`, `${owner}#${repo}#${pr.number}`);
|
||||
|
||||
return pr;
|
||||
});
|
||||
|
||||
const openPR = l10n.t('Open PR');
|
||||
const action = await window.showInformationMessage(l10n.t('The PR "{0}/{1}#{2}" was successfully created on GitHub.', owner, repo, pr.number), openPR);
|
||||
|
||||
if (action === openPR) {
|
||||
await commands.executeCommand('vscode.open', Uri.parse(pr.html_url));
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
private async handlePushProtectionError(owner: string, repo: string, stderr: string): Promise<void> {
|
||||
// Open command output in an editor
|
||||
const timestamp = new Date().getTime();
|
||||
const uri = Uri.parse(`github-output:/github-error-${timestamp}`);
|
||||
this.commandErrors.set(uri, stderr);
|
||||
|
||||
try {
|
||||
const doc = await workspace.openTextDocument(uri);
|
||||
await window.showTextDocument(doc);
|
||||
}
|
||||
finally {
|
||||
this.commandErrors.set(uri, stderr);
|
||||
}
|
||||
|
||||
// Show dialog
|
||||
const learnMore = l10n.t('Learn More');
|
||||
const message = l10n.t('Your push to "{0}/{1}" was rejected by GitHub because push protection is enabled and one or more secrets were detected.', owner, repo);
|
||||
const answer = await window.showWarningMessage(message, { modal: true }, learnMore);
|
||||
if (answer === learnMore) {
|
||||
commands.executeCommand('vscode.open', 'https://aka.ms/vscode-github-push-protection');
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Uri, env, l10n, workspace } from 'vscode';
|
||||
import { RemoteSourceProvider, RemoteSource, RemoteSourceAction } from './typings/git-base';
|
||||
import { getOctokit } from './auth';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { getRepositoryFromQuery, getRepositoryFromUrl } from './util';
|
||||
import { getBranchLink, getVscodeDevHost } from './links';
|
||||
|
||||
function asRemoteSource(raw: any): RemoteSource {
|
||||
const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol');
|
||||
return {
|
||||
name: `$(github) ${raw.full_name}`,
|
||||
description: `${raw.stargazers_count > 0 ? `$(star-full) ${raw.stargazers_count}` : ''
|
||||
}`,
|
||||
detail: raw.description || undefined,
|
||||
url: protocol === 'https' ? raw.clone_url : raw.ssh_url
|
||||
};
|
||||
}
|
||||
|
||||
export class GithubRemoteSourceProvider implements RemoteSourceProvider {
|
||||
|
||||
readonly name = 'GitHub';
|
||||
readonly icon = 'github';
|
||||
readonly supportsQuery = true;
|
||||
|
||||
private userReposCache: RemoteSource[] = [];
|
||||
|
||||
async getRemoteSources(query?: string): Promise<RemoteSource[]> {
|
||||
const octokit = await getOctokit();
|
||||
|
||||
if (query) {
|
||||
const repository = getRepositoryFromUrl(query);
|
||||
|
||||
if (repository) {
|
||||
const raw = await octokit.repos.get(repository);
|
||||
return [asRemoteSource(raw.data)];
|
||||
}
|
||||
}
|
||||
|
||||
const all = await Promise.all([
|
||||
this.getQueryRemoteSources(octokit, query),
|
||||
this.getUserRemoteSources(octokit, query),
|
||||
]);
|
||||
|
||||
const map = new Map<string, RemoteSource>();
|
||||
|
||||
for (const group of all) {
|
||||
for (const remoteSource of group) {
|
||||
map.set(remoteSource.name, remoteSource);
|
||||
}
|
||||
}
|
||||
|
||||
return [...map.values()];
|
||||
}
|
||||
|
||||
private async getUserRemoteSources(octokit: Octokit, query?: string): Promise<RemoteSource[]> {
|
||||
if (!query) {
|
||||
const user = await octokit.users.getAuthenticated({});
|
||||
const username = user.data.login;
|
||||
const res = await octokit.repos.listForAuthenticatedUser({ username, sort: 'updated', per_page: 100 });
|
||||
this.userReposCache = res.data.map(asRemoteSource);
|
||||
}
|
||||
|
||||
return this.userReposCache;
|
||||
}
|
||||
|
||||
private async getQueryRemoteSources(octokit: Octokit, query?: string): Promise<RemoteSource[]> {
|
||||
if (!query) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const repository = getRepositoryFromQuery(query);
|
||||
|
||||
if (repository) {
|
||||
query = `user:${repository.owner}+${repository.repo}`;
|
||||
}
|
||||
|
||||
query += ` fork:true`;
|
||||
|
||||
const raw = await octokit.search.repos({ q: query, sort: 'stars' });
|
||||
return raw.data.items.map(asRemoteSource);
|
||||
}
|
||||
|
||||
async getBranches(url: string): Promise<string[]> {
|
||||
const repository = getRepositoryFromUrl(url);
|
||||
|
||||
if (!repository) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const octokit = await getOctokit();
|
||||
|
||||
const branches: string[] = [];
|
||||
let page = 1;
|
||||
|
||||
while (true) {
|
||||
const res = await octokit.repos.listBranches({ ...repository, per_page: 100, page });
|
||||
|
||||
if (res.data.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
branches.push(...res.data.map(b => b.name));
|
||||
page++;
|
||||
}
|
||||
|
||||
const repo = await octokit.repos.get(repository);
|
||||
const defaultBranch = repo.data.default_branch;
|
||||
|
||||
return branches.sort((a, b) => a === defaultBranch ? -1 : b === defaultBranch ? 1 : 0);
|
||||
}
|
||||
|
||||
async getRemoteSourceActions(url: string): Promise<RemoteSourceAction[]> {
|
||||
const repository = getRepositoryFromUrl(url);
|
||||
if (!repository) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{
|
||||
label: l10n.t('Open on GitHub'),
|
||||
icon: 'github',
|
||||
run(branch: string) {
|
||||
const link = getBranchLink(url, branch);
|
||||
env.openExternal(Uri.parse(link));
|
||||
}
|
||||
}, {
|
||||
label: l10n.t('Checkout on vscode.dev'),
|
||||
icon: 'globe',
|
||||
run(branch: string) {
|
||||
const link = getBranchLink(url, branch, getVscodeDevHost());
|
||||
env.openExternal(Uri.parse(link));
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { publishRepository } from './publish';
|
||||
import { API as GitAPI, RemoteSourcePublisher, Repository } from './typings/git';
|
||||
|
||||
export class GithubRemoteSourcePublisher implements RemoteSourcePublisher {
|
||||
readonly name = 'GitHub';
|
||||
readonly icon = 'github';
|
||||
|
||||
constructor(private gitAPI: GitAPI) { }
|
||||
|
||||
publishRepository(repository: Repository): Promise<void> {
|
||||
return publishRepository(this.gitAPI, repository);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { API } from './typings/git';
|
||||
import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util';
|
||||
import { encodeURIComponentExceptSlashes, ensurePublished, getRepositoryForFile, notebookCellRangeString, rangeString } from './links';
|
||||
|
||||
export class VscodeDevShareProvider implements vscode.ShareProvider, vscode.Disposable {
|
||||
readonly id: string = 'copyVscodeDevLink';
|
||||
readonly label: string = vscode.l10n.t('Copy vscode.dev Link');
|
||||
readonly priority: number = 10;
|
||||
|
||||
|
||||
private _hasGitHubRepositories: boolean = false;
|
||||
private set hasGitHubRepositories(value: boolean) {
|
||||
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', value);
|
||||
this._hasGitHubRepositories = value;
|
||||
this.ensureShareProviderRegistration();
|
||||
}
|
||||
|
||||
private shareProviderRegistration: vscode.Disposable | undefined;
|
||||
private disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(private readonly gitAPI: API) {
|
||||
this.initializeGitHubRepoContext();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
|
||||
private initializeGitHubRepoContext() {
|
||||
if (this.gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
|
||||
this.hasGitHubRepositories = true;
|
||||
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
|
||||
} else {
|
||||
this.disposables.push(this.gitAPI.onDidOpenRepository(async e => {
|
||||
await e.status();
|
||||
if (repositoryHasGitHubRemote(e)) {
|
||||
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
|
||||
this.hasGitHubRepositories = true;
|
||||
}
|
||||
}));
|
||||
}
|
||||
this.disposables.push(this.gitAPI.onDidCloseRepository(() => {
|
||||
if (!this.gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
|
||||
this.hasGitHubRepositories = false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private ensureShareProviderRegistration() {
|
||||
if (vscode.env.appHost !== 'codespaces' && !this.shareProviderRegistration && this._hasGitHubRepositories) {
|
||||
const shareProviderRegistration = vscode.window.registerShareProvider({ scheme: 'file' }, this);
|
||||
this.shareProviderRegistration = shareProviderRegistration;
|
||||
this.disposables.push(shareProviderRegistration);
|
||||
} else if (this.shareProviderRegistration && !this._hasGitHubRepositories) {
|
||||
this.shareProviderRegistration.dispose();
|
||||
this.shareProviderRegistration = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async provideShare(item: vscode.ShareableItem, _token: vscode.CancellationToken): Promise<vscode.Uri | undefined> {
|
||||
const repository = getRepositoryForFile(this.gitAPI, item.resourceUri);
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ensurePublished(repository, item.resourceUri);
|
||||
|
||||
let repo: { owner: string; repo: string } | undefined;
|
||||
repository.state.remotes.find(remote => {
|
||||
if (remote.fetchUrl) {
|
||||
const foundRepo = getRepositoryFromUrl(remote.fetchUrl);
|
||||
if (foundRepo && (remote.name === repository.state.HEAD?.upstream?.remote)) {
|
||||
repo = foundRepo;
|
||||
return;
|
||||
} else if (foundRepo && !repo) {
|
||||
repo = foundRepo;
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
|
||||
if (!repo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blobSegment = repository?.state.HEAD?.name ? encodeURIComponentExceptSlashes(repository.state.HEAD?.name) : repository?.state.HEAD?.commit;
|
||||
const filepathSegment = encodeURIComponentExceptSlashes(item.resourceUri.path.substring(repository?.rootUri.path.length));
|
||||
const rangeSegment = getRangeSegment(item);
|
||||
return vscode.Uri.parse(`${this.getVscodeDevHost()}/${repo.owner}/${repo.repo}/blob/${blobSegment}${filepathSegment}${rangeSegment}`);
|
||||
|
||||
}
|
||||
|
||||
private getVscodeDevHost(): string {
|
||||
return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`;
|
||||
}
|
||||
}
|
||||
|
||||
function getRangeSegment(item: vscode.ShareableItem) {
|
||||
if (item.resourceUri.scheme === 'vscode-notebook-cell') {
|
||||
const notebookEditor = vscode.window.visibleNotebookEditors.find(editor => editor.notebook.uri.fsPath === item.resourceUri.fsPath);
|
||||
const cell = notebookEditor?.notebook.getCells().find(cell => cell.document.uri.fragment === item.resourceUri?.fragment);
|
||||
const cellIndex = cell?.index ?? notebookEditor?.selection.start;
|
||||
return notebookCellRangeString(cellIndex, item.selection);
|
||||
}
|
||||
|
||||
return rangeString(item.selection);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { workspace, extensions, Uri, commands } from 'vscode';
|
||||
import { findPullRequestTemplates, pickPullRequestTemplate } from '../pushErrorHandler';
|
||||
|
||||
suite('github smoke test', function () {
|
||||
const cwd = workspace.workspaceFolders![0].uri;
|
||||
|
||||
suiteSetup(async function () {
|
||||
const ext = extensions.getExtension('vscode.github');
|
||||
await ext?.activate();
|
||||
});
|
||||
|
||||
test('should find all templates', async function () {
|
||||
const expectedValuesSorted = [
|
||||
'PULL_REQUEST_TEMPLATE/a.md',
|
||||
'PULL_REQUEST_TEMPLATE/b.md',
|
||||
'docs/PULL_REQUEST_TEMPLATE.md',
|
||||
'docs/PULL_REQUEST_TEMPLATE/a.md',
|
||||
'docs/PULL_REQUEST_TEMPLATE/b.md',
|
||||
'.github/PULL_REQUEST_TEMPLATE.md',
|
||||
'.github/PULL_REQUEST_TEMPLATE/a.md',
|
||||
'.github/PULL_REQUEST_TEMPLATE/b.md',
|
||||
'PULL_REQUEST_TEMPLATE.md'
|
||||
];
|
||||
expectedValuesSorted.sort();
|
||||
|
||||
const uris = await findPullRequestTemplates(cwd);
|
||||
|
||||
const urisSorted = uris.map(x => x.path.slice(cwd.path.length));
|
||||
urisSorted.sort();
|
||||
|
||||
assert.deepStrictEqual(urisSorted, expectedValuesSorted);
|
||||
});
|
||||
|
||||
test('selecting non-default quick-pick item should correspond to a template', async () => {
|
||||
const template0 = Uri.file("some-imaginary-template-0");
|
||||
const template1 = Uri.file("some-imaginary-template-1");
|
||||
const templates = [template0, template1];
|
||||
|
||||
const pick = pickPullRequestTemplate(Uri.file("/"), templates);
|
||||
|
||||
await commands.executeCommand('workbench.action.quickOpenSelectNext');
|
||||
await commands.executeCommand('workbench.action.quickOpenSelectNext');
|
||||
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
|
||||
|
||||
assert.ok(await pick === template0);
|
||||
});
|
||||
|
||||
test('selecting first quick-pick item should return undefined', async () => {
|
||||
const templates = [Uri.file("some-imaginary-file")];
|
||||
|
||||
const pick = pickPullRequestTemplate(Uri.file("/"), templates);
|
||||
|
||||
await commands.executeCommand('workbench.action.quickOpenSelectNext');
|
||||
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
|
||||
|
||||
assert.ok(await pick === undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as testRunner from '../../../../test/integration/electron/testrunner';
|
||||
|
||||
const suite = 'Github Tests';
|
||||
|
||||
const options: import('mocha').MochaOptions = {
|
||||
ui: 'tdd',
|
||||
color: true,
|
||||
timeout: 60000
|
||||
};
|
||||
|
||||
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
|
||||
options.reporter = 'mocha-multi-reporters';
|
||||
options.reporterOptions = {
|
||||
reporterEnabled: 'spec, mocha-junit-reporter',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
testsuitesTitle: `${suite} ${process.platform}`,
|
||||
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
testRunner.configure(options);
|
||||
|
||||
export = testRunner;
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, Event, ProviderResult, Uri } from 'vscode';
|
||||
export { ProviderResult } from 'vscode';
|
||||
|
||||
export interface API {
|
||||
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
|
||||
pickRemoteSource(options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;
|
||||
}
|
||||
|
||||
export interface GitBaseExtension {
|
||||
|
||||
readonly enabled: boolean;
|
||||
readonly onDidChangeEnablement: Event<boolean>;
|
||||
|
||||
/**
|
||||
* Returns a specific API version.
|
||||
*
|
||||
* Throws error if git-base extension is disabled. You can listed to the
|
||||
* [GitBaseExtension.onDidChangeEnablement](#GitBaseExtension.onDidChangeEnablement)
|
||||
* event to know when the extension becomes enabled/disabled.
|
||||
*
|
||||
* @param version Version number.
|
||||
* @returns API instance
|
||||
*/
|
||||
getAPI(version: 1): API;
|
||||
}
|
||||
|
||||
export interface PickRemoteSourceOptions {
|
||||
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
|
||||
readonly urlLabel?: string | ((url: string) => string);
|
||||
readonly providerName?: string;
|
||||
readonly title?: string;
|
||||
readonly placeholder?: string;
|
||||
readonly branch?: boolean; // then result is PickRemoteSourceResult
|
||||
readonly showRecentSources?: boolean;
|
||||
}
|
||||
|
||||
export interface PickRemoteSourceResult {
|
||||
readonly url: string;
|
||||
readonly branch?: string;
|
||||
}
|
||||
|
||||
export interface RemoteSourceAction {
|
||||
readonly label: string;
|
||||
/**
|
||||
* Codicon name
|
||||
*/
|
||||
readonly icon: string;
|
||||
run(branch: string): void;
|
||||
}
|
||||
|
||||
export interface RemoteSource {
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly detail?: string;
|
||||
/**
|
||||
* Codicon name
|
||||
*/
|
||||
readonly icon?: string;
|
||||
readonly url: string | string[];
|
||||
}
|
||||
|
||||
export interface RecentRemoteSource extends RemoteSource {
|
||||
readonly timestamp: number;
|
||||
}
|
||||
|
||||
export interface RemoteSourceProvider {
|
||||
readonly name: string;
|
||||
/**
|
||||
* Codicon name
|
||||
*/
|
||||
readonly icon?: string;
|
||||
readonly label?: string;
|
||||
readonly placeholder?: string;
|
||||
readonly supportsQuery?: boolean;
|
||||
|
||||
getBranches?(url: string): ProviderResult<string[]>;
|
||||
getRemoteSourceActions?(url: string): ProviderResult<RemoteSourceAction[]>;
|
||||
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
|
||||
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
|
||||
}
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Uri, Event, Disposable, ProviderResult, Command } from 'vscode';
|
||||
export { ProviderResult } from 'vscode';
|
||||
|
||||
export interface Git {
|
||||
readonly path: string;
|
||||
}
|
||||
|
||||
export interface InputBox {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const enum ForcePushMode {
|
||||
Force,
|
||||
ForceWithLease
|
||||
}
|
||||
|
||||
export const enum RefType {
|
||||
Head,
|
||||
RemoteHead,
|
||||
Tag
|
||||
}
|
||||
|
||||
export interface Ref {
|
||||
readonly type: RefType;
|
||||
readonly name?: string;
|
||||
readonly commit?: string;
|
||||
readonly remote?: string;
|
||||
}
|
||||
|
||||
export interface UpstreamRef {
|
||||
readonly remote: string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface Branch extends Ref {
|
||||
readonly upstream?: UpstreamRef;
|
||||
readonly ahead?: number;
|
||||
readonly behind?: number;
|
||||
}
|
||||
|
||||
export interface Commit {
|
||||
readonly hash: string;
|
||||
readonly message: string;
|
||||
readonly parents: string[];
|
||||
readonly authorDate?: Date;
|
||||
readonly authorName?: string;
|
||||
readonly authorEmail?: string;
|
||||
readonly commitDate?: Date;
|
||||
}
|
||||
|
||||
export interface Submodule {
|
||||
readonly name: string;
|
||||
readonly path: string;
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
export interface Remote {
|
||||
readonly name: string;
|
||||
readonly fetchUrl?: string;
|
||||
readonly pushUrl?: string;
|
||||
readonly isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const enum Status {
|
||||
INDEX_MODIFIED,
|
||||
INDEX_ADDED,
|
||||
INDEX_DELETED,
|
||||
INDEX_RENAMED,
|
||||
INDEX_COPIED,
|
||||
|
||||
MODIFIED,
|
||||
DELETED,
|
||||
UNTRACKED,
|
||||
IGNORED,
|
||||
INTENT_TO_ADD,
|
||||
INTENT_TO_RENAME,
|
||||
TYPE_CHANGED,
|
||||
|
||||
ADDED_BY_US,
|
||||
ADDED_BY_THEM,
|
||||
DELETED_BY_US,
|
||||
DELETED_BY_THEM,
|
||||
BOTH_ADDED,
|
||||
BOTH_DELETED,
|
||||
BOTH_MODIFIED
|
||||
}
|
||||
|
||||
export interface Change {
|
||||
|
||||
/**
|
||||
* Returns either `originalUri` or `renameUri`, depending
|
||||
* on whether this change is a rename change. When
|
||||
* in doubt always use `uri` over the other two alternatives.
|
||||
*/
|
||||
readonly uri: Uri;
|
||||
readonly originalUri: Uri;
|
||||
readonly renameUri: Uri | undefined;
|
||||
readonly status: Status;
|
||||
}
|
||||
|
||||
export interface RepositoryState {
|
||||
readonly HEAD: Branch | undefined;
|
||||
readonly refs: Ref[];
|
||||
readonly remotes: Remote[];
|
||||
readonly submodules: Submodule[];
|
||||
readonly rebaseCommit: Commit | undefined;
|
||||
|
||||
readonly mergeChanges: Change[];
|
||||
readonly indexChanges: Change[];
|
||||
readonly workingTreeChanges: Change[];
|
||||
|
||||
readonly onDidChange: Event<void>;
|
||||
}
|
||||
|
||||
export interface RepositoryUIState {
|
||||
readonly selected: boolean;
|
||||
readonly onDidChange: Event<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log options.
|
||||
*/
|
||||
export interface LogOptions {
|
||||
/** Max number of log entries to retrieve. If not specified, the default is 32. */
|
||||
readonly maxEntries?: number;
|
||||
readonly path?: string;
|
||||
}
|
||||
|
||||
export interface CommitOptions {
|
||||
all?: boolean | 'tracked';
|
||||
amend?: boolean;
|
||||
signoff?: boolean;
|
||||
signCommit?: boolean;
|
||||
empty?: boolean;
|
||||
noVerify?: boolean;
|
||||
requireUserConfig?: boolean;
|
||||
useEditor?: boolean;
|
||||
verbose?: boolean;
|
||||
/**
|
||||
* string - execute the specified command after the commit operation
|
||||
* undefined - execute the command specified in git.postCommitCommand
|
||||
* after the commit operation
|
||||
* null - do not execute any command after the commit operation
|
||||
*/
|
||||
postCommitCommand?: string | null;
|
||||
}
|
||||
|
||||
export interface FetchOptions {
|
||||
remote?: string;
|
||||
ref?: string;
|
||||
all?: boolean;
|
||||
prune?: boolean;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
export interface InitOptions {
|
||||
defaultBranch?: string;
|
||||
}
|
||||
|
||||
export interface BranchQuery {
|
||||
readonly remote?: boolean;
|
||||
readonly pattern?: string;
|
||||
readonly count?: number;
|
||||
readonly contains?: string;
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
|
||||
readonly rootUri: Uri;
|
||||
readonly inputBox: InputBox;
|
||||
readonly state: RepositoryState;
|
||||
readonly ui: RepositoryUIState;
|
||||
|
||||
getConfigs(): Promise<{ key: string; value: string; }[]>;
|
||||
getConfig(key: string): Promise<string>;
|
||||
setConfig(key: string, value: string): Promise<string>;
|
||||
getGlobalConfig(key: string): Promise<string>;
|
||||
|
||||
getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>;
|
||||
detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>;
|
||||
buffer(ref: string, path: string): Promise<Buffer>;
|
||||
show(ref: string, path: string): Promise<string>;
|
||||
getCommit(ref: string): Promise<Commit>;
|
||||
|
||||
add(paths: string[]): Promise<void>;
|
||||
revert(paths: string[]): Promise<void>;
|
||||
clean(paths: string[]): Promise<void>;
|
||||
|
||||
apply(patch: string, reverse?: boolean): Promise<void>;
|
||||
diff(cached?: boolean): Promise<string>;
|
||||
diffWithHEAD(): Promise<Change[]>;
|
||||
diffWithHEAD(path: string): Promise<string>;
|
||||
diffWith(ref: string): Promise<Change[]>;
|
||||
diffWith(ref: string, path: string): Promise<string>;
|
||||
diffIndexWithHEAD(): Promise<Change[]>;
|
||||
diffIndexWithHEAD(path: string): Promise<string>;
|
||||
diffIndexWith(ref: string): Promise<Change[]>;
|
||||
diffIndexWith(ref: string, path: string): Promise<string>;
|
||||
diffBlobs(object1: string, object2: string): Promise<string>;
|
||||
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
|
||||
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
|
||||
|
||||
hashObject(data: string): Promise<string>;
|
||||
|
||||
createBranch(name: string, checkout: boolean, ref?: string): Promise<void>;
|
||||
deleteBranch(name: string, force?: boolean): Promise<void>;
|
||||
getBranch(name: string): Promise<Branch>;
|
||||
getBranches(query: BranchQuery): Promise<Ref[]>;
|
||||
setBranchUpstream(name: string, upstream: string): Promise<void>;
|
||||
|
||||
getMergeBase(ref1: string, ref2: string): Promise<string>;
|
||||
|
||||
tag(name: string, upstream: string): Promise<void>;
|
||||
deleteTag(name: string): Promise<void>;
|
||||
|
||||
status(): Promise<void>;
|
||||
checkout(treeish: string): Promise<void>;
|
||||
|
||||
addRemote(name: string, url: string): Promise<void>;
|
||||
removeRemote(name: string): Promise<void>;
|
||||
renameRemote(name: string, newName: string): Promise<void>;
|
||||
|
||||
fetch(options?: FetchOptions): Promise<void>;
|
||||
fetch(remote?: string, ref?: string, depth?: number): Promise<void>;
|
||||
pull(unshallow?: boolean): Promise<void>;
|
||||
push(remoteName?: string, branchName?: string, setUpstream?: boolean, force?: ForcePushMode): Promise<void>;
|
||||
|
||||
blame(path: string): Promise<string>;
|
||||
log(options?: LogOptions): Promise<Commit[]>;
|
||||
|
||||
commit(message: string, opts?: CommitOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export interface RemoteSource {
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly url: string | string[];
|
||||
}
|
||||
|
||||
export interface RemoteSourceProvider {
|
||||
readonly name: string;
|
||||
readonly icon?: string; // codicon name
|
||||
readonly supportsQuery?: boolean;
|
||||
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
|
||||
getBranches?(url: string): ProviderResult<string[]>;
|
||||
publishRepository?(repository: Repository): Promise<void>;
|
||||
}
|
||||
|
||||
export interface RemoteSourcePublisher {
|
||||
readonly name: string;
|
||||
readonly icon?: string; // codicon name
|
||||
publishRepository(repository: Repository): Promise<void>;
|
||||
}
|
||||
|
||||
export interface Credentials {
|
||||
readonly username: string;
|
||||
readonly password: string;
|
||||
}
|
||||
|
||||
export interface CredentialsProvider {
|
||||
getCredentials(host: Uri): ProviderResult<Credentials>;
|
||||
}
|
||||
|
||||
export interface PostCommitCommandsProvider {
|
||||
getCommands(repository: Repository): Command[];
|
||||
}
|
||||
|
||||
export interface PushErrorHandler {
|
||||
handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface BranchProtection {
|
||||
readonly remote: string;
|
||||
readonly rules: BranchProtectionRule[];
|
||||
}
|
||||
|
||||
export interface BranchProtectionRule {
|
||||
readonly include?: string[];
|
||||
readonly exclude?: string[];
|
||||
}
|
||||
|
||||
export interface BranchProtectionProvider {
|
||||
onDidChangeBranchProtection: Event<Uri>;
|
||||
provideBranchProtection(): BranchProtection[];
|
||||
}
|
||||
|
||||
export type APIState = 'uninitialized' | 'initialized';
|
||||
|
||||
export interface PublishEvent {
|
||||
repository: Repository;
|
||||
branch?: string;
|
||||
}
|
||||
|
||||
export interface API {
|
||||
readonly state: APIState;
|
||||
readonly onDidChangeState: Event<APIState>;
|
||||
readonly onDidPublish: Event<PublishEvent>;
|
||||
readonly git: Git;
|
||||
readonly repositories: Repository[];
|
||||
readonly onDidOpenRepository: Event<Repository>;
|
||||
readonly onDidCloseRepository: Event<Repository>;
|
||||
|
||||
toGitUri(uri: Uri, ref: string): Uri;
|
||||
getRepository(uri: Uri): Repository | null;
|
||||
init(root: Uri, options?: InitOptions): Promise<Repository | null>;
|
||||
openRepository(root: Uri): Promise<Repository | null>
|
||||
|
||||
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;
|
||||
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
|
||||
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
|
||||
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
|
||||
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
|
||||
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable;
|
||||
}
|
||||
|
||||
export interface GitExtension {
|
||||
|
||||
readonly enabled: boolean;
|
||||
readonly onDidChangeEnablement: Event<boolean>;
|
||||
|
||||
/**
|
||||
* Returns a specific API version.
|
||||
*
|
||||
* Throws error if git extension is disabled. You can listen to the
|
||||
* [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event
|
||||
* to know when the extension becomes enabled/disabled.
|
||||
*
|
||||
* @param version Version number.
|
||||
* @returns API instance
|
||||
*/
|
||||
getAPI(version: 1): API;
|
||||
}
|
||||
|
||||
export const enum GitErrorCodes {
|
||||
BadConfigFile = 'BadConfigFile',
|
||||
AuthenticationFailed = 'AuthenticationFailed',
|
||||
NoUserNameConfigured = 'NoUserNameConfigured',
|
||||
NoUserEmailConfigured = 'NoUserEmailConfigured',
|
||||
NoRemoteRepositorySpecified = 'NoRemoteRepositorySpecified',
|
||||
NotAGitRepository = 'NotAGitRepository',
|
||||
NotAtRepositoryRoot = 'NotAtRepositoryRoot',
|
||||
Conflict = 'Conflict',
|
||||
StashConflict = 'StashConflict',
|
||||
UnmergedChanges = 'UnmergedChanges',
|
||||
PushRejected = 'PushRejected',
|
||||
RemoteConnectionError = 'RemoteConnectionError',
|
||||
DirtyWorkTree = 'DirtyWorkTree',
|
||||
CantOpenResource = 'CantOpenResource',
|
||||
GitNotFound = 'GitNotFound',
|
||||
CantCreatePipe = 'CantCreatePipe',
|
||||
PermissionDenied = 'PermissionDenied',
|
||||
CantAccessRemote = 'CantAccessRemote',
|
||||
RepositoryNotFound = 'RepositoryNotFound',
|
||||
RepositoryIsLocked = 'RepositoryIsLocked',
|
||||
BranchNotFullyMerged = 'BranchNotFullyMerged',
|
||||
NoRemoteReference = 'NoRemoteReference',
|
||||
InvalidBranchName = 'InvalidBranchName',
|
||||
BranchAlreadyExists = 'BranchAlreadyExists',
|
||||
NoLocalChanges = 'NoLocalChanges',
|
||||
NoStashFound = 'NoStashFound',
|
||||
LocalChangesOverwritten = 'LocalChangesOverwritten',
|
||||
NoUpstreamBranch = 'NoUpstreamBranch',
|
||||
IsInSubmodule = 'IsInSubmodule',
|
||||
WrongCase = 'WrongCase',
|
||||
CantLockRef = 'CantLockRef',
|
||||
CantRebaseMultipleBranches = 'CantRebaseMultipleBranches',
|
||||
PatchDoesNotApply = 'PatchDoesNotApply',
|
||||
NoPathFound = 'NoPathFound',
|
||||
UnknownPath = 'UnknownPath',
|
||||
EmptyCommitMessage = 'EmptyCommitMessage'
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
declare module 'tunnel';
|
||||
@@ -0,0 +1,47 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
declare module 'vscode' {
|
||||
|
||||
// https://github.com/microsoft/vscode/issues/180582
|
||||
|
||||
export namespace workspace {
|
||||
/**
|
||||
*
|
||||
* @param scheme The URI scheme that this provider can provide canonical URIs for.
|
||||
* A canonical URI represents the conversion of a resource's alias into a source of truth URI.
|
||||
* Multiple aliases may convert to the same source of truth URI.
|
||||
* @param provider A provider which can convert URIs of scheme @param scheme to
|
||||
* a canonical URI which is stable across machines.
|
||||
*/
|
||||
export function registerCanonicalUriProvider(scheme: string, provider: CanonicalUriProvider): Disposable;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param uri The URI to provide a canonical URI for.
|
||||
* @param token A cancellation token for the request.
|
||||
*/
|
||||
export function getCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult<Uri>;
|
||||
}
|
||||
|
||||
export interface CanonicalUriProvider {
|
||||
/**
|
||||
*
|
||||
* @param uri The URI to provide a canonical URI for.
|
||||
* @param options Options that the provider should honor in the URI it returns.
|
||||
* @param token A cancellation token for the request.
|
||||
* @returns The canonical URI for the requested URI or undefined if no canonical URI can be provided.
|
||||
*/
|
||||
provideCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult<Uri>;
|
||||
}
|
||||
|
||||
export interface CanonicalUriRequestOptions {
|
||||
/**
|
||||
*
|
||||
* The desired scheme of the canonical URI.
|
||||
*/
|
||||
targetScheme: string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// https://github.com/microsoft/vscode/issues/176316
|
||||
|
||||
declare module 'vscode' {
|
||||
export interface TreeItem {
|
||||
shareableItem?: ShareableItem;
|
||||
}
|
||||
|
||||
export interface ShareableItem {
|
||||
resourceUri: Uri;
|
||||
selection?: Range;
|
||||
}
|
||||
|
||||
export interface ShareProvider {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly priority: number;
|
||||
|
||||
provideShare(item: ShareableItem, token: CancellationToken): ProviderResult<Uri>;
|
||||
}
|
||||
|
||||
export namespace window {
|
||||
export function registerShareProvider(selector: DocumentSelector, provider: ShareProvider): Disposable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Repository } from './typings/git';
|
||||
|
||||
export class DisposableStore {
|
||||
|
||||
private disposables = new Set<vscode.Disposable>();
|
||||
|
||||
add(disposable: vscode.Disposable): void {
|
||||
this.disposables.add(disposable);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const disposable of this.disposables) {
|
||||
disposable.dispose();
|
||||
}
|
||||
|
||||
this.disposables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export function getRepositoryFromUrl(url: string): { owner: string; repo: string } | undefined {
|
||||
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(\.git)?$/i.exec(url)
|
||||
|| /^git@github\.com:([^/]+)\/([^/]+?)(\.git)?$/i.exec(url);
|
||||
return match ? { owner: match[1], repo: match[2] } : undefined;
|
||||
}
|
||||
|
||||
export function getRepositoryFromQuery(query: string): { owner: string; repo: string } | undefined {
|
||||
const match = /^([^/]+)\/([^/]+)$/i.exec(query);
|
||||
return match ? { owner: match[1], repo: match[2] } : undefined;
|
||||
}
|
||||
|
||||
export function repositoryHasGitHubRemote(repository: Repository) {
|
||||
return !!repository.state.remotes.find(remote => remote.fetchUrl ? getRepositoryFromUrl(remote.fetchUrl) : undefined);
|
||||
}
|
||||
Reference in New Issue
Block a user