Merge vscode source through release 1.79.2 (#23482)

* log when an editor action doesn't run because of enablement

* notebooks create/dispose editors. this means controllers must be created eagerly (😢) and that notebooks need a custom way of plugging comparision keys for session. works unless creating another session for the same cell of a duplicated editor

* Set offSide to sql lang configuration to true (#183461)

* Fixes #181764 (#183550)

* fix typo

* Always scroll down and focus the input (#183557)

* Fixes #180386 (#183561)

* cli: ensure ordering of rpc server messages (#183558)

* cli: ensure ordering of rpc server messages

Sending lots of messages to a stream would block them around the async
tokio mutex, which is "fair" so doesn't preserve ordering. Instead, use
the write_loop approach I introduced to the server_multiplexer for the
same reason some time ago.

* fix clippy

* update for May endgame

* testing: allow invalidateTestResults to take an array (#183569)

* Document `ShareProvider` API proposal (#183568)

* Document `ShareProvider` API proposal

* Remove mention of VS Code from JSDoc

* Add support for rendering svg and md in welcome message (#183580)

* Remove toggle setting more eagerly (#183584)

* rm message abt macOS

* Change text (#183589)

* Change text

* Accidentally changed the wrong file

* cli: improve output for code tunnel status (#183571)

* testing: allow invalidateTestResults to take an array

* cli: improve output for code tunnel status

Fixes #183570

* [json/css/html] update services (#183595)

* Add experimental setting to enable this dialog

* Fix exporting chat model to JSON before it is initialized (#183597)

* minimum scrolling to reveal the next cell on shift+enter (#183600)

do minimum scrolling to reveal the next cell on Execute cell and select next

* Fixing Jupyter notebook issue 13263 (#183527)

fix for the issue, still need to understand why there is strange focusing

* Tweak proposed API JSDoc (#183590)

* Tweak proposed API JSDoc

* workbench -> workspace

* fix ? operator

* Use active editor and show progress when sharing (#183603)

Use active editor and show progress

* use scroll setting variable correctly

* Schedule welcome widget to show once between typing. (#183606)

* Schedule dialog to show once between typing

* Don't re-render if already displayed once

* Add F10 keybinding for debugger step, even on Web. (#183510)

Fixes #181792.
Previously, for Web the keyboard shortcut was Alt-F10, because it was
believed that F10 could not be bound on browsers. This turned out to be
incorrect, so we make the shortcut consistent (F10) with desktop VSCode
which is also what many other debuggers use.
We keep Alt-F10 on web as a secondary keybinding to keep the experience
some web users may have gotten used to by now.

* Also pass process.env

* Restore missing chat clear commands (#183651)

* chore: update electron@22.5.4 (#183716)

* Show remote indicator in web when remoteAuthority is set (#183728)

* feat: .vuerc as json file (#153017)

Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>

* Delete --compatibility=1.63 code from the server (#183738)

* Copy vscode.dev link to tunnel generates an invalid link when an untitled workspace is open (#183739)

* Recent devcontainer display string corrupted on Get Started page (#183740)

* Improve "next codeblock" navigation (#183744)

* Improve "next codeblock" navigation
Operate on the current focused response, or the last one, and scroll to the selected item

* Normalize command title

* Git - run git status if similarityThreshold changes (#183762)

* fix aria-label issue in kb editor

fixes A11y_GradeB_VSCode_Keyboard shortcut reads words together - Blind: Arrow key navigation to row Find the binding keys and  "when" cell data are read together resulting in a word " CTRL + FeditorFocus  instead of CTRL + F editorFocus" #182490

* Status - fix compact padding (#183768)

* Remove angle brackets from VB brackets (#183782)

Fixes #183359

* Update language config schema with more details about brackets. (#183779)

* fix comment (#183812)

* Support for `Notebook` CodeAction Kind (#183457)

* nb kind support -- wip

* allow notebook codeactions around single cell edit check

* move notebook code action type out of editor

---------

Co-authored-by: rebornix <penn.lv@gmail.com>

* cli: fix connection default being applied (#183827)

* cli: bump to openssl 1.1.1u (#183828)

* Implement "delete" action for chat history (#183609)

* Use desired file name when generating new md pasted file paths (#183861)

Fixes #183851

* Default to filename for markdown new file if empty (#183864)

Fixes #183848

* Fix small typo (#183865)

Fixes #183819

* Noop when moving a symbol into the file it is already in (#183866)

Fixes #183793

* Adjust codeAction validation to account for notebook kind (#183859)

* Make JS/TS `go to configuration` commands work on non-`file:` file systems (#183688)

Make `go to project` commands work on non-`file:` file systems

Fixes #183685

* Can't do regex search after opening notebook (#183884)

Fixes #183858

* Default to current dir for `move to file` select (#183875)

Fixes #183870

`showOpenDialog` seems to ignore `defaultUri` if the file doesn't exist

* Use `<...>` style markdown links when needed (#183876)

Fixes #183849

* Remove check for context keys

* Update xterm package

* Enable updating a chat model without triggering incremental typing (#183894)

* Enable chat "move" commands on empty sessions (#183895)

* Enable chat "move" commands on empty sessions
and also imported sessions

* Fix command name

* Fix some chat keybindings on windows (#183896)

* "Revert File" on inactive editors are ignored (fix #177557) (#183903)

* Empty reason while switching profile (fix #183775) (#183904)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4278 (#183910)

* fix https://github.com/microsoft/vscode/issues/183770 (#183914)

* code --status displays a lot of errors before actual status output (fix #183787) (#183915)

* joh/icy manatee (#183917)

* Use idle value for widget of interactive editor controller

https://github.com/microsoft/vscode/issues/183820

* also make preview editors idle values

https://github.com/microsoft/vscode/issues/183820

* Fix #183777 (#183929)

* Fix #182309 (#183925)

* Tree checkbox item -> items (#183931)

Fixes #183826

* Fixes #183909 (#183940)

* Fix #183837 (#183943)

fix #183837

* Git - fix #183941 (#183944)

* Update xterm.css

Fixes #181242

* chore: add @ulugbekna and @aiday-mar to my-endgame notebook (#183946)

* Revert "When snippet mode is active, make `Tab` not accept suggestion but advance placeholder"

This reverts commit 50a80cdb61511343996ff1d41d0b676c3d329f48.

* revert not focusing completion list when quick suggest happens during snippet

* change `snippetsPreventQuickSuggestions` default to false

* Fix #181446 (#183956)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4298 (#183957)

* fix: remove extraneous incorrect context keys (#183959)

These were actually getting added in getTestItemContextOverlay, and the test ID was using the extended ID which extensions do not know about.

Fixes #183612

* Fixes https://github.com/microsoft/monaco-editor/issues/3920 (#183960)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4324 (#183961)

* fix #183030

* fix #180826 (#183962)

* make message more generic for interactive editor help

* .

* fix #183968

* Keep codeblock toolbar visible when focused

* Fix when clause on "Run in terminal" command

* add important info to help menu

* fix #183970

* Set `isRefactoring` for all TS refactoring edits (#183982)

* consolidate

* Disable move to file in TS versions < 5.2 (#183992)

There are still a few key bugs with refactoring. We will  ship this as a preview for TS 5.2+ instead of for 5.1

* Polish query accepting (#183995)

We shouldn't send the same request to Copilot if the query hasn't changed. So if the query is the same, we short circut.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4286

Also, when we open in chat, we should use the last accepted query, not what's in the input box.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4280

* Allow widget to have focus (#184000)

So that selecting non-code text works.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4294

* Fix microsoft/vscode-internalbacklog#4257. Mitigate zindex for zone widgets. (#184001)

* Change welcome dialog contribution to Eventually

* Misc fixes

* Workspace folder picker entry descriptions are suboptimal for some filesystems (fix #183418) (#184018)

* cli - ignore std error unless verbose (#183787) (#184031)

* joh/inquisitive meerkat (#184034)

* only stash sessions that are none empty

https://github.com/microsoft/vscode-internalbacklog/issues/4281

* only unstash a session once - unless new exchanges are made,

https://github.com/microsoft/vscode-internalbacklog/issues/4281

* account for all exchange types

* Improve declared components (#184039)

* make sure to read setting (#184040)

d'oh, related to https://github.com/microsoft/vscode/issues/173387#issuecomment-1571696644

* [html] update service (#184049)

[html] update service. FIxes #181176

* reset context keys on reset/hide (#184042)

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4330

* use `Lazy`, not `IdleValue` for the IE widget held by the eager controller (#184048)

https://github.com/microsoft/vscode/issues/183820

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4333 (#184067)

* use undo-loop instead of undo-edit when discarding chat session (#184063)

* use undo-loop instead of undo-edit when discarding chat session

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4118

* fix tests, wait for correct state

* Add logging to node download (#184070)

Add logging to node download. For #182951

* re-enable default zone widget revealing when showing (#184072)

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4332, also fixes https://github.com/microsoft/vscode-internalbacklog/issues/3784

* fix #178202

* Allow APIs in stable (#184062)

* Fix microsoft/vscode-internalbacklog#4206. Override List view whitespace css for monaco editor (#184087)

* Fix JSDoc grammatical error (#184090)

* Pick up TS 5.1.3 (#184091)

Fixes #182931

* Misc fixes

* update distro (#184097)

* chore: update electron@22.5.5 (#184116)

* Extension host veto is registered multiple times on restart (fix #183778) (#184127)

Extension host veto is registered multiple times on restart (#183778)

* Do not auto start the local web worker extension host (#184137)

* Allow embedders to intercept trustedTypes.createPolicy calls (#184136)

Allow embedders to intercept trustedTypes.createPolicy calls (#184100)

* fix: reading from console output for --status on windows and linux (#184138)

fix: reading from console output for --status on windows and linux (#184118)

* Misc fixes

* code --status displays a lot of errors before actual status output (fix #183787) (#184200)

fix 183787

* (cherry-pick to 1.79 from main) Handle galleryExtension failure in featuredExtensionService (#184205)

Handle galleryExtension failure in featuredExtensionService (#184198)

Handle galleryExtension failure

* Fix #184183. Multiple output height updates are skipped. (#184188)

* Post merge init fixes

* Misc build issues

* disable toggle inline diff of `alt` down

https://github.com/microsoft/vscode-internalbacklog/issues/4342

* Take into account already activated extensions when computing running locations (#184303)

Take into account already activated extensions when computing running locations (fixes #184180)

* Avoid `extensionService.getExtension` and use `ActivationKind.Immediate` to allow that URI handling works while resolving (#184310)

Avoid `extensionService.getExtension` and use `ActivationKind.Immediate` to allow that URI handling works while resolving (fixes #182217)

* WIP

* rm fish auto injection

* More breaks

* Fix Port Attributes constructor (#184412)

* WIP

* WIP

* Allow extensions to get at the exports of other extensions during resolving (#184487)

Allow extensions to get at the exports of other extensions during resolving (fixes #184472)

* do not auto finish session when inline chat widgets have focus

re https://github.com/microsoft/vscode-internalbacklog/issues/4354

* fix compile errors caused by new base method

* WIP

* WIP

* WIP

* WIP

* Build errors

* unc - fix path traversal bypass

* Bump version

* cherry-pick prod changes from main

* Disable sandbox

* Build break from merge

* bump version

* Merge pull request #184739 from max06/max06/issue184659

Restore ShellIntegration for fish (#184659)

* Git - only add --find-renames if the value is not the default one (#185053)

Git - only add --find-renames if the value is not the default one (#184992)

* Cherry-pick: Revert changes to render featured extensions when available (#184747)

Revert changes to render featured extensions when available.  (#184573)

* Lower timeouts for experimentation and gallery service

* Revert changes to render extensions when available

* Add audio cues

* fix: disable app sandbox when --no-sandbox is present (#184913)

* fix: disable app sandbox when --no-sandbox is present (#184897)

* fix: loading minimist in packaged builds

* Runtime errors

* UNC allow list checks cannot be disabled in extension host (fix #184989) (#185085)

* UNC allow list checks cannot be disabled in extension host (#184989)

* Update src/vs/base/node/unc.js

Co-authored-by: Robo <hop2deep@gmail.com>

---------

Co-authored-by: Robo <hop2deep@gmail.com>

* Add notebook extension

* Fix mangling issues

* Fix mangling issues

* npm install

* npm install

* Issues blocking bundle

* Fix build folder compile errors

* Fix windows bundle build

* Linting fixes

* Fix sqllint issues

* Update yarn.lock files

* Fix unit tests

* Fix a couple breaks from test fixes

* Bump distro

* redo the checkbox style

* Update linux build container dockerfile

* Bump build image tag

* Bump native watch dog package

* Bump node-pty

* Bump distro

* Fix documnetation error

* Update distro

* redo the button styles

* Update datasource TS

* Add missing yarn.lock files

* Windows setup fix

* Turn off extension unit tests while investigating

* color box style

* Remove appx

* Turn off test log upload

* update dropdownlist style

* fix universal app build error (#23488)

* Skip flaky bufferContext vscode test

---------

Co-authored-by: Johannes <johannes.rieken@gmail.com>
Co-authored-by: Henning Dieterichs <hdieterichs@microsoft.com>
Co-authored-by: Julien Richard <jairbubbles@hotmail.com>
Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
Co-authored-by: Megan Rogge <merogge@microsoft.com>
Co-authored-by: meganrogge <megan.rogge@microsoft.com>
Co-authored-by: Rob Lourens <roblourens@gmail.com>
Co-authored-by: Connor Peet <connor@peet.io>
Co-authored-by: Joyce Er <joyce.er@microsoft.com>
Co-authored-by: Bhavya U <bhavyau@microsoft.com>
Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com>
Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>
Co-authored-by: Aaron Munger <aamunger@microsoft.com>
Co-authored-by: Aiday Marlen Kyzy <amarlenkyzy@microsoft.com>
Co-authored-by: rebornix <penn.lv@gmail.com>
Co-authored-by: Ole <oler@google.com>
Co-authored-by: Jean Pierre <jeanp413@hotmail.com>
Co-authored-by: Robo <hop2deep@gmail.com>
Co-authored-by: Yash Singh <saiansh2525@gmail.com>
Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com>
Co-authored-by: Ulugbek Abdullaev <ulugbekna@gmail.com>
Co-authored-by: Alex Ross <alros@microsoft.com>
Co-authored-by: Michael Lively <milively@microsoft.com>
Co-authored-by: Matt Bierner <matb@microsoft.com>
Co-authored-by: Andrea Mah <31675041+andreamah@users.noreply.github.com>
Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
Co-authored-by: Sandeep Somavarapu <sasomava@microsoft.com>
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Co-authored-by: Tyler James Leonhardt <me@tylerleonhardt.com>
Co-authored-by: Alexandru Dima <alexdima@microsoft.com>
Co-authored-by: Joao Moreno <Joao.Moreno@microsoft.com>
Co-authored-by: Alan Ren <alanren@microsoft.com>
This commit is contained in:
Karl Burtram
2023-06-27 15:26:51 -07:00
committed by GitHub
parent 7975fda6dd
commit 01e66ab3e6
4335 changed files with 252586 additions and 164604 deletions

View File

@@ -5,10 +5,13 @@
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;
@@ -53,3 +56,34 @@ export function getOctokit(): Promise<Octokit> {
return _octokit;
}
let _octokitGraphql: Promise<graphql> | undefined;
export async function getOctokitGraphql(silent = false): Promise<graphql> {
if (!_octokitGraphql) {
try {
const session = await authentication.getSession('github', scopes, { silent });
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;
}

View File

@@ -0,0 +1,231 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. 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';
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)));
}
} else {
this.providerDisposables.dispose();
}
this._enabled = enabled;
}
constructor(
private readonly gitAPI: API,
private readonly globalState: Memento,
private readonly logger: LogOutputChannel) {
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.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) {
// 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(true);
}
});
this.updateRepositoryBranchProtection();
});
}
provideBranchProtection(): BranchProtection[] {
return this.branchProtection;
}
private async getRepositoryDetails(owner: string, repo: string, silent: boolean): Promise<GitHubRepository> {
const graphql = await getOctokitGraphql(silent);
const { repository } = await graphql<{ repository: GitHubRepository }>(REPOSITORY_QUERY, { owner, repo });
return repository;
}
private async getRepositoryRulesets(owner: string, repo: string, silent: boolean): Promise<RepositoryRuleset[]> {
const rulesets: RepositoryRuleset[] = [];
let cursor: string | undefined = undefined;
const graphql = await getOctokitGraphql(silent);
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(silent = false): 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, silent);
// 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, silent);
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)}.`);
} 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. In this case
// we have to clear the branch protection information.
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;
}
}
}

View File

@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. 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;
}

View File

@@ -7,15 +7,11 @@ import * as vscode from 'vscode';
import { API as GitAPI } from './typings/git';
import { publishRepository } from './publish';
import { DisposableStore } from './util';
import { getPermalink } from './links';
import { LinkContext, getLink, getVscodeDevHost } from './links';
function getVscodeDevHost(): string {
return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`;
}
async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean) {
async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) {
try {
const permalink = getPermalink(gitAPI, useSelection, getVscodeDevHost());
const permalink = getLink(gitAPI, useSelection, getVscodeDevHost(), 'headlink', context, includeRange);
if (permalink) {
return vscode.env.clipboard.writeText(permalink);
}
@@ -26,8 +22,8 @@ async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean) {
async function openVscodeDevLink(gitAPI: GitAPI): Promise<vscode.Uri | undefined> {
try {
const permalink = getPermalink(gitAPI, true, getVscodeDevHost());
return permalink ? vscode.Uri.parse(permalink) : undefined;
const headlink = getLink(gitAPI, true, getVscodeDevHost(), 'headlink');
return headlink ? vscode.Uri.parse(headlink) : undefined;
} catch (err) {
vscode.window.showErrorMessage(err.message);
return undefined;
@@ -45,12 +41,16 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
}
}));
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLink', async () => {
return copyVscodeDevLink(gitAPI, true);
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLink', async (context: LinkContext) => {
return copyVscodeDevLink(gitAPI, true, context);
}));
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLinkFile', async () => {
return copyVscodeDevLink(gitAPI, false);
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 () => {

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { commands, Disposable, ExtensionContext, extensions } from 'vscode';
import { commands, Disposable, ExtensionContext, extensions, l10n, LogLevel, LogOutputChannel, window } from 'vscode';
import { GithubRemoteSourceProvider } from './remoteSourceProvider';
import { API, GitExtension } from './typings/git';
import { registerCommands } from './commands';
@@ -12,10 +12,25 @@ 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 {
context.subscriptions.push(initializeGitBaseExtension());
context.subscriptions.push(initializeGitExtension());
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);
disposables.push(initializeGitBaseExtension());
disposables.push(initializeGitExtension(context, logger));
}
function initializeGitBaseExtension(): Disposable {
@@ -63,7 +78,7 @@ function setGitHubContext(gitAPI: API, disposables: DisposableStore) {
}
}
function initializeGitExtension(): Disposable {
function initializeGitExtension(context: ExtensionContext, logger: LogOutputChannel): Disposable {
const disposables = new DisposableStore();
let gitExtension = extensions.getExtension<GitExtension>('vscode.git');
@@ -77,8 +92,11 @@ function initializeGitExtension(): Disposable {
disposables.add(registerCommands(gitAPI));
disposables.add(new GithubCredentialProviderManager(gitAPI));
disposables.add(new GithubBranchProtectionProviderManager(gitAPI, context.globalState, logger));
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler()));
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);

View File

@@ -40,22 +40,40 @@ interface INotebookPosition {
range: vscode.Range | undefined;
}
function getFileAndPosition(): IFilePosition | INotebookPosition | undefined {
let uri: vscode.Uri | undefined;
let range: vscode.Range | undefined;
if (vscode.window.activeTextEditor) {
uri = vscode.window.activeTextEditor.document.uri;
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 = cell !== undefined ? vscode.window.activeTextEditor.selection : undefined;
const range = getRangeOrSelection(lineNumber);
return { type: LinkType.Notebook, uri, cellIndex, range };
} else {
// the active editor is a text editor
range = vscode.window.activeTextEditor.selection;
range = getRangeOrSelection(lineNumber);
return { type: LinkType.File, uri, range };
}
}
@@ -68,7 +86,13 @@ function getFileAndPosition(): IFilePosition | INotebookPosition | undefined {
return undefined;
}
function rangeString(range: vscode.Range | 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 '';
}
@@ -95,9 +119,19 @@ export function notebookCellRangeString(index: number | undefined, range: vscode
return hash;
}
export function getPermalink(gitAPI: GitAPI, useSelection: boolean, hostPrefix?: string): string | undefined {
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 function getLink(gitAPI: GitAPI, useSelection: boolean, hostPrefix?: string, linkType: 'permalink' | 'headlink' = 'permalink', context?: LinkContext, useRange?: boolean): string | undefined {
hostPrefix = hostPrefix ?? 'https://github.com';
const fileAndPosition = getFileAndPosition();
const fileAndPosition = getFileAndPosition(context);
if (!fileAndPosition) {
return;
}
@@ -125,11 +159,26 @@ export function getPermalink(gitAPI: GitAPI, useSelection: boolean, hostPrefix?:
return;
}
const commitHash = (gitRepo.state.HEAD?.ahead === 0) ? `/blob/${gitRepo.state.HEAD?.commit}` : '';
const blobSegment = gitRepo.state.HEAD ? (`/blob/${linkType === 'headlink' && gitRepo.state.HEAD.name ? encodeURIComponentExceptSlashes(gitRepo.state.HEAD.name) : gitRepo.state.HEAD?.commit}`) : '';
const encodedFilePath = encodeURIComponentExceptSlashes(uri.path.substring(gitRepo.rootUri.path.length));
const fileSegments = fileAndPosition.type === LinkType.File
? (useSelection ? `${uri.path.substring(gitRepo.rootUri.path.length)}${rangeString(fileAndPosition.range)}` : '')
: (useSelection ? `${uri.path.substring(gitRepo.rootUri.path.length)}${notebookCellRangeString(fileAndPosition.cellIndex, fileAndPosition.range)}` : '');
? (useSelection ? `${encodedFilePath}${useRange ? rangeString(fileAndPosition.range) : ''}` : '')
: (useSelection ? `${encodedFilePath}${useRange ? notebookCellRangeString(fileAndPosition.cellIndex, fileAndPosition.range) : ''}` : '');
return `${hostPrefix}/${repo.owner}/${repo.repo}${commitHash
return `${hostPrefix}/${repo.owner}/${repo.repo}${blobSegment
}${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`;
}

View File

@@ -4,14 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
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';
const localize = nls.loadMessageBundle();
import { isInCodespaces } from './pushErrorHandler';
function sanitizeRepositoryName(value: string): string {
return value.trim().replace(/[^a-z0-9_.]/ig, '-');
@@ -40,7 +38,7 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
folder = vscode.workspace.workspaceFolders[0].uri;
} else {
const picks = vscode.workspace.workspaceFolders.map(folder => ({ label: folder.name, folder }));
const placeHolder = localize('pick folder', "Pick a folder to publish to GitHub");
const placeHolder = vscode.l10n.t('Pick a folder to publish to GitHub');
const pick = await vscode.window.showQuickPick(picks, { placeHolder });
if (!pick) {
@@ -129,7 +127,7 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
if (shouldGenerateGitignore) {
quickpick = vscode.window.createQuickPick();
quickpick.placeholder = localize('ignore', "Select which files should be included in the repository.");
quickpick.placeholder = vscode.l10n.t('Select which files should be included in the repository.');
quickpick.canSelectMany = true;
quickpick.show();
@@ -170,37 +168,45 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
const githubRepository = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: false, title: 'Publish to GitHub' }, async progress => {
progress.report({
message: isPrivate
? localize('publishing_private', "Publishing to a private GitHub repository")
: localize('publishing_public', "Publishing to a public GitHub repository"),
? vscode.l10n.t('Publishing to a private GitHub repository')
: vscode.l10n.t('Publishing to a public GitHub repository'),
increment: 25
});
const res = await octokit.repos.createForAuthenticatedUser({
name: repo!,
private: isPrivate
});
type CreateRepositoryResponseData = Awaited<ReturnType<typeof octokit.repos.createForAuthenticatedUser>>['data'];
let createdGithubRepository: CreateRepositoryResponseData | undefined = undefined;
const createdGithubRepository = res.data;
progress.report({ message: localize('publishing_firstcommit', "Creating first commit"), increment: 25 });
if (!repository) {
repository = await gitAPI.init(folder) || undefined;
if (!repository) {
return;
}
await repository.commit('first commit', { all: true });
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;
}
progress.report({ message: localize('publishing_uploading', "Uploading files"), increment: 25 });
if (createdGithubRepository) {
progress.report({ message: vscode.l10n.t('Creating first commit'), 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);
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;
});
@@ -209,8 +215,8 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
return;
}
const openOnGitHub = localize('openingithub', "Open on GitHub");
vscode.window.showInformationMessage(localize('publishing_done', "Successfully published the '{0}' repository to GitHub.", `${owner}/${repo}`), openOnGitHub).then(action => {
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));
}

View File

@@ -4,13 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { TextDecoder } from 'util';
import { commands, env, ProgressLocation, Uri, window, workspace, QuickPickOptions, FileType } from 'vscode';
import * as nls from 'vscode-nls';
import { commands, env, ProgressLocation, Uri, window, workspace, QuickPickOptions, FileType, l10n } from 'vscode';
import { getOctokit } from './auth';
import { GitErrorCodes, PushErrorHandler, Remote, Repository } from './typings/git';
import path = require('path');
const localize = nls.loadMessageBundle();
import * as path from 'path';
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
@@ -19,11 +16,11 @@ export function isInCodespaces(): boolean {
}
async function handlePushError(repository: Repository, remote: Remote, refspec: string, owner: string, repo: string): Promise<void> {
const yes = localize('create a fork', "Create Fork");
const no = localize('no', "No");
const askFork = localize('fork', "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 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.showInformationMessage(askFork, yes, no);
const answer = await window.showWarningMessage(askFork, { modal: true }, yes, no);
if (answer !== yes) {
return;
}
@@ -32,8 +29,8 @@ async function handlePushError(repository: Repository, remote: Remote, 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: localize('create fork', 'Create GitHub fork') }, async progress => {
progress.report({ message: localize('forking', "Forking '{0}/{1}'...", owner, repo), increment: 33 });
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();
@@ -68,7 +65,7 @@ async function handlePushError(repository: Repository, remote: Remote, refspec:
throw ex;
}
progress.report({ message: localize('forking_pushing', "Pushing changes..."), increment: 33 });
progress.report({ message: l10n.t('Pushing changes...'), increment: 33 });
// Issue: what if there's already an `upstream` repo?
await repository.renameRemote(remote.name, 'upstream');
@@ -92,14 +89,14 @@ async function handlePushError(repository: Repository, remote: Remote, refspec:
// yield
(async () => {
const openOnGitHub = localize('openingithub', "Open on GitHub");
const createPR = localize('createpr', "Create PR");
const action = await window.showInformationMessage(localize('forking_done', "The fork '{0}' was successfully created on GitHub.", ghRepository.full_name), openOnGitHub, createPR);
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: localize('createghpr', "Creating GitHub Pull Request...") }, async _ => {
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;
@@ -138,8 +135,8 @@ async function handlePushError(repository: Repository, remote: Remote, refspec:
return pr;
});
const openPR = localize('openpr', "Open PR");
const action = await window.showInformationMessage(localize('donepr', "The PR '{0}/{1}#{2}' was successfully created on GitHub.", owner, repo, pr.number), openPR);
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));
@@ -196,14 +193,14 @@ export async function pickPullRequestTemplate(repositoryRootUri: Uri, templates:
const quickPickItemFromUri = (x: Uri) => ({ label: path.relative(repositoryRootUri.path, x.path), template: x });
const quickPickItems = [
{
label: localize('no pr template', "No template"),
label: l10n.t('No template'),
picked: true,
template: undefined,
},
...templates.map(quickPickItemFromUri)
];
const quickPickOptions: QuickPickOptions = {
placeHolder: localize('select pr template', "Select the Pull Request template"),
placeHolder: l10n.t('Select the Pull Request template'),
ignoreFocusOut: true
};
const pickedTemplate = await window.showQuickPick(quickPickItems, quickPickOptions);

View File

@@ -3,11 +3,12 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace } from 'vscode';
import { RemoteSourceProvider, RemoteSource } from './typings/git-base';
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');
@@ -112,4 +113,27 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider {
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));
}
}];
}
}

View File

@@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. 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, 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;
}
}
provideShare(item: vscode.ShareableItem, _token: vscode.CancellationToken): vscode.ProviderResult<vscode.Uri> {
const repository = getRepositoryForFile(this.gitAPI, item.resourceUri);
if (!repository) {
return;
}
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}${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);
}

View File

@@ -3,12 +3,12 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const testRunner = require('../../../../test/integration/electron/testrunner');
import * as path from 'path';
import * as testRunner from '../../../../test/integration/electron/testrunner';
const suite = 'Github Tests';
const options: any = {
const options: import('mocha').MochaOptions = {
ui: 'tdd',
color: true,
timeout: 60000

View File

@@ -44,6 +44,15 @@ export interface PickRemoteSourceResult {
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;
@@ -70,6 +79,7 @@ export interface RemoteSourceProvider {
readonly supportsQuery?: boolean;
getBranches?(url: string): ProviderResult<string[]>;
getRemoteSourceActions?(url: string): ProviderResult<RemoteSourceAction[]>;
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri, Event, Disposable, ProviderResult } from 'vscode';
import { Uri, Event, Disposable, ProviderResult, Command } from 'vscode';
export { ProviderResult } from 'vscode';
export interface Git {
@@ -14,6 +14,11 @@ export interface InputBox {
value: string;
}
export const enum ForcePushMode {
Force,
ForceWithLease
}
export const enum RefType {
Head,
RemoteHead,
@@ -73,6 +78,7 @@ export const enum Status {
UNTRACKED,
IGNORED,
INTENT_TO_ADD,
INTENT_TO_RENAME,
ADDED_BY_US,
ADDED_BY_THEM,
@@ -131,6 +137,28 @@ export interface CommitOptions {
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 {
@@ -158,6 +186,8 @@ export interface Repository {
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>;
@@ -184,6 +214,9 @@ export interface Repository {
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>;
@@ -191,9 +224,10 @@ export interface Repository {
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): Promise<void>;
push(remoteName?: string, branchName?: string, setUpstream?: boolean, force?: ForcePushMode): Promise<void>;
blame(path: string): Promise<string>;
log(options?: LogOptions): Promise<Commit[]>;
@@ -231,15 +265,40 @@ 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>;
@@ -247,12 +306,15 @@ export interface API {
toGitUri(uri: Uri, ref: string): Uri;
getRepository(uri: Uri): Repository | null;
init(root: Uri): Promise<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 {
@@ -263,7 +325,7 @@ export interface GitExtension {
/**
* Returns a specific API version.
*
* Throws error if git extension is disabled. You can listed to the
* 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.
*
@@ -309,4 +371,5 @@ export const enum GitErrorCodes {
PatchDoesNotApply = 'PatchDoesNotApply',
NoPathFound = 'NoPathFound',
UnknownPath = 'UnknownPath',
EmptyCommitMessage = 'EmptyCommitMessage'
}

View File

@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. 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;
}
}

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. 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;
}
}