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
co-authored by Robo Johannes Henning Dieterichs Julien Richard Charles Gagnon Megan Rogge meganrogge Rob Lourens Connor Peet Joyce Er Bhavya U Raymond Zhao Martin Aeschlimann Aaron Munger Aiday Marlen Kyzy rebornix Ole Jean Pierre Yash Singh Ladislau Szomoru Ulugbek Abdullaev Alex Ross Michael Lively Matt Bierner Andrea Mah Benjamin Pasero Sandeep Somavarapu Daniel Imms Tyler James Leonhardt Alexandru Dima Joao Moreno Alan Ren
parent 7975fda6dd
commit 01e66ab3e6
4335 changed files with 252586 additions and 164604 deletions
+6 -2
View File
@@ -1,9 +1,13 @@
{
"parserOptions": {
"project": "./extensions/tsconfig.base.json",
"createDefaultProgram": true
},
"rules": {
"no-cond-assign": 2,
"jsdoc/check-param-names": "error",
"@typescript-eslint/explicit-function-return-type": ["error"],
"@typescript-eslint/explicit-function-return-type": ["off"],
"@typescript-eslint/await-thenable": ["error"],
"@typescript-eslint/no-unsafe-assignment": "error"
"@typescript-eslint/no-unsafe-assignment": "off"
}
}
+1
View File
@@ -38,6 +38,7 @@ export class MockInputBox implements vscode.InputBox {
enabled: boolean = false;
busy: boolean = false;
ignoreFocusOut: boolean = false;
valueSelection: readonly [number, number] | undefined = undefined;
show(): void { }
hide(): void {
@@ -99,7 +99,7 @@ describe('Azure Authentication', function () {
should(securityToken).be.undefined();
});
it('dont find correct resources', async function () {
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, TypeMoq.It.isAny(), -1);
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, TypeMoq.It.isAny(), <any>-1);
should(securityToken).be.undefined();
});
it('incorrect tenant', async function () {
@@ -6,4 +6,5 @@ extension.webpack.config.js
extension-browser.webpack.config.js
yarn.lock
build/**
schemas/*.schema.src.json
schemas/devContainer.codespaces.schema.json
schemas/devContainer.vscode.schema.json
@@ -1,103 +0,0 @@
#!/usr/bin/env ts-node
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Inlines "allOf"s to allow for "additionalProperties": false. (https://github.com/microsoft/vscode-remote-release/issues/2967)
// Run this manually after updating devContainer.schema.src.json.
import * as fs from 'fs';
function transform(schema: any) {
const definitions = Object.keys(schema.definitions)
.reduce((d, k) => {
d[`#/definitions/${k}`] = (schema.definitions as any)[k];
return d;
}, {} as any);
function copy(from: any) {
const type = Array.isArray(from) ? 'array' : typeof from;
switch (type) {
case 'object': {
const to: any = {};
for (const key in from) {
switch (key) {
case 'definitions':
break;
case 'oneOf':
const list = copy(from[key])
.reduce((a: any[], o: any) => {
if (o.oneOf) {
a.push(...o.oneOf);
} else {
a.push(o);
}
return a;
}, [] as any[]);
if (list.length === 1) {
Object.assign(to, list[0]);
} else {
to.oneOf = list;
}
break;
case 'allOf':
const all = copy(from[key]);
const leaves = all.map((one: any) => (one.oneOf ? one.oneOf : [one]));
function cross(res: any, leaves: any[][]): any[] {
if (leaves.length) {
const rest = leaves.slice(1);
return ([] as any[]).concat(...leaves[0].map(leaf => {
const intermediate = { ...res, ...leaf };
if ('properties' in res && 'properties' in leaf) {
intermediate.properties = {
...res.properties,
...leaf.properties,
};
}
return cross(intermediate, rest);
}));
}
return [res];
}
const list2 = cross({}, leaves);
if (list2.length === 1) {
Object.assign(to, list2[0]);
} else {
to.oneOf = list2;
}
break;
case '$ref':
const ref = from[key];
const definition = definitions[ref];
if (definition) {
Object.assign(to, copy(definition));
} else {
to[key] = ref;
}
break;
default:
to[key] = copy(from[key]);
break;
}
}
if (to.type === 'object' && !('additionalProperties' in to)) {
to.additionalProperties = false;
}
return to;
}
case 'array': {
return from.map(copy);
}
default:
return from;
}
}
return copy(schema);
}
const devContainer = JSON.parse(fs.readFileSync('../schemas/devContainer.schema.src.json', 'utf8'));
fs.writeFileSync('../schemas/devContainer.schema.generated.json', JSON.stringify(transform(devContainer), undefined, ' '));
@@ -1,7 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"resolveJsonModule": true,
"outDir": "./out"
}
}
@@ -7,6 +7,7 @@
'use strict';
const path = require('path');
const withBrowserDefaults = require('../shared.webpack.config').browser;
module.exports = withBrowserDefaults({
@@ -16,6 +17,11 @@ module.exports = withBrowserDefaults({
},
output: {
filename: 'configurationEditingMain.js'
},
resolve: {
alias: {
'./node/net': path.resolve(__dirname, 'src', 'browser', 'net'),
}
}
});
+17 -5
View File
@@ -10,8 +10,11 @@
},
"icon": "images/icon.png",
"activationEvents": [
"onLanguage:json",
"onLanguage:jsonc"
"onProfile",
"onProfile:github"
],
"enabledApiProposals": [
"profileContentHandlers"
],
"main": "./out/configurationEditingMain",
"browser": "./dist/browser/configurationEditingMain",
@@ -21,7 +24,8 @@
},
"dependencies": {
"jsonc-parser": "^2.2.1",
"vscode-nls": "^5.0.0"
"@octokit/rest": "19.0.4",
"tunnel": "^0.0.6"
},
"capabilities": {
"virtualWorkspaces": true,
@@ -71,6 +75,10 @@
"fileMatch": "%APP_SETTINGS_HOME%/keybindings.json",
"url": "vscode://schemas/keybindings"
},
{
"fileMatch": "%APP_SETTINGS_HOME%/profiles/*/keybindings.json",
"url": "vscode://schemas/keybindings"
},
{
"fileMatch": "vscode://defaultsettings/*.json",
"url": "vscode://schemas/settings/default"
@@ -119,6 +127,10 @@
"fileMatch": "%APP_SETTINGS_HOME%/snippets/*.json",
"url": "vscode://schemas/snippets"
},
{
"fileMatch": "%APP_SETTINGS_HOME%/profiles/*/snippets/.json",
"url": "vscode://schemas/snippets"
},
{
"fileMatch": "%APP_SETTINGS_HOME%/sync/snippets/preview/*.json",
"url": "vscode://schemas/snippets"
@@ -133,11 +145,11 @@
},
{
"fileMatch": "devcontainer.json",
"url": "./schemas/devContainer.schema.generated.json"
"url": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json"
},
{
"fileMatch": ".devcontainer.json",
"url": "./schemas/devContainer.schema.generated.json"
"url": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json"
},
{
"fileMatch": "%APP_SETTINGS_HOME%/globalStorage/ms-vscode-remote.remote-containers/nameConfigs/*.json",
@@ -173,6 +173,13 @@
]
}
}
},
"openFiles": {
"type": "array",
"description": "The paths to the files to open when the codespace is created. Paths are relative to the workspace.",
"items": {
"type": "string"
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,771 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Defines a dev container",
"allowComments": true,
"allowTrailingCommas": false,
"definitions": {
"devContainerCommon": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "A name for the dev container displayed in the UI."
},
"extensions": {
"type": "array",
"description": "An array of extensions that should be installed into the container.",
"items": {
"type": "string",
"pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)((@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|@prerelease)?$",
"errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'."
}
},
"settings": {
"$ref": "vscode://schemas/settings/machine",
"description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again."
},
"features": {
"type": "object",
"description": "Features to add to the dev container.",
"additionalProperties": true
},
"forwardPorts": {
"type": "array",
"description": "Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format \"host:port_number\".",
"items": {
"oneOf": [
{
"type": "integer",
"maximum": 65535,
"minimum": 0
},
{
"type": "string",
"pattern": "^([a-z0-9-]+):(\\d{1,5})$"
}
]
}
},
"portsAttributes": {
"type": "object",
"patternProperties": {
"(^\\d+(-\\d+)?$)|(.+)": {
"type": "object",
"description": "A port, range of ports (ex. \"40000-55000\"), or regular expression (ex. \".+\\\\/server.js\"). For a port number or range, the attributes will apply to that port number or range of port numbers. Attributes which use a regular expression will apply to ports whose associated process command line matches the expression.",
"properties": {
"onAutoForward": {
"type": "string",
"enum": [
"notify",
"openBrowser",
"openBrowserOnce",
"openPreview",
"silent",
"ignore"
],
"enumDescriptions": [
"Shows a notification when a port is automatically forwarded.",
"Opens the browser when the port is automatically forwarded. Depending on your settings, this could open an embedded browser.",
"Opens the browser when the port is automatically forwarded, but only the first time the port is forward during a session. Depending on your settings, this could open an embedded browser.",
"Opens a preview in the same window when the port is automatically forwarded.",
"Shows no notification and takes no action when this port is automatically forwarded.",
"This port will not be automatically forwarded."
],
"description": "Defines the action that occurs when the port is discovered for automatic forwarding",
"default": "notify"
},
"elevateIfNeeded": {
"type": "boolean",
"description": "Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port.",
"default": false
},
"label": {
"type": "string",
"description": "Label that will be shown in the UI for this port.",
"default": "Application"
},
"requireLocalPort": {
"type": "boolean",
"markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.",
"default": false
},
"protocol": {
"type": "string",
"enum": [
"http",
"https"
],
"description": "The protocol to use when forwarding this port."
}
},
"default": {
"label": "Application",
"onAutoForward": "notify"
}
}
},
"markdownDescription": "Set default properties that are applied when a specific port number is forwarded. For example:\n\n```\n\"3000\": {\n \"label\": \"Application\"\n},\n\"40000-55000\": {\n \"onAutoForward\": \"ignore\"\n},\n\".+\\\\/server.js\": {\n \"onAutoForward\": \"openPreview\"\n}\n```",
"defaultSnippets": [
{
"body": {
"${1:3000}": {
"label": "${2:Application}",
"onAutoForward": "notify"
}
}
}
],
"additionalProperties": false
},
"otherPortsAttributes": {
"type": "object",
"properties": {
"onAutoForward": {
"type": "string",
"enum": [
"notify",
"openBrowser",
"openPreview",
"silent",
"ignore"
],
"enumDescriptions": [
"Shows a notification when a port is automatically forwarded.",
"Opens the browser when the port is automatically forwarded. Depending on your settings, this could open an embedded browser.",
"Opens a preview in the same window when the port is automatically forwarded.",
"Shows no notification and takes no action when this port is automatically forwarded.",
"This port will not be automatically forwarded."
],
"description": "Defines the action that occurs when the port is discovered for automatic forwarding",
"default": "notify"
},
"elevateIfNeeded": {
"type": "boolean",
"description": "Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port.",
"default": false
},
"label": {
"type": "string",
"description": "Label that will be shown in the UI for this port.",
"default": "Application"
},
"requireLocalPort": {
"type": "boolean",
"markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.",
"default": false
},
"protocol": {
"type": "string",
"enum": [
"http",
"https"
],
"description": "The protocol to use when forwarding this port."
}
},
"defaultSnippets": [
{
"body": {
"onAutoForward": "ignore"
}
}
],
"markdownDescription": "Set default properties that are applied to all ports that don't get properties from the setting `remote.portsAttributes`. For example:\n\n```\n{\n \"onAutoForward\": \"ignore\"\n}\n```",
"additionalProperties": false
},
"updateRemoteUserUID": {
"type": "boolean",
"description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default when opening from a local folder."
},
"remoteEnv": {
"type": "object",
"additionalProperties": {
"type": [
"string",
"null"
]
},
"description": "Remote environment variables. If these are used in the Integrated Terminal, make sure the 'Terminal > Integrated: Inherit Env' setting is enabled."
},
"remoteUser": {
"type": "string",
"description": "The user VS Code Server will be started with. The default is the same user as the container."
},
"initializeCommand": {
"type": [
"string",
"array"
],
"description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.",
"items": {
"type": "string"
}
},
"onCreateCommand": {
"type": [
"string",
"array"
],
"description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.",
"items": {
"type": "string"
}
},
"updateContentCommand": {
"type": [
"string",
"array"
],
"description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.",
"items": {
"type": "string"
}
},
"postCreateCommand": {
"type": [
"string",
"array"
],
"description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.",
"items": {
"type": "string"
}
},
"postStartCommand": {
"type": [
"string",
"array"
],
"description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.",
"items": {
"type": "string"
}
},
"postAttachCommand": {
"type": [
"string",
"array"
],
"description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.",
"items": {
"type": "string"
}
},
"waitFor": {
"type": "string",
"enum": [
"initializeCommand",
"onCreateCommand",
"updateContentCommand",
"postCreateCommand",
"postStartCommand"
],
"description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"."
},
"devPort": {
"type": "integer",
"description": "The port VS Code can use to connect to its backend."
},
"userEnvProbe": {
"type": "string",
"enum": [
"none",
"loginShell",
"loginInteractiveShell",
"interactiveShell"
],
"description": "User environment probe to run. The default is \"loginInteractiveShell\"."
},
"codespaces": {
"type": "object",
"additionalProperties": true,
"description": "Codespaces-specific configuration."
},
"hostRequirements": {
"type": "object",
"description": "Host hardware requirements.",
"allOf": [
{
"type": "object",
"properties": {
"cpus": {
"type": "integer",
"minimum": 1,
"description": "Number of required CPUs."
},
"memory": {
"type": "string",
"pattern": "^\\d+([tgmk]b)?$",
"description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb."
},
"storage": {
"type": "string",
"pattern": "^\\d+([tgmk]b)?$",
"description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb."
}
}
}
]
},
"customizations": {
"type": "object",
"description": "Tool-specific configuration. Each tool should use a JSON object subproperty with a unique name to group its customizations.",
"properties": {
"vscode": {
"type": "object",
"properties": {
"extensions": {
"type": "array",
"description": "An array of extensions that should be installed into the container.",
"items": {
"type": "string",
"pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$",
"errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'."
}
},
"settings": {
"$ref": "vscode://schemas/settings/machine",
"description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again."
},
"devPort": {
"type": "integer",
"description": "The port VS Code can use to connect to its backend."
}
}
},
"codespaces": {
"type": "object",
"description": "Customizations specific to GitHub Codespaces",
"properties": {
"repositories": {
"type": "object",
"description": "Configuration relative to the given repositories, following the format 'owner/repo'.\n A wildcard (*) is permitted for the repo name (eg: 'microsoft/*')",
"patternProperties": {
"^[a-zA-Z0-9-_.]+[.]*\/[a-zA-Z0-9-_*]+[.]*$": {
"type": "object",
"additionalProperties": true,
"oneOf": [
{
"properties": {
"permissions": {
"type": "object",
"description": "Additional repository permissions.\n See https://aka.ms/ghcs/multi-repo-auth for more info.",
"additionalProperties": true,
"anyOf": [
{
"properties": {
"actions": {
"type": "string",
"enum": [
"read",
"write"
]
}
}
},
{
"properties": {
"checks": {
"type": "string",
"enum": [
"read",
"write"
]
}
}
},
{
"properties": {
"contents": {
"type": "string",
"enum": [
"read",
"write"
]
}
}
},
{
"properties": {
"deployments": {
"type": "string",
"enum": [
"read",
"write"
]
}
}
},
{
"properties": {
"discussions": {
"type": "string",
"enum": [
"read",
"write"
]
}
}
},
{
"properties": {
"issues": {
"type": "string",
"enum": [
"read",
"write"
]
}
}
},
{
"properties": {
"packages": {
"type": "string",
"enum": [
"read"
]
}
}
},
{
"properties": {
"pages": {
"type": "string",
"enum": [
"read",
"write"
]
}
}
},
{
"properties": {
"pull_requests": {
"type": "string",
"enum": [
"read",
"write"
]
}
}
},
{
"properties": {
"repository_projects": {
"type": "string",
"enum": [
"read",
"write"
]
}
}
},
{
"properties": {
"statuses": {
"type": "string",
"enum": [
"read",
"write"
]
}
}
},
{
"properties": {
"workflows": {
"type": "string",
"enum": [
"write"
]
}
}
}
]
}
}
},
{
"properties": {
"permissions": {
"type": "string",
"description": "Additional repository permissions.\n See https://aka.ms/ghcs/multi-repo-auth for more info.",
"enum": [
"read-all",
"write-all"
]
}
}
}
]
}
}
}
}
}
}
},
"additionalProperties": {
"type": "object",
"additionalProperties": true
}
}
},
"nonComposeBase": {
"type": "object",
"properties": {
"appPort": {
"type": [
"integer",
"string",
"array"
],
"description": "Application ports that are exposed by the container. This can be a single port or an array of ports. Each port can be a number or a string. A number is mapped to the same port on the host. A string is passed to Docker unchanged and can be used to map ports differently, e.g. \"8000:8010\".",
"items": {
"type": [
"integer",
"string"
]
}
},
"containerEnv": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Container environment variables."
},
"containerUser": {
"type": "string",
"description": "The user the container will be started with. The default is the user on the Docker image."
},
"mounts": {
"type": "array",
"description": "Mount points to set up when creating the container. See Docker's documentation for the --mount option for the supported syntax.",
"items": {
"type": "string"
}
},
"runArgs": {
"type": "array",
"description": "The arguments required when starting in the container.",
"items": {
"type": "string"
}
},
"shutdownAction": {
"type": "string",
"enum": [
"none",
"stopContainer"
],
"description": "Action to take when the VS Code window is closed. The default is to stop the container."
},
"overrideCommand": {
"type": "boolean",
"description": "Whether to overwrite the command specified in the image. The default is true."
},
"workspaceFolder": {
"type": "string",
"description": "The path of the workspace folder inside the container."
},
"workspaceMount": {
"type": "string",
"description": "The --mount parameter for docker run. The default is to mount the project folder at /workspaces/$project."
}
}
},
"dockerfileContainer": {
"oneOf": [
{
"type": "object",
"properties": {
"build": {
"type": "object",
"description": "Docker build-related options.",
"allOf": [
{
"type": "object",
"properties": {
"dockerfile": {
"type": "string",
"description": "The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file."
},
"context": {
"type": "string",
"description": "The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file."
}
},
"required": [
"dockerfile"
]
},
{
"$ref": "#/definitions/buildOptions"
}
]
}
},
"required": [
"build"
]
},
{
"allOf": [
{
"type": "object",
"properties": {
"dockerFile": {
"type": "string",
"description": "The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file."
},
"context": {
"type": "string",
"description": "The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file."
}
},
"required": [
"dockerFile"
]
},
{
"type": "object",
"properties": {
"build": {
"description": "Docker build-related options.",
"$ref": "#/definitions/buildOptions"
}
}
}
]
}
]
},
"buildOptions": {
"type": "object",
"properties": {
"target": {
"type": "string",
"description": "Target stage in a multi-stage build."
},
"args": {
"type": "object",
"additionalProperties": {
"type": [
"string"
]
},
"description": "Build arguments."
},
"cacheFrom": {
"type": [
"string",
"array"
],
"description": "The image to consider as a cache. Use an array to specify multiple images.",
"items": {
"type": "string"
}
}
}
},
"imageContainer": {
"type": "object",
"properties": {
"image": {
"type": "string",
"description": "The docker image that will be used to create the container."
}
},
"required": [
"image"
]
},
"composeContainer": {
"type": "object",
"properties": {
"dockerComposeFile": {
"type": [
"string",
"array"
],
"description": "The name of the docker-compose file(s) used to start the services.",
"items": {
"type": "string"
}
},
"service": {
"type": "string",
"description": "The service you want to work on."
},
"runServices": {
"type": "array",
"description": "An array of services that should be started and stopped.",
"items": {
"type": "string"
}
},
"workspaceFolder": {
"type": "string",
"description": "The path of the workspace folder inside the container. This is typically the target path of a volume mount in the docker-compose.yml."
},
"shutdownAction": {
"type": "string",
"enum": [
"none",
"stopCompose"
],
"description": "Action to take when the VS Code window is closed. The default is to stop the containers."
},
"overrideCommand": {
"type": "boolean",
"description": "Whether to overwrite the command specified in the image. The default is false."
}
},
"required": [
"dockerComposeFile",
"service",
"workspaceFolder"
]
}
},
"oneOf": [
{
"allOf": [
{
"oneOf": [
{
"allOf": [
{
"oneOf": [
{
"$ref": "#/definitions/dockerfileContainer"
},
{
"$ref": "#/definitions/imageContainer"
}
]
},
{
"$ref": "#/definitions/nonComposeBase"
}
]
},
{
"$ref": "#/definitions/composeContainer"
}
]
},
{
"$ref": "#/definitions/devContainerCommon"
}
]
},
{
"type": "object",
"$ref": "#/definitions/devContainerCommon",
"additionalProperties": false
}
]
}
@@ -13,7 +13,7 @@
"description": "An array of extensions that should be installed into the container.",
"items": {
"type": "string",
"pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$",
"pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)((@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|@prerelease)?$",
"errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'."
}
},
@@ -2,4 +2,5 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'markdown-it-named-headers' { }
export const agent = undefined;
@@ -5,10 +5,9 @@
import { getLocation, JSONPath, parse, visit, Location } from 'jsonc-parser';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { SettingsDocument } from './settingsDocumentHelper';
import { provideInstalledExtensionProposals } from './extensionsProposals';
const localize = nls.loadMessageBundle();
import './importExportProfiles';
export function activate(context: vscode.ExtensionContext): void {
//settings.json suggestions
@@ -23,6 +22,9 @@ export function activate(context: vscode.ExtensionContext): void {
// task.json variable suggestions
context.subscriptions.push(registerVariableCompletions('**/tasks.json'));
// Workspace file launch/tasks variable completions
context.subscriptions.push(registerVariableCompletions('**/*.code-workspace'));
// keybindings.json/package.json context key suggestions
context.subscriptions.push(registerContextKeyCompletions());
}
@@ -40,27 +42,31 @@ function registerVariableCompletions(pattern: string): vscode.Disposable {
provideCompletionItems(document, position, _token) {
const location = getLocation(document.getText(), document.offsetAt(position));
if (isCompletingInsidePropertyStringValue(document, location, position)) {
if (document.fileName.endsWith('.code-workspace') && !isLocationInsideTopLevelProperty(location, ['launch', 'tasks'])) {
return [];
}
let range = document.getWordRangeAtPosition(position, /\$\{[^"\}]*\}?/);
if (!range || range.start.isEqual(position) || range.end.isEqual(position) && document.getText(range).endsWith('}')) {
range = new vscode.Range(position, position);
}
return [
{ label: 'workspaceFolder', detail: localize('workspaceFolder', "The path of the folder opened in VS Code") },
{ label: 'workspaceFolderBasename', detail: localize('workspaceFolderBasename', "The name of the folder opened in VS Code without any slashes (/)") },
{ label: 'relativeFile', detail: localize('relativeFile', "The current opened file relative to ${workspaceFolder}") },
{ label: 'relativeFileDirname', detail: localize('relativeFileDirname', "The current opened file's dirname relative to ${workspaceFolder}") },
{ label: 'file', detail: localize('file', "The current opened file") },
{ label: 'cwd', detail: localize('cwd', "The task runner's current working directory on startup") },
{ label: 'lineNumber', detail: localize('lineNumber', "The current selected line number in the active file") },
{ label: 'selectedText', detail: localize('selectedText', "The current selected text in the active file") },
{ label: 'fileDirname', detail: localize('fileDirname', "The current opened file's dirname") },
{ label: 'fileExtname', detail: localize('fileExtname', "The current opened file's extension") },
{ label: 'fileBasename', detail: localize('fileBasename', "The current opened file's basename") },
{ label: 'fileBasenameNoExtension', detail: localize('fileBasenameNoExtension', "The current opened file's basename with no file extension") },
{ label: 'defaultBuildTask', detail: localize('defaultBuildTask', "The name of the default build task. If there is not a single default build task then a quick pick is shown to choose the build task.") },
{ label: 'pathSeparator', detail: localize('pathSeparator', "The character used by the operating system to separate components in file paths") },
{ label: 'extensionInstallFolder', detail: localize('extensionInstallFolder', "The path where an an extension is installed."), param: 'publisher.extension' },
{ label: 'workspaceFolder', detail: vscode.l10n.t("The path of the folder opened in VS Code") },
{ label: 'workspaceFolderBasename', detail: vscode.l10n.t("The name of the folder opened in VS Code without any slashes (/)") },
{ label: 'relativeFile', detail: vscode.l10n.t("The current opened file relative to ${workspaceFolder}") },
{ label: 'relativeFileDirname', detail: vscode.l10n.t("The current opened file's dirname relative to ${workspaceFolder}") },
{ label: 'file', detail: vscode.l10n.t("The current opened file") },
{ label: 'cwd', detail: vscode.l10n.t("The task runner's current working directory on startup") },
{ label: 'lineNumber', detail: vscode.l10n.t("The current selected line number in the active file") },
{ label: 'selectedText', detail: vscode.l10n.t("The current selected text in the active file") },
{ label: 'fileDirname', detail: vscode.l10n.t("The current opened file's dirname") },
{ label: 'fileExtname', detail: vscode.l10n.t("The current opened file's extension") },
{ label: 'fileBasename', detail: vscode.l10n.t("The current opened file's basename") },
{ label: 'fileBasenameNoExtension', detail: vscode.l10n.t("The current opened file's basename with no file extension") },
{ label: 'defaultBuildTask', detail: vscode.l10n.t("The name of the default build task. If there is not a single default build task then a quick pick is shown to choose the build task.") },
{ label: 'pathSeparator', detail: vscode.l10n.t("The character used by the operating system to separate components in file paths") },
{ label: 'extensionInstallFolder', detail: vscode.l10n.t("The path where an an extension is installed."), param: 'publisher.extension' },
].map(variable => ({
label: `\${${variable.label}}`,
range,
@@ -86,6 +92,10 @@ function isCompletingInsidePropertyStringValue(document: vscode.TextDocument, lo
return false;
}
function isLocationInsideTopLevelProperty(location: Location, values: string[]) {
return values.includes(location.path[0] as string);
}
interface IExtensionsContent {
recommendations: string[];
}
@@ -4,8 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export async function provideInstalledExtensionProposals(existing: string[], additionalText: string, range: vscode.Range, includeBuiltinExtensions: boolean): Promise<vscode.CompletionItem[] | vscode.CompletionList> {
@@ -23,7 +21,7 @@ export async function provideInstalledExtensionProposals(existing: string[], add
return item;
});
} else {
const example = new vscode.CompletionItem(localize('exampleExtension', "Example"));
const example = new vscode.CompletionItem(vscode.l10n.t("Example"));
example.insertText = '"vscode.csharp"';
example.kind = vscode.CompletionItemKind.Value;
example.range = range;
@@ -48,7 +46,7 @@ export async function provideWorkspaceTrustExtensionProposals(existing: string[]
return item;
});
} else {
const example = new vscode.CompletionItem(localize('exampleExtension', "Example"));
const example = new vscode.CompletionItem(vscode.l10n.t("Example"));
example.insertText = '"vscode.csharp: {\n\t"supported": false,\n\t"version": "0.0.0"\n}`;"';
example.kind = vscode.CompletionItemKind.Value;
example.range = range;
@@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Octokit } from '@octokit/rest';
import * as vscode from 'vscode';
import { basename } from 'path';
import { agent } from './node/net';
class GitHubGistProfileContentHandler implements vscode.ProfileContentHandler {
readonly name = vscode.l10n.t('GitHub');
readonly description = vscode.l10n.t('gist');
private _octokit: Promise<Octokit> | undefined;
private getOctokit(): Promise<Octokit> {
if (!this._octokit) {
this._octokit = (async () => {
const session = await vscode.authentication.getSession('github', ['gist', 'user:email'], { createIfNone: true });
const token = session.accessToken;
const { Octokit } = await import('@octokit/rest');
return new Octokit({
request: { agent },
userAgent: 'GitHub VSCode',
auth: `token ${token}`
});
})();
}
return this._octokit;
}
async saveProfile(name: string, content: string): Promise<{ readonly id: string; readonly link: vscode.Uri } | null> {
const octokit = await this.getOctokit();
const result = await octokit.gists.create({
public: false,
files: {
[name]: {
content
}
}
});
if (result.data.id && result.data.html_url) {
const link = vscode.Uri.parse(result.data.html_url);
return { id: result.data.id, link };
}
return null;
}
private _public_octokit: Promise<Octokit> | undefined;
private getPublicOctokit(): Promise<Octokit> {
if (!this._public_octokit) {
this._public_octokit = (async () => {
const { Octokit } = await import('@octokit/rest');
return new Octokit({ request: { agent }, userAgent: 'GitHub VSCode' });
})();
}
return this._public_octokit;
}
async readProfile(id: string): Promise<string | null>;
async readProfile(uri: vscode.Uri): Promise<string | null>;
async readProfile(arg: string | vscode.Uri): Promise<string | null> {
const gist_id = typeof arg === 'string' ? arg : basename(arg.path);
const octokit = await this.getPublicOctokit();
try {
const gist = await octokit.gists.get({ gist_id });
if (gist.data.files) {
return gist.data.files[Object.keys(gist.data.files)[0]]?.content ?? null;
}
} catch (error) {
// ignore
}
return null;
}
}
vscode.window.registerProfileContentHandler('github', new GitHubGistProfileContentHandler());
@@ -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.
*--------------------------------------------------------------------------------------------*/
import { Agent, globalAgent } from 'https';
import { URL } from 'url';
import { httpsOverHttp } from 'tunnel';
import { window } from 'vscode';
export const agent = getAgent();
/**
* Return an https agent for the given proxy URL, or return the
* global https agent if the URL was empty or invalid.
*/
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;
}
}
@@ -5,10 +5,8 @@
import * as vscode from 'vscode';
import { getLocation, Location, parse } from 'jsonc-parser';
import * as nls from 'vscode-nls';
import { provideInstalledExtensionProposals } from './extensionsProposals';
const localize = nls.loadMessageBundle();
const OVERRIDE_IDENTIFIER_REGEX = /\[([^\[\]]*)\]/g;
export class SettingsDocument {
@@ -28,8 +26,8 @@ export class SettingsDocument {
return this.provideFilesAssociationsCompletionItems(location, position);
}
// files.exclude, search.exclude
if (location.path[0] === 'files.exclude' || location.path[0] === 'search.exclude') {
// files.exclude, search.exclude, explorer.autoRevealExclude
if (location.path[0] === 'files.exclude' || location.path[0] === 'search.exclude' || location.path[0] === 'explorer.autoRevealExclude') {
return this.provideExcludeCompletionItems(location, position);
}
@@ -107,20 +105,21 @@ export class SettingsDocument {
};
completions.push(this.newSimpleCompletionItem(getText('activeEditorShort'), range, localize('activeEditorShort', "the file name (e.g. myFile.txt)")));
completions.push(this.newSimpleCompletionItem(getText('activeEditorMedium'), range, localize('activeEditorMedium', "the path of the file relative to the workspace folder (e.g. myFolder/myFileFolder/myFile.txt)")));
completions.push(this.newSimpleCompletionItem(getText('activeEditorLong'), range, localize('activeEditorLong', "the full path of the file (e.g. /Users/Development/myFolder/myFileFolder/myFile.txt)")));
completions.push(this.newSimpleCompletionItem(getText('activeFolderShort'), range, localize('activeFolderShort', "the name of the folder the file is contained in (e.g. myFileFolder)")));
completions.push(this.newSimpleCompletionItem(getText('activeFolderMedium'), range, localize('activeFolderMedium', "the path of the folder the file is contained in, relative to the workspace folder (e.g. myFolder/myFileFolder)")));
completions.push(this.newSimpleCompletionItem(getText('activeFolderLong'), range, localize('activeFolderLong', "the full path of the folder the file is contained in (e.g. /Users/Development/myFolder/myFileFolder)")));
completions.push(this.newSimpleCompletionItem(getText('rootName'), range, localize('rootName', "name of the workspace (e.g. myFolder or myWorkspace)")));
completions.push(this.newSimpleCompletionItem(getText('rootPath'), range, localize('rootPath', "file path of the workspace (e.g. /Users/Development/myWorkspace)")));
completions.push(this.newSimpleCompletionItem(getText('folderName'), range, localize('folderName', "name of the workspace folder the file is contained in (e.g. myFolder)")));
completions.push(this.newSimpleCompletionItem(getText('folderPath'), range, localize('folderPath', "file path of the workspace folder the file is contained in (e.g. /Users/Development/myFolder)")));
completions.push(this.newSimpleCompletionItem(getText('appName'), range, localize('appName', "e.g. VS Code")));
completions.push(this.newSimpleCompletionItem(getText('remoteName'), range, localize('remoteName', "e.g. SSH")));
completions.push(this.newSimpleCompletionItem(getText('dirty'), range, localize('dirty', "an indicator for when the active editor has unsaved changes")));
completions.push(this.newSimpleCompletionItem(getText('separator'), range, localize('separator', "a conditional separator (' - ') that only shows when surrounded by variables with values")));
completions.push(this.newSimpleCompletionItem(getText('activeEditorShort'), range, vscode.l10n.t("the file name (e.g. myFile.txt)")));
completions.push(this.newSimpleCompletionItem(getText('activeEditorMedium'), range, vscode.l10n.t("the path of the file relative to the workspace folder (e.g. myFolder/myFileFolder/myFile.txt)")));
completions.push(this.newSimpleCompletionItem(getText('activeEditorLong'), range, vscode.l10n.t("the full path of the file (e.g. /Users/Development/myFolder/myFileFolder/myFile.txt)")));
completions.push(this.newSimpleCompletionItem(getText('activeFolderShort'), range, vscode.l10n.t("the name of the folder the file is contained in (e.g. myFileFolder)")));
completions.push(this.newSimpleCompletionItem(getText('activeFolderMedium'), range, vscode.l10n.t("the path of the folder the file is contained in, relative to the workspace folder (e.g. myFolder/myFileFolder)")));
completions.push(this.newSimpleCompletionItem(getText('activeFolderLong'), range, vscode.l10n.t("the full path of the folder the file is contained in (e.g. /Users/Development/myFolder/myFileFolder)")));
completions.push(this.newSimpleCompletionItem(getText('rootName'), range, vscode.l10n.t("name of the workspace with optional remote name and workspace indicator if applicable (e.g. myFolder, myRemoteFolder [SSH] or myWorkspace (Workspace))")));
completions.push(this.newSimpleCompletionItem(getText('rootNameShort'), range, vscode.l10n.t("shortened name of the workspace without suffixes (e.g. myFolder or myWorkspace)")));
completions.push(this.newSimpleCompletionItem(getText('rootPath'), range, vscode.l10n.t("file path of the workspace (e.g. /Users/Development/myWorkspace)")));
completions.push(this.newSimpleCompletionItem(getText('folderName'), range, vscode.l10n.t("name of the workspace folder the file is contained in (e.g. myFolder)")));
completions.push(this.newSimpleCompletionItem(getText('folderPath'), range, vscode.l10n.t("file path of the workspace folder the file is contained in (e.g. /Users/Development/myFolder)")));
completions.push(this.newSimpleCompletionItem(getText('appName'), range, vscode.l10n.t("e.g. VS Code")));
completions.push(this.newSimpleCompletionItem(getText('remoteName'), range, vscode.l10n.t("e.g. SSH")));
completions.push(this.newSimpleCompletionItem(getText('dirty'), range, vscode.l10n.t("an indicator for when the active editor has unsaved changes")));
completions.push(this.newSimpleCompletionItem(getText('separator'), range, vscode.l10n.t("a conditional separator (' - ') that only shows when surrounded by variables with values")));
return completions;
}
@@ -134,15 +133,15 @@ export class SettingsDocument {
const range = this.getReplaceRange(location, position);
completions.push(this.newSnippetCompletionItem({
label: localize('assocLabelFile', "Files with Extension"),
documentation: localize('assocDescriptionFile', "Map all files matching the glob pattern in their filename to the language with the given identifier."),
label: vscode.l10n.t("Files with Extension"),
documentation: vscode.l10n.t("Map all files matching the glob pattern in their filename to the language with the given identifier."),
snippet: location.isAtPropertyKey ? '"*.${1:extension}": "${2:language}"' : '{ "*.${1:extension}": "${2:language}" }',
range
}));
completions.push(this.newSnippetCompletionItem({
label: localize('assocLabelPath', "Files with Path"),
documentation: localize('assocDescriptionPath', "Map all files matching the absolute path glob pattern in their path to the language with the given identifier."),
label: vscode.l10n.t("Files with Path"),
documentation: vscode.l10n.t("Map all files matching the absolute path glob pattern in their path to the language with the given identifier."),
snippet: location.isAtPropertyKey ? '"/${1:path to file}/*.${2:extension}": "${3:language}"' : '{ "/${1:path to file}/*.${2:extension}": "${3:language}" }',
range
}));
@@ -163,43 +162,43 @@ export class SettingsDocument {
const range = this.getReplaceRange(location, position);
completions.push(this.newSnippetCompletionItem({
label: localize('fileLabel', "Files by Extension"),
documentation: localize('fileDescription', "Match all files of a specific file extension."),
label: vscode.l10n.t("Files by Extension"),
documentation: vscode.l10n.t("Match all files of a specific file extension."),
snippet: location.path.length === 2 ? '"**/*.${1:extension}": true' : '{ "**/*.${1:extension}": true }',
range
}));
completions.push(this.newSnippetCompletionItem({
label: localize('filesLabel', "Files with Multiple Extensions"),
documentation: localize('filesDescription', "Match all files with any of the file extensions."),
label: vscode.l10n.t("Files with Multiple Extensions"),
documentation: vscode.l10n.t("Match all files with any of the file extensions."),
snippet: location.path.length === 2 ? '"**/*.{ext1,ext2,ext3}": true' : '{ "**/*.{ext1,ext2,ext3}": true }',
range
}));
completions.push(this.newSnippetCompletionItem({
label: localize('derivedLabel', "Files with Siblings by Name"),
documentation: localize('derivedDescription', "Match files that have siblings with the same name but a different extension."),
label: vscode.l10n.t("Files with Siblings by Name"),
documentation: vscode.l10n.t("Match files that have siblings with the same name but a different extension."),
snippet: location.path.length === 2 ? '"**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" }' : '{ "**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" } }',
range
}));
completions.push(this.newSnippetCompletionItem({
label: localize('topFolderLabel', "Folder by Name (Top Level)"),
documentation: localize('topFolderDescription', "Match a top level folder with a specific name."),
label: vscode.l10n.t("Folder by Name (Top Level)"),
documentation: vscode.l10n.t("Match a top level folder with a specific name."),
snippet: location.path.length === 2 ? '"${1:name}": true' : '{ "${1:name}": true }',
range
}));
completions.push(this.newSnippetCompletionItem({
label: localize('topFoldersLabel', "Folders with Multiple Names (Top Level)"),
documentation: localize('topFoldersDescription', "Match multiple top level folders."),
label: vscode.l10n.t("Folders with Multiple Names (Top Level)"),
documentation: vscode.l10n.t("Match multiple top level folders."),
snippet: location.path.length === 2 ? '"{folder1,folder2,folder3}": true' : '{ "{folder1,folder2,folder3}": true }',
range
}));
completions.push(this.newSnippetCompletionItem({
label: localize('folderLabel', "Folder by Name (Any Location)"),
documentation: localize('folderDescription', "Match a folder with a specific name in any location."),
label: vscode.l10n.t("Folder by Name (Any Location)"),
documentation: vscode.l10n.t("Match a folder with a specific name in any location."),
snippet: location.path.length === 2 ? '"**/${1:name}": true' : '{ "**/${1:name}": true }',
range
}));
@@ -209,8 +208,8 @@ export class SettingsDocument {
else if (location.path.length === 2 && this.isCompletingPropertyValue(location, position)) {
const range = this.getReplaceRange(location, position);
completions.push(this.newSnippetCompletionItem({
label: localize('derivedLabel', "Files with Siblings by Name"),
documentation: localize('siblingsDescription', "Match files that have siblings with the same name but a different extension."),
label: vscode.l10n.t("Files with Siblings by Name"),
documentation: vscode.l10n.t("Match files that have siblings with the same name but a different extension."),
snippet: '{ "when": "$(basename).${1:extension}" }',
range
}));
@@ -224,7 +223,7 @@ export class SettingsDocument {
const range = this.getReplaceRange(location, position);
const languages = await vscode.languages.getLanguages();
return [
this.newSimpleCompletionItem(JSON.stringify('${activeEditorLanguage}'), range, localize('activeEditor', "Use the language of the currently active text editor if any")),
this.newSimpleCompletionItem(JSON.stringify('${activeEditorLanguage}'), range, vscode.l10n.t("Use the language of the currently active text editor if any")),
...languages.map(l => this.newSimpleCompletionItem(JSON.stringify(l), range))
];
}
@@ -267,7 +266,7 @@ export class SettingsDocument {
const languageOverrideRange = languageOverridesRanges.find(range => range.contains(position));
/**
* Skip if suggestsions are for first language override range
* Skip if suggestions are for first language override range
* Since VSCode registers language overrides to the schema, JSON language server does suggestions for first language override.
*/
if (languageOverrideRange && !languageOverrideRange.isEqual(languageOverridesRanges[0])) {
@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as testRunner from '../../../../test/integration/electron/testrunner';
const options: import('mocha').MochaOptions = {
ui: 'tdd',
color: true,
timeout: 60000
};
// These integration tests is being run in multiple environments (electron, web, remote)
// so we need to set the suite name based on the environment as the suite name is used
// for the test results file name
let suite = '';
if (process.env.VSCODE_BROWSER) {
suite = `${process.env.VSCODE_BROWSER} Browser Integration Configuration-Editing Tests`;
} else if (process.env.REMOTE_VSCODE) {
suite = 'Remote Integration Configuration-Editing Tests';
} else {
suite = 'Integration Configuration-Editing Tests';
}
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;
@@ -3,6 +3,4 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function isEmptyOrWhitespace(str: string): boolean {
return /^\s*$/.test(str);
}
declare module 'tunnel';
@@ -8,6 +8,7 @@
},
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts"
"../../src/vscode-dts/vscode.d.ts",
"../../src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts",
]
}
+173 -3
View File
@@ -2,17 +2,187 @@
# yarn lockfile v1
"@octokit/auth-token@^3.0.0":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.2.tgz#a0fc8de149fd15876e1ac78f6525c1c5ab48435f"
integrity sha512-pq7CwIMV1kmzkFTimdwjAINCXKTajZErLB4wMLYapR2nuB/Jpr66+05wOTZMSCBXP6n4DdDWT2W19Bm17vU69Q==
dependencies:
"@octokit/types" "^8.0.0"
"@octokit/core@^4.0.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-4.1.0.tgz#b6b03a478f1716de92b3f4ec4fd64d05ba5a9251"
integrity sha512-Czz/59VefU+kKDy+ZfDwtOIYIkFjExOKf+HA92aiTZJ6EfWpFzYQWw0l54ji8bVmyhc+mGaLUbSUmXazG7z5OQ==
dependencies:
"@octokit/auth-token" "^3.0.0"
"@octokit/graphql" "^5.0.0"
"@octokit/request" "^6.0.0"
"@octokit/request-error" "^3.0.0"
"@octokit/types" "^8.0.0"
before-after-hook "^2.2.0"
universal-user-agent "^6.0.0"
"@octokit/endpoint@^7.0.0":
version "7.0.3"
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-7.0.3.tgz#0b96035673a9e3bedf8bab8f7335de424a2147ed"
integrity sha512-57gRlb28bwTsdNXq+O3JTQ7ERmBTuik9+LelgcLIVfYwf235VHbN9QNo4kXExtp/h8T423cR5iJThKtFYxC7Lw==
dependencies:
"@octokit/types" "^8.0.0"
is-plain-object "^5.0.0"
universal-user-agent "^6.0.0"
"@octokit/graphql@^5.0.0":
version "5.0.4"
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-5.0.4.tgz#519dd5c05123868276f3ae4e50ad565ed7dff8c8"
integrity sha512-amO1M5QUQgYQo09aStR/XO7KAl13xpigcy/kI8/N1PnZYSS69fgte+xA4+c2DISKqUZfsh0wwjc2FaCt99L41A==
dependencies:
"@octokit/request" "^6.0.0"
"@octokit/types" "^8.0.0"
universal-user-agent "^6.0.0"
"@octokit/openapi-types@^13.11.0":
version "13.13.1"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-13.13.1.tgz#a783bacb1817c9f61a2a0c3f81ea22ad62340fdf"
integrity sha512-4EuKSk3N95UBWFau3Bz9b3pheQ8jQYbKmBL5+GSuY8YDPDwu03J4BjI+66yNi8aaX/3h1qDpb0mbBkLdr+cfGQ==
"@octokit/openapi-types@^14.0.0":
version "14.0.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-14.0.0.tgz#949c5019028c93f189abbc2fb42f333290f7134a"
integrity sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw==
"@octokit/plugin-paginate-rest@^4.0.0":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-4.3.1.tgz#553e653ee0318605acd23bf3a799c8bfafdedae3"
integrity sha512-h8KKxESmSFTcXX409CAxlaOYscEDvN2KGQRsLCGT1NSqRW+D6EXLVQ8vuHhFznS9MuH9QYw1GfsUN30bg8hjVA==
dependencies:
"@octokit/types" "^7.5.0"
"@octokit/plugin-request-log@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85"
integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==
"@octokit/plugin-rest-endpoint-methods@^6.0.0":
version "6.7.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.7.0.tgz#2f6f17f25b6babbc8b41d2bb0a95a8839672ce7c"
integrity sha512-orxQ0fAHA7IpYhG2flD2AygztPlGYNAdlzYz8yrD8NDgelPfOYoRPROfEyIe035PlxvbYrgkfUZIhSBKju/Cvw==
dependencies:
"@octokit/types" "^8.0.0"
deprecation "^2.3.1"
"@octokit/request-error@^3.0.0":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-3.0.2.tgz#f74c0f163d19463b87528efe877216c41d6deb0a"
integrity sha512-WMNOFYrSaX8zXWoJg9u/pKgWPo94JXilMLb2VManNOby9EZxrQaBe/QSC4a1TzpAlpxofg2X/jMnCyZgL6y7eg==
dependencies:
"@octokit/types" "^8.0.0"
deprecation "^2.0.0"
once "^1.4.0"
"@octokit/request@^6.0.0":
version "6.2.2"
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-6.2.2.tgz#a2ba5ac22bddd5dcb3f539b618faa05115c5a255"
integrity sha512-6VDqgj0HMc2FUX2awIs+sM6OwLgwHvAi4KCK3mT2H2IKRt6oH9d0fej5LluF5mck1lRR/rFWN0YIDSYXYSylbw==
dependencies:
"@octokit/endpoint" "^7.0.0"
"@octokit/request-error" "^3.0.0"
"@octokit/types" "^8.0.0"
is-plain-object "^5.0.0"
node-fetch "^2.6.7"
universal-user-agent "^6.0.0"
"@octokit/rest@19.0.4":
version "19.0.4"
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-19.0.4.tgz#fd8bed1cefffa486e9ae46a9dc608ce81bcfcbdd"
integrity sha512-LwG668+6lE8zlSYOfwPj4FxWdv/qFXYBpv79TWIQEpBLKA9D/IMcWsF/U9RGpA3YqMVDiTxpgVpEW3zTFfPFTA==
dependencies:
"@octokit/core" "^4.0.0"
"@octokit/plugin-paginate-rest" "^4.0.0"
"@octokit/plugin-request-log" "^1.0.4"
"@octokit/plugin-rest-endpoint-methods" "^6.0.0"
"@octokit/types@^7.5.0":
version "7.5.1"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-7.5.1.tgz#4e8b182933c17e1f41cc25d44757dbdb7bd76c1b"
integrity sha512-Zk4OUMLCSpXNI8KZZn47lVLJSsgMyCimsWWQI5hyjZg7hdYm0kjotaIkbG0Pp8SfU2CofMBzonboTqvzn3FrJA==
dependencies:
"@octokit/openapi-types" "^13.11.0"
"@octokit/types@^8.0.0":
version "8.1.1"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-8.1.1.tgz#92e304e0f00d563667dfdbe0ae6b52e70d5149bb"
integrity sha512-7tjk+6DyhYAmei8FOEwPfGKc0VE1x56CKPJ+eE44zhDbOyMT+9yan8apfQFxo8oEFsy+0O7PiBtH8w0Yo0Y9Kw==
dependencies:
"@octokit/openapi-types" "^14.0.0"
"@types/node@16.x":
version "16.11.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae"
integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==
before-after-hook@^2.2.0:
version "2.2.3"
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c"
integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==
deprecation@^2.0.0, deprecation@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
jsonc-parser@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.1.tgz#db73cd59d78cce28723199466b2a03d1be1df2bc"
integrity sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==
vscode-nls@^5.0.0:
node-fetch@^2.6.7:
version "2.6.8"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.8.tgz#a68d30b162bc1d8fd71a367e81b997e1f4d4937e"
integrity sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==
dependencies:
whatwg-url "^5.0.0"
once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
tunnel@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
universal-user-agent@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"
integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840"
integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA==
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
-2
View File
@@ -1,2 +0,0 @@
test/**
cgmanifest.json
-18
View File
@@ -1,18 +0,0 @@
{
"registrations": [
{
"component": {
"type": "git",
"git": {
"name": "dotnet/csharp-tmLanguage",
"repositoryUrl": "https://github.com/dotnet/csharp-tmLanguage",
"commitHash": "16612717ccd557383c0c821d7b6ae6662492ffde"
}
},
"license": "MIT",
"version": "0.1.0",
"description": "The file syntaxes/csharp.tmLanguage.json was derived from https://github.com/dotnet/csharp-tmLanguage"
}
],
"version": 1
}
@@ -1,32 +0,0 @@
{
"comments": {
"lineComment": "//",
"blockComment": ["/*", "*/"]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
{ "open": "\"", "close": "\"", "notIn": ["string", "comment"] }
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["<", ">"],
["'", "'"],
["\"", "\""]
],
"folding": {
"markers": {
"start": "^\\s*#region\\b",
"end": "^\\s*#endregion\\b"
}
}
}
-48
View File
@@ -1,48 +0,0 @@
{
"name": "csharp",
"displayName": "%displayName%",
"description": "%description%",
"version": "1.0.0",
"publisher": "vscode",
"license": "MIT",
"engines": {
"vscode": "0.10.x"
},
"scripts": {
"update-grammar": "node ../node_modules/vscode-grammar-updater/bin dotnet/csharp-tmLanguage grammars/csharp.tmLanguage ./syntaxes/csharp.tmLanguage.json"
},
"contributes": {
"languages": [
{
"id": "csharp",
"extensions": [
".cs",
".csx",
".cake"
],
"aliases": [
"C#",
"csharp"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "csharp",
"scopeName": "source.cs",
"path": "./syntaxes/csharp.tmLanguage.json"
}
],
"snippets": [
{
"language": "csharp",
"path": "./snippets/csharp.code-snippets"
}
]
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/vscode.git"
}
}
-4
View File
@@ -1,4 +0,0 @@
{
"displayName": "C# Language Basics",
"description": "Provides snippets, syntax highlighting, bracket matching and folding in C# files."
}
@@ -1,16 +0,0 @@
{
"Region Start": {
"prefix": "#region",
"body": [
"#region $0"
],
"description": "Folding Region Start"
},
"Region End": {
"prefix": "#endregion",
"body": [
"#endregion"
],
"description": "Folding Region End"
}
}
File diff suppressed because it is too large Load Diff
@@ -1,7 +0,0 @@
{
"rules": {
"@typescript-eslint/explicit-function-return-type": ["off"],
"@typescript-eslint/await-thenable": ["off"],
"@typescript-eslint/no-unsafe-assignment": "off"
}
}
@@ -1,108 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, Connection, Diagnostic, Disposable, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportKind, TextDocuments } from 'vscode-languageserver';
import { TextDocument } from 'vscode-css-languageservice';
import { formatError, runSafeAsync } from './runner';
import { RuntimeEnvironment } from '../cssServer';
export type Validator = (textDocument: TextDocument) => Promise<Diagnostic[]>;
export type DiagnosticsSupport = {
dispose(): void;
requestRefresh(): void;
};
export function registerDiagnosticsPushSupport(documents: TextDocuments<TextDocument>, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport {
const pendingValidationRequests: { [uri: string]: Disposable } = {};
const validationDelayMs = 500;
const disposables: Disposable[] = [];
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
triggerValidation(change.document);
}, undefined, disposables);
// a document has closed: clear all diagnostics
documents.onDidClose(event => {
cleanPendingValidation(event.document);
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
}, undefined, disposables);
function cleanPendingValidation(textDocument: TextDocument): void {
const request = pendingValidationRequests[textDocument.uri];
if (request) {
request.dispose();
delete pendingValidationRequests[textDocument.uri];
}
}
function triggerValidation(textDocument: TextDocument): void {
cleanPendingValidation(textDocument);
const request = pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(async () => {
if (request === pendingValidationRequests[textDocument.uri]) {
try {
const diagnostics = await validate(textDocument);
if (request === pendingValidationRequests[textDocument.uri]) {
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
delete pendingValidationRequests[textDocument.uri];
} catch (e) {
connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e));
}
}
}, validationDelayMs);
}
return {
requestRefresh: () => {
documents.all().forEach(triggerValidation);
},
dispose: () => {
disposables.forEach(d => d.dispose());
disposables.length = 0;
const keys = Object.keys(pendingValidationRequests);
for (const key of keys) {
pendingValidationRequests[key].dispose();
delete pendingValidationRequests[key];
}
}
};
}
export function registerDiagnosticsPullSupport(documents: TextDocuments<TextDocument>, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport {
function newDocumentDiagnosticReport(diagnostics: Diagnostic[]): DocumentDiagnosticReport {
return {
kind: DocumentDiagnosticReportKind.Full,
items: diagnostics
};
}
const registration = connection.languages.diagnostics.on(async (params: DocumentDiagnosticParams, token: CancellationToken) => {
return runSafeAsync(runtime, async () => {
const document = documents.get(params.textDocument.uri);
if (document) {
return newDocumentDiagnosticReport(await validate(document));
}
return newDocumentDiagnosticReport([]);
}, newDocumentDiagnosticReport([]), `Error while computing diagnostics for ${params.textDocument.uri}`, token);
});
function requestRefresh(): void {
connection.languages.diagnostics.refresh();
}
return {
requestRefresh,
dispose: () => {
registration.dispose();
}
};
}
-2
View File
@@ -1,2 +0,0 @@
build/**
cgmanifest.json
-46
View File
@@ -1,46 +0,0 @@
{
"registrations": [
{
"component": {
"type": "git",
"git": {
"name": "dart-lang/dart-syntax-highlight",
"repositoryUrl": "https://github.com/dart-lang/dart-syntax-highlight",
"commitHash": "9d4857e114b7000d94232d83187ad142961c678a"
}
},
"licenseDetail": [
"Copyright 2020, the Dart project authors.",
"",
"Redistribution and use in source and binary forms, with or without",
"modification, are permitted provided that the following conditions are",
"met:",
"",
" * Redistributions of source code must retain the above copyright",
" notice, this list of conditions and the following disclaimer.",
" * Redistributions in binary form must reproduce the above",
" copyright notice, this list of conditions and the following",
" disclaimer in the documentation and/or other materials provided",
" with the distribution.",
" * Neither the name of Google LLC nor the names of its",
" contributors may be used to endorse or promote products derived",
" from this software without specific prior written permission.",
"",
"THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS",
"\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT",
"LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR",
"A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT",
"OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,",
"SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT",
"LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,",
"DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY",
"THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT",
"(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE",
"OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
],
"license": "BSD",
"version": "0.0.0"
}
],
"version": 1
}
@@ -1,29 +0,0 @@
{
"comments": {
"lineComment": "//",
"blockComment": [ "/*", "*/" ]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "`", "close": "`", "notIn": ["string", "comment"] },
{ "open": "/**", "close": " */", "notIn": ["string"] }
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["<", ">"],
["'", "'"],
["\"", "\""],
["`", "`"]
]
}
-35
View File
@@ -1,35 +0,0 @@
{
"name": "dart",
"displayName": "%displayName%",
"description": "%description%",
"version": "1.0.0",
"publisher": "vscode",
"license": "MIT",
"engines": {
"vscode": "0.10.x"
},
"scripts": {
"update-grammar": "node ../node_modules/vscode-grammar-updater/bin dart-lang/dart-syntax-highlight grammars/dart.json ./syntaxes/dart.tmLanguage.json"
},
"contributes": {
"languages": [
{
"id": "dart",
"extensions": [
".dart"
],
"aliases": [
"Dart"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "dart",
"scopeName": "source.dart",
"path": "./syntaxes/dart.tmLanguage.json"
}
]
}
}
-4
View File
@@ -1,4 +0,0 @@
{
"displayName": "Dart Language Basics",
"description": "Provides syntax highlighting & bracket matching in Dart files."
}
@@ -1,516 +0,0 @@
{
"information_for_contributors": [
"This file has been converted from https://github.com/dart-lang/dart-syntax-highlight/blob/master/grammars/dart.json",
"If you want to provide a fix or improvement, please create a pull request against the original repository.",
"Once accepted there, we are happy to receive an update request."
],
"version": "https://github.com/dart-lang/dart-syntax-highlight/commit/9d4857e114b7000d94232d83187ad142961c678a",
"name": "Dart",
"scopeName": "source.dart",
"patterns": [
{
"name": "meta.preprocessor.script.dart",
"match": "^(#!.*)$"
},
{
"name": "meta.declaration.dart",
"begin": "^\\w*\\b(library|import|part of|part|export)\\b",
"beginCaptures": {
"0": {
"name": "keyword.other.import.dart"
}
},
"end": ";",
"endCaptures": {
"0": {
"name": "punctuation.terminator.dart"
}
},
"patterns": [
{
"include": "#strings"
},
{
"include": "#comments"
},
{
"name": "keyword.other.import.dart",
"match": "\\b(as|show|hide)\\b"
},
{
"name": "keyword.control.dart",
"match": "\\b(if)\\b"
}
]
},
{
"include": "#comments"
},
{
"include": "#punctuation"
},
{
"include": "#annotations"
},
{
"include": "#keywords"
},
{
"include": "#constants-and-special-vars"
},
{
"include": "#strings"
}
],
"repository": {
"dartdoc": {
"patterns": [
{
"match": "(\\[.*?\\])",
"captures": {
"0": {
"name": "variable.name.source.dart"
}
}
},
{
"match": "^ {4,}(?![ \\*]).*",
"captures": {
"0": {
"name": "variable.name.source.dart"
}
}
},
{
"contentName": "variable.other.source.dart",
"begin": "```.*?$",
"end": "```"
},
{
"match": "(`.*?`)",
"captures": {
"0": {
"name": "variable.other.source.dart"
}
}
},
{
"match": "(`.*?`)",
"captures": {
"0": {
"name": "variable.other.source.dart"
}
}
},
{
"match": "(\\* (( ).*))$",
"captures": {
"2": {
"name": "variable.other.source.dart"
}
}
}
]
},
"comments": {
"patterns": [
{
"name": "comment.block.empty.dart",
"match": "/\\*\\*/",
"captures": {
"0": {
"name": "punctuation.definition.comment.dart"
}
}
},
{
"include": "#comments-doc-oldschool"
},
{
"include": "#comments-doc"
},
{
"include": "#comments-inline"
}
]
},
"comments-doc-oldschool": {
"patterns": [
{
"name": "comment.block.documentation.dart",
"begin": "/\\*\\*",
"end": "\\*/",
"patterns": [
{
"include": "#comments-doc-oldschool"
},
{
"include": "#comments-block"
},
{
"include": "#dartdoc"
}
]
}
]
},
"comments-doc": {
"patterns": [
{
"name": "comment.block.documentation.dart",
"begin": "///",
"while": "^\\s*///",
"patterns": [
{
"include": "#dartdoc"
}
]
}
]
},
"comments-inline": {
"patterns": [
{
"include": "#comments-block"
},
{
"match": "((//).*)$",
"captures": {
"1": {
"name": "comment.line.double-slash.dart"
}
}
}
]
},
"comments-block": {
"patterns": [
{
"name": "comment.block.dart",
"begin": "/\\*",
"end": "\\*/",
"patterns": [
{
"include": "#comments-block"
}
]
}
]
},
"annotations": {
"patterns": [
{
"name": "storage.type.annotation.dart",
"match": "@[a-zA-Z]+"
}
]
},
"constants-and-special-vars": {
"patterns": [
{
"name": "constant.language.dart",
"match": "(?<!\\$)\\b(true|false|null)\\b(?!\\$)"
},
{
"name": "variable.language.dart",
"match": "(?<!\\$)\\b(this|super)\\b(?!\\$)"
},
{
"name": "constant.numeric.dart",
"match": "(?<!\\$)\\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\\.?[0-9]*)|(\\.[0-9]+))((e|E)(\\+|-)?[0-9]+)?)\\b(?!\\$)"
},
{
"include": "#class-identifier"
},
{
"include": "#function-identifier"
}
]
},
"class-identifier": {
"patterns": [
{
"match": "(?<![a-zA-Z0-9_$])([_$]*[A-Z][a-zA-Z0-9_$]*(<(?:[a-zA-Z0-9_$<>?]|,\\s*|\\s+extends\\s+)+>)?|bool\\b|num\\b|int\\b|double\\b|dynamic\\b|(void)\\b)",
"captures": {
"1": {
"name": "support.class.dart"
},
"2": {
"patterns": [
{
"include": "#type-args"
}
]
},
"3": {
"name": "storage.type.primitive.dart"
}
}
}
]
},
"function-identifier": {
"patterns": [
{
"match": "([_$]*[a-z][a-zA-Z0-9_$]*)(<(?:[a-zA-Z0-9_$<>?]|,\\s*|\\s+extends\\s+)+>)?[!?]?(\\(|\\s+=>)",
"captures": {
"1": {
"name": "entity.name.function.dart"
},
"2": {
"patterns": [
{
"include": "#type-args"
}
]
}
}
}
]
},
"type-args": {
"begin": "(<)",
"end": "(>)",
"beginCaptures": {
"1": {
"name": "other.source.dart"
}
},
"endCaptures": {
"1": {
"name": "other.source.dart"
}
},
"patterns": [
{
"include": "#class-identifier"
},
{
"match": "[\\s,]+"
},
{
"name": "keyword.declaration.dart",
"match": "extends"
}
]
},
"keywords": {
"patterns": [
{
"name": "keyword.cast.dart",
"match": "(?<!\\$)\\bas\\b(?!\\$)"
},
{
"name": "keyword.control.catch-exception.dart",
"match": "(?<!\\$)\\b(try|on|catch|finally|throw|rethrow)\\b(?!\\$)"
},
{
"name": "keyword.control.dart",
"match": "(?<!\\$)\\b(break|case|continue|default|do|else|for|if|in|return|switch|while)\\b(?!\\$)"
},
{
"name": "keyword.control.dart",
"match": "(?<!\\$)\\b(sync(\\*)?|async(\\*)?|await|yield(\\*)?)\\b(?!\\$)"
},
{
"name": "keyword.control.dart",
"match": "(?<!\\$)\\bassert\\b(?!\\$)"
},
{
"name": "keyword.control.new.dart",
"match": "(?<!\\$)\\b(new)\\b(?!\\$)"
},
{
"name": "keyword.declaration.dart",
"match": "(?<!\\$)\\b(abstract|class|enum|extends|extension|external|factory|implements|get|mixin|native|operator|set|typedef|with|covariant)\\b(?!\\$)"
},
{
"name": "keyword.operator.dart",
"match": "(?<!\\$)\\b(is\\!?)\\b(?!\\$)"
},
{
"name": "keyword.operator.ternary.dart",
"match": "\\?|:"
},
{
"name": "keyword.operator.bitwise.dart",
"match": "(<<|>>>?|~|\\^|\\||&)"
},
{
"name": "keyword.operator.assignment.bitwise.dart",
"match": "((&|\\^|\\||<<|>>>?)=)"
},
{
"name": "keyword.operator.closure.dart",
"match": "(=>)"
},
{
"name": "keyword.operator.comparison.dart",
"match": "(==|!=|<=?|>=?)"
},
{
"name": "keyword.operator.assignment.arithmetic.dart",
"match": "(([+*/%-]|\\~)=)"
},
{
"name": "keyword.operator.assignment.dart",
"match": "(=)"
},
{
"name": "keyword.operator.increment-decrement.dart",
"match": "(\\-\\-|\\+\\+)"
},
{
"name": "keyword.operator.arithmetic.dart",
"match": "(\\-|\\+|\\*|\\/|\\~\\/|%)"
},
{
"name": "keyword.operator.logical.dart",
"match": "(!|&&|\\|\\|)"
},
{
"name": "storage.modifier.dart",
"match": "(?<!\\$)\\b(static|final|const|required|late)\\b(?!\\$)"
},
{
"name": "storage.type.primitive.dart",
"match": "(?<!\\$)\\b(?:void|var)\\b(?!\\$)"
}
]
},
"string-interp": {
"patterns": [
{
"match": "\\$([a-zA-Z0-9_]+)",
"captures": {
"1": {
"name": "variable.parameter.dart"
}
}
},
{
"name": "string.interpolated.expression.dart",
"begin": "\\$\\{",
"end": "\\}",
"patterns": [
{
"include": "#constants-and-special-vars",
"name": "variable.parameter.dart"
},
{
"include": "#strings"
},
{
"name": "variable.parameter.dart",
"match": "[a-zA-Z0-9_]+"
}
]
},
{
"name": "constant.character.escape.dart",
"match": "\\\\."
}
]
},
"strings": {
"patterns": [
{
"name": "string.interpolated.triple.double.dart",
"begin": "(?<!r)\"\"\"",
"end": "\"\"\"(?!\")",
"patterns": [
{
"include": "#string-interp"
}
]
},
{
"name": "string.interpolated.triple.single.dart",
"begin": "(?<!r)'''",
"end": "'''(?!')",
"patterns": [
{
"include": "#string-interp"
}
]
},
{
"name": "string.quoted.triple.double.dart",
"begin": "r\"\"\"",
"end": "\"\"\"(?!\")"
},
{
"name": "string.quoted.triple.single.dart",
"begin": "r'''",
"end": "'''(?!')"
},
{
"name": "string.interpolated.double.dart",
"begin": "(?<!\\|r)\"",
"end": "\"",
"patterns": [
{
"name": "invalid.string.newline",
"match": "\\n"
},
{
"include": "#string-interp"
}
]
},
{
"name": "string.quoted.double.dart",
"begin": "r\"",
"end": "\"",
"patterns": [
{
"name": "invalid.string.newline",
"match": "\\n"
}
]
},
{
"name": "string.interpolated.single.dart",
"begin": "(?<!\\|r)'",
"end": "'",
"patterns": [
{
"name": "invalid.string.newline",
"match": "\\n"
},
{
"include": "#string-interp"
}
]
},
{
"name": "string.quoted.single.dart",
"begin": "r'",
"end": "'",
"patterns": [
{
"name": "invalid.string.newline",
"match": "\\n"
}
]
}
]
},
"punctuation": {
"patterns": [
{
"name": "punctuation.comma.dart",
"match": ","
},
{
"name": "punctuation.terminator.dart",
"match": ";"
},
{
"name": "punctuation.dot.dart",
"match": "\\."
}
]
}
}
}
-2
View File
@@ -1,2 +0,0 @@
test/**
cgmanifest.json
-18
View File
@@ -1,18 +0,0 @@
{
"registrations": [
{
"component": {
"type": "git",
"git": {
"name": "ionide/ionide-fsgrammar",
"repositoryUrl": "https://github.com/ionide/ionide-fsgrammar",
"commitHash": "e177bd7f9d3402f70d2f1fb42c74057ed1ccf6fa"
}
},
"license": "MIT",
"description": "The file syntaxes/fsharp.json was included from https://github.com/ionide/ionide-fsgrammar/blob/master/grammar/fsharp.json.",
"version": "0.0.0"
}
],
"version": 1
}
@@ -1,31 +0,0 @@
{
"comments": {
"lineComment": "//",
"blockComment": [ "(*", "*)" ]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"", "notIn": ["string"] }
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
],
"folding": {
"offSide": true,
"markers": {
"start": "^\\s*//\\s*#region\\b|^\\s*\\(\\*\\s*#region(.*)\\*\\)",
"end": "^\\s*//\\s*#endregion\\b|^\\s*\\(\\*\\s*#endregion\\s*\\*\\)"
}
}
}
-50
View File
@@ -1,50 +0,0 @@
{
"name": "fsharp",
"displayName": "%displayName%",
"description": "%description%",
"version": "1.0.0",
"publisher": "vscode",
"license": "MIT",
"engines": {
"vscode": "*"
},
"scripts": {
"update-grammar": "node ../node_modules/vscode-grammar-updater/bin ionide/ionide-fsgrammar grammars/fsharp.json ./syntaxes/fsharp.tmLanguage.json"
},
"contributes": {
"languages": [
{
"id": "fsharp",
"extensions": [
".fs",
".fsi",
".fsx",
".fsscript"
],
"aliases": [
"F#",
"FSharp",
"fsharp"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "fsharp",
"scopeName": "source.fsharp",
"path": "./syntaxes/fsharp.tmLanguage.json"
}
],
"snippets": [
{
"language": "fsharp",
"path": "./snippets/fsharp.code-snippets"
}
]
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/vscode.git"
}
}
-4
View File
@@ -1,4 +0,0 @@
{
"displayName": "F# Language Basics",
"description": "Provides snippets, syntax highlighting, bracket matching and folding in F# files."
}
@@ -1,16 +0,0 @@
{
"Region Start": {
"prefix": "#region",
"body": [
"//#region $0"
],
"description": "Folding Region Start"
},
"Region End": {
"prefix": "#endregion",
"body": [
"//#endregion"
],
"description": "Folding Region End"
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -6,5 +6,5 @@
var updateGrammar = require('vscode-grammar-updater');
updateGrammar.update('textmate/git.tmbundle', 'Syntaxes/Git%20Commit%20Message.tmLanguage', './syntaxes/git-commit.tmLanguage.json');
updateGrammar.update('walles/git-commit-message-plus', 'syntaxes/git-commit.tmLanguage.json', './syntaxes/git-commit.tmLanguage.json', undefined, 'main');
updateGrammar.update('textmate/git.tmbundle', 'Syntaxes/Git%20Rebase%20Message.tmLanguage', './syntaxes/git-rebase.tmLanguage.json');
+35
View File
@@ -33,6 +33,41 @@
],
"license": "MIT",
"version": "0.0.0"
},
{
"component": {
"type": "git",
"git": {
"name": "walles/git-commit-message-plus",
"repositoryUrl": "https://github.com/walles/git-commit-message-plus",
"commitHash": "35a079dea5a91b087021b40c01a6bb4eb0337a87"
}
},
"licenseDetail": [
"Copyright (c) 2023 Johan Walles <johan.walles@gmail.com>",
"",
"Permission is hereby granted, free of charge, to any person obtaining",
"a copy of this software and associated documentation files (the\"",
"Software\"), to deal in the Software without restriction, including",
"without limitation the rights to use, copy, modify, merge, publish,",
"distribute, sublicense, and/or sell copies of the Software, and to",
"permit persons to whom the Software is furnished to do so, subject to",
"the following conditions:",
"",
"The above copyright notice and this permission notice shall be",
"included in all copies or substantial portions of the Software.",
"",
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,",
"EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF",
"MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND",
"NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE",
"LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION",
"OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION",
"WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
],
"license": "MIT",
"version": "1.0.0",
"description": "The original JSON grammars were derived from https://github.com/microsoft/vscode/blob/e95c74c4c7af876e79ec58df262464467c06df28/extensions/git-base/syntaxes/git-commit.tmLanguage.json. That file was originally copied from https://github.com/textmate/git.tmbundle."
}
],
"version": 1
+5 -5
View File
@@ -6,9 +6,9 @@
"publisher": "vscode",
"license": "MIT",
"engines": {
"vscode": "0.10.x"
"vscode": "0.10.0"
},
"categories": [
"categories": [
"Other"
],
"activationEvents": [
@@ -66,6 +66,9 @@
"filenames": [
"git-rebase-todo"
],
"filenamePatterns": [
"**/rebase-merge/done"
],
"configuration": "./languages/git-rebase.language-configuration.json"
},
{
@@ -99,9 +102,6 @@
}
]
},
"dependencies": {
"vscode-nls": "^5.0.0"
},
"devDependencies": {
"@types/node": "16.x"
},
+6 -2
View File
@@ -5,9 +5,9 @@
import { Disposable, commands } from 'vscode';
import { Model } from '../model';
import { pickRemoteSource } from '../remoteSource';
import { getRemoteSourceActions, pickRemoteSource } from '../remoteSource';
import { GitBaseExtensionImpl } from './extension';
import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceProvider } from './git-base';
import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction, RemoteSourceProvider } from './git-base';
export class ApiImpl implements API {
@@ -17,6 +17,10 @@ export class ApiImpl implements API {
return pickRemoteSource(this._model, options as any);
}
getRemoteSourceActions(url: string): Promise<RemoteSourceAction[]> {
return getRemoteSourceActions(this._model, url);
}
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable {
return this._model.registerRemoteSourceProvider(provider);
}
+10
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[]>;
}
+27 -17
View File
@@ -3,14 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { QuickPickItem, window, QuickPick, QuickPickItemKind } from 'vscode';
import * as nls from 'vscode-nls';
import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult } from './api/git-base';
import { QuickPickItem, window, QuickPick, QuickPickItemKind, l10n } from 'vscode';
import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction } from './api/git-base';
import { Model } from './model';
import { throttle, debounce } from './decorators';
const localize = nls.loadMessageBundle();
async function getQuickPickResult<T extends QuickPickItem>(quickpick: QuickPick<T>): Promise<T | undefined> {
const result = await new Promise<T | undefined>(c => {
quickpick.onDidAccept(() => c(quickpick.selectedItems[0]));
@@ -33,10 +30,10 @@ class RemoteSourceProviderQuickPick {
this.quickpick = window.createQuickPick();
this.quickpick.ignoreFocusOut = true;
if (this.provider.supportsQuery) {
this.quickpick.placeholder = this.provider.placeholder ?? localize('type to search', "Repository name (type to search)");
this.quickpick.placeholder = this.provider.placeholder ?? l10n.t('Repository name (type to search)');
this.quickpick.onDidChangeValue(this.onDidChangeValue, this);
} else {
this.quickpick.placeholder = this.provider.placeholder ?? localize('type to filter', "Repository name");
this.quickpick.placeholder = this.provider.placeholder ?? l10n.t('Repository name');
}
}
}
@@ -57,7 +54,7 @@ class RemoteSourceProviderQuickPick {
if (remoteSources.length === 0) {
this.quickpick!.items = [{
label: localize('none found', "No remote repositories found."),
label: l10n.t('No remote repositories found.'),
alwaysShow: true
}];
} else {
@@ -70,7 +67,7 @@ class RemoteSourceProviderQuickPick {
}));
}
} catch (err) {
this.quickpick!.items = [{ label: localize('error', "{0} Error: {1}", '$(error)', err.message), alwaysShow: true }];
this.quickpick!.items = [{ label: l10n.t('{0} Error: {1}', '$(error)', err.message), alwaysShow: true }];
console.error(err);
} finally {
this.quickpick!.busy = false;
@@ -84,11 +81,24 @@ class RemoteSourceProviderQuickPick {
}
}
export async function getRemoteSourceActions(model: Model, url: string): Promise<RemoteSourceAction[]> {
const providers = model.getRemoteProviders();
const remoteSourceActions = [];
for (const provider of providers) {
const providerActions = await provider.getRemoteSourceActions?.(url);
if (providerActions?.length) {
remoteSourceActions.push(...providerActions);
}
}
return remoteSourceActions;
}
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise<string | undefined>;
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch: true }): Promise<PickRemoteSourceResult | undefined>;
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {
const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider; url?: string })>();
quickpick.ignoreFocusOut = true;
quickpick.title = options.title;
if (options.providerName) {
@@ -118,19 +128,19 @@ export async function pickRemoteSource(model: Model, options: PickRemoteSourceOp
}
const items = [
{ kind: QuickPickItemKind.Separator, label: localize('remote sources', 'remote sources') },
{ kind: QuickPickItemKind.Separator, label: l10n.t('remote sources') },
...remoteProviders,
{ kind: QuickPickItemKind.Separator, label: localize('recently opened', 'recently opened') },
{ kind: QuickPickItemKind.Separator, label: l10n.t('recently opened') },
...recentSources.sort((a, b) => b.timestamp - a.timestamp)
];
quickpick.placeholder = options.placeholder ?? (remoteProviders.length === 0
? localize('provide url', "Provide repository URL")
: localize('provide url or pick', "Provide repository URL or pick a repository source."));
? l10n.t('Provide repository URL')
: l10n.t('Provide repository URL or pick a repository source.'));
const updatePicks = (value?: string) => {
if (value) {
const label = (typeof options.urlLabel === 'string' ? options.urlLabel : options.urlLabel?.(value)) ?? localize('url', "URL");
const label = (typeof options.urlLabel === 'string' ? options.urlLabel : options.urlLabel?.(value)) ?? l10n.t('URL');
quickpick.items = [{
label: label,
description: value,
@@ -170,7 +180,7 @@ async function pickProviderSource(provider: RemoteSourceProvider, options: PickR
if (typeof remote.url === 'string') {
url = remote.url;
} else if (remote.url.length > 0) {
url = await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: localize('pick url', "Choose a URL to clone from.") });
url = await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: l10n.t('Choose a URL to clone from.') });
}
}
@@ -189,7 +199,7 @@ async function pickProviderSource(provider: RemoteSourceProvider, options: PickR
}
const branch = await window.showQuickPick(branches, {
placeHolder: localize('branch name', "Branch name")
placeHolder: l10n.t('Branch name')
});
if (!branch) {
@@ -1,141 +1,90 @@
{
"information_for_contributors": [
"This file has been converted from https://github.com/textmate/git.tmbundle/blob/master/Syntaxes/Git%20Commit%20Message.tmLanguage",
"This file has been converted from https://github.com/walles/git-commit-message-plus/blob/master/syntaxes/git-commit.tmLanguage.json",
"If you want to provide a fix or improvement, please create a pull request against the original repository.",
"Once accepted there, we are happy to receive an update request."
],
"version": "https://github.com/textmate/git.tmbundle/commit/93897a78c6e52bef13dadc0d4091d203c5facb40",
"version": "https://github.com/walles/git-commit-message-plus/commit/35a079dea5a91b087021b40c01a6bb4eb0337a87",
"name": "Git Commit Message",
"scopeName": "text.git-commit",
"patterns": [
{
"begin": "\\A(?!# Please enter the commit message)",
"end": "^(?=# Please enter the commit message)",
"name": "meta.scope.message.git-commit",
"comment": "diff presented at the end of the commit message when using commit -v.",
"name": "meta.embedded.diff.git-commit",
"contentName": "source.diff",
"begin": "(?=^diff\\ \\-\\-git)",
"end": "\\z",
"patterns": [
{
"begin": "\\A(?=#)",
"end": "^(?!#)",
"patterns": [
{
"include": "#comment"
}
]
},
{
"begin": "^(?!# Please enter the commit message)",
"end": "^(?=# Please enter the commit message)",
"patterns": [
{
"begin": "\\G",
"end": "^(?!\\G)",
"name": "meta.scope.subject.git-commit",
"patterns": [
{
"captures": {
"1": {
"name": "keyword.other.$2.git-commit"
}
},
"match": "\\G((fixup|squash)!)\\s*"
},
{
"match": ".{73,}$",
"name": "invalid.illegal.line-too-long.git-commit"
},
{
"match": ".{51,}$",
"name": "invalid.deprecated.line-too-long.git-commit"
}
]
},
{
"begin": "^(?!# Please enter the commit message)",
"end": "^(?=# Please enter the commit message)",
"patterns": [
{
"include": "#comment"
}
]
}
]
"include": "source.diff"
}
]
},
{
"begin": "^(?=# Please enter the commit message)",
"end": "\\z",
"name": "meta.scope.metadata.git-commit",
"comment": "User supplied message",
"name": "meta.scope.message.git-commit",
"begin": "^(?!#)",
"end": "^(?=#)",
"patterns": [
{
"include": "#metadata"
"comment": "Mark > 50 lines as deprecated, > 72 as illegal",
"name": "meta.scope.subject.git-commit",
"match": "\\G.{0,50}(.{0,22}(.*))$",
"captures": {
"1": {
"name": "invalid.deprecated.line-too-long.git-commit"
},
"2": {
"name": "invalid.illegal.line-too-long.git-commit"
}
}
}
]
}
],
"repository": {
"comment": {
"begin": "^(#)",
"captures": {
"1": {
"name": "punctuation.definition.comment.git-commit"
}
},
"end": "\\n",
"name": "comment.line.number-sign.git-commit"
},
"metadata": {
{
"comment": "Git supplied metadata in a number of lines starting with #",
"name": "meta.scope.metadata.git-commit",
"begin": "^(?=#)",
"contentName": "comment.line.number-sign.git-commit",
"end": "^(?!#)",
"patterns": [
{
"begin": "(?=^# Changes to be committed:)",
"end": "(?!\\G)((?=^# \\w)|(?!^#))",
"patterns": [
{
"begin": "(^[ \\t]+)?(?=#)",
"beginCaptures": {
"1": {
"name": "punctuation.whitespace.comment.leading.git-commit"
}
},
"contentName": "comment.line.number-sign.git-commit",
"end": "(?!\\G)^",
"patterns": [
{
"match": "\\G#",
"name": "punctuation.definition.comment.git-commit"
},
{
"match": "((modified|renamed):.*)$\\n?",
"name": "markup.changed.git-commit"
},
{
"match": "(new file:.*)$\\n?",
"name": "markup.inserted.git-commit"
},
{
"match": "(deleted:.*)$\\n?",
"name": "markup.deleted.git-commit"
}
]
"match": "^#\\t((modified|renamed):.*)$",
"captures": {
"1": {
"name": "markup.changed.git-commit"
}
]
}
},
{
"include": "#comment"
"match": "^#\\t(new file:.*)$",
"captures": {
"1": {
"name": "markup.inserted.git-commit"
}
}
},
{
"begin": "(?=diff\\ \\-\\-git)",
"comment": "diff presented at the end of the commit message when using commit -v.",
"contentName": "source.diff",
"end": "\\z",
"name": "meta.embedded.diff.git-commit",
"patterns": [
{
"include": "source.diff"
"match": "^#\\t(deleted.*)$",
"captures": {
"1": {
"name": "markup.deleted.git-commit"
}
]
}
},
{
"comment": "Fallback for non-English git commit template",
"match": "^#\\t([^:]+): *(.*)$",
"captures": {
"1": {
"name": "keyword.other.file-type.git-commit"
},
"2": {
"name": "string.unquoted.filename.git-commit"
}
}
}
]
}
}
]
}
-5
View File
@@ -6,8 +6,3 @@
version "16.11.21"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.21.tgz#474d7589a30afcf5291f59bd49cca9ad171ffde4"
integrity sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A==
vscode-nls@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840"
integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA==
File diff suppressed because it is too large Load Diff
+71 -24
View File
@@ -1,7 +1,8 @@
{
"displayName": "Git",
"description": "Git SCM Integration",
"command.setLogLevel": "Set Log Level...",
"command.continueInLocalClone": "Clone Repository Locally and Open on Desktop...",
"command.continueInLocalClone.qualifiedName": "Continue Working in New Local Clone",
"command.clone": "Clone",
"command.cloneRecursive": "Clone (Recursive)",
"command.init": "Initialize Repository",
@@ -58,9 +59,11 @@
"command.renameBranch": "Rename Branch...",
"command.cherryPick": "Cherry Pick...",
"command.merge": "Merge Branch...",
"command.mergeAbort": "Abort Merge",
"command.rebase": "Rebase Branch...",
"command.createTag": "Create Tag",
"command.deleteTag": "Delete Tag",
"command.deleteRemoteTag": "Delete Remote Tag",
"command.fetch": "Fetch",
"command.fetchPrune": "Fetch (Prune)",
"command.fetchAll": "Fetch From All Remotes",
@@ -88,6 +91,7 @@
"command.rebaseAbort": "Abort Rebase",
"command.stashIncludeUntracked": "Stash (Include Untracked)",
"command.stash": "Stash",
"command.stashStaged": "Stash Staged",
"command.stashPop": "Pop Stash...",
"command.stashPopLatest": "Pop Latest Stash",
"command.stashApply": "Apply Stash...",
@@ -99,10 +103,15 @@
"command.timelineCopyCommitMessage": "Copy Commit Message",
"command.timelineSelectForCompare": "Select for Compare",
"command.timelineCompareWithSelected": "Compare with Selected",
"command.manageUnsafeRepositories": "Manage Unsafe Repositories",
"command.openRepositoriesInParentFolders": "Open Repositories In Parent Folders",
"command.api.getRepositories": "Get Repositories",
"command.api.getRepositoryState": "Get Repository State",
"command.api.getRemoteSources": "Get Remote Sources",
"command.git.acceptMerge": "Accept Merge",
"command.git.acceptMerge": "Complete Merge",
"command.git.openMergeEditor": "Resolve in Merge Editor",
"command.git.runGitMerge": "Compute Conflicts With Git",
"command.git.runGitMergeDiff3": "Compute Conflicts With Git (Diff3)",
"config.enabled": "Whether git is enabled.",
"config.path": "Path and filename of the git executable, e.g. `C:\\Program Files\\Git\\bin\\git.exe` (Windows). This can also be an array of string values containing multiple paths to look up.",
"config.autoRepositoryDetection": "Configures when repositories should be automatically detected.",
@@ -122,9 +131,10 @@
"config.checkoutType.local": "Local branches",
"config.checkoutType.tags": "Tags",
"config.checkoutType.remote": "Remote branches",
"config.defaultBranchName": "The name of the default branch (ex: main, trunk, development) when initializing a new git repository. When set to empty, the default branch name configured in git will be used.",
"config.branchPrefix": "Prefix used when creating a new branch.",
"config.branchProtection": "List of protected branches. By default, a prompt is shown before changes are committed to a protected branch. The prompt can be controlled using the `#git.branchProtectionPrompt#` setting.",
"config.branchProtectionPrompt": "Controls whether a prompt is being before changes are committed to a protected branch.",
"config.branchProtectionPrompt": "Controls whether a prompt is being shown before changes are committed to a protected branch.",
"config.branchProtectionPrompt.alwaysCommit": "Always commit changes to the protected branch.",
"config.branchProtectionPrompt.alwaysCommitToNewBranch": "Always commit changes to a new branch.",
"config.branchProtectionPrompt.alwaysPrompt": "Always prompt before changes are committed to a protected branch.",
@@ -153,7 +163,7 @@
"config.discardAllScope": "Controls what changes are discarded by the `Discard all changes` command. `all` discards all changes. `tracked` discards only tracked files. `prompt` shows a prompt dialog every time the action is run.",
"config.decorations.enabled": "Controls whether Git contributes colors and badges to the Explorer and the Open Editors view.",
"config.enableStatusBarSync": "Controls whether the Git Sync command appears in the status bar.",
"config.followTagsWhenSync": "Follow push all tags when running the sync command.",
"config.followTagsWhenSync": "Push all annotated tags when running the sync command.",
"config.promptToSaveFilesBeforeStash": "Controls whether Git should check for unsaved files before stashing changes.",
"config.promptToSaveFilesBeforeStash.always": "Check for any unsaved files.",
"config.promptToSaveFilesBeforeStash.staged": "Check only for unsaved staged files.",
@@ -162,10 +172,11 @@
"config.promptToSaveFilesBeforeCommit.always": "Check for any unsaved files.",
"config.promptToSaveFilesBeforeCommit.staged": "Check only for unsaved staged files.",
"config.promptToSaveFilesBeforeCommit.never": "Disable this check.",
"config.postCommitCommand": "Runs a git command after a successful commit.",
"config.postCommitCommand": "Run a git command after a successful commit.",
"config.postCommitCommand.none": "Don't run any command after a commit.",
"config.postCommitCommand.push": "Run 'Git Push' after a successful commit.",
"config.postCommitCommand.sync": "Run 'Git Sync' after a successful commit.",
"config.postCommitCommand.push": "Run 'git push' after a successful commit.",
"config.postCommitCommand.sync": "Run 'git pull' and 'git push' after a successful commit.",
"config.rememberPostCommitCommand": "Remember the last git command that ran after a commit.",
"config.openAfterClone": "Controls whether to open a repository automatically after cloning.",
"config.openAfterClone.always": "Always open in current window.",
"config.openAfterClone.alwaysNewWindow": "Always open in a new window.",
@@ -195,6 +206,7 @@
"config.rebaseWhenSync": "Force git to use rebase when running the sync command.",
"config.confirmEmptyCommits": "Always confirm the creation of empty commits for the 'Git: Commit Empty' command.",
"config.fetchOnPull": "When enabled, fetch all branches when pulling. Otherwise, fetch just the current one.",
"config.pullBeforeCheckout": "Controls whether a branch that does not have outgoing commits is fast-forwarded before it is checked out.",
"config.pullTags": "Fetch all tags when pulling.",
"config.pruneOnFetch": "Prune when fetching.",
"config.autoStash": "Stash any changes before pulling and restore them after successful pull.",
@@ -213,8 +225,8 @@
"config.untrackedChanges.hidden": "Untracked changes are hidden and excluded from several actions.",
"config.requireGitUserConfig": "Controls whether to require explicit Git user configuration or allow Git to guess if missing.",
"config.showCommitInput": "Controls whether to show the commit input in the Git source control panel.",
"config.terminalAuthentication": "Controls whether to enable VS Code to be the authentication handler for git processes spawned in the integrated terminal. Note: terminals need to be restarted to pick up a change in this setting.",
"config.terminalGitEditor": "Controls whether to enable VS Code to be git editor for git processes spawned in the integrated terminal. Note: terminals need to be restarted to pick up a change in this setting.",
"config.terminalAuthentication": "Controls whether to enable VS Code to be the authentication handler for Git processes spawned in the Integrated Terminal. Note: Terminals need to be restarted to pick up a change in this setting.",
"config.terminalGitEditor": "Controls whether to enable VS Code to be the Git editor for Git processes spawned in the integrated terminal. Note: Terminals need to be restarted to pick up a change in this setting.",
"config.timeline.showAuthor": "Controls whether to show the commit author in the Timeline view.",
"config.timeline.showUncommitted": "Controls whether to show uncommitted changes in the Timeline view.",
"config.timeline.date": "Controls which date to use for items in the Timeline view.",
@@ -230,22 +242,17 @@
"config.repositoryScanIgnoredFolders": "List of folders that are ignored while scanning for Git repositories when `#git.autoRepositoryDetection#` is set to `true` or `subFolders`.",
"config.repositoryScanMaxDepth": "Controls the depth used when scanning workspace folders for Git repositories when `#git.autoRepositoryDetection#` is set to `true` or `subFolders`. Can be set to `-1` for no limit.",
"config.useIntegratedAskPass": "Controls whether GIT_ASKPASS should be overwritten to use the integrated version.",
"config.logLevel": {
"message": "Specifies how much information (if any) to log to the [git output](command:git.showOutput).",
"comment": [
"{Locked='](command:git.showOutput'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"config.logLevel.trace": "Log all information",
"config.logLevel.debug": "Log only debug, information, warning, error, and critical information",
"config.logLevel.info": "Log only information, warning, error, and critical information",
"config.logLevel.warn": "Log only warning, error, and critical information",
"config.logLevel.error": "Log only error, and critical information",
"config.logLevel.critical": "Log only critical information",
"config.logLevel.off": "Log nothing",
"config.mergeEditor": "Open the merge editor for files that are currently under conflict.",
"config.optimisticUpdate": "Controls whether to optimistically update the state of the Source Control view after running git commands.",
"config.openRepositoryInParentFolders": "Control whether a repository in parent folders of workspaces or open files should be opened.",
"config.openRepositoryInParentFolders.always": "Always open a repository in parent folders of workspaces or open files.",
"config.openRepositoryInParentFolders.never": "Never open a repository in parent folders of workspaces or open files.",
"config.openRepositoryInParentFolders.prompt": "Prompt before opening a repository the parent folders of workspaces or open files.",
"config.publishBeforeContinueOn": "Controls whether to publish unpublished git state when using Continue Working On from a git repository.",
"config.publishBeforeContinueOn.always": "Always publish unpublished git state when using Continue Working On from a git repository",
"config.publishBeforeContinueOn.never": "Never publish unpublished git state when using Continue Working On from a git repository",
"config.publishBeforeContinueOn.prompt": "Prompt to publish unpublished git state when using Continue Working On from a git repository",
"config.similarityThreshold": "Controls the threshold of the similarity index (i.e. amount of additions/deletions compared to the file's size) for changes in a pair of added/deleted files to be considered a rename.",
"submenu.explorer": "Git",
"submenu.commit": "Commit",
"submenu.commit.amend": "Amend",
@@ -331,6 +338,46 @@
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.scanFolderForRepositories": {
"message": "Scanning folder for git repositories..."
},
"view.workbench.scm.scanWorkspaceForRepositories": {
"message": "Scanning workspace for git repositories..."
},
"view.workbench.scm.repositoryInParentFolders": {
"message": "A git repository was found in the parent folders of the workspace or the open file(s).\n[Open Repository](command:git.openRepositoriesInParentFolders)\nUse the [git.openRepositoryInParentFolders](command:workbench.action.openSettings?%5B%22git.openRepositoryInParentFolders%22%5D) setting to control whether git repositories in parent folders of workspaces or open files are opened. To learn more [read our docs](https://aka.ms/vscode-git-repository-in-parent-folders).",
"comment": [
"{Locked='](command:git.openRepositoriesInParentFolders'}",
"{Locked='](command:workbench.action.openSettings?%5B%22git.openRepositoryInParentFolders%22%5D'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.repositoriesInParentFolders": {
"message": "Git repositories were found in the parent folders of the workspace or the open file(s).\n[Open Repository](command:git.openRepositoriesInParentFolders)\nUse the [git.openRepositoryInParentFolders](command:workbench.action.openSettings?%5B%22git.openRepositoryInParentFolders%22%5D) setting to control whether git repositories in parent folders of workspace or open files are opened. To learn more [read our docs](https://aka.ms/vscode-git-repository-in-parent-folders).",
"comment": [
"{Locked='](command:git.openRepositoriesInParentFolders'}",
"{Locked='](command:workbench.action.openSettings?%5B%22git.openRepositoryInParentFolders%22%5D'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.unsafeRepository": {
"message": "The detected git repository is potentially unsafe as the folder is owned by someone other than the current user.\n[Manage Unsafe Repositories](command:git.manageUnsafeRepositories)\nTo learn more about unsafe repositories [read our docs](https://aka.ms/vscode-git-unsafe-repository).",
"comment": [
"{Locked='](command:git.manageUnsafeRepositories'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.unsafeRepositories": {
"message": "The detected git repositories are potentially unsafe as the folders are owned by someone other than the current user.\n[Manage Unsafe Repositories](command:git.manageUnsafeRepositories)\nTo learn more about unsafe repositories [read our docs](https://aka.ms/vscode-git-unsafe-repository).",
"comment": [
"{Locked='](command:git.manageUnsafeRepositories'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.cloneRepository": {
"message": "You can clone a repository locally.\n[Clone Repository](command:git.clone 'Clone a repository once the git extension has activated')",
"comment": [
+56 -104
View File
@@ -3,18 +3,16 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { Command, Disposable, Event, EventEmitter, SourceControlActionButton, Uri, workspace } from 'vscode';
import { ApiRepository } from './api/api1';
import { Branch, Status } from './api/git';
import { IPostCommitCommandsProviderRegistry } from './postCommitCommands';
import { Repository, Operation } from './repository';
import { Command, Disposable, Event, EventEmitter, SourceControlActionButton, Uri, workspace, l10n } from 'vscode';
import { Branch, RefType, Status } from './api/git';
import { OperationKind } from './operation';
import { CommitCommandsCenter } from './postCommitCommands';
import { Repository } from './repository';
import { dispose } from './util';
const localize = nls.loadMessageBundle();
interface ActionButtonState {
readonly HEAD: Branch | undefined;
readonly isCheckoutInProgress: boolean;
readonly isCommitInProgress: boolean;
readonly isMergeInProgress: boolean;
readonly isRebaseInProgress: boolean;
@@ -39,9 +37,10 @@ export class ActionButtonCommand {
constructor(
readonly repository: Repository,
readonly postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry) {
readonly postCommitCommandCenter: CommitCommandsCenter) {
this._state = {
HEAD: undefined,
isCheckoutInProgress: false,
isCommitInProgress: false,
isMergeInProgress: false,
isRebaseInProgress: false,
@@ -52,7 +51,8 @@ export class ActionButtonCommand {
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
this.disposables.push(postCommitCommandsProviderRegistry.onDidChangePostCommitCommandsProviders(() => this._onDidChange.fire()));
this.disposables.push(repository.onDidChangeBranchProtection(() => this._onDidChange.fire()));
this.disposables.push(postCommitCommandCenter.onDidChange(() => this._onDidChange.fire()));
const root = Uri.file(repository.root);
this.disposables.push(workspace.onDidChangeConfiguration(e => {
@@ -62,9 +62,9 @@ export class ActionButtonCommand {
this.onDidChangeSmartCommitSettings();
}
if (e.affectsConfiguration('git.branchProtection', root) ||
e.affectsConfiguration('git.branchProtectionPrompt', root) ||
if (e.affectsConfiguration('git.branchProtectionPrompt', root) ||
e.affectsConfiguration('git.postCommitCommand', root) ||
e.affectsConfiguration('git.rememberPostCommitCommand', root) ||
e.affectsConfiguration('git.showActionButton', root)) {
this._onDidChange.fire();
}
@@ -92,8 +92,10 @@ export class ActionButtonCommand {
// The button is disabled
if (!showActionButton.commit) { return undefined; }
const primaryCommand = this.getCommitActionButtonPrimaryCommand();
return {
command: this.getCommitActionButtonPrimaryCommand(),
command: primaryCommand,
secondaryCommands: this.getCommitActionButtonSecondaryCommands(),
enabled: (this.state.repositoryHasChangesToCommit || this.state.isRebaseInProgress) && !this.state.isCommitInProgress && !this.state.isMergeInProgress
};
@@ -104,90 +106,28 @@ export class ActionButtonCommand {
if (this.state.isRebaseInProgress) {
return {
command: 'git.commit',
title: localize('scm button continue title', "{0} Continue", '$(check)'),
tooltip: this.state.isCommitInProgress ? localize('scm button continuing tooltip', "Continuing Rebase...") : localize('scm button continue tooltip', "Continue Rebase"),
title: l10n.t('{0} Continue', '$(check)'),
tooltip: this.state.isCommitInProgress ? l10n.t('Continuing Rebase...') : l10n.t('Continue Rebase'),
arguments: [this.repository.sourceControl, '']
};
}
// Commit
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const postCommitCommand = config.get<string>('postCommitCommand');
// Branch protection
const isBranchProtected = this.repository.isBranchProtected();
const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;
const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt';
const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch';
// Icon
const icon = alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined;
let commandArg = '';
let title = localize('scm button commit title', "{0} Commit", icon ?? '$(check)');
let tooltip = this.state.isCommitInProgress ? localize('scm button committing tooltip', "Committing Changes...") : localize('scm button commit tooltip', "Commit Changes");
// Title, tooltip
switch (postCommitCommand) {
case 'push': {
commandArg = 'git.push';
title = localize('scm button commit and push title', "{0} Commit & Push", icon ?? '$(arrow-up)');
if (alwaysCommitToNewBranch) {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing to new branch and pushing tooltip', "Committing to New Branch & Pushing Changes...") :
localize('scm button commit to new branch and push tooltip', "Commit to New Branch & Push Changes");
} else {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing and pushing tooltip', "Committing & Pushing Changes...") :
localize('scm button commit and push tooltip', "Commit & Push Changes");
}
break;
}
case 'sync': {
commandArg = 'git.sync';
title = localize('scm button commit and sync title', "{0} Commit & Sync", icon ?? '$(sync)');
if (alwaysCommitToNewBranch) {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing to new branch and synching tooltip', "Committing to New Branch & Synching Changes...") :
localize('scm button commit to new branch and sync tooltip', "Commit to New Branch & Sync Changes");
} else {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing and synching tooltip', "Committing & Synching Changes...") :
localize('scm button commit and sync tooltip', "Commit & Sync Changes");
}
break;
}
default: {
if (alwaysCommitToNewBranch) {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing to new branch tooltip', "Committing Changes to New Branch...") :
localize('scm button commit to new branch tooltip', "Commit Changes to New Branch");
}
break;
}
}
return { command: 'git.commit', title, tooltip, arguments: [this.repository.sourceControl, commandArg] };
return this.postCommitCommandCenter.getPrimaryCommand();
}
private getCommitActionButtonSecondaryCommands(): Command[][] {
// Rebase Continue
if (this.state.isRebaseInProgress) {
return [];
}
// Commit
const commandGroups: Command[][] = [];
if (!this.state.isRebaseInProgress) {
for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) {
const commands = provider.getCommands(new ApiRepository(this.repository));
commandGroups.push((commands ?? []).map(c => {
return {
command: 'git.commit',
title: c.title,
arguments: [this.repository.sourceControl, c.command]
};
}));
}
if (commandGroups.length > 0) {
commandGroups[0].splice(0, 0, { command: 'git.commit', title: localize('scm secondary button commit', "Commit") });
}
for (const commands of this.postCommitCommandCenter.getSecondaryCommands()) {
commandGroups.push(commands.map(c => {
return { command: 'git.commit', title: c.title, tooltip: c.tooltip, arguments: c.arguments };
}));
}
return commandGroups;
@@ -197,19 +137,26 @@ export class ActionButtonCommand {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ publish: boolean }>('showActionButton', { publish: true });
// Branch does have an upstream, commit/merge/rebase is in progress, or the button is disabled
if (this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.publish) { return undefined; }
// Not a branch (tag, detached), branch does have an upstream, commit/merge/rebase is in progress, or the button is disabled
if (this.state.HEAD?.type === RefType.Tag || !this.state.HEAD?.name || this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.publish) { return undefined; }
// Button icon
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(cloud-upload)';
return {
command: {
command: 'git.publish',
title: localize({ key: 'scm publish branch action button title', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }, "{0} Publish Branch", '$(cloud-upload)'),
title: l10n.t({ message: '{0} Publish Branch', args: [icon], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }),
tooltip: this.state.isSyncInProgress ?
localize({ key: 'scm button publish branch running', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }, "Publishing Branch...") :
localize({ key: 'scm button publish branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }, "Publish Branch"),
(this.state.HEAD?.name ?
l10n.t({ message: 'Publishing Branch "{0}"...', args: [this.state.HEAD.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
l10n.t({ message: 'Publishing Branch...', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })) :
(this.repository.HEAD?.name ?
l10n.t({ message: 'Publish Branch "{0}"', args: [this.state.HEAD?.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
l10n.t({ message: 'Publish Branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })),
arguments: [this.repository.sourceControl],
},
enabled: !this.state.isSyncInProgress
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress
};
}
@@ -228,28 +175,33 @@ export class ActionButtonCommand {
return {
command: {
command: 'git.sync',
title: `${icon}${behind}${ahead}`,
title: l10n.t('{0} Sync Changes{1}{2}', icon, behind, ahead),
tooltip: this.state.isSyncInProgress ?
localize('syncing changes', "Synchronizing Changes...")
l10n.t('Synchronizing Changes...')
: this.repository.syncTooltip,
arguments: [this.repository.sourceControl],
},
description: localize('scm button sync description', "{0} Sync Changes{1}{2}", icon, behind, ahead),
enabled: !this.state.isSyncInProgress
description: `${icon}${behind}${ahead}`,
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress
};
}
private onDidChangeOperations(): void {
const isCheckoutInProgress
= this.repository.operations.isRunning(OperationKind.Checkout) ||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
const isCommitInProgress =
this.repository.operations.isRunning(Operation.Commit) ||
this.repository.operations.isRunning(Operation.RebaseContinue);
this.repository.operations.isRunning(OperationKind.Commit) ||
this.repository.operations.isRunning(OperationKind.PostCommitCommand) ||
this.repository.operations.isRunning(OperationKind.RebaseContinue);
const isSyncInProgress =
this.repository.operations.isRunning(Operation.Sync) ||
this.repository.operations.isRunning(Operation.Push) ||
this.repository.operations.isRunning(Operation.Pull);
this.repository.operations.isRunning(OperationKind.Sync) ||
this.repository.operations.isRunning(OperationKind.Push) ||
this.repository.operations.isRunning(OperationKind.Pull);
this.state = { ...this.state, isCommitInProgress, isSyncInProgress };
this.state = { ...this.state, isCheckoutInProgress, isCommitInProgress, isSyncInProgress };
}
private onDidChangeSmartCommitSettings(): void {
+20 -8
View File
@@ -5,8 +5,8 @@
import { Model } from '../model';
import { Repository as BaseRepository, Resource } from '../repository';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, ICloneOptions, PostCommitCommandsProvider } from './git'; // {{SQL CARBON EDIT}} add ICloneOptions
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode'; // {{SQL CARBON EDIT}} Add cancellationToken
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions } from './git';
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode';
import { combinedDisposable, mapEvent } from '../util';
import { toGitUri } from '../uri';
import { GitExtensionImpl } from './extension';
@@ -32,7 +32,10 @@ export class ApiChange implements Change {
export class ApiRepositoryState implements RepositoryState {
get HEAD(): Branch | undefined { return this._repository.HEAD; }
get refs(): Ref[] { return [...this._repository.refs]; }
/**
* @deprecated Use ApiRepository.getRefs() instead.
*/
get refs(): Ref[] { console.warn('Deprecated. Use ApiRepository.getRefs() instead.'); return []; }
get remotes(): Remote[] { return [...this._repository.remotes]; }
get submodules(): Submodule[] { return [...this._repository.submodules]; }
get rebaseCommit(): Commit | undefined { return this._repository.rebaseCommit; }
@@ -170,14 +173,18 @@ export class ApiRepository implements Repository {
return this.repository.getBranch(name);
}
getBranches(query: BranchQuery): Promise<Ref[]> {
return this.repository.getBranches(query);
getBranches(query: BranchQuery, cancellationToken?: CancellationToken): Promise<Ref[]> {
return this.repository.getBranches(query, cancellationToken);
}
setBranchUpstream(name: string, upstream: string): Promise<void> {
return this.repository.setBranchUpstream(name, upstream);
}
getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]> {
return this.repository.getRefs(query, cancellationToken);
}
getMergeBase(ref1: string, ref2: string): Promise<string> {
return this.repository.getMergeBase(ref1, ref2);
}
@@ -279,7 +286,7 @@ export class ApiImpl implements API {
}
// {{SQL CARBON EDIT}}
async clone(url: string, options: ICloneOptions, cancellationToken?: CancellationToken): Promise<string> {
async clone(url: string, options: any, cancellationToken?: CancellationToken): Promise<string> {
return this._model.git.clone(url, options, cancellationToken);
}
@@ -292,9 +299,9 @@ export class ApiImpl implements API {
return result ? new ApiRepository(result) : null;
}
async init(root: Uri): Promise<Repository | null> {
async init(root: Uri, options?: InitOptions): Promise<Repository | null> {
const path = root.fsPath;
await this._model.git.init(path);
await this._model.git.init(path, options);
await this._model.openRepository(path);
return this.getRepository(root) || null;
}
@@ -331,6 +338,10 @@ export class ApiImpl implements API {
return this._model.registerPushErrorHandler(handler);
}
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable {
return this._model.registerBranchProtectionProvider(root, provider);
}
constructor(private _model: Model) { }
}
@@ -356,6 +367,7 @@ function getStatus(status: Status): string {
case Status.UNTRACKED: return 'UNTRACKED';
case Status.IGNORED: return 'IGNORED';
case Status.INTENT_TO_ADD: return 'INTENT_TO_ADD';
case Status.INTENT_TO_RENAME: return 'INTENT_TO_RENAME';
case Status.ADDED_BY_US: return 'ADDED_BY_US';
case Status.ADDED_BY_THEM: return 'ADDED_BY_THEM';
case Status.DELETED_BY_US: return 'DELETED_BY_US';
+27 -1
View File
@@ -8,6 +8,7 @@ export { ProviderResult } from 'vscode';
export interface API {
pickRemoteSource(options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;
getRemoteSourceActions(url: string): Promise<RemoteSourceAction[]>;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
}
@@ -31,9 +32,12 @@ export interface GitBaseExtension {
export interface PickRemoteSourceOptions {
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
readonly urlLabel?: 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 {
@@ -41,20 +45,42 @@ 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;
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[]>;
}
+47 -10
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, CancellationToken, Progress, Command } from 'vscode'; // {{SQL CARBON EDIT}} add CancellationToken
import { Uri, Event, Disposable, ProviderResult, Command, Progress, CancellationToken } from 'vscode'; // {{SQL CARBON EDIT}} - add Progress
export { ProviderResult } from 'vscode';
export interface Git {
@@ -78,6 +78,7 @@ export const enum Status {
UNTRACKED,
IGNORED,
INTENT_TO_ADD,
INTENT_TO_RENAME,
ADDED_BY_US,
ADDED_BY_THEM,
@@ -139,7 +140,13 @@ export interface CommitOptions {
requireUserConfig?: boolean;
useEditor?: boolean;
verbose?: boolean;
postCommitCommand?: string;
/**
* 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 {
@@ -150,11 +157,19 @@ export interface FetchOptions {
depth?: number;
}
export interface BranchQuery {
readonly remote?: boolean;
readonly pattern?: string;
readonly count?: number;
export interface InitOptions {
defaultBranch?: string;
}
export interface RefQuery {
readonly contains?: string;
readonly count?: number;
readonly pattern?: string;
readonly sort?: 'alphabetically' | 'committerdate';
}
export interface BranchQuery extends RefQuery {
readonly remote?: boolean;
}
export interface Repository {
@@ -198,9 +213,11 @@ export interface Repository {
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[]>;
getBranches(query: BranchQuery, cancellationToken?: CancellationToken): Promise<Ref[]>;
setBranchUpstream(name: string, upstream: string): Promise<void>;
getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]>;
getMergeBase(ref1: string, ref2: string): Promise<string>;
tag(name: string, upstream: string): Promise<void>;
@@ -262,6 +279,21 @@ 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 {
@@ -285,10 +317,10 @@ export interface API {
* @param cancellationToken
* @returns a promise to the string location where the repository was cloned to
*/
clone(url: string, options: ICloneOptions, cancellationToken?: CancellationToken): Promise<string>; // {{SQL CARBON EDIT}}
clone(url: string, options: any, cancellationToken?: CancellationToken): Promise<string>; // {{SQL CARBON EDIT}}
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;
@@ -296,6 +328,7 @@ export interface API {
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable;
}
export interface GitExtension {
@@ -352,7 +385,10 @@ export const enum GitErrorCodes {
PatchDoesNotApply = 'PatchDoesNotApply',
NoPathFound = 'NoPathFound',
UnknownPath = 'UnknownPath',
EmptyCommitMessage = 'EmptyCommitMessage'
EmptyCommitMessage = 'EmptyCommitMessage',
BranchFastForwardRejected = 'BranchFastForwardRejected',
BranchNotYetBorn = 'BranchNotYetBorn',
TagConflict = 'TagConflict'
}
// {{SQL CARBON EDIT}} move ICloneOptions from git.ts to here since it's used in clone()
@@ -360,4 +396,5 @@ export interface ICloneOptions {
readonly parentPath: string;
readonly progress: Progress<{ increment: number }>;
readonly recursive?: boolean;
readonly ref?: string;
}
+34 -12
View File
@@ -4,36 +4,58 @@
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as nls from 'vscode-nls';
import { IPCClient } from './ipc/ipcClient';
const localize = nls.loadMessageBundle();
function fatal(err: any): void {
console.error(localize('missOrInvalid', "Missing or invalid credentials."));
console.error('Missing or invalid credentials.');
console.error(err);
process.exit(1);
}
function main(argv: string[]): void {
if (argv.length !== 5) {
return fatal('Wrong number of arguments');
}
if (!process.env['VSCODE_GIT_ASKPASS_PIPE']) {
return fatal('Missing pipe');
}
if (!process.env['VSCODE_GIT_ASKPASS_TYPE']) {
return fatal('Missing type');
}
if (process.env['VSCODE_GIT_ASKPASS_TYPE'] !== 'https' && process.env['VSCODE_GIT_ASKPASS_TYPE'] !== 'ssh') {
return fatal(`Invalid type: ${process.env['VSCODE_GIT_ASKPASS_TYPE']}`);
}
if (process.env['VSCODE_GIT_COMMAND'] === 'fetch' && !!process.env['VSCODE_GIT_FETCH_SILENT']) {
return fatal('Skip silent fetch commands');
}
const output = process.env['VSCODE_GIT_ASKPASS_PIPE'] as string;
const request = argv[2];
const host = argv[4].replace(/^["']+|["':]+$/g, '');
const ipcClient = new IPCClient('askpass');
const askpassType = process.env['VSCODE_GIT_ASKPASS_TYPE'] as 'https' | 'ssh';
ipcClient.call({ request, host }).then(res => {
// HTTPS (username | password), SSH (passphrase | authenticity)
const request = askpassType === 'https' ? argv[2] : argv[3];
let host: string | undefined,
file: string | undefined,
fingerprint: string | undefined;
if (askpassType === 'https') {
host = argv[4].replace(/^["']+|["':]+$/g, '');
}
if (askpassType === 'ssh') {
if (/passphrase/i.test(request)) {
// passphrase
file = argv[6].replace(/^["']+|["':]+$/g, '');
} else {
// authenticity
host = argv[6].replace(/^["']+|["':]+$/g, '');
fingerprint = argv[15];
}
}
const ipcClient = new IPCClient('askpass');
ipcClient.call({ askpassType, request, host, file, fingerprint }).then(res => {
fs.writeFileSync(output, res + '\n');
setTimeout(() => process.exit(0), 0);
}).catch(err => fatal(err));
+1 -1
View File
@@ -1,5 +1,5 @@
#!/bin/sh
VSCODE_GIT_ASKPASS_PIPE=`mktemp`
ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $VSCODE_GIT_ASKPASS_EXTRA_ARGS $*
ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" VSCODE_GIT_ASKPASS_TYPE="https" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $VSCODE_GIT_ASKPASS_EXTRA_ARGS $*
cat $VSCODE_GIT_ASKPASS_PIPE
rm $VSCODE_GIT_ASKPASS_PIPE
+49 -3
View File
@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { window, InputBoxOptions, Uri, Disposable, workspace } from 'vscode';
import { window, InputBoxOptions, Uri, Disposable, workspace, QuickPickOptions, l10n } from 'vscode';
import { IDisposable, EmptyDisposable, toDisposable } from './util';
import * as path from 'path';
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
@@ -13,6 +13,7 @@ import { ITerminalEnvironmentProvider } from './terminal';
export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {
private env: { [key: string]: string };
private sshEnv: { [key: string]: string };
private disposable: IDisposable = EmptyDisposable;
private cache = new Map<string, Credentials>();
private credentialsProviders = new Set<CredentialsProvider>();
@@ -23,14 +24,25 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {
}
this.env = {
// GIT_ASKPASS
GIT_ASKPASS: path.join(__dirname, this.ipc ? 'askpass.sh' : 'askpass-empty.sh'),
// VSCODE_GIT_ASKPASS
VSCODE_GIT_ASKPASS_NODE: process.execPath,
VSCODE_GIT_ASKPASS_EXTRA_ARGS: (process.versions['electron'] && process.versions['microsoft-build']) ? '--ms-enable-electron-run-as-node' : '',
VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js'),
};
this.sshEnv = {
// SSH_ASKPASS
SSH_ASKPASS: path.join(__dirname, this.ipc ? 'ssh-askpass.sh' : 'ssh-askpass-empty.sh'),
SSH_ASKPASS_REQUIRE: 'force',
};
}
async handle({ request, host }: { request: string; host: string }): Promise<string> {
async handle(payload:
{ askpassType: 'https'; request: string; host: string } |
{ askpassType: 'ssh'; request: string; host?: string; file?: string; fingerprint?: string }
): Promise<string> {
const config = workspace.getConfiguration('git', null);
const enabled = config.get<boolean>('enabled');
@@ -38,6 +50,16 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {
return '';
}
// https
if (payload.askpassType === 'https') {
return await this.handleAskpass(payload.request, payload.host);
}
// ssh
return await this.handleSSHAskpass(payload.request, payload.host, payload.file, payload.fingerprint);
}
async handleAskpass(request: string, host: string): Promise<string> {
const uri = Uri.parse(host);
const authority = uri.authority.replace(/^.*@/, '');
const password = /password/i.test(request);
@@ -72,9 +94,33 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {
return await window.showInputBox(options) || '';
}
async handleSSHAskpass(request: string, host?: string, file?: string, fingerprint?: string): Promise<string> {
// passphrase
if (/passphrase/i.test(request)) {
const options: InputBoxOptions = {
password: true,
placeHolder: l10n.t('Passphrase'),
prompt: `SSH Key: ${file}`,
ignoreFocusOut: true
};
return await window.showInputBox(options) || '';
}
// authenticity
const options: QuickPickOptions = {
canPickMany: false,
ignoreFocusOut: true,
placeHolder: l10n.t('Are you sure you want to continue connecting?'),
title: l10n.t('"{0}" has fingerprint "{1}"', host ?? '', fingerprint ?? '')
};
const items = [l10n.t('yes'), l10n.t('no')];
return await window.showQuickPick(items, options) ?? '';
}
getEnv(): { [key: string]: string } {
const config = workspace.getConfiguration('git');
return config.get<boolean>('useIntegratedAskPass') ? this.env : {};
return config.get<boolean>('useIntegratedAskPass') ? { ...this.env, ...this.sshEnv } : {};
}
getTerminalEnv(): { [key: string]: string } {
+8 -16
View File
@@ -3,18 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget, Uri, ConfigurationChangeEvent } from 'vscode';
import { Repository, Operation } from './repository';
import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget, Uri, ConfigurationChangeEvent, l10n, env } from 'vscode';
import { Repository } from './repository';
import { eventToPromise, filterEvent, onceEvent } from './util';
import * as nls from 'vscode-nls';
import { GitErrorCodes } from './api/git';
const localize = nls.loadMessageBundle();
function isRemoteOperation(operation: Operation): boolean {
return operation === Operation.Pull || operation === Operation.Push || operation === Operation.Sync || operation === Operation.Fetch;
}
export class AutoFetcher {
private static DidInformUser = 'autofetch.didInformUser';
@@ -33,7 +26,7 @@ export class AutoFetcher {
workspace.onDidChangeConfiguration(this.onConfiguration, this, this.disposables);
this.onConfiguration();
const onGoodRemoteOperation = filterEvent(repository.onDidRunOperation, ({ operation, error }) => !error && isRemoteOperation(operation));
const onGoodRemoteOperation = filterEvent(repository.onDidRunOperation, ({ operation, error }) => !error && operation.remote);
const onFirstGoodRemoteOperation = onceEvent(onGoodRemoteOperation);
onFirstGoodRemoteOperation(this.onFirstGoodRemoteOperation, this, this.disposables);
}
@@ -51,11 +44,10 @@ export class AutoFetcher {
return;
}
const yes: MessageItem = { title: localize('yes', "Yes") };
const no: MessageItem = { isCloseAffordance: true, title: localize('no', "No") };
const askLater: MessageItem = { title: localize('not now', "Ask Me Later") };
// {{SQL CARBON EDIT}}
const result = await window.showInformationMessage(localize('suggest auto fetch', "Would you like Azure Data Studio to [periodically run 'git fetch']({0})?", 'https://go.microsoft.com/fwlink/?linkid=865294'), yes, no, askLater);
const yes: MessageItem = { title: l10n.t('Yes') };
const no: MessageItem = { isCloseAffordance: true, title: l10n.t('No') };
const askLater: MessageItem = { title: l10n.t('Ask Me Later') };
const result = await window.showInformationMessage(l10n.t('Would you like {0} to [periodically run "git fetch"]({1})?', env.appName, 'https://go.microsoft.com/fwlink/?linkid=865294'), yes, no, askLater);
if (result === askLater) {
return;
@@ -115,7 +107,7 @@ export class AutoFetcher {
try {
if (this._fetchAll) {
await this.repository.fetchAll();
await this.repository.fetchAll({ silent: true });
} else {
await this.repository.fetchDefault({ silent: true });
}
+52
View File
@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event, EventEmitter, Uri, workspace } from 'vscode';
import { BranchProtection, BranchProtectionProvider } from './api/git';
import { dispose, filterEvent } from './util';
export interface IBranchProtectionProviderRegistry {
readonly onDidChangeBranchProtectionProviders: Event<Uri>;
getBranchProtectionProviders(root: Uri): BranchProtectionProvider[];
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable;
}
export class GitBranchProtectionProvider implements BranchProtectionProvider {
private readonly _onDidChangeBranchProtection = new EventEmitter<Uri>();
onDidChangeBranchProtection = this._onDidChangeBranchProtection.event;
private branchProtection!: BranchProtection;
private disposables: Disposable[] = [];
constructor(private readonly repositoryRoot: Uri) {
const onDidChangeBranchProtectionEvent = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.branchProtection', repositoryRoot));
onDidChangeBranchProtectionEvent(this.updateBranchProtection, this, this.disposables);
this.updateBranchProtection();
}
provideBranchProtection(): BranchProtection[] {
return [this.branchProtection];
}
private updateBranchProtection(): void {
const scopedConfig = workspace.getConfiguration('git', this.repositoryRoot);
const branchProtectionConfig = scopedConfig.get<unknown>('branchProtection') ?? [];
const branchProtectionValues = Array.isArray(branchProtectionConfig) ? branchProtectionConfig : [branchProtectionConfig];
const branches = branchProtectionValues
.map(bp => typeof bp === 'string' ? bp.trim() : '')
.filter(bp => bp !== '');
this.branchProtection = { remote: '', rules: [{ include: branches }] };
this._onDidChangeBranchProtection.fire(this.repositoryRoot);
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -127,7 +127,11 @@ class GitDecorationProvider implements FileDecorationProvider {
// not deleted and has a decoration
bucket.set(r.original.toString(), decoration);
if (r.type === Status.INDEX_RENAMED) {
if (r.type === Status.DELETED && r.rightUri) {
bucket.set(r.rightUri.toString(), decoration);
}
if (r.type === Status.INDEX_RENAMED || r.type === Status.INTENT_TO_RENAME) {
bucket.set(r.resourceUri.toString(), decoration);
}
}
@@ -0,0 +1,115 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import { RefType } from './api/git';
import { Model } from './model';
export class GitEditSessionIdentityProvider implements vscode.EditSessionIdentityProvider, vscode.Disposable {
private providerRegistration: vscode.Disposable;
constructor(private model: Model) {
this.providerRegistration = vscode.workspace.registerEditSessionIdentityProvider('file', this);
vscode.workspace.onWillCreateEditSessionIdentity((e) => {
e.waitUntil(this._onWillCreateEditSessionIdentity(e.workspaceFolder));
});
}
dispose() {
this.providerRegistration.dispose();
}
async provideEditSessionIdentity(workspaceFolder: vscode.WorkspaceFolder, token: vscode.CancellationToken): Promise<string | undefined> {
await this.model.openRepository(path.dirname(workspaceFolder.uri.fsPath));
const repository = this.model.getRepository(workspaceFolder.uri);
await repository?.status();
if (!repository || !repository?.HEAD?.upstream) {
return undefined;
}
const remoteUrl = repository.remotes.find((remote) => remote.name === repository.HEAD?.upstream?.remote)?.pushUrl?.replace(/^(git@[^\/:]+)(:)/i, 'ssh://$1/');
const remote = remoteUrl ? await vscode.workspace.getCanonicalUri(vscode.Uri.parse(remoteUrl), { targetScheme: 'https' }, token) : null;
return JSON.stringify({
remote: remote?.toString() ?? remoteUrl,
ref: repository.HEAD?.upstream?.name ?? null,
sha: repository.HEAD?.commit ?? null,
});
}
provideEditSessionIdentityMatch(identity1: string, identity2: string): vscode.EditSessionIdentityMatch {
try {
const normalizedIdentity1 = normalizeEditSessionIdentity(identity1);
const normalizedIdentity2 = normalizeEditSessionIdentity(identity2);
if (normalizedIdentity1.remote === normalizedIdentity2.remote &&
normalizedIdentity1.ref === normalizedIdentity2.ref &&
normalizedIdentity1.sha === normalizedIdentity2.sha) {
// This is a perfect match
return vscode.EditSessionIdentityMatch.Complete;
} else if (normalizedIdentity1.remote === normalizedIdentity2.remote &&
normalizedIdentity1.ref === normalizedIdentity2.ref &&
normalizedIdentity1.sha !== normalizedIdentity2.sha) {
// Same branch and remote but different SHA
return vscode.EditSessionIdentityMatch.Partial;
} else {
return vscode.EditSessionIdentityMatch.None;
}
} catch (ex) {
return vscode.EditSessionIdentityMatch.Partial;
}
}
private async _onWillCreateEditSessionIdentity(workspaceFolder: vscode.WorkspaceFolder): Promise<void> {
await this._doPublish(workspaceFolder);
}
private async _doPublish(workspaceFolder: vscode.WorkspaceFolder) {
await this.model.openRepository(path.dirname(workspaceFolder.uri.fsPath));
const repository = this.model.getRepository(workspaceFolder.uri);
if (!repository) {
return;
}
await repository.status();
// If this branch hasn't been published to the remote yet,
// ensure that it is published before Continue On is invoked
if (!repository.HEAD?.upstream && repository.HEAD?.type === RefType.Head) {
const publishBranch = vscode.l10n.t('Publish Branch');
const selection = await vscode.window.showInformationMessage(
vscode.l10n.t('The current branch is not published to the remote. Would you like to publish it to access your changes elsewhere?'),
{ modal: true },
publishBranch
);
if (selection !== publishBranch) {
throw new vscode.CancellationError();
}
await vscode.commands.executeCommand('git.publish');
}
}
}
function normalizeEditSessionIdentity(identity: string) {
let { remote, ref, sha } = JSON.parse(identity);
if (typeof remote === 'string' && remote.endsWith('.git')) {
remote = remote.slice(0, remote.length - 4);
}
return {
remote,
ref,
sha
};
}
+381 -122
View File
@@ -12,10 +12,10 @@ import * as which from 'which';
import { EventEmitter } from 'events';
import * as iconv from '@vscode/iconv-lite-umd';
import * as filetype from 'file-type';
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows } from './util';
import { CancellationToken, ConfigurationChangeEvent, Uri, workspace } from 'vscode'; // {{SQL CARBON EDIT}} remove Progress
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals } from './util';
import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Uri, workspace } from 'vscode'; // {{SQL CARBON EDIT}} remove Progress
import { detectEncoding } from './encoding';
import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, BranchQuery, ICloneOptions } from './api/git'; // {{SQL CARBON EDIT}} add ICloneOptions
import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery, InitOptions, ICloneOptions } from './api/git'; // {{SQL CARBON EDIT}} add ICloneOptions
import * as byline from 'byline';
import { StringDecoder } from 'string_decoder';
@@ -198,7 +198,7 @@ async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToke
}
if (cancellationToken && cancellationToken.isCancellationRequested) {
throw new GitError({ message: 'Cancelled' });
throw new CancellationError();
}
const disposables: IDisposable[] = [];
@@ -239,7 +239,7 @@ async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToke
// noop
}
e(new GitError({ message: 'Cancelled' }));
e(new CancellationError());
});
});
@@ -360,6 +360,7 @@ const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B';
readonly parentPath: string;
readonly progress: Progress<{ increment: number }>;
readonly recursive?: boolean;
readonly ref?: string;
}*/
export class Git {
@@ -396,13 +397,18 @@ export class Git {
return Versions.compare(Versions.fromString(this.version), Versions.fromString(version));
}
open(repository: string, dotGit: { path: string; commonPath?: string }): Repository {
return new Repository(this, repository, dotGit);
open(repository: string, dotGit: { path: string; commonPath?: string }, logger: LogOutputChannel): Repository {
return new Repository(this, repository, dotGit, logger);
}
async init(repository: string): Promise<void> {
await this.exec(repository, ['init']);
return;
async init(repository: string, options: InitOptions = {}): Promise<void> {
const args = ['init'];
if (options.defaultBranch && options.defaultBranch !== '') {
args.push('-b', options.defaultBranch);
}
await this.exec(repository, args);
}
async clone(url: string, options: ICloneOptions, cancellationToken?: CancellationToken): Promise<string> {
@@ -427,7 +433,7 @@ export class Git {
let previousProgress = 0;
lineStream.on('data', (line: string) => {
let match: RegExpMatchArray | null = null;
let match: RegExpExecArray | null = null;
if (match = /Counting objects:\s*(\d+)%/i.exec(line)) {
totalProgress = Math.floor(parseInt(match[1]) * 0.1);
@@ -451,6 +457,9 @@ export class Git {
if (options.recursive) {
command.push('--recursive');
}
if (options.ref) {
command.push('--branch', options.ref);
}
await this.exec(options.parentPath, command, {
cancellationToken,
env: { 'GIT_HTTP_USER_AGENT': this.userAgent },
@@ -481,7 +490,7 @@ export class Git {
const repoUri = Uri.file(repoPath);
const pathUri = Uri.file(repositoryPath);
if (repoUri.authority.length !== 0 && pathUri.authority.length === 0) {
// eslint-disable-next-line code-no-look-behind-regex
// eslint-disable-next-line local/code-no-look-behind-regex
const match = /(?<=^\/?)([a-zA-Z])(?=:\/)/.exec(pathUri.path);
if (match !== null) {
const [, letter] = match;
@@ -493,10 +502,14 @@ export class Git {
),
);
if (networkPath !== undefined) {
// If the repository is at the root of the mapped drive then we
// have to append `\` (ex: D:\) otherwise the path is not valid.
const isDriveRoot = pathEquals(repoUri.fsPath, networkPath);
return path.normalize(
repoUri.fsPath.replace(
networkPath,
`${letter.toLowerCase()}:${networkPath.endsWith('\\') ? '\\' : ''}`
`${letter.toLowerCase()}:${isDriveRoot || networkPath.endsWith('\\') ? '\\' : ''}`
),
);
}
@@ -505,13 +518,6 @@ export class Git {
return path.normalize(pathUri.fsPath);
}
// On Windows, there are cases in which the normalized path for a mapped folder contains a trailing `\`
// character (ex: \\server\folder\) due to the implementation of `path.normalize()`. This behaviour is
// by design as documented in https://github.com/nodejs/node/issues/1765.
if (repoUri.authority.length !== 0) {
return repoPath.replace(/\\$/, '');
}
}
return repoPath;
@@ -554,7 +560,7 @@ export class Git {
if (options.log !== false) {
const startTime = Date.now();
child.on('exit', (_) => {
this.log(`> git ${args.join(' ')} [${Date.now() - startTime}ms]\n`);
this.log(`> git ${args.join(' ')} [${Date.now() - startTime}ms]${child.killed ? ' (cancelled)' : ''}\n`);
});
}
@@ -570,12 +576,22 @@ export class Git {
child.stdin!.end(options.input, 'utf8');
}
const startTime = Date.now();
const bufferResult = await exec(child, options.cancellationToken);
const startExec = Date.now();
let bufferResult: IExecutionResult<Buffer>;
try {
bufferResult = await exec(child, options.cancellationToken);
} catch (ex) {
if (ex instanceof CancellationError) {
this.log(`> git ${args.join(' ')} [${Date.now() - startExec}ms] (cancelled)\n`);
}
throw ex;
}
if (options.log !== false) {
// command
this.log(`> git ${args.join(' ')} [${Date.now() - startTime}ms]\n`);
this.log(`> git ${args.join(' ')} [${Date.now() - startExec}ms]\n`);
// stdout
if (bufferResult.stdout.length > 0 && args.find(a => this.commandsToLog.includes(a))) {
@@ -656,6 +672,32 @@ export class Git {
private log(output: string): void {
this._onOutput.emit('log', output);
}
async mergeFile(options: { input1Path: string; input2Path: string; basePath: string; diff3?: boolean }): Promise<string> {
const args = ['merge-file', '-p', options.input1Path, options.basePath, options.input2Path];
if (options.diff3) {
args.push('--diff3');
} else {
args.push('--no-diff3');
}
try {
const result = await this.exec(os.homedir(), args);
return result.stdout;
} catch (err) {
if (typeof err.stdout === 'string') {
// The merge had conflicts, stdout still contains the merged result (with conflict markers)
return err.stdout;
} else {
throw err;
}
}
}
async addSafeDirectory(repositoryPath: string): Promise<void> {
await this.exec(os.homedir(), ['config', '--global', '--add', 'safe.directory', repositoryPath]);
return;
}
}
export interface Commit {
@@ -669,6 +711,50 @@ export interface Commit {
refNames: string[];
}
interface GitConfigSection {
name: string;
subSectionName?: string;
properties: { [key: string]: string };
}
class GitConfigParser {
private static readonly _lineSeparator = /\r?\n/;
private static readonly _propertyRegex = /^\s*(\w+)\s*=\s*"?([^"]+)"?$/;
private static readonly _sectionRegex = /^\s*\[\s*([^\]]+?)\s*(\"[^"]+\")*\]\s*$/;
static parse(raw: string): GitConfigSection[] {
const config: { sections: GitConfigSection[] } = { sections: [] };
let section: GitConfigSection = { name: 'DEFAULT', properties: {} };
const addSection = (section?: GitConfigSection) => {
if (!section) { return; }
config.sections.push(section);
};
for (const line of raw.split(GitConfigParser._lineSeparator)) {
// Section
const sectionMatch = line.match(GitConfigParser._sectionRegex);
if (sectionMatch?.length === 3) {
addSection(section);
section = { name: sectionMatch[1], subSectionName: sectionMatch[2]?.replaceAll('"', ''), properties: {} };
continue;
}
// Property
const propertyMatch = line.match(GitConfigParser._propertyRegex);
if (propertyMatch?.length === 3 && !Object.keys(section.properties).includes(propertyMatch[1])) {
section.properties[propertyMatch[1]] = propertyMatch[2];
}
}
addSection(section);
return config.sections;
}
}
export class GitStatusParser {
private lastRaw = '';
@@ -707,7 +793,7 @@ export class GitStatusParser {
// space
i++;
if (entry.x === 'R' || entry.x === 'C') {
if (entry.x === 'R' || entry.y === 'R' || entry.x === 'C') {
lastIndex = raw.indexOf('\0', i);
if (lastIndex === -1) {
@@ -742,61 +828,38 @@ export interface Submodule {
}
export function parseGitmodules(raw: string): Submodule[] {
const regex = /\r?\n/g;
let position = 0;
let match: RegExpExecArray | null = null;
const result: Submodule[] = [];
let submodule: Partial<Submodule> = {};
function parseLine(line: string): void {
const sectionMatch = /^\s*\[submodule "([^"]+)"\]\s*$/.exec(line);
if (sectionMatch) {
if (submodule.name && submodule.path && submodule.url) {
result.push(submodule as Submodule);
}
const name = sectionMatch[1];
if (name) {
submodule = { name };
return;
}
for (const submoduleSection of GitConfigParser.parse(raw).filter(s => s.name === 'submodule')) {
if (submoduleSection.subSectionName && submoduleSection.properties['path'] && submoduleSection.properties['url']) {
result.push({
name: submoduleSection.subSectionName,
path: submoduleSection.properties['path'],
url: submoduleSection.properties['url']
});
}
if (!submodule) {
return;
}
const propertyMatch = /^\s*(\w+)\s*=\s*(.*)$/.exec(line);
if (!propertyMatch) {
return;
}
const [, key, value] = propertyMatch;
switch (key) {
case 'path': submodule.path = value; break;
case 'url': submodule.url = value; break;
}
}
while (match = regex.exec(raw)) {
parseLine(raw.substring(position, match.index));
position = match.index + match[0].length;
}
parseLine(raw.substring(position));
if (submodule.name && submodule.path && submodule.url) {
result.push(submodule as Submodule);
}
return result;
}
export function parseGitRemotes(raw: string): MutableRemote[] {
const remotes: MutableRemote[] = [];
for (const remoteSection of GitConfigParser.parse(raw).filter(s => s.name === 'remote')) {
if (remoteSection.subSectionName) {
remotes.push({
name: remoteSection.subSectionName,
fetchUrl: remoteSection.properties['url'],
pushUrl: remoteSection.properties['pushurl'] ?? remoteSection.properties['url'],
isReadOnly: false
});
}
}
return remotes;
}
const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm;
export function parseGitCommits(data: string): Commit[] {
@@ -882,7 +945,8 @@ export class Repository {
constructor(
private _git: Git,
private repositoryRoot: string,
readonly dotGit: { path: string; commonPath?: string }
readonly dotGit: { path: string; commonPath?: string },
private logger: LogOutputChannel
) { }
get git(): Git {
@@ -1399,6 +1463,8 @@ export class Repository {
if (/Please,? commit your changes or stash them/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
err.gitTreeish = treeish;
} else if (/You are on a branch yet to be born/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.BranchNotYetBorn;
}
throw err;
@@ -1556,6 +1622,10 @@ export class Repository {
}
}
async mergeAbort(): Promise<void> {
await this.exec(['merge', '--abort']);
}
async tag(name: string, message?: string): Promise<void> {
let args = ['tag'];
@@ -1573,6 +1643,11 @@ export class Repository {
await this.exec(args);
}
async deleteRemoteTag(remoteName: string, tagName: string): Promise<void> {
const args = ['push', '--delete', remoteName, tagName];
await this.exec(args);
}
async clean(paths: string[]): Promise<void> {
const pathsByGroup = groupBy(paths.map(sanitizePath), p => path.dirname(p));
const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]);
@@ -1690,12 +1765,34 @@ export class Repository {
err.gitErrorCode = GitErrorCodes.NoRemoteRepositorySpecified;
} else if (/Could not read from remote repository/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
} else if (/! \[rejected\].*\(non-fast-forward\)/m.test(err.stderr || '')) {
// The local branch has outgoing changes and it cannot be fast-forwarded.
err.gitErrorCode = GitErrorCodes.BranchFastForwardRejected;
}
throw err;
}
}
async fetchTags(options: { remote: string; tags: string[]; force?: boolean }): Promise<void> {
const args = ['fetch'];
const spawnOptions: SpawnOptions = {
env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent }
};
args.push(options.remote);
for (const tag of options.tags) {
args.push(`refs/tags/${tag}:refs/tags/${tag}`);
}
if (options.force) {
args.push('--force');
}
await this.exec(args, spawnOptions);
}
async pull(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> {
const args = ['pull'];
@@ -1735,6 +1832,8 @@ export class Repository {
err.gitErrorCode = GitErrorCodes.CantLockRef;
} else if (/cannot rebase onto multiple branches/i.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.CantRebaseMultipleBranches;
} else if (/! \[rejected\].*\(would clobber existing tag\)/m.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.TagConflict;
}
throw err;
@@ -1824,7 +1923,7 @@ export class Repository {
}
}
async createStash(message?: string, includeUntracked?: boolean): Promise<void> {
async createStash(message?: string, includeUntracked?: boolean, staged?: boolean): Promise<void> {
try {
const args = ['stash', 'push'];
@@ -1832,6 +1931,10 @@ export class Repository {
args.push('-u');
}
if (staged) {
args.push('-S');
}
if (message) {
args.push('-m', message);
}
@@ -1897,25 +2000,37 @@ export class Repository {
}
}
getStatus(opts?: { limit?: number; ignoreSubmodules?: boolean; untrackedChanges?: 'mixed' | 'separate' | 'hidden' }): Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }> {
return new Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }>((c, e) => {
async getStatus(opts?: { limit?: number; ignoreSubmodules?: boolean; similarityThreshold?: number; untrackedChanges?: 'mixed' | 'separate' | 'hidden'; cancellationToken?: CancellationToken }): Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }> {
if (opts?.cancellationToken && opts?.cancellationToken.isCancellationRequested) {
throw new CancellationError();
}
const disposables: IDisposable[] = [];
const env = { GIT_OPTIONAL_LOCKS: '0' };
const args = ['status', '-z'];
if (opts?.untrackedChanges === 'hidden') {
args.push('-uno');
} else {
args.push('-uall');
}
if (opts?.ignoreSubmodules) {
args.push('--ignore-submodules');
}
// --find-renames option is only available starting with git 2.18.0
if (opts?.similarityThreshold && opts.similarityThreshold !== 50 && this._git.compareGitVersionTo('2.18.0') !== -1) {
args.push(`--find-renames=${opts.similarityThreshold}%`);
}
const child = this.stream(args, { env });
let result = new Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }>((c, e) => {
const parser = new GitStatusParser();
const env = { GIT_OPTIONAL_LOCKS: '0' };
const args = ['status', '-z'];
if (opts?.untrackedChanges === 'hidden') {
args.push('-uno');
} else {
args.push('-uall');
}
if (opts?.ignoreSubmodules) {
args.push('--ignore-submodules');
}
const child = this.stream(args, { env });
const onExit = (exitCode: number) => {
const onClose = (exitCode: number) => {
if (exitCode !== 0) {
const stderr = stderrData.join('');
return e(new GitError({
@@ -1936,7 +2051,7 @@ export class Repository {
parser.update(raw);
if (limit !== 0 && parser.status.length > limit) {
child.removeListener('exit', onExit);
child.removeListener('close', onClose);
child.stdout!.removeListener('data', onStdoutData);
child.kill();
@@ -1952,12 +2067,71 @@ export class Repository {
child.stderr!.on('data', raw => stderrData.push(raw as string));
child.on('error', cpErrorHandler(e));
child.on('exit', onExit);
child.on('close', onClose);
});
if (opts?.cancellationToken) {
const cancellationPromise = new Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }>((_, e) => {
disposables.push(onceEvent(opts.cancellationToken!.onCancellationRequested)(() => {
try {
child.kill();
} catch (err) {
// noop
}
e(new CancellationError());
}));
});
result = Promise.race([result, cancellationPromise]);
}
try {
const { status, statusLength, didHitLimit } = await result;
return { status, statusLength, didHitLimit };
}
finally {
dispose(disposables);
}
}
async getHEADRef(): Promise<Branch | undefined> {
let HEAD: Branch | undefined;
try {
HEAD = await this.getHEAD();
if (HEAD.name) {
// Branch
HEAD = await this.getBranch(HEAD.name);
} else if (HEAD.commit) {
// Tag || Commit
const tags = await this.getRefs({ pattern: 'refs/tags' });
const tag = tags.find(tag => tag.commit === HEAD!.commit);
if (tag) {
HEAD = { ...HEAD, name: tag.name, type: RefType.Tag };
}
}
} catch (err) {
// noop
}
return HEAD;
}
async getHEAD(): Promise<Ref> {
try {
// Attempt to parse the HEAD file
const result = await this.getHEADFS();
return result;
}
catch (err) {
this.logger.warn(err.message);
}
try {
// Fallback to using git to determine HEAD
const result = await this.exec(['symbolic-ref', '--short', 'HEAD']);
if (!result.stdout) {
@@ -1965,15 +2139,35 @@ export class Repository {
}
return { name: result.stdout.trim(), commit: undefined, type: RefType.Head };
} catch (err) {
const result = await this.exec(['rev-parse', 'HEAD']);
if (!result.stdout) {
throw new Error('Error parsing HEAD');
}
return { name: undefined, commit: result.stdout.trim(), type: RefType.Head };
}
catch (err) { }
// Detached HEAD
const result = await this.exec(['rev-parse', 'HEAD']);
if (!result.stdout) {
throw new Error('Error parsing HEAD');
}
return { name: undefined, commit: result.stdout.trim(), type: RefType.Head };
}
async getHEADFS(): Promise<Ref> {
const raw = await fs.readFile(path.join(this.dotGit.path, 'HEAD'), 'utf8');
// Branch
const branchMatch = raw.match(/^ref: refs\/heads\/(?<name>.*)$/m);
if (branchMatch?.groups?.name) {
return { name: branchMatch.groups.name, commit: undefined, type: RefType.Head };
}
// Detached
const commitMatch = raw.match(/^(?<commit>[0-9a-f]{40})$/m);
if (commitMatch?.groups?.commit) {
return { name: undefined, commit: commitMatch.groups.commit, type: RefType.Head };
}
throw new Error(`Unable to parse HEAD file. HEAD file contents: ${raw}.`);
}
async findTrackingBranches(upstreamBranch: string): Promise<Branch[]> {
@@ -1984,28 +2178,32 @@ export class Repository {
.map(([ref]) => ({ name: ref, type: RefType.Head } as Branch));
}
async getRefs(opts?: { sort?: 'alphabetically' | 'committerdate'; contains?: string; pattern?: string; count?: number }): Promise<Ref[]> {
const args = ['for-each-ref'];
if (opts?.count) {
args.push(`--count=${opts.count}`);
async getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]> {
if (cancellationToken && cancellationToken.isCancellationRequested) {
throw new CancellationError();
}
if (opts && opts.sort && opts.sort !== 'alphabetically') {
args.push('--sort', `-${opts.sort}`);
const args = ['for-each-ref'];
if (query.count) {
args.push(`--count=${query.count}`);
}
if (query.sort && query.sort !== 'alphabetically') {
args.push('--sort', `-${query.sort}`);
}
args.push('--format', '%(refname) %(objectname) %(*objectname)');
if (opts?.pattern) {
args.push(opts.pattern);
if (query.pattern) {
args.push(query.pattern.startsWith('refs/') ? query.pattern : `refs/${query.pattern}`);
}
if (opts?.contains) {
args.push('--contains', opts.contains);
if (query.contains) {
args.push('--contains', query.contains);
}
const result = await this.exec(args);
const result = await this.exec(args, { cancellationToken });
const fn = (line: string): Ref | null => {
let match: RegExpExecArray | null;
@@ -2027,6 +2225,43 @@ export class Repository {
.filter(ref => !!ref) as Ref[];
}
async getRemoteRefs(remote: string, opts?: { heads?: boolean; tags?: boolean; cancellationToken?: CancellationToken }): Promise<Ref[]> {
if (opts?.cancellationToken && opts?.cancellationToken.isCancellationRequested) {
throw new CancellationError();
}
const args = ['ls-remote'];
if (opts?.heads) {
args.push('--heads');
}
if (opts?.tags) {
args.push('--tags');
}
args.push(remote);
const result = await this.exec(args, { cancellationToken: opts?.cancellationToken });
const fn = (line: string): Ref | null => {
let match: RegExpExecArray | null;
if (match = /^([0-9a-f]{40})\trefs\/heads\/([^ ]+)$/.exec(line)) {
return { name: match[1], commit: match[2], type: RefType.Head };
} else if (match = /^([0-9a-f]{40})\trefs\/tags\/([^ ]+)$/.exec(line)) {
return { name: match[2], commit: match[1], type: RefType.Tag };
}
return null;
};
return result.stdout.split('\n')
.filter(line => !!line)
.map(fn)
.filter(ref => !!ref) as Ref[];
}
async getStashes(): Promise<Stash[]> {
const result = await this.exec(['stash', 'list']);
const regex = /^stash@{(\d+)}:(.+)$/;
@@ -2040,9 +2275,41 @@ export class Repository {
}
async getRemotes(): Promise<Remote[]> {
const remotes: MutableRemote[] = [];
try {
// Attempt to parse the config file
remotes.push(...await this.getRemotesFS());
if (remotes.length === 0) {
this.logger.info('No remotes found in the git config file.');
}
}
catch (err) {
this.logger.warn(`getRemotes() - ${err.message}`);
// Fallback to using git to get the remotes
remotes.push(...await this.getRemotesGit());
}
for (const remote of remotes) {
// https://github.com/microsoft/vscode/issues/45271
remote.isReadOnly = remote.pushUrl === undefined || remote.pushUrl === 'no_push';
}
return remotes;
}
private async getRemotesFS(): Promise<MutableRemote[]> {
const raw = await fs.readFile(path.join(this.dotGit.commonPath ?? this.dotGit.path, 'config'), 'utf8');
return parseGitRemotes(raw);
}
private async getRemotesGit(): Promise<MutableRemote[]> {
const remotes: MutableRemote[] = [];
const result = await this.exec(['remote', '--verbose']);
const lines = result.stdout.trim().split('\n').filter(l => !!l);
const remotes: MutableRemote[] = [];
for (const line of lines) {
const parts = line.split(/\s/);
@@ -2063,9 +2330,6 @@ export class Repository {
remote.fetchUrl = url;
remote.pushUrl = url;
}
// https://github.com/microsoft/vscode/issues/45271
remote.isReadOnly = remote.pushUrl === undefined || remote.pushUrl === 'no_push';
}
return remotes;
@@ -2154,11 +2418,6 @@ export class Repository {
return Promise.reject<Branch>(new Error('No such branch'));
}
async getBranches(query: BranchQuery): Promise<Ref[]> {
const refs = await this.getRefs({ contains: query.contains, pattern: query.pattern ? `refs/${query.pattern}` : undefined, count: query.count });
return refs.filter(value => (value.type !== RefType.Tag) && (query.remote || !value.remote));
}
// TODO: Support core.commentChar
stripCommitMessageComments(message: string): string {
return message.replace(/^\s*#.*$\n?/gm, '').trim();
+1 -1
View File
@@ -17,7 +17,7 @@ function getIPCHandlePath(id: string): string {
return `\\\\.\\pipe\\vscode-git-${id}-sock`;
}
if (process.env['XDG_RUNTIME_DIR']) {
if (process.platform !== 'darwin' && process.env['XDG_RUNTIME_DIR']) {
return path.join(process.env['XDG_RUNTIME_DIR'] as string, `vscode-git-${id}.sock`);
}
-115
View File
@@ -1,115 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { commands, Disposable, Event, EventEmitter, OutputChannel, window, workspace } from 'vscode';
import { dispose } from './util';
/**
* The severity level of a log message
*/
export enum LogLevel {
Trace = 1,
Debug = 2,
Info = 3,
Warning = 4,
Error = 5,
Critical = 6,
Off = 7
}
/**
* Output channel logger
*/
export class OutputChannelLogger {
private _onDidChangeLogLevel = new EventEmitter<LogLevel>();
readonly onDidChangeLogLevel: Event<LogLevel> = this._onDidChangeLogLevel.event;
private _currentLogLevel!: LogLevel;
get currentLogLevel(): LogLevel {
return this._currentLogLevel;
}
set currentLogLevel(value: LogLevel) {
if (this._currentLogLevel === value) {
return;
}
this._currentLogLevel = value;
this._onDidChangeLogLevel.fire(value);
this.log(localize('gitLogLevel', "Log level: {0}", LogLevel[value]));
}
private _defaultLogLevel!: LogLevel;
get defaultLogLevel(): LogLevel {
return this._defaultLogLevel;
}
private _outputChannel: OutputChannel;
private _disposables: Disposable[] = [];
constructor() {
// Output channel
this._outputChannel = window.createOutputChannel('Git');
commands.registerCommand('git.showOutput', () => this.showOutputChannel());
this._disposables.push(this._outputChannel);
this._disposables.push(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git.logLevel')) {
this.onLogLevelChange();
}
}));
this.onLogLevelChange();
}
private onLogLevelChange(): void {
const config = workspace.getConfiguration('git');
const logLevel: keyof typeof LogLevel = config.get('logLevel', 'Info');
this.currentLogLevel = this._defaultLogLevel = LogLevel[logLevel] ?? LogLevel.Info;
}
log(message: string, logLevel?: LogLevel): void {
if (logLevel && logLevel < this._currentLogLevel) {
return;
}
this._outputChannel.appendLine(`[${new Date().toISOString()}]${logLevel ? ` [${LogLevel[logLevel].toLowerCase()}]` : ''} ${message}`);
}
logCritical(message: string): void {
this.log(message, LogLevel.Critical);
}
logDebug(message: string): void {
this.log(message, LogLevel.Debug);
}
logError(message: string): void {
this.log(message, LogLevel.Error);
}
logInfo(message: string): void {
this.log(message, LogLevel.Info);
}
logTrace(message: string): void {
this.log(message, LogLevel.Trace);
}
logWarning(message: string): void {
this.log(message, LogLevel.Warning);
}
showOutputChannel(): void {
this._outputChannel.show();
}
dispose(): void {
this._disposables = dispose(this._disposables);
}
}
+36 -29
View File
@@ -3,11 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ExtensionContext, workspace, Disposable, commands } from 'vscode';
import { findGit, Git } from './git';
import { ExtensionContext, workspace, window, Disposable, commands, LogOutputChannel, l10n, LogLevel } from 'vscode'; // {{SQL CARBON EDIT}} - remove unused
import { findGit, Git } from './git'; // {{SQL CARBON EDIT}} - remove unused
import { Model } from './model';
import { CommandCenter } from './commands';
import { GitFileSystemProvider } from './fileSystemProvider';
@@ -19,14 +16,15 @@ import { GitExtension } from './api/git';
import { GitProtocolHandler } from './protocolHandler';
import { GitExtensionImpl } from './api/extension';
import * as path from 'path';
// import * as fs from 'fs';
// import * as fs from 'fs'; // {{SQL CARBON EDIT}} - remove unused
import * as os from 'os';
import { GitTimelineProvider } from './timelineProvider';
import { registerAPICommands } from './api/api1';
import { TerminalEnvironmentManager } from './terminal';
import { OutputChannelLogger } from './log';
import { createIPCServer, IPCServer } from './ipc/ipcServer';
import { GitEditor } from './gitEditor';
// import { GitPostCommitCommandsProvider } from './postCommitCommands'; // {{SQL CARBON EDIT}} - remove unused
import { GitEditSessionIdentityProvider } from './editSessionIdentityProvider';
const deactivateTasks: { (): Promise<any> }[] = [];
@@ -36,7 +34,7 @@ export async function deactivate(): Promise<any> {
}
}
async function createModel(context: ExtensionContext, outputChannelLogger: OutputChannelLogger, telemetryReporter: TelemetryReporter, disposables: Disposable[]): Promise<Model> {
async function createModel(context: ExtensionContext, logger: LogOutputChannel, telemetryReporter: TelemetryReporter, disposables: Disposable[]): Promise<Model> {
const pathValue = workspace.getConfiguration('git').get<string | string[]>('path');
let pathHints = Array.isArray(pathValue) ? pathValue : pathValue ? [pathValue] : [];
@@ -49,7 +47,7 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
}
const info = await findGit(pathHints, gitPath => {
outputChannelLogger.logInfo(localize('validating', "Validating found git in: {0}", gitPath));
logger.info(l10n.t('Validating found git in: "{0}"', gitPath));
if (excludes.length === 0) {
return true;
}
@@ -57,7 +55,7 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
const normalized = path.normalize(gitPath).replace(/[\r\n]+$/, '');
const skip = excludes.some(e => normalized.startsWith(e));
if (skip) {
outputChannelLogger.logInfo(localize('skipped', "Skipped found git in: {0}", gitPath));
logger.info(l10n.t('Skipped found git in: "{0}"', gitPath));
}
return !skip;
});
@@ -67,7 +65,7 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
try {
ipcServer = await createIPCServer(context.storagePath);
} catch (err) {
outputChannelLogger.logError(`Failed to create git IPC: ${err}`);
logger.error(`Failed to create git IPC: ${err}`);
}
const askpass = new Askpass(ipcServer);
@@ -80,15 +78,15 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
const terminalEnvironmentManager = new TerminalEnvironmentManager(context, [askpass, gitEditor, ipcServer]);
disposables.push(terminalEnvironmentManager);
outputChannelLogger.logInfo(localize('using git', "Using git {0} from {1}", info.version, info.path));
logger.info(l10n.t('Using git "{0}" from "{1}"', info.version, info.path));
const git = new Git({
gitPath: info.path,
userAgent: `git/${info.version} (${(os as any).version?.() ?? os.type()} ${os.release()}; ${os.platform()} ${os.arch()}) azuredatudio`,
userAgent: `git/${info.version} (${(os as any).version?.() ?? os.type()} ${os.release()}; ${os.platform()} ${os.arch()}) azuredatudio`, // {{SQL CARBON EDIT}} - update product name
version: info.version,
env: environment,
});
const model = new Model(git, askpass, context.globalState, outputChannelLogger, telemetryReporter);
const model = new Model(git, askpass, context.globalState, logger, telemetryReporter);
disposables.push(model);
const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`);
@@ -103,24 +101,25 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
lines.pop();
}
outputChannelLogger.log(lines.join('\n'));
logger.appendLine(lines.join('\n'));
};
git.onOutput.addListener('log', onOutput);
disposables.push(toDisposable(() => git.onOutput.removeListener('log', onOutput)));
const cc = new CommandCenter(git, model, outputChannelLogger, telemetryReporter);
const cc = new CommandCenter(git, model, context.globalState, logger, telemetryReporter);
disposables.push(
cc,
new GitFileSystemProvider(model),
new GitDecorations(model),
new GitProtocolHandler(),
new GitTimelineProvider(model, cc)
new GitTimelineProvider(model, cc),
new GitEditSessionIdentityProvider(model)
);
// const postCommitCommandsProvider = new GitPostCommitCommandsProvider(); {{SQL CARBON TODO}} lewissanchez - Do we need this?
// model.registerPostCommitCommandsProvider(postCommitCommandsProvider); {{SQL CARBON TODO}} lewissanchez - Do we need this?
// checkGitVersion(info); {{SQL CARBON EDIT}} Don't check git version
// commands.executeCommand('setContext', 'gitVersion2.35', git.compareGitVersionTo('2.35') >= 0);
return model;
}
@@ -159,10 +158,10 @@ async function warnAboutMissingGit(): Promise<void> {
return;
}
const download = localize('downloadgit', "Download Git");
const neverShowAgain = localize('neverShowAgain', "Don't Show Again");
const download = l10n.t('Download Git');
const neverShowAgain = l10n.t('Don\'t Show Again');
const choice = await window.showWarningMessage(
localize('notfound', "Git not found. Install it or configure it using the 'git.path' setting."),
l10n.t('Git not found. Install it or configure it using the "git.path" setting.'),
download,
neverShowAgain
);
@@ -172,17 +171,23 @@ async function warnAboutMissingGit(): Promise<void> {
} else if (choice === neverShowAgain) {
await config.update('ignoreMissingGitWarning', true, true);
}
}*/
}*/ // {{SQl CARBON EDIT}} - end comment block
export async function _activate(context: ExtensionContext): Promise<GitExtensionImpl> {
const disposables: Disposable[] = [];
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
const outputChannelLogger = new OutputChannelLogger();
disposables.push(outputChannelLogger);
const logger = window.createOutputChannel('Git', { log: true });
disposables.push(logger);
const { name, version, aiKey } = require('../package.json') as { name: string; version: string; aiKey: string };
const telemetryReporter = new TelemetryReporter(name, version, aiKey);
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);
deactivateTasks.push(() => telemetryReporter.dispose());
const config = workspace.getConfiguration('git', null);
@@ -193,12 +198,12 @@ export async function _activate(context: ExtensionContext): Promise<GitExtension
const onEnabled = filterEvent(onConfigChange, () => workspace.getConfiguration('git', null).get<boolean>('enabled') === true);
const result = new GitExtensionImpl();
eventToPromise(onEnabled).then(async () => result.model = await createModel(context, outputChannelLogger, telemetryReporter, disposables));
eventToPromise(onEnabled).then(async () => result.model = await createModel(context, logger, telemetryReporter, disposables));
return result;
}
try {
const model = await createModel(context, outputChannelLogger, telemetryReporter, disposables);
const model = await createModel(context, logger, telemetryReporter, disposables);
return new GitExtensionImpl(model);
} catch (err) {
if (!/Git installation not found/.test(err.message || '')) {
@@ -206,7 +211,7 @@ export async function _activate(context: ExtensionContext): Promise<GitExtension
}
// console.warn(err.message); {{SQL CARBON EDIT}} turn-off Git missing prompt
// outputChannel.appendLine(err.message); {{SQL CARBON EDIT}} turn-off Git missing prompt
// logger.warn(err.message);
/* __GDPR__
"git.missing" : {
@@ -219,6 +224,8 @@ export async function _activate(context: ExtensionContext): Promise<GitExtension
// warnAboutMissingGit(); {{SQL CARBON EDIT}} turn-off Git missing prompt
return new GitExtensionImpl();
} finally {
disposables.push(new GitProtocolHandler(logger));
}
}
+342 -60
View File
@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands } from 'vscode';
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands, LogOutputChannel, l10n, ProgressLocation, WorkspaceFolder } from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { Repository, RepositoryState } from './repository';
import { memoize, sequentialize, debounce } from './decorators';
@@ -11,17 +11,14 @@ import { dispose, anyEvent, filterEvent, isDescendant, pathEquals, toDisposable,
import { Git } from './git';
import * as path from 'path';
import * as fs from 'fs';
import * as nls from 'vscode-nls';
import { fromGitUri } from './uri';
import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider } from './api/git';
import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider } from './api/git';
import { Askpass } from './askpass';
import { IPushErrorHandlerRegistry } from './pushError';
import { ApiRepository } from './api/api1';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
import { OutputChannelLogger } from './log';
import { IPostCommitCommandsProviderRegistry } from './postCommitCommands';
const localize = nls.loadMessageBundle();
import { IBranchProtectionProviderRegistry } from './branchProtection';
class RepositoryPick implements QuickPickItem {
@memoize get label(): string {
@@ -37,6 +34,50 @@ class RepositoryPick implements QuickPickItem {
constructor(public readonly repository: Repository, public readonly index: number) { }
}
abstract class RepositoryMap<T = void> extends Map<string, T> {
constructor() {
super();
this.updateContextKey();
}
override set(key: string, value: T): this {
const result = super.set(key, value);
this.updateContextKey();
return result;
}
override delete(key: string): boolean {
const result = super.delete(key);
this.updateContextKey();
return result;
}
abstract updateContextKey(): void;
}
/**
* Key - normalized path used in user interface
* Value - path extracted from the output of the `git status` command
* used when calling `git config --global --add safe.directory`
*/
class UnsafeRepositoryMap extends RepositoryMap<string> {
updateContextKey(): void {
commands.executeCommand('setContext', 'git.unsafeRepositoryCount', this.size);
}
}
/**
* Key - normalized path used in user interface
* Value - value indicating whether the repository should be opened
*/
class ParentRepositoryMap extends RepositoryMap {
updateContextKey(): void {
commands.executeCommand('setContext', 'git.parentRepositoryCount', this.size);
}
}
export interface ModelChangeEvent {
repository: Repository;
uri: Uri;
@@ -51,7 +92,7 @@ interface OpenRepository extends Disposable {
repository: Repository;
}
export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry {
export class Model implements IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry {
private _onDidOpenRepository = new EventEmitter<Repository>();
readonly onDidOpenRepository: Event<Repository> = this._onDidOpenRepository.event;
@@ -111,12 +152,36 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
private _onDidChangePostCommitCommandsProviders = new EventEmitter<void>();
readonly onDidChangePostCommitCommandsProviders = this._onDidChangePostCommitCommandsProviders.event;
private showRepoOnHomeDriveRootWarning = true;
private branchProtectionProviders = new Map<string, Set<BranchProtectionProvider>>();
private _onDidChangeBranchProtectionProviders = new EventEmitter<Uri>();
readonly onDidChangeBranchProtectionProviders = this._onDidChangeBranchProtectionProviders.event;
private pushErrorHandlers = new Set<PushErrorHandler>();
private _unsafeRepositories = new UnsafeRepositoryMap();
get unsafeRepositories(): UnsafeRepositoryMap {
return this._unsafeRepositories;
}
private _parentRepositories = new ParentRepositoryMap();
get parentRepositories(): ParentRepositoryMap {
return this._parentRepositories;
}
/**
* We maintain a map containing both the path and the canonical path of the
* workspace folders. We are doing this as `git.exe` expands the symbolic links
* while there are scenarios in which VS Code does not.
*
* Key - path of the workspace folder
* Value - canonical path of the workspace folder
*/
private _workspaceFolders = new Map<string, string>();
private disposables: Disposable[] = [];
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private outputChannelLogger: OutputChannelLogger, private telemetryReporter: TelemetryReporter) {
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private logger: LogOutputChannel, private telemetryReporter: TelemetryReporter) {
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables);
@@ -134,11 +199,40 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
}
private async doInitialScan(): Promise<void> {
await Promise.all([
const config = workspace.getConfiguration('git');
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt');
// Initial repository scan function
const initialScanFn = () => Promise.all([
this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] }),
this.onDidChangeVisibleTextEditors(window.visibleTextEditors),
this.scanWorkspaceFolders()
]);
if (config.get<boolean>('showProgress', true)) {
await window.withProgress({ location: ProgressLocation.SourceControl }, initialScanFn);
} else {
await initialScanFn();
}
if (this._parentRepositories.size !== 0 &&
parentRepositoryConfig === 'prompt') {
// Parent repositories notification
this.showParentRepositoryNotification();
} else if (this._unsafeRepositories.size !== 0) {
// Unsafe repositories notification
this.showUnsafeRepositoryNotification();
}
/* __GDPR__
"git.repositoryInitialScan" : {
"owner": "lszomoru",
"autoRepositoryDetection": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Setting that controls the initial repository scan" },
"repositoryCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of repositories opened during initial repository scan" }
}
*/
this.telemetryReporter.sendTelemetryEvent('git.repositoryInitialScan', { autoRepositoryDetection: String(autoRepositoryDetection) }, { repositoryCount: this.openRepositories.length });
}
/**
@@ -149,7 +243,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
private async scanWorkspaceFolders(): Promise<void> {
const config = workspace.getConfiguration('git');
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
this.outputChannelLogger.logTrace(`[swsf] Scan workspace sub folders. autoRepositoryDetection=${autoRepositoryDetection}`);
this.logger.trace(`[swsf] Scan workspace sub folders. autoRepositoryDetection=${autoRepositoryDetection}`);
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') {
return;
@@ -157,7 +251,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
await Promise.all((workspace.workspaceFolders || []).map(async folder => {
const root = folder.uri.fsPath;
this.outputChannelLogger.logTrace(`[swsf] Workspace folder: ${root}`);
this.logger.trace(`[swsf] Workspace folder: ${root}`);
// Workspace folder children
const repositoryScanMaxDepth = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<number>('repositoryScanMaxDepth', 1);
@@ -167,17 +261,17 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
// Repository scan folders
const scanPaths = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<string[]>('scanRepositories') || [];
this.outputChannelLogger.logTrace(`[swsf] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`);
this.logger.trace(`[swsf] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`);
for (const scanPath of scanPaths) {
if (scanPath === '.git') {
this.outputChannelLogger.logTrace('[swsf] \'.git\' not supported in \'git.scanRepositories\' setting.');
this.logger.trace('[swsf] \'.git\' not supported in \'git.scanRepositories\' setting.');
continue;
}
if (path.isAbsolute(scanPath)) {
const notSupportedMessage = localize('not supported', "Absolute paths not supported in 'git.scanRepositories' setting.");
this.outputChannelLogger.logWarning(notSupportedMessage);
const notSupportedMessage = l10n.t('Absolute paths not supported in "git.scanRepositories" setting.');
this.logger.warn(notSupportedMessage);
console.warn(notSupportedMessage);
continue;
}
@@ -185,7 +279,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
subfolders.add(path.join(root, scanPath));
}
this.outputChannelLogger.logTrace(`[swsf] Workspace scan sub folders: [${[...subfolders].join(', ')}]`);
this.logger.trace(`[swsf] Workspace scan sub folders: [${[...subfolders].join(', ')}]`);
await Promise.all([...subfolders].map(f => this.openRepository(f)));
}));
}
@@ -256,7 +350,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
.filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[];
openRepositoriesToDispose.forEach(r => r.dispose());
this.outputChannelLogger.logTrace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
this.logger.trace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath)));
}
@@ -270,20 +364,20 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
.filter(({ root }) => workspace.getConfiguration('git', root).get<boolean>('enabled') !== true)
.map(({ repository }) => repository);
this.outputChannelLogger.logTrace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
this.logger.trace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath));
openRepositoriesToDispose.forEach(r => r.dispose());
}
private async onDidChangeVisibleTextEditors(editors: readonly TextEditor[]): Promise<void> {
if (!workspace.isTrusted) {
this.outputChannelLogger.logTrace('[svte] Workspace is not trusted.');
this.logger.trace('[svte] Workspace is not trusted.');
return;
}
const config = workspace.getConfiguration('git');
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
this.outputChannelLogger.logTrace(`[svte] Scan visible text editors. autoRepositoryDetection=${autoRepositoryDetection}`);
this.logger.trace(`[svte] Scan visible text editors. autoRepositoryDetection=${autoRepositoryDetection}`);
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') {
return;
@@ -299,20 +393,20 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
const repository = this.getRepository(uri);
if (repository) {
this.outputChannelLogger.logTrace(`[svte] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`);
this.logger.trace(`[svte] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`);
return;
}
this.outputChannelLogger.logTrace(`[svte] Open repository for editor resource ${uri.fsPath}`);
this.logger.trace(`[svte] Open repository for editor resource ${uri.fsPath}`);
await this.openRepository(path.dirname(uri.fsPath));
}));
}
@sequentialize
async openRepository(repoPath: string): Promise<void> {
this.outputChannelLogger.logTrace(`Opening repository: ${repoPath}`);
if (this.getRepository(repoPath)) {
this.outputChannelLogger.logTrace(`Repository for path ${repoPath} already exists`);
this.logger.trace(`Opening repository: ${repoPath}`);
if (this.getRepositoryExact(repoPath)) {
this.logger.trace(`Repository for path ${repoPath} already exists`);
return;
}
@@ -320,7 +414,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
const enabled = config.get<boolean>('enabled') === true;
if (!enabled) {
this.outputChannelLogger.logTrace('Git is not enabled');
this.logger.trace('Git is not enabled');
return;
}
@@ -330,7 +424,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
fs.accessSync(path.join(repoPath, 'HEAD'), fs.constants.F_OK);
const result = await this.git.exec(repoPath, ['-C', repoPath, 'rev-parse', '--show-cdup']);
if (result.stderr.trim() === '' && result.stdout.trim() === '') {
this.outputChannelLogger.logTrace(`Bare repository: ${repoPath}`);
this.logger.trace(`Bare repository: ${repoPath}`);
return;
}
} catch {
@@ -339,49 +433,88 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
}
try {
const rawRoot = await this.git.getRepositoryRoot(repoPath);
const { repositoryRoot, unsafeRepositoryMatch } = await this.getRepositoryRoot(repoPath);
this.logger.trace(`Repository root for path ${repoPath} is: ${repositoryRoot}`);
// This can happen whenever `path` has the wrong case sensitivity in
// case insensitive file systems
// https://github.com/microsoft/vscode/issues/33498
const repositoryRoot = Uri.file(rawRoot).fsPath;
this.outputChannelLogger.logTrace(`Repository root: ${repositoryRoot}`);
if (this.getRepository(repositoryRoot)) {
this.outputChannelLogger.logTrace(`Repository for path ${repositoryRoot} already exists`);
if (this.getRepositoryExact(repositoryRoot)) {
this.logger.trace(`Repository for path ${repositoryRoot} already exists`);
return;
}
if (this.shouldRepositoryBeIgnored(rawRoot)) {
this.outputChannelLogger.logTrace(`Repository for path ${repositoryRoot} is ignored`);
if (this.shouldRepositoryBeIgnored(repositoryRoot)) {
this.logger.trace(`Repository for path ${repositoryRoot} is ignored`);
return;
}
// On Window, opening a git repository from the root of the HOMEDRIVE poses a security risk.
// We will only a open git repository from the root of the HOMEDRIVE if the user explicitly
// opens the HOMEDRIVE as a folder. Only show the warning once during repository discovery.
if (process.platform === 'win32' && process.env.HOMEDRIVE && pathEquals(`${process.env.HOMEDRIVE}\\`, repositoryRoot)) {
const isRepoInWorkspaceFolders = (workspace.workspaceFolders ?? []).find(f => pathEquals(f.uri.fsPath, repositoryRoot))!!;
// Handle git repositories that are in parent folders
const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt');
if (parentRepositoryConfig !== 'always' && this.globalState.get<boolean>(`parentRepository:${repositoryRoot}`) !== true) {
const isRepositoryOutsideWorkspace = await this.isRepositoryOutsideWorkspace(repositoryRoot);
if (isRepositoryOutsideWorkspace) {
this.logger.trace(`Repository in parent folder: ${repositoryRoot}`);
if (!isRepoInWorkspaceFolders) {
if (this.showRepoOnHomeDriveRootWarning) {
window.showWarningMessage(localize('repoOnHomeDriveRootWarning', "Unable to automatically open the git repository at '{0}'. To open that git repository, open it directly as a folder in VS Code.", repositoryRoot));
this.showRepoOnHomeDriveRootWarning = false;
if (!this._parentRepositories.has(repositoryRoot)) {
// Show a notification if the parent repository is opened after the initial scan
if (this.state === 'initialized' && parentRepositoryConfig === 'prompt') {
this.showParentRepositoryNotification();
}
this._parentRepositories.set(repositoryRoot);
}
this.outputChannelLogger.logTrace(`Repository for path ${repositoryRoot} is on the root of the HOMEDRIVE`);
return;
}
}
// Handle unsafe repositories
if (unsafeRepositoryMatch && unsafeRepositoryMatch.length === 3) {
this.logger.trace(`Unsafe repository: ${repositoryRoot}`);
// Show a notification if the unsafe repository is opened after the initial scan
if (this._state === 'initialized' && !this._unsafeRepositories.has(repositoryRoot)) {
this.showUnsafeRepositoryNotification();
}
this._unsafeRepositories.set(repositoryRoot, unsafeRepositoryMatch[2]);
return;
}
// Open repository
const dotGit = await this.git.getRepositoryDotGit(repositoryRoot);
const repository = new Repository(this.git.open(repositoryRoot, dotGit), this, this, this, this.globalState, this.outputChannelLogger, this.telemetryReporter);
const repository = new Repository(this.git.open(repositoryRoot, dotGit, this.logger), this, this, this, this, this.globalState, this.logger, this.telemetryReporter);
this.open(repository);
repository.status(); // do not await this, we want SCM to know about the repo asap
} catch (ex) {
} catch (err) {
// noop
this.outputChannelLogger.logTrace(`Opening repository for path='${repoPath}' failed; ex=${ex}`);
this.logger.trace(`Opening repository for path='${repoPath}' failed; ex=${err}`);
}
}
async openParentRepository(repoPath: string): Promise<void> {
// Mark the repository to be opened from the parent folders
this.globalState.update(`parentRepository:${repoPath}`, true);
await this.openRepository(repoPath);
this.parentRepositories.delete(repoPath);
}
private async getRepositoryRoot(repoPath: string): Promise<{ repositoryRoot: string; unsafeRepositoryMatch: RegExpMatchArray | null }> {
try {
const rawRoot = await this.git.getRepositoryRoot(repoPath);
// This can happen whenever `path` has the wrong case sensitivity in case
// insensitive file systems https://github.com/microsoft/vscode/issues/33498
return { repositoryRoot: Uri.file(rawRoot).fsPath, unsafeRepositoryMatch: null };
} catch (err) {
// Handle unsafe repository
const unsafeRepositoryMatch = /^fatal: detected dubious ownership in repository at \'([^']+)\'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec(err.stderr);
if (unsafeRepositoryMatch && unsafeRepositoryMatch.length === 3) {
return { repositoryRoot: path.normalize(unsafeRepositoryMatch[1]), unsafeRepositoryMatch };
}
throw err;
}
}
@@ -407,7 +540,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
}
private open(repository: Repository): void {
this.outputChannelLogger.logInfo(`Open repository: ${repository.root}`);
this.logger.info(`Open repository: ${repository.root}`);
const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed);
const disappearListener = onDidDisappearRepository(() => dispose());
@@ -424,28 +557,62 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
const checkForSubmodules = () => {
if (!shouldDetectSubmodules) {
this.logger.trace('Automatic detection of git submodules is not enabled.');
return;
}
if (repository.submodules.length > submodulesLimit) {
window.showWarningMessage(localize('too many submodules', "The '{0}' repository has {1} submodules which won't be opened automatically. You can still open each one individually by opening a file within.", path.basename(repository.root), repository.submodules.length));
window.showWarningMessage(l10n.t('The "{0}" repository has {1} submodules which won\'t be opened automatically. You can still open each one individually by opening a file within.', path.basename(repository.root), repository.submodules.length));
statusListener.dispose();
}
repository.submodules
.slice(0, submodulesLimit)
.map(r => path.join(repository.root, r.path))
.forEach(p => this.eventuallyScanPossibleGitRepository(p));
.forEach(p => {
this.logger.trace(`Opening submodule: '${p}'`);
this.eventuallyScanPossibleGitRepository(p);
});
};
const statusListener = repository.onDidRunGitStatus(checkForSubmodules);
const updateMergeChanges = () => {
// set mergeChanges context
const mergeChanges: Uri[] = [];
for (const { repository } of this.openRepositories.values()) {
for (const state of repository.mergeGroup.resourceStates) {
mergeChanges.push(state.resourceUri);
}
}
commands.executeCommand('setContext', 'git.mergeChanges', mergeChanges);
};
const statusListener = repository.onDidRunGitStatus(() => {
checkForSubmodules();
updateMergeChanges();
});
checkForSubmodules();
const updateOperationInProgressContext = () => {
let operationInProgress = false;
for (const { repository } of this.openRepositories.values()) {
if (repository.operations.shouldDisableCommands()) {
operationInProgress = true;
}
}
commands.executeCommand('setContext', 'operationInProgress', operationInProgress);
};
const operationEvent = anyEvent(repository.onDidRunOperation as Event<any>, repository.onRunOperation as Event<any>);
const operationListener = operationEvent(() => updateOperationInProgressContext());
updateOperationInProgressContext();
const dispose = () => {
disappearListener.dispose();
changeListener.dispose();
originalResourceChangeListener.dispose();
statusListener.dispose();
operationListener.dispose();
repository.dispose();
this.openRepositories = this.openRepositories.filter(e => e !== openRepository);
@@ -454,6 +621,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
const openRepository = { repository, dispose };
this.openRepositories.push(openRepository);
updateMergeChanges();
this._onDidOpenRepository.fire(repository);
}
@@ -464,13 +632,13 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
return;
}
this.outputChannelLogger.logInfo(`Close repository: ${repository.root}`);
this.logger.info(`Close repository: ${repository.root}`);
openRepository.dispose();
}
async pickRepository(): Promise<Repository | undefined> {
if (this.openRepositories.length === 0) {
throw new Error(localize('no repositories', "There are no available repositories"));
throw new Error(l10n.t('There are no available repositories'));
}
const picks = this.openRepositories.map((e, index) => new RepositoryPick(e.repository, index));
@@ -483,7 +651,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
picks.unshift(...picks.splice(index, 1));
}
const placeHolder = localize('pick repo', "Choose a repository");
const placeHolder = l10n.t('Choose a repository');
const pick = await window.showQuickPick(picks, { placeHolder });
return pick && pick.repository;
@@ -498,6 +666,12 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
return liveRepository && liveRepository.repository;
}
private getRepositoryExact(repoPath: string): Repository | undefined {
const openRepository = this.openRepositories
.find(r => pathEquals(r.repository.root, repoPath));
return openRepository?.repository;
}
private getOpenRepository(repository: Repository): OpenRepository | undefined;
private getOpenRepository(sourceControl: SourceControl): OpenRepository | undefined;
private getOpenRepository(resourceGroup: SourceControlResourceGroup): OpenRepository | undefined;
@@ -556,7 +730,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
return liveRepository;
}
if (hint === repository.mergeGroup || hint === repository.indexGroup || hint === repository.workingTreeGroup) {
if (hint === repository.mergeGroup || hint === repository.indexGroup || hint === repository.workingTreeGroup || hint === repository.untrackedGroup) {
return liveRepository;
}
}
@@ -592,6 +766,31 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
return [...this.remoteSourcePublishers.values()];
}
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable {
const providerDisposables: Disposable[] = [];
this.branchProtectionProviders.set(root.toString(), (this.branchProtectionProviders.get(root.toString()) ?? new Set()).add(provider));
providerDisposables.push(provider.onDidChangeBranchProtection(uri => this._onDidChangeBranchProtectionProviders.fire(uri)));
this._onDidChangeBranchProtectionProviders.fire(root);
return toDisposable(() => {
const providers = this.branchProtectionProviders.get(root.toString());
if (providers && providers.has(provider)) {
providers.delete(provider);
this.branchProtectionProviders.set(root.toString(), providers);
this._onDidChangeBranchProtectionProviders.fire(root);
}
dispose(providerDisposables);
});
}
getBranchProtectionProviders(root: Uri): BranchProtectionProvider[] {
return [...(this.branchProtectionProviders.get(root.toString()) ?? new Set()).values()];
}
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable {
this.postCommitCommandsProviders.add(provider);
this._onDidChangePostCommitCommandsProviders.fire();
@@ -619,6 +818,89 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
return [...this.pushErrorHandlers];
}
private async isRepositoryOutsideWorkspace(repositoryPath: string): Promise<boolean> {
const workspaceFolders = (workspace.workspaceFolders || [])
.filter(folder => folder.uri.scheme === 'file');
if (workspaceFolders.length === 0) {
return true;
}
const result = await Promise.all(workspaceFolders.map(async folder => {
const workspaceFolderRealPath = await this.getWorkspaceFolderRealPath(folder);
return workspaceFolderRealPath ? pathEquals(workspaceFolderRealPath, repositoryPath) || isDescendant(workspaceFolderRealPath, repositoryPath) : undefined;
}));
return !result.some(r => r);
}
private async getWorkspaceFolderRealPath(workspaceFolder: WorkspaceFolder): Promise<string | undefined> {
let result = this._workspaceFolders.get(workspaceFolder.uri.fsPath);
if (!result) {
try {
result = await fs.promises.realpath(workspaceFolder.uri.fsPath, { encoding: 'utf8' });
this._workspaceFolders.set(workspaceFolder.uri.fsPath, result);
} catch (err) {
// noop - Workspace folder does not exist
this.logger.trace(`Failed to resolve workspace folder: "${workspaceFolder.uri.fsPath}". ${err}`);
}
}
return result;
}
private async showParentRepositoryNotification(): Promise<void> {
const message = this.parentRepositories.size === 1 ?
l10n.t('A git repository was found in the parent folders of the workspace or the open file(s). Would you like to open the repository?') :
l10n.t('Git repositories were found in the parent folders of the workspace or the open file(s). Would you like to open the repositories?');
const yes = l10n.t('Yes');
const always = l10n.t('Always');
const never = l10n.t('Never');
const choice = await window.showInformationMessage(message, yes, always, never);
if (choice === yes) {
// Open Parent Repositories
commands.executeCommand('git.openRepositoriesInParentFolders');
} else if (choice === always || choice === never) {
// Update setting
const config = workspace.getConfiguration('git');
await config.update('openRepositoryInParentFolders', choice === always ? 'always' : 'never', true);
if (choice === always) {
for (const parentRepository of [...this.parentRepositories.keys()]) {
await this.openParentRepository(parentRepository);
}
}
}
}
private async showUnsafeRepositoryNotification(): Promise<void> {
// If no repositories are open, we will use a welcome view to inform the user
// that a potentially unsafe repository was found so we do not have to show
// the notification
if (this.repositories.length === 0) {
return;
}
const message = this._unsafeRepositories.size === 1 ?
l10n.t('The git repository in the current folder is potentially unsafe as the folder is owned by someone other than the current user.') :
l10n.t('The git repositories in the current folder are potentially unsafe as the folders are owned by someone other than the current user.');
const manageUnsafeRepositories = l10n.t('Manage Unsafe Repositories');
const learnMore = l10n.t('Learn More');
const choice = await window.showErrorMessage(message, manageUnsafeRepositories, learnMore);
if (choice === manageUnsafeRepositories) {
// Manage Unsafe Repositories
commands.executeCommand('git.manageUnsafeRepositories');
} else if (choice === learnMore) {
// Learn More
commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-git-unsafe-repository'));
}
}
dispose(): void {
const openRepositories = [...this.openRepositories];
openRepositories.forEach(r => r.dispose());
+273
View File
@@ -0,0 +1,273 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LogOutputChannel } from 'vscode';
export const enum OperationKind {
Add = 'Add',
AddNoProgress = 'AddNoProgress',
Apply = 'Apply',
Blame = 'Blame',
Branch = 'Branch',
CheckIgnore = 'CheckIgnore',
Checkout = 'Checkout',
CheckoutTracking = 'CheckoutTracking',
CherryPick = 'CherryPick',
Clean = 'Clean',
CleanNoProgress = 'CleanNoProgress',
Commit = 'Commit',
Config = 'Config',
DeleteBranch = 'DeleteBranch',
DeleteRef = 'DeleteRef',
DeleteRemoteTag = 'DeleteRemoteTag',
DeleteTag = 'DeleteTag',
Diff = 'Diff',
Fetch = 'Fetch',
FetchNoProgress = 'FetchNoProgress',
FindTrackingBranches = 'GetTracking',
GetBranch = 'GetBranch',
GetBranches = 'GetBranches',
GetCommitTemplate = 'GetCommitTemplate',
GetObjectDetails = 'GetObjectDetails',
GetRefs = 'GetRefs',
GetRemoteRefs = 'GetRemoteRefs',
HashObject = 'HashObject',
Ignore = 'Ignore',
Log = 'Log',
LogFile = 'LogFile',
Merge = 'Merge',
MergeAbort = 'MergeAbort',
MergeBase = 'MergeBase',
Move = 'Move',
PostCommitCommand = 'PostCommitCommand',
Pull = 'Pull',
Push = 'Push',
Remote = 'Remote',
RenameBranch = 'RenameBranch',
Remove = 'Remove',
Reset = 'Reset',
Rebase = 'Rebase',
RebaseAbort = 'RebaseAbort',
RebaseContinue = 'RebaseContinue',
RevertFiles = 'RevertFiles',
RevertFilesNoProgress = 'RevertFilesNoProgress',
SetBranchUpstream = 'SetBranchUpstream',
Show = 'Show',
Stage = 'Stage',
Status = 'Status',
Stash = 'Stash',
SubmoduleUpdate = 'SubmoduleUpdate',
Sync = 'Sync',
Tag = 'Tag',
}
export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchOperation | CheckIgnoreOperation | CherryPickOperation |
CheckoutOperation | CheckoutTrackingOperation | CleanOperation | CommitOperation | ConfigOperation | DeleteBranchOperation |
DeleteRefOperation | DeleteRemoteTagOperation | DeleteTagOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation |
GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetRefsOperation | GetRemoteRefsOperation |
HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | MergeBaseOperation |
MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation | RemoveOperation |
ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RevertFilesOperation | SetBranchUpstreamOperation |
ShowOperation | StageOperation | StatusOperation | StashOperation | SubmoduleUpdateOperation | SyncOperation | TagOperation;
type BaseOperation = { kind: OperationKind; blocking: boolean; readOnly: boolean; remote: boolean; retry: boolean; showProgress: boolean };
export type AddOperation = BaseOperation & { kind: OperationKind.Add };
export type ApplyOperation = BaseOperation & { kind: OperationKind.Apply };
export type BlameOperation = BaseOperation & { kind: OperationKind.Blame };
export type BranchOperation = BaseOperation & { kind: OperationKind.Branch };
export type CheckIgnoreOperation = BaseOperation & { kind: OperationKind.CheckIgnore };
export type CherryPickOperation = BaseOperation & { kind: OperationKind.CherryPick };
export type CheckoutOperation = BaseOperation & { kind: OperationKind.Checkout; refLabel: string };
export type CheckoutTrackingOperation = BaseOperation & { kind: OperationKind.CheckoutTracking; refLabel: string };
export type CleanOperation = BaseOperation & { kind: OperationKind.Clean };
export type CommitOperation = BaseOperation & { kind: OperationKind.Commit };
export type ConfigOperation = BaseOperation & { kind: OperationKind.Config };
export type DeleteBranchOperation = BaseOperation & { kind: OperationKind.DeleteBranch };
export type DeleteRefOperation = BaseOperation & { kind: OperationKind.DeleteRef };
export type DeleteRemoteTagOperation = BaseOperation & { kind: OperationKind.DeleteRemoteTag };
export type DeleteTagOperation = BaseOperation & { kind: OperationKind.DeleteTag };
export type DiffOperation = BaseOperation & { kind: OperationKind.Diff };
export type FetchOperation = BaseOperation & { kind: OperationKind.Fetch };
export type FindTrackingBranchesOperation = BaseOperation & { kind: OperationKind.FindTrackingBranches };
export type GetBranchOperation = BaseOperation & { kind: OperationKind.GetBranch };
export type GetBranchesOperation = BaseOperation & { kind: OperationKind.GetBranches };
export type GetCommitTemplateOperation = BaseOperation & { kind: OperationKind.GetCommitTemplate };
export type GetObjectDetailsOperation = BaseOperation & { kind: OperationKind.GetObjectDetails };
export type GetRefsOperation = BaseOperation & { kind: OperationKind.GetRefs };
export type GetRemoteRefsOperation = BaseOperation & { kind: OperationKind.GetRemoteRefs };
export type HashObjectOperation = BaseOperation & { kind: OperationKind.HashObject };
export type IgnoreOperation = BaseOperation & { kind: OperationKind.Ignore };
export type LogOperation = BaseOperation & { kind: OperationKind.Log };
export type LogFileOperation = BaseOperation & { kind: OperationKind.LogFile };
export type MergeOperation = BaseOperation & { kind: OperationKind.Merge };
export type MergeAbortOperation = BaseOperation & { kind: OperationKind.MergeAbort };
export type MergeBaseOperation = BaseOperation & { kind: OperationKind.MergeBase };
export type MoveOperation = BaseOperation & { kind: OperationKind.Move };
export type PostCommitCommandOperation = BaseOperation & { kind: OperationKind.PostCommitCommand };
export type PullOperation = BaseOperation & { kind: OperationKind.Pull };
export type PushOperation = BaseOperation & { kind: OperationKind.Push };
export type RemoteOperation = BaseOperation & { kind: OperationKind.Remote };
export type RenameBranchOperation = BaseOperation & { kind: OperationKind.RenameBranch };
export type RemoveOperation = BaseOperation & { kind: OperationKind.Remove };
export type ResetOperation = BaseOperation & { kind: OperationKind.Reset };
export type RebaseOperation = BaseOperation & { kind: OperationKind.Rebase };
export type RebaseAbortOperation = BaseOperation & { kind: OperationKind.RebaseAbort };
export type RebaseContinueOperation = BaseOperation & { kind: OperationKind.RebaseContinue };
export type RevertFilesOperation = BaseOperation & { kind: OperationKind.RevertFiles };
export type SetBranchUpstreamOperation = BaseOperation & { kind: OperationKind.SetBranchUpstream };
export type ShowOperation = BaseOperation & { kind: OperationKind.Show };
export type StageOperation = BaseOperation & { kind: OperationKind.Stage };
export type StatusOperation = BaseOperation & { kind: OperationKind.Status };
export type StashOperation = BaseOperation & { kind: OperationKind.Stash };
export type SubmoduleUpdateOperation = BaseOperation & { kind: OperationKind.SubmoduleUpdate };
export type SyncOperation = BaseOperation & { kind: OperationKind.Sync };
export type TagOperation = BaseOperation & { kind: OperationKind.Tag };
export const Operation = {
Add: (showProgress: boolean) => ({ kind: OperationKind.Add, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as AddOperation),
Apply: { kind: OperationKind.Apply, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as ApplyOperation,
Blame: { kind: OperationKind.Blame, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as BlameOperation,
Branch: { kind: OperationKind.Branch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as BranchOperation,
CheckIgnore: { kind: OperationKind.CheckIgnore, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as CheckIgnoreOperation,
CherryPick: { kind: OperationKind.CherryPick, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as CherryPickOperation,
Checkout: (refLabel: string) => ({ kind: OperationKind.Checkout, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true, refLabel } as CheckoutOperation),
CheckoutTracking: (refLabel: string) => ({ kind: OperationKind.CheckoutTracking, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true, refLabel } as CheckoutTrackingOperation),
Clean: (showProgress: boolean) => ({ kind: OperationKind.Clean, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as CleanOperation),
Commit: { kind: OperationKind.Commit, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true } as CommitOperation,
Config: { kind: OperationKind.Config, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as ConfigOperation,
DeleteBranch: { kind: OperationKind.DeleteBranch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteBranchOperation,
DeleteRef: { kind: OperationKind.DeleteRef, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteRefOperation,
DeleteRemoteTag: { kind: OperationKind.DeleteRemoteTag, blocking: false, readOnly: false, remote: true, retry: false, showProgress: true } as DeleteRemoteTagOperation,
DeleteTag: { kind: OperationKind.DeleteTag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteTagOperation,
Diff: { kind: OperationKind.Diff, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as DiffOperation,
Fetch: (showProgress: boolean) => ({ kind: OperationKind.Fetch, blocking: false, readOnly: false, remote: true, retry: true, showProgress } as FetchOperation),
FindTrackingBranches: { kind: OperationKind.FindTrackingBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as FindTrackingBranchesOperation,
GetBranch: { kind: OperationKind.GetBranch, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetBranchOperation,
GetBranches: { kind: OperationKind.GetBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetBranchesOperation,
GetCommitTemplate: { kind: OperationKind.GetCommitTemplate, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetCommitTemplateOperation,
GetObjectDetails: { kind: OperationKind.GetObjectDetails, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectDetailsOperation,
GetRefs: { kind: OperationKind.GetRefs, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetRefsOperation,
GetRemoteRefs: { kind: OperationKind.GetRemoteRefs, blocking: false, readOnly: true, remote: true, retry: false, showProgress: false } as GetRemoteRefsOperation,
HashObject: { kind: OperationKind.HashObject, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as HashObjectOperation,
Ignore: { kind: OperationKind.Ignore, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as IgnoreOperation,
Log: { kind: OperationKind.Log, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as LogOperation,
LogFile: { kind: OperationKind.LogFile, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as LogFileOperation,
Merge: { kind: OperationKind.Merge, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as MergeOperation,
MergeAbort: { kind: OperationKind.MergeAbort, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as MergeAbortOperation,
MergeBase: { kind: OperationKind.MergeBase, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as MergeBaseOperation,
Move: { kind: OperationKind.Move, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as MoveOperation,
PostCommitCommand: { kind: OperationKind.PostCommitCommand, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as PostCommitCommandOperation,
Pull: { kind: OperationKind.Pull, blocking: true, readOnly: false, remote: true, retry: true, showProgress: true } as PullOperation,
Push: { kind: OperationKind.Push, blocking: true, readOnly: false, remote: true, retry: false, showProgress: true } as PushOperation,
Remote: { kind: OperationKind.Remote, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RemoteOperation,
RenameBranch: { kind: OperationKind.RenameBranch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RenameBranchOperation,
Remove: { kind: OperationKind.Remove, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RemoveOperation,
Reset: { kind: OperationKind.Reset, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as ResetOperation,
Rebase: { kind: OperationKind.Rebase, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseOperation,
RebaseAbort: { kind: OperationKind.RebaseAbort, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseAbortOperation,
RebaseContinue: { kind: OperationKind.RebaseContinue, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseContinueOperation,
RevertFiles: (showProgress: boolean) => ({ kind: OperationKind.RevertFiles, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as RevertFilesOperation),
SetBranchUpstream: { kind: OperationKind.SetBranchUpstream, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as SetBranchUpstreamOperation,
Show: { kind: OperationKind.Show, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as ShowOperation,
Stage: { kind: OperationKind.Stage, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StageOperation,
Status: { kind: OperationKind.Status, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StatusOperation,
Stash: { kind: OperationKind.Stash, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StashOperation,
SubmoduleUpdate: { kind: OperationKind.SubmoduleUpdate, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as SubmoduleUpdateOperation,
Sync: { kind: OperationKind.Sync, blocking: true, readOnly: false, remote: true, retry: true, showProgress: true } as SyncOperation,
Tag: { kind: OperationKind.Tag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as TagOperation
};
export interface OperationResult {
operation: Operation;
error: any;
}
interface IOperationManager {
getOperations(operationKind: OperationKind): Operation[];
isIdle(): boolean;
isRunning(operationKind: OperationKind): boolean;
shouldDisableCommands(): boolean;
shouldShowProgress(): boolean;
}
export class OperationManager implements IOperationManager {
private operations = new Map<OperationKind, Set<Operation>>();
constructor(private readonly logger: LogOutputChannel) { }
start(operation: Operation): void {
if (this.operations.has(operation.kind)) {
this.operations.get(operation.kind)!.add(operation);
} else {
this.operations.set(operation.kind, new Set([operation]));
}
this.logger.trace(`Operation start: ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`);
}
end(operation: Operation): void {
const operationSet = this.operations.get(operation.kind);
if (operationSet) {
operationSet.delete(operation);
if (operationSet.size === 0) {
this.operations.delete(operation.kind);
}
}
this.logger.trace(`Operation end: ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`);
}
getOperations(operationKind: OperationKind): Operation[] {
const operationSet = this.operations.get(operationKind);
return operationSet ? Array.from(operationSet) : [];
}
isIdle(): boolean {
const operationSets = this.operations.values();
for (const operationSet of operationSets) {
for (const operation of operationSet) {
if (!operation.readOnly) {
return false;
}
}
}
return true;
}
isRunning(operationKind: OperationKind): boolean {
return this.operations.has(operationKind);
}
shouldDisableCommands(): boolean {
const operationSets = this.operations.values();
for (const operationSet of operationSets) {
for (const operation of operationSet) {
if (operation.blocking) {
return true;
}
}
}
return false;
}
shouldShowProgress(): boolean {
const operationSets = this.operations.values();
for (const operationSet of operationSets) {
for (const operation of operationSet) {
if (operation.showProgress) {
return true;
}
}
}
return false;
}
}
+205 -7
View File
@@ -3,9 +3,12 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { Command, Disposable, Event } from 'vscode';
import { Command, commands, Disposable, Event, EventEmitter, Memento, Uri, workspace, l10n } from 'vscode';
import { PostCommitCommandsProvider } from './api/git';
import { Repository } from './repository';
import { ApiRepository } from './api/api1';
import { dispose } from './util';
import { OperationKind } from './operation';
export interface IPostCommitCommandsProviderRegistry {
readonly onDidChangePostCommitCommandsProviders: Event<void>;
@@ -14,19 +17,214 @@ export interface IPostCommitCommandsProviderRegistry {
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
}
const localize = nls.loadMessageBundle();
export class GitPostCommitCommandsProvider implements PostCommitCommandsProvider {
getCommands(): Command[] {
getCommands(apiRepository: ApiRepository): Command[] {
const config = workspace.getConfiguration('git', Uri.file(apiRepository.repository.root));
// Branch protection
const isBranchProtected = apiRepository.repository.isBranchProtected();
const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;
const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt';
const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch';
// Icon
const repository = apiRepository.repository;
const isCommitInProgress = repository.operations.isRunning(OperationKind.Commit) || repository.operations.isRunning(OperationKind.PostCommitCommand);
const icon = isCommitInProgress ? '$(sync~spin)' : alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined;
// Tooltip (default)
let pushCommandTooltip = !alwaysCommitToNewBranch ?
l10n.t('Commit & Push Changes') :
l10n.t('Commit to New Branch & Push Changes');
let syncCommandTooltip = !alwaysCommitToNewBranch ?
l10n.t('Commit & Sync Changes') :
l10n.t('Commit to New Branch & Synchronize Changes');
// Tooltip (in progress)
if (isCommitInProgress) {
pushCommandTooltip = !alwaysCommitToNewBranch ?
l10n.t('Committing & Pushing Changes...') :
l10n.t('Committing to New Branch & Pushing Changes...');
syncCommandTooltip = !alwaysCommitToNewBranch ?
l10n.t('Committing & Synchronizing Changes...') :
l10n.t('Committing to New Branch & Synchronizing Changes...');
}
return [
{
command: 'git.push',
title: localize('scm secondary button commit and push', "Commit & Push")
title: l10n.t('{0} Commit & Push', icon ?? '$(arrow-up)'),
tooltip: pushCommandTooltip
},
{
command: 'git.sync',
title: localize('scm secondary button commit and sync', "Commit & Sync")
title: l10n.t('{0} Commit & Sync', icon ?? '$(sync)'),
tooltip: syncCommandTooltip
},
];
}
}
export class CommitCommandsCenter {
private _onDidChange = new EventEmitter<void>();
get onDidChange(): Event<void> { return this._onDidChange.event; }
private disposables: Disposable[] = [];
set postCommitCommand(command: string | null | undefined) {
if (command === undefined) {
// Commit WAS NOT initiated using the action button
// so there is no need to store the post-commit command
return;
}
this.globalState.update(this.getGlobalStateKey(), command)
.then(() => this._onDidChange.fire());
}
constructor(
private readonly globalState: Memento,
private readonly repository: Repository,
private readonly postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry
) {
const root = Uri.file(repository.root);
// Migrate post commit command storage
this.migratePostCommitCommandStorage()
.then(() => {
const onRememberPostCommitCommandChange = async () => {
const config = workspace.getConfiguration('git', root);
if (!config.get<boolean>('rememberPostCommitCommand')) {
await this.globalState.update(this.getGlobalStateKey(), undefined);
}
};
this.disposables.push(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git.rememberPostCommitCommand', root)) {
onRememberPostCommitCommandChange();
}
}));
onRememberPostCommitCommandChange();
this.disposables.push(postCommitCommandsProviderRegistry.onDidChangePostCommitCommandsProviders(() => this._onDidChange.fire()));
});
}
getPrimaryCommand(): Command {
const allCommands = this.getSecondaryCommands().map(c => c).flat();
const commandFromStorage = allCommands.find(c => c.arguments?.length === 2 && c.arguments[1] === this.getPostCommitCommandStringFromStorage());
const commandFromSetting = allCommands.find(c => c.arguments?.length === 2 && c.arguments[1] === this.getPostCommitCommandStringFromSetting());
return commandFromStorage ?? commandFromSetting ?? this.getCommitCommand();
}
getSecondaryCommands(): Command[][] {
const commandGroups: Command[][] = [];
for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) {
const commands = provider.getCommands(new ApiRepository(this.repository));
commandGroups.push((commands ?? []).map(c => {
return { command: 'git.commit', title: c.title, tooltip: c.tooltip, arguments: [this.repository.sourceControl, c.command] };
}));
}
if (commandGroups.length > 0) {
commandGroups[0].splice(0, 0, this.getCommitCommand());
}
return commandGroups;
}
async executePostCommitCommand(command: string | null | undefined): Promise<void> {
try {
if (command === null) {
// No post-commit command
return;
}
if (command === undefined) {
// Commit WAS NOT initiated using the action button (ex: keybinding, toolbar action,
// command palette) so we have to honour the default post commit command (memento/setting).
const primaryCommand = this.getPrimaryCommand();
command = primaryCommand.arguments?.length === 2 ? primaryCommand.arguments[1] : null;
}
if (command !== null) {
await commands.executeCommand(command!.toString(), new ApiRepository(this.repository));
}
} catch (err) {
throw err;
}
finally {
if (!this.isRememberPostCommitCommandEnabled()) {
await this.globalState.update(this.getGlobalStateKey(), undefined);
this._onDidChange.fire();
}
}
}
private getGlobalStateKey(): string {
return `postCommitCommand:${this.repository.root}`;
}
private getCommitCommand(): Command {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
// Branch protection
const isBranchProtected = this.repository.isBranchProtected();
const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;
const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt';
const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch';
// Icon
const icon = alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined;
// Tooltip (default)
const branch = this.repository.HEAD?.name;
let tooltip = alwaysCommitToNewBranch ?
l10n.t('Commit Changes to New Branch') :
branch ?
l10n.t('Commit Changes on "{0}"', branch) :
l10n.t('Commit Changes');
// Tooltip (in progress)
if (this.repository.operations.isRunning(OperationKind.Commit)) {
tooltip = !alwaysCommitToNewBranch ?
l10n.t('Committing Changes...') :
l10n.t('Committing Changes to New Branch...');
}
return { command: 'git.commit', title: l10n.t('{0} Commit', icon ?? '$(check)'), tooltip, arguments: [this.repository.sourceControl, null] };
}
private getPostCommitCommandStringFromSetting(): string | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const postCommitCommandSetting = config.get<string>('postCommitCommand');
return postCommitCommandSetting === 'push' || postCommitCommandSetting === 'sync' ? `git.${postCommitCommandSetting}` : undefined;
}
private getPostCommitCommandStringFromStorage(): string | null | undefined {
return this.globalState.get<string | null>(this.getGlobalStateKey());
}
private async migratePostCommitCommandStorage(): Promise<void> {
const postCommitCommandString = this.globalState.get<string | null>(this.repository.root);
if (postCommitCommandString !== undefined) {
await this.globalState.update(this.getGlobalStateKey(), postCommitCommandString);
await this.globalState.update(this.repository.root, undefined);
}
}
private isRememberPostCommitCommandEnabled(): boolean {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
return config.get<boolean>('rememberPostCommitCommand') === true;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}
+36 -7
View File
@@ -3,36 +3,45 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { UriHandler, Uri, window, Disposable, commands } from 'vscode';
import { UriHandler, Uri, window, Disposable, commands, LogOutputChannel, l10n } from 'vscode';
import { dispose } from './util';
import * as querystring from 'querystring';
const schemes = new Set(['file', 'git', 'http', 'https', 'ssh']);
const refRegEx = /^$|[~\^:\\\*\s\[\]]|^-|^\.|\/\.|\.\.|\.lock\/|\.lock$|\/$|\.$/;
export class GitProtocolHandler implements UriHandler {
private disposables: Disposable[] = [];
constructor() {
constructor(private readonly logger: LogOutputChannel) {
this.disposables.push(window.registerUriHandler(this));
}
handleUri(uri: Uri): void {
this.logger.info(`GitProtocolHandler.handleUri(${uri.toString()})`);
switch (uri.path) {
case '/clone': this.clone(uri);
}
}
private clone(uri: Uri): void {
private async clone(uri: Uri): Promise<void> {
const data = querystring.parse(uri.query);
const ref = data.ref;
if (!data.url) {
console.warn('Failed to open URI:', uri);
this.logger.warn('Failed to open URI:' + uri.toString());
return;
}
if (Array.isArray(data.url) && data.url.length === 0) {
console.warn('Failed to open URI:', uri);
this.logger.warn('Failed to open URI:' + uri.toString());
return;
}
if (ref !== undefined && typeof ref !== 'string') {
this.logger.warn('Failed to open URI due to multiple references:' + uri.toString());
return;
}
@@ -50,13 +59,33 @@ export class GitProtocolHandler implements UriHandler {
if (!schemes.has(cloneUri.scheme.toLowerCase())) {
throw new Error('Unsupported scheme.');
}
// Validate the reference
if (typeof ref === 'string' && refRegEx.test(ref)) {
throw new Error('Invalid reference.');
}
}
catch (ex) {
console.warn('Invalid URI:', uri);
this.logger.warn('Invalid URI:' + uri.toString());
return;
}
commands.executeCommand('git.clone', cloneUri.toString(true));
if (!(await commands.getCommands(true)).includes('git.clone')) {
this.logger.error('Could not complete git clone operation as git installation was not found.');
const errorMessage = l10n.t('Could not clone your repository as Git is not installed.');
const downloadGit = l10n.t('Download Git');
if (await window.showErrorMessage(errorMessage, { modal: true }, downloadGit) === downloadGit) {
commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-download-git'));
}
return;
} else {
const cloneTarget = cloneUri.toString(true);
this.logger.info(`Executing git.clone for ${cloneTarget}`);
commands.executeCommand('git.clone', cloneTarget, undefined, { ref: ref });
}
}
dispose(): void {
+4
View File
@@ -11,3 +11,7 @@ export async function pickRemoteSource(options: PickRemoteSourceOptions & { bran
export async function pickRemoteSource(options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {
return GitBaseApi.getAPI().pickRemoteSource(options);
}
export async function getRemoteSourceActions(url: string) {
return GitBaseApi.getAPI().getRemoteSourceActions(url);
}
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
echo ''
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
VSCODE_GIT_ASKPASS_PIPE=`mktemp`
ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" VSCODE_GIT_ASKPASS_TYPE="ssh" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $VSCODE_GIT_ASKPASS_EXTRA_ARGS $*
cat $VSCODE_GIT_ASKPASS_PIPE
rm $VSCODE_GIT_ASKPASS_PIPE
+117 -20
View File
@@ -3,14 +3,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Command, EventEmitter, Event, workspace, Uri } from 'vscode';
import { Repository, Operation } from './repository';
import { Disposable, Command, EventEmitter, Event, workspace, Uri, l10n } from 'vscode';
import { Repository } from './repository';
import { anyEvent, dispose, filterEvent } from './util';
import * as nls from 'vscode-nls';
import { Branch, RemoteSourcePublisher } from './api/git';
import { Branch, RefType, RemoteSourcePublisher } from './api/git';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
import { CheckoutOperation, CheckoutTrackingOperation, OperationKind } from './operation';
const localize = nls.loadMessageBundle();
interface CheckoutStatusBarState {
readonly isCheckoutRunning: boolean;
readonly isCommitRunning: boolean;
readonly isSyncRunning: boolean;
}
class CheckoutStatusBar {
@@ -18,23 +22,95 @@ class CheckoutStatusBar {
get onDidChange(): Event<void> { return this._onDidChange.event; }
private disposables: Disposable[] = [];
private _state: CheckoutStatusBarState;
private get state() { return this._state; }
private set state(state: CheckoutStatusBarState) {
this._state = state;
this._onDidChange.fire();
}
constructor(private repository: Repository) {
this._state = {
isCheckoutRunning: false,
isCommitRunning: false,
isSyncRunning: false
};
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
repository.onDidRunGitStatus(this._onDidChange.fire, this._onDidChange, this.disposables);
repository.onDidChangeBranchProtection(this._onDidChange.fire, this._onDidChange, this.disposables);
}
get command(): Command | undefined {
const operationData = [
...this.repository.operations.getOperations(OperationKind.Checkout) as CheckoutOperation[],
...this.repository.operations.getOperations(OperationKind.CheckoutTracking) as CheckoutTrackingOperation[]
];
const rebasing = !!this.repository.rebaseCommit;
const isBranchProtected = this.repository.isBranchProtected();
const title = `${isBranchProtected ? '$(lock)' : '$(git-branch)'} ${this.repository.headLabel}${rebasing ? ` (${localize('rebasing', 'Rebasing')})` : ''}`;
const label = operationData[0]?.refLabel ?? `${this.repository.headLabel}${rebasing ? ` (${l10n.t('Rebasing')})` : ''}`;
const command = (this.state.isCheckoutRunning || this.state.isCommitRunning || this.state.isSyncRunning) ? '' : 'git.checkout';
return {
command: 'git.checkout',
tooltip: localize('checkout', "Checkout branch/tag..."),
title,
command,
tooltip: `${label}, ${this.getTooltip()}`,
title: `${this.getIcon()} ${label}`,
arguments: [this.repository.sourceControl]
};
}
private getIcon(): string {
if (!this.repository.HEAD) {
return '';
}
// Checkout
if (this.state.isCheckoutRunning) {
return '$(loading~spin)';
}
// Branch
if (this.repository.HEAD.type === RefType.Head && this.repository.HEAD.name) {
return this.repository.isBranchProtected() ? '$(lock)' : '$(git-branch)';
}
// Tag
if (this.repository.HEAD.type === RefType.Tag) {
return '$(tag)';
}
// Commit
return '$(git-commit)';
}
private getTooltip(): string {
if (this.state.isCheckoutRunning) {
return l10n.t('Checking Out Branch/Tag...');
}
if (this.state.isCommitRunning) {
return l10n.t('Committing Changes...');
}
if (this.state.isSyncRunning) {
return l10n.t('Synchronizing Changes...');
}
return l10n.t('Checkout Branch/Tag...');
}
private onDidChangeOperations(): void {
const isCommitRunning = this.repository.operations.isRunning(OperationKind.Commit);
const isCheckoutRunning = this.repository.operations.isRunning(OperationKind.Checkout) ||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
const isSyncRunning = this.repository.operations.isRunning(OperationKind.Sync) ||
this.repository.operations.isRunning(OperationKind.Push) ||
this.repository.operations.isRunning(OperationKind.Pull);
this.state = { ...this.state, isCheckoutRunning, isCommitRunning, isSyncRunning };
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
@@ -42,6 +118,8 @@ class CheckoutStatusBar {
interface SyncStatusBarState {
readonly enabled: boolean;
readonly isCheckoutRunning: boolean;
readonly isCommitRunning: boolean;
readonly isSyncRunning: boolean;
readonly hasRemotes: boolean;
readonly HEAD: Branch | undefined;
@@ -64,6 +142,8 @@ class SyncStatusBar {
constructor(private repository: Repository, private remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry) {
this._state = {
enabled: true,
isCheckoutRunning: false,
isCommitRunning: false,
isSyncRunning: false,
hasRemotes: false,
HEAD: undefined,
@@ -89,11 +169,14 @@ class SyncStatusBar {
}
private onDidChangeOperations(): void {
const isSyncRunning = this.repository.operations.isRunning(Operation.Sync) ||
this.repository.operations.isRunning(Operation.Push) ||
this.repository.operations.isRunning(Operation.Pull);
const isCommitRunning = this.repository.operations.isRunning(OperationKind.Commit);
const isCheckoutRunning = this.repository.operations.isRunning(OperationKind.Checkout) ||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
const isSyncRunning = this.repository.operations.isRunning(OperationKind.Sync) ||
this.repository.operations.isRunning(OperationKind.Push) ||
this.repository.operations.isRunning(OperationKind.Pull);
this.state = { ...this.state, isSyncRunning };
this.state = { ...this.state, isCheckoutRunning, isCommitRunning, isSyncRunning };
}
private onDidRunGitStatus(): void {
@@ -121,12 +204,16 @@ class SyncStatusBar {
return;
}
const tooltip = this.state.remoteSourcePublishers.length === 1
? localize('publish to', "Publish to {0}", this.state.remoteSourcePublishers[0].name)
: localize('publish to...', "Publish to...");
const command = (this.state.isCheckoutRunning || this.state.isCommitRunning) ? '' : 'git.publish';
const tooltip =
this.state.isCheckoutRunning ? l10n.t('Checking Out Changes...') :
this.state.isCommitRunning ? l10n.t('Committing Changes...') :
this.state.remoteSourcePublishers.length === 1
? l10n.t('Publish to {0}', this.state.remoteSourcePublishers[0].name)
: l10n.t('Publish to...');
return {
command: 'git.publish',
command,
title: `$(cloud-upload)`,
tooltip,
arguments: [this.repository.sourceControl]
@@ -150,17 +237,27 @@ class SyncStatusBar {
} else {
icon = '$(cloud-upload)';
command = 'git.publish';
tooltip = localize('publish branch', "Publish Branch");
tooltip = l10n.t('Publish Branch');
}
} else {
command = '';
tooltip = '';
}
if (this.state.isCheckoutRunning) {
command = '';
tooltip = l10n.t('Checking Out Changes...');
}
if (this.state.isCommitRunning) {
command = '';
tooltip = l10n.t('Committing Changes...');
}
if (this.state.isSyncRunning) {
icon = '$(sync~spin)';
command = '';
tooltip = localize('syncing changes', "Synchronizing Changes...");
tooltip = l10n.t('Synchronizing Changes...');
}
return {
+72 -1
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles, parseGitRemotes } from '../git';
import * as assert from 'assert';
import { splitInChunks } from '../util';
@@ -197,6 +197,77 @@ suite('git', () => {
});
});
suite('parseGitRemotes', () => {
test('empty', () => {
assert.deepStrictEqual(parseGitRemotes(''), []);
});
test('single remote', () => {
const sample = `[remote "origin"]
url = https://github.com/microsoft/vscode.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), [
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode.git', isReadOnly: false }
]);
});
test('single remote (multiple urls)', () => {
const sample = `[remote "origin"]
url = https://github.com/microsoft/vscode.git
url = https://github.com/microsoft/vscode2.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), [
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode.git', isReadOnly: false }
]);
});
test('multiple remotes', () => {
const sample = `[remote "origin"]
url = https://github.com/microsoft/vscode.git
pushurl = https://github.com/microsoft/vscode1.git
fetch = +refs/heads/*:refs/remotes/origin/*
[remote "remote2"]
url = https://github.com/microsoft/vscode2.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), [
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode1.git', isReadOnly: false },
{ name: 'remote2', fetchUrl: 'https://github.com/microsoft/vscode2.git', pushUrl: 'https://github.com/microsoft/vscode2.git', isReadOnly: false }
]);
});
test('remotes (white space)', () => {
const sample = ` [remote "origin"]
url = https://github.com/microsoft/vscode.git
pushurl=https://github.com/microsoft/vscode1.git
fetch = +refs/heads/*:refs/remotes/origin/*
[ remote"remote2"]
url = https://github.com/microsoft/vscode2.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), [
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode1.git', isReadOnly: false },
{ name: 'remote2', fetchUrl: 'https://github.com/microsoft/vscode2.git', pushUrl: 'https://github.com/microsoft/vscode2.git', isReadOnly: false }
]);
});
test('remotes (invalid section)', () => {
const sample = `[remote "origin"
url = https://github.com/microsoft/vscode.git
pushurl = https://github.com/microsoft/vscode1.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), []);
});
});
suite('parseGitCommit', () => {
test('single parent commit', function () {
const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1
+3 -3
View File
@@ -3,10 +3,10 @@
* 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 options: any = {
const options: import('mocha').MochaOptions = {
ui: 'tdd',
color: true,
timeout: 60000
+1 -1
View File
@@ -44,7 +44,7 @@ suite('git smoke test', function () {
fs.writeFileSync(file('index.pug'), 'hello', 'utf8');
cp.execSync('git init -b main', { cwd });
cp.execSync('git config user.name testuser', { cwd });
cp.execSync('git config user.email monacotools@microsoft.com', { cwd });
cp.execSync('git config user.email monacotools@example.com', { cwd });
cp.execSync('git config commit.gpgsign false', { cwd });
cp.execSync('git add .', { cwd });
cp.execSync('git commit -m "initial commit"', { cwd });
+34 -20
View File
@@ -3,15 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { CancellationToken, ConfigurationChangeEvent, Disposable, env, Event, EventEmitter, MarkdownString, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace } from 'vscode';
import { CancellationToken, ConfigurationChangeEvent, Disposable, env, Event, EventEmitter, MarkdownString, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace, l10n } from 'vscode';
import { Model } from './model';
import { Repository, Resource } from './repository';
import { debounce } from './decorators';
import { emojify, ensureEmojis } from './emoji';
import { CommandCenter } from './commands';
const localize = nls.loadMessageBundle();
import { OperationKind, OperationResult } from './operation';
export class GitTimelineItem extends TimelineItem {
static is(item: TimelineItem): item is GitTimelineItem {
@@ -54,7 +52,7 @@ export class GitTimelineItem extends TimelineItem {
this.tooltip = new MarkdownString('', true);
if (email) {
const emailTitle = localize('git.timeline.email', "Email");
const emailTitle = l10n.t('Email');
this.tooltip.appendMarkdown(`$(account) [**${author}**](mailto:${email} "${emailTitle} ${author}")\n\n`);
} else {
this.tooltip.appendMarkdown(`$(account) **${author}**\n\n`);
@@ -79,14 +77,14 @@ export class GitTimelineProvider implements TimelineProvider {
}
readonly id = 'git-history';
readonly label = localize('git.timeline.source', 'Git History');
readonly label = l10n.t('Git History');
private readonly disposable: Disposable;
private providerDisposable: Disposable | undefined;
private repo: Repository | undefined;
private repoDisposable: Disposable | undefined;
private repoStatusDate: Date | undefined;
private repoOperationDate: Date | undefined;
constructor(private readonly model: Model, private commands: CommandCenter) {
this.disposable = Disposable.from(
@@ -105,12 +103,12 @@ export class GitTimelineProvider implements TimelineProvider {
}
async provideTimeline(uri: Uri, options: TimelineOptions, _token: CancellationToken): Promise<Timeline> {
// console.log(`GitTimelineProvider.provideTimeline: uri=${uri} state=${this._model.state}`);
// console.log(`GitTimelineProvider.provideTimeline: uri=${uri}`);
const repo = this.model.getRepository(uri);
if (!repo) {
this.repoDisposable?.dispose();
this.repoStatusDate = undefined;
this.repoOperationDate = undefined;
this.repo = undefined;
return { items: [] };
@@ -120,10 +118,11 @@ export class GitTimelineProvider implements TimelineProvider {
this.repoDisposable?.dispose();
this.repo = repo;
this.repoStatusDate = new Date();
this.repoOperationDate = new Date();
this.repoDisposable = Disposable.from(
repo.onDidChangeRepository(uri => this.onRepositoryChanged(repo, uri)),
repo.onDidRunGitStatus(() => this.onRepositoryStatusChanged(repo))
repo.onDidRunGitStatus(() => this.onRepositoryStatusChanged(repo)),
repo.onDidRunOperation(result => this.onRepositoryOperationRun(repo, result))
);
}
@@ -171,7 +170,7 @@ export class GitTimelineProvider implements TimelineProvider {
const showAuthor = config.get<boolean>('showAuthor');
const showUncommitted = config.get<boolean>('showUncommitted');
const openComparison = localize('git.timeline.openComparison', "Open Comparison");
const openComparison = l10n.t('Open Comparison');
const items = commits.map<GitTimelineItem>((c, i) => {
const date = dateType === 'authored' ? c.authorDate : c.commitDate;
@@ -199,13 +198,13 @@ export class GitTimelineProvider implements TimelineProvider {
});
if (options.cursor === undefined) {
const you = localize('git.timeline.you', 'You');
const you = l10n.t('You');
const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
if (index) {
const date = this.repoStatusDate ?? new Date();
const date = this.repoOperationDate ?? new Date();
const item = new GitTimelineItem('~', 'HEAD', localize('git.timeline.stagedChanges', 'Staged Changes'), date.getTime(), 'index', 'git:file:index');
const item = new GitTimelineItem('~', 'HEAD', l10n.t('Staged Changes'), date.getTime(), 'index', 'git:file:index');
// TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
item.iconPath = new ThemeIcon('git-commit');
item.description = '';
@@ -228,7 +227,7 @@ export class GitTimelineProvider implements TimelineProvider {
if (working) {
const date = new Date();
const item = new GitTimelineItem('', index ? '~' : 'HEAD', localize('git.timeline.uncommitedChanges', 'Uncommitted Changes'), date.getTime(), 'working', 'git:file:working');
const item = new GitTimelineItem('', index ? '~' : 'HEAD', l10n.t('Uncommitted Changes'), date.getTime(), 'working', 'git:file:working');
item.iconPath = new ThemeIcon('circle-outline');
item.description = '';
item.setItemDetails(you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type));
@@ -255,7 +254,7 @@ export class GitTimelineProvider implements TimelineProvider {
private ensureProviderRegistration() {
if (this.providerDisposable === undefined) {
this.providerDisposable = workspace.registerTimelineProvider(['file', 'git', 'vscode-remote', 'gitlens-git', 'vscode-local-history'], this);
this.providerDisposable = workspace.registerTimelineProvider(['file', 'git', 'vscode-remote', 'vscode-local-history'], this);
}
}
@@ -283,10 +282,25 @@ export class GitTimelineProvider implements TimelineProvider {
private onRepositoryStatusChanged(_repo: Repository) {
// console.log(`GitTimelineProvider.onRepositoryStatusChanged`);
// This is less than ideal, but for now just save the last time a status was run and use that as the timestamp for staged items
this.repoStatusDate = new Date();
const config = workspace.getConfiguration('git.timeline');
const showUncommitted = config.get<boolean>('showUncommitted') === true;
this.fireChanged();
if (showUncommitted) {
this.fireChanged();
}
}
private onRepositoryOperationRun(_repo: Repository, _result: OperationResult) {
// console.log(`GitTimelineProvider.onRepositoryOperationRun`);
// Successful operations that are not read-only and not status operations
if (!_result.error && !_result.operation.readOnly && _result.operation.kind !== OperationKind.Status) {
// This is less than ideal, but for now just save the last time an
// operation was run and use that as the timestamp for staged items
this.repoOperationDate = new Date();
this.fireChanged();
}
}
@debounce(500)
@@ -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;
}
}
@@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* 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/157734
export namespace workspace {
/**
* An event that is emitted when an edit session identity is about to be requested.
*/
export const onWillCreateEditSessionIdentity: Event<EditSessionIdentityWillCreateEvent>;
/**
*
* @param scheme The URI scheme that this provider can provide edit session identities for.
* @param provider A provider which can convert URIs for workspace folders of scheme @param scheme to
* an edit session identifier which is stable across machines. This enables edit sessions to be resolved.
*/
export function registerEditSessionIdentityProvider(scheme: string, provider: EditSessionIdentityProvider): Disposable;
}
export interface EditSessionIdentityProvider {
/**
*
* @param workspaceFolder The workspace folder to provide an edit session identity for.
* @param token A cancellation token for the request.
* @returns A string representing the edit session identity for the requested workspace folder.
*/
provideEditSessionIdentity(workspaceFolder: WorkspaceFolder, token: CancellationToken): ProviderResult<string>;
/**
*
* @param identity1 An edit session identity.
* @param identity2 A second edit session identity to compare to @param identity1.
* @param token A cancellation token for the request.
* @returns An {@link EditSessionIdentityMatch} representing the edit session identity match confidence for the provided identities.
*/
provideEditSessionIdentityMatch(identity1: string, identity2: string, token: CancellationToken): ProviderResult<EditSessionIdentityMatch>;
}
export enum EditSessionIdentityMatch {
Complete = 100,
Partial = 50,
None = 0
}
export interface EditSessionIdentityWillCreateEvent {
/**
* A cancellation token.
*/
readonly token: CancellationToken;
/**
* The workspace folder to create an edit session identity for.
*/
readonly workspaceFolder: WorkspaceFolder;
/**
* Allows to pause the event until the provided thenable resolves.
*
* *Note:* This function can only be called during event dispatch.
*
* @param thenable A thenable that delays saving.
*/
waitUntil(thenable: Thenable<any>): void;
}
}
+7
View File
@@ -314,6 +314,13 @@ export function pathEquals(a: string, b: string): boolean {
* casing.
*/
export function relativePath(from: string, to: string): string {
// On Windows, there are cases in which `from` is a path that contains a trailing `\` character
// (ex: C:\, \\server\folder\) due to the implementation of `path.normalize()`. This behavior is
// by design as documented in https://github.com/nodejs/node/issues/1765.
if (isWindows) {
from = from.replace(/\\$/, '');
}
if (isDescendant(from, to) && from.length < to.length) {
return to.substring(from.length + 1);
}
+329 -44
View File
@@ -2,52 +2,178 @@
# yarn lockfile v1
"@joaomoreno/unique-names-generator@5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@joaomoreno/unique-names-generator/-/unique-names-generator-5.0.0.tgz#a67fe66e3d825c929fc97abfdf17fd80a72beab0"
integrity sha512-3kP6z7aoGEoM3tvhTBZioYa1QFkovOU8uxAlVclnZlXivwF/WTE5EcOzvoDdM+jtjJyWvCMDR1Q4RBjDqupD3A==
"@microsoft/1ds-core-js@3.2.3", "@microsoft/1ds-core-js@^3.2.3":
version "3.2.3"
resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.3.tgz#2217d92ec8b073caa4577a13f40ea3a5c4c4d4e7"
integrity sha512-796A8fd90oUKDRO7UXUT9BwZ3G+a9XzJj5v012FcCN/2qRhEsIV3x/0wkx2S08T4FiQEUPkB2uoYHpEjEneM7g==
"@azure/abort-controller@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.1.0.tgz#788ee78457a55af8a1ad342acb182383d2119249"
integrity sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==
dependencies:
"@microsoft/applicationinsights-core-js" "2.8.4"
"@microsoft/applicationinsights-shims" "^2.0.1"
"@microsoft/dynamicproto-js" "^1.1.6"
tslib "^2.2.0"
"@microsoft/1ds-post-js@^3.2.3":
version "3.2.3"
resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.3.tgz#1fa7d51615a44f289632ae8c588007ba943db216"
integrity sha512-tcGJQXXr2LYoBbIXPoUVe1KCF3OtBsuKDFL7BXfmNtuSGtWF0yejm6H83DrR8/cUIGMRMUP9lqNlqFGwDYiwAQ==
"@azure/core-auth@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.4.0.tgz#6fa9661c1705857820dbc216df5ba5665ac36a9e"
integrity sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==
dependencies:
"@microsoft/1ds-core-js" "3.2.3"
"@microsoft/applicationinsights-shims" "^2.0.1"
"@microsoft/dynamicproto-js" "^1.1.6"
"@azure/abort-controller" "^1.0.0"
tslib "^2.2.0"
"@microsoft/applicationinsights-core-js@2.8.4":
version "2.8.4"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.4.tgz#607e531bb241a8920d43960f68a7c76a6f9af596"
integrity sha512-FoA0FNOsFbJnLyTyQlYs6+HR7HMEa6nAOE6WOm9WVejBHMHQ/Bdb+hfVFi6slxwCimr/ner90jchi4/sIYdnyQ==
"@azure/core-rest-pipeline@^1.10.0":
version "1.10.3"
resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.3.tgz#7603afd71ff3c290351dbeeab2c814832e47b8ef"
integrity sha512-AMQb0ttiGJ0MIV/r+4TVra6U4+90mPeOveehFnrqKlo7dknPJYdJ61wOzYJXJjDxF8LcCtSogfRelkq+fCGFTw==
dependencies:
"@microsoft/applicationinsights-shims" "2.0.1"
"@microsoft/dynamicproto-js" "^1.1.6"
"@azure/abort-controller" "^1.0.0"
"@azure/core-auth" "^1.4.0"
"@azure/core-tracing" "^1.0.1"
"@azure/core-util" "^1.3.0"
"@azure/logger" "^1.0.0"
form-data "^4.0.0"
http-proxy-agent "^5.0.0"
https-proxy-agent "^5.0.0"
tslib "^2.2.0"
"@microsoft/applicationinsights-shims@2.0.1", "@microsoft/applicationinsights-shims@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.1.tgz#5d72fb7aaf4056c4fda54f9d7c93ccf8ca9bcbfd"
integrity sha512-G0MXf6R6HndRbDy9BbEj0zrLeuhwt2nsXk2zKtF0TnYo39KgYqhYC2ayIzKPTm2KAE+xzD7rgyLdZnrcRvt9WQ==
"@azure/core-tracing@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.0.1.tgz#352a38cbea438c4a83c86b314f48017d70ba9503"
integrity sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==
dependencies:
tslib "^2.2.0"
"@microsoft/dynamicproto-js@^1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.6.tgz#6fe03468862861f5f88ac4c3959a652b3797f1bc"
integrity sha512-D1Oivw1A4bIXhzBIy3/BBPn3p2On+kpO2NiYt9shICDK7L/w+cR6FFBUsBZ05l6iqzTeL+Jm8lAYn0g6G7DmDg==
"@azure/core-util@^1.3.0":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.3.2.tgz#3f8cfda1e87fac0ce84f8c1a42fcd6d2a986632d"
integrity sha512-2bECOUh88RvL1pMZTcc6OzfobBeWDBf5oBbhjIhT1MV9otMVWCzpOJkkiKtrnO88y5GGBelgY8At73KGAdbkeQ==
dependencies:
"@azure/abort-controller" "^1.0.0"
tslib "^2.2.0"
"@azure/logger@^1.0.0":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@azure/logger/-/logger-1.0.4.tgz#28bc6d0e5b3c38ef29296b32d35da4e483593fa1"
integrity sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==
dependencies:
tslib "^2.2.0"
"@joaomoreno/unique-names-generator@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@joaomoreno/unique-names-generator/-/unique-names-generator-5.1.0.tgz#d577d425aed794c44c0e8863cddd5dea349f74f3"
integrity sha512-KEVThTpUIKPb7dBKJ9mJ3WYnD1mJZZsEinCSp9CVEPlWbDagurFv1RKRjvvujrLfJzsGc0HkBHS9W8Bughao4A==
"@microsoft/1ds-core-js@3.2.11", "@microsoft/1ds-core-js@^3.2.8":
version "3.2.11"
resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.11.tgz#0a3857562a9bde7c15a2fa141c647f40aabd52e2"
integrity sha512-UuqZ1iWjaEFsBsnB+J0Q4IbgJdvJAq3LcNArAdMQQq9+wBWpjyGG4yu9gL6fS5AKfpF6yy73mtgWD7WzvphMLQ==
dependencies:
"@microsoft/applicationinsights-core-js" "2.8.13"
"@microsoft/applicationinsights-shims" "^2.0.2"
"@microsoft/dynamicproto-js" "^1.1.7"
"@microsoft/1ds-post-js@^3.2.8":
version "3.2.11"
resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.11.tgz#bbfc935bafb43982ccaf94ebd12f1fabf5ef8ebe"
integrity sha512-86prrN8cKjfpqfeyC4k0rFqg7fJDkTwPeKLHnkUgfq9mKVzu+BdtlEzaJtc56MroKWNYPkitZ/PWSwwpTPIgMA==
dependencies:
"@microsoft/1ds-core-js" "3.2.11"
"@microsoft/applicationinsights-shims" "^2.0.2"
"@microsoft/dynamicproto-js" "^1.1.7"
"@microsoft/applicationinsights-channel-js@2.8.13":
version "2.8.13"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.13.tgz#bd117cc7b00ec929e74a6555cb7462ab4fccdf62"
integrity sha512-zc2BSsHk4HAqK5STdNzKGV817jKNbiTZPYpNt3zuE+jO5druJgloqrvclUfLnCoa7zwrQ2UxoAXlpJGmroGZPA==
dependencies:
"@microsoft/applicationinsights-common" "2.8.13"
"@microsoft/applicationinsights-core-js" "2.8.13"
"@microsoft/applicationinsights-shims" "2.0.2"
"@microsoft/dynamicproto-js" "^1.1.9"
"@microsoft/applicationinsights-common@2.8.13":
version "2.8.13"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.13.tgz#e3457bdc54a61cbfa481494da25e55bb9156096e"
integrity sha512-UYLLGVtuzrWUEmGYRroMzLyTi2fHqL6SwJUlmVWPJrmdK43PGpviRix/sBW0Qs+6qjiI1Z6CiG4Xah6w/HylhA==
dependencies:
"@microsoft/applicationinsights-core-js" "2.8.13"
"@microsoft/applicationinsights-shims" "2.0.2"
"@microsoft/dynamicproto-js" "^1.1.9"
"@microsoft/applicationinsights-core-js@2.8.13":
version "2.8.13"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.13.tgz#45c2b8fff35e5aa519355dd3a69e3758293b08f4"
integrity sha512-PP7Xjplvy0d5G2Tk7DcSDYRmgDYRv+7n3wEiqgm63DrSoa8rEuoODavjWunhX058zPNIeKbus59NE+DusLLyZg==
dependencies:
"@microsoft/applicationinsights-shims" "2.0.2"
"@microsoft/dynamicproto-js" "^1.1.9"
"@microsoft/applicationinsights-shims@2.0.2", "@microsoft/applicationinsights-shims@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz#92b36a09375e2d9cb2b4203383b05772be837085"
integrity sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==
"@microsoft/applicationinsights-web-basic@^2.8.9":
version "2.8.13"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.13.tgz#a3934fa7b7f221d09fbf403fcfeb06a8c18ca246"
integrity sha512-DgPx1ryZucLWc285qkAEBG6LCcTSG6Gdb4u4yAlmAW0G+Qau49GoJnfJGR+cDXvyXSwHcH0dsainqzeYYY1K7A==
dependencies:
"@microsoft/applicationinsights-channel-js" "2.8.13"
"@microsoft/applicationinsights-common" "2.8.13"
"@microsoft/applicationinsights-core-js" "2.8.13"
"@microsoft/applicationinsights-shims" "2.0.2"
"@microsoft/dynamicproto-js" "^1.1.9"
"@microsoft/applicationinsights-web-snippet@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-web-snippet/-/applicationinsights-web-snippet-1.0.1.tgz#6bb788b2902e48bf5d460c38c6bb7fedd686ddd7"
integrity sha512-2IHAOaLauc8qaAitvWS+U931T+ze+7MNWrDHY47IENP5y2UA0vqJDu67kWZDdpCN1fFC77sfgfB+HV7SrKshnQ==
"@microsoft/dynamicproto-js@^1.1.7", "@microsoft/dynamicproto-js@^1.1.9":
version "1.1.9"
resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz#7437db7aa061162ee94e4131b69a62b8dad5dea6"
integrity sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==
"@opentelemetry/api@^1.0.4":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.4.1.tgz#ff22eb2e5d476fbc2450a196e40dd243cc20c28f"
integrity sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==
"@opentelemetry/core@1.13.0", "@opentelemetry/core@^1.0.1":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.13.0.tgz#7cdcb4176d260d279b0aa31456c4ce2ba7f410aa"
integrity sha512-2dBX3Sj99H96uwJKvc2w9NOiNgbvAO6mOFJFramNkKfS9O4Um+VWgpnlAazoYjT6kUJ1MP70KQ5ngD4ed+4NUw==
dependencies:
"@opentelemetry/semantic-conventions" "1.13.0"
"@opentelemetry/resources@1.13.0":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.13.0.tgz#436b33ea950004e66fce6575f2776a05faca7f8e"
integrity sha512-euqjOkiN6xhjE//0vQYGvbStxoD/WWQRhDiO0OTLlnLBO9Yw2Gd/VoSx2H+svsebjzYk5OxLuREBmcdw6rbUNg==
dependencies:
"@opentelemetry/core" "1.13.0"
"@opentelemetry/semantic-conventions" "1.13.0"
"@opentelemetry/sdk-trace-base@^1.0.1":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.13.0.tgz#096cc2759430d880c5d886e009df2605097403dc"
integrity sha512-moTiQtc0uPR1hQLt6gLDJH9IIkeBhgRb71OKjNHZPE1VF45fHtD6nBDi5J/DkTHTwYP5X3kBJLa3xN7ub6J4eg==
dependencies:
"@opentelemetry/core" "1.13.0"
"@opentelemetry/resources" "1.13.0"
"@opentelemetry/semantic-conventions" "1.13.0"
"@opentelemetry/semantic-conventions@1.13.0", "@opentelemetry/semantic-conventions@^1.0.1":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.13.0.tgz#0290398b3eaebc6029c348988a78c3b688fe9219"
integrity sha512-LMGqfSZkaMQXqewO0o1wvWr/2fQdCh4a3Sqlxka/UsJCe0cfLulh6x2aqnKLnsrSGiCq5rSCwvINd152i0nCqw==
"@tokenizer/token@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
"@types/byline@4.2.31":
version "4.2.31"
resolved "https://registry.yarnpkg.com/@types/byline/-/byline-4.2.31.tgz#0e61fcb9c03e047d21c4496554c7116297ab60cd"
@@ -80,24 +206,125 @@
resolved "https://registry.yarnpkg.com/@types/which/-/which-1.0.28.tgz#016e387629b8817bed653fe32eab5d11279c8df6"
integrity sha1-AW44dim4gXvtZT/jLqtdESecjfY=
"@vscode/extension-telemetry@0.6.2":
version "0.6.2"
resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.6.2.tgz#b86814ee680615730da94220c2b03ea9c3c14a8e"
integrity sha512-yb/wxLuaaCRcBAZtDCjNYSisAXz3FWsSqAha5nhHcYxx2ZPdQdWuZqVXGKq0ZpHVndBWWtK6XqtpCN2/HB4S1w==
"@vscode/extension-telemetry@0.7.5":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.7.5.tgz#bf965731816e08c3f146f96d901ec67954fc913b"
integrity sha512-fJ5y3TcpqqkFYHneabYaoB4XAhDdVflVm+TDKshw9VOs77jkgNS4UA7LNXrWeO0eDne3Sh3JgURf+xzc1rk69w==
dependencies:
"@microsoft/1ds-core-js" "^3.2.3"
"@microsoft/1ds-post-js" "^3.2.3"
"@microsoft/1ds-core-js" "^3.2.8"
"@microsoft/1ds-post-js" "^3.2.8"
"@microsoft/applicationinsights-web-basic" "^2.8.9"
applicationinsights "2.4.1"
"@vscode/iconv-lite-umd@0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48"
integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
applicationinsights@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-2.4.1.tgz#4de4c4dd3c7c4a44445cfbf3d15808fc0dcc423d"
integrity sha512-0n0Ikd0gzSm460xm+M0UTWIwXrhrH/0bqfZatcJjYObWyefxfAxapGEyNnSGd1Tg90neHz+Yhf+Ff/zgvPiQYA==
dependencies:
"@azure/core-auth" "^1.4.0"
"@azure/core-rest-pipeline" "^1.10.0"
"@microsoft/applicationinsights-web-snippet" "^1.0.1"
"@opentelemetry/api" "^1.0.4"
"@opentelemetry/core" "^1.0.1"
"@opentelemetry/sdk-trace-base" "^1.0.1"
"@opentelemetry/semantic-conventions" "^1.0.1"
cls-hooked "^4.2.2"
continuation-local-storage "^3.2.1"
diagnostic-channel "1.1.0"
diagnostic-channel-publishers "1.0.5"
async-hook-jl@^1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68"
integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==
dependencies:
stack-chain "^1.3.7"
async-listener@^0.6.0:
version "0.6.10"
resolved "https://registry.yarnpkg.com/async-listener/-/async-listener-0.6.10.tgz#a7c97abe570ba602d782273c0de60a51e3e17cbc"
integrity sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==
dependencies:
semver "^5.3.0"
shimmer "^1.1.0"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
byline@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=
cls-hooked@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908"
integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==
dependencies:
async-hook-jl "^1.7.6"
emitter-listener "^1.0.1"
semver "^5.4.1"
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
continuation-local-storage@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb"
integrity sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==
dependencies:
async-listener "^0.6.0"
emitter-listener "^1.1.1"
debug@4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
diagnostic-channel-publishers@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.5.tgz#df8c317086c50f5727fdfb5d2fce214d2e4130ae"
integrity sha512-dJwUS0915pkjjimPJVDnS/QQHsH0aOYhnZsLJdnZIMOrB+csj8RnZhWTuwnm8R5v3Z7OZs+ksv5luC14DGB7eg==
diagnostic-channel@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz#6985e9dfedfbc072d91dc4388477e4087147756e"
integrity sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==
dependencies:
semver "^5.3.0"
emitter-listener@^1.0.1, emitter-listener@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8"
integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==
dependencies:
shimmer "^1.2.0"
file-type@16.5.4:
version "16.5.4"
resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd"
@@ -107,6 +334,32 @@ file-type@16.5.4:
strtok3 "^6.2.4"
token-types "^4.1.1"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
http-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==
dependencies:
"@tootallnate/once" "2"
agent-base "6"
debug "4"
https-proxy-agent@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"
ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -127,6 +380,23 @@ jschardet@3.0.0:
resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882"
integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
peek-readable@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72"
@@ -158,6 +428,21 @@ safe-buffer@~5.2.0:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
semver@^5.3.0, semver@^5.4.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
shimmer@^1.1.0, shimmer@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
stack-chain@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
integrity sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -181,16 +466,16 @@ token-types@^4.1.1:
"@tokenizer/token" "^0.3.0"
ieee754 "^1.2.1"
tslib@^2.2.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
vscode-nls@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002"
integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw==
vscode-uri@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.0.0.tgz#2df704222f72b8a71ff266ba0830ed6c51ac1542"
@@ -16,14 +16,12 @@ module.exports = withBrowserDefaults({
entry: {
extension: './src/extension.ts',
},
externals: {
'keytar': 'commonjs keytar',
},
resolve: {
alias: {
'node-fetch': path.resolve(__dirname, 'node_modules/node-fetch/browser.js'),
'uuid': path.resolve(__dirname, 'node_modules/uuid/dist/esm-browser/index.js'),
'./authServer': path.resolve(__dirname, 'src/env/browser/authServer'),
'./node/authServer': path.resolve(__dirname, 'src/browser/authServer'),
'./node/crypto': path.resolve(__dirname, 'src/browser/crypto'),
'./node/fetch': path.resolve(__dirname, 'src/browser/fetch')
}
}
});
@@ -13,8 +13,5 @@ module.exports = withDefaults({
context: __dirname,
entry: {
extension: './src/extension.ts',
},
externals: {
'keytar': 'commonjs keytar'
}
});
+10 -13
View File
@@ -17,14 +17,14 @@
"ui",
"workspace"
],
"activationEvents": [
"onAuthenticationRequest:github",
"onAuthenticationRequest:github-enterprise"
],
"activationEvents": [],
"capabilities": {
"virtualWorkspaces": true,
"untrustedWorkspaces": {
"supported": true
"supported": "limited",
"restrictedConfigurations": [
"github-enterprise.uri"
]
}
},
"contributes": {
@@ -34,16 +34,16 @@
"id": "github"
},
{
"label": "GitHub Enterprise",
"label": "GitHub Enterprise Server",
"id": "github-enterprise"
}
],
"configuration": {
"title": "GitHub Enterprise Authentication Provider",
"title": "GitHub Enterprise Server Authentication Provider",
"properties": {
"github-enterprise.uri": {
"type": "string",
"description": "URI of your GitHub Enterprise Instance"
"description": "GitHub Enterprise Server URI"
}
}
}
@@ -60,15 +60,12 @@
},
"dependencies": {
"node-fetch": "2.6.7",
"uuid": "8.1.0",
"@vscode/extension-telemetry": "0.6.2",
"vscode-nls": "^5.0.0",
"@vscode/extension-telemetry": "0.7.5",
"vscode-tas-client": "^0.1.47"
},
"devDependencies": {
"@types/node": "16.x",
"@types/node-fetch": "^2.5.7",
"@types/uuid": "8.0.0"
"@types/node-fetch": "^2.5.7"
},
"repository": {
"type": "git",
@@ -3,9 +3,4 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export async function sha256(s: string | Uint8Array): Promise<string> {
const createHash = require('sha.js');
return createHash('sha256').update(s).digest('base64');
}
export const crypto = globalThis.crypto;
@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri } from 'vscode';
import { AuthProviderType } from '../github';
const VALID_DESKTOP_CALLBACK_SCHEMES = [
'vscode',
@@ -15,7 +16,7 @@ const VALID_DESKTOP_CALLBACK_SCHEMES = [
'vscode-exploration'
];
export function isSupportedEnvironment(uri: Uri): boolean {
export function isSupportedClient(uri: Uri): boolean {
return (
VALID_DESKTOP_CALLBACK_SCHEMES.includes(uri.scheme) ||
// vscode.dev & insiders.vscode.dev
@@ -24,3 +25,10 @@ export function isSupportedEnvironment(uri: Uri): boolean {
/(?:^|\.)github\.dev$/.test(uri.authority)
);
}
export function isSupportedTarget(type: AuthProviderType, gheUri?: Uri): boolean {
return (
type === AuthProviderType.github ||
/\.ghe\.com$/.test(gheUri!.authority)
);
}
@@ -18,14 +18,19 @@ export class ExperimentationTelemetry implements IExperimentationTelemetry {
switch (vscode.env.uriScheme) {
case 'vscode':
targetPopulation = TargetPopulation.Public;
break;
case 'vscode-insiders':
targetPopulation = TargetPopulation.Insiders;
break;
case 'vscode-exploration':
targetPopulation = TargetPopulation.Internal;
break;
case 'code-oss':
targetPopulation = TargetPopulation.Team;
break;
default:
targetPopulation = TargetPopulation.Public;
break;
}
const id = this.context.extension.id;
@@ -6,53 +6,24 @@
import * as vscode from 'vscode';
import { AuthProviderType } from '../github';
type LogLevel = 'Trace' | 'Info' | 'Error';
export class Log {
private output: vscode.OutputChannel;
private output: vscode.LogOutputChannel;
constructor(private readonly type: AuthProviderType) {
const friendlyName = this.type === AuthProviderType.github ? 'GitHub' : 'GitHub Enterprise';
this.output = vscode.window.createOutputChannel(`${friendlyName} Authentication`);
this.output = vscode.window.createOutputChannel(`${friendlyName} Authentication`, { log: true });
}
private data2String(data: any): string {
if (data instanceof Error) {
return data.stack || data.message;
}
if (data.success === false && data.message) {
return data.message;
}
return data.toString();
public trace(message: string): void {
this.output.trace(message);
}
public trace(message: string, data?: any): void {
this.logLevel('Trace', message, data);
public info(message: string): void {
this.output.info(message);
}
public info(message: string, data?: any): void {
this.logLevel('Info', message, data);
public error(message: string): void {
this.output.error(message);
}
public error(message: string, data?: any): void {
this.logLevel('Error', message, data);
}
public logLevel(level: LogLevel, message: string, data?: any): void {
this.output.appendLine(`[${level} - ${this.now()}] ${message}`);
if (data) {
this.output.appendLine(this.data2String(data));
}
}
private now(): string {
const now = new Date();
return padLeft(now.getUTCHours() + '', 2, '0')
+ ':' + padLeft(now.getMinutes() + '', 2, '0')
+ ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds();
}
}
function padLeft(s: string, n: number, pad = ' ') {
return pad.repeat(Math.max(0, n - s.length)) + s;
}
@@ -4,22 +4,42 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { GitHubAuthenticationProvider, AuthProviderType } from './github';
import { GitHubAuthenticationProvider, UriEventHandler } from './github';
function initGHES(context: vscode.ExtensionContext, uriHandler: UriEventHandler) {
const settingValue = vscode.workspace.getConfiguration().get<string>('github-enterprise.uri');
if (!settingValue) {
return undefined;
}
// validate user value
let uri: vscode.Uri;
try {
uri = vscode.Uri.parse(settingValue, true);
} catch (e) {
vscode.window.showErrorMessage(vscode.l10n.t('GitHub Enterprise Server URI is not a valid URI: {0}', e.message ?? e));
return;
}
const githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, uriHandler, uri);
context.subscriptions.push(githubEnterpriseAuthProvider);
return githubEnterpriseAuthProvider;
}
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(new GitHubAuthenticationProvider(context, AuthProviderType.github));
const uriHandler = new UriEventHandler();
context.subscriptions.push(uriHandler);
context.subscriptions.push(vscode.window.registerUriHandler(uriHandler));
let githubEnterpriseAuthProvider: GitHubAuthenticationProvider | undefined;
if (vscode.workspace.getConfiguration().get<string>('github-enterprise.uri')) {
githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, AuthProviderType.githubEnterprise);
context.subscriptions.push(githubEnterpriseAuthProvider);
}
context.subscriptions.push(new GitHubAuthenticationProvider(context, uriHandler));
let githubEnterpriseAuthProvider: GitHubAuthenticationProvider | undefined = initGHES(context, uriHandler);
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration('github-enterprise.uri')) {
if (!githubEnterpriseAuthProvider && vscode.workspace.getConfiguration().get<string>('github-enterprise.uri')) {
githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, AuthProviderType.githubEnterprise);
context.subscriptions.push(githubEnterpriseAuthProvider);
if (vscode.workspace.getConfiguration().get<string>('github-enterprise.uri')) {
githubEnterpriseAuthProvider?.dispose();
githubEnterpriseAuthProvider = initGHES(context, uriHandler);
}
}
}));
+47 -30
View File
@@ -4,13 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { v4 as uuid } from 'uuid';
import { Keychain } from './common/keychain';
import { GitHubEnterpriseServer, GitHubServer, IGitHubServer } from './githubServer';
import { arrayEquals } from './common/utils';
import { ExperimentationTelemetry } from './experimentationService';
import TelemetryReporter from '@vscode/extension-telemetry';
import { Keychain } from './common/keychain';
import { GitHubServer, IGitHubServer } from './githubServer';
import { arrayEquals } from './common/utils';
import { ExperimentationTelemetry } from './common/experimentationService';
import { Log } from './common/logger';
import { crypto } from './node/crypto';
interface SessionData {
id: string;
@@ -28,30 +28,48 @@ export enum AuthProviderType {
githubEnterprise = 'github-enterprise'
}
export class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
public handleUri(uri: vscode.Uri) {
this.fire(uri);
}
}
export class GitHubAuthenticationProvider implements vscode.AuthenticationProvider, vscode.Disposable {
private _sessionChangeEmitter = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
private _logger = new Log(this.type);
private _githubServer: IGitHubServer;
private _telemetryReporter: ExperimentationTelemetry;
private readonly _sessionChangeEmitter = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
private readonly _logger: Log;
private readonly _githubServer: IGitHubServer;
private readonly _telemetryReporter: ExperimentationTelemetry;
private readonly _keychain: Keychain;
private readonly _accountsSeen = new Set<string>();
private readonly _disposable: vscode.Disposable | undefined;
private _keychain: Keychain = new Keychain(this.context, `${this.type}.auth`, this._logger);
private _sessionsPromise: Promise<vscode.AuthenticationSession[]>;
private _accountsSeen = new Set<string>();
private _disposable: vscode.Disposable;
constructor(private readonly context: vscode.ExtensionContext, private readonly type: AuthProviderType) {
const { name, version, aiKey } = context.extension.packageJSON as { name: string; version: string; aiKey: string };
this._telemetryReporter = new ExperimentationTelemetry(context, new TelemetryReporter(name, version, aiKey));
constructor(
private readonly context: vscode.ExtensionContext,
uriHandler: UriEventHandler,
ghesUri?: vscode.Uri
) {
const { aiKey } = context.extension.packageJSON as { name: string; version: string; aiKey: string };
this._telemetryReporter = new ExperimentationTelemetry(context, new TelemetryReporter(aiKey));
if (this.type === AuthProviderType.github) {
this._githubServer = new GitHubServer(
// We only can use the Device Code flow when we have a full node environment because of CORS.
context.extension.extensionKind === vscode.ExtensionKind.Workspace || vscode.env.uiKind === vscode.UIKind.Desktop,
this._logger,
this._telemetryReporter);
} else {
this._githubServer = new GitHubEnterpriseServer(this._logger, this._telemetryReporter);
}
const type = ghesUri ? AuthProviderType.githubEnterprise : AuthProviderType.github;
this._logger = new Log(type);
this._keychain = new Keychain(
this.context,
type === AuthProviderType.github
? `${type}.auth`
: `${ghesUri?.authority}${ghesUri?.path}.ghes.auth`,
this._logger);
this._githubServer = new GitHubServer(
this._logger,
this._telemetryReporter,
uriHandler,
context.extension.extensionKind,
ghesUri);
// Contains the current state of the sessions we have available.
this._sessionsPromise = this.readSessions().then((sessions) => {
@@ -62,14 +80,13 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
this._disposable = vscode.Disposable.from(
this._telemetryReporter,
this._githubServer,
vscode.authentication.registerAuthenticationProvider(type, this._githubServer.friendlyName, this, { supportsMultipleAccounts: false }),
this.context.secrets.onDidChange(() => this.checkForUpdates())
);
}
dispose() {
this._disposable.dispose();
this._disposable?.dispose();
}
get onDidChangeSessions() {
@@ -93,7 +110,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
// We only want to fire a telemetry if we haven't seen this account yet in this session.
if (!this._accountsSeen.has(session.account.id)) {
this._accountsSeen.add(session.account.id);
this._githubServer.sendAdditionalTelemetryInfo(session.accessToken);
this._githubServer.sendAdditionalTelemetryInfo(session);
}
}
@@ -259,7 +276,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
*/
this._telemetryReporter?.sendTelemetryEvent('loginFailed');
vscode.window.showErrorMessage(`Sign in failed: ${e}`);
vscode.window.showErrorMessage(vscode.l10n.t('Sign in failed: {0}', `${e}`));
this._logger.error(e);
throw e;
}
@@ -268,7 +285,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
private async tokenToSession(token: string, scopes: string[]): Promise<vscode.AuthenticationSession> {
const userInfo = await this._githubServer.getUserInfo(token);
return {
id: uuid(),
id: crypto.getRandomValues(new Uint32Array(2)).reduce((prev, curr) => prev += curr.toString(16), ''),
accessToken: token,
account: { label: userInfo.accountName, id: userInfo.id },
scopes
@@ -302,7 +319,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
*/
this._telemetryReporter?.sendTelemetryEvent('logoutFailed');
vscode.window.showErrorMessage(`Sign out failed: ${e}`);
vscode.window.showErrorMessage(vscode.l10n.t('Sign out failed: {0}', `${e}`));
this._logger.error(e);
throw e;
}
@@ -3,45 +3,36 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import fetch, { Response } from 'node-fetch';
import { v4 as uuid } from 'uuid';
import * as path from 'path';
import { PromiseAdapter, promiseFromEvent } from './common/utils';
import { ExperimentationTelemetry } from './experimentationService';
import { AuthProviderType } from './github';
import { ExperimentationTelemetry } from './common/experimentationService';
import { AuthProviderType, UriEventHandler } from './github';
import { Log } from './common/logger';
import { isSupportedEnvironment } from './common/env';
import { LoopbackAuthServer } from './authServer';
import path = require('path');
import { isSupportedClient, isSupportedTarget } from './common/env';
import { LoopbackAuthServer } from './node/authServer';
import { crypto } from './node/crypto';
import { fetching } from './node/fetch';
const localize = nls.loadMessageBundle();
const CLIENT_ID = '01ab8ac9400c4e429b23';
const GITHUB_AUTHORIZE_URL = 'https://github.com/login/oauth/authorize';
// TODO: change to stable when that happens
const GITHUB_TOKEN_URL = 'https://vscode.dev/codeExchangeProxyEndpoints/github/login/oauth/access_token';
// This is the error message that we throw if the login was cancelled for any reason. Extensions
// calling `getSession` can handle this error to know that the user cancelled the login.
const CANCELLATION_ERROR = 'Cancelled';
// These error messages are internal and should not be shown to the user in any way.
const TIMED_OUT_ERROR = 'Timed out';
const USER_CANCELLATION_ERROR = 'User Cancelled';
const NETWORK_ERROR = 'network error';
const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect';
const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect';
class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
constructor(private readonly Logger: Log) {
super();
}
public handleUri(uri: vscode.Uri) {
this.Logger.trace('Handling Uri...');
this.fire(uri);
}
}
export interface IGitHubServer extends vscode.Disposable {
export interface IGitHubServer {
login(scopes: string): Promise<string>;
getUserInfo(token: string): Promise<{ id: string; accountName: string }>;
sendAdditionalTelemetryInfo(token: string): Promise<void>;
sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise<void>;
friendlyName: string;
type: AuthProviderType;
}
interface IGitHubDeviceCodeResponse {
@@ -54,10 +45,10 @@ interface IGitHubDeviceCodeResponse {
async function getScopes(token: string, serverUri: vscode.Uri, logger: Log): Promise<string[]> {
try {
logger.info('Getting token scopes...');
const result = await fetch(serverUri.toString(), {
const result = await fetching(serverUri.toString(), {
headers: {
Authorization: `token ${token}`,
'User-Agent': 'Visual-Studio-Code'
'User-Agent': `${vscode.env.appName} (${vscode.env.appHost})`
}
});
@@ -74,71 +65,55 @@ async function getScopes(token: string, serverUri: vscode.Uri, logger: Log): Pro
}
}
async function getUserInfo(token: string, serverUri: vscode.Uri, logger: Log): Promise<{ id: string; accountName: string }> {
let result: Response;
try {
logger.info('Getting user info...');
result = await fetch(serverUri.toString(), {
headers: {
Authorization: `token ${token}`,
'User-Agent': 'Visual-Studio-Code'
}
});
} catch (ex) {
logger.error(ex.message);
throw new Error(NETWORK_ERROR);
}
if (result.ok) {
try {
const json = await result.json();
logger.info('Got account info!');
return { id: json.id, accountName: json.login };
} catch (e) {
logger.error(`Unexpected error parsing response from GitHub: ${e.message ?? e}`);
throw e;
}
} else {
// either display the response message or the http status text
let errorMessage = result.statusText;
try {
const json = await result.json();
if (json.message) {
errorMessage = json.message;
}
} catch (err) {
// noop
}
logger.error(`Getting account info failed: ${errorMessage}`);
throw new Error(errorMessage);
}
}
export class GitHubServer implements IGitHubServer {
friendlyName = 'GitHub';
type = AuthProviderType.github;
readonly friendlyName: string;
private _pendingNonces = new Map<string, string[]>();
private _codeExchangePromises = new Map<string, { promise: Promise<string>; cancel: vscode.EventEmitter<void> }>();
private _disposable: vscode.Disposable;
private _uriHandler = new UriEventHandler(this._logger);
private readonly getRedirectEndpoint: Thenable<string>;
private readonly _pendingNonces = new Map<string, string[]>();
private readonly _codeExchangePromises = new Map<string, { promise: Promise<string>; cancel: vscode.EventEmitter<void> }>();
private readonly _type: AuthProviderType;
constructor(private readonly _supportDeviceCodeFlow: boolean, private readonly _logger: Log, private readonly _telemetryReporter: ExperimentationTelemetry) {
this._disposable = vscode.window.registerUriHandler(this._uriHandler);
private _redirectEndpoint: string | undefined;
this.getRedirectEndpoint = vscode.commands.executeCommand<{ [providerId: string]: string } | undefined>('workbench.getCodeExchangeProxyEndpoints').then((proxyEndpoints) => {
// If we are running in insiders vscode.dev, then ensure we use the redirect route on that.
let redirectUri = REDIRECT_URL_STABLE;
if (proxyEndpoints?.github && new URL(proxyEndpoints.github).hostname === 'insiders.vscode.dev') {
redirectUri = REDIRECT_URL_INSIDERS;
}
return redirectUri;
});
constructor(
private readonly _logger: Log,
private readonly _telemetryReporter: ExperimentationTelemetry,
private readonly _uriHandler: UriEventHandler,
private readonly _extensionKind: vscode.ExtensionKind,
private readonly _ghesUri?: vscode.Uri
) {
this._type = _ghesUri ? AuthProviderType.githubEnterprise : AuthProviderType.github;
this.friendlyName = this._type === AuthProviderType.github ? 'GitHub' : _ghesUri?.authority!;
}
dispose() {
this._disposable.dispose();
get baseUri() {
if (this._type === AuthProviderType.github) {
return vscode.Uri.parse('https://github.com/');
}
return this._ghesUri!;
}
private async getRedirectEndpoint(): Promise<string> {
if (this._redirectEndpoint) {
return this._redirectEndpoint;
}
if (this._type === AuthProviderType.github) {
const proxyEndpoints = await vscode.commands.executeCommand<{ [providerId: string]: string } | undefined>('workbench.getCodeExchangeProxyEndpoints');
// If we are running in insiders vscode.dev, then ensure we use the redirect route on that.
this._redirectEndpoint = REDIRECT_URL_STABLE;
if (proxyEndpoints?.github && new URL(proxyEndpoints.github).hostname === 'insiders.vscode.dev') {
this._redirectEndpoint = REDIRECT_URL_INSIDERS;
}
} else {
// GHE only supports a single redirect endpoint, so we can't use
// insiders.vscode.dev/redirect when we're running in Insiders, unfortunately.
// Additionally, we make the assumption that this function will only be used
// in flows that target supported GHE targets, not on-prem GHES. Because of this
// assumption, we can assume that the GHE version used is at least 3.8 which is
// the version that changed the redirect endpoint to this URI from the old
// GitHub maintained server.
this._redirectEndpoint = 'https://vscode.dev/redirect';
}
return this._redirectEndpoint;
}
// TODO@joaomoreno TODO@TylerLeonhardt
@@ -152,99 +127,117 @@ export class GitHubServer implements IGitHubServer {
// Used for showing a friendlier message to the user when the explicitly cancel a flow.
let userCancelled: boolean | undefined;
const yes = localize('yes', "Yes");
const no = localize('no', "No");
const promptToContinue = async () => {
const yes = vscode.l10n.t('Yes');
const no = vscode.l10n.t('No');
const promptToContinue = async (mode: string) => {
if (userCancelled === undefined) {
// We haven't had a failure yet so wait to prompt
return;
}
const message = userCancelled
? localize('userCancelledMessage', "Having trouble logging in? Would you like to try a different way?")
: localize('otherReasonMessage', "You have not yet finished authorizing this extension to use GitHub. Would you like to keep trying?");
? vscode.l10n.t('Having trouble logging in? Would you like to try a different way? ({0})', mode)
: vscode.l10n.t('You have not yet finished authorizing this extension to use GitHub. Would you like to try a different way? ({0})', mode);
const result = await vscode.window.showWarningMessage(message, yes, no);
if (result !== yes) {
throw new Error('Cancelled');
throw new Error(CANCELLATION_ERROR);
}
};
const nonce = uuid();
const nonce: string = crypto.getRandomValues(new Uint32Array(2)).reduce((prev, curr) => prev += curr.toString(16), '');
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate?nonce=${encodeURIComponent(nonce)}`));
const supported = isSupportedEnvironment(callbackUri);
if (supported) {
const supportedClient = isSupportedClient(callbackUri);
const supportedTarget = isSupportedTarget(this._type, this._ghesUri);
if (supportedClient && supportedTarget) {
try {
return await this.doLoginWithoutLocalServer(scopes, nonce, callbackUri);
} catch (e) {
this._logger.error(e);
userCancelled = e.message ?? e === 'User Cancelled';
userCancelled = e.message ?? e === USER_CANCELLATION_ERROR;
}
}
// Starting a local server isn't supported in web
if (vscode.env.uiKind === vscode.UIKind.Desktop) {
// Starting a local server is only supported if:
// 1. We are in a UI extension because we need to open a port on the machine that has the browser
// 2. We are in a node runtime because we need to open a port on the machine
// 3. code exchange can only be done with a supported target
if (
this._extensionKind === vscode.ExtensionKind.UI &&
typeof navigator === 'undefined' &&
supportedTarget
) {
try {
await promptToContinue();
await promptToContinue(vscode.l10n.t('local server'));
return await this.doLoginWithLocalServer(scopes);
} catch (e) {
this._logger.error(e);
userCancelled = e.message ?? e === 'User Cancelled';
userCancelled = this.processLoginError(e);
}
}
if (this._supportDeviceCodeFlow) {
// We only can use the Device Code flow when we have a full node environment because of CORS.
if (typeof navigator === 'undefined') {
try {
await promptToContinue();
await promptToContinue(vscode.l10n.t('device code'));
return await this.doLoginDeviceCodeFlow(scopes);
} catch (e) {
this._logger.error(e);
userCancelled = e.message ?? e === 'User Cancelled';
}
} else if (!supported) {
try {
await promptToContinue();
return await this.doLoginWithPat(scopes);
} catch (e) {
this._logger.error(e);
userCancelled = e.message ?? e === 'User Cancelled';
userCancelled = this.processLoginError(e);
}
}
throw new Error(userCancelled ? 'Cancelled' : 'No auth flow succeeded.');
// In a supported environment, we can't use PAT auth because we use this auth for Settings Sync and it doesn't support PATs.
// With that said, GitHub Enterprise isn't used by Settings Sync so we can use PATs for that.
if (!supportedClient || this._type === AuthProviderType.githubEnterprise) {
try {
await promptToContinue(vscode.l10n.t('personal access token'));
return await this.doLoginWithPat(scopes);
} catch (e) {
userCancelled = this.processLoginError(e);
}
}
throw new Error(userCancelled ? CANCELLATION_ERROR : 'No auth flow succeeded.');
}
private async doLoginWithoutLocalServer(scopes: string, nonce: string, callbackUri: vscode.Uri): Promise<string> {
this._logger.info(`Trying without local server... (${scopes})`);
return await vscode.window.withProgress<string>({
location: vscode.ProgressLocation.Notification,
title: localize('signingIn', "Signing in to github.com..."),
title: vscode.l10n.t({
message: 'Signing in to {0}...',
args: [this.baseUri.authority],
comment: ['The {0} will be a url, e.g. github.com']
}),
cancellable: true
}, async (_, token) => {
const existingNonces = this._pendingNonces.get(scopes) || [];
this._pendingNonces.set(scopes, [...existingNonces, nonce]);
const redirectUri = await this.getRedirectEndpoint;
const redirectUri = await this.getRedirectEndpoint();
const searchParams = new URLSearchParams([
['client_id', CLIENT_ID],
['redirect_uri', redirectUri],
['scope', scopes],
['state', encodeURIComponent(callbackUri.toString(true))]
]);
const uri = vscode.Uri.parse(`${GITHUB_AUTHORIZE_URL}?${searchParams.toString()}`);
const uri = vscode.Uri.parse(this.baseUri.with({
path: '/login/oauth/authorize',
query: searchParams.toString()
}).toString(true));
await vscode.env.openExternal(uri);
// Register a single listener for the URI callback, in case the user starts the login process multiple times
// before completing it.
let codeExchangePromise = this._codeExchangePromises.get(scopes);
if (!codeExchangePromise) {
codeExchangePromise = promiseFromEvent(this._uriHandler.event, this.handleUri(scopes));
codeExchangePromise = promiseFromEvent(this._uriHandler!.event, this.handleUri(scopes));
this._codeExchangePromises.set(scopes, codeExchangePromise);
}
try {
return await Promise.race([
codeExchangePromise.promise,
new Promise<string>((_, reject) => setTimeout(() => reject('Cancelled'), 60000)),
promiseFromEvent<any, any>(token.onCancellationRequested, (_, __, reject) => { reject('User Cancelled'); }).promise
new Promise<string>((_, reject) => setTimeout(() => reject(TIMED_OUT_ERROR), 300_000)), // 5min timeout
promiseFromEvent<any, any>(token.onCancellationRequested, (_, __, reject) => { reject(USER_CANCELLATION_ERROR); }).promise
]);
} finally {
this._pendingNonces.delete(scopes);
@@ -258,17 +251,25 @@ export class GitHubServer implements IGitHubServer {
this._logger.info(`Trying with local server... (${scopes})`);
return await vscode.window.withProgress<string>({
location: vscode.ProgressLocation.Notification,
title: localize('signingInAnotherWay', "Signing in to github.com..."),
title: vscode.l10n.t({
message: 'Signing in to {0}...',
args: [this.baseUri.authority],
comment: ['The {0} will be a url, e.g. github.com']
}),
cancellable: true
}, async (_, token) => {
const redirectUri = await this.getRedirectEndpoint;
const redirectUri = await this.getRedirectEndpoint();
const searchParams = new URLSearchParams([
['client_id', CLIENT_ID],
['redirect_uri', redirectUri],
['scope', scopes],
]);
const loginUrl = `${GITHUB_AUTHORIZE_URL}?${searchParams.toString()}`;
const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl);
const loginUrl = this.baseUri.with({
path: '/login/oauth/authorize',
query: searchParams.toString()
});
const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl.toString(true));
const port = await server.start();
let codeToExchange;
@@ -276,8 +277,8 @@ export class GitHubServer implements IGitHubServer {
vscode.env.openExternal(vscode.Uri.parse(`http://127.0.0.1:${port}/signin?nonce=${encodeURIComponent(server.nonce)}`));
const { code } = await Promise.race([
server.waitForOAuthResponse(),
new Promise<any>((_, reject) => setTimeout(() => reject('Cancelled'), 60000)),
promiseFromEvent<any, any>(token.onCancellationRequested, (_, __, reject) => { reject('User Cancelled'); }).promise
new Promise<any>((_, reject) => setTimeout(() => reject(TIMED_OUT_ERROR), 300_000)), // 5min timeout
promiseFromEvent<any, any>(token.onCancellationRequested, (_, __, reject) => { reject(USER_CANCELLATION_ERROR); }).promise
]);
codeToExchange = code;
} finally {
@@ -295,8 +296,11 @@ export class GitHubServer implements IGitHubServer {
this._logger.info(`Trying device code flow... (${scopes})`);
// Get initial device code
const uri = `https://github.com/login/device/code?client_id=${CLIENT_ID}&scope=${scopes}`;
const result = await fetch(uri, {
const uri = this.baseUri.with({
path: '/login/device/code',
query: `client_id=${CLIENT_ID}&scope=${scopes}`
});
const result = await fetching(uri.toString(true), {
method: 'POST',
headers: {
Accept: 'application/json'
@@ -308,16 +312,16 @@ export class GitHubServer implements IGitHubServer {
const json = await result.json() as IGitHubDeviceCodeResponse;
const button = vscode.l10n.t('Copy & Continue to GitHub');
const modalResult = await vscode.window.showInformationMessage(
localize('code.title', "Your Code: {0}", json.user_code),
vscode.l10n.t({ message: 'Your Code: {0}', args: [json.user_code], comment: ['The {0} will be a code, e.g. 123-456'] }),
{
modal: true,
detail: localize('code.detail', "To finish authenticating, navigate to GitHub and paste in the above one-time code.")
}, 'Copy & Continue to GitHub');
detail: vscode.l10n.t('To finish authenticating, navigate to GitHub and paste in the above one-time code.')
}, button);
if (modalResult !== 'Copy & Continue to GitHub') {
throw new Error('User Cancelled');
if (modalResult !== button) {
throw new Error(USER_CANCELLATION_ERROR);
}
await vscode.env.clipboard.writeText(json.user_code);
@@ -330,8 +334,24 @@ export class GitHubServer implements IGitHubServer {
private async doLoginWithPat(scopes: string): Promise<string> {
this._logger.info(`Trying to retrieve PAT... (${scopes})`);
const token = await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true });
if (!token) { throw new Error('User Cancelled'); }
const button = vscode.l10n.t('Continue to GitHub');
const modalResult = await vscode.window.showInformationMessage(
vscode.l10n.t('Continue to GitHub to create a Personal Access Token (PAT)'),
{
modal: true,
detail: vscode.l10n.t('To finish authenticating, navigate to GitHub to create a PAT then paste the PAT into the input box.')
}, button);
if (modalResult !== button) {
throw new Error(USER_CANCELLATION_ERROR);
}
const description = `${vscode.env.appName} (${scopes})`;
const uriToOpen = await vscode.env.asExternalUri(this.baseUri.with({ path: '/settings/tokens/new', query: `description=${description}&scopes=${scopes.split(' ').join(',')}` }));
await vscode.env.openExternal(uriToOpen);
const token = await vscode.window.showInputBox({ placeHolder: `ghp_1a2b3c4...`, prompt: `GitHub Personal Access Token - ${scopes}`, ignoreFocusOut: true });
if (!token) { throw new Error(USER_CANCELLATION_ERROR); }
const tokenScopes = await getScopes(token, this.getServerUri('/'), this._logger); // Example: ['repo', 'user']
const scopesList = scopes.split(' '); // Example: 'read:user repo user:email'
@@ -357,24 +377,30 @@ export class GitHubServer implements IGitHubServer {
return await vscode.window.withProgress<string>({
location: vscode.ProgressLocation.Notification,
cancellable: true,
title: localize(
'progress',
"Open [{0}]({0}) in a new tab and paste your one-time code: {1}",
json.verification_uri,
json.user_code)
title: vscode.l10n.t({
message: 'Open [{0}]({0}) in a new tab and paste your one-time code: {1}',
args: [json.verification_uri, json.user_code],
comment: [
'The [{0}]({0}) will be a url and the {1} will be a code, e.g. 123-456',
'{Locked="[{0}]({0})"}'
]
})
}, async (_, token) => {
const refreshTokenUri = `https://github.com/login/oauth/access_token?client_id=${CLIENT_ID}&device_code=${json.device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code`;
const refreshTokenUri = this.baseUri.with({
path: '/login/oauth/access_token',
query: `client_id=${CLIENT_ID}&device_code=${json.device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code`
});
// Try for 2 minutes
const attempts = 120 / json.interval;
for (let i = 0; i < attempts; i++) {
await new Promise(resolve => setTimeout(resolve, json.interval * 1000));
if (token.isCancellationRequested) {
throw new Error('User Cancelled');
throw new Error(USER_CANCELLATION_ERROR);
}
let accessTokenResult;
try {
accessTokenResult = await fetch(refreshTokenUri, {
accessTokenResult = await fetching(refreshTokenUri.toString(true), {
method: 'POST',
headers: {
Accept: 'application/json'
@@ -401,7 +427,7 @@ export class GitHubServer implements IGitHubServer {
return accessTokenJson.access_token;
}
throw new Error('Cancelled');
throw new Error(TIMED_OUT_ERROR);
});
}
@@ -439,8 +465,12 @@ export class GitHubServer implements IGitHubServer {
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
const endpointUrl = proxyEndpoints?.github ? `${proxyEndpoints.github}login/oauth/access_token` : GITHUB_TOKEN_URL;
const body = `code=${code}`;
const result = await fetch(endpointUrl, {
const body = new URLSearchParams([['code', code]]);
if (this._type === AuthProviderType.githubEnterprise) {
body.append('github_enterprise', this.baseUri.toString(true));
body.append('redirect_uri', await this.getRedirectEndpoint());
}
const result = await fetching(endpointUrl, {
method: 'POST',
headers: {
Accept: 'application/json',
@@ -448,7 +478,7 @@ export class GitHubServer implements IGitHubServer {
'Content-Length': body.toString()
},
body
body: body.toString()
});
if (result.ok) {
@@ -464,15 +494,56 @@ export class GitHubServer implements IGitHubServer {
}
private getServerUri(path: string = '') {
const apiUri = vscode.Uri.parse('https://api.github.com');
return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}${path}`);
const apiUri = this.baseUri;
// github.com and Hosted GitHub Enterprise instances
if (isSupportedTarget(this._type, this._ghesUri)) {
return vscode.Uri.parse(`${apiUri.scheme}://api.${apiUri.authority}`).with({ path });
}
// GitHub Enterprise Server (aka on-prem)
return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}/api/v3${path}`);
}
public getUserInfo(token: string): Promise<{ id: string; accountName: string }> {
return getUserInfo(token, this.getServerUri('/user'), this._logger);
public async getUserInfo(token: string): Promise<{ id: string; accountName: string }> {
let result;
try {
this._logger.info('Getting user info...');
result = await fetching(this.getServerUri('/user').toString(), {
headers: {
Authorization: `token ${token}`,
'User-Agent': `${vscode.env.appName} (${vscode.env.appHost})`
}
});
} catch (ex) {
this._logger.error(ex.message);
throw new Error(NETWORK_ERROR);
}
if (result.ok) {
try {
const json = await result.json();
this._logger.info('Got account info!');
return { id: json.id, accountName: json.login };
} catch (e) {
this._logger.error(`Unexpected error parsing response from GitHub: ${e.message ?? e}`);
throw e;
}
} else {
// either display the response message or the http status text
let errorMessage = result.statusText;
try {
const json = await result.json();
if (json.message) {
errorMessage = json.message;
}
} catch (err) {
// noop
}
this._logger.error(`Getting account info failed: ${errorMessage}`);
throw new Error(errorMessage);
}
}
public async sendAdditionalTelemetryInfo(token: string): Promise<void> {
public async sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise<void> {
if (!vscode.env.isTelemetryEnabled) {
return;
}
@@ -482,53 +553,75 @@ export class GitHubServer implements IGitHubServer {
return;
}
if (this._type === AuthProviderType.github) {
return await this.checkUserDetails(session);
}
// GHES
await this.checkEnterpriseVersion(session.accessToken);
}
private async checkUserDetails(session: vscode.AuthenticationSession): Promise<void> {
let edu: string | undefined;
try {
const result = await fetch('https://education.github.com/api/user', {
const result = await fetching('https://education.github.com/api/user', {
headers: {
Authorization: `token ${token}`,
Authorization: `token ${session.accessToken}`,
'faculty-check-preview': 'true',
'User-Agent': 'Visual-Studio-Code'
'User-Agent': `${vscode.env.appName} (${vscode.env.appHost})`
}
});
if (result.ok) {
const json: { student: boolean; faculty: boolean } = await result.json();
/* __GDPR__
"session" : {
"owner": "TylerLeonhardt",
"isEdu": { "classification": "NonIdentifiableDemographicInfo", "purpose": "FeatureInsight" }
}
*/
this._telemetryReporter.sendTelemetryEvent('session', {
isEdu: json.student
? 'student'
: json.faculty
? 'faculty'
: 'none'
});
edu = json.student
? 'student'
: json.faculty
? 'faculty'
: 'none';
} else {
edu = 'unknown';
}
} catch (e) {
// No-op
edu = 'unknown';
}
/* __GDPR__
"session" : {
"owner": "TylerLeonhardt",
"isEdu": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"isManaged": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this._telemetryReporter.sendTelemetryEvent('session', {
isEdu: edu,
// Apparently, this is how you tell if a user is an EMU...
isManaged: session.account.label.includes('_') ? 'true' : 'false'
});
}
public async checkEnterpriseVersion(token: string): Promise<void> {
private async checkEnterpriseVersion(token: string): Promise<void> {
try {
let version: string;
if (!isSupportedTarget(this._type, this._ghesUri)) {
const result = await fetching(this.getServerUri('/meta').toString(), {
headers: {
Authorization: `token ${token}`,
'User-Agent': `${vscode.env.appName} (${vscode.env.appHost})`
}
});
const result = await fetch(this.getServerUri('/meta').toString(), {
headers: {
Authorization: `token ${token}`,
'User-Agent': 'Visual-Studio-Code'
if (!result.ok) {
return;
}
});
if (!result.ok) {
return;
const json: { verifiable_password_authentication: boolean; installed_version: string } = await result.json();
version = json.installed_version;
} else {
version = 'hosted';
}
const json: { verifiable_password_authentication: boolean; installed_version: string } = await result.json();
/* __GDPR__
"ghe-session" : {
"owner": "TylerLeonhardt",
@@ -536,82 +629,18 @@ export class GitHubServer implements IGitHubServer {
}
*/
this._telemetryReporter.sendTelemetryEvent('ghe-session', {
version: json.installed_version
version
});
} catch {
// No-op
}
}
}
export class GitHubEnterpriseServer implements IGitHubServer {
friendlyName = 'GitHub Enterprise';
type = AuthProviderType.githubEnterprise;
constructor(private readonly _logger: Log, private readonly telemetryReporter: ExperimentationTelemetry) { }
dispose() { }
public async login(scopes: string): Promise<string> {
this._logger.info(`Logging in for the following scopes: ${scopes}`);
const token = await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true });
if (!token) { throw new Error('Sign in failed: No token provided'); }
const tokenScopes = await getScopes(token, this.getServerUri('/'), this._logger); // Example: ['repo', 'user']
const scopesList = scopes.split(' '); // Example: 'read:user repo user:email'
if (!scopesList.every(scope => {
const included = tokenScopes.includes(scope);
if (included || !scope.includes(':')) {
return included;
}
return scope.split(':').some(splitScopes => {
return tokenScopes.includes(splitScopes);
});
})) {
throw new Error(`The provided token does not match the requested scopes: ${scopes}`);
}
return token;
}
private getServerUri(path: string = '') {
const apiUri = vscode.Uri.parse(vscode.workspace.getConfiguration('github-enterprise').get<string>('uri') || '', true);
return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}/api/v3${path}`);
}
public async getUserInfo(token: string): Promise<{ id: string; accountName: string }> {
return getUserInfo(token, this.getServerUri('/user'), this._logger);
}
public async sendAdditionalTelemetryInfo(token: string): Promise<void> {
try {
const result = await fetch(this.getServerUri('/meta').toString(), {
headers: {
Authorization: `token ${token}`,
'User-Agent': 'Visual-Studio-Code'
}
});
if (!result.ok) {
return;
}
const json: { verifiable_password_authentication: boolean; installed_version: string } = await result.json();
/* __GDPR__
"ghe-session" : {
"owner": "TylerLeonhardt",
"version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryReporter.sendTelemetryEvent('ghe-session', {
version: json.installed_version
});
} catch {
// No-op
private processLoginError(error: Error): boolean {
if (error.message === CANCELLATION_ERROR) {
throw error;
}
this._logger.error(error.message ?? error);
return error.message === USER_CANCELLATION_ERROR;
}
}

Some files were not shown because too many files have changed in this diff Show More