chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
legacy-peer-deps="true"
|
||||
timeout=180000
|
||||
@@ -0,0 +1,7 @@
|
||||
src/**
|
||||
!src/common/config.json
|
||||
out/**
|
||||
build/**
|
||||
extension.webpack.config.js
|
||||
tsconfig.json
|
||||
package-lock.json
|
||||
@@ -0,0 +1,12 @@
|
||||
# GitHub for Visual Studio Code
|
||||
|
||||
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
|
||||
|
||||
## Features
|
||||
|
||||
This extension provides the following GitHub-related features for VS Code:
|
||||
|
||||
- `Publish to GitHub` command
|
||||
- `Clone from GitHub` participant to the `Git: Clone` command
|
||||
- GitHub authentication for built-in git commands, controlled via the `github.gitAuthentication` command
|
||||
- Automatic fork creation when attempting to push to a repository without permissions
|
||||
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withDefaults = require('../shared.webpack.config');
|
||||
|
||||
module.exports = withDefaults({
|
||||
context: __dirname,
|
||||
entry: {
|
||||
extension: './src/extension.ts'
|
||||
}
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,11 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.vscode-dark img[src$=\#gh-light-mode-only],
|
||||
.vscode-light img[src$=\#gh-dark-mode-only],
|
||||
.vscode-high-contrast:not(.vscode-high-contrast-light) img[src$=\#gh-light-mode-only],
|
||||
.vscode-high-contrast-light img[src$=\#gh-dark-mode-only] {
|
||||
display: none;
|
||||
}
|
||||
Generated
+405
@@ -0,0 +1,405 @@
|
||||
{
|
||||
"name": "github",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "github",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/graphql": "8.2.0",
|
||||
"@octokit/graphql-schema": "14.4.0",
|
||||
"@octokit/rest": "21.1.0",
|
||||
"@vscode/extension-telemetry": "^0.9.8",
|
||||
"tunnel": "^0.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.x"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.41.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/1ds-core-js": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz",
|
||||
"integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/applicationinsights-core-js": "3.3.4",
|
||||
"@microsoft/applicationinsights-shims": "3.0.1",
|
||||
"@microsoft/dynamicproto-js": "^2.0.3",
|
||||
"@nevware21/ts-async": ">= 0.5.2 < 2.x",
|
||||
"@nevware21/ts-utils": ">= 0.11.3 < 2.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/1ds-post-js": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz",
|
||||
"integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/1ds-core-js": "4.3.4",
|
||||
"@microsoft/applicationinsights-shims": "3.0.1",
|
||||
"@microsoft/dynamicproto-js": "^2.0.3",
|
||||
"@nevware21/ts-async": ">= 0.5.2 < 2.x",
|
||||
"@nevware21/ts-utils": ">= 0.11.3 < 2.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/applicationinsights-channel-js": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz",
|
||||
"integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/applicationinsights-common": "3.3.4",
|
||||
"@microsoft/applicationinsights-core-js": "3.3.4",
|
||||
"@microsoft/applicationinsights-shims": "3.0.1",
|
||||
"@microsoft/dynamicproto-js": "^2.0.3",
|
||||
"@nevware21/ts-async": ">= 0.5.2 < 2.x",
|
||||
"@nevware21/ts-utils": ">= 0.11.3 < 2.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tslib": ">= 1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/applicationinsights-common": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz",
|
||||
"integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/applicationinsights-core-js": "3.3.4",
|
||||
"@microsoft/applicationinsights-shims": "3.0.1",
|
||||
"@microsoft/dynamicproto-js": "^2.0.3",
|
||||
"@nevware21/ts-utils": ">= 0.11.3 < 2.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tslib": ">= 1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/applicationinsights-core-js": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz",
|
||||
"integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/applicationinsights-shims": "3.0.1",
|
||||
"@microsoft/dynamicproto-js": "^2.0.3",
|
||||
"@nevware21/ts-async": ">= 0.5.2 < 2.x",
|
||||
"@nevware21/ts-utils": ">= 0.11.3 < 2.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tslib": ">= 1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/applicationinsights-shims": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz",
|
||||
"integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nevware21/ts-utils": ">= 0.9.4 < 2.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/applicationinsights-web-basic": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz",
|
||||
"integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/applicationinsights-channel-js": "3.3.4",
|
||||
"@microsoft/applicationinsights-common": "3.3.4",
|
||||
"@microsoft/applicationinsights-core-js": "3.3.4",
|
||||
"@microsoft/applicationinsights-shims": "3.0.1",
|
||||
"@microsoft/dynamicproto-js": "^2.0.3",
|
||||
"@nevware21/ts-async": ">= 0.5.2 < 2.x",
|
||||
"@nevware21/ts-utils": ">= 0.11.3 < 2.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tslib": ">= 1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/dynamicproto-js": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz",
|
||||
"integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nevware21/ts-utils": ">= 0.10.4 < 2.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@nevware21/ts-async": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz",
|
||||
"integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nevware21/ts-utils": ">= 0.11.6 < 2.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@nevware21/ts-utils": {
|
||||
"version": "0.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.6.tgz",
|
||||
"integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/auth-token": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz",
|
||||
"integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/core": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.4.tgz",
|
||||
"integrity": "sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^5.0.0",
|
||||
"@octokit/graphql": "^8.1.2",
|
||||
"@octokit/request": "^9.2.1",
|
||||
"@octokit/request-error": "^6.1.7",
|
||||
"@octokit/types": "^13.6.2",
|
||||
"before-after-hook": "^3.0.2",
|
||||
"universal-user-agent": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz",
|
||||
"integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.6.2",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.0.tgz",
|
||||
"integrity": "sha512-gejfDywEml/45SqbWTWrhfwvLBrcGYhOn50sPOjIeVvH6i7D16/9xcFA8dAJNp2HMcd+g4vru41g4E2RBiZvfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/request": "^9.1.4",
|
||||
"@octokit/types": "^13.8.0",
|
||||
"universal-user-agent": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql-schema": {
|
||||
"version": "14.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql-schema/-/graphql-schema-14.4.0.tgz",
|
||||
"integrity": "sha512-+O6/dsLlR6V9gv+t1lqsN+x73TLwyQWZpd3M8/eYnuny7VaznV9TAyUxf18tX8WBBS5IqtlLDk1nG+aSTPRZzQ==",
|
||||
"dependencies": {
|
||||
"graphql": "^16.0.0",
|
||||
"graphql-tag": "^2.10.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/openapi-types": {
|
||||
"version": "23.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz",
|
||||
"integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest": {
|
||||
"version": "11.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.2.tgz",
|
||||
"integrity": "sha512-BXJ7XPCTDXFF+wxcg/zscfgw2O/iDPtNSkwwR1W1W5c4Mb3zav/M2XvxQ23nVmKj7jpweB4g8viMeCQdm7LMVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-request-log": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz",
|
||||
"integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-rest-endpoint-methods": {
|
||||
"version": "13.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.1.tgz",
|
||||
"integrity": "sha512-o8uOBdsyR+WR8MK9Cco8dCgvG13H1RlM1nWnK/W7TEACQBFux/vPREgKucxUfuDQ5yi1T3hGf4C5ZmZXAERgwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.1.tgz",
|
||||
"integrity": "sha512-TqHLIdw1KFvx8WvLc7Jv94r3C3+AzKY2FWq7c20zvrxmCIa6MCVkLCE/826NCXnml3LFJjLsidDh1BhMaGEDQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/endpoint": "^10.1.3",
|
||||
"@octokit/request-error": "^6.1.6",
|
||||
"@octokit/types": "^13.6.2",
|
||||
"fast-content-type-parse": "^2.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request-error": {
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz",
|
||||
"integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest": {
|
||||
"version": "21.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.0.tgz",
|
||||
"integrity": "sha512-93iLxcKDJboUpmnUyeJ6cRIi7z7cqTZT1K7kRK4LobGxwTwpsa+2tQQbRQNGy7IFDEAmrtkf4F4wBj3D5rVlJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/core": "^6.1.3",
|
||||
"@octokit/plugin-paginate-rest": "^11.4.0",
|
||||
"@octokit/plugin-request-log": "^5.3.1",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^13.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/types": {
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz",
|
||||
"integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^23.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.11.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
|
||||
"integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/extension-telemetry": {
|
||||
"version": "0.9.8",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.8.tgz",
|
||||
"integrity": "sha512-7YcKoUvmHlIB8QYCE4FNzt3ErHi9gQPhdCM3ZWtpw1bxPT0I+lMdx52KHlzTNoJzQ2NvMX7HyzyDwBEiMgTrWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/1ds-core-js": "^4.3.4",
|
||||
"@microsoft/1ds-post-js": "^4.3.4",
|
||||
"@microsoft/applicationinsights-web-basic": "^3.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
}
|
||||
},
|
||||
"node_modules/before-after-hook": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
|
||||
"integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fast-content-type-parse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
|
||||
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "16.8.1",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
|
||||
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graphql-tag": {
|
||||
"version": "2.12.6",
|
||||
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz",
|
||||
"integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||
},
|
||||
"node_modules/tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||
"engines": {
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/universal-user-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
{
|
||||
"name": "github",
|
||||
"displayName": "%displayName%",
|
||||
"description": "%description%",
|
||||
"publisher": "vscode",
|
||||
"license": "MIT",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"vscode": "^1.41.0"
|
||||
},
|
||||
"aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
|
||||
"icon": "images/icon.png",
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"*"
|
||||
],
|
||||
"extensionDependencies": [
|
||||
"vscode.git-base"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"capabilities": {
|
||||
"virtualWorkspaces": false,
|
||||
"untrustedWorkspaces": {
|
||||
"supported": true
|
||||
}
|
||||
},
|
||||
"enabledApiProposals": [
|
||||
"canonicalUriProvider",
|
||||
"contribEditSessions",
|
||||
"contribShareMenu",
|
||||
"contribSourceControlHistoryItemMenu",
|
||||
"scmHistoryProvider",
|
||||
"shareProvider",
|
||||
"timeline"
|
||||
],
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "github.publish",
|
||||
"title": "%command.publish%"
|
||||
},
|
||||
{
|
||||
"command": "github.copyVscodeDevLink",
|
||||
"title": "%command.copyVscodeDevLink%"
|
||||
},
|
||||
{
|
||||
"command": "github.copyVscodeDevLinkFile",
|
||||
"title": "%command.copyVscodeDevLink%"
|
||||
},
|
||||
{
|
||||
"command": "github.copyVscodeDevLinkWithoutRange",
|
||||
"title": "%command.copyVscodeDevLink%"
|
||||
},
|
||||
{
|
||||
"command": "github.openOnVscodeDev",
|
||||
"title": "%command.openOnVscodeDev%",
|
||||
"icon": "$(globe)"
|
||||
},
|
||||
{
|
||||
"command": "github.graph.openOnGitHub",
|
||||
"title": "%command.openOnGitHub%",
|
||||
"icon": "$(github)"
|
||||
},
|
||||
{
|
||||
"command": "github.timeline.openOnGitHub",
|
||||
"title": "%command.openOnGitHub%",
|
||||
"icon": "$(github)"
|
||||
}
|
||||
],
|
||||
"continueEditSession": [
|
||||
{
|
||||
"command": "github.openOnVscodeDev",
|
||||
"when": "github.hasGitHubRepo",
|
||||
"qualifiedName": "Continue Working in vscode.dev",
|
||||
"category": "Remote Repositories",
|
||||
"remoteGroup": "virtualfs_44_vscode-vfs_2_web@2"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "github.publish",
|
||||
"when": "git-base.gitEnabled && workspaceFolderCount != 0 && remoteName != 'codespaces'"
|
||||
},
|
||||
{
|
||||
"command": "github.graph.openOnGitHub",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "github.copyVscodeDevLink",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "github.copyVscodeDevLinkFile",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "github.copyVscodeDevLinkWithoutRange",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "github.openOnVscodeDev",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "github.timeline.openOnGitHub",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"file/share": [
|
||||
{
|
||||
"command": "github.copyVscodeDevLinkFile",
|
||||
"when": "github.hasGitHubRepo && remoteName != 'codespaces'",
|
||||
"group": "0_vscode@0"
|
||||
}
|
||||
],
|
||||
"editor/context/share": [
|
||||
{
|
||||
"command": "github.copyVscodeDevLink",
|
||||
"when": "github.hasGitHubRepo && resourceScheme != untitled && !isInEmbeddedEditor && remoteName != 'codespaces'",
|
||||
"group": "0_vscode@0"
|
||||
}
|
||||
],
|
||||
"explorer/context/share": [
|
||||
{
|
||||
"command": "github.copyVscodeDevLinkWithoutRange",
|
||||
"when": "github.hasGitHubRepo && resourceScheme != untitled && !isInEmbeddedEditor && remoteName != 'codespaces'",
|
||||
"group": "0_vscode@0"
|
||||
}
|
||||
],
|
||||
"editor/lineNumber/context": [
|
||||
{
|
||||
"command": "github.copyVscodeDevLink",
|
||||
"when": "github.hasGitHubRepo && resourceScheme != untitled && activeEditor == workbench.editors.files.textFileEditor && config.editor.lineNumbers == on && remoteName != 'codespaces'",
|
||||
"group": "1_cutcopypaste@2"
|
||||
},
|
||||
{
|
||||
"command": "github.copyVscodeDevLink",
|
||||
"when": "github.hasGitHubRepo && resourceScheme != untitled && activeEditor == workbench.editor.notebook && remoteName != 'codespaces'",
|
||||
"group": "1_cutcopypaste@2"
|
||||
}
|
||||
],
|
||||
"editor/title/context/share": [
|
||||
{
|
||||
"command": "github.copyVscodeDevLinkWithoutRange",
|
||||
"when": "github.hasGitHubRepo && resourceScheme != untitled && remoteName != 'codespaces'",
|
||||
"group": "0_vscode@0"
|
||||
}
|
||||
],
|
||||
"scm/historyItem/context": [
|
||||
{
|
||||
"command": "github.graph.openOnGitHub",
|
||||
"when": "github.hasGitHubRepo",
|
||||
"group": "0_view@2"
|
||||
}
|
||||
],
|
||||
"scm/historyItem/hover": [
|
||||
{
|
||||
"command": "github.graph.openOnGitHub",
|
||||
"when": "github.hasGitHubRepo",
|
||||
"group": "1_open@1"
|
||||
}
|
||||
],
|
||||
"timeline/item/context": [
|
||||
{
|
||||
"command": "github.timeline.openOnGitHub",
|
||||
"group": "1_actions@3",
|
||||
"when": "github.hasGitHubRepo && timelineItem =~ /git:file:commit\\b/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": [
|
||||
{
|
||||
"title": "GitHub",
|
||||
"properties": {
|
||||
"github.branchProtection": {
|
||||
"type": "boolean",
|
||||
"scope": "resource",
|
||||
"default": true,
|
||||
"description": "%config.branchProtection%"
|
||||
},
|
||||
"github.gitAuthentication": {
|
||||
"type": "boolean",
|
||||
"scope": "resource",
|
||||
"default": true,
|
||||
"description": "%config.gitAuthentication%"
|
||||
},
|
||||
"github.gitProtocol": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"https",
|
||||
"ssh"
|
||||
],
|
||||
"default": "https",
|
||||
"description": "%config.gitProtocol%"
|
||||
},
|
||||
"github.showAvatar": {
|
||||
"type": "boolean",
|
||||
"scope": "resource",
|
||||
"default": true,
|
||||
"description": "%config.showAvatar%"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"viewsWelcome": [
|
||||
{
|
||||
"view": "scm",
|
||||
"contents": "%welcome.publishFolder%",
|
||||
"when": "config.git.enabled && git.state == initialized && workbenchState == folder && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0"
|
||||
},
|
||||
{
|
||||
"view": "scm",
|
||||
"contents": "%welcome.publishWorkspaceFolder%",
|
||||
"when": "config.git.enabled && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0"
|
||||
}
|
||||
],
|
||||
"markdown.previewStyles": [
|
||||
"./markdown.css"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "gulp compile-extension:github",
|
||||
"watch": "gulp watch-extension:github"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/graphql": "8.2.0",
|
||||
"@octokit/graphql-schema": "14.4.0",
|
||||
"@octokit/rest": "21.1.0",
|
||||
"tunnel": "^0.0.6",
|
||||
"@vscode/extension-telemetry": "^0.9.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.x"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/microsoft/vscode.git"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"displayName": "GitHub",
|
||||
"description": "GitHub features for VS Code",
|
||||
"command.copyVscodeDevLink": "Copy vscode.dev Link",
|
||||
"command.publish": "Publish to GitHub",
|
||||
"command.openOnGitHub": "Open on GitHub",
|
||||
"command.openOnVscodeDev": "Open in vscode.dev",
|
||||
"config.branchProtection": "Controls whether to query repository rules for GitHub repositories",
|
||||
"config.gitAuthentication": "Controls whether to enable automatic GitHub authentication for git commands within VS Code.",
|
||||
"config.gitProtocol": "Controls which protocol is used to clone a GitHub repository",
|
||||
"config.showAvatar": "Controls whether to show the GitHub avatar of the commit author in various hovers (ex: Git blame, Timeline, Source Control Graph, etc.)",
|
||||
"welcome.publishFolder": {
|
||||
"message": "You can directly publish this folder to a GitHub repository. Once published, you'll have access to source control features powered by Git and GitHub.\n[$(github) Publish to GitHub](command:github.publish)",
|
||||
"comment": [
|
||||
"{Locked='$(github)'}",
|
||||
"Do not translate '$(github)'. It will be rendered as an icon",
|
||||
"{Locked='](command:github.publish'}",
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"welcome.publishWorkspaceFolder": {
|
||||
"message": "You can directly publish a workspace folder to a GitHub repository. Once published, you'll have access to source control features powered by Git and GitHub.\n[$(github) Publish to GitHub](command:github.publish)",
|
||||
"comment": [
|
||||
"{Locked='$(github)'}",
|
||||
"Do not translate '$(github)'. It will be rendered as an icon",
|
||||
"{Locked='](command:github.publish'}",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AuthenticationSession, authentication, window } from 'vscode';
|
||||
import { Agent, globalAgent } from 'https';
|
||||
import { graphql } from '@octokit/graphql/dist-types/types';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { httpsOverHttp } from 'tunnel';
|
||||
import { URL } from 'url';
|
||||
|
||||
export class AuthenticationError extends Error { }
|
||||
|
||||
function getAgent(url: string | undefined = process.env.HTTPS_PROXY): Agent {
|
||||
if (!url) {
|
||||
return globalAgent;
|
||||
}
|
||||
|
||||
try {
|
||||
const { hostname, port, username, password } = new URL(url);
|
||||
const auth = username && password && `${username}:${password}`;
|
||||
return httpsOverHttp({ proxy: { host: hostname, port, proxyAuth: auth } });
|
||||
} catch (e) {
|
||||
window.showErrorMessage(`HTTPS_PROXY environment variable ignored: ${e.message}`);
|
||||
return globalAgent;
|
||||
}
|
||||
}
|
||||
|
||||
const scopes = ['repo', 'workflow', 'user:email', 'read:user'];
|
||||
|
||||
export async function getSession(): Promise<AuthenticationSession> {
|
||||
return await authentication.getSession('github', scopes, { createIfNone: true });
|
||||
}
|
||||
|
||||
let _octokit: Promise<Octokit> | undefined;
|
||||
|
||||
export function getOctokit(): Promise<Octokit> {
|
||||
if (!_octokit) {
|
||||
_octokit = getSession().then(async session => {
|
||||
const token = session.accessToken;
|
||||
const agent = getAgent();
|
||||
|
||||
const { Octokit } = await import('@octokit/rest');
|
||||
|
||||
return new Octokit({
|
||||
request: { agent },
|
||||
userAgent: 'GitHub VSCode',
|
||||
auth: `token ${token}`
|
||||
});
|
||||
}).then(null, async err => {
|
||||
_octokit = undefined;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
return _octokit;
|
||||
}
|
||||
|
||||
let _octokitGraphql: Promise<graphql> | undefined;
|
||||
|
||||
export async function getOctokitGraphql(): Promise<graphql> {
|
||||
if (!_octokitGraphql) {
|
||||
try {
|
||||
const session = await authentication.getSession('github', scopes, { silent: true });
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError('No GitHub authentication session available.');
|
||||
}
|
||||
|
||||
const token = session.accessToken;
|
||||
const { graphql } = await import('@octokit/graphql');
|
||||
|
||||
return graphql.defaults({
|
||||
headers: {
|
||||
authorization: `token ${token}`
|
||||
},
|
||||
request: {
|
||||
agent: getAgent()
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
_octokitGraphql = undefined;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return _octokitGraphql;
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { authentication, EventEmitter, LogOutputChannel, Memento, Uri, workspace } from 'vscode';
|
||||
import { Repository as GitHubRepository, RepositoryRuleset } from '@octokit/graphql-schema';
|
||||
import { AuthenticationError, getOctokitGraphql } from './auth';
|
||||
import { API, BranchProtection, BranchProtectionProvider, BranchProtectionRule, Repository } from './typings/git';
|
||||
import { DisposableStore, getRepositoryFromUrl } from './util';
|
||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||
|
||||
const REPOSITORY_QUERY = `
|
||||
query repositoryPermissions($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
defaultBranchRef {
|
||||
name
|
||||
},
|
||||
viewerPermission
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const REPOSITORY_RULESETS_QUERY = `
|
||||
query repositoryRulesets($owner: String!, $repo: String!, $cursor: String, $limit: Int = 100) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
rulesets(includeParents: true, first: $limit, after: $cursor) {
|
||||
nodes {
|
||||
name
|
||||
enforcement
|
||||
rules(type: PULL_REQUEST) {
|
||||
totalCount
|
||||
}
|
||||
conditions {
|
||||
refName {
|
||||
include
|
||||
exclude
|
||||
}
|
||||
}
|
||||
target
|
||||
},
|
||||
pageInfo {
|
||||
endCursor,
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export class GitHubBranchProtectionProviderManager {
|
||||
|
||||
private readonly disposables = new DisposableStore();
|
||||
private readonly providerDisposables = new DisposableStore();
|
||||
|
||||
private _enabled = false;
|
||||
private set enabled(enabled: boolean) {
|
||||
if (this._enabled === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
for (const repository of this.gitAPI.repositories) {
|
||||
this.providerDisposables.add(this.gitAPI.registerBranchProtectionProvider(repository.rootUri, new GitHubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter)));
|
||||
}
|
||||
} else {
|
||||
this.providerDisposables.dispose();
|
||||
}
|
||||
|
||||
this._enabled = enabled;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly gitAPI: API,
|
||||
private readonly globalState: Memento,
|
||||
private readonly logger: LogOutputChannel,
|
||||
private readonly telemetryReporter: TelemetryReporter) {
|
||||
this.disposables.add(this.gitAPI.onDidOpenRepository(repository => {
|
||||
if (this._enabled) {
|
||||
this.providerDisposables.add(gitAPI.registerBranchProtectionProvider(repository.rootUri, new GitHubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter)));
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.add(workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('github.branchProtection')) {
|
||||
this.updateEnablement();
|
||||
}
|
||||
}));
|
||||
|
||||
this.updateEnablement();
|
||||
}
|
||||
|
||||
private updateEnablement(): void {
|
||||
const config = workspace.getConfiguration('github', null);
|
||||
this.enabled = config.get<boolean>('branchProtection', true) === true;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.enabled = false;
|
||||
this.disposables.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class GitHubBranchProtectionProvider implements BranchProtectionProvider {
|
||||
private readonly _onDidChangeBranchProtection = new EventEmitter<Uri>();
|
||||
onDidChangeBranchProtection = this._onDidChangeBranchProtection.event;
|
||||
|
||||
private branchProtection: BranchProtection[];
|
||||
private readonly globalStateKey = `branchProtection:${this.repository.rootUri.toString()}`;
|
||||
|
||||
constructor(
|
||||
private readonly repository: Repository,
|
||||
private readonly globalState: Memento,
|
||||
private readonly logger: LogOutputChannel,
|
||||
private readonly telemetryReporter: TelemetryReporter) {
|
||||
// Restore branch protection from global state
|
||||
this.branchProtection = this.globalState.get<BranchProtection[]>(this.globalStateKey, []);
|
||||
|
||||
repository.status().then(() => {
|
||||
authentication.onDidChangeSessions(e => {
|
||||
if (e.provider.id === 'github') {
|
||||
this.updateRepositoryBranchProtection();
|
||||
}
|
||||
});
|
||||
this.updateRepositoryBranchProtection();
|
||||
});
|
||||
}
|
||||
|
||||
provideBranchProtection(): BranchProtection[] {
|
||||
return this.branchProtection;
|
||||
}
|
||||
|
||||
private async getRepositoryDetails(owner: string, repo: string): Promise<GitHubRepository> {
|
||||
const graphql = await getOctokitGraphql();
|
||||
const { repository } = await graphql<{ repository: GitHubRepository }>(REPOSITORY_QUERY, { owner, repo });
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
private async getRepositoryRulesets(owner: string, repo: string): Promise<RepositoryRuleset[]> {
|
||||
const rulesets: RepositoryRuleset[] = [];
|
||||
|
||||
let cursor: string | undefined = undefined;
|
||||
const graphql = await getOctokitGraphql();
|
||||
|
||||
while (true) {
|
||||
const { repository } = await graphql<{ repository: GitHubRepository }>(REPOSITORY_RULESETS_QUERY, { owner, repo, cursor });
|
||||
|
||||
rulesets.push(...(repository.rulesets?.nodes ?? [])
|
||||
// Active branch ruleset that contains the pull request required rule
|
||||
.filter(node => node && node.target === 'BRANCH' && node.enforcement === 'ACTIVE' && (node.rules?.totalCount ?? 0) > 0) as RepositoryRuleset[]);
|
||||
|
||||
if (repository.rulesets?.pageInfo.hasNextPage) {
|
||||
cursor = repository.rulesets.pageInfo.endCursor as string | undefined;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return rulesets;
|
||||
}
|
||||
|
||||
private async updateRepositoryBranchProtection(): Promise<void> {
|
||||
const branchProtection: BranchProtection[] = [];
|
||||
|
||||
try {
|
||||
for (const remote of this.repository.state.remotes) {
|
||||
const repository = getRepositoryFromUrl(remote.pushUrl ?? remote.fetchUrl ?? '');
|
||||
|
||||
if (!repository) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Repository details
|
||||
this.logger.trace(`[GitHubBranchProtectionProvider][updateRepositoryBranchProtection] Fetching repository details for "${repository.owner}/${repository.repo}".`);
|
||||
const repositoryDetails = await this.getRepositoryDetails(repository.owner, repository.repo);
|
||||
|
||||
// Check repository write permission
|
||||
if (repositoryDetails.viewerPermission !== 'ADMIN' && repositoryDetails.viewerPermission !== 'MAINTAIN' && repositoryDetails.viewerPermission !== 'WRITE') {
|
||||
this.logger.trace(`[GitHubBranchProtectionProvider][updateRepositoryBranchProtection] Skipping branch protection for "${repository.owner}/${repository.repo}" due to missing repository write permission.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get repository rulesets
|
||||
const branchProtectionRules: BranchProtectionRule[] = [];
|
||||
const repositoryRulesets = await this.getRepositoryRulesets(repository.owner, repository.repo);
|
||||
|
||||
for (const ruleset of repositoryRulesets) {
|
||||
branchProtectionRules.push({
|
||||
include: (ruleset.conditions.refName?.include ?? []).map(r => this.parseRulesetRefName(repositoryDetails, r)),
|
||||
exclude: (ruleset.conditions.refName?.exclude ?? []).map(r => this.parseRulesetRefName(repositoryDetails, r))
|
||||
});
|
||||
}
|
||||
|
||||
branchProtection.push({ remote: remote.name, rules: branchProtectionRules });
|
||||
}
|
||||
|
||||
this.branchProtection = branchProtection;
|
||||
this._onDidChangeBranchProtection.fire(this.repository.rootUri);
|
||||
|
||||
// Save branch protection to global state
|
||||
await this.globalState.update(this.globalStateKey, branchProtection);
|
||||
this.logger.trace(`[GitHubBranchProtectionProvider][updateRepositoryBranchProtection] Branch protection for "${this.repository.rootUri.toString()}": ${JSON.stringify(branchProtection)}.`);
|
||||
|
||||
/* __GDPR__
|
||||
"branchProtection" : {
|
||||
"owner": "lszomoru",
|
||||
"rulesetCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of repository rulesets" }
|
||||
}
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('branchProtection', undefined, { rulesetCount: this.branchProtection.length });
|
||||
} catch (err) {
|
||||
this.logger.warn(`[GitHubBranchProtectionProvider][updateRepositoryBranchProtection] Failed to update repository branch protection: ${err.message}`);
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
// A GitHub authentication session could be missing if the user has not yet
|
||||
// signed in with their GitHub account or they have signed out. If there is
|
||||
// branch protection information we have to clear it.
|
||||
if (this.branchProtection.length !== 0) {
|
||||
this.branchProtection = branchProtection;
|
||||
this._onDidChangeBranchProtection.fire(this.repository.rootUri);
|
||||
|
||||
await this.globalState.update(this.globalStateKey, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseRulesetRefName(repository: GitHubRepository, refName: string): string {
|
||||
if (refName.startsWith('refs/heads/')) {
|
||||
return refName.substring(11);
|
||||
}
|
||||
|
||||
switch (refName) {
|
||||
case '~ALL':
|
||||
return '**/*';
|
||||
case '~DEFAULT_BRANCH':
|
||||
return repository.defaultBranchRef!.name;
|
||||
default:
|
||||
return refName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken, CanonicalUriProvider, CanonicalUriRequestOptions, Disposable, ProviderResult, Uri, workspace } from 'vscode';
|
||||
import { API } from './typings/git';
|
||||
|
||||
const SUPPORTED_SCHEMES = ['ssh', 'https', 'file'];
|
||||
|
||||
export class GitHubCanonicalUriProvider implements CanonicalUriProvider {
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
constructor(private gitApi: API) {
|
||||
this.disposables.push(...SUPPORTED_SCHEMES.map((scheme) => workspace.registerCanonicalUriProvider(scheme, this)));
|
||||
}
|
||||
|
||||
dispose() { this.disposables.forEach((disposable) => disposable.dispose()); }
|
||||
|
||||
provideCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, _token: CancellationToken): ProviderResult<Uri> {
|
||||
if (options.targetScheme !== 'https') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (uri.scheme) {
|
||||
case 'file': {
|
||||
const repository = this.gitApi.getRepository(uri);
|
||||
const remote = repository?.state.remotes.find((remote) => remote.name === repository.state.HEAD?.remote)?.pushUrl?.replace(/^(git@[^\/:]+)(:)/i, 'ssh://$1/');
|
||||
if (remote) {
|
||||
return toHttpsGitHubRemote(uri);
|
||||
}
|
||||
}
|
||||
default:
|
||||
return toHttpsGitHubRemote(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toHttpsGitHubRemote(uri: Uri) {
|
||||
if (uri.scheme === 'ssh' && uri.authority === 'git@github.com') {
|
||||
// if this is a git@github.com URI, return the HTTPS equivalent
|
||||
const [owner, repo] = (uri.path.endsWith('.git') ? uri.path.slice(0, -4) : uri.path).split('/').filter((segment) => segment.length > 0);
|
||||
return Uri.parse(`https://github.com/${owner}/${repo}`);
|
||||
}
|
||||
if (uri.scheme === 'https' && uri.authority === 'github.com') {
|
||||
return uri;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { API as GitAPI, RefType, Repository } from './typings/git';
|
||||
import { publishRepository } from './publish';
|
||||
import { DisposableStore, getRepositoryFromUrl } from './util';
|
||||
import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links';
|
||||
|
||||
async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) {
|
||||
try {
|
||||
const permalink = await getLink(gitAPI, useSelection, true, getVscodeDevHost(), 'headlink', context, includeRange);
|
||||
if (permalink) {
|
||||
return vscode.env.clipboard.writeText(permalink);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!(err instanceof vscode.CancellationError)) {
|
||||
vscode.window.showErrorMessage(err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openVscodeDevLink(gitAPI: GitAPI): Promise<vscode.Uri | undefined> {
|
||||
try {
|
||||
const headlink = await getLink(gitAPI, true, false, getVscodeDevHost(), 'headlink');
|
||||
return headlink ? vscode.Uri.parse(headlink) : undefined;
|
||||
} catch (err) {
|
||||
if (!(err instanceof vscode.CancellationError)) {
|
||||
vscode.window.showErrorMessage(err.message);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function openOnGitHub(repository: Repository, commit: string): Promise<void> {
|
||||
// Get the unique remotes that contain the commit
|
||||
const branches = await repository.getBranches({ contains: commit, remote: true });
|
||||
const remoteNames = new Set(branches.filter(b => b.type === RefType.RemoteHead && b.remote).map(b => b.remote!));
|
||||
|
||||
// GitHub remotes that contain the commit
|
||||
const remotes = repository.state.remotes
|
||||
.filter(r => remoteNames.has(r.name) && r.fetchUrl && getRepositoryFromUrl(r.fetchUrl));
|
||||
|
||||
if (remotes.length === 0) {
|
||||
vscode.window.showInformationMessage(vscode.l10n.t('No GitHub remotes found that contain this commit.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// upstream -> origin -> first
|
||||
const remote = remotes.find(r => r.name === 'upstream')
|
||||
?? remotes.find(r => r.name === 'origin')
|
||||
?? remotes[0];
|
||||
|
||||
const link = getCommitLink(remote.fetchUrl!, commit);
|
||||
vscode.env.openExternal(vscode.Uri.parse(link));
|
||||
}
|
||||
|
||||
export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.publish', async () => {
|
||||
try {
|
||||
publishRepository(gitAPI);
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(err.message);
|
||||
}
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLink', async (context: LinkContext) => {
|
||||
return copyVscodeDevLink(gitAPI, true, context);
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLinkFile', async (context: LinkContext) => {
|
||||
return copyVscodeDevLink(gitAPI, false, context);
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLinkWithoutRange', async (context: LinkContext) => {
|
||||
return copyVscodeDevLink(gitAPI, true, context, false);
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.openOnGitHub', async (url: string, historyItemId: string) => {
|
||||
const link = getCommitLink(url, historyItemId);
|
||||
vscode.env.openExternal(vscode.Uri.parse(link));
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.graph.openOnGitHub', async (repository: vscode.SourceControl, historyItem: vscode.SourceControlHistoryItem) => {
|
||||
if (!repository || !historyItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiRepository = gitAPI.repositories.find(r => r.rootUri.fsPath === repository.rootUri?.fsPath);
|
||||
if (!apiRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
await openOnGitHub(apiRepository, historyItem.id);
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.timeline.openOnGitHub', async (item: vscode.TimelineItem, uri: vscode.Uri) => {
|
||||
if (!item.id || !uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiRepository = gitAPI.getRepository(uri);
|
||||
if (!apiRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
await openOnGitHub(apiRepository, item.id);
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.openOnVscodeDev', async () => {
|
||||
return openVscodeDevLink(gitAPI);
|
||||
}));
|
||||
|
||||
return disposables;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CredentialsProvider, Credentials, API as GitAPI } from './typings/git';
|
||||
import { workspace, Uri, Disposable } from 'vscode';
|
||||
import { getSession } from './auth';
|
||||
|
||||
const EmptyDisposable: Disposable = { dispose() { } };
|
||||
|
||||
class GitHubCredentialProvider implements CredentialsProvider {
|
||||
|
||||
async getCredentials(host: Uri): Promise<Credentials | undefined> {
|
||||
if (!/github\.com/i.test(host.authority)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
return { username: session.account.id, password: session.accessToken };
|
||||
}
|
||||
}
|
||||
|
||||
export class GithubCredentialProviderManager {
|
||||
|
||||
private providerDisposable: Disposable = EmptyDisposable;
|
||||
private readonly disposable: Disposable;
|
||||
|
||||
private _enabled = false;
|
||||
private set enabled(enabled: boolean) {
|
||||
if (this._enabled === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._enabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
this.providerDisposable = this.gitAPI.registerCredentialsProvider(new GitHubCredentialProvider());
|
||||
} else {
|
||||
this.providerDisposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private gitAPI: GitAPI) {
|
||||
this.disposable = workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('github')) {
|
||||
this.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private refresh(): void {
|
||||
const config = workspace.getConfiguration('github', null);
|
||||
const enabled = config.get<boolean>('gitAuthentication', true);
|
||||
this.enabled = !!enabled;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.enabled = false;
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { commands, Disposable, ExtensionContext, extensions, l10n, LogLevel, LogOutputChannel, window } from 'vscode';
|
||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||
import { GithubRemoteSourceProvider } from './remoteSourceProvider';
|
||||
import { API, GitExtension } from './typings/git';
|
||||
import { registerCommands } from './commands';
|
||||
import { GithubCredentialProviderManager } from './credentialProvider';
|
||||
import { DisposableStore, repositoryHasGitHubRemote } from './util';
|
||||
import { GithubPushErrorHandler } from './pushErrorHandler';
|
||||
import { GitBaseExtension } from './typings/git-base';
|
||||
import { GithubRemoteSourcePublisher } from './remoteSourcePublisher';
|
||||
import { GitHubBranchProtectionProviderManager } from './branchProtection';
|
||||
import { GitHubCanonicalUriProvider } from './canonicalUriProvider';
|
||||
import { VscodeDevShareProvider } from './shareProviders';
|
||||
import { GitHubSourceControlHistoryItemDetailsProvider } from './historyItemDetailsProvider';
|
||||
|
||||
export function activate(context: ExtensionContext): void {
|
||||
const disposables: Disposable[] = [];
|
||||
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
|
||||
|
||||
const logger = window.createOutputChannel('GitHub', { log: true });
|
||||
disposables.push(logger);
|
||||
|
||||
const onDidChangeLogLevel = (logLevel: LogLevel) => {
|
||||
logger.appendLine(l10n.t('Log level: {0}', LogLevel[logLevel]));
|
||||
};
|
||||
disposables.push(logger.onDidChangeLogLevel(onDidChangeLogLevel));
|
||||
onDidChangeLogLevel(logger.logLevel);
|
||||
|
||||
const { aiKey } = require('../package.json') as { aiKey: string };
|
||||
const telemetryReporter = new TelemetryReporter(aiKey);
|
||||
disposables.push(telemetryReporter);
|
||||
|
||||
disposables.push(initializeGitBaseExtension());
|
||||
disposables.push(initializeGitExtension(context, telemetryReporter, logger));
|
||||
}
|
||||
|
||||
function initializeGitBaseExtension(): Disposable {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
const initialize = () => {
|
||||
try {
|
||||
const gitBaseAPI = gitBaseExtension.getAPI(1);
|
||||
|
||||
disposables.add(gitBaseAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider()));
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Could not initialize GitHub extension');
|
||||
console.warn(err);
|
||||
}
|
||||
};
|
||||
|
||||
const onDidChangeGitBaseExtensionEnablement = (enabled: boolean) => {
|
||||
if (!enabled) {
|
||||
disposables.dispose();
|
||||
} else {
|
||||
initialize();
|
||||
}
|
||||
};
|
||||
|
||||
const gitBaseExtension = extensions.getExtension<GitBaseExtension>('vscode.git-base')!.exports;
|
||||
disposables.add(gitBaseExtension.onDidChangeEnablement(onDidChangeGitBaseExtensionEnablement));
|
||||
onDidChangeGitBaseExtensionEnablement(gitBaseExtension.enabled);
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
function setGitHubContext(gitAPI: API, disposables: DisposableStore) {
|
||||
if (gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
|
||||
commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
|
||||
} else {
|
||||
const openRepoDisposable = gitAPI.onDidOpenRepository(async e => {
|
||||
await e.status();
|
||||
if (repositoryHasGitHubRemote(e)) {
|
||||
commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
|
||||
openRepoDisposable.dispose();
|
||||
}
|
||||
});
|
||||
disposables.add(openRepoDisposable);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeGitExtension(context: ExtensionContext, telemetryReporter: TelemetryReporter, logger: LogOutputChannel): Disposable {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
let gitExtension = extensions.getExtension<GitExtension>('vscode.git');
|
||||
|
||||
const initialize = () => {
|
||||
gitExtension!.activate()
|
||||
.then(extension => {
|
||||
const onDidChangeGitExtensionEnablement = (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
const gitAPI = extension.getAPI(1);
|
||||
|
||||
disposables.add(registerCommands(gitAPI));
|
||||
disposables.add(new GithubCredentialProviderManager(gitAPI));
|
||||
disposables.add(new GitHubBranchProtectionProviderManager(gitAPI, context.globalState, logger, telemetryReporter));
|
||||
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler(telemetryReporter)));
|
||||
disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI)));
|
||||
disposables.add(gitAPI.registerSourceControlHistoryItemDetailsProvider(new GitHubSourceControlHistoryItemDetailsProvider(gitAPI, logger)));
|
||||
disposables.add(new GitHubCanonicalUriProvider(gitAPI));
|
||||
disposables.add(new VscodeDevShareProvider(gitAPI));
|
||||
setGitHubContext(gitAPI, disposables);
|
||||
|
||||
commands.executeCommand('setContext', 'git-base.gitEnabled', true);
|
||||
} else {
|
||||
disposables.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
disposables.add(extension.onDidChangeEnablement(onDidChangeGitExtensionEnablement));
|
||||
onDidChangeGitExtensionEnablement(extension.enabled);
|
||||
});
|
||||
};
|
||||
|
||||
if (gitExtension) {
|
||||
initialize();
|
||||
} else {
|
||||
const listener = extensions.onDidChange(() => {
|
||||
if (!gitExtension && extensions.getExtension<GitExtension>('vscode.git')) {
|
||||
gitExtension = extensions.getExtension<GitExtension>('vscode.git');
|
||||
initialize();
|
||||
listener.dispose();
|
||||
}
|
||||
});
|
||||
disposables.add(listener);
|
||||
}
|
||||
|
||||
return disposables;
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { authentication, Command, l10n, LogOutputChannel, workspace } from 'vscode';
|
||||
import { Commit, Repository as GitHubRepository, Maybe } from '@octokit/graphql-schema';
|
||||
import { API, AvatarQuery, AvatarQueryCommit, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git';
|
||||
import { DisposableStore, getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl, getRepositoryFromUrl, groupBy, sequentialize } from './util';
|
||||
import { AuthenticationError, getOctokitGraphql } from './auth';
|
||||
import { getAvatarLink } from './links';
|
||||
|
||||
const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/g;
|
||||
|
||||
const ASSIGNABLE_USERS_QUERY = `
|
||||
query assignableUsers($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
assignableUsers(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
login
|
||||
name
|
||||
email
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const COMMIT_AUTHOR_QUERY = `
|
||||
query commitAuthor($owner: String!, $repo: String!, $commit: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
object(expression: $commit) {
|
||||
... on Commit {
|
||||
author {
|
||||
name
|
||||
email
|
||||
avatarUrl
|
||||
user {
|
||||
id
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface GitHubRepositoryStore {
|
||||
readonly users: GitHubUser[];
|
||||
readonly commits: Set<string>;
|
||||
}
|
||||
|
||||
interface GitHubUser {
|
||||
readonly id: string;
|
||||
readonly login: string;
|
||||
readonly name?: Maybe<string>;
|
||||
readonly email: string;
|
||||
readonly avatarUrl: string;
|
||||
}
|
||||
|
||||
function getUserIdFromNoReplyEmail(email: string | undefined): string | undefined {
|
||||
const match = email?.match(/^([0-9]+)\+[^@]+@users\.noreply\.github\.com$/);
|
||||
return match?.[1];
|
||||
}
|
||||
|
||||
function compareAvatarQuery(a: AvatarQueryCommit, b: AvatarQueryCommit): number {
|
||||
// Email
|
||||
const emailComparison = (a.authorEmail ?? '').localeCompare(b.authorEmail ?? '');
|
||||
if (emailComparison !== 0) {
|
||||
return emailComparison;
|
||||
}
|
||||
|
||||
// Name
|
||||
return (a.authorName ?? '').localeCompare(b.authorName ?? '');
|
||||
}
|
||||
|
||||
export class GitHubSourceControlHistoryItemDetailsProvider implements SourceControlHistoryItemDetailsProvider {
|
||||
private _isUserAuthenticated = true;
|
||||
private readonly _store = new Map<string, GitHubRepositoryStore>();
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
constructor(private readonly _gitAPI: API, private readonly _logger: LogOutputChannel) {
|
||||
this._disposables.add(this._gitAPI.onDidCloseRepository(repository => this._onDidCloseRepository(repository)));
|
||||
|
||||
this._disposables.add(authentication.onDidChangeSessions(e => {
|
||||
if (e.provider.id === 'github') {
|
||||
this._isUserAuthenticated = true;
|
||||
}
|
||||
}));
|
||||
|
||||
this._disposables.add(workspace.onDidChangeConfiguration(e => {
|
||||
if (!e.affectsConfiguration('github.showAvatar')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._store.clear();
|
||||
}));
|
||||
}
|
||||
|
||||
async provideAvatar(repository: Repository, query: AvatarQuery): Promise<Map<string, string | undefined> | undefined> {
|
||||
this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution for ${query.commits.length} commit(s) in ${repository.rootUri.fsPath}.`);
|
||||
|
||||
const config = workspace.getConfiguration('github', repository.rootUri);
|
||||
const showAvatar = config.get<boolean>('showAvatar', true) === true;
|
||||
|
||||
if (!this._isUserAuthenticated || !showAvatar) {
|
||||
this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution is disabled. (${showAvatar === false ? 'setting' : 'auth'})`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const descriptor = getRepositoryDefaultRemote(repository);
|
||||
if (!descriptor) {
|
||||
this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Repository does not have a GitHub remote.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const logs = { cached: 0, email: 0, github: 0, incomplete: 0 };
|
||||
|
||||
// Warm up the in-memory cache with the first page
|
||||
// (100 users) from this list of assignable users
|
||||
await this._loadAssignableUsers(descriptor);
|
||||
|
||||
const repositoryStore = this._store.get(this._getRepositoryKey(descriptor));
|
||||
if (!repositoryStore) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Group the query by author
|
||||
const authorQuery = groupBy<AvatarQueryCommit>(query.commits, compareAvatarQuery);
|
||||
|
||||
const results = new Map<string, string | undefined>();
|
||||
await Promise.all(authorQuery.map(async commits => {
|
||||
if (commits.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Query the in-memory cache for the user
|
||||
const avatarUrl = repositoryStore.users.find(
|
||||
user => user.email === commits[0].authorEmail || user.name === commits[0].authorName)?.avatarUrl;
|
||||
|
||||
// Cache hit
|
||||
if (avatarUrl) {
|
||||
// Add avatar for each commit
|
||||
logs.cached += commits.length;
|
||||
commits.forEach(({ hash }) => results.set(hash, `${avatarUrl}&s=${query.size}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if any of the commit are being tracked in the list
|
||||
// of known commits that have incomplte author information
|
||||
if (commits.some(({ hash }) => repositoryStore.commits.has(hash))) {
|
||||
commits.forEach(({ hash }) => results.set(hash, undefined));
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to extract the user identifier from GitHub no-reply emails
|
||||
const userIdFromEmail = getUserIdFromNoReplyEmail(commits[0].authorEmail);
|
||||
if (userIdFromEmail) {
|
||||
logs.email += commits.length;
|
||||
const avatarUrl = getAvatarLink(userIdFromEmail, query.size);
|
||||
commits.forEach(({ hash }) => results.set(hash, avatarUrl));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the commit details
|
||||
const commitAuthor = await this._getCommitAuthor(descriptor, commits[0].hash);
|
||||
if (!commitAuthor) {
|
||||
// The commit has incomplete author information, so
|
||||
// we should not try to query the authors details again
|
||||
logs.incomplete += commits.length;
|
||||
for (const { hash } of commits) {
|
||||
repositoryStore.commits.add(hash);
|
||||
results.set(hash, undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the user to the cache
|
||||
repositoryStore.users.push(commitAuthor);
|
||||
|
||||
// Add avatar for each commit
|
||||
logs.github += commits.length;
|
||||
commits.forEach(({ hash }) => results.set(hash, `${commitAuthor.avatarUrl}&s=${query.size}`));
|
||||
}));
|
||||
|
||||
this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution for ${query.commits.length} commit(s) in ${repository.rootUri.fsPath} complete: ${JSON.stringify(logs)}.`);
|
||||
|
||||
return results;
|
||||
} catch (err) {
|
||||
// A GitHub authentication session could be missing if the user has not yet
|
||||
// signed in with their GitHub account or they have signed out. Disable the
|
||||
// avatar resolution until the user signes in with their GitHub account.
|
||||
if (err instanceof AuthenticationError) {
|
||||
this._isUserAuthenticated = false;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async provideHoverCommands(repository: Repository): Promise<Command[] | undefined> {
|
||||
const url = getRepositoryDefaultRemoteUrl(repository);
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [{
|
||||
title: l10n.t('{0} Open on GitHub', '$(github)'),
|
||||
tooltip: l10n.t('Open on GitHub'),
|
||||
command: 'github.openOnGitHub',
|
||||
arguments: [url]
|
||||
}];
|
||||
}
|
||||
|
||||
async provideMessageLinks(repository: Repository, message: string): Promise<string | undefined> {
|
||||
const descriptor = getRepositoryDefaultRemote(repository);
|
||||
if (!descriptor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return message.replace(
|
||||
ISSUE_EXPRESSION,
|
||||
(match, _group1, owner: string | undefined, repo: string | undefined, _group2, number: string | undefined) => {
|
||||
if (!number || Number.isNaN(parseInt(number))) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const label = owner && repo
|
||||
? `${owner}/${repo}#${number}`
|
||||
: `#${number}`;
|
||||
|
||||
owner = owner ?? descriptor.owner;
|
||||
repo = repo ?? descriptor.repo;
|
||||
|
||||
return `[${label}](https://github.com/${owner}/${repo}/issues/${number})`;
|
||||
});
|
||||
}
|
||||
|
||||
private _onDidCloseRepository(repository: Repository) {
|
||||
for (const remote of repository.state.remotes) {
|
||||
if (!remote.fetchUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const repository = getRepositoryFromUrl(remote.fetchUrl);
|
||||
if (!repository) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._store.delete(this._getRepositoryKey(repository));
|
||||
}
|
||||
}
|
||||
|
||||
@sequentialize
|
||||
private async _loadAssignableUsers(descriptor: { owner: string; repo: string }): Promise<void> {
|
||||
if (this._store.has(this._getRepositoryKey(descriptor))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][_loadAssignableUsers] Querying assignable user(s) for ${descriptor.owner}/${descriptor.repo}.`);
|
||||
|
||||
try {
|
||||
const graphql = await getOctokitGraphql();
|
||||
const { repository } = await graphql<{ repository: GitHubRepository }>(ASSIGNABLE_USERS_QUERY, descriptor);
|
||||
|
||||
const users: GitHubUser[] = [];
|
||||
for (const node of repository.assignableUsers.nodes ?? []) {
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
users.push({
|
||||
id: node.id,
|
||||
login: node.login,
|
||||
name: node.name,
|
||||
email: node.email,
|
||||
avatarUrl: node.avatarUrl,
|
||||
} satisfies GitHubUser);
|
||||
}
|
||||
|
||||
this._store.set(this._getRepositoryKey(descriptor), { users, commits: new Set() });
|
||||
this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][_loadAssignableUsers] Successfully queried assignable user(s) for ${descriptor.owner}/${descriptor.repo}: ${users.length} user(s).`);
|
||||
} catch (err) {
|
||||
this._logger.warn(`[GitHubSourceControlHistoryItemDetailsProvider][_loadAssignableUsers] Failed to load assignable user(s) for ${descriptor.owner}/${descriptor.repo}: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async _getCommitAuthor(descriptor: { owner: string; repo: string }, commit: string): Promise<GitHubUser | undefined> {
|
||||
this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][_getCommitAuthor] Querying commit author for ${descriptor.owner}/${descriptor.repo}/${commit}.`);
|
||||
|
||||
try {
|
||||
const graphql = await getOctokitGraphql();
|
||||
const { repository } = await graphql<{ repository: GitHubRepository }>(COMMIT_AUTHOR_QUERY, { ...descriptor, commit });
|
||||
|
||||
const commitAuthor = (repository.object as Commit).author;
|
||||
if (!commitAuthor?.user?.id || !commitAuthor.user?.login ||
|
||||
!commitAuthor?.name || !commitAuthor?.email || !commitAuthor?.avatarUrl) {
|
||||
this._logger.info(`[GitHubSourceControlHistoryItemDetailsProvider][_getCommitAuthor] Incomplete commit author for ${descriptor.owner}/${descriptor.repo}/${commit}: ${JSON.stringify(repository.object)}`);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const user = {
|
||||
id: commitAuthor.user.id,
|
||||
login: commitAuthor.user.login,
|
||||
name: commitAuthor.name,
|
||||
email: commitAuthor.email,
|
||||
avatarUrl: commitAuthor.avatarUrl,
|
||||
} satisfies GitHubUser;
|
||||
|
||||
this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][_getCommitAuthor] Successfully queried commit author for ${descriptor.owner}/${descriptor.repo}/${commit}: ${user.login}.`);
|
||||
return user;
|
||||
} catch (err) {
|
||||
this._logger.warn(`[GitHubSourceControlHistoryItemDetailsProvider][_getCommitAuthor] Failed to get commit author for ${descriptor.owner}/${descriptor.repo}/${commit}: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private _getRepositoryKey(descriptor: { owner: string; repo: string }): string {
|
||||
return `${descriptor.owner}/${descriptor.repo}`;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { API as GitAPI, RefType, Repository } from './typings/git';
|
||||
import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util';
|
||||
|
||||
export function isFileInRepo(repository: Repository, file: vscode.Uri): boolean {
|
||||
return file.path.toLowerCase() === repository.rootUri.path.toLowerCase() ||
|
||||
(file.path.toLowerCase().startsWith(repository.rootUri.path.toLowerCase()) &&
|
||||
file.path.substring(repository.rootUri.path.length).startsWith('/'));
|
||||
}
|
||||
|
||||
export function getRepositoryForFile(gitAPI: GitAPI, file: vscode.Uri): Repository | undefined {
|
||||
for (const repository of gitAPI.repositories) {
|
||||
if (isFileInRepo(repository, file)) {
|
||||
return repository;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
enum LinkType {
|
||||
File = 1,
|
||||
Notebook = 2
|
||||
}
|
||||
|
||||
interface IFilePosition {
|
||||
type: LinkType.File;
|
||||
uri: vscode.Uri;
|
||||
range: vscode.Range | undefined;
|
||||
}
|
||||
|
||||
interface INotebookPosition {
|
||||
type: LinkType.Notebook;
|
||||
uri: vscode.Uri;
|
||||
cellIndex: number;
|
||||
range: vscode.Range | undefined;
|
||||
}
|
||||
|
||||
interface EditorLineNumberContext {
|
||||
uri: vscode.Uri;
|
||||
lineNumber: number;
|
||||
}
|
||||
export type LinkContext = vscode.Uri | EditorLineNumberContext | undefined;
|
||||
|
||||
function extractContext(context: LinkContext): { fileUri: vscode.Uri | undefined; lineNumber: number | undefined } {
|
||||
if (context instanceof vscode.Uri) {
|
||||
return { fileUri: context, lineNumber: undefined };
|
||||
} else if (context !== undefined && 'lineNumber' in context && 'uri' in context) {
|
||||
return { fileUri: context.uri, lineNumber: context.lineNumber };
|
||||
} else {
|
||||
return { fileUri: undefined, lineNumber: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
function getFileAndPosition(context: LinkContext): IFilePosition | INotebookPosition | undefined {
|
||||
let range: vscode.Range | undefined;
|
||||
|
||||
const { fileUri, lineNumber } = extractContext(context);
|
||||
const uri = fileUri ?? vscode.window.activeTextEditor?.document.uri;
|
||||
|
||||
if (uri) {
|
||||
if (uri.scheme === 'vscode-notebook-cell' && vscode.window.activeNotebookEditor?.notebook.uri.fsPath === uri.fsPath) {
|
||||
// if the active editor is a notebook editor and the focus is inside any a cell text editor
|
||||
// generate deep link for text selection for the notebook cell.
|
||||
const cell = vscode.window.activeNotebookEditor.notebook.getCells().find(cell => cell.document.uri.fragment === uri?.fragment);
|
||||
const cellIndex = cell?.index ?? vscode.window.activeNotebookEditor.selection.start;
|
||||
|
||||
const range = getRangeOrSelection(lineNumber);
|
||||
return { type: LinkType.Notebook, uri, cellIndex, range };
|
||||
} else {
|
||||
// the active editor is a text editor
|
||||
range = getRangeOrSelection(lineNumber);
|
||||
return { type: LinkType.File, uri, range };
|
||||
}
|
||||
}
|
||||
|
||||
if (vscode.window.activeNotebookEditor) {
|
||||
// if the active editor is a notebook editor but the focus is not inside any cell text editor, generate deep link for the cell selection in the notebook document.
|
||||
return { type: LinkType.Notebook, uri: vscode.window.activeNotebookEditor.notebook.uri, cellIndex: vscode.window.activeNotebookEditor.selection.start, range: undefined };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getRangeOrSelection(lineNumber: number | undefined) {
|
||||
return lineNumber !== undefined && (!vscode.window.activeTextEditor || vscode.window.activeTextEditor.selection.isEmpty || !vscode.window.activeTextEditor.selection.contains(new vscode.Position(lineNumber - 1, 0)))
|
||||
? new vscode.Range(lineNumber - 1, 0, lineNumber - 1, 1)
|
||||
: vscode.window.activeTextEditor?.selection;
|
||||
}
|
||||
|
||||
export function rangeString(range: vscode.Range | undefined) {
|
||||
if (!range) {
|
||||
return '';
|
||||
}
|
||||
let hash = `#L${range.start.line + 1}`;
|
||||
if (range.start.line !== range.end.line) {
|
||||
hash += `-L${range.end.line + 1}`;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function notebookCellRangeString(index: number | undefined, range: vscode.Range | undefined) {
|
||||
if (index === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!range) {
|
||||
return `#C${index + 1}`;
|
||||
}
|
||||
|
||||
let hash = `#C${index + 1}:L${range.start.line + 1}`;
|
||||
if (range.start.line !== range.end.line) {
|
||||
hash += `-L${range.end.line + 1}`;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function encodeURIComponentExceptSlashes(path: string) {
|
||||
// There may be special characters like # and whitespace in the path.
|
||||
// These characters are not escaped by encodeURI(), so it is not sufficient to
|
||||
// feed the full URI to encodeURI().
|
||||
// Additonally, if we feed the full path into encodeURIComponent(),
|
||||
// this will also encode the path separators, leading to an invalid path.
|
||||
// Therefore, split on the path separator and encode each segment individually.
|
||||
return path.split('/').map((segment) => encodeURIComponent(segment)).join('/');
|
||||
}
|
||||
|
||||
export async function getLink(gitAPI: GitAPI, useSelection: boolean, shouldEnsurePublished: boolean, hostPrefix?: string, linkType: 'permalink' | 'headlink' = 'permalink', context?: LinkContext, useRange?: boolean): Promise<string | undefined> {
|
||||
hostPrefix = hostPrefix ?? 'https://github.com';
|
||||
const fileAndPosition = getFileAndPosition(context);
|
||||
const fileUri = fileAndPosition?.uri;
|
||||
|
||||
// Use the first repo if we cannot determine a repo from the uri.
|
||||
const githubRepository = gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo));
|
||||
const gitRepo = (fileUri ? getRepositoryForFile(gitAPI, fileUri) : githubRepository) ?? githubRepository;
|
||||
if (!gitRepo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldEnsurePublished && fileUri) {
|
||||
await ensurePublished(gitRepo, fileUri);
|
||||
}
|
||||
|
||||
let repo: { owner: string; repo: string } | undefined;
|
||||
gitRepo.state.remotes.find(remote => {
|
||||
if (remote.fetchUrl) {
|
||||
const foundRepo = getRepositoryFromUrl(remote.fetchUrl);
|
||||
if (foundRepo && (remote.name === gitRepo.state.HEAD?.upstream?.remote)) {
|
||||
repo = foundRepo;
|
||||
return;
|
||||
} else if (foundRepo && !repo) {
|
||||
repo = foundRepo;
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
if (!repo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blobSegment = gitRepo.state.HEAD ? (`/blob/${linkType === 'headlink' && gitRepo.state.HEAD.name ? encodeURIComponentExceptSlashes(gitRepo.state.HEAD.name) : gitRepo.state.HEAD?.commit}`) : '';
|
||||
const uriWithoutFileSegments = `${hostPrefix}/${repo.owner}/${repo.repo}${blobSegment}`;
|
||||
if (!fileUri) {
|
||||
return uriWithoutFileSegments;
|
||||
}
|
||||
|
||||
const encodedFilePath = encodeURIComponentExceptSlashes(fileUri.path.substring(gitRepo.rootUri.path.length));
|
||||
const fileSegments = fileAndPosition.type === LinkType.File
|
||||
? (useSelection ? `${encodedFilePath}${useRange ? rangeString(fileAndPosition.range) : ''}` : '')
|
||||
: (useSelection ? `${encodedFilePath}${useRange ? notebookCellRangeString(fileAndPosition.cellIndex, fileAndPosition.range) : ''}` : '');
|
||||
|
||||
return `${uriWithoutFileSegments}${fileSegments}`;
|
||||
}
|
||||
|
||||
export function getAvatarLink(userId: string, size: number): string {
|
||||
return `https://avatars.githubusercontent.com/u/${userId}?s=${size}`;
|
||||
}
|
||||
|
||||
export function getBranchLink(url: string, branch: string, hostPrefix: string = 'https://github.com') {
|
||||
const repo = getRepositoryFromUrl(url);
|
||||
if (!repo) {
|
||||
throw new Error('Invalid repository URL provided');
|
||||
}
|
||||
|
||||
branch = encodeURIComponentExceptSlashes(branch);
|
||||
return `${hostPrefix}/${repo.owner}/${repo.repo}/tree/${branch}`;
|
||||
}
|
||||
|
||||
export function getCommitLink(url: string, hash: string, hostPrefix: string = 'https://github.com') {
|
||||
const repo = getRepositoryFromUrl(url);
|
||||
if (!repo) {
|
||||
throw new Error('Invalid repository URL provided');
|
||||
}
|
||||
|
||||
return `${hostPrefix}/${repo.owner}/${repo.repo}/commit/${hash}`;
|
||||
}
|
||||
|
||||
export function getVscodeDevHost(): string {
|
||||
return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`;
|
||||
}
|
||||
|
||||
export async function ensurePublished(repository: Repository, file: vscode.Uri) {
|
||||
await repository.status();
|
||||
|
||||
if ((repository.state.HEAD?.type === RefType.Head || repository.state.HEAD?.type === RefType.Tag)
|
||||
// If HEAD is not published, make sure it is
|
||||
&& !repository?.state.HEAD?.upstream
|
||||
) {
|
||||
const publishBranch = vscode.l10n.t('Publish Branch & Copy Link');
|
||||
const selection = await vscode.window.showInformationMessage(
|
||||
vscode.l10n.t('The current branch is not published to the remote. Would you like to publish your branch before copying a link?'),
|
||||
{ modal: true },
|
||||
publishBranch
|
||||
);
|
||||
if (selection !== publishBranch) {
|
||||
throw new vscode.CancellationError();
|
||||
}
|
||||
|
||||
await vscode.commands.executeCommand('git.publish');
|
||||
}
|
||||
|
||||
const uncommittedChanges = [...repository.state.workingTreeChanges, ...repository.state.indexChanges];
|
||||
if (uncommittedChanges.find((c) => c.uri.toString() === file.toString()) && !repository.state.HEAD?.ahead && !repository.state.HEAD?.behind) {
|
||||
const commitChanges = vscode.l10n.t('Commit Changes');
|
||||
const copyAnyway = vscode.l10n.t('Copy Anyway');
|
||||
const selection = await vscode.window.showWarningMessage(
|
||||
vscode.l10n.t('The current file has uncommitted changes. Please commit your changes before copying a link.'),
|
||||
{ modal: true },
|
||||
commitChanges,
|
||||
copyAnyway
|
||||
);
|
||||
|
||||
if (selection !== copyAnyway) {
|
||||
// Focus the SCM view
|
||||
vscode.commands.executeCommand('workbench.view.scm');
|
||||
throw new vscode.CancellationError();
|
||||
}
|
||||
} else if (repository.state.HEAD?.ahead) {
|
||||
const pushCommits = vscode.l10n.t('Push Commits & Copy Link');
|
||||
const selection = await vscode.window.showInformationMessage(
|
||||
vscode.l10n.t('The current branch has unpublished commits. Would you like to push your commits before copying a link?'),
|
||||
{ modal: true },
|
||||
pushCommits
|
||||
);
|
||||
if (selection !== pushCommits) {
|
||||
throw new vscode.CancellationError();
|
||||
}
|
||||
|
||||
await repository.push();
|
||||
} else if (repository.state.HEAD?.behind) {
|
||||
const pull = vscode.l10n.t('Pull Changes & Copy Link');
|
||||
const selection = await vscode.window.showInformationMessage(
|
||||
vscode.l10n.t('The current branch is not up to date. Would you like to pull before copying a link?'),
|
||||
{ modal: true },
|
||||
pull
|
||||
);
|
||||
if (selection !== pull) {
|
||||
throw new vscode.CancellationError();
|
||||
}
|
||||
|
||||
await repository.pull();
|
||||
}
|
||||
|
||||
await repository.status();
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { API as GitAPI, Repository } from './typings/git';
|
||||
import { getOctokit } from './auth';
|
||||
import { TextEncoder } from 'util';
|
||||
import { basename } from 'path';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { isInCodespaces } from './pushErrorHandler';
|
||||
|
||||
function sanitizeRepositoryName(value: string): string {
|
||||
return value.trim().replace(/[^a-z0-9_.]/ig, '-');
|
||||
}
|
||||
|
||||
function getPick<T extends vscode.QuickPickItem>(quickpick: vscode.QuickPick<T>): Promise<T | undefined> {
|
||||
return Promise.race<T | undefined>([
|
||||
new Promise<T>(c => quickpick.onDidAccept(() => quickpick.selectedItems.length > 0 && c(quickpick.selectedItems[0]))),
|
||||
new Promise<undefined>(c => quickpick.onDidHide(() => c(undefined)))
|
||||
]);
|
||||
}
|
||||
|
||||
export async function publishRepository(gitAPI: GitAPI, repository?: Repository): Promise<void> {
|
||||
if (!vscode.workspace.workspaceFolders?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let folder: vscode.Uri;
|
||||
|
||||
if (repository) {
|
||||
folder = repository.rootUri;
|
||||
} else if (gitAPI.repositories.length === 1) {
|
||||
repository = gitAPI.repositories[0];
|
||||
folder = repository.rootUri;
|
||||
} else if (vscode.workspace.workspaceFolders.length === 1) {
|
||||
folder = vscode.workspace.workspaceFolders[0].uri;
|
||||
} else {
|
||||
const picks = vscode.workspace.workspaceFolders.map(folder => ({ label: folder.name, folder }));
|
||||
const placeHolder = vscode.l10n.t('Pick a folder to publish to GitHub');
|
||||
const pick = await vscode.window.showQuickPick(picks, { placeHolder });
|
||||
|
||||
if (!pick) {
|
||||
return;
|
||||
}
|
||||
|
||||
folder = pick.folder.uri;
|
||||
}
|
||||
|
||||
let quickpick = vscode.window.createQuickPick<vscode.QuickPickItem & { repo?: string; auth?: 'https' | 'ssh'; isPrivate?: boolean }>();
|
||||
quickpick.ignoreFocusOut = true;
|
||||
|
||||
quickpick.placeholder = 'Repository Name';
|
||||
quickpick.value = basename(folder.fsPath);
|
||||
quickpick.show();
|
||||
quickpick.busy = true;
|
||||
|
||||
let owner: string;
|
||||
let octokit: Octokit;
|
||||
try {
|
||||
octokit = await getOctokit();
|
||||
const user = await octokit.users.getAuthenticated({});
|
||||
owner = user.data.login;
|
||||
} catch (e) {
|
||||
// User has cancelled sign in
|
||||
quickpick.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
quickpick.busy = false;
|
||||
|
||||
let repo: string | undefined;
|
||||
let isPrivate: boolean;
|
||||
|
||||
const onDidChangeValue = async () => {
|
||||
const sanitizedRepo = sanitizeRepositoryName(quickpick.value);
|
||||
|
||||
if (!sanitizedRepo) {
|
||||
quickpick.items = [];
|
||||
} else {
|
||||
quickpick.items = [
|
||||
{ label: `$(repo) Publish to GitHub private repository`, description: `$(github) ${owner}/${sanitizedRepo}`, alwaysShow: true, repo: sanitizedRepo, isPrivate: true },
|
||||
{ label: `$(repo) Publish to GitHub public repository`, description: `$(github) ${owner}/${sanitizedRepo}`, alwaysShow: true, repo: sanitizedRepo, isPrivate: false },
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
onDidChangeValue();
|
||||
|
||||
while (true) {
|
||||
const listener = quickpick.onDidChangeValue(onDidChangeValue);
|
||||
const pick = await getPick(quickpick);
|
||||
listener.dispose();
|
||||
|
||||
repo = pick?.repo;
|
||||
isPrivate = pick?.isPrivate ?? true;
|
||||
|
||||
if (repo) {
|
||||
try {
|
||||
quickpick.busy = true;
|
||||
await octokit.repos.get({ owner, repo: repo });
|
||||
quickpick.items = [{ label: `$(error) GitHub repository already exists`, description: `$(github) ${owner}/${repo}`, alwaysShow: true }];
|
||||
} catch {
|
||||
break;
|
||||
} finally {
|
||||
quickpick.busy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quickpick.dispose();
|
||||
|
||||
if (!repo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repository) {
|
||||
const gitignore = vscode.Uri.joinPath(folder, '.gitignore');
|
||||
let shouldGenerateGitignore = false;
|
||||
|
||||
try {
|
||||
await vscode.workspace.fs.stat(gitignore);
|
||||
} catch (err) {
|
||||
shouldGenerateGitignore = true;
|
||||
}
|
||||
|
||||
if (shouldGenerateGitignore) {
|
||||
quickpick = vscode.window.createQuickPick();
|
||||
quickpick.placeholder = vscode.l10n.t('Select which files should be included in the repository.');
|
||||
quickpick.canSelectMany = true;
|
||||
quickpick.show();
|
||||
|
||||
try {
|
||||
quickpick.busy = true;
|
||||
|
||||
const children = (await vscode.workspace.fs.readDirectory(folder))
|
||||
.map(([name]) => name)
|
||||
.filter(name => name !== '.git');
|
||||
|
||||
quickpick.items = children.map(name => ({ label: name }));
|
||||
quickpick.selectedItems = quickpick.items;
|
||||
quickpick.busy = false;
|
||||
|
||||
const result = await Promise.race([
|
||||
new Promise<readonly vscode.QuickPickItem[]>(c => quickpick.onDidAccept(() => c(quickpick.selectedItems))),
|
||||
new Promise<undefined>(c => quickpick.onDidHide(() => c(undefined)))
|
||||
]);
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ignored = new Set(children);
|
||||
result.forEach(c => ignored.delete(c.label));
|
||||
|
||||
if (ignored.size > 0) {
|
||||
const raw = [...ignored].map(i => `/${i}`).join('\n');
|
||||
const encoder = new TextEncoder();
|
||||
await vscode.workspace.fs.writeFile(gitignore, encoder.encode(raw));
|
||||
}
|
||||
} finally {
|
||||
quickpick.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const githubRepository = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: false, title: 'Publish to GitHub' }, async progress => {
|
||||
progress.report({
|
||||
message: isPrivate
|
||||
? vscode.l10n.t('Publishing to a private GitHub repository')
|
||||
: vscode.l10n.t('Publishing to a public GitHub repository'),
|
||||
increment: 25
|
||||
});
|
||||
|
||||
type CreateRepositoryResponseData = Awaited<ReturnType<typeof octokit.repos.createForAuthenticatedUser>>['data'];
|
||||
let createdGithubRepository: CreateRepositoryResponseData | undefined = undefined;
|
||||
|
||||
if (isInCodespaces()) {
|
||||
createdGithubRepository = await vscode.commands.executeCommand<CreateRepositoryResponseData>('github.codespaces.publish', { name: repo!, isPrivate });
|
||||
} else {
|
||||
const res = await octokit.repos.createForAuthenticatedUser({
|
||||
name: repo!,
|
||||
private: isPrivate
|
||||
});
|
||||
createdGithubRepository = res.data;
|
||||
}
|
||||
|
||||
if (createdGithubRepository) {
|
||||
progress.report({ message: vscode.l10n.t('Creating first commit'), increment: 25 });
|
||||
|
||||
if (!repository) {
|
||||
repository = await gitAPI.init(folder, { defaultBranch: createdGithubRepository.default_branch }) || undefined;
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
await repository.commit('first commit', { all: true, postCommitCommand: null });
|
||||
}
|
||||
|
||||
progress.report({ message: vscode.l10n.t('Uploading files'), increment: 25 });
|
||||
|
||||
const branch = await repository.getBranch('HEAD');
|
||||
const protocol = vscode.workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol');
|
||||
const remoteUrl = protocol === 'https' ? createdGithubRepository.clone_url : createdGithubRepository.ssh_url;
|
||||
await repository.addRemote('origin', remoteUrl);
|
||||
await repository.push('origin', branch.name, true);
|
||||
}
|
||||
|
||||
return createdGithubRepository;
|
||||
});
|
||||
|
||||
if (!githubRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
const openOnGitHub = vscode.l10n.t('Open on GitHub');
|
||||
vscode.window.showInformationMessage(vscode.l10n.t('Successfully published the "{0}" repository to GitHub.', `${owner}/${repo}`), openOnGitHub).then(action => {
|
||||
if (action === openOnGitHub) {
|
||||
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(githubRepository.html_url));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TextDecoder } from 'util';
|
||||
import { commands, env, ProgressLocation, Uri, window, workspace, QuickPickOptions, FileType, l10n, Disposable, TextDocumentContentProvider } from 'vscode';
|
||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||
import { getOctokit } from './auth';
|
||||
import { GitErrorCodes, PushErrorHandler, Remote, Repository } from './typings/git';
|
||||
import * as path from 'path';
|
||||
|
||||
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
|
||||
|
||||
export function isInCodespaces(): boolean {
|
||||
return env.remoteName === 'codespaces';
|
||||
}
|
||||
|
||||
const PR_TEMPLATE_FILES = [
|
||||
{ dir: '.', files: ['pull_request_template.md', 'PULL_REQUEST_TEMPLATE.md'] },
|
||||
{ dir: 'docs', files: ['pull_request_template.md', 'PULL_REQUEST_TEMPLATE.md'] },
|
||||
{ dir: '.github', files: ['PULL_REQUEST_TEMPLATE.md', 'PULL_REQUEST_TEMPLATE.md'] }
|
||||
];
|
||||
|
||||
const PR_TEMPLATE_DIRECTORY_NAMES = [
|
||||
'PULL_REQUEST_TEMPLATE',
|
||||
'docs/PULL_REQUEST_TEMPLATE',
|
||||
'.github/PULL_REQUEST_TEMPLATE'
|
||||
];
|
||||
|
||||
async function assertMarkdownFiles(dir: Uri, files: string[]): Promise<Uri[]> {
|
||||
const dirFiles = await workspace.fs.readDirectory(dir);
|
||||
return dirFiles
|
||||
.filter(([name, type]) => Boolean(type & FileType.File) && files.indexOf(name) !== -1)
|
||||
.map(([name]) => Uri.joinPath(dir, name));
|
||||
}
|
||||
|
||||
async function findMarkdownFilesInDir(uri: Uri): Promise<Uri[]> {
|
||||
const files = await workspace.fs.readDirectory(uri);
|
||||
return files
|
||||
.filter(([name, type]) => Boolean(type & FileType.File) && path.extname(name) === '.md')
|
||||
.map(([name]) => Uri.joinPath(uri, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* PR templates can be:
|
||||
* - In the root, `docs`, or `.github` folders, called `pull_request_template.md` or `PULL_REQUEST_TEMPLATE.md`
|
||||
* - Or, in a `PULL_REQUEST_TEMPLATE` directory directly below the root, `docs`, or `.github` folders, called `*.md`
|
||||
*
|
||||
* NOTE This method is a modified copy of a method with same name at microsoft/vscode-pull-request-github repository:
|
||||
* https://github.com/microsoft/vscode-pull-request-github/blob/0a0c3c6c21c0b9c2f4d5ffbc3f8c6a825472e9e6/src/github/folderRepositoryManager.ts#L1061
|
||||
*
|
||||
*/
|
||||
export async function findPullRequestTemplates(repositoryRootUri: Uri): Promise<Uri[]> {
|
||||
const results = await Promise.allSettled([
|
||||
...PR_TEMPLATE_FILES.map(x => assertMarkdownFiles(Uri.joinPath(repositoryRootUri, x.dir), x.files)),
|
||||
...PR_TEMPLATE_DIRECTORY_NAMES.map(x => findMarkdownFilesInDir(Uri.joinPath(repositoryRootUri, x)))
|
||||
]);
|
||||
|
||||
return results.flatMap(x => x.status === 'fulfilled' && x.value || []);
|
||||
}
|
||||
|
||||
export async function pickPullRequestTemplate(repositoryRootUri: Uri, templates: Uri[]): Promise<Uri | undefined> {
|
||||
const quickPickItemFromUri = (x: Uri) => ({ label: path.relative(repositoryRootUri.path, x.path), template: x });
|
||||
const quickPickItems = [
|
||||
{
|
||||
label: l10n.t('No template'),
|
||||
picked: true,
|
||||
template: undefined,
|
||||
},
|
||||
...templates.map(quickPickItemFromUri)
|
||||
];
|
||||
const quickPickOptions: QuickPickOptions = {
|
||||
placeHolder: l10n.t('Select the Pull Request template'),
|
||||
ignoreFocusOut: true
|
||||
};
|
||||
const pickedTemplate = await window.showQuickPick(quickPickItems, quickPickOptions);
|
||||
return pickedTemplate?.template;
|
||||
}
|
||||
|
||||
class CommandErrorOutputTextDocumentContentProvider implements TextDocumentContentProvider {
|
||||
|
||||
private items = new Map<string, string>();
|
||||
|
||||
set(uri: Uri, contents: string): void {
|
||||
this.items.set(uri.path, contents);
|
||||
}
|
||||
|
||||
delete(uri: Uri): void {
|
||||
this.items.delete(uri.path);
|
||||
}
|
||||
|
||||
provideTextDocumentContent(uri: Uri): string | undefined {
|
||||
return this.items.get(uri.path);
|
||||
}
|
||||
}
|
||||
|
||||
export class GithubPushErrorHandler implements PushErrorHandler {
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
private commandErrors = new CommandErrorOutputTextDocumentContentProvider();
|
||||
|
||||
constructor(private readonly telemetryReporter: TelemetryReporter) {
|
||||
this.disposables.push(workspace.registerTextDocumentContentProvider('github-output', this.commandErrors));
|
||||
}
|
||||
|
||||
async handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { stderr: string; gitErrorCode: GitErrorCodes }): Promise<boolean> {
|
||||
if (error.gitErrorCode !== GitErrorCodes.PermissionDenied && error.gitErrorCode !== GitErrorCodes.PushRejected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const remoteUrl = remote.pushUrl || (isInCodespaces() ? remote.fetchUrl : undefined);
|
||||
if (!remoteUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const match = /^(?:https:\/\/github\.com\/|git@github\.com:)([^\/]+)\/([^\/.]+)/i.exec(remoteUrl);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/^:/.test(refspec)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [, owner, repo] = match;
|
||||
|
||||
if (error.gitErrorCode === GitErrorCodes.PermissionDenied) {
|
||||
await this.handlePermissionDeniedError(repository, remote, refspec, owner, repo);
|
||||
|
||||
/* __GDPR__
|
||||
"pushErrorHandler" : {
|
||||
"owner": "lszomoru",
|
||||
"handler": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('pushErrorHandler', { handler: 'PermissionDenied' });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Push protection
|
||||
if (/GH009: Secrets detected!/i.test(error.stderr)) {
|
||||
await this.handlePushProtectionError(owner, repo, error.stderr);
|
||||
|
||||
/* __GDPR__
|
||||
"pushErrorHandler" : {
|
||||
"owner": "lszomoru",
|
||||
"handler": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('pushErrorHandler', { handler: 'PushRejected.PushProtection' });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* __GDPR__
|
||||
"pushErrorHandler" : {
|
||||
"owner": "lszomoru",
|
||||
"handler": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('pushErrorHandler', { handler: 'None' });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async handlePermissionDeniedError(repository: Repository, remote: Remote, refspec: string, owner: string, repo: string): Promise<void> {
|
||||
const yes = l10n.t('Create Fork');
|
||||
const no = l10n.t('No');
|
||||
const askFork = l10n.t('You don\'t have permissions to push to "{0}/{1}" on GitHub. Would you like to create a fork and push to it instead?', owner, repo);
|
||||
|
||||
const answer = await window.showWarningMessage(askFork, { modal: true }, yes, no);
|
||||
if (answer !== yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = /^([^:]*):([^:]*)$/.exec(refspec);
|
||||
const localName = match ? match[1] : refspec;
|
||||
let remoteName = match ? match[2] : refspec;
|
||||
|
||||
const [octokit, ghRepository] = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: l10n.t('Create GitHub fork') }, async progress => {
|
||||
progress.report({ message: l10n.t('Forking "{0}/{1}"...', owner, repo), increment: 33 });
|
||||
|
||||
const octokit = await getOctokit();
|
||||
|
||||
type CreateForkResponseData = Awaited<ReturnType<typeof octokit.repos.createFork>>['data'];
|
||||
|
||||
// Issue: what if the repo already exists?
|
||||
let ghRepository: CreateForkResponseData;
|
||||
try {
|
||||
if (isInCodespaces()) {
|
||||
// Call into the codespaces extension to fork the repository
|
||||
const resp = await commands.executeCommand<{ repository: CreateForkResponseData; ref: string }>('github.codespaces.forkRepository');
|
||||
if (!resp) {
|
||||
throw new Error('Unable to fork respository');
|
||||
}
|
||||
|
||||
ghRepository = resp.repository;
|
||||
|
||||
if (resp.ref) {
|
||||
let ref = resp.ref;
|
||||
if (ref.startsWith('refs/heads/')) {
|
||||
ref = ref.substr(11);
|
||||
}
|
||||
|
||||
remoteName = ref;
|
||||
}
|
||||
} else {
|
||||
const resp = await octokit.repos.createFork({ owner, repo });
|
||||
ghRepository = resp.data;
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
progress.report({ message: l10n.t('Pushing changes...'), increment: 33 });
|
||||
|
||||
// Issue: what if there's already an `upstream` repo?
|
||||
await repository.renameRemote(remote.name, 'upstream');
|
||||
|
||||
// Issue: what if there's already another `origin` repo?
|
||||
const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol');
|
||||
const remoteUrl = protocol === 'https' ? ghRepository.clone_url : ghRepository.ssh_url;
|
||||
await repository.addRemote('origin', remoteUrl);
|
||||
|
||||
try {
|
||||
await repository.fetch('origin', remoteName);
|
||||
await repository.setBranchUpstream(localName, `origin/${remoteName}`);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
await repository.push('origin', localName, true);
|
||||
|
||||
return [octokit, ghRepository] as const;
|
||||
});
|
||||
|
||||
// yield
|
||||
(async () => {
|
||||
const openOnGitHub = l10n.t('Open on GitHub');
|
||||
const createPR = l10n.t('Create PR');
|
||||
const action = await window.showInformationMessage(l10n.t('The fork "{0}" was successfully created on GitHub.', ghRepository.full_name), openOnGitHub, createPR);
|
||||
|
||||
if (action === openOnGitHub) {
|
||||
await commands.executeCommand('vscode.open', Uri.parse(ghRepository.html_url));
|
||||
} else if (action === createPR) {
|
||||
const pr = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: l10n.t('Creating GitHub Pull Request...') }, async _ => {
|
||||
let title = `Update ${remoteName}`;
|
||||
const head = repository.state.HEAD?.name;
|
||||
|
||||
let body: string | undefined;
|
||||
|
||||
if (head) {
|
||||
const commit = await repository.getCommit(head);
|
||||
title = commit.message.split('\n')[0];
|
||||
body = commit.message.slice(title.length + 1).trim();
|
||||
}
|
||||
|
||||
const templates = await findPullRequestTemplates(repository.rootUri);
|
||||
if (templates.length > 0) {
|
||||
templates.sort((a, b) => a.path.localeCompare(b.path));
|
||||
|
||||
const template = await pickPullRequestTemplate(repository.rootUri, templates);
|
||||
|
||||
if (template) {
|
||||
body = new TextDecoder('utf-8').decode(await workspace.fs.readFile(template));
|
||||
}
|
||||
}
|
||||
|
||||
const { data: pr } = await octokit.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
body,
|
||||
head: `${ghRepository.owner.login}:${remoteName}`,
|
||||
base: ghRepository.default_branch
|
||||
});
|
||||
|
||||
await repository.setConfig(`branch.${localName}.remote`, 'upstream');
|
||||
await repository.setConfig(`branch.${localName}.merge`, `refs/heads/${remoteName}`);
|
||||
await repository.setConfig(`branch.${localName}.github-pr-owner-number`, `${owner}#${repo}#${pr.number}`);
|
||||
|
||||
return pr;
|
||||
});
|
||||
|
||||
const openPR = l10n.t('Open PR');
|
||||
const action = await window.showInformationMessage(l10n.t('The PR "{0}/{1}#{2}" was successfully created on GitHub.', owner, repo, pr.number), openPR);
|
||||
|
||||
if (action === openPR) {
|
||||
await commands.executeCommand('vscode.open', Uri.parse(pr.html_url));
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
private async handlePushProtectionError(owner: string, repo: string, stderr: string): Promise<void> {
|
||||
// Open command output in an editor
|
||||
const timestamp = new Date().getTime();
|
||||
const uri = Uri.parse(`github-output:/github-error-${timestamp}`);
|
||||
this.commandErrors.set(uri, stderr);
|
||||
|
||||
try {
|
||||
const doc = await workspace.openTextDocument(uri);
|
||||
await window.showTextDocument(doc);
|
||||
}
|
||||
finally {
|
||||
this.commandErrors.set(uri, stderr);
|
||||
}
|
||||
|
||||
// Show dialog
|
||||
const learnMore = l10n.t('Learn More');
|
||||
const message = l10n.t('Your push to "{0}/{1}" was rejected by GitHub because push protection is enabled and one or more secrets were detected.', owner, repo);
|
||||
const answer = await window.showWarningMessage(message, { modal: true }, learnMore);
|
||||
if (answer === learnMore) {
|
||||
commands.executeCommand('vscode.open', 'https://aka.ms/vscode-github-push-protection');
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Uri, env, l10n, workspace } from 'vscode';
|
||||
import { RemoteSourceProvider, RemoteSource, RemoteSourceAction } from './typings/git-base';
|
||||
import { getOctokit } from './auth';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { getRepositoryFromQuery, getRepositoryFromUrl } from './util';
|
||||
import { getBranchLink, getVscodeDevHost } from './links';
|
||||
|
||||
function asRemoteSource(raw: any): RemoteSource {
|
||||
const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol');
|
||||
return {
|
||||
name: `$(github) ${raw.full_name}`,
|
||||
description: `${raw.stargazers_count > 0 ? `$(star-full) ${raw.stargazers_count}` : ''
|
||||
}`,
|
||||
detail: raw.description || undefined,
|
||||
url: protocol === 'https' ? raw.clone_url : raw.ssh_url
|
||||
};
|
||||
}
|
||||
|
||||
export class GithubRemoteSourceProvider implements RemoteSourceProvider {
|
||||
|
||||
readonly name = 'GitHub';
|
||||
readonly icon = 'github';
|
||||
readonly supportsQuery = true;
|
||||
|
||||
private userReposCache: RemoteSource[] = [];
|
||||
|
||||
async getRemoteSources(query?: string): Promise<RemoteSource[]> {
|
||||
const octokit = await getOctokit();
|
||||
|
||||
if (query) {
|
||||
const repository = getRepositoryFromUrl(query);
|
||||
|
||||
if (repository) {
|
||||
const raw = await octokit.repos.get(repository);
|
||||
return [asRemoteSource(raw.data)];
|
||||
}
|
||||
}
|
||||
|
||||
const all = await Promise.all([
|
||||
this.getQueryRemoteSources(octokit, query),
|
||||
this.getUserRemoteSources(octokit, query),
|
||||
]);
|
||||
|
||||
const map = new Map<string, RemoteSource>();
|
||||
|
||||
for (const group of all) {
|
||||
for (const remoteSource of group) {
|
||||
map.set(remoteSource.name, remoteSource);
|
||||
}
|
||||
}
|
||||
|
||||
return [...map.values()];
|
||||
}
|
||||
|
||||
private async getUserRemoteSources(octokit: Octokit, query?: string): Promise<RemoteSource[]> {
|
||||
if (!query) {
|
||||
const user = await octokit.users.getAuthenticated({});
|
||||
const username = user.data.login;
|
||||
const res = await octokit.repos.listForAuthenticatedUser({ username, sort: 'updated', per_page: 100 });
|
||||
this.userReposCache = res.data.map(asRemoteSource);
|
||||
}
|
||||
|
||||
return this.userReposCache;
|
||||
}
|
||||
|
||||
private async getQueryRemoteSources(octokit: Octokit, query?: string): Promise<RemoteSource[]> {
|
||||
if (!query) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const repository = getRepositoryFromQuery(query);
|
||||
|
||||
if (repository) {
|
||||
query = `user:${repository.owner}+${repository.repo}`;
|
||||
}
|
||||
|
||||
query += ` fork:true`;
|
||||
|
||||
const raw = await octokit.search.repos({ q: query, sort: 'stars' });
|
||||
return raw.data.items.map(asRemoteSource);
|
||||
}
|
||||
|
||||
async getBranches(url: string): Promise<string[]> {
|
||||
const repository = getRepositoryFromUrl(url);
|
||||
|
||||
if (!repository) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const octokit = await getOctokit();
|
||||
|
||||
const branches: string[] = [];
|
||||
let page = 1;
|
||||
|
||||
while (true) {
|
||||
const res = await octokit.repos.listBranches({ ...repository, per_page: 100, page });
|
||||
|
||||
if (res.data.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
branches.push(...res.data.map(b => b.name));
|
||||
page++;
|
||||
}
|
||||
|
||||
const repo = await octokit.repos.get(repository);
|
||||
const defaultBranch = repo.data.default_branch;
|
||||
|
||||
return branches.sort((a, b) => a === defaultBranch ? -1 : b === defaultBranch ? 1 : 0);
|
||||
}
|
||||
|
||||
async getRemoteSourceActions(url: string): Promise<RemoteSourceAction[]> {
|
||||
const repository = getRepositoryFromUrl(url);
|
||||
if (!repository) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{
|
||||
label: l10n.t('Open on GitHub'),
|
||||
icon: 'github',
|
||||
run(branch: string) {
|
||||
const link = getBranchLink(url, branch);
|
||||
env.openExternal(Uri.parse(link));
|
||||
}
|
||||
}, {
|
||||
label: l10n.t('Checkout on vscode.dev'),
|
||||
icon: 'globe',
|
||||
run(branch: string) {
|
||||
const link = getBranchLink(url, branch, getVscodeDevHost());
|
||||
env.openExternal(Uri.parse(link));
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { publishRepository } from './publish';
|
||||
import { API as GitAPI, RemoteSourcePublisher, Repository } from './typings/git';
|
||||
|
||||
export class GithubRemoteSourcePublisher implements RemoteSourcePublisher {
|
||||
readonly name = 'GitHub';
|
||||
readonly icon = 'github';
|
||||
|
||||
constructor(private gitAPI: GitAPI) { }
|
||||
|
||||
publishRepository(repository: Repository): Promise<void> {
|
||||
return publishRepository(this.gitAPI, repository);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { API } from './typings/git';
|
||||
import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util';
|
||||
import { encodeURIComponentExceptSlashes, ensurePublished, getRepositoryForFile, notebookCellRangeString, rangeString } from './links';
|
||||
|
||||
export class VscodeDevShareProvider implements vscode.ShareProvider, vscode.Disposable {
|
||||
readonly id: string = 'copyVscodeDevLink';
|
||||
readonly label: string = vscode.l10n.t('Copy vscode.dev Link');
|
||||
readonly priority: number = 10;
|
||||
|
||||
|
||||
private _hasGitHubRepositories: boolean = false;
|
||||
private set hasGitHubRepositories(value: boolean) {
|
||||
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', value);
|
||||
this._hasGitHubRepositories = value;
|
||||
this.ensureShareProviderRegistration();
|
||||
}
|
||||
|
||||
private shareProviderRegistration: vscode.Disposable | undefined;
|
||||
private disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(private readonly gitAPI: API) {
|
||||
this.initializeGitHubRepoContext();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
|
||||
private initializeGitHubRepoContext() {
|
||||
if (this.gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
|
||||
this.hasGitHubRepositories = true;
|
||||
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
|
||||
} else {
|
||||
this.disposables.push(this.gitAPI.onDidOpenRepository(async e => {
|
||||
await e.status();
|
||||
if (repositoryHasGitHubRemote(e)) {
|
||||
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
|
||||
this.hasGitHubRepositories = true;
|
||||
}
|
||||
}));
|
||||
}
|
||||
this.disposables.push(this.gitAPI.onDidCloseRepository(() => {
|
||||
if (!this.gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
|
||||
this.hasGitHubRepositories = false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private ensureShareProviderRegistration() {
|
||||
if (vscode.env.appHost !== 'codespaces' && !this.shareProviderRegistration && this._hasGitHubRepositories) {
|
||||
const shareProviderRegistration = vscode.window.registerShareProvider({ scheme: 'file' }, this);
|
||||
this.shareProviderRegistration = shareProviderRegistration;
|
||||
this.disposables.push(shareProviderRegistration);
|
||||
} else if (this.shareProviderRegistration && !this._hasGitHubRepositories) {
|
||||
this.shareProviderRegistration.dispose();
|
||||
this.shareProviderRegistration = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async provideShare(item: vscode.ShareableItem, _token: vscode.CancellationToken): Promise<vscode.Uri | undefined> {
|
||||
const repository = getRepositoryForFile(this.gitAPI, item.resourceUri);
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ensurePublished(repository, item.resourceUri);
|
||||
|
||||
let repo: { owner: string; repo: string } | undefined;
|
||||
repository.state.remotes.find(remote => {
|
||||
if (remote.fetchUrl) {
|
||||
const foundRepo = getRepositoryFromUrl(remote.fetchUrl);
|
||||
if (foundRepo && (remote.name === repository.state.HEAD?.upstream?.remote)) {
|
||||
repo = foundRepo;
|
||||
return;
|
||||
} else if (foundRepo && !repo) {
|
||||
repo = foundRepo;
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
|
||||
if (!repo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blobSegment = repository?.state.HEAD?.name ? encodeURIComponentExceptSlashes(repository.state.HEAD?.name) : repository?.state.HEAD?.commit;
|
||||
const filepathSegment = encodeURIComponentExceptSlashes(item.resourceUri.path.substring(repository?.rootUri.path.length));
|
||||
const rangeSegment = getRangeSegment(item);
|
||||
return vscode.Uri.parse(`${this.getVscodeDevHost()}/${repo.owner}/${repo.repo}/blob/${blobSegment}${filepathSegment}${rangeSegment}`);
|
||||
|
||||
}
|
||||
|
||||
private getVscodeDevHost(): string {
|
||||
return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`;
|
||||
}
|
||||
}
|
||||
|
||||
function getRangeSegment(item: vscode.ShareableItem) {
|
||||
if (item.resourceUri.scheme === 'vscode-notebook-cell') {
|
||||
const notebookEditor = vscode.window.visibleNotebookEditors.find(editor => editor.notebook.uri.fsPath === item.resourceUri.fsPath);
|
||||
const cell = notebookEditor?.notebook.getCells().find(cell => cell.document.uri.fragment === item.resourceUri?.fragment);
|
||||
const cellIndex = cell?.index ?? notebookEditor?.selection.start;
|
||||
return notebookCellRangeString(cellIndex, item.selection);
|
||||
}
|
||||
|
||||
return rangeString(item.selection);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { workspace, extensions, Uri, commands } from 'vscode';
|
||||
import { findPullRequestTemplates, pickPullRequestTemplate } from '../pushErrorHandler';
|
||||
|
||||
suite('github smoke test', function () {
|
||||
const cwd = workspace.workspaceFolders![0].uri;
|
||||
|
||||
suiteSetup(async function () {
|
||||
const ext = extensions.getExtension('vscode.github');
|
||||
await ext?.activate();
|
||||
});
|
||||
|
||||
test('should find all templates', async function () {
|
||||
const expectedValuesSorted = [
|
||||
'PULL_REQUEST_TEMPLATE/a.md',
|
||||
'PULL_REQUEST_TEMPLATE/b.md',
|
||||
'docs/PULL_REQUEST_TEMPLATE.md',
|
||||
'docs/PULL_REQUEST_TEMPLATE/a.md',
|
||||
'docs/PULL_REQUEST_TEMPLATE/b.md',
|
||||
'.github/PULL_REQUEST_TEMPLATE.md',
|
||||
'.github/PULL_REQUEST_TEMPLATE/a.md',
|
||||
'.github/PULL_REQUEST_TEMPLATE/b.md',
|
||||
'PULL_REQUEST_TEMPLATE.md'
|
||||
];
|
||||
expectedValuesSorted.sort();
|
||||
|
||||
const uris = await findPullRequestTemplates(cwd);
|
||||
|
||||
const urisSorted = uris.map(x => x.path.slice(cwd.path.length));
|
||||
urisSorted.sort();
|
||||
|
||||
assert.deepStrictEqual(urisSorted, expectedValuesSorted);
|
||||
});
|
||||
|
||||
test('selecting non-default quick-pick item should correspond to a template', async () => {
|
||||
const template0 = Uri.file("some-imaginary-template-0");
|
||||
const template1 = Uri.file("some-imaginary-template-1");
|
||||
const templates = [template0, template1];
|
||||
|
||||
const pick = pickPullRequestTemplate(Uri.file("/"), templates);
|
||||
|
||||
await commands.executeCommand('workbench.action.quickOpenSelectNext');
|
||||
await commands.executeCommand('workbench.action.quickOpenSelectNext');
|
||||
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
|
||||
|
||||
assert.ok(await pick === template0);
|
||||
});
|
||||
|
||||
test('selecting first quick-pick item should return undefined', async () => {
|
||||
const templates = [Uri.file("some-imaginary-file")];
|
||||
|
||||
const pick = pickPullRequestTemplate(Uri.file("/"), templates);
|
||||
|
||||
await commands.executeCommand('workbench.action.quickOpenSelectNext');
|
||||
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
|
||||
|
||||
assert.ok(await pick === undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as testRunner from '../../../../test/integration/electron/testrunner';
|
||||
|
||||
const suite = 'Github Tests';
|
||||
|
||||
const options: import('mocha').MochaOptions = {
|
||||
ui: 'tdd',
|
||||
color: true,
|
||||
timeout: 60000
|
||||
};
|
||||
|
||||
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
|
||||
options.reporter = 'mocha-multi-reporters';
|
||||
options.reporterOptions = {
|
||||
reporterEnabled: 'spec, mocha-junit-reporter',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
testsuitesTitle: `${suite} ${process.platform}`,
|
||||
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
testRunner.configure(options);
|
||||
|
||||
export = testRunner;
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Command, Disposable, Event, ProviderResult } from 'vscode';
|
||||
export { ProviderResult } from 'vscode';
|
||||
|
||||
export interface API {
|
||||
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
|
||||
getRemoteSourceActions(url: string): Promise<RemoteSourceAction[]>;
|
||||
pickRemoteSource(options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;
|
||||
}
|
||||
|
||||
export interface GitBaseExtension {
|
||||
|
||||
readonly enabled: boolean;
|
||||
readonly onDidChangeEnablement: Event<boolean>;
|
||||
|
||||
/**
|
||||
* Returns a specific API version.
|
||||
*
|
||||
* Throws error if git-base extension is disabled. You can listed to the
|
||||
* [GitBaseExtension.onDidChangeEnablement](#GitBaseExtension.onDidChangeEnablement)
|
||||
* event to know when the extension becomes enabled/disabled.
|
||||
*
|
||||
* @param version Version number.
|
||||
* @returns API instance
|
||||
*/
|
||||
getAPI(version: 1): API;
|
||||
}
|
||||
|
||||
export interface PickRemoteSourceOptions {
|
||||
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
|
||||
readonly urlLabel?: string | ((url: string) => string);
|
||||
readonly providerName?: string;
|
||||
readonly title?: string;
|
||||
readonly placeholder?: string;
|
||||
readonly branch?: boolean; // then result is PickRemoteSourceResult
|
||||
readonly showRecentSources?: boolean;
|
||||
}
|
||||
|
||||
export interface PickRemoteSourceResult {
|
||||
readonly url: string;
|
||||
readonly branch?: string;
|
||||
}
|
||||
|
||||
export interface RemoteSourceAction {
|
||||
readonly label: string;
|
||||
/**
|
||||
* Codicon name
|
||||
*/
|
||||
readonly icon: string;
|
||||
run(branch: string): void;
|
||||
}
|
||||
|
||||
export interface RemoteSource {
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly detail?: string;
|
||||
/**
|
||||
* Codicon name
|
||||
*/
|
||||
readonly icon?: string;
|
||||
readonly url: string | string[];
|
||||
}
|
||||
|
||||
export interface RecentRemoteSource extends RemoteSource {
|
||||
readonly timestamp: number;
|
||||
}
|
||||
|
||||
export interface RemoteSourceProvider {
|
||||
readonly name: string;
|
||||
/**
|
||||
* Codicon name
|
||||
*/
|
||||
readonly icon?: string;
|
||||
readonly label?: string;
|
||||
readonly placeholder?: string;
|
||||
readonly supportsQuery?: boolean;
|
||||
|
||||
getBranches?(url: string): ProviderResult<string[]>;
|
||||
getRemoteSourceActions?(url: string): ProviderResult<RemoteSourceAction[]>;
|
||||
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
|
||||
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
|
||||
}
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Uri, Event, Disposable, ProviderResult, Command, SourceControlHistoryItem } from 'vscode';
|
||||
export { ProviderResult } from 'vscode';
|
||||
|
||||
export interface Git {
|
||||
readonly path: string;
|
||||
}
|
||||
|
||||
export interface InputBox {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const enum ForcePushMode {
|
||||
Force,
|
||||
ForceWithLease
|
||||
}
|
||||
|
||||
export const enum RefType {
|
||||
Head,
|
||||
RemoteHead,
|
||||
Tag
|
||||
}
|
||||
|
||||
export interface Ref {
|
||||
readonly type: RefType;
|
||||
readonly name?: string;
|
||||
readonly commit?: string;
|
||||
readonly remote?: string;
|
||||
}
|
||||
|
||||
export interface UpstreamRef {
|
||||
readonly remote: string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface Branch extends Ref {
|
||||
readonly upstream?: UpstreamRef;
|
||||
readonly ahead?: number;
|
||||
readonly behind?: number;
|
||||
}
|
||||
|
||||
export interface Commit {
|
||||
readonly hash: string;
|
||||
readonly message: string;
|
||||
readonly parents: string[];
|
||||
readonly authorDate?: Date;
|
||||
readonly authorName?: string;
|
||||
readonly authorEmail?: string;
|
||||
readonly commitDate?: Date;
|
||||
}
|
||||
|
||||
export interface Submodule {
|
||||
readonly name: string;
|
||||
readonly path: string;
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
export interface Remote {
|
||||
readonly name: string;
|
||||
readonly fetchUrl?: string;
|
||||
readonly pushUrl?: string;
|
||||
readonly isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const enum Status {
|
||||
INDEX_MODIFIED,
|
||||
INDEX_ADDED,
|
||||
INDEX_DELETED,
|
||||
INDEX_RENAMED,
|
||||
INDEX_COPIED,
|
||||
|
||||
MODIFIED,
|
||||
DELETED,
|
||||
UNTRACKED,
|
||||
IGNORED,
|
||||
INTENT_TO_ADD,
|
||||
INTENT_TO_RENAME,
|
||||
TYPE_CHANGED,
|
||||
|
||||
ADDED_BY_US,
|
||||
ADDED_BY_THEM,
|
||||
DELETED_BY_US,
|
||||
DELETED_BY_THEM,
|
||||
BOTH_ADDED,
|
||||
BOTH_DELETED,
|
||||
BOTH_MODIFIED
|
||||
}
|
||||
|
||||
export interface Change {
|
||||
|
||||
/**
|
||||
* Returns either `originalUri` or `renameUri`, depending
|
||||
* on whether this change is a rename change. When
|
||||
* in doubt always use `uri` over the other two alternatives.
|
||||
*/
|
||||
readonly uri: Uri;
|
||||
readonly originalUri: Uri;
|
||||
readonly renameUri: Uri | undefined;
|
||||
readonly status: Status;
|
||||
}
|
||||
|
||||
export interface RepositoryState {
|
||||
readonly HEAD: Branch | undefined;
|
||||
readonly refs: Ref[];
|
||||
readonly remotes: Remote[];
|
||||
readonly submodules: Submodule[];
|
||||
readonly rebaseCommit: Commit | undefined;
|
||||
|
||||
readonly mergeChanges: Change[];
|
||||
readonly indexChanges: Change[];
|
||||
readonly workingTreeChanges: Change[];
|
||||
|
||||
readonly onDidChange: Event<void>;
|
||||
}
|
||||
|
||||
export interface RepositoryUIState {
|
||||
readonly selected: boolean;
|
||||
readonly onDidChange: Event<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log options.
|
||||
*/
|
||||
export interface LogOptions {
|
||||
/** Max number of log entries to retrieve. If not specified, the default is 32. */
|
||||
readonly maxEntries?: number;
|
||||
readonly path?: string;
|
||||
}
|
||||
|
||||
export interface CommitOptions {
|
||||
all?: boolean | 'tracked';
|
||||
amend?: boolean;
|
||||
signoff?: boolean;
|
||||
signCommit?: boolean;
|
||||
empty?: boolean;
|
||||
noVerify?: boolean;
|
||||
requireUserConfig?: boolean;
|
||||
useEditor?: boolean;
|
||||
verbose?: boolean;
|
||||
/**
|
||||
* string - execute the specified command after the commit operation
|
||||
* undefined - execute the command specified in git.postCommitCommand
|
||||
* after the commit operation
|
||||
* null - do not execute any command after the commit operation
|
||||
*/
|
||||
postCommitCommand?: string | null;
|
||||
}
|
||||
|
||||
export interface FetchOptions {
|
||||
remote?: string;
|
||||
ref?: string;
|
||||
all?: boolean;
|
||||
prune?: boolean;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
export interface InitOptions {
|
||||
defaultBranch?: string;
|
||||
}
|
||||
|
||||
export interface BranchQuery {
|
||||
readonly remote?: boolean;
|
||||
readonly pattern?: string;
|
||||
readonly count?: number;
|
||||
readonly contains?: string;
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
|
||||
readonly rootUri: Uri;
|
||||
readonly inputBox: InputBox;
|
||||
readonly state: RepositoryState;
|
||||
readonly ui: RepositoryUIState;
|
||||
|
||||
getConfigs(): Promise<{ key: string; value: string; }[]>;
|
||||
getConfig(key: string): Promise<string>;
|
||||
setConfig(key: string, value: string): Promise<string>;
|
||||
getGlobalConfig(key: string): Promise<string>;
|
||||
|
||||
getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>;
|
||||
detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>;
|
||||
buffer(ref: string, path: string): Promise<Buffer>;
|
||||
show(ref: string, path: string): Promise<string>;
|
||||
getCommit(ref: string): Promise<Commit>;
|
||||
|
||||
add(paths: string[]): Promise<void>;
|
||||
revert(paths: string[]): Promise<void>;
|
||||
clean(paths: string[]): Promise<void>;
|
||||
|
||||
apply(patch: string, reverse?: boolean): Promise<void>;
|
||||
diff(cached?: boolean): Promise<string>;
|
||||
diffWithHEAD(): Promise<Change[]>;
|
||||
diffWithHEAD(path: string): Promise<string>;
|
||||
diffWith(ref: string): Promise<Change[]>;
|
||||
diffWith(ref: string, path: string): Promise<string>;
|
||||
diffIndexWithHEAD(): Promise<Change[]>;
|
||||
diffIndexWithHEAD(path: string): Promise<string>;
|
||||
diffIndexWith(ref: string): Promise<Change[]>;
|
||||
diffIndexWith(ref: string, path: string): Promise<string>;
|
||||
diffBlobs(object1: string, object2: string): Promise<string>;
|
||||
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
|
||||
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
|
||||
|
||||
hashObject(data: string): Promise<string>;
|
||||
|
||||
createBranch(name: string, checkout: boolean, ref?: string): Promise<void>;
|
||||
deleteBranch(name: string, force?: boolean): Promise<void>;
|
||||
getBranch(name: string): Promise<Branch>;
|
||||
getBranches(query: BranchQuery): Promise<Ref[]>;
|
||||
setBranchUpstream(name: string, upstream: string): Promise<void>;
|
||||
|
||||
getMergeBase(ref1: string, ref2: string): Promise<string>;
|
||||
|
||||
tag(name: string, upstream: string): Promise<void>;
|
||||
deleteTag(name: string): Promise<void>;
|
||||
|
||||
status(): Promise<void>;
|
||||
checkout(treeish: string): Promise<void>;
|
||||
|
||||
addRemote(name: string, url: string): Promise<void>;
|
||||
removeRemote(name: string): Promise<void>;
|
||||
renameRemote(name: string, newName: string): Promise<void>;
|
||||
|
||||
fetch(options?: FetchOptions): Promise<void>;
|
||||
fetch(remote?: string, ref?: string, depth?: number): Promise<void>;
|
||||
pull(unshallow?: boolean): Promise<void>;
|
||||
push(remoteName?: string, branchName?: string, setUpstream?: boolean, force?: ForcePushMode): Promise<void>;
|
||||
|
||||
blame(path: string): Promise<string>;
|
||||
log(options?: LogOptions): Promise<Commit[]>;
|
||||
|
||||
commit(message: string, opts?: CommitOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export interface RemoteSource {
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly url: string | string[];
|
||||
}
|
||||
|
||||
export interface RemoteSourceProvider {
|
||||
readonly name: string;
|
||||
readonly icon?: string; // codicon name
|
||||
readonly supportsQuery?: boolean;
|
||||
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
|
||||
getBranches?(url: string): ProviderResult<string[]>;
|
||||
publishRepository?(repository: Repository): Promise<void>;
|
||||
}
|
||||
|
||||
export interface RemoteSourcePublisher {
|
||||
readonly name: string;
|
||||
readonly icon?: string; // codicon name
|
||||
publishRepository(repository: Repository): Promise<void>;
|
||||
}
|
||||
|
||||
export interface Credentials {
|
||||
readonly username: string;
|
||||
readonly password: string;
|
||||
}
|
||||
|
||||
export interface CredentialsProvider {
|
||||
getCredentials(host: Uri): ProviderResult<Credentials>;
|
||||
}
|
||||
|
||||
export interface PostCommitCommandsProvider {
|
||||
getCommands(repository: Repository): Command[];
|
||||
}
|
||||
|
||||
export interface PushErrorHandler {
|
||||
handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface BranchProtection {
|
||||
readonly remote: string;
|
||||
readonly rules: BranchProtectionRule[];
|
||||
}
|
||||
|
||||
export interface BranchProtectionRule {
|
||||
readonly include?: string[];
|
||||
readonly exclude?: string[];
|
||||
}
|
||||
|
||||
export interface BranchProtectionProvider {
|
||||
onDidChangeBranchProtection: Event<Uri>;
|
||||
provideBranchProtection(): BranchProtection[];
|
||||
}
|
||||
|
||||
export interface AvatarQueryCommit {
|
||||
readonly hash: string;
|
||||
readonly authorName?: string;
|
||||
readonly authorEmail?: string;
|
||||
}
|
||||
|
||||
export interface AvatarQuery {
|
||||
readonly commits: AvatarQueryCommit[];
|
||||
readonly size: number;
|
||||
}
|
||||
|
||||
export interface SourceControlHistoryItemDetailsProvider {
|
||||
provideAvatar(repository: Repository, query: AvatarQuery): Promise<Map<string, string | undefined> | undefined>;
|
||||
provideHoverCommands(repository: Repository): Promise<Command[] | undefined>;
|
||||
provideMessageLinks(repository: Repository, message: string): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
export type APIState = 'uninitialized' | 'initialized';
|
||||
|
||||
export interface PublishEvent {
|
||||
repository: Repository;
|
||||
branch?: string;
|
||||
}
|
||||
|
||||
export interface API {
|
||||
readonly state: APIState;
|
||||
readonly onDidChangeState: Event<APIState>;
|
||||
readonly onDidPublish: Event<PublishEvent>;
|
||||
readonly git: Git;
|
||||
readonly repositories: Repository[];
|
||||
readonly onDidOpenRepository: Event<Repository>;
|
||||
readonly onDidCloseRepository: Event<Repository>;
|
||||
|
||||
toGitUri(uri: Uri, ref: string): Uri;
|
||||
getRepository(uri: Uri): Repository | null;
|
||||
init(root: Uri, options?: InitOptions): Promise<Repository | null>;
|
||||
openRepository(root: Uri): Promise<Repository | null>
|
||||
|
||||
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;
|
||||
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
|
||||
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
|
||||
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
|
||||
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
|
||||
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable;
|
||||
registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable;
|
||||
}
|
||||
|
||||
export interface GitExtension {
|
||||
|
||||
readonly enabled: boolean;
|
||||
readonly onDidChangeEnablement: Event<boolean>;
|
||||
|
||||
/**
|
||||
* Returns a specific API version.
|
||||
*
|
||||
* Throws error if git extension is disabled. You can listen to the
|
||||
* [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event
|
||||
* to know when the extension becomes enabled/disabled.
|
||||
*
|
||||
* @param version Version number.
|
||||
* @returns API instance
|
||||
*/
|
||||
getAPI(version: 1): API;
|
||||
}
|
||||
|
||||
export const enum GitErrorCodes {
|
||||
BadConfigFile = 'BadConfigFile',
|
||||
AuthenticationFailed = 'AuthenticationFailed',
|
||||
NoUserNameConfigured = 'NoUserNameConfigured',
|
||||
NoUserEmailConfigured = 'NoUserEmailConfigured',
|
||||
NoRemoteRepositorySpecified = 'NoRemoteRepositorySpecified',
|
||||
NotAGitRepository = 'NotAGitRepository',
|
||||
NotAtRepositoryRoot = 'NotAtRepositoryRoot',
|
||||
Conflict = 'Conflict',
|
||||
StashConflict = 'StashConflict',
|
||||
UnmergedChanges = 'UnmergedChanges',
|
||||
PushRejected = 'PushRejected',
|
||||
RemoteConnectionError = 'RemoteConnectionError',
|
||||
DirtyWorkTree = 'DirtyWorkTree',
|
||||
CantOpenResource = 'CantOpenResource',
|
||||
GitNotFound = 'GitNotFound',
|
||||
CantCreatePipe = 'CantCreatePipe',
|
||||
PermissionDenied = 'PermissionDenied',
|
||||
CantAccessRemote = 'CantAccessRemote',
|
||||
RepositoryNotFound = 'RepositoryNotFound',
|
||||
RepositoryIsLocked = 'RepositoryIsLocked',
|
||||
BranchNotFullyMerged = 'BranchNotFullyMerged',
|
||||
NoRemoteReference = 'NoRemoteReference',
|
||||
InvalidBranchName = 'InvalidBranchName',
|
||||
BranchAlreadyExists = 'BranchAlreadyExists',
|
||||
NoLocalChanges = 'NoLocalChanges',
|
||||
NoStashFound = 'NoStashFound',
|
||||
LocalChangesOverwritten = 'LocalChangesOverwritten',
|
||||
NoUpstreamBranch = 'NoUpstreamBranch',
|
||||
IsInSubmodule = 'IsInSubmodule',
|
||||
WrongCase = 'WrongCase',
|
||||
CantLockRef = 'CantLockRef',
|
||||
CantRebaseMultipleBranches = 'CantRebaseMultipleBranches',
|
||||
PatchDoesNotApply = 'PatchDoesNotApply',
|
||||
NoPathFound = 'NoPathFound',
|
||||
UnknownPath = 'UnknownPath',
|
||||
EmptyCommitMessage = 'EmptyCommitMessage'
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
declare module 'tunnel';
|
||||
@@ -0,0 +1,108 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Repository } from './typings/git';
|
||||
|
||||
export class DisposableStore {
|
||||
|
||||
private disposables = new Set<vscode.Disposable>();
|
||||
|
||||
add(disposable: vscode.Disposable): void {
|
||||
this.disposables.add(disposable);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const disposable of this.disposables) {
|
||||
disposable.dispose();
|
||||
}
|
||||
|
||||
this.disposables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
function decorate(decorator: (fn: Function, key: string) => Function): Function {
|
||||
return (_target: any, key: string, descriptor: any) => {
|
||||
let fnKey: string | null = null;
|
||||
let fn: Function | null = null;
|
||||
|
||||
if (typeof descriptor.value === 'function') {
|
||||
fnKey = 'value';
|
||||
fn = descriptor.value;
|
||||
} else if (typeof descriptor.get === 'function') {
|
||||
fnKey = 'get';
|
||||
fn = descriptor.get;
|
||||
}
|
||||
|
||||
if (!fn || !fnKey) {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
|
||||
descriptor[fnKey] = decorator(fn, key);
|
||||
};
|
||||
}
|
||||
|
||||
function _sequentialize(fn: Function, key: string): Function {
|
||||
const currentKey = `__$sequence$${key}`;
|
||||
|
||||
return function (this: any, ...args: any[]) {
|
||||
const currentPromise = this[currentKey] as Promise<any> || Promise.resolve(null);
|
||||
const run = async () => await fn.apply(this, args);
|
||||
this[currentKey] = currentPromise.then(run, run);
|
||||
return this[currentKey];
|
||||
};
|
||||
}
|
||||
|
||||
export const sequentialize = decorate(_sequentialize);
|
||||
|
||||
export function groupBy<T>(data: ReadonlyArray<T>, compare: (a: T, b: T) => number): T[][] {
|
||||
const result: T[][] = [];
|
||||
let currentGroup: T[] | undefined = undefined;
|
||||
for (const element of data.slice(0).sort(compare)) {
|
||||
if (!currentGroup || compare(currentGroup[0], element) !== 0) {
|
||||
currentGroup = [element];
|
||||
result.push(currentGroup);
|
||||
} else {
|
||||
currentGroup.push(element);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getRepositoryFromUrl(url: string): { owner: string; repo: string } | undefined {
|
||||
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(\.git)?$/i.exec(url)
|
||||
|| /^git@github\.com:([^/]+)\/([^/]+?)(\.git)?$/i.exec(url);
|
||||
return match ? { owner: match[1], repo: match[2] } : undefined;
|
||||
}
|
||||
|
||||
export function getRepositoryFromQuery(query: string): { owner: string; repo: string } | undefined {
|
||||
const match = /^([^/]+)\/([^/]+)$/i.exec(query);
|
||||
return match ? { owner: match[1], repo: match[2] } : undefined;
|
||||
}
|
||||
|
||||
export function repositoryHasGitHubRemote(repository: Repository) {
|
||||
return !!repository.state.remotes.find(remote => remote.fetchUrl ? getRepositoryFromUrl(remote.fetchUrl) : undefined);
|
||||
}
|
||||
|
||||
export function getRepositoryDefaultRemoteUrl(repository: Repository): string | undefined {
|
||||
const remotes = repository.state.remotes
|
||||
.filter(remote => remote.fetchUrl && getRepositoryFromUrl(remote.fetchUrl));
|
||||
|
||||
if (remotes.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// upstream -> origin -> first
|
||||
const remote = remotes.find(remote => remote.name === 'upstream')
|
||||
?? remotes.find(remote => remote.name === 'origin')
|
||||
?? remotes[0];
|
||||
|
||||
return remote.fetchUrl;
|
||||
}
|
||||
|
||||
export function getRepositoryDefaultRemote(repository: Repository): { owner: string; repo: string } | undefined {
|
||||
const fetchUrl = getRepositoryDefaultRemoteUrl(repository);
|
||||
return fetchUrl ? getRepositoryFromUrl(fetchUrl) : undefined;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out",
|
||||
"experimentalDecorators": true,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../../src/vscode-dts/vscode.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.shareProvider.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.timeline.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user