chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:42 +08:00
commit 2d485ba600
8326 changed files with 2641263 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
legacy-peer-deps="true"
timeout=180000
+7
View File
@@ -0,0 +1,7 @@
src/**
test/**
out/**
tsconfig.json
build/**
extension.webpack.config.js
package-lock.json
+26
View File
@@ -0,0 +1,26 @@
# Git integration for Visual Studio Code
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
## Features
See [Git support in VS Code](https://code.visualstudio.com/docs/editor/versioncontrol#_git-support) to learn about the features of this extension.
## API
The Git extension exposes an API, reachable by any other extension.
1. Copy `src/api/git.d.ts` to your extension's sources;
2. Include `git.d.ts` in your extension's compilation.
3. Get a hold of the API with the following snippet:
```ts
const gitExtension = vscode.extensions.getExtension<GitExtension>('vscode.git').exports;
const git = gitExtension.getAPI(1);
```
**Note:** To ensure that the `vscode.git` extension is activated before your extension, add `extensionDependencies` ([docs](https://code.visualstudio.com/api/references/extension-manifest)) into the `package.json` of your extension:
```json
"extensionDependencies": [
"vscode.git"
]
```
+101
View File
@@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/no-var-requires */
const fs = require('fs');
const https = require('https');
const path = require('path');
async function generate() {
/**
* @type {Map<string, string>}
*/
const shortcodeMap = new Map();
// Get emoji data from https://github.com/milesj/emojibase
// https://github.com/milesj/emojibase/
const files = ['github.raw.json'] //, 'emojibase.raw.json']; //, 'iamcal.raw.json', 'joypixels.raw.json'];
for (const file of files) {
await download(
`https://raw.githubusercontent.com/milesj/emojibase/master/packages/data/en/shortcodes/${file}`,
file,
);
/**
* @type {Record<string, string | string[]>}}
*/
// eslint-disable-next-line import/no-dynamic-require
const data = require(path.join(process.cwd(), file));
for (const [emojis, codes] of Object.entries(data)) {
const emoji = emojis
.split('-')
.map(c => String.fromCodePoint(parseInt(c, 16)))
.join('');
for (const code of Array.isArray(codes) ? codes : [codes]) {
if (shortcodeMap.has(code)) {
// console.warn(`${file}: ${code}`);
continue;
}
shortcodeMap.set(code, emoji);
}
}
fs.unlink(file, () => { });
}
// Get gitmoji data from https://github.com/carloscuesta/gitmoji
// https://github.com/carloscuesta/gitmoji/blob/master/src/data/gitmojis.json
await download(
'https://raw.githubusercontent.com/carloscuesta/gitmoji/master/src/data/gitmojis.json',
'gitmojis.json',
);
/**
* @type {({ code: string; emoji: string })[]}
*/
// eslint-disable-next-line import/no-dynamic-require
const gitmojis = require(path.join(process.cwd(), 'gitmojis.json')).gitmojis;
for (const emoji of gitmojis) {
if (emoji.code.startsWith(':') && emoji.code.endsWith(':')) {
emoji.code = emoji.code.substring(1, emoji.code.length - 2);
}
if (shortcodeMap.has(emoji.code)) {
// console.warn(`GitHub: ${emoji.code}`);
continue;
}
shortcodeMap.set(emoji.code, emoji.emoji);
}
fs.unlink('gitmojis.json', () => { });
// Sort the emojis for easier diff checking
const list = [...shortcodeMap.entries()];
list.sort();
const map = list.reduce((m, [key, value]) => {
m[key] = value;
return m;
}, Object.create(null));
fs.writeFileSync(path.join(process.cwd(), 'resources/emojis.json'), JSON.stringify(map), 'utf8');
}
function download(url, destination) {
return new Promise(resolve => {
const stream = fs.createWriteStream(destination);
https.get(url, rsp => {
rsp.pipe(stream);
stream.on('finish', () => {
stream.close();
resolve();
});
});
});
}
void generate();
@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* 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: {
main: './src/main.ts',
['askpass-main']: './src/askpass-main.ts',
['git-editor-main']: './src/git-editor-main.ts'
}
});
+416
View File
@@ -0,0 +1,416 @@
{
"name": "git",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "git",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@joaomoreno/unique-names-generator": "^5.2.0",
"@vscode/extension-telemetry": "^0.9.8",
"byline": "^5.0.0",
"file-type": "16.5.4",
"picomatch": "2.3.1",
"vscode-uri": "^2.0.0",
"which": "4.0.0"
},
"devDependencies": {
"@types/byline": "4.2.31",
"@types/mocha": "^9.1.1",
"@types/node": "20.x",
"@types/picomatch": "2.3.0",
"@types/which": "3.0.0"
},
"engines": {
"vscode": "^1.5.0"
}
},
"node_modules/@joaomoreno/unique-names-generator": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@joaomoreno/unique-names-generator/-/unique-names-generator-5.2.0.tgz",
"integrity": "sha512-JEh3qZ85Z6syFvQlhRGRyTPI1M5VticiiP8Xl8EV0XfyfI4Mwzd6Zw28BBrEgUJCYv/cpKCQClVj3J8Tn0KFiA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"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/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
},
"node_modules/@types/byline": {
"version": "4.2.31",
"resolved": "https://registry.npmjs.org/@types/byline/-/byline-4.2.31.tgz",
"integrity": "sha1-DmH8ucA+BH0hxEllVMcRYperYM0= sha512-TC6Ljn7tALesQMQyTNoMWoM44SNvWtCLkJDrA/TxcwE5ILkWt4zi5wbEokqiDk42S75eykAY1onPImWDybOkmQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/mocha": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz",
"integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==",
"dev": true
},
"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/@types/picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==",
"dev": true
},
"node_modules/@types/which": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.0.tgz",
"integrity": "sha512-ASCxdbsrwNfSMXALlC3Decif9rwDMu+80KGp5zI2RLRotfMsTv7fHL8W8VDp24wymzDyIFudhUeSCugrgRFfHQ==",
"dev": true
},
"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/byline": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz",
"integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/file-type": {
"version": "16.5.4",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
"integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==",
"dependencies": {
"readable-web-to-node-stream": "^3.0.0",
"strtok3": "^6.2.4",
"token-types": "^4.1.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/isexe": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
"engines": {
"node": ">=16"
}
},
"node_modules/peek-readable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
"integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==",
"engines": {
"node": ">=8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readable-web-to-node-stream": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
"dependencies": {
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strtok3": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
"integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"peek-readable": "^4.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/token-types": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.0.tgz",
"integrity": "sha512-P0rrp4wUpefLncNamWIef62J0v0kQR/GfDVji9WKY7GDCWy5YbVSrKUTam07iWPZQGy0zWNOfstYTykMmPNR7w==",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"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/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/vscode-uri": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.0.0.tgz",
"integrity": "sha512-lWXWofDSYD8r/TIyu64MdwB4FaSirQ608PP/TzUyslyOeHGwQ0eTHUZeJrK1ILOmwUHaJtV693m2JoUYroUDpw=="
},
"node_modules/which": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
"dependencies": {
"isexe": "^3.1.1"
},
"bin": {
"node-which": "bin/which.js"
},
"engines": {
"node": "^16.13.0 || >=18.0.0"
}
}
}
}
File diff suppressed because it is too large Load Diff
+460
View File
@@ -0,0 +1,460 @@
{
"displayName": "Git",
"description": "Git SCM Integration",
"command.continueInLocalClone": "Clone Repository Locally and Open on Desktop...",
"command.continueInLocalClone.qualifiedName": "Continue Working in New Local Clone",
"command.clone": "Clone",
"command.cloneRecursive": "Clone (Recursive)",
"command.init": "Initialize Repository",
"command.openRepository": "Open Repository",
"command.reopenClosedRepositories": "Reopen Closed Repositories...",
"command.close": "Close Repository",
"command.closeOtherRepositories": "Close Other Repositories",
"command.refresh": "Refresh",
"command.openChange": "Open Changes",
"command.openAllChanges": "Open All Changes",
"command.openFile": "Open File",
"command.openHEADFile": "Open File (HEAD)",
"command.stage": "Stage Changes",
"command.stageAll": "Stage All Changes",
"command.stageAllTracked": "Stage All Tracked Changes",
"command.stageAllUntracked": "Stage All Untracked Changes",
"command.stageAllMerge": "Stage All Merge Changes",
"command.stageSelectedRanges": "Stage Selected Ranges",
"command.revertSelectedRanges": "Revert Selected Ranges",
"command.stageChange": "Stage Change",
"command.stageSelection": "Stage Selection",
"command.stageBlock": "Stage Block",
"command.revertChange": "Revert Change",
"command.unstage": "Unstage Changes",
"command.unstageAll": "Unstage All Changes",
"command.unstageSelectedRanges": "Unstage Selected Ranges",
"command.rename": "Rename",
"command.clean": "Discard Changes",
"command.cleanAll": "Discard All Changes",
"command.cleanAllTracked": "Discard All Tracked Changes",
"command.cleanAllUntracked": "Discard All Untracked Changes",
"command.closeAllDiffEditors": "Close All Diff Editors",
"command.closeAllUnmodifiedEditors": "Close All Unmodified Editors",
"command.commit": "Commit",
"command.commitAmend": "Commit (Amend)",
"command.commitSigned": "Commit (Signed Off)",
"command.commitStaged": "Commit Staged",
"command.commitEmpty": "Commit Empty",
"command.commitStagedSigned": "Commit Staged (Signed Off)",
"command.commitStagedAmend": "Commit Staged (Amend)",
"command.commitAll": "Commit All",
"command.commitAllSigned": "Commit All (Signed Off)",
"command.commitAllAmend": "Commit All (Amend)",
"command.commitNoVerify": "Commit (No Verify)",
"command.commitStagedNoVerify": "Commit Staged (No Verify)",
"command.commitEmptyNoVerify": "Commit Empty (No Verify)",
"command.commitStagedSignedNoVerify": "Commit Staged (Signed Off, No Verify)",
"command.commitAmendNoVerify": "Commit (Amend, No Verify)",
"command.commitSignedNoVerify": "Commit (Signed Off, No Verify)",
"command.commitStagedAmendNoVerify": "Commit Staged (Amend, No Verify)",
"command.commitAllNoVerify": "Commit All (No Verify)",
"command.commitAllSignedNoVerify": "Commit All (Signed Off, No Verify)",
"command.commitAllAmendNoVerify": "Commit All (Amend, No Verify)",
"command.commitMessageAccept": "Accept Commit Message",
"command.commitMessageDiscard": "Discard Commit Message",
"command.restoreCommitTemplate": "Restore Commit Template",
"command.undoCommit": "Undo Last Commit",
"command.checkout": "Checkout to...",
"command.checkoutDetached": "Checkout to (Detached)...",
"command.branch": "Create Branch...",
"command.branchFrom": "Create Branch From...",
"command.deleteBranch": "Delete Branch...",
"command.deleteRemoteBranch": "Delete Remote Branch...",
"command.renameBranch": "Rename Branch...",
"command.cherryPick": "Cherry Pick...",
"command.cherryPickAbort": "Abort Cherry Pick",
"command.merge": "Merge...",
"command.mergeAbort": "Abort Merge",
"command.rebase": "Rebase Branch...",
"command.createTag": "Create Tag...",
"command.deleteTag": "Delete Tag...",
"command.deleteRemoteTag": "Delete Remote Tag...",
"command.fetch": "Fetch",
"command.fetchPrune": "Fetch (Prune)",
"command.fetchAll": "Fetch From All Remotes",
"command.pull": "Pull",
"command.pullRebase": "Pull (Rebase)",
"command.pullFrom": "Pull from...",
"command.push": "Push",
"command.pushForce": "Push (Force)",
"command.pushTo": "Push to...",
"command.pushToForce": "Push to... (Force)",
"command.pushFollowTags": "Push (Follow Tags)",
"command.pushFollowTagsForce": "Push (Follow Tags, Force)",
"command.pushTags": "Push Tags",
"command.addRemote": "Add Remote...",
"command.removeRemote": "Remove Remote",
"command.sync": "Sync",
"command.syncRebase": "Sync (Rebase)",
"command.publish": "Publish Branch...",
"command.showOutput": "Show Git Output",
"command.ignore": "Add to .gitignore",
"command.revealInExplorer": "Reveal in Explorer View",
"command.revealFileInOS.linux": "Open Containing Folder",
"command.revealFileInOS.mac": "Reveal in Finder",
"command.revealFileInOS.windows": "Reveal in File Explorer",
"command.rebaseAbort": "Abort Rebase",
"command.stashIncludeUntracked": "Stash (Include Untracked)",
"command.stash": "Stash",
"command.stashStaged": "Stash Staged",
"command.stashPop": "Pop Stash...",
"command.stashPopLatest": "Pop Latest Stash",
"command.stashPopEditor": "Pop Stash",
"command.stashApply": "Apply Stash...",
"command.stashApplyLatest": "Apply Latest Stash",
"command.stashApplyEditor": "Apply Stash",
"command.stashDrop": "Drop Stash...",
"command.stashDropAll": "Drop All Stashes...",
"command.stashDropEditor": "Drop Stash",
"command.stashView": "View Stash...",
"command.timelineOpenDiff": "Open Changes",
"command.timelineCopyCommitId": "Copy Commit ID",
"command.timelineCopyCommitMessage": "Copy Commit Message",
"command.timelineSelectForCompare": "Select for Compare",
"command.timelineCompareWithSelected": "Compare with Selected",
"command.manageUnsafeRepositories": "Manage Unsafe Repositories",
"command.openRepositoriesInParentFolders": "Open Repositories In Parent Folders",
"command.viewChanges": "Open Changes",
"command.viewStagedChanges": "Open Staged Changes",
"command.viewUntrackedChanges": "Open Untracked Changes",
"command.viewCommit": "Open Commit",
"command.graphCheckout": "Checkout",
"command.graphCheckoutDetached": "Checkout (Detached)",
"command.graphCherryPick": "Cherry Pick",
"command.graphDeleteBranch": "Delete Branch",
"command.graphDeleteTag": "Delete Tag",
"command.blameToggleEditorDecoration": "Toggle Git Blame Editor Decoration",
"command.blameToggleStatusBarItem": "Toggle Git Blame Status Bar Item",
"command.api.getRepositories": "Get Repositories",
"command.api.getRepositoryState": "Get Repository State",
"command.api.getRemoteSources": "Get Remote Sources",
"command.git.acceptMerge": "Complete Merge",
"command.git.openMergeEditor": "Resolve in Merge Editor",
"command.git.runGitMerge": "Compute Conflicts With Git",
"command.git.runGitMergeDiff3": "Compute Conflicts With Git (Diff3)",
"config.enabled": "Whether Git is enabled.",
"config.path": "Path and filename of the git executable, e.g. `C:\\Program Files\\Git\\bin\\git.exe` (Windows). This can also be an array of string values containing multiple paths to look up.",
"config.autoRepositoryDetection": "Configures when repositories should be automatically detected.",
"config.autoRepositoryDetection.true": "Scan for both subfolders of the current opened folder and parent folders of open files.",
"config.autoRepositoryDetection.false": "Disable automatic repository scanning.",
"config.autoRepositoryDetection.subFolders": "Scan for subfolders of the currently opened folder.",
"config.autoRepositoryDetection.openEditors": "Scan for parent folders of open files.",
"config.autorefresh": "Whether auto refreshing is enabled.",
"config.autofetch": "When set to true, commits will automatically be fetched from the default remote of the current Git repository. Setting to `all` will fetch from all remotes.",
"config.autofetchPeriod": "Duration in seconds between each automatic git fetch, when `#git.autofetch#` is enabled.",
"config.confirmSync": "Confirm before synchronizing Git repositories.",
"config.countBadge": "Controls the Git count badge.",
"config.countBadge.all": "Count all changes.",
"config.countBadge.tracked": "Count only tracked changes.",
"config.countBadge.off": "Turn off counter.",
"config.checkoutType": "Controls what type of Git refs are listed when running `Checkout to...`.",
"config.checkoutType.local": "Local branches",
"config.checkoutType.tags": "Tags",
"config.checkoutType.remote": "Remote branches",
"config.defaultBranchName": "The name of the default branch (example: main, trunk, development) when initializing a new Git repository. When set to empty, the default branch name configured in Git will be used. **Note:** Requires Git version `2.28.0` or later.",
"config.branchPrefix": "Prefix used when creating a new branch.",
"config.branchProtection": "List of protected branches. By default, a prompt is shown before changes are committed to a protected branch. The prompt can be controlled using the `#git.branchProtectionPrompt#` setting.",
"config.branchProtectionPrompt": "Controls whether a prompt is being shown before changes are committed to a protected branch.",
"config.branchProtectionPrompt.alwaysCommit": "Always commit changes to the protected branch.",
"config.branchProtectionPrompt.alwaysCommitToNewBranch": "Always commit changes to a new branch.",
"config.branchProtectionPrompt.alwaysPrompt": "Always prompt before changes are committed to a protected branch.",
"config.branchRandomNameDictionary": "List of dictionaries used for the randomly generated branch name. Each value represents the dictionary used to generate the segment of the branch name. Supported dictionaries: `adjectives`, `animals`, `colors` and `numbers`.",
"config.branchRandomNameDictionary.adjectives": "A random adjective",
"config.branchRandomNameDictionary.animals": "A random animal name",
"config.branchRandomNameDictionary.colors": "A random color name",
"config.branchRandomNameDictionary.numbers": "A random number between 100 and 999",
"config.branchRandomNameEnable": "Controls whether a random name is generated when creating a new branch.",
"config.branchValidationRegex": "A regular expression to validate new branch names.",
"config.branchWhitespaceChar": "The character to replace whitespace in new branch names, and to separate segments of a randomly generated branch name.",
"config.ignoreLegacyWarning": "Ignores the legacy Git warning.",
"config.ignoreMissingGitWarning": "Ignores the warning when Git is missing.",
"config.ignoreWindowsGit27Warning": "Ignores the warning when Git 2.25 - 2.26 is installed on Windows.",
"config.ignoreLimitWarning": "Ignores the warning when there are too many changes in a repository.",
"config.ignoreRebaseWarning": "Ignores the warning when it looks like the branch might have been rebased when pulling.",
"config.defaultCloneDirectory": "The default location to clone a Git repository.",
"config.useEditorAsCommitInput": "Controls whether a full text editor will be used to author commit messages, whenever no message is provided in the commit input box.",
"config.verboseCommit": "Enable verbose output when `#git.useEditorAsCommitInput#` is enabled.",
"config.enableSmartCommit": "Commit all changes when there are no staged changes.",
"config.smartCommitChanges": "Control which changes are automatically staged by Smart Commit.",
"config.smartCommitChanges.all": "Automatically stage all changes.",
"config.smartCommitChanges.tracked": "Automatically stage tracked changes only.",
"config.suggestSmartCommit": "Suggests to enable smart commit (commit all changes when there are no staged changes).",
"config.enableCommitSigning": "Enables commit signing with GPG, X.509, or SSH.",
"config.discardAllScope": "Controls what changes are discarded by the `Discard all changes` command. `all` discards all changes. `tracked` discards only tracked files. `prompt` shows a prompt dialog every time the action is run.",
"config.decorations.enabled": "Controls whether Git contributes colors and badges to the Explorer and the Open Editors view.",
"config.enableStatusBarSync": "Controls whether the Git Sync command appears in the status bar.",
"config.followTagsWhenSync": "Push all annotated tags when running the sync command.",
"config.replaceTagsWhenPull": "Automatically replace the local tags with the remote tags in case of a conflict when running the pull command.",
"config.promptToSaveFilesBeforeStash": "Controls whether Git should check for unsaved files before stashing changes.",
"config.promptToSaveFilesBeforeStash.always": "Check for any unsaved files.",
"config.promptToSaveFilesBeforeStash.staged": "Check only for unsaved staged files.",
"config.promptToSaveFilesBeforeStash.never": "Disable this check.",
"config.promptToSaveFilesBeforeCommit": "Controls whether Git should check for unsaved files before committing.",
"config.promptToSaveFilesBeforeCommit.always": "Check for any unsaved files.",
"config.promptToSaveFilesBeforeCommit.staged": "Check only for unsaved staged files.",
"config.promptToSaveFilesBeforeCommit.never": "Disable this check.",
"config.postCommitCommand": "Run a git command after a successful commit.",
"config.postCommitCommand.none": "Don't run any command after a commit.",
"config.postCommitCommand.push": "Run 'git push' after a successful commit.",
"config.postCommitCommand.sync": "Run 'git pull' and 'git push' after a successful commit.",
"config.rememberPostCommitCommand": "Remember the last git command that ran after a commit.",
"config.openAfterClone": "Controls whether to open a repository automatically after cloning.",
"config.openAfterClone.always": "Always open in current window.",
"config.openAfterClone.alwaysNewWindow": "Always open in a new window.",
"config.openAfterClone.whenNoFolderOpen": "Only open in current window when no folder is opened.",
"config.openAfterClone.prompt": "Always prompt for action.",
"config.showInlineOpenFileAction": "Controls whether to show an inline Open File action in the Git changes view.",
"config.showPushSuccessNotification": "Controls whether to show a notification when a push is successful.",
"config.inputValidation": "Controls whether to show commit message input validation diagnostics.",
"config.inputValidationLength": "Controls the commit message length threshold for showing a warning.",
"config.inputValidationSubjectLength": "Controls the commit message subject length threshold for showing a warning. Unset it to inherit the value of `#git.inputValidationLength#`.",
"config.detectSubmodules": "Controls whether to automatically detect Git submodules.",
"config.detectSubmodulesLimit": "Controls the limit of Git submodules detected.",
"config.alwaysShowStagedChangesResourceGroup": "Always show the Staged Changes resource group.",
"config.alwaysSignOff": "Controls the signoff flag for all commits.",
"config.ignoreSubmodules": "Ignore modifications to submodules in the file tree.",
"config.ignoredRepositories": "List of Git repositories to ignore.",
"config.scanRepositories": "List of paths to search for Git repositories in.",
"config.commandsToLog": {
"message": "List of git commands (ex: commit, push) that would have their `stdout` logged to the [git output](command:git.showOutput). If the git command has a client-side hook configured, the client-side hook's `stdout` will also be logged to the [git output](command:git.showOutput).",
"comment": [
"{Locked='](command:git.showOutput'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"config.showProgress": "Controls whether Git actions should show progress.",
"config.rebaseWhenSync": "Force Git to use rebase when running the sync command.",
"config.confirmEmptyCommits": "Always confirm the creation of empty commits for the 'Git: Commit Empty' command.",
"config.fetchOnPull": "When enabled, fetch all branches when pulling. Otherwise, fetch just the current one.",
"config.pullBeforeCheckout": "Controls whether a branch that does not have outgoing commits is fast-forwarded before it is checked out.",
"config.pullTags": "Fetch all tags when pulling.",
"config.pruneOnFetch": "Prune when fetching.",
"config.autoStash": "Stash any changes before pulling and restore them after successful pull.",
"config.allowForcePush": "Controls whether force push (with or without lease) is enabled.",
"config.useForcePushWithLease": "Controls whether force pushing uses the safer force-with-lease variant.",
"config.useForcePushIfIncludes": "Controls whether force pushing uses the safer force-if-includes variant. Note: This setting requires the `#git.useForcePushWithLease#` setting to be enabled, and Git version `2.30.0` or later.",
"config.confirmForcePush": "Controls whether to ask for confirmation before force-pushing.",
"config.allowNoVerifyCommit": "Controls whether commits without running pre-commit and commit-msg hooks are allowed.",
"config.confirmNoVerifyCommit": "Controls whether to ask for confirmation before committing without verification.",
"config.closeDiffOnOperation": "Controls whether the diff editor should be automatically closed when changes are stashed, committed, discarded, staged, or unstaged.",
"config.openDiffOnClick": "Controls whether the diff editor should be opened when clicking a change. Otherwise the regular editor will be opened.",
"config.supportCancellation": "Controls whether a notification comes up when running the Sync action, which allows the user to cancel the operation.",
"config.branchSortOrder": "Controls the sort order for branches.",
"config.untrackedChanges": "Controls how untracked changes behave.",
"config.untrackedChanges.mixed": "All changes, tracked and untracked, appear together and behave equally.",
"config.untrackedChanges.separate": "Untracked changes appear separately in the Source Control view. They are also excluded from several actions.",
"config.untrackedChanges.hidden": "Untracked changes are hidden and excluded from several actions.",
"config.requireGitUserConfig": "Controls whether to require explicit Git user configuration or allow Git to guess if missing.",
"config.showCommitInput": "Controls whether to show the commit input in the Git source control panel.",
"config.terminalAuthentication": "Controls whether to enable VS Code to be the authentication handler for Git processes spawned in the Integrated Terminal. Note: Terminals need to be restarted to pick up a change in this setting.",
"config.terminalGitEditor": "Controls whether to enable VS Code to be the Git editor for Git processes spawned in the integrated terminal. Note: Terminals need to be restarted to pick up a change in this setting.",
"config.timeline.showAuthor": "Controls whether to show the commit author in the Timeline view.",
"config.timeline.showUncommitted": "Controls whether to show uncommitted changes in the Timeline view.",
"config.timeline.date": "Controls which date to use for items in the Timeline view.",
"config.timeline.date.committed": "Use the committed date",
"config.timeline.date.authored": "Use the authored date",
"config.useCommitInputAsStashMessage": "Controls whether to use the message from the commit input box as the default stash message.",
"config.showActionButton": "Controls whether an action button is shown in the Source Control view.",
"config.showActionButton.commit": "Show an action button to commit changes when the local branch has modified files ready to be committed.",
"config.showActionButton.publish": "Show an action button to publish the local branch when it does not have a tracking remote branch.",
"config.showActionButton.sync": "Show an action button to synchronize changes when the local branch is either ahead or behind the remote branch.",
"config.statusLimit": "Controls how to limit the number of changes that can be parsed from Git status command. Can be set to 0 for no limit.",
"config.experimental.installGuide": "Experimental improvements for the Git setup flow.",
"config.repositoryScanIgnoredFolders": "List of folders that are ignored while scanning for Git repositories when `#git.autoRepositoryDetection#` is set to `true` or `subFolders`.",
"config.repositoryScanMaxDepth": "Controls the depth used when scanning workspace folders for Git repositories when `#git.autoRepositoryDetection#` is set to `true` or `subFolders`. Can be set to `-1` for no limit.",
"config.useIntegratedAskPass": "Controls whether GIT_ASKPASS should be overwritten to use the integrated version.",
"config.mergeEditor": "Open the merge editor for files that are currently under conflict.",
"config.optimisticUpdate": "Controls whether to optimistically update the state of the Source Control view after running git commands.",
"config.openRepositoryInParentFolders": "Control whether a repository in parent folders of workspaces or open files should be opened.",
"config.openRepositoryInParentFolders.always": "Always open a repository in parent folders of workspaces or open files.",
"config.openRepositoryInParentFolders.never": "Never open a repository in parent folders of workspaces or open files.",
"config.openRepositoryInParentFolders.prompt": "Prompt before opening a repository the parent folders of workspaces or open files.",
"config.publishBeforeContinueOn": "Controls whether to publish unpublished Git state when using Continue Working On from a Git repository.",
"config.publishBeforeContinueOn.always": "Always publish unpublished Git state when using Continue Working On from a Git repository",
"config.publishBeforeContinueOn.never": "Never publish unpublished Git state when using Continue Working On from a Git repository",
"config.publishBeforeContinueOn.prompt": "Prompt to publish unpublished Git state when using Continue Working On from a Git repository",
"config.similarityThreshold": "Controls the threshold of the similarity index (the amount of additions/deletions compared to the file's size) for changes in a pair of added/deleted files to be considered a rename. **Note:** Requires Git version `2.18.0` or later.",
"config.blameEditorDecoration.enabled": "Controls whether to show blame information in the editor using editor decorations.",
"config.blameEditorDecoration.template": "Template for the blame information editor decoration. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First N characters of the commit hash according to `#git.commitShortHashLength#`\n\n* `subject`: First line of the commit message\n\n* `authorName`: Author name\n\n* `authorEmail`: Author email\n\n* `authorDate`: Author date\n\n* `authorDateAgo`: Time difference between now and the author date\n\n",
"config.blameStatusBarItem.enabled": "Controls whether to show blame information in the status bar.",
"config.blameStatusBarItem.template": "Template for the blame information status bar item. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First N characters of the commit hash according to `#git.commitShortHashLength#`\n\n* `subject`: First line of the commit message\n\n* `authorName`: Author name\n\n* `authorEmail`: Author email\n\n* `authorDate`: Author date\n\n* `authorDateAgo`: Time difference between now and the author date\n\n",
"config.commitShortHashLength": "Controls the length of the commit short hash.",
"config.diagnosticsCommitHook.Enabled": "Controls whether to check for unresolved diagnostics before committing.",
"config.diagnosticsCommitHook.Sources": "Controls the list of sources (**Item**) and the minimum severity (**Value**) to be considered before committing. **Note:** To ignore diagnostics from a particular source, add the source to the list and set the minimum severity to `none`.",
"config.discardUntrackedChangesToTrash": "Controls whether discarding untracked changes moves the file(s) to the Recycle Bin (Windows), Trash (macOS, Linux) instead of deleting them permanently. **Note:** This setting has no effect when connected to a remote or when running in Linux as a snap package.",
"config.showReferenceDetails": "Controls whether to show the details of the last commit for Git refs in the checkout, branch, and tag pickers.",
"submenu.explorer": "Git",
"submenu.commit": "Commit",
"submenu.commit.amend": "Amend",
"submenu.commit.signoff": "Sign Off",
"submenu.changes": "Changes",
"submenu.pullpush": "Pull, Push",
"submenu.branch": "Branch",
"submenu.remotes": "Remote",
"submenu.stash": "Stash",
"submenu.tags": "Tags",
"colors.added": "Color for added resources.",
"colors.modified": "Color for modified resources.",
"colors.stageModified": "Color for modified resources which have been staged.",
"colors.stageDeleted": "Color for deleted resources which have been staged.",
"colors.deleted": "Color for deleted resources.",
"colors.renamed": "Color for renamed or copied resources.",
"colors.untracked": "Color for untracked resources.",
"colors.ignored": "Color for ignored resources.",
"colors.conflict": "Color for resources with conflicts.",
"colors.submodule": "Color for submodule resources.",
"colors.incomingAdded": "Color for added incoming resource.",
"colors.incomingDeleted": "Color for deleted incoming resource.",
"colors.incomingRenamed": "Color for renamed incoming resource.",
"colors.incomingModified": "Color for modified incoming resource.",
"colors.blameEditorDecoration": "Color for the blame editor decoration.",
"view.workbench.scm.missing.windows": {
"message": "[Download Git for Windows](https://git-scm.com/download/win)\nAfter installing, please [reload](command:workbench.action.reloadWindow) (or [troubleshoot](command:git.showOutput)). Additional source control providers can be installed [from the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22).",
"comment": [
"{Locked='](command:workbench.action.reloadWindow'}",
"{Locked='](command:git.showOutput'}",
"{Locked='](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.missing.mac": {
"message": "[Download Git for macOS](https://git-scm.com/download/mac)\nAfter installing, please [reload](command:workbench.action.reloadWindow) (or [troubleshoot](command:git.showOutput)). Additional source control providers can be installed [from the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22).",
"comment": [
"{Locked='](command:workbench.action.reloadWindow'}",
"{Locked='](command:git.showOutput'}",
"{Locked='](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.missing.linux": {
"message": "Source control depends on Git being installed.\n[Download Git for Linux](https://git-scm.com/download/linux)\nAfter installing, please [reload](command:workbench.action.reloadWindow) (or [troubleshoot](command:git.showOutput)). Additional source control providers can be installed [from the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22).",
"comment": [
"{Locked='](command:workbench.action.reloadWindow'}",
"{Locked='](command:git.showOutput'}",
"{Locked='](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.missing": {
"message": "Install Git, a popular source control system, to track code changes and collaborate with others. Learn more in our [Git guides](https://aka.ms/vscode-scm).",
"comment": [
"{Locked='](https://aka.ms/vscode-scm'}",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.disabled": {
"message": "If you would like to use Git features, please enable Git in your [settings](command:workbench.action.openSettings?%5B%22git.enabled%22%5D).\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).",
"comment": [
"{Locked='](command:workbench.action.openSettings?%5B%22git.enabled%22%5D'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.empty": {
"message": "In order to use Git features, you can open a folder containing a Git repository or clone from a URL.\n[Open Folder](command:vscode.openFolder)\n[Clone Repository](command:git.cloneRecursive)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).",
"comment": [
"{Locked='](command:vscode.openFolder'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.folder": {
"message": "The folder currently open doesn't have a Git repository. You can initialize a repository which will enable source control features powered by Git.\n[Initialize Repository](command:git.init?%5Btrue%5D)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).",
"comment": [
"{Locked='](command:git.init?%5Btrue%5D'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.workspace": {
"message": "The workspace currently open doesn't have any folders containing Git repositories. You can initialize a repository on a folder which will enable source control features powered by Git.\n[Initialize Repository](command:git.init)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).",
"comment": [
"{Locked='](command:git.init'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.emptyWorkspace": {
"message": "The workspace currently open doesn't have any folders containing Git repositories.\n[Add Folder to Workspace](command:workbench.action.addRootFolder)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).",
"comment": [
"{Locked='](command:workbench.action.addRootFolder'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.scanFolderForRepositories": {
"message": "Scanning folder for Git repositories..."
},
"view.workbench.scm.scanWorkspaceForRepositories": {
"message": "Scanning workspace for Git repositories..."
},
"view.workbench.scm.repositoryInParentFolders": {
"message": "A Git repository was found in the parent folders of the workspace or the open file(s).\n[Open Repository](command:git.openRepositoriesInParentFolders)\nUse the [git.openRepositoryInParentFolders](command:workbench.action.openSettings?%5B%22git.openRepositoryInParentFolders%22%5D) setting to control whether Git repositories in parent folders of workspaces or open files are opened. To learn more [read our docs](https://aka.ms/vscode-git-repository-in-parent-folders).",
"comment": [
"{Locked='](command:git.openRepositoriesInParentFolders'}",
"{Locked='](command:workbench.action.openSettings?%5B%22git.openRepositoryInParentFolders%22%5D'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.repositoriesInParentFolders": {
"message": "Git repositories were found in the parent folders of the workspace or the open file(s).\n[Open Repository](command:git.openRepositoriesInParentFolders)\nUse the [git.openRepositoryInParentFolders](command:workbench.action.openSettings?%5B%22git.openRepositoryInParentFolders%22%5D) setting to control whether Git repositories in parent folders of workspace or open files are opened. To learn more [read our docs](https://aka.ms/vscode-git-repository-in-parent-folders).",
"comment": [
"{Locked='](command:git.openRepositoriesInParentFolders'}",
"{Locked='](command:workbench.action.openSettings?%5B%22git.openRepositoryInParentFolders%22%5D'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.unsafeRepository": {
"message": "The detected Git repository is potentially unsafe as the folder is owned by someone other than the current user.\n[Manage Unsafe Repositories](command:git.manageUnsafeRepositories)\nTo learn more about unsafe repositories [read our docs](https://aka.ms/vscode-git-unsafe-repository).",
"comment": [
"{Locked='](command:git.manageUnsafeRepositories'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.unsafeRepositories": {
"message": "The detected Git repositories are potentially unsafe as the folders are owned by someone other than the current user.\n[Manage Unsafe Repositories](command:git.manageUnsafeRepositories)\nTo learn more about unsafe repositories [read our docs](https://aka.ms/vscode-git-unsafe-repository).",
"comment": [
"{Locked='](command:git.manageUnsafeRepositories'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.closedRepository": {
"message": "A Git repository was found that was previously closed.\n[Reopen Closed Repository](command:git.reopenClosedRepositories)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).",
"comment": [
"{Locked='](command:git.reopenClosedRepositories'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.closedRepositories": {
"message": "Git repositories were found that were previously closed.\n[Reopen Closed Repositories](command:git.reopenClosedRepositories)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).",
"comment": [
"{Locked='](command:git.reopenClosedRepositories'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.cloneRepository": {
"message": "You can clone a repository locally.\n[Clone Repository](command:git.clone 'Clone a repository once the Git extension has activated')",
"comment": [
"{Locked='](command:git.clone'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.learnMore": "To learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm)."
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#3c8746" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
A
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#7F4E7E" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
C
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#692C77" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
C
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#9E121D" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
D
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#969696" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
I
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#1B80B2" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
M
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#CC6633" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
R
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#B21B8F" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
T
</text>
</svg>

After

Width:  |  Height:  |  Size: 432 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#6C6C6C" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
U
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#2d883e" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
A
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#9B4F96" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
C
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#682079" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
C
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#B9131A" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
D
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#969696" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
I
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#007ACC" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
M
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#CC6633" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
R
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#B21B8F" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
T
</text>
</svg>

After

Width:  |  Height:  |  Size: 432 B

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#6C6C6C" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
U
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

+321
View File
@@ -0,0 +1,321 @@
/*---------------------------------------------------------------------------------------------
* 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, EventEmitter, SourceControlActionButton, Uri, workspace, l10n, LogOutputChannel } from 'vscode';
import { Branch, RefType, Status } from './api/git';
import { OperationKind } from './operation';
import { CommitCommandsCenter } from './postCommitCommands';
import { Repository } from './repository';
import { dispose } from './util';
function isActionButtonStateEqual(state1: ActionButtonState, state2: ActionButtonState): boolean {
return state1.HEAD?.name === state2.HEAD?.name &&
state1.HEAD?.commit === state2.HEAD?.commit &&
state1.HEAD?.remote === state2.HEAD?.remote &&
state1.HEAD?.type === state2.HEAD?.type &&
state1.HEAD?.ahead === state2.HEAD?.ahead &&
state1.HEAD?.behind === state2.HEAD?.behind &&
state1.HEAD?.upstream?.name === state2.HEAD?.upstream?.name &&
state1.HEAD?.upstream?.remote === state2.HEAD?.upstream?.remote &&
state1.HEAD?.upstream?.commit === state2.HEAD?.upstream?.commit &&
state1.isCheckoutInProgress === state2.isCheckoutInProgress &&
state1.isCommitInProgress === state2.isCommitInProgress &&
state1.isMergeInProgress === state2.isMergeInProgress &&
state1.isRebaseInProgress === state2.isRebaseInProgress &&
state1.isSyncInProgress === state2.isSyncInProgress &&
state1.repositoryHasChangesToCommit === state2.repositoryHasChangesToCommit &&
state1.repositoryHasUnresolvedConflicts === state2.repositoryHasUnresolvedConflicts;
}
interface ActionButtonState {
readonly HEAD: Branch | undefined;
readonly isCheckoutInProgress: boolean;
readonly isCommitInProgress: boolean;
readonly isMergeInProgress: boolean;
readonly isRebaseInProgress: boolean;
readonly isSyncInProgress: boolean;
readonly repositoryHasChangesToCommit: boolean;
readonly repositoryHasUnresolvedConflicts: boolean;
}
export class ActionButton {
private _onDidChange = new EventEmitter<void>();
get onDidChange(): Event<void> { return this._onDidChange.event; }
private _state: ActionButtonState;
private get state() { return this._state; }
private set state(state: ActionButtonState) {
if (isActionButtonStateEqual(this._state, state)) {
return;
}
this.logger.trace(`[ActionButton][setState] ${JSON.stringify(state)}`);
this._state = state;
this._onDidChange.fire();
}
private disposables: Disposable[] = [];
constructor(
private readonly repository: Repository,
private readonly postCommitCommandCenter: CommitCommandsCenter,
private readonly logger: LogOutputChannel) {
this._state = {
HEAD: undefined,
isCheckoutInProgress: false,
isCommitInProgress: false,
isMergeInProgress: false,
isRebaseInProgress: false,
isSyncInProgress: false,
repositoryHasChangesToCommit: false,
repositoryHasUnresolvedConflicts: false
};
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
this.disposables.push(repository.onDidChangeBranchProtection(() => this._onDidChange.fire()));
this.disposables.push(postCommitCommandCenter.onDidChange(() => this._onDidChange.fire()));
const root = Uri.file(repository.root);
this.disposables.push(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git.enableSmartCommit', root) ||
e.affectsConfiguration('git.smartCommitChanges', root) ||
e.affectsConfiguration('git.suggestSmartCommit', root)) {
this.onDidChangeSmartCommitSettings();
}
if (e.affectsConfiguration('git.branchProtectionPrompt', root) ||
e.affectsConfiguration('git.postCommitCommand', root) ||
e.affectsConfiguration('git.rememberPostCommitCommand', root) ||
e.affectsConfiguration('git.showActionButton', root)) {
this._onDidChange.fire();
}
}));
}
get button(): SourceControlActionButton | undefined {
if (!this.state.HEAD) { return undefined; }
let actionButton: SourceControlActionButton | undefined;
if (this.state.repositoryHasChangesToCommit) {
// Commit Changes (enabled)
actionButton = this.getCommitActionButton();
}
// Commit Changes (enabled) -> Publish Branch -> Sync Changes -> Commit Changes (disabled)
actionButton = actionButton ?? this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton() ?? this.getCommitActionButton();
this.logger.trace(`[ActionButton][getButton] ${JSON.stringify({
command: actionButton?.command.command,
title: actionButton?.command.title,
enabled: actionButton?.enabled
})}`);
return actionButton;
}
private getCommitActionButton(): SourceControlActionButton | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ commit: boolean }>('showActionButton', { commit: true });
// The button is disabled
if (!showActionButton.commit) { return undefined; }
const primaryCommand = this.getCommitActionButtonPrimaryCommand();
return {
command: primaryCommand,
secondaryCommands: this.getCommitActionButtonSecondaryCommands(),
enabled: (
this.state.repositoryHasChangesToCommit ||
(this.state.isRebaseInProgress && !this.state.repositoryHasUnresolvedConflicts) ||
(this.state.isMergeInProgress && !this.state.repositoryHasUnresolvedConflicts)) &&
!this.state.isCommitInProgress
};
}
private getCommitActionButtonPrimaryCommand(): Command {
// Rebase Continue
if (this.state.isRebaseInProgress) {
return {
command: 'git.commit',
title: l10n.t('{0} Continue', '$(check)'),
tooltip: this.state.isCommitInProgress ? l10n.t('Continuing Rebase...') : l10n.t('Continue Rebase'),
arguments: [this.repository.sourceControl, null]
};
}
// Merge Continue
if (this.state.isMergeInProgress) {
return {
command: 'git.commit',
title: l10n.t('{0} Continue', '$(check)'),
tooltip: this.state.isCommitInProgress ? l10n.t('Continuing Merge...') : l10n.t('Continue Merge'),
arguments: [this.repository.sourceControl, null]
};
}
// Not a branch (tag, detached)
if (this.state.HEAD?.type === RefType.Tag || !this.state.HEAD?.name) {
return {
command: 'git.commit',
title: l10n.t('{0} Commit', '$(check)'),
tooltip: this.state.isCommitInProgress ? l10n.t('Committing Changes...') : l10n.t('Commit Changes'),
arguments: [this.repository.sourceControl, null]
};
}
// Commit
return this.postCommitCommandCenter.getPrimaryCommand();
}
private getCommitActionButtonSecondaryCommands(): Command[][] {
// Rebase Continue
if (this.state.isRebaseInProgress) {
return [];
}
// Merge Continue
if (this.state.isMergeInProgress) {
return [];
}
// Not a branch (tag, detached)
if (this.state.HEAD?.type === RefType.Tag || !this.state.HEAD?.name) {
return [];
}
// Commit
const commandGroups: Command[][] = [];
for (const commands of this.postCommitCommandCenter.getSecondaryCommands()) {
commandGroups.push(commands.map(c => {
return { command: c.command, title: c.title, tooltip: c.tooltip, arguments: c.arguments };
}));
}
return commandGroups;
}
private getPublishBranchActionButton(): SourceControlActionButton | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ publish: boolean }>('showActionButton', { publish: true });
// Not a branch (tag, detached), branch does have an upstream, commit/merge/rebase is in progress, or the button is disabled
if (this.state.HEAD?.type === RefType.Tag || !this.state.HEAD?.name || this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.publish) { return undefined; }
// Button icon
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(cloud-upload)';
return {
command: {
command: 'git.publish',
title: l10n.t({ message: '{0} Publish Branch', args: [icon], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }),
tooltip: this.state.isSyncInProgress ?
(this.state.HEAD?.name ?
l10n.t({ message: 'Publishing Branch "{0}"...', args: [this.state.HEAD.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
l10n.t({ message: 'Publishing Branch...', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })) :
(this.repository.HEAD?.name ?
l10n.t({ message: 'Publish Branch "{0}"', args: [this.state.HEAD?.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
l10n.t({ message: 'Publish Branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })),
arguments: [this.repository.sourceControl],
},
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress
};
}
private getSyncChangesActionButton(): SourceControlActionButton | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ sync: boolean }>('showActionButton', { sync: true });
const branchIsAheadOrBehind = (this.state.HEAD?.behind ?? 0) > 0 || (this.state.HEAD?.ahead ?? 0) > 0;
// Branch does not have an upstream, branch is not ahead/behind the remote branch, commit/merge/rebase is in progress, or the button is disabled
if (!this.state.HEAD?.upstream || !branchIsAheadOrBehind || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.sync) { return undefined; }
const ahead = this.state.HEAD.ahead ? ` ${this.state.HEAD.ahead}$(arrow-up)` : '';
const behind = this.state.HEAD.behind ? ` ${this.state.HEAD.behind}$(arrow-down)` : '';
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(sync)';
return {
command: {
command: 'git.sync',
title: l10n.t('{0} Sync Changes{1}{2}', icon, behind, ahead),
shortTitle: `${icon}${behind}${ahead}`,
tooltip: this.state.isSyncInProgress ?
l10n.t('Synchronizing Changes...')
: this.repository.syncTooltip,
arguments: [this.repository.sourceControl],
},
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress
};
}
private onDidChangeOperations(): void {
const isCheckoutInProgress
= this.repository.operations.isRunning(OperationKind.Checkout) ||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
const isCommitInProgress =
this.repository.operations.isRunning(OperationKind.Commit) ||
this.repository.operations.isRunning(OperationKind.PostCommitCommand) ||
this.repository.operations.isRunning(OperationKind.RebaseContinue);
const isSyncInProgress =
this.repository.operations.isRunning(OperationKind.Sync) ||
this.repository.operations.isRunning(OperationKind.Push) ||
this.repository.operations.isRunning(OperationKind.Pull);
this.state = { ...this.state, isCheckoutInProgress, isCommitInProgress, isSyncInProgress };
}
private onDidChangeSmartCommitSettings(): void {
this.state = {
...this.state,
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit()
};
}
private onDidRunGitStatus(): void {
this.state = {
...this.state,
HEAD: this.repository.HEAD,
isMergeInProgress: this.repository.mergeInProgress,
isRebaseInProgress: !!this.repository.rebaseCommit,
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit(),
repositoryHasUnresolvedConflicts: this.repository.mergeGroup.resourceStates.length > 0
};
}
private repositoryHasChangesToCommit(): boolean {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges', 'all');
const resources = [...this.repository.indexGroup.resourceStates];
if (
// Smart commit enabled (all)
(enableSmartCommit && smartCommitChanges === 'all') ||
// Smart commit disabled, smart suggestion enabled
(!enableSmartCommit && suggestSmartCommit)
) {
resources.push(...this.repository.workingTreeGroup.resourceStates);
}
// Smart commit enabled (tracked only)
if (enableSmartCommit && smartCommitChanges === 'tracked') {
resources.push(...this.repository.workingTreeGroup.resourceStates.filter(r => r.type !== Status.UNTRACKED));
}
return resources.length !== 0;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}
+505
View File
@@ -0,0 +1,505 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable local/code-no-native-private */
import { Model } from '../model';
import { Repository as BaseRepository, Resource } from '../repository';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider } from './git';
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode';
import { combinedDisposable, filterEvent, mapEvent } from '../util';
import { toGitUri } from '../uri';
import { GitExtensionImpl } from './extension';
import { GitBaseApi } from '../git-base';
import { PickRemoteSourceOptions } from '../typings/git-base';
import { OperationKind, OperationResult } from '../operation';
class ApiInputBox implements InputBox {
#inputBox: SourceControlInputBox;
constructor(inputBox: SourceControlInputBox) { this.#inputBox = inputBox; }
set value(value: string) { this.#inputBox.value = value; }
get value(): string { return this.#inputBox.value; }
}
export class ApiChange implements Change {
#resource: Resource;
constructor(resource: Resource) { this.#resource = resource; }
get uri(): Uri { return this.#resource.resourceUri; }
get originalUri(): Uri { return this.#resource.original; }
get renameUri(): Uri | undefined { return this.#resource.renameResourceUri; }
get status(): Status { return this.#resource.type; }
}
export class ApiRepositoryState implements RepositoryState {
#repository: BaseRepository;
readonly onDidChange: Event<void>;
constructor(repository: BaseRepository) {
this.#repository = repository;
this.onDidChange = this.#repository.onDidRunGitStatus;
}
get HEAD(): Branch | undefined { return this.#repository.HEAD; }
/**
* @deprecated Use ApiRepository.getRefs() instead.
*/
get refs(): Ref[] { console.warn('Deprecated. Use ApiRepository.getRefs() instead.'); return []; }
get remotes(): Remote[] { return [...this.#repository.remotes]; }
get submodules(): Submodule[] { return [...this.#repository.submodules]; }
get rebaseCommit(): Commit | undefined { return this.#repository.rebaseCommit; }
get mergeChanges(): Change[] { return this.#repository.mergeGroup.resourceStates.map(r => new ApiChange(r)); }
get indexChanges(): Change[] { return this.#repository.indexGroup.resourceStates.map(r => new ApiChange(r)); }
get workingTreeChanges(): Change[] { return this.#repository.workingTreeGroup.resourceStates.map(r => new ApiChange(r)); }
get untrackedChanges(): Change[] { return this.#repository.untrackedGroup.resourceStates.map(r => new ApiChange(r)); }
}
export class ApiRepositoryUIState implements RepositoryUIState {
#sourceControl: SourceControl;
readonly onDidChange: Event<void>;
constructor(sourceControl: SourceControl) {
this.#sourceControl = sourceControl;
this.onDidChange = mapEvent<boolean, void>(this.#sourceControl.onDidChangeSelection, () => null);
}
get selected(): boolean { return this.#sourceControl.selected; }
}
export class ApiRepository implements Repository {
#repository: BaseRepository;
readonly rootUri: Uri;
readonly inputBox: InputBox;
readonly state: RepositoryState;
readonly ui: RepositoryUIState;
readonly onDidCommit: Event<void>;
readonly onDidCheckout: Event<void>;
constructor(repository: BaseRepository) {
this.#repository = repository;
this.rootUri = Uri.file(this.#repository.root);
this.inputBox = new ApiInputBox(this.#repository.inputBox);
this.state = new ApiRepositoryState(this.#repository);
this.ui = new ApiRepositoryUIState(this.#repository.sourceControl);
this.onDidCommit = mapEvent<OperationResult, void>(
filterEvent(this.#repository.onDidRunOperation, e => e.operation.kind === OperationKind.Commit), () => null);
this.onDidCheckout = mapEvent<OperationResult, void>(
filterEvent(this.#repository.onDidRunOperation, e => e.operation.kind === OperationKind.Checkout || e.operation.kind === OperationKind.CheckoutTracking), () => null);
}
apply(patch: string, reverse?: boolean): Promise<void> {
return this.#repository.apply(patch, reverse);
}
getConfigs(): Promise<{ key: string; value: string }[]> {
return this.#repository.getConfigs();
}
getConfig(key: string): Promise<string> {
return this.#repository.getConfig(key);
}
setConfig(key: string, value: string): Promise<string> {
return this.#repository.setConfig(key, value);
}
unsetConfig(key: string): Promise<string> {
return this.#repository.unsetConfig(key);
}
getGlobalConfig(key: string): Promise<string> {
return this.#repository.getGlobalConfig(key);
}
getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number }> {
return this.#repository.getObjectDetails(treeish, path);
}
detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }> {
return this.#repository.detectObjectType(object);
}
buffer(ref: string, filePath: string): Promise<Buffer> {
return this.#repository.buffer(ref, filePath);
}
show(ref: string, path: string): Promise<string> {
return this.#repository.show(ref, path);
}
getCommit(ref: string): Promise<Commit> {
return this.#repository.getCommit(ref);
}
add(paths: string[]) {
return this.#repository.add(paths.map(p => Uri.file(p)));
}
revert(paths: string[]) {
return this.#repository.revert(paths.map(p => Uri.file(p)));
}
clean(paths: string[]) {
return this.#repository.clean(paths.map(p => Uri.file(p)));
}
diff(cached?: boolean) {
return this.#repository.diff(cached);
}
diffWithHEAD(): Promise<Change[]>;
diffWithHEAD(path: string): Promise<string>;
diffWithHEAD(path?: string): Promise<string | Change[]> {
return this.#repository.diffWithHEAD(path);
}
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffWith(ref: string, path?: string): Promise<string | Change[]> {
return this.#repository.diffWith(ref, path);
}
diffIndexWithHEAD(): Promise<Change[]>;
diffIndexWithHEAD(path: string): Promise<string>;
diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
return this.#repository.diffIndexWithHEAD(path);
}
diffIndexWith(ref: string): Promise<Change[]>;
diffIndexWith(ref: string, path: string): Promise<string>;
diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
return this.#repository.diffIndexWith(ref, path);
}
diffBlobs(object1: string, object2: string): Promise<string> {
return this.#repository.diffBlobs(object1, object2);
}
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
return this.#repository.diffBetween(ref1, ref2, path);
}
hashObject(data: string): Promise<string> {
return this.#repository.hashObject(data);
}
createBranch(name: string, checkout: boolean, ref?: string | undefined): Promise<void> {
return this.#repository.branch(name, checkout, ref);
}
deleteBranch(name: string, force?: boolean): Promise<void> {
return this.#repository.deleteBranch(name, force);
}
getBranch(name: string): Promise<Branch> {
return this.#repository.getBranch(name);
}
getBranches(query: BranchQuery, cancellationToken?: CancellationToken): Promise<Ref[]> {
return this.#repository.getBranches(query, cancellationToken);
}
getBranchBase(name: string): Promise<Branch | undefined> {
return this.#repository.getBranchBase(name);
}
setBranchUpstream(name: string, upstream: string): Promise<void> {
return this.#repository.setBranchUpstream(name, upstream);
}
getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]> {
return this.#repository.getRefs(query, cancellationToken);
}
checkIgnore(paths: string[]): Promise<Set<string>> {
return this.#repository.checkIgnore(paths);
}
getMergeBase(ref1: string, ref2: string): Promise<string | undefined> {
return this.#repository.getMergeBase(ref1, ref2);
}
tag(name: string, message: string, ref?: string | undefined): Promise<void> {
return this.#repository.tag({ name, message, ref });
}
deleteTag(name: string): Promise<void> {
return this.#repository.deleteTag(name);
}
status(): Promise<void> {
return this.#repository.status();
}
checkout(treeish: string): Promise<void> {
return this.#repository.checkout(treeish);
}
addRemote(name: string, url: string): Promise<void> {
return this.#repository.addRemote(name, url);
}
removeRemote(name: string): Promise<void> {
return this.#repository.removeRemote(name);
}
renameRemote(name: string, newName: string): Promise<void> {
return this.#repository.renameRemote(name, newName);
}
fetch(arg0?: FetchOptions | string | undefined,
ref?: string | undefined,
depth?: number | undefined,
prune?: boolean | undefined
): Promise<void> {
if (arg0 !== undefined && typeof arg0 !== 'string') {
return this.#repository.fetch(arg0);
}
return this.#repository.fetch({ remote: arg0, ref, depth, prune });
}
pull(unshallow?: boolean): Promise<void> {
return this.#repository.pull(undefined, unshallow);
}
push(remoteName?: string, branchName?: string, setUpstream: boolean = false, force?: ForcePushMode): Promise<void> {
return this.#repository.pushTo(remoteName, branchName, setUpstream, force);
}
blame(path: string): Promise<string> {
return this.#repository.blame(path);
}
log(options?: LogOptions): Promise<Commit[]> {
return this.#repository.log(options);
}
commit(message: string, opts?: CommitOptions): Promise<void> {
return this.#repository.commit(message, { ...opts, postCommitCommand: null });
}
merge(ref: string): Promise<void> {
return this.#repository.merge(ref);
}
mergeAbort(): Promise<void> {
return this.#repository.mergeAbort();
}
applyStash(index?: number): Promise<void> {
return this.#repository.applyStash(index);
}
popStash(index?: number): Promise<void> {
return this.#repository.popStash(index);
}
dropStash(index?: number): Promise<void> {
return this.#repository.dropStash(index);
}
}
export class ApiGit implements Git {
#model: Model;
private _env: { [key: string]: string } | undefined;
constructor(model: Model) { this.#model = model; }
get path(): string { return this.#model.git.path; }
get env(): { [key: string]: string } {
if (this._env === undefined) {
this._env = Object.freeze(this.#model.git.env);
}
return this._env;
}
}
export class ApiImpl implements API {
#model: Model;
readonly git: ApiGit;
constructor(model: Model) {
this.#model = model;
this.git = new ApiGit(this.#model);
}
get state(): APIState {
return this.#model.state;
}
get onDidChangeState(): Event<APIState> {
return this.#model.onDidChangeState;
}
get onDidPublish(): Event<PublishEvent> {
return this.#model.onDidPublish;
}
get onDidOpenRepository(): Event<Repository> {
return mapEvent(this.#model.onDidOpenRepository, r => new ApiRepository(r));
}
get onDidCloseRepository(): Event<Repository> {
return mapEvent(this.#model.onDidCloseRepository, r => new ApiRepository(r));
}
get repositories(): Repository[] {
return this.#model.repositories.map(r => new ApiRepository(r));
}
toGitUri(uri: Uri, ref: string): Uri {
return toGitUri(uri, ref);
}
getRepository(uri: Uri): Repository | null {
const result = this.#model.getRepository(uri);
return result ? new ApiRepository(result) : null;
}
async init(root: Uri, options?: InitOptions): Promise<Repository | null> {
const path = root.fsPath;
await this.#model.git.init(path, options);
await this.#model.openRepository(path);
return this.getRepository(root) || null;
}
async openRepository(root: Uri): Promise<Repository | null> {
if (root.scheme !== 'file') {
return null;
}
await this.#model.openRepository(root.fsPath);
return this.getRepository(root) || null;
}
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable {
const disposables: Disposable[] = [];
if (provider.publishRepository) {
disposables.push(this.#model.registerRemoteSourcePublisher(provider as RemoteSourcePublisher));
}
disposables.push(GitBaseApi.getAPI().registerRemoteSourceProvider(provider));
return combinedDisposable(disposables);
}
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable {
return this.#model.registerRemoteSourcePublisher(publisher);
}
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
return this.#model.registerCredentialsProvider(provider);
}
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable {
return this.#model.registerPostCommitCommandsProvider(provider);
}
registerPushErrorHandler(handler: PushErrorHandler): Disposable {
return this.#model.registerPushErrorHandler(handler);
}
registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable {
return this.#model.registerSourceControlHistoryItemDetailsProvider(provider);
}
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable {
return this.#model.registerBranchProtectionProvider(root, provider);
}
}
function getRefType(type: RefType): string {
switch (type) {
case RefType.Head: return 'Head';
case RefType.RemoteHead: return 'RemoteHead';
case RefType.Tag: return 'Tag';
}
return 'unknown';
}
function getStatus(status: Status): string {
switch (status) {
case Status.INDEX_MODIFIED: return 'INDEX_MODIFIED';
case Status.INDEX_ADDED: return 'INDEX_ADDED';
case Status.INDEX_DELETED: return 'INDEX_DELETED';
case Status.INDEX_RENAMED: return 'INDEX_RENAMED';
case Status.INDEX_COPIED: return 'INDEX_COPIED';
case Status.MODIFIED: return 'MODIFIED';
case Status.DELETED: return 'DELETED';
case Status.UNTRACKED: return 'UNTRACKED';
case Status.IGNORED: return 'IGNORED';
case Status.INTENT_TO_ADD: return 'INTENT_TO_ADD';
case Status.INTENT_TO_RENAME: return 'INTENT_TO_RENAME';
case Status.TYPE_CHANGED: return 'TYPE_CHANGED';
case Status.ADDED_BY_US: return 'ADDED_BY_US';
case Status.ADDED_BY_THEM: return 'ADDED_BY_THEM';
case Status.DELETED_BY_US: return 'DELETED_BY_US';
case Status.DELETED_BY_THEM: return 'DELETED_BY_THEM';
case Status.BOTH_ADDED: return 'BOTH_ADDED';
case Status.BOTH_DELETED: return 'BOTH_DELETED';
case Status.BOTH_MODIFIED: return 'BOTH_MODIFIED';
}
return 'UNKNOWN';
}
export function registerAPICommands(extension: GitExtensionImpl): Disposable {
const disposables: Disposable[] = [];
disposables.push(commands.registerCommand('git.api.getRepositories', () => {
const api = extension.getAPI(1);
return api.repositories.map(r => r.rootUri.toString());
}));
disposables.push(commands.registerCommand('git.api.getRepositoryState', (uri: string) => {
const api = extension.getAPI(1);
const repository = api.getRepository(Uri.parse(uri));
if (!repository) {
return null;
}
const state = repository.state;
const ref = (ref: Ref | undefined) => (ref && { ...ref, type: getRefType(ref.type) });
const change = (change: Change) => ({
uri: change.uri.toString(),
originalUri: change.originalUri.toString(),
renameUri: change.renameUri?.toString(),
status: getStatus(change.status)
});
return {
HEAD: ref(state.HEAD),
refs: state.refs.map(ref),
remotes: state.remotes,
submodules: state.submodules,
rebaseCommit: state.rebaseCommit,
mergeChanges: state.mergeChanges.map(change),
indexChanges: state.indexChanges.map(change),
workingTreeChanges: state.workingTreeChanges.map(change)
};
}));
disposables.push(commands.registerCommand('git.api.getRemoteSources', (opts?: PickRemoteSourceOptions) => {
return commands.executeCommand('git-base.api.getRemoteSources', opts);
}));
return Disposable.from(...disposables);
}
+85
View File
@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Model } from '../model';
import { GitExtension, Repository, API } from './git';
import { ApiRepository, ApiImpl } from './api1';
import { Event, EventEmitter } from 'vscode';
export function deprecated(_target: any, key: string, descriptor: any): void {
if (typeof descriptor.value !== 'function') {
throw new Error('not supported');
}
const fn = descriptor.value;
descriptor.value = function () {
console.warn(`Git extension API method '${key}' is deprecated.`);
return fn.apply(this, arguments);
};
}
export class GitExtensionImpl implements GitExtension {
enabled: boolean = false;
private _onDidChangeEnablement = new EventEmitter<boolean>();
readonly onDidChangeEnablement: Event<boolean> = this._onDidChangeEnablement.event;
private _model: Model | undefined = undefined;
set model(model: Model | undefined) {
this._model = model;
const enabled = !!model;
if (this.enabled === enabled) {
return;
}
this.enabled = enabled;
this._onDidChangeEnablement.fire(this.enabled);
}
get model(): Model | undefined {
return this._model;
}
constructor(model?: Model) {
if (model) {
this.enabled = true;
this._model = model;
}
}
@deprecated
async getGitPath(): Promise<string> {
if (!this._model) {
throw new Error('Git model not found');
}
return this._model.git.path;
}
@deprecated
async getRepositories(): Promise<Repository[]> {
if (!this._model) {
throw new Error('Git model not found');
}
return this._model.repositories.map(repository => new ApiRepository(repository));
}
getAPI(version: number): API {
if (!this._model) {
throw new Error('Git model not found');
}
if (version !== 1) {
throw new Error(`No API version ${version} found.`);
}
return new ApiImpl(this._model);
}
}
+440
View File
@@ -0,0 +1,440 @@
/*---------------------------------------------------------------------------------------------
* 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, CancellationToken, SourceControlHistoryItem } from 'vscode';
export { ProviderResult } from 'vscode';
export interface Git {
readonly path: string;
}
export interface InputBox {
value: string;
}
export const enum ForcePushMode {
Force,
ForceWithLease,
ForceWithLeaseIfIncludes,
}
export const enum RefType {
Head,
RemoteHead,
Tag
}
export interface Ref {
readonly type: RefType;
readonly name?: string;
readonly commit?: string;
readonly commitDetails?: Commit;
readonly remote?: string;
}
export interface UpstreamRef {
readonly remote: string;
readonly name: string;
readonly commit?: string;
}
export interface Branch extends Ref {
readonly upstream?: UpstreamRef;
readonly ahead?: number;
readonly behind?: number;
}
export interface CommitShortStat {
readonly files: number;
readonly insertions: number;
readonly deletions: 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;
readonly shortStat?: CommitShortStat;
}
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 untrackedChanges: 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;
/** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */
readonly range?: string;
readonly reverse?: boolean;
readonly sortByAuthorDate?: boolean;
readonly shortStats?: boolean;
readonly author?: string;
readonly refNames?: string[];
readonly maxParents?: number;
readonly skip?: number;
}
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 RefQuery {
readonly contains?: string;
readonly count?: number;
readonly pattern?: string | string[];
readonly sort?: 'alphabetically' | 'committerdate';
}
export interface BranchQuery extends RefQuery {
readonly remote?: boolean;
}
export interface Repository {
readonly rootUri: Uri;
readonly inputBox: InputBox;
readonly state: RepositoryState;
readonly ui: RepositoryUIState;
readonly onDidCommit: Event<void>;
readonly onDidCheckout: Event<void>;
getConfigs(): Promise<{ key: string; value: string; }[]>;
getConfig(key: string): Promise<string>;
setConfig(key: string, value: string): Promise<string>;
unsetConfig(key: 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, cancellationToken?: CancellationToken): Promise<Ref[]>;
getBranchBase(name: string): Promise<Branch | undefined>;
setBranchUpstream(name: string, upstream: string): Promise<void>;
checkIgnore(paths: string[]): Promise<Set<string>>;
getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]>;
getMergeBase(ref1: string, ref2: string): Promise<string | undefined>;
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>;
merge(ref: string): Promise<void>;
mergeAbort(): Promise<void>;
applyStash(index?: number): Promise<void>;
popStash(index?: number): Promise<void>;
dropStash(index?: number): 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): ProviderResult<Map<string, string | undefined>>;
provideHoverCommands(repository: Repository): ProviderResult<Command[]>;
provideMessageLinks(repository: Repository, message: string): ProviderResult<string>;
}
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',
ForcePushWithLeaseRejected = 'ForcePushWithLeaseRejected',
ForcePushWithLeaseIfIncludesRejected = 'ForcePushWithLeaseIfIncludesRejected',
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',
BranchFastForwardRejected = 'BranchFastForwardRejected',
BranchNotYetBorn = 'BranchNotYetBorn',
TagConflict = 'TagConflict',
CherryPickEmpty = 'CherryPickEmpty',
CherryPickConflict = 'CherryPickConflict'
}
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
echo ''
+66
View File
@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { IPCClient } from './ipc/ipcClient';
function fatal(err: any): void {
console.error('Missing or invalid credentials.');
console.error(err);
process.exit(1);
}
function main(argv: string[]): void {
if (!process.env['VSCODE_GIT_ASKPASS_PIPE']) {
return fatal('Missing pipe');
}
if (!process.env['VSCODE_GIT_ASKPASS_TYPE']) {
return fatal('Missing type');
}
if (process.env['VSCODE_GIT_ASKPASS_TYPE'] !== 'https' && process.env['VSCODE_GIT_ASKPASS_TYPE'] !== 'ssh') {
return fatal(`Invalid type: ${process.env['VSCODE_GIT_ASKPASS_TYPE']}`);
}
if (process.env['VSCODE_GIT_COMMAND'] === 'fetch' && !!process.env['VSCODE_GIT_FETCH_SILENT']) {
return fatal('Skip silent fetch commands');
}
const output = process.env['VSCODE_GIT_ASKPASS_PIPE'] as string;
const askpassType = process.env['VSCODE_GIT_ASKPASS_TYPE'] as 'https' | 'ssh';
// HTTPS (username | password), SSH (passphrase | authenticity)
const request = askpassType === 'https' ? argv[2] : argv[3];
let host: string | undefined,
file: string | undefined,
fingerprint: string | undefined;
if (askpassType === 'https') {
host = argv[4].replace(/^["']+|["':]+$/g, '');
}
if (askpassType === 'ssh') {
if (/passphrase/i.test(request)) {
// passphrase
// Commit signing - Enter passphrase:
// Git operation - Enter passphrase for key '/c/Users/<username>/.ssh/id_ed25519':
file = argv[6]?.replace(/^["']+|["':]+$/g, '');
} else {
// authenticity
host = argv[6].replace(/^["']+|["':]+$/g, '');
fingerprint = argv[15];
}
}
const ipcClient = new IPCClient('askpass');
ipcClient.call({ askpassType, request, host, file, fingerprint }).then(res => {
fs.writeFileSync(output, res + '\n');
setTimeout(() => process.exit(0), 0);
}).catch(err => fatal(err));
}
main(process.argv);
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
VSCODE_GIT_ASKPASS_PIPE=`mktemp`
ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" VSCODE_GIT_ASKPASS_TYPE="https" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $VSCODE_GIT_ASKPASS_EXTRA_ARGS $*
cat $VSCODE_GIT_ASKPASS_PIPE
rm $VSCODE_GIT_ASKPASS_PIPE
+141
View File
@@ -0,0 +1,141 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { window, InputBoxOptions, Uri, Disposable, workspace, QuickPickOptions, l10n } from 'vscode';
import { IDisposable, EmptyDisposable, toDisposable } from './util';
import * as path from 'path';
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
import { CredentialsProvider, Credentials } from './api/git';
import { ITerminalEnvironmentProvider } from './terminal';
export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {
private env: { [key: string]: string };
private sshEnv: { [key: string]: string };
private disposable: IDisposable = EmptyDisposable;
private cache = new Map<string, Credentials>();
private credentialsProviders = new Set<CredentialsProvider>();
readonly featureDescription = 'git auth provider';
constructor(private ipc?: IIPCServer) {
if (ipc) {
this.disposable = ipc.registerHandler('askpass', this);
}
this.env = {
// GIT_ASKPASS
GIT_ASKPASS: path.join(__dirname, this.ipc ? 'askpass.sh' : 'askpass-empty.sh'),
// VSCODE_GIT_ASKPASS
VSCODE_GIT_ASKPASS_NODE: process.execPath,
VSCODE_GIT_ASKPASS_EXTRA_ARGS: '',
VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js'),
};
this.sshEnv = {
// SSH_ASKPASS
SSH_ASKPASS: path.join(__dirname, this.ipc ? 'ssh-askpass.sh' : 'ssh-askpass-empty.sh'),
SSH_ASKPASS_REQUIRE: 'force',
};
}
async handle(payload:
{ askpassType: 'https'; request: string; host: string } |
{ askpassType: 'ssh'; request: string; host?: string; file?: string; fingerprint?: string }
): Promise<string> {
const config = workspace.getConfiguration('git', null);
const enabled = config.get<boolean>('enabled');
if (!enabled) {
return '';
}
// https
if (payload.askpassType === 'https') {
return await this.handleAskpass(payload.request, payload.host);
}
// ssh
return await this.handleSSHAskpass(payload.request, payload.host, payload.file, payload.fingerprint);
}
async handleAskpass(request: string, host: string): Promise<string> {
const uri = Uri.parse(host);
const authority = uri.authority.replace(/^.*@/, '');
const password = /password/i.test(request);
const cached = this.cache.get(authority);
if (cached && password) {
this.cache.delete(authority);
return cached.password;
}
if (!password) {
for (const credentialsProvider of this.credentialsProviders) {
try {
const credentials = await credentialsProvider.getCredentials(uri);
if (credentials) {
this.cache.set(authority, credentials);
setTimeout(() => this.cache.delete(authority), 60_000);
return credentials.username;
}
} catch { }
}
}
const options: InputBoxOptions = {
password,
placeHolder: request,
prompt: `Git: ${host}`,
ignoreFocusOut: true
};
return await window.showInputBox(options) || '';
}
async handleSSHAskpass(request: string, host?: string, file?: string, fingerprint?: string): Promise<string> {
// passphrase
if (/passphrase/i.test(request)) {
const options: InputBoxOptions = {
password: true,
placeHolder: l10n.t('Passphrase'),
prompt: file ? `SSH Key: ${file}` : undefined,
ignoreFocusOut: true
};
return await window.showInputBox(options) || '';
}
// authenticity
const options: QuickPickOptions = {
canPickMany: false,
ignoreFocusOut: true,
placeHolder: l10n.t('Are you sure you want to continue connecting?'),
title: l10n.t('"{0}" has fingerprint "{1}"', host ?? '', fingerprint ?? '')
};
const items = [l10n.t('yes'), l10n.t('no')];
return await window.showQuickPick(items, options) ?? '';
}
getEnv(): { [key: string]: string } {
const config = workspace.getConfiguration('git');
return config.get<boolean>('useIntegratedAskPass') ? { ...this.env, ...this.sshEnv } : {};
}
getTerminalEnv(): { [key: string]: string } {
const config = workspace.getConfiguration('git');
return config.get<boolean>('useIntegratedAskPass') && config.get<boolean>('terminalAuthentication') ? this.env : {};
}
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
this.credentialsProviders.add(provider);
return toDisposable(() => this.credentialsProviders.delete(provider));
}
dispose(): void {
this.disposable.dispose();
}
}
+136
View File
@@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget, Uri, ConfigurationChangeEvent, l10n, env } from 'vscode';
import { Repository } from './repository';
import { eventToPromise, filterEvent, onceEvent } from './util';
import { GitErrorCodes } from './api/git';
export class AutoFetcher {
private static DidInformUser = 'autofetch.didInformUser';
private _onDidChange = new EventEmitter<boolean>();
private onDidChange = this._onDidChange.event;
private _enabled: boolean = false;
private _fetchAll: boolean = false;
get enabled(): boolean { return this._enabled; }
set enabled(enabled: boolean) { this._enabled = enabled; this._onDidChange.fire(enabled); }
private disposables: Disposable[] = [];
constructor(private repository: Repository, private globalState: Memento) {
workspace.onDidChangeConfiguration(this.onConfiguration, this, this.disposables);
this.onConfiguration();
const onGoodRemoteOperation = filterEvent(repository.onDidRunOperation, ({ operation, error }) => !error && operation.remote);
const onFirstGoodRemoteOperation = onceEvent(onGoodRemoteOperation);
onFirstGoodRemoteOperation(this.onFirstGoodRemoteOperation, this, this.disposables);
}
private async onFirstGoodRemoteOperation(): Promise<void> {
const didInformUser = !this.globalState.get<boolean>(AutoFetcher.DidInformUser);
if (this.enabled && !didInformUser) {
this.globalState.update(AutoFetcher.DidInformUser, true);
}
const shouldInformUser = !this.enabled && didInformUser;
if (!shouldInformUser) {
return;
}
const yes: MessageItem = { title: l10n.t('Yes') };
const no: MessageItem = { isCloseAffordance: true, title: l10n.t('No') };
const askLater: MessageItem = { title: l10n.t('Ask Me Later') };
const result = await window.showInformationMessage(l10n.t('Would you like {0} to [periodically run "git fetch"]({1})?', env.appName, 'https://go.microsoft.com/fwlink/?linkid=865294'), yes, no, askLater);
if (result === askLater) {
return;
}
if (result === yes) {
const gitConfig = workspace.getConfiguration('git', Uri.file(this.repository.root));
gitConfig.update('autofetch', true, ConfigurationTarget.Global);
}
this.globalState.update(AutoFetcher.DidInformUser, true);
}
private onConfiguration(e?: ConfigurationChangeEvent): void {
if (e !== undefined && !e.affectsConfiguration('git.autofetch')) {
return;
}
const gitConfig = workspace.getConfiguration('git', Uri.file(this.repository.root));
switch (gitConfig.get<boolean | 'all'>('autofetch')) {
case true:
this._fetchAll = false;
this.enable();
break;
case 'all':
this._fetchAll = true;
this.enable();
break;
case false:
default:
this._fetchAll = false;
this.disable();
break;
}
}
enable(): void {
if (this.enabled) {
return;
}
this.enabled = true;
this.run();
}
disable(): void {
this.enabled = false;
}
private async run(): Promise<void> {
while (this.enabled) {
await this.repository.whenIdleAndFocused();
if (!this.enabled) {
return;
}
try {
if (this._fetchAll) {
await this.repository.fetchAll({ silent: true });
} else {
await this.repository.fetchDefault({ silent: true });
}
} catch (err) {
if (err.gitErrorCode === GitErrorCodes.AuthenticationFailed) {
this.disable();
}
}
if (!this.enabled) {
return;
}
const period = workspace.getConfiguration('git', Uri.file(this.repository.root)).get<number>('autofetchPeriod', 180) * 1000;
const timeout = new Promise(c => setTimeout(c, period));
const whenDisabled = eventToPromise(filterEvent(this.onDidChange, enabled => !enabled));
await Promise.race([timeout, whenDisabled]);
}
}
dispose(): void {
this.disable();
this.disposables.forEach(d => d.dispose());
}
}
+785
View File
@@ -0,0 +1,785 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, languages, HoverProvider, CancellationToken, Hover, TextDocument } from 'vscode';
import { Model } from './model';
import { dispose, fromNow, getCommitShortHash, IDisposable } from './util';
import { Repository } from './repository';
import { throttle } from './decorators';
import { BlameInformation, Commit } from './git';
import { fromGitUri, isGitUri, toGitUri } from './uri';
import { emojify, ensureEmojis } from './emoji';
import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging';
import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider';
import { AvatarQuery, AvatarQueryCommit } from './api/git';
import { LRUCache } from './cache';
const AVATAR_SIZE = 20;
function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean {
return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive);
}
function lineRangeLength(startLineNumber: number, endLineNumberExclusive: number): number {
return endLineNumberExclusive - startLineNumber;
}
function mapModifiedLineNumberToOriginalLineNumber(lineNumber: number, changes: readonly TextEditorChange[]): number {
if (changes.length === 0) {
return lineNumber;
}
for (const change of changes) {
// Do not process changes after the line number
if (lineNumber < change.modified.startLineNumber) {
break;
}
// Map line number to the original line number
if (change.kind === TextEditorChangeKind.Addition) {
// Addition
lineNumber = lineNumber - lineRangeLength(change.modified.startLineNumber, change.modified.endLineNumberExclusive);
} else if (change.kind === TextEditorChangeKind.Deletion) {
// Deletion
lineNumber = lineNumber + lineRangeLength(change.original.startLineNumber, change.original.endLineNumberExclusive);
} else if (change.kind === TextEditorChangeKind.Modification) {
// Modification
const originalRangeLength = lineRangeLength(change.original.startLineNumber, change.original.endLineNumberExclusive);
const modifiedRangeLength = lineRangeLength(change.modified.startLineNumber, change.modified.endLineNumberExclusive);
if (originalRangeLength !== modifiedRangeLength) {
lineNumber = lineNumber - (modifiedRangeLength - originalRangeLength);
}
} else {
throw new Error('Unexpected change kind');
}
}
return lineNumber;
}
function getEditorDecorationRange(lineNumber: number): Range {
const position = new Position(lineNumber, Number.MAX_SAFE_INTEGER);
return new Range(position, position);
}
function isResourceSchemeSupported(uri: Uri): boolean {
return uri.scheme === 'file' || isGitUri(uri);
}
function isResourceBlameInformationEqual(a: ResourceBlameInformation | undefined, b: ResourceBlameInformation | undefined): boolean {
if (a === b) {
return true;
}
if (!a || !b ||
a.resource.toString() !== b.resource.toString() ||
a.blameInformation.length !== b.blameInformation.length) {
return false;
}
for (let index = 0; index < a.blameInformation.length; index++) {
if (a.blameInformation[index].lineNumber !== b.blameInformation[index].lineNumber) {
return false;
}
const aBlameInformation = a.blameInformation[index].blameInformation;
const bBlameInformation = b.blameInformation[index].blameInformation;
if (typeof aBlameInformation === 'string' && typeof bBlameInformation === 'string') {
if (aBlameInformation !== bBlameInformation) {
return false;
}
} else if (typeof aBlameInformation !== 'string' && typeof bBlameInformation !== 'string') {
if (aBlameInformation.hash !== bBlameInformation.hash) {
return false;
}
} else {
return false;
}
}
return true;
}
type BlameInformationTemplateTokens = {
readonly hash: string;
readonly hashShort: string;
readonly subject: string;
readonly authorName: string;
readonly authorEmail: string;
readonly authorDate: string;
readonly authorDateAgo: string;
};
interface ResourceBlameInformation {
readonly resource: Uri;
readonly blameInformation: readonly LineBlameInformation[];
}
interface LineBlameInformation {
readonly lineNumber: number;
readonly blameInformation: BlameInformation | string;
}
class GitBlameInformationCache {
private readonly _cache = new Map<Repository, LRUCache<string, BlameInformation[]>>();
delete(repository: Repository): boolean {
return this._cache.delete(repository);
}
get(repository: Repository, resource: Uri, commit: string): BlameInformation[] | undefined {
const key = this._getCacheKey(resource, commit);
return this._cache.get(repository)?.get(key);
}
set(repository: Repository, resource: Uri, commit: string, blameInformation: BlameInformation[]): void {
if (!this._cache.has(repository)) {
this._cache.set(repository, new LRUCache<string, BlameInformation[]>(100));
}
const key = this._getCacheKey(resource, commit);
this._cache.get(repository)!.set(key, blameInformation);
}
private _getCacheKey(resource: Uri, commit: string): string {
return toGitUri(resource, commit).toString();
}
}
export class GitBlameController {
private readonly _subjectMaxLength = 50;
private readonly _onDidChangeBlameInformation = new EventEmitter<void>();
public readonly onDidChangeBlameInformation = this._onDidChangeBlameInformation.event;
private _textEditorBlameInformation: ResourceBlameInformation | undefined;
get textEditorBlameInformation(): ResourceBlameInformation | undefined {
return this._textEditorBlameInformation;
}
private set textEditorBlameInformation(blameInformation: ResourceBlameInformation | undefined) {
if (isResourceBlameInformationEqual(this._textEditorBlameInformation, blameInformation)) {
return;
}
this._textEditorBlameInformation = blameInformation;
this._onDidChangeBlameInformation.fire();
}
private _HEAD: string | undefined;
private readonly _commitInformationCache = new LRUCache<string, Commit>(100);
private readonly _repositoryBlameCache = new GitBlameInformationCache();
private _editorDecoration: GitBlameEditorDecoration | undefined;
private _statusBarItem: GitBlameStatusBarItem | undefined;
private _repositoryDisposables = new Map<Repository, IDisposable[]>();
private _enablementDisposables: IDisposable[] = [];
private _disposables: IDisposable[] = [];
constructor(private readonly _model: Model) {
workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables);
this._onDidChangeConfiguration();
}
formatBlameInformationMessage(documentUri: Uri, template: string, blameInformation: BlameInformation): string {
const subject = blameInformation.subject && blameInformation.subject.length > this._subjectMaxLength
? `${blameInformation.subject.substring(0, this._subjectMaxLength)}\u2026`
: blameInformation.subject;
const templateTokens = {
hash: blameInformation.hash,
hashShort: getCommitShortHash(documentUri, blameInformation.hash),
subject: emojify(subject ?? ''),
authorName: blameInformation.authorName ?? '',
authorEmail: blameInformation.authorEmail ?? '',
authorDate: new Date(blameInformation.authorDate ?? new Date()).toLocaleString(),
authorDateAgo: fromNow(blameInformation.authorDate ?? new Date(), true, true)
} satisfies BlameInformationTemplateTokens;
return template.replace(/\$\{(.+?)\}/g, (_, token) => {
return token in templateTokens ? templateTokens[token as keyof BlameInformationTemplateTokens] : `\${${token}}`;
});
}
async getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation): Promise<MarkdownString> {
const remoteHoverCommands: Command[] = [];
let commitAvatar: string | undefined;
let commitInformation: Commit | undefined;
let commitMessageWithLinks: string | undefined;
const repository = this._model.getRepository(documentUri);
if (repository) {
try {
// Commit details
commitInformation = this._commitInformationCache.get(blameInformation.hash);
if (!commitInformation) {
commitInformation = await repository.getCommit(blameInformation.hash);
this._commitInformationCache.set(blameInformation.hash, commitInformation);
}
// Avatar
const avatarQuery = {
commits: [{
hash: blameInformation.hash,
authorName: blameInformation.authorName,
authorEmail: blameInformation.authorEmail
} satisfies AvatarQueryCommit],
size: AVATAR_SIZE
} satisfies AvatarQuery;
const avatarResult = await provideSourceControlHistoryItemAvatar(this._model, repository, avatarQuery);
commitAvatar = avatarResult?.get(blameInformation.hash);
} catch { }
// Remote hover commands
const unpublishedCommits = await repository.getUnpublishedCommits();
if (!unpublishedCommits.has(blameInformation.hash)) {
remoteHoverCommands.push(...await provideSourceControlHistoryItemHoverCommands(this._model, repository) ?? []);
}
// Message links
commitMessageWithLinks = await provideSourceControlHistoryItemMessageLinks(
this._model, repository, commitInformation?.message ?? blameInformation.subject ?? '');
}
const markdownString = new MarkdownString();
markdownString.isTrusted = true;
markdownString.supportThemeIcons = true;
// Author, date
const hash = commitInformation?.hash ?? blameInformation.hash;
const authorName = commitInformation?.authorName ?? blameInformation.authorName;
const authorEmail = commitInformation?.authorEmail ?? blameInformation.authorEmail;
const authorDate = commitInformation?.authorDate ?? blameInformation.authorDate;
const avatar = commitAvatar ? `![${authorName}](${commitAvatar}|width=${AVATAR_SIZE},height=${AVATAR_SIZE})` : '$(account)';
if (authorName) {
if (authorEmail) {
const emailTitle = l10n.t('Email');
markdownString.appendMarkdown(`${avatar} [**${authorName}**](mailto:${authorEmail} "${emailTitle} ${authorName}")`);
} else {
markdownString.appendMarkdown(`${avatar} **${authorName}**`);
}
if (authorDate) {
const dateString = new Date(authorDate).toLocaleString(undefined, {
year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric'
});
markdownString.appendMarkdown(`, $(history) ${fromNow(authorDate, true, true)} (${dateString})`);
}
markdownString.appendMarkdown('\n\n');
}
// Subject | Message
markdownString.appendMarkdown(`${emojify(commitMessageWithLinks ?? commitInformation?.message ?? blameInformation.subject ?? '')}\n\n`);
markdownString.appendMarkdown(`---\n\n`);
// Short stats
if (commitInformation?.shortStat) {
markdownString.appendMarkdown(`<span>${commitInformation.shortStat.files === 1 ?
l10n.t('{0} file changed', commitInformation.shortStat.files) :
l10n.t('{0} files changed', commitInformation.shortStat.files)}</span>`);
if (commitInformation.shortStat.insertions) {
markdownString.appendMarkdown(`,&nbsp;<span style="color:var(--vscode-scmGraph-historyItemHoverAdditionsForeground);">${commitInformation.shortStat.insertions === 1 ?
l10n.t('{0} insertion{1}', commitInformation.shortStat.insertions, '(+)') :
l10n.t('{0} insertions{1}', commitInformation.shortStat.insertions, '(+)')}</span>`);
}
if (commitInformation.shortStat.deletions) {
markdownString.appendMarkdown(`,&nbsp;<span style="color:var(--vscode-scmGraph-historyItemHoverDeletionsForeground);">${commitInformation.shortStat.deletions === 1 ?
l10n.t('{0} deletion{1}', commitInformation.shortStat.deletions, '(-)') :
l10n.t('{0} deletions{1}', commitInformation.shortStat.deletions, '(-)')}</span>`);
}
markdownString.appendMarkdown(`\n\n---\n\n`);
}
// Commands
markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, hash]))} "${l10n.t('Open Commit')}")`);
markdownString.appendMarkdown('&nbsp;');
markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`);
// Remote hover commands
if (remoteHoverCommands.length > 0) {
markdownString.appendMarkdown('&nbsp;&nbsp;|&nbsp;&nbsp;');
const remoteCommandsMarkdown = remoteHoverCommands
.map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify([...command.arguments ?? [], hash]))} "${command.tooltip}")`);
markdownString.appendMarkdown(remoteCommandsMarkdown.join('&nbsp;'));
}
markdownString.appendMarkdown('&nbsp;&nbsp;|&nbsp;&nbsp;');
markdownString.appendMarkdown(`[$(gear)](command:workbench.action.openSettings?%5B%22git.blame%22%5D "${l10n.t('Open Settings')}")`);
return markdownString;
}
private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void {
if (e &&
!e.affectsConfiguration('git.blame.editorDecoration.enabled') &&
!e.affectsConfiguration('git.blame.statusBarItem.enabled')) {
return;
}
const config = workspace.getConfiguration('git');
const editorDecorationEnabled = config.get<boolean>('blame.editorDecoration.enabled') === true;
const statusBarItemEnabled = config.get<boolean>('blame.statusBarItem.enabled') === true;
// Editor decoration
if (editorDecorationEnabled) {
if (!this._editorDecoration) {
this._editorDecoration = new GitBlameEditorDecoration(this);
}
} else {
this._editorDecoration?.dispose();
this._editorDecoration = undefined;
}
// StatusBar item
if (statusBarItemEnabled) {
if (!this._statusBarItem) {
this._statusBarItem = new GitBlameStatusBarItem(this);
}
} else {
this._statusBarItem?.dispose();
this._statusBarItem = undefined;
}
// Listeners
if (editorDecorationEnabled || statusBarItemEnabled) {
if (this._enablementDisposables.length === 0) {
this._model.onDidOpenRepository(this._onDidOpenRepository, this, this._enablementDisposables);
this._model.onDidCloseRepository(this._onDidCloseRepository, this, this._enablementDisposables);
for (const repository of this._model.repositories) {
this._onDidOpenRepository(repository);
}
window.onDidChangeActiveTextEditor(e => this._updateTextEditorBlameInformation(e), this, this._enablementDisposables);
window.onDidChangeTextEditorSelection(e => this._updateTextEditorBlameInformation(e.textEditor, 'selection'), this, this._enablementDisposables);
window.onDidChangeTextEditorDiffInformation(e => this._updateTextEditorBlameInformation(e.textEditor), this, this._enablementDisposables);
}
} else {
this._enablementDisposables = dispose(this._enablementDisposables);
}
this._updateTextEditorBlameInformation(window.activeTextEditor);
}
private _onDidOpenRepository(repository: Repository): void {
const repositoryDisposables: IDisposable[] = [];
repository.onDidRunGitStatus(() => this._onDidRunGitStatus(repository), this, repositoryDisposables);
this._repositoryDisposables.set(repository, repositoryDisposables);
}
private _onDidCloseRepository(repository: Repository): void {
const disposables = this._repositoryDisposables.get(repository);
if (disposables) {
dispose(disposables);
}
this._repositoryDisposables.delete(repository);
this._repositoryBlameCache.delete(repository);
}
private _onDidRunGitStatus(repository: Repository): void {
if (!repository.HEAD?.commit || this._HEAD === repository.HEAD.commit) {
return;
}
this._HEAD = repository.HEAD.commit;
this._updateTextEditorBlameInformation(window.activeTextEditor);
}
private async _getBlameInformation(resource: Uri, commit: string): Promise<BlameInformation[] | undefined> {
const repository = this._model.getRepository(resource);
if (!repository) {
return undefined;
}
const resourceBlameInformation = this._repositoryBlameCache.get(repository, resource, commit);
if (resourceBlameInformation) {
return resourceBlameInformation;
}
// Ensure that the emojis are loaded as we will need
// access to them when formatting the blame information.
await ensureEmojis();
// Get blame information for the resource and cache it
const blameInformation = await repository.blame2(resource.fsPath, commit) ?? [];
this._repositoryBlameCache.set(repository, resource, commit, blameInformation);
return blameInformation;
}
@throttle
private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined, reason?: 'selection'): Promise<void> {
if (textEditor) {
if (!textEditor.diffInformation || textEditor !== window.activeTextEditor) {
return;
}
} else {
this.textEditorBlameInformation = undefined;
return;
}
const repository = this._model.getRepository(textEditor.document.uri);
if (!repository || !repository.HEAD?.commit) {
return;
}
// Only support resources with `file` and `git` schemes
if (!isResourceSchemeSupported(textEditor.document.uri)) {
this.textEditorBlameInformation = undefined;
return;
}
// Do not show blame information when there is a single selection and it is at the beginning
// of the file [0, 0, 0, 0] unless the user explicitly navigates the cursor there. We do this
// to avoid showing blame information when the editor is not focused.
if (reason !== 'selection' && textEditor.selections.length === 1 &&
textEditor.selections[0].start.line === 0 && textEditor.selections[0].start.character === 0 &&
textEditor.selections[0].end.line === 0 && textEditor.selections[0].end.character === 0) {
this.textEditorBlameInformation = undefined;
return;
}
let allChanges: readonly TextEditorChange[];
let workingTreeChanges: readonly TextEditorChange[];
let workingTreeAndIndexChanges: readonly TextEditorChange[] | undefined;
if (isGitUri(textEditor.document.uri)) {
const { ref } = fromGitUri(textEditor.document.uri);
// For the following scenarios we can discard the diff information
// 1) Commit - Resource in the multi-file diff editor when viewing the details of a commit.
// 2) HEAD - Resource on the left-hand side of the diff editor when viewing a resource from the index.
// 3) ~ - Resource on the left-hand side of the diff editor when viewing a resource from the working tree.
if (/^[0-9a-f]{40}$/i.test(ref) || ref === 'HEAD' || ref === '~') {
workingTreeChanges = allChanges = [];
workingTreeAndIndexChanges = undefined;
} else if (ref === '') {
// Resource on the right-hand side of the diff editor when viewing a resource from the index.
const diffInformationWorkingTreeAndIndex = getWorkingTreeAndIndexDiffInformation(textEditor);
// Working tree + index diff information is present and it is stale. Diff information
// may be stale when the selection changes because of a content change and the diff
// information is not yet updated.
if (diffInformationWorkingTreeAndIndex && diffInformationWorkingTreeAndIndex.isStale) {
this.textEditorBlameInformation = undefined;
return;
}
workingTreeChanges = [];
workingTreeAndIndexChanges = allChanges = diffInformationWorkingTreeAndIndex?.changes ?? [];
} else {
throw new Error(`Unexpected ref: ${ref}`);
}
} else {
// Working tree diff information. Diff Editor (Working Tree) -> Text Editor
const diffInformationWorkingTree = getWorkingTreeDiffInformation(textEditor);
// Working tree diff information is not present or it is stale. Diff information
// may be stale when the selection changes because of a content change and the diff
// information is not yet updated.
if (!diffInformationWorkingTree || diffInformationWorkingTree.isStale) {
this.textEditorBlameInformation = undefined;
return;
}
// Working tree + index diff information
const diffInformationWorkingTreeAndIndex = getWorkingTreeAndIndexDiffInformation(textEditor);
// Working tree + index diff information is present and it is stale. Diff information
// may be stale when the selection changes because of a content change and the diff
// information is not yet updated.
if (diffInformationWorkingTreeAndIndex && diffInformationWorkingTreeAndIndex.isStale) {
this.textEditorBlameInformation = undefined;
return;
}
workingTreeChanges = diffInformationWorkingTree.changes;
workingTreeAndIndexChanges = diffInformationWorkingTreeAndIndex?.changes;
// For staged resources, we provide an additional "original resource" so that the editor
// diff information contains both the changes that are in the working tree and the changes
// that are in the working tree + index.
allChanges = workingTreeAndIndexChanges ?? workingTreeChanges;
}
let commit: string;
if (!isGitUri(textEditor.document.uri)) {
// Resource with the `file` scheme
commit = repository.HEAD.commit;
} else {
// Resource with the `git` scheme
const { ref } = fromGitUri(textEditor.document.uri);
commit = /^[0-9a-f]{40}$/i.test(ref) ? ref : repository.HEAD.commit;
}
// Git blame information
const resourceBlameInformation = await this._getBlameInformation(textEditor.document.uri, commit);
if (!resourceBlameInformation) {
return;
}
const lineBlameInformation: LineBlameInformation[] = [];
for (const lineNumber of new Set(textEditor.selections.map(s => s.active.line))) {
// Check if the line is contained in the working tree diff information
if (lineRangesContainLine(workingTreeChanges, lineNumber + 1)) {
if (reason === 'selection') {
// Only show the `Not Committed Yet` message upon selection change due to navigation
lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet') });
}
continue;
}
// Check if the line is contained in the working tree + index diff information
if (lineRangesContainLine(workingTreeAndIndexChanges ?? [], lineNumber + 1)) {
lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet (Staged)') });
continue;
}
// Map the line number to the git blame ranges using the diff information
const lineNumberWithDiff = mapModifiedLineNumberToOriginalLineNumber(lineNumber + 1, allChanges);
const blameInformation = resourceBlameInformation.find(blameInformation => {
return blameInformation.ranges.find(range => {
return lineNumberWithDiff >= range.startLineNumber && lineNumberWithDiff <= range.endLineNumber;
});
});
if (blameInformation) {
lineBlameInformation.push({ lineNumber, blameInformation });
}
}
this.textEditorBlameInformation = {
resource: textEditor.document.uri,
blameInformation: lineBlameInformation
};
}
dispose() {
for (const disposables of this._repositoryDisposables.values()) {
dispose(disposables);
}
this._repositoryDisposables.clear();
this._disposables = dispose(this._disposables);
}
}
class GitBlameEditorDecoration implements HoverProvider {
private _decoration: TextEditorDecorationType;
private _hoverDisposable: IDisposable | undefined;
private _disposables: IDisposable[] = [];
constructor(private readonly _controller: GitBlameController) {
this._decoration = window.createTextEditorDecorationType({
after: {
color: new ThemeColor('git.blame.editorDecorationForeground')
}
});
this._disposables.push(this._decoration);
workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables);
window.onDidChangeActiveTextEditor(this._onDidChangeActiveTextEditor, this, this._disposables);
this._controller.onDidChangeBlameInformation(() => this._onDidChangeBlameInformation(), this, this._disposables);
this._onDidChangeConfiguration();
}
async provideHover(document: TextDocument, position: Position, token: CancellationToken): Promise<Hover | undefined> {
if (token.isCancellationRequested) {
return undefined;
}
const textEditor = window.activeTextEditor;
if (!textEditor) {
return undefined;
}
// Position must be at the end of the line
if (position.character !== document.lineAt(position.line).range.end.character) {
return undefined;
}
// Get blame information
const blameInformation = this._controller.textEditorBlameInformation?.blameInformation;
const lineBlameInformation = blameInformation?.find(blame => blame.lineNumber === position.line);
if (!lineBlameInformation || typeof lineBlameInformation.blameInformation === 'string') {
return undefined;
}
const contents = await this._controller.getBlameInformationHover(textEditor.document.uri, lineBlameInformation.blameInformation);
if (!contents || token.isCancellationRequested) {
return undefined;
}
return { range: getEditorDecorationRange(position.line), contents: [contents] };
}
private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void {
if (e &&
!e.affectsConfiguration('git.commitShortHashLength') &&
!e.affectsConfiguration('git.blame.editorDecoration.template')) {
return;
}
this._registerHoverProvider();
this._onDidChangeBlameInformation();
}
private _onDidChangeActiveTextEditor(): void {
// Clear decorations
for (const editor of window.visibleTextEditors) {
if (editor !== window.activeTextEditor) {
editor.setDecorations(this._decoration, []);
}
}
// Register hover provider
this._registerHoverProvider();
}
private _onDidChangeBlameInformation(): void {
const textEditor = window.activeTextEditor;
if (!textEditor) {
return;
}
// Get blame information
const blameInformation = this._controller.textEditorBlameInformation?.blameInformation;
if (!blameInformation || blameInformation.length === 0) {
textEditor.setDecorations(this._decoration, []);
return;
}
// Set decorations for the editor
const config = workspace.getConfiguration('git');
const template = config.get<string>('blame.editorDecoration.template', '${subject}, ${authorName} (${authorDateAgo})');
const decorations = blameInformation.map(blame => {
const contentText = typeof blame.blameInformation !== 'string'
? this._controller.formatBlameInformationMessage(textEditor.document.uri, template, blame.blameInformation)
: blame.blameInformation;
return this._createDecoration(blame.lineNumber, contentText);
});
textEditor.setDecorations(this._decoration, decorations);
}
private _createDecoration(lineNumber: number, contentText: string): DecorationOptions {
return {
range: getEditorDecorationRange(lineNumber),
renderOptions: {
after: {
contentText,
margin: '0 0 0 50px'
}
},
};
}
private _registerHoverProvider(): void {
this._hoverDisposable?.dispose();
if (window.activeTextEditor && isResourceSchemeSupported(window.activeTextEditor.document.uri)) {
this._hoverDisposable = languages.registerHoverProvider({
pattern: window.activeTextEditor.document.uri.fsPath
}, this);
}
}
dispose() {
this._hoverDisposable?.dispose();
this._hoverDisposable = undefined;
this._disposables = dispose(this._disposables);
}
}
class GitBlameStatusBarItem {
private _statusBarItem: StatusBarItem;
private _disposables: IDisposable[] = [];
constructor(private readonly _controller: GitBlameController) {
this._statusBarItem = window.createStatusBarItem('git.blame', StatusBarAlignment.Right, 200);
this._statusBarItem.name = l10n.t('Git Blame Information');
this._disposables.push(this._statusBarItem);
workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables);
this._controller.onDidChangeBlameInformation(() => this._onDidChangeBlameInformation(), this, this._disposables);
}
private _onDidChangeConfiguration(e: ConfigurationChangeEvent): void {
if (!e.affectsConfiguration('git.commitShortHashLength') &&
!e.affectsConfiguration('git.blame.statusBarItem.template')) {
return;
}
this._onDidChangeBlameInformation();
}
private async _onDidChangeBlameInformation(): Promise<void> {
if (!window.activeTextEditor) {
this._statusBarItem.hide();
return;
}
const blameInformation = this._controller.textEditorBlameInformation?.blameInformation;
if (!blameInformation || blameInformation.length === 0) {
this._statusBarItem.hide();
return;
}
if (typeof blameInformation[0].blameInformation === 'string') {
this._statusBarItem.text = `$(git-commit) ${blameInformation[0].blameInformation}`;
this._statusBarItem.tooltip = l10n.t('Git Blame Information');
this._statusBarItem.command = undefined;
} else {
const config = workspace.getConfiguration('git');
const template = config.get<string>('blame.statusBarItem.template', '${authorName} (${authorDateAgo})');
this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage(
window.activeTextEditor.document.uri, template, blameInformation[0].blameInformation)}`;
this._statusBarItem.tooltip2 = (cancellationToken: CancellationToken) => {
return this._provideTooltip(window.activeTextEditor!.document.uri,
blameInformation[0].blameInformation as BlameInformation, cancellationToken);
};
this._statusBarItem.command = {
title: l10n.t('Open Commit'),
command: 'git.viewCommit',
arguments: [window.activeTextEditor.document.uri, blameInformation[0].blameInformation.hash]
} satisfies Command;
}
this._statusBarItem.show();
}
private async _provideTooltip(uri: Uri, blameInformation: BlameInformation, cancellationToken: CancellationToken): Promise<MarkdownString | undefined> {
if (cancellationToken.isCancellationRequested) {
return undefined;
}
const tooltip = await this._controller.getBlameInformationHover(uri, blameInformation);
return cancellationToken.isCancellationRequested ? undefined : tooltip;
}
dispose() {
this._disposables = dispose(this._disposables);
}
}
+52
View File
@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event, EventEmitter, Uri, workspace } from 'vscode';
import { BranchProtection, BranchProtectionProvider } from './api/git';
import { dispose, filterEvent } from './util';
export interface IBranchProtectionProviderRegistry {
readonly onDidChangeBranchProtectionProviders: Event<Uri>;
getBranchProtectionProviders(root: Uri): BranchProtectionProvider[];
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable;
}
export class GitBranchProtectionProvider implements BranchProtectionProvider {
private readonly _onDidChangeBranchProtection = new EventEmitter<Uri>();
onDidChangeBranchProtection = this._onDidChangeBranchProtection.event;
private branchProtection!: BranchProtection;
private disposables: Disposable[] = [];
constructor(private readonly repositoryRoot: Uri) {
const onDidChangeBranchProtectionEvent = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.branchProtection', repositoryRoot));
onDidChangeBranchProtectionEvent(this.updateBranchProtection, this, this.disposables);
this.updateBranchProtection();
}
provideBranchProtection(): BranchProtection[] {
return [this.branchProtection];
}
private updateBranchProtection(): void {
const scopedConfig = workspace.getConfiguration('git', this.repositoryRoot);
const branchProtectionConfig = scopedConfig.get<unknown>('branchProtection') ?? [];
const branchProtectionValues = Array.isArray(branchProtectionConfig) ? branchProtectionConfig : [branchProtectionConfig];
const branches = branchProtectionValues
.map(bp => typeof bp === 'string' ? bp.trim() : '')
.filter(bp => bp !== '');
this.branchProtection = { remote: '', rules: [{ include: branches }] };
this._onDidChangeBranchProtection.fire(this.repositoryRoot);
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}
+485
View File
@@ -0,0 +1,485 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
interface Item<K, V> {
previous: Item<K, V> | undefined;
next: Item<K, V> | undefined;
key: K;
value: V;
}
const enum Touch {
None = 0,
AsOld = 1,
AsNew = 2
}
class LinkedMap<K, V> implements Map<K, V> {
readonly [Symbol.toStringTag] = 'LinkedMap';
private _map: Map<K, Item<K, V>>;
private _head: Item<K, V> | undefined;
private _tail: Item<K, V> | undefined;
private _size: number;
private _state: number;
constructor() {
this._map = new Map<K, Item<K, V>>();
this._head = undefined;
this._tail = undefined;
this._size = 0;
this._state = 0;
}
clear(): void {
this._map.clear();
this._head = undefined;
this._tail = undefined;
this._size = 0;
this._state++;
}
isEmpty(): boolean {
return !this._head && !this._tail;
}
get size(): number {
return this._size;
}
get first(): V | undefined {
return this._head?.value;
}
get last(): V | undefined {
return this._tail?.value;
}
has(key: K): boolean {
return this._map.has(key);
}
get(key: K, touch: Touch = Touch.None): V | undefined {
const item = this._map.get(key);
if (!item) {
return undefined;
}
if (touch !== Touch.None) {
this.touch(item, touch);
}
return item.value;
}
set(key: K, value: V, touch: Touch = Touch.None): this {
let item = this._map.get(key);
if (item) {
item.value = value;
if (touch !== Touch.None) {
this.touch(item, touch);
}
} else {
item = { key, value, next: undefined, previous: undefined };
switch (touch) {
case Touch.None:
this.addItemLast(item);
break;
case Touch.AsOld:
this.addItemFirst(item);
break;
case Touch.AsNew:
this.addItemLast(item);
break;
default:
this.addItemLast(item);
break;
}
this._map.set(key, item);
this._size++;
}
return this;
}
delete(key: K): boolean {
return !!this.remove(key);
}
remove(key: K): V | undefined {
const item = this._map.get(key);
if (!item) {
return undefined;
}
this._map.delete(key);
this.removeItem(item);
this._size--;
return item.value;
}
shift(): V | undefined {
if (!this._head && !this._tail) {
return undefined;
}
if (!this._head || !this._tail) {
throw new Error('Invalid list');
}
const item = this._head;
this._map.delete(item.key);
this.removeItem(item);
this._size--;
return item.value;
}
forEach(callbackfn: (value: V, key: K, map: LinkedMap<K, V>) => void, thisArg?: any): void {
const state = this._state;
let current = this._head;
while (current) {
if (thisArg) {
callbackfn.bind(thisArg)(current.value, current.key, this);
} else {
callbackfn(current.value, current.key, this);
}
if (this._state !== state) {
throw new Error(`LinkedMap got modified during iteration.`);
}
current = current.next;
}
}
keys(): IterableIterator<K> {
const map = this;
const state = this._state;
let current = this._head;
const iterator: IterableIterator<K> = {
[Symbol.iterator]() {
return iterator;
},
next(): IteratorResult<K> {
if (map._state !== state) {
throw new Error(`LinkedMap got modified during iteration.`);
}
if (current) {
const result = { value: current.key, done: false };
current = current.next;
return result;
} else {
return { value: undefined, done: true };
}
}
};
return iterator;
}
values(): IterableIterator<V> {
const map = this;
const state = this._state;
let current = this._head;
const iterator: IterableIterator<V> = {
[Symbol.iterator]() {
return iterator;
},
next(): IteratorResult<V> {
if (map._state !== state) {
throw new Error(`LinkedMap got modified during iteration.`);
}
if (current) {
const result = { value: current.value, done: false };
current = current.next;
return result;
} else {
return { value: undefined, done: true };
}
}
};
return iterator;
}
entries(): IterableIterator<[K, V]> {
const map = this;
const state = this._state;
let current = this._head;
const iterator: IterableIterator<[K, V]> = {
[Symbol.iterator]() {
return iterator;
},
next(): IteratorResult<[K, V]> {
if (map._state !== state) {
throw new Error(`LinkedMap got modified during iteration.`);
}
if (current) {
const result: IteratorResult<[K, V]> = { value: [current.key, current.value], done: false };
current = current.next;
return result;
} else {
return { value: undefined, done: true };
}
}
};
return iterator;
}
[Symbol.iterator](): IterableIterator<[K, V]> {
return this.entries();
}
protected trimOld(newSize: number) {
if (newSize >= this.size) {
return;
}
if (newSize === 0) {
this.clear();
return;
}
let current = this._head;
let currentSize = this.size;
while (current && currentSize > newSize) {
this._map.delete(current.key);
current = current.next;
currentSize--;
}
this._head = current;
this._size = currentSize;
if (current) {
current.previous = undefined;
}
this._state++;
}
protected trimNew(newSize: number) {
if (newSize >= this.size) {
return;
}
if (newSize === 0) {
this.clear();
return;
}
let current = this._tail;
let currentSize = this.size;
while (current && currentSize > newSize) {
this._map.delete(current.key);
current = current.previous;
currentSize--;
}
this._tail = current;
this._size = currentSize;
if (current) {
current.next = undefined;
}
this._state++;
}
private addItemFirst(item: Item<K, V>): void {
// First time Insert
if (!this._head && !this._tail) {
this._tail = item;
} else if (!this._head) {
throw new Error('Invalid list');
} else {
item.next = this._head;
this._head.previous = item;
}
this._head = item;
this._state++;
}
private addItemLast(item: Item<K, V>): void {
// First time Insert
if (!this._head && !this._tail) {
this._head = item;
} else if (!this._tail) {
throw new Error('Invalid list');
} else {
item.previous = this._tail;
this._tail.next = item;
}
this._tail = item;
this._state++;
}
private removeItem(item: Item<K, V>): void {
if (item === this._head && item === this._tail) {
this._head = undefined;
this._tail = undefined;
}
else if (item === this._head) {
// This can only happen if size === 1 which is handled
// by the case above.
if (!item.next) {
throw new Error('Invalid list');
}
item.next.previous = undefined;
this._head = item.next;
}
else if (item === this._tail) {
// This can only happen if size === 1 which is handled
// by the case above.
if (!item.previous) {
throw new Error('Invalid list');
}
item.previous.next = undefined;
this._tail = item.previous;
}
else {
const next = item.next;
const previous = item.previous;
if (!next || !previous) {
throw new Error('Invalid list');
}
next.previous = previous;
previous.next = next;
}
item.next = undefined;
item.previous = undefined;
this._state++;
}
private touch(item: Item<K, V>, touch: Touch): void {
if (!this._head || !this._tail) {
throw new Error('Invalid list');
}
if ((touch !== Touch.AsOld && touch !== Touch.AsNew)) {
return;
}
if (touch === Touch.AsOld) {
if (item === this._head) {
return;
}
const next = item.next;
const previous = item.previous;
// Unlink the item
if (item === this._tail) {
// previous must be defined since item was not head but is tail
// So there are more than on item in the map
previous!.next = undefined;
this._tail = previous;
}
else {
// Both next and previous are not undefined since item was neither head nor tail.
next!.previous = previous;
previous!.next = next;
}
// Insert the node at head
item.previous = undefined;
item.next = this._head;
this._head.previous = item;
this._head = item;
this._state++;
} else if (touch === Touch.AsNew) {
if (item === this._tail) {
return;
}
const next = item.next;
const previous = item.previous;
// Unlink the item.
if (item === this._head) {
// next must be defined since item was not tail but is head
// So there are more than on item in the map
next!.previous = undefined;
this._head = next;
} else {
// Both next and previous are not undefined since item was neither head nor tail.
next!.previous = previous;
previous!.next = next;
}
item.next = undefined;
item.previous = this._tail;
this._tail.next = item;
this._tail = item;
this._state++;
}
}
toJSON(): [K, V][] {
const data: [K, V][] = [];
this.forEach((value, key) => {
data.push([key, value]);
});
return data;
}
fromJSON(data: [K, V][]): void {
this.clear();
for (const [key, value] of data) {
this.set(key, value);
}
}
}
abstract class Cache<K, V> extends LinkedMap<K, V> {
protected _limit: number;
protected _ratio: number;
constructor(limit: number, ratio: number = 1) {
super();
this._limit = limit;
this._ratio = Math.min(Math.max(0, ratio), 1);
}
get limit(): number {
return this._limit;
}
set limit(limit: number) {
this._limit = limit;
this.checkTrim();
}
get ratio(): number {
return this._ratio;
}
set ratio(ratio: number) {
this._ratio = Math.min(Math.max(0, ratio), 1);
this.checkTrim();
}
override get(key: K, touch: Touch = Touch.AsNew): V | undefined {
return super.get(key, touch);
}
peek(key: K): V | undefined {
return super.get(key, Touch.None);
}
override set(key: K, value: V): this {
super.set(key, value, Touch.AsNew);
return this;
}
protected checkTrim() {
if (this.size > this._limit) {
this.trim(Math.round(this._limit * this._ratio));
}
}
protected abstract trim(newSize: number): void;
}
export class LRUCache<K, V> extends Cache<K, V> {
constructor(limit: number, ratio: number = 1) {
super(limit, ratio);
}
protected override trim(newSize: number) {
this.trimOld(newSize);
}
override set(key: K, value: V): this {
super.set(key, value);
this.checkTrim();
return this;
}
}
File diff suppressed because it is too large Load Diff
+339
View File
@@ -0,0 +1,339 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor, l10n, SourceControlHistoryItemRef } from 'vscode';
import * as path from 'path';
import { Repository, GitResourceGroup } from './repository';
import { Model } from './model';
import { debounce } from './decorators';
import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource, combinedDisposable, runAndSubscribeEvent } from './util';
import { Change, GitErrorCodes, Status } from './api/git';
function equalSourceControlHistoryItemRefs(ref1?: SourceControlHistoryItemRef, ref2?: SourceControlHistoryItemRef): boolean {
if (ref1 === ref2) {
return true;
}
return ref1?.id === ref2?.id &&
ref1?.name === ref2?.name &&
ref1?.revision === ref2?.revision;
}
class GitIgnoreDecorationProvider implements FileDecorationProvider {
private static Decoration: FileDecoration = { color: new ThemeColor('gitDecoration.ignoredResourceForeground') };
readonly onDidChangeFileDecorations: Event<Uri[]>;
private queue = new Map<string, { repository: Repository; queue: Map<string, PromiseSource<FileDecoration | undefined>> }>();
private disposables: Disposable[] = [];
constructor(private model: Model) {
this.onDidChangeFileDecorations = fireEvent(anyEvent<any>(
filterEvent(workspace.onDidSaveTextDocument, e => /\.gitignore$|\.git\/info\/exclude$/.test(e.uri.path)),
model.onDidOpenRepository,
model.onDidCloseRepository
));
this.disposables.push(window.registerFileDecorationProvider(this));
}
async provideFileDecoration(uri: Uri): Promise<FileDecoration | undefined> {
const repository = this.model.getRepository(uri);
if (!repository) {
return;
}
let queueItem = this.queue.get(repository.root);
if (!queueItem) {
queueItem = { repository, queue: new Map<string, PromiseSource<FileDecoration | undefined>>() };
this.queue.set(repository.root, queueItem);
}
let promiseSource = queueItem.queue.get(uri.fsPath);
if (!promiseSource) {
promiseSource = new PromiseSource();
queueItem!.queue.set(uri.fsPath, promiseSource);
this.checkIgnoreSoon();
}
return await promiseSource.promise;
}
@debounce(500)
private checkIgnoreSoon(): void {
const queue = new Map(this.queue.entries());
this.queue.clear();
for (const [, item] of queue) {
const paths = [...item.queue.keys()];
item.repository.checkIgnore(paths).then(ignoreSet => {
for (const [path, promiseSource] of item.queue.entries()) {
promiseSource.resolve(ignoreSet.has(path) ? GitIgnoreDecorationProvider.Decoration : undefined);
}
}, err => {
if (err.gitErrorCode !== GitErrorCodes.IsInSubmodule) {
console.error(err);
}
for (const [, promiseSource] of item.queue.entries()) {
promiseSource.reject(err);
}
});
}
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
this.queue.clear();
}
}
class GitDecorationProvider implements FileDecorationProvider {
private static SubmoduleDecorationData: FileDecoration = {
tooltip: 'Submodule',
badge: 'S',
color: new ThemeColor('gitDecoration.submoduleResourceForeground')
};
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
private disposables: Disposable[] = [];
private decorations = new Map<string, FileDecoration>();
constructor(private repository: Repository) {
this.disposables.push(
window.registerFileDecorationProvider(this),
runAndSubscribeEvent(repository.onDidRunGitStatus, () => this.onDidRunGitStatus())
);
}
private onDidRunGitStatus(): void {
const newDecorations = new Map<string, FileDecoration>();
this.collectDecorationData(this.repository.indexGroup, newDecorations);
this.collectDecorationData(this.repository.untrackedGroup, newDecorations);
this.collectDecorationData(this.repository.workingTreeGroup, newDecorations);
this.collectDecorationData(this.repository.mergeGroup, newDecorations);
this.collectSubmoduleDecorationData(newDecorations);
const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()]));
this.decorations = newDecorations;
this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));
}
private collectDecorationData(group: GitResourceGroup, bucket: Map<string, FileDecoration>): void {
for (const r of group.resourceStates) {
const decoration = r.resourceDecoration;
if (decoration) {
// not deleted and has a decoration
bucket.set(r.original.toString(), decoration);
if (r.type === Status.DELETED && r.rightUri) {
bucket.set(r.rightUri.toString(), decoration);
}
if (r.type === Status.INDEX_RENAMED || r.type === Status.INTENT_TO_RENAME) {
bucket.set(r.resourceUri.toString(), decoration);
}
}
}
}
private collectSubmoduleDecorationData(bucket: Map<string, FileDecoration>): void {
for (const submodule of this.repository.submodules) {
bucket.set(Uri.file(path.join(this.repository.root, submodule.path)).toString(), GitDecorationProvider.SubmoduleDecorationData);
}
}
provideFileDecoration(uri: Uri): FileDecoration | undefined {
return this.decorations.get(uri.toString());
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}
class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider {
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
private _currentHistoryItemRef: SourceControlHistoryItemRef | undefined;
private _currentHistoryItemRemoteRef: SourceControlHistoryItemRef | undefined;
private _decorations = new Map<string, FileDecoration>();
private readonly disposables: Disposable[] = [];
constructor(private readonly repository: Repository) {
this.disposables.push(
window.registerFileDecorationProvider(this),
runAndSubscribeEvent(repository.historyProvider.onDidChangeCurrentHistoryItemRefs, () => this.onDidChangeCurrentHistoryItemRefs())
);
}
private async onDidChangeCurrentHistoryItemRefs(): Promise<void> {
const historyProvider = this.repository.historyProvider;
const currentHistoryItemRef = historyProvider.currentHistoryItemRef;
const currentHistoryItemRemoteRef = historyProvider.currentHistoryItemRemoteRef;
if (equalSourceControlHistoryItemRefs(this._currentHistoryItemRef, currentHistoryItemRef) &&
equalSourceControlHistoryItemRefs(this._currentHistoryItemRemoteRef, currentHistoryItemRemoteRef)) {
return;
}
const decorations = new Map<string, FileDecoration>();
await this.collectIncomingChangesFileDecorations(decorations);
const uris = new Set([...this._decorations.keys()].concat([...decorations.keys()]));
this._decorations = decorations;
this._currentHistoryItemRef = currentHistoryItemRef;
this._currentHistoryItemRemoteRef = currentHistoryItemRemoteRef;
this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));
}
private async collectIncomingChangesFileDecorations(bucket: Map<string, FileDecoration>): Promise<void> {
for (const change of await this.getIncomingChanges()) {
switch (change.status) {
case Status.INDEX_ADDED:
bucket.set(change.uri.toString(), {
badge: '↓A',
tooltip: l10n.t('Incoming Changes (added)'),
});
break;
case Status.DELETED:
bucket.set(change.uri.toString(), {
badge: '↓D',
tooltip: l10n.t('Incoming Changes (deleted)'),
});
break;
case Status.INDEX_RENAMED:
bucket.set(change.originalUri.toString(), {
badge: '↓R',
tooltip: l10n.t('Incoming Changes (renamed)'),
});
break;
case Status.MODIFIED:
bucket.set(change.uri.toString(), {
badge: '↓M',
tooltip: l10n.t('Incoming Changes (modified)'),
});
break;
default: {
bucket.set(change.uri.toString(), {
badge: '↓~',
tooltip: l10n.t('Incoming Changes'),
});
break;
}
}
}
}
private async getIncomingChanges(): Promise<Change[]> {
try {
const historyProvider = this.repository.historyProvider;
const currentHistoryItemRef = historyProvider.currentHistoryItemRef;
const currentHistoryItemRemoteRef = historyProvider.currentHistoryItemRemoteRef;
if (!currentHistoryItemRef || !currentHistoryItemRemoteRef) {
return [];
}
const ancestor = await historyProvider.resolveHistoryItemRefsCommonAncestor([currentHistoryItemRef.id, currentHistoryItemRemoteRef.id]);
if (!ancestor) {
return [];
}
const changes = await this.repository.diffBetween(ancestor, currentHistoryItemRemoteRef.id);
return changes;
} catch (err) {
return [];
}
}
provideFileDecoration(uri: Uri): FileDecoration | undefined {
return this._decorations.get(uri.toString());
}
dispose(): void {
dispose(this.disposables);
}
}
export class GitDecorations {
private enabled = false;
private disposables: Disposable[] = [];
private modelDisposables: Disposable[] = [];
private providers = new Map<Repository, Disposable>();
constructor(private model: Model) {
this.disposables.push(new GitIgnoreDecorationProvider(model));
const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.decorations.enabled'));
onEnablementChange(this.update, this, this.disposables);
this.update();
}
private update(): void {
const config = workspace.getConfiguration('git');
const enabled = config.get<boolean>('decorations.enabled') === true;
if (this.enabled === enabled) {
return;
}
if (enabled) {
this.enable();
} else {
this.disable();
}
this.enabled = enabled;
}
private enable(): void {
this.model.onDidOpenRepository(this.onDidOpenRepository, this, this.modelDisposables);
this.model.onDidCloseRepository(this.onDidCloseRepository, this, this.modelDisposables);
this.model.repositories.forEach(this.onDidOpenRepository, this);
}
private disable(): void {
this.modelDisposables = dispose(this.modelDisposables);
this.providers.forEach(value => value.dispose());
this.providers.clear();
}
private onDidOpenRepository(repository: Repository): void {
const providers = combinedDisposable([
new GitDecorationProvider(repository),
new GitIncomingChangesFileDecorationProvider(repository)
]);
this.providers.set(repository, providers);
}
private onDidCloseRepository(repository: Repository): void {
const provider = this.providers.get(repository);
if (provider) {
provider.dispose();
this.providers.delete(repository);
}
}
dispose(): void {
this.disable();
this.disposables = dispose(this.disposables);
}
}
+101
View File
@@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { done } from './util';
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 _memoize(fn: Function, key: string): Function {
const memoizeKey = `$memoize$${key}`;
return function (this: any, ...args: any[]) {
if (!this.hasOwnProperty(memoizeKey)) {
Object.defineProperty(this, memoizeKey, {
configurable: false,
enumerable: false,
writable: false,
value: fn.apply(this, args)
});
}
return this[memoizeKey];
};
}
export const memoize = decorate(_memoize);
function _throttle<T>(fn: Function, key: string): Function {
const currentKey = `$throttle$current$${key}`;
const nextKey = `$throttle$next$${key}`;
const trigger = function (this: any, ...args: any[]) {
if (this[nextKey]) {
return this[nextKey];
}
if (this[currentKey]) {
this[nextKey] = done(this[currentKey]).then(() => {
this[nextKey] = undefined;
return trigger.apply(this, args);
});
return this[nextKey];
}
this[currentKey] = fn.apply(this, args) as Promise<T>;
const clear = () => this[currentKey] = undefined;
done(this[currentKey]).then(clear, clear);
return this[currentKey];
};
return trigger;
}
export const throttle = decorate(_throttle);
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 debounce(delay: number): Function {
return decorate((fn, key) => {
const timerKey = `$debounce$${key}`;
return function (this: any, ...args: any[]) {
clearTimeout(this[timerKey]);
this[timerKey] = setTimeout(() => fn.apply(this, args), delay);
};
});
}
+224
View File
@@ -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 { CodeAction, CodeActionKind, CodeActionProvider, Diagnostic, DiagnosticCollection, DiagnosticSeverity, Disposable, Range, Selection, TextDocument, Uri, WorkspaceEdit, l10n, languages, workspace } from 'vscode';
import { mapEvent, filterEvent, dispose } from './util';
import { Model } from './model';
export enum DiagnosticCodes {
empty_message = 'empty_message',
line_length = 'line_length'
}
export class GitCommitInputBoxDiagnosticsManager {
private readonly diagnostics: DiagnosticCollection;
private readonly severity = DiagnosticSeverity.Warning;
private readonly disposables: Disposable[] = [];
constructor(private readonly model: Model) {
this.diagnostics = languages.createDiagnosticCollection();
this.migrateInputValidationSettings()
.then(() => {
mapEvent(filterEvent(workspace.onDidChangeTextDocument, e => e.document.uri.scheme === 'vscode-scm'), e => e.document)(this.onDidChangeTextDocument, this, this.disposables);
filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.inputValidation') || e.affectsConfiguration('git.inputValidationLength') || e.affectsConfiguration('git.inputValidationSubjectLength'))(this.onDidChangeConfiguration, this, this.disposables);
});
}
public getDiagnostics(uri: Uri): ReadonlyArray<Diagnostic> {
return this.diagnostics.get(uri) ?? [];
}
private async migrateInputValidationSettings(): Promise<void> {
try {
const config = workspace.getConfiguration('git');
const inputValidation = config.inspect<'always' | 'warn' | 'off' | boolean>('inputValidation');
if (inputValidation === undefined) {
return;
}
// Workspace setting
if (typeof inputValidation.workspaceValue === 'string') {
await config.update('inputValidation', inputValidation.workspaceValue !== 'off', false);
}
// User setting
if (typeof inputValidation.globalValue === 'string') {
await config.update('inputValidation', inputValidation.workspaceValue !== 'off', true);
}
} catch { }
}
private onDidChangeConfiguration(): void {
for (const repository of this.model.repositories) {
this.onDidChangeTextDocument(repository.inputBox.document);
}
}
private onDidChangeTextDocument(document: TextDocument): void {
const config = workspace.getConfiguration('git');
const inputValidation = config.get<boolean>('inputValidation', false);
if (!inputValidation) {
this.diagnostics.set(document.uri, undefined);
return;
}
if (/^\s+$/.test(document.getText())) {
const documentRange = new Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end);
const diagnostic = new Diagnostic(documentRange, l10n.t('Current commit message only contains whitespace characters'), this.severity);
diagnostic.code = DiagnosticCodes.empty_message;
this.diagnostics.set(document.uri, [diagnostic]);
return;
}
const diagnostics: Diagnostic[] = [];
const inputValidationLength = config.get<number>('inputValidationLength', 50);
const inputValidationSubjectLength = config.get<number | undefined>('inputValidationSubjectLength', undefined);
for (let index = 0; index < document.lineCount; index++) {
const line = document.lineAt(index);
const threshold = index === 0 ? inputValidationSubjectLength ?? inputValidationLength : inputValidationLength;
if (line.text.length > threshold) {
const diagnostic = new Diagnostic(line.range, l10n.t('{0} characters over {1} in current line', line.text.length - threshold, threshold), this.severity);
diagnostic.code = DiagnosticCodes.line_length;
diagnostics.push(diagnostic);
}
}
this.diagnostics.set(document.uri, diagnostics);
}
dispose() {
dispose(this.disposables);
}
}
export class GitCommitInputBoxCodeActionsProvider implements CodeActionProvider {
private readonly disposables: Disposable[] = [];
constructor(private readonly diagnosticsManager: GitCommitInputBoxDiagnosticsManager) {
this.disposables.push(languages.registerCodeActionsProvider({ scheme: 'vscode-scm' }, this));
}
provideCodeActions(document: TextDocument, range: Range | Selection): CodeAction[] {
const codeActions: CodeAction[] = [];
const diagnostics = this.diagnosticsManager.getDiagnostics(document.uri);
const wrapAllLinesCodeAction = this.getWrapAllLinesCodeAction(document, diagnostics);
for (const diagnostic of diagnostics) {
if (!diagnostic.range.contains(range)) {
continue;
}
switch (diagnostic.code) {
case DiagnosticCodes.empty_message: {
const workspaceEdit = new WorkspaceEdit();
workspaceEdit.delete(document.uri, diagnostic.range);
const codeAction = new CodeAction(l10n.t('Clear whitespace characters'), CodeActionKind.QuickFix);
codeAction.diagnostics = [diagnostic];
codeAction.edit = workspaceEdit;
codeActions.push(codeAction);
break;
}
case DiagnosticCodes.line_length: {
const workspaceEdit = this.getWrapLineWorkspaceEdit(document, diagnostic.range);
const codeAction = new CodeAction(l10n.t('Hard wrap line'), CodeActionKind.QuickFix);
codeAction.diagnostics = [diagnostic];
codeAction.edit = workspaceEdit;
codeActions.push(codeAction);
if (wrapAllLinesCodeAction) {
wrapAllLinesCodeAction.diagnostics = [diagnostic];
codeActions.push(wrapAllLinesCodeAction);
}
break;
}
}
}
return codeActions;
}
private getWrapLineWorkspaceEdit(document: TextDocument, range: Range): WorkspaceEdit {
const lineSegments = this.wrapTextDocumentLine(document, range.start.line);
const workspaceEdit = new WorkspaceEdit();
workspaceEdit.replace(document.uri, range, lineSegments.join('\n'));
return workspaceEdit;
}
private getWrapAllLinesCodeAction(document: TextDocument, diagnostics: readonly Diagnostic[]): CodeAction | undefined {
const lineLengthDiagnostics = diagnostics.filter(d => d.code === DiagnosticCodes.line_length);
if (lineLengthDiagnostics.length < 2) {
return undefined;
}
const wrapAllLinesCodeAction = new CodeAction(l10n.t('Hard wrap all lines'), CodeActionKind.QuickFix);
wrapAllLinesCodeAction.edit = this.getWrapAllLinesWorkspaceEdit(document, lineLengthDiagnostics);
return wrapAllLinesCodeAction;
}
private getWrapAllLinesWorkspaceEdit(document: TextDocument, diagnostics: Diagnostic[]): WorkspaceEdit {
const workspaceEdit = new WorkspaceEdit();
for (const diagnostic of diagnostics) {
const lineSegments = this.wrapTextDocumentLine(document, diagnostic.range.start.line);
workspaceEdit.replace(document.uri, diagnostic.range, lineSegments.join('\n'));
}
return workspaceEdit;
}
private wrapTextDocumentLine(document: TextDocument, line: number): string[] {
const config = workspace.getConfiguration('git');
const inputValidationLength = config.get<number>('inputValidationLength', 50);
const inputValidationSubjectLength = config.get<number | undefined>('inputValidationSubjectLength', undefined);
const lineLengthThreshold = line === 0 ? inputValidationSubjectLength ?? inputValidationLength : inputValidationLength;
const lineSegments: string[] = [];
const lineText = document.lineAt(line).text.trim();
let position = 0;
while (lineText.length - position > lineLengthThreshold) {
const lastSpaceBeforeThreshold = lineText.lastIndexOf(' ', position + lineLengthThreshold);
if (lastSpaceBeforeThreshold !== -1 && lastSpaceBeforeThreshold > position) {
lineSegments.push(lineText.substring(position, lastSpaceBeforeThreshold));
position = lastSpaceBeforeThreshold + 1;
} else {
// Find first space after threshold
const firstSpaceAfterThreshold = lineText.indexOf(' ', position + lineLengthThreshold);
if (firstSpaceAfterThreshold !== -1) {
lineSegments.push(lineText.substring(position, firstSpaceAfterThreshold));
position = firstSpaceAfterThreshold + 1;
} else {
lineSegments.push(lineText.substring(position));
position = lineText.length;
}
}
}
if (position < lineText.length) {
lineSegments.push(lineText.substring(position));
}
return lineSegments;
}
dispose() {
dispose(this.disposables);
}
}
@@ -0,0 +1,115 @@
/*---------------------------------------------------------------------------------------------
* 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 vscode from 'vscode';
import { RefType } from './api/git';
import { Model } from './model';
export class GitEditSessionIdentityProvider implements vscode.EditSessionIdentityProvider, vscode.Disposable {
private providerRegistration: vscode.Disposable;
constructor(private model: Model) {
this.providerRegistration = vscode.workspace.registerEditSessionIdentityProvider('file', this);
vscode.workspace.onWillCreateEditSessionIdentity((e) => {
e.waitUntil(this._onWillCreateEditSessionIdentity(e.workspaceFolder));
});
}
dispose() {
this.providerRegistration.dispose();
}
async provideEditSessionIdentity(workspaceFolder: vscode.WorkspaceFolder, token: vscode.CancellationToken): Promise<string | undefined> {
await this.model.openRepository(path.dirname(workspaceFolder.uri.fsPath));
const repository = this.model.getRepository(workspaceFolder.uri);
await repository?.status();
if (!repository || !repository?.HEAD?.upstream) {
return undefined;
}
const remoteUrl = repository.remotes.find((remote) => remote.name === repository.HEAD?.upstream?.remote)?.pushUrl?.replace(/^(git@[^\/:]+)(:)/i, 'ssh://$1/');
const remote = remoteUrl ? await vscode.workspace.getCanonicalUri(vscode.Uri.parse(remoteUrl), { targetScheme: 'https' }, token) : null;
return JSON.stringify({
remote: remote?.toString() ?? remoteUrl,
ref: repository.HEAD?.upstream?.name ?? null,
sha: repository.HEAD?.commit ?? null,
});
}
provideEditSessionIdentityMatch(identity1: string, identity2: string): vscode.EditSessionIdentityMatch {
try {
const normalizedIdentity1 = normalizeEditSessionIdentity(identity1);
const normalizedIdentity2 = normalizeEditSessionIdentity(identity2);
if (normalizedIdentity1.remote === normalizedIdentity2.remote &&
normalizedIdentity1.ref === normalizedIdentity2.ref &&
normalizedIdentity1.sha === normalizedIdentity2.sha) {
// This is a perfect match
return vscode.EditSessionIdentityMatch.Complete;
} else if (normalizedIdentity1.remote === normalizedIdentity2.remote &&
normalizedIdentity1.ref === normalizedIdentity2.ref &&
normalizedIdentity1.sha !== normalizedIdentity2.sha) {
// Same branch and remote but different SHA
return vscode.EditSessionIdentityMatch.Partial;
} else {
return vscode.EditSessionIdentityMatch.None;
}
} catch (ex) {
return vscode.EditSessionIdentityMatch.Partial;
}
}
private async _onWillCreateEditSessionIdentity(workspaceFolder: vscode.WorkspaceFolder): Promise<void> {
await this._doPublish(workspaceFolder);
}
private async _doPublish(workspaceFolder: vscode.WorkspaceFolder) {
await this.model.openRepository(path.dirname(workspaceFolder.uri.fsPath));
const repository = this.model.getRepository(workspaceFolder.uri);
if (!repository) {
return;
}
await repository.status();
// If this branch hasn't been published to the remote yet,
// ensure that it is published before Continue On is invoked
if (!repository.HEAD?.upstream && repository.HEAD?.type === RefType.Head) {
const publishBranch = vscode.l10n.t('Publish Branch');
const selection = await vscode.window.showInformationMessage(
vscode.l10n.t('The current branch is not published to the remote. Would you like to publish it to access your changes elsewhere?'),
{ modal: true },
publishBranch
);
if (selection !== publishBranch) {
throw new vscode.CancellationError();
}
await vscode.commands.executeCommand('git.publish');
}
}
}
function normalizeEditSessionIdentity(identity: string) {
let { remote, ref, sha } = JSON.parse(identity);
if (typeof remote === 'string' && remote.endsWith('.git')) {
remote = remote.slice(0, remote.length - 4);
}
return {
remote,
ref,
sha
};
}
+39
View File
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { workspace, Uri } from 'vscode';
import { getExtensionContext } from './main';
import { TextDecoder } from 'util';
const emojiRegex = /:([-+_a-z0-9]+):/g;
let emojiMap: Record<string, string> | undefined;
let emojiMapPromise: Promise<void> | undefined;
export async function ensureEmojis() {
if (emojiMap === undefined) {
if (emojiMapPromise === undefined) {
emojiMapPromise = loadEmojiMap();
}
await emojiMapPromise;
}
}
async function loadEmojiMap() {
const context = getExtensionContext();
const uri = (Uri as any).joinPath(context.extensionUri, 'resources', 'emojis.json');
emojiMap = JSON.parse(new TextDecoder('utf8').decode(await workspace.fs.readFile(uri)));
}
export function emojify(message: string) {
if (emojiMap === undefined) {
return message;
}
return message.replace(emojiRegex, (s, code) => {
return emojiMap?.[code] || s;
});
}
+235
View File
@@ -0,0 +1,235 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, Uri, Disposable, Event, EventEmitter, window, FileSystemProvider, FileChangeEvent, FileStat, FileType, FileChangeType, FileSystemError, LogOutputChannel } from 'vscode';
import { debounce, throttle } from './decorators';
import { fromGitUri, toGitUri } from './uri';
import { Model, ModelChangeEvent, OriginalResourceChangeEvent } from './model';
import { filterEvent, eventToPromise, isDescendant, pathEquals, EmptyDisposable } from './util';
import { Repository } from './repository';
interface CacheRow {
uri: Uri;
timestamp: number;
}
const THREE_MINUTES = 1000 * 60 * 3;
const FIVE_MINUTES = 1000 * 60 * 5;
function sanitizeRef(ref: string, path: string, submoduleOf: string | undefined, repository: Repository): string {
if (ref === '~') {
const fileUri = Uri.file(path);
const uriString = fileUri.toString();
const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
return indexStatus ? '' : 'HEAD';
}
if (/^~\d$/.test(ref)) {
return `:${ref[1]}`;
}
// Submodule HEAD
if (submoduleOf && (ref === 'index' || ref === 'wt')) {
return 'HEAD';
}
return ref;
}
export class GitFileSystemProvider implements FileSystemProvider {
private _onDidChangeFile = new EventEmitter<FileChangeEvent[]>();
readonly onDidChangeFile: Event<FileChangeEvent[]> = this._onDidChangeFile.event;
private changedRepositoryRoots = new Set<string>();
private cache = new Map<string, CacheRow>();
private mtime = new Date().getTime();
private disposables: Disposable[] = [];
constructor(private readonly model: Model, private readonly logger: LogOutputChannel) {
this.disposables.push(
model.onDidChangeRepository(this.onDidChangeRepository, this),
model.onDidChangeOriginalResource(this.onDidChangeOriginalResource, this),
workspace.registerFileSystemProvider('git', this, { isReadonly: true, isCaseSensitive: true }),
);
setInterval(() => this.cleanup(), FIVE_MINUTES);
}
private onDidChangeRepository({ repository }: ModelChangeEvent): void {
this.changedRepositoryRoots.add(repository.root);
this.eventuallyFireChangeEvents();
}
private onDidChangeOriginalResource({ uri }: OriginalResourceChangeEvent): void {
if (uri.scheme !== 'file') {
return;
}
const diffOriginalResourceUri = toGitUri(uri, '~',);
const quickDiffOriginalResourceUri = toGitUri(uri, '', { replaceFileExtension: true });
this.mtime = new Date().getTime();
this._onDidChangeFile.fire([
{ type: FileChangeType.Changed, uri: diffOriginalResourceUri },
{ type: FileChangeType.Changed, uri: quickDiffOriginalResourceUri }
]);
}
@debounce(1100)
private eventuallyFireChangeEvents(): void {
this.fireChangeEvents();
}
@throttle
private async fireChangeEvents(): Promise<void> {
if (!window.state.focused) {
const onDidFocusWindow = filterEvent(window.onDidChangeWindowState, e => e.focused);
await eventToPromise(onDidFocusWindow);
}
const events: FileChangeEvent[] = [];
for (const { uri } of this.cache.values()) {
const fsPath = uri.fsPath;
for (const root of this.changedRepositoryRoots) {
if (isDescendant(root, fsPath)) {
events.push({ type: FileChangeType.Changed, uri });
break;
}
}
}
if (events.length > 0) {
this.mtime = new Date().getTime();
this._onDidChangeFile.fire(events);
}
this.changedRepositoryRoots.clear();
}
private cleanup(): void {
const now = new Date().getTime();
const cache = new Map<string, CacheRow>();
for (const row of this.cache.values()) {
const { path } = fromGitUri(row.uri);
const isOpen = workspace.textDocuments
.filter(d => d.uri.scheme === 'file')
.some(d => pathEquals(d.uri.fsPath, path));
if (isOpen || now - row.timestamp < THREE_MINUTES) {
cache.set(row.uri.toString(), row);
} else {
// TODO: should fire delete events?
}
}
this.cache = cache;
}
watch(): Disposable {
return EmptyDisposable;
}
async stat(uri: Uri): Promise<FileStat> {
await this.model.isInitialized;
const { submoduleOf, path, ref } = fromGitUri(uri);
const repository = submoduleOf ? this.model.getRepository(submoduleOf) : this.model.getRepository(uri);
if (!repository) {
this.logger.warn(`[GitFileSystemProvider][stat] Repository not found - ${uri.toString()}`);
throw FileSystemError.FileNotFound();
}
try {
const details = await repository.getObjectDetails(sanitizeRef(ref, path, submoduleOf, repository), path);
return { type: FileType.File, size: details.size, mtime: this.mtime, ctime: 0 };
} catch {
// Empty tree
if (ref === await repository.getEmptyTree()) {
this.logger.warn(`[GitFileSystemProvider][stat] Empty tree - ${uri.toString()}`);
return { type: FileType.File, size: 0, mtime: this.mtime, ctime: 0 };
}
// File does not exist in git. This could be because the file is untracked or ignored
this.logger.warn(`[GitFileSystemProvider][stat] File not found - ${uri.toString()}`);
throw FileSystemError.FileNotFound();
}
}
readDirectory(): Thenable<[string, FileType][]> {
throw new Error('Method not implemented.');
}
createDirectory(): void {
throw new Error('Method not implemented.');
}
async readFile(uri: Uri): Promise<Uint8Array> {
await this.model.isInitialized;
const { path, ref, submoduleOf } = fromGitUri(uri);
if (submoduleOf) {
const repository = this.model.getRepository(submoduleOf);
if (!repository) {
throw FileSystemError.FileNotFound();
}
const encoder = new TextEncoder();
if (ref === 'index') {
return encoder.encode(await repository.diffIndexWithHEAD(path));
} else {
return encoder.encode(await repository.diffWithHEAD(path));
}
}
const repository = this.model.getRepository(uri);
if (!repository) {
this.logger.warn(`[GitFileSystemProvider][readFile] Repository not found - ${uri.toString()}`);
throw FileSystemError.FileNotFound();
}
const timestamp = new Date().getTime();
const cacheValue: CacheRow = { uri, timestamp };
this.cache.set(uri.toString(), cacheValue);
try {
return await repository.buffer(sanitizeRef(ref, path, submoduleOf, repository), path);
} catch {
// Empty tree
if (ref === await repository.getEmptyTree()) {
this.logger.warn(`[GitFileSystemProvider][readFile] Empty tree - ${uri.toString()}`);
return new Uint8Array(0);
}
// File does not exist in git. This could be because the file is untracked or ignored
this.logger.warn(`[GitFileSystemProvider][readFile] File not found - ${uri.toString()}`);
throw FileSystemError.FileNotFound();
}
}
writeFile(): void {
throw new Error('Method not implemented.');
}
delete(): void {
throw new Error('Method not implemented.');
}
rename(): void {
throw new Error('Method not implemented.');
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}
+30
View File
@@ -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 { extensions } from 'vscode';
import { API as GitBaseAPI, GitBaseExtension } from './typings/git-base';
export class GitBaseApi {
private static _gitBaseApi: GitBaseAPI | undefined;
static getAPI(): GitBaseAPI {
if (!this._gitBaseApi) {
const gitBaseExtension = extensions.getExtension<GitBaseExtension>('vscode.git-base')!.exports;
const onDidChangeGitBaseExtensionEnablement = (enabled: boolean) => {
this._gitBaseApi = enabled ? gitBaseExtension.getAPI(1) : undefined;
};
gitBaseExtension.onDidChangeEnablement(onDidChangeGitBaseExtensionEnablement);
onDidChangeGitBaseExtensionEnablement(gitBaseExtension.enabled);
if (!this._gitBaseApi) {
throw new Error('vscode.git-base extension is not enabled.');
}
}
return this._gitBaseApi;
}
}
+1
View File
@@ -0,0 +1 @@
#!/bin/sh
+21
View File
@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IPCClient } from './ipc/ipcClient';
function fatal(err: any): void {
console.error(err);
process.exit(1);
}
function main(argv: string[]): void {
const ipcClient = new IPCClient('git-editor');
const commitMessagePath = argv[argv.length - 1];
ipcClient.call({ commitMessagePath }).then(() => {
setTimeout(() => process.exit(0), 0);
}).catch(err => fatal(err));
}
main(process.argv);
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
ELECTRON_RUN_AS_NODE="1" \
"$VSCODE_GIT_EDITOR_NODE" "$VSCODE_GIT_EDITOR_MAIN" $VSCODE_GIT_EDITOR_EXTRA_ARGS "$@"
File diff suppressed because it is too large Load Diff
+115
View File
@@ -0,0 +1,115 @@
/*---------------------------------------------------------------------------------------------
* 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 { CancellationToken, DocumentLink, DocumentLinkProvider, l10n, Range, TabInputText, TextDocument, Uri, window, workspace } from 'vscode';
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
import { ITerminalEnvironmentProvider } from './terminal';
import { EmptyDisposable, IDisposable } from './util';
import { Model } from './model';
import { Repository } from './repository';
interface GitEditorRequest {
commitMessagePath?: string;
}
export class GitEditor implements IIPCHandler, ITerminalEnvironmentProvider {
private env: { [key: string]: string };
private disposable: IDisposable = EmptyDisposable;
readonly featureDescription = 'git editor';
constructor(ipc?: IIPCServer) {
if (ipc) {
this.disposable = ipc.registerHandler('git-editor', this);
}
this.env = {
GIT_EDITOR: `"${path.join(__dirname, ipc ? 'git-editor.sh' : 'git-editor-empty.sh')}"`,
VSCODE_GIT_EDITOR_NODE: process.execPath,
VSCODE_GIT_EDITOR_EXTRA_ARGS: '',
VSCODE_GIT_EDITOR_MAIN: path.join(__dirname, 'git-editor-main.js')
};
}
async handle({ commitMessagePath }: GitEditorRequest): Promise<any> {
if (commitMessagePath) {
const uri = Uri.file(commitMessagePath);
const doc = await workspace.openTextDocument(uri);
await window.showTextDocument(doc, { preview: false });
return new Promise((c) => {
const onDidClose = window.tabGroups.onDidChangeTabs(async (tabs) => {
if (tabs.closed.some(t => t.input instanceof TabInputText && t.input.uri.toString() === uri.toString())) {
onDidClose.dispose();
return c(true);
}
});
});
}
}
getEnv(): { [key: string]: string } {
const config = workspace.getConfiguration('git');
return config.get<boolean>('useEditorAsCommitInput') ? this.env : {};
}
getTerminalEnv(): { [key: string]: string } {
const config = workspace.getConfiguration('git');
return config.get<boolean>('useEditorAsCommitInput') && config.get<boolean>('terminalGitEditor') ? this.env : {};
}
dispose(): void {
this.disposable.dispose();
}
}
export class GitEditorDocumentLinkProvider implements DocumentLinkProvider {
private readonly _regex = /^#\s+(modified|new file|deleted|renamed|copied|type change):\s+(?<file1>.*?)(?:\s+->\s+(?<file2>.*))*$/gm;
constructor(private readonly _model: Model) { }
provideDocumentLinks(document: TextDocument, token: CancellationToken): DocumentLink[] {
if (token.isCancellationRequested) {
return [];
}
const repository = this._model.getRepository(document.uri);
if (!repository) {
return [];
}
const links: DocumentLink[] = [];
for (const match of document.getText().matchAll(this._regex)) {
if (!match.groups) {
continue;
}
const { file1, file2 } = match.groups;
if (file1) {
links.push(this._createDocumentLink(repository, document, match, file1));
}
if (file2) {
links.push(this._createDocumentLink(repository, document, match, file2));
}
}
return links;
}
private _createDocumentLink(repository: Repository, document: TextDocument, match: RegExpExecArray, file: string): DocumentLink {
const startIndex = match[0].indexOf(file);
const startPosition = document.positionAt(match.index + startIndex);
const endPosition = document.positionAt(match.index + startIndex + file.length);
const documentLink = new DocumentLink(
new Range(startPosition, endPosition),
Uri.file(path.join(repository.root, file)));
documentLink.tooltip = l10n.t('Open File');
return documentLink;
}
}
@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* 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 } from 'vscode';
import { AvatarQuery, SourceControlHistoryItemDetailsProvider } from './api/git';
import { Repository } from './repository';
import { ApiRepository } from './api/api1';
export interface ISourceControlHistoryItemDetailsProviderRegistry {
registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable;
getSourceControlHistoryItemDetailsProviders(): SourceControlHistoryItemDetailsProvider[];
}
export async function provideSourceControlHistoryItemAvatar(
registry: ISourceControlHistoryItemDetailsProviderRegistry,
repository: Repository,
query: AvatarQuery
): Promise<Map<string, string | undefined> | undefined> {
for (const provider of registry.getSourceControlHistoryItemDetailsProviders()) {
const result = await provider.provideAvatar(new ApiRepository(repository), query);
if (result) {
return result;
}
}
return undefined;
}
export async function provideSourceControlHistoryItemHoverCommands(
registry: ISourceControlHistoryItemDetailsProviderRegistry,
repository: Repository
): Promise<Command[] | undefined> {
for (const provider of registry.getSourceControlHistoryItemDetailsProviders()) {
const result = await provider.provideHoverCommands(new ApiRepository(repository));
if (result) {
return result;
}
}
return undefined;
}
export async function provideSourceControlHistoryItemMessageLinks(
registry: ISourceControlHistoryItemDetailsProviderRegistry,
repository: Repository,
message: string
): Promise<string | undefined> {
for (const provider of registry.getSourceControlHistoryItemDetailsProviders()) {
const result = await provider.provideMessageLinks(
new ApiRepository(repository), message);
if (result) {
return result;
}
}
return undefined;
}
+475
View File
@@ -0,0 +1,475 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent } from 'vscode';
import { Repository, Resource } from './repository';
import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, getCommitShortHash } from './util';
import { toMultiFileDiffEditorUris } from './uri';
import { AvatarQuery, AvatarQueryCommit, Branch, LogOptions, Ref, RefType } from './api/git';
import { emojify, ensureEmojis } from './emoji';
import { Commit } from './git';
import { OperationKind, OperationResult } from './operation';
import { ISourceControlHistoryItemDetailsProviderRegistry, provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider';
function toSourceControlHistoryItemRef(repository: Repository, ref: Ref): SourceControlHistoryItemRef {
const rootUri = Uri.file(repository.root);
switch (ref.type) {
case RefType.RemoteHead:
return {
id: `refs/remotes/${ref.name}`,
name: ref.name ?? '',
description: ref.commit ? l10n.t('Remote branch at {0}', getCommitShortHash(rootUri, ref.commit)) : undefined,
revision: ref.commit,
icon: new ThemeIcon('cloud'),
category: l10n.t('remote branches')
};
case RefType.Tag:
return {
id: `refs/tags/${ref.name}`,
name: ref.name ?? '',
description: ref.commit ? l10n.t('Tag at {0}', getCommitShortHash(rootUri, ref.commit)) : undefined,
revision: ref.commit,
icon: new ThemeIcon('tag'),
category: l10n.t('tags')
};
default:
return {
id: `refs/heads/${ref.name}`,
name: ref.name ?? '',
description: ref.commit ? getCommitShortHash(rootUri, ref.commit) : undefined,
revision: ref.commit,
icon: new ThemeIcon('git-branch'),
category: l10n.t('branches')
};
}
}
function compareSourceControlHistoryItemRef(ref1: SourceControlHistoryItemRef, ref2: SourceControlHistoryItemRef): number {
const getOrder = (ref: SourceControlHistoryItemRef): number => {
if (ref.id.startsWith('refs/heads/')) {
return 1;
} else if (ref.id.startsWith('refs/remotes/')) {
return 2;
} else if (ref.id.startsWith('refs/tags/')) {
return 3;
}
return 99;
};
const ref1Order = getOrder(ref1);
const ref2Order = getOrder(ref2);
if (ref1Order !== ref2Order) {
return ref1Order - ref2Order;
}
return ref1.name.localeCompare(ref2.name);
}
export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable {
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
private _currentHistoryItemRef: SourceControlHistoryItemRef | undefined;
get currentHistoryItemRef(): SourceControlHistoryItemRef | undefined { return this._currentHistoryItemRef; }
private _currentHistoryItemRemoteRef: SourceControlHistoryItemRef | undefined;
get currentHistoryItemRemoteRef(): SourceControlHistoryItemRef | undefined { return this._currentHistoryItemRemoteRef; }
private _currentHistoryItemBaseRef: SourceControlHistoryItemRef | undefined;
get currentHistoryItemBaseRef(): SourceControlHistoryItemRef | undefined { return this._currentHistoryItemBaseRef; }
private readonly _onDidChangeCurrentHistoryItemRefs = new EventEmitter<void>();
readonly onDidChangeCurrentHistoryItemRefs: Event<void> = this._onDidChangeCurrentHistoryItemRefs.event;
private readonly _onDidChangeHistoryItemRefs = new EventEmitter<SourceControlHistoryItemRefsChangeEvent>();
readonly onDidChangeHistoryItemRefs: Event<SourceControlHistoryItemRefsChangeEvent> = this._onDidChangeHistoryItemRefs.event;
private _HEAD: Branch | undefined;
private _historyItemRefs: SourceControlHistoryItemRef[] = [];
private historyItemDecorations = new Map<string, FileDecoration>();
private disposables: Disposable[] = [];
constructor(
private historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailsProviderRegistry,
private readonly repository: Repository,
private readonly logger: LogOutputChannel
) {
const onDidRunWriteOperation = filterEvent(repository.onDidRunOperation, e => !e.operation.readOnly);
this.disposables.push(onDidRunWriteOperation(this.onDidRunWriteOperation, this));
this.disposables.push(window.registerFileDecorationProvider(this));
}
private async onDidRunWriteOperation(result: OperationResult): Promise<void> {
if (!this.repository.HEAD) {
this.logger.trace('[GitHistoryProvider][onDidRunWriteOperation] repository.HEAD is undefined');
this._currentHistoryItemRef = this._currentHistoryItemRemoteRef = this._currentHistoryItemBaseRef = undefined;
this._onDidChangeCurrentHistoryItemRefs.fire();
return;
}
// Refs (alphabetically)
const historyItemRefs = this.repository.refs
.map(ref => toSourceControlHistoryItemRef(this.repository, ref))
.sort((a, b) => a.id.localeCompare(b.id));
const delta = deltaHistoryItemRefs(this._historyItemRefs, historyItemRefs);
this._historyItemRefs = historyItemRefs;
let historyItemRefId = '';
let historyItemRefName = '';
switch (this.repository.HEAD.type) {
case RefType.Head: {
if (this.repository.HEAD.name !== undefined) {
// Branch
historyItemRefId = `refs/heads/${this.repository.HEAD.name}`;
historyItemRefName = this.repository.HEAD.name;
// Remote
if (this.repository.HEAD.upstream) {
if (this.repository.HEAD.upstream.remote === '.') {
// Local branch
this._currentHistoryItemRemoteRef = {
id: `refs/heads/${this.repository.HEAD.upstream.name}`,
name: this.repository.HEAD.upstream.name,
revision: this.repository.HEAD.upstream.commit,
icon: new ThemeIcon('gi-branch')
};
} else {
// Remote branch
this._currentHistoryItemRemoteRef = {
id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`,
name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`,
revision: this.repository.HEAD.upstream.commit,
icon: new ThemeIcon('cloud')
};
}
} else {
this._currentHistoryItemRemoteRef = undefined;
}
// Base
if (this._HEAD?.name !== this.repository.HEAD.name) {
// Compute base if the branch has changed
const mergeBase = await this.resolveHEADMergeBase();
this._currentHistoryItemBaseRef = mergeBase && mergeBase.name && mergeBase.remote &&
(mergeBase.remote !== this.repository.HEAD.upstream?.remote ||
mergeBase.name !== this.repository.HEAD.upstream?.name) ? {
id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`,
name: `${mergeBase.remote}/${mergeBase.name}`,
revision: mergeBase.commit,
icon: new ThemeIcon('cloud')
} : undefined;
} else {
// Update base revision if it has changed
const mergeBaseModified = delta.modified
.find(ref => ref.id === this._currentHistoryItemBaseRef?.id);
if (this._currentHistoryItemBaseRef && mergeBaseModified) {
this._currentHistoryItemBaseRef = {
...this._currentHistoryItemBaseRef,
revision: mergeBaseModified.revision
};
}
}
} else {
// Detached commit
historyItemRefId = this.repository.HEAD.commit ?? '';
historyItemRefName = this.repository.HEAD.commit ?? '';
this._currentHistoryItemRemoteRef = undefined;
this._currentHistoryItemBaseRef = undefined;
}
break;
}
case RefType.Tag: {
// Tag
historyItemRefId = `refs/tags/${this.repository.HEAD.name}`;
historyItemRefName = this.repository.HEAD.name ?? this.repository.HEAD.commit ?? '';
this._currentHistoryItemRemoteRef = undefined;
this._currentHistoryItemBaseRef = undefined;
break;
}
}
this._HEAD = this.repository.HEAD;
this._currentHistoryItemRef = {
id: historyItemRefId,
name: historyItemRefName,
revision: this.repository.HEAD.commit,
icon: new ThemeIcon('target'),
};
this._onDidChangeCurrentHistoryItemRefs.fire();
this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] currentHistoryItemRef: ${JSON.stringify(this._currentHistoryItemRef)}`);
this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] currentHistoryItemRemoteRef: ${JSON.stringify(this._currentHistoryItemRemoteRef)}`);
this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] currentHistoryItemBaseRef: ${JSON.stringify(this._currentHistoryItemBaseRef)}`);
// Auto-fetch
const silent = result.operation.kind === OperationKind.Fetch && result.operation.showProgress === false;
this._onDidChangeHistoryItemRefs.fire({ ...delta, silent });
const deltaLog = {
added: delta.added.map(ref => ref.id),
modified: delta.modified.map(ref => ref.id),
removed: delta.removed.map(ref => ref.id),
silent
};
this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] historyItemRefs: ${JSON.stringify(deltaLog)}`);
}
async provideHistoryItemRefs(historyItemRefs: string[] | undefined): Promise<SourceControlHistoryItemRef[]> {
const refs = await this.repository.getRefs({ pattern: historyItemRefs });
const branches: SourceControlHistoryItemRef[] = [];
const remoteBranches: SourceControlHistoryItemRef[] = [];
const tags: SourceControlHistoryItemRef[] = [];
for (const ref of refs) {
switch (ref.type) {
case RefType.RemoteHead:
remoteBranches.push(toSourceControlHistoryItemRef(this.repository, ref));
break;
case RefType.Tag:
tags.push(toSourceControlHistoryItemRef(this.repository, ref));
break;
default:
branches.push(toSourceControlHistoryItemRef(this.repository, ref));
break;
}
}
return [...branches, ...remoteBranches, ...tags];
}
async provideHistoryItems(options: SourceControlHistoryOptions): Promise<SourceControlHistoryItem[]> {
if (!this.currentHistoryItemRef || !options.historyItemRefs) {
return [];
}
// Deduplicate refNames
const refNames = Array.from(new Set<string>(options.historyItemRefs));
let logOptions: LogOptions = { refNames, shortStats: true };
try {
if (options.limit === undefined || typeof options.limit === 'number') {
logOptions = { ...logOptions, maxEntries: options.limit ?? 50 };
} else if (typeof options.limit.id === 'string') {
// Get the common ancestor commit, and commits
const commit = await this.repository.getCommit(options.limit.id);
const commitParentId = commit.parents.length > 0 ? commit.parents[0] : await this.repository.getEmptyTree();
logOptions = { ...logOptions, range: `${commitParentId}..` };
}
if (typeof options.skip === 'number') {
logOptions = { ...logOptions, skip: options.skip };
}
const commits = await this.repository.log({ ...logOptions, silent: true });
// Avatars
const avatarQuery = {
commits: commits.map(c => ({
hash: c.hash,
authorName: c.authorName,
authorEmail: c.authorEmail
} satisfies AvatarQueryCommit)),
size: 20
} satisfies AvatarQuery;
const commitAvatars = await provideSourceControlHistoryItemAvatar(
this.historyItemDetailProviderRegistry, this.repository, avatarQuery);
await ensureEmojis();
const historyItems: SourceControlHistoryItem[] = [];
for (const commit of commits) {
const message = emojify(commit.message);
const messageWithLinks = await provideSourceControlHistoryItemMessageLinks(
this.historyItemDetailProviderRegistry, this.repository, message) ?? message;
const newLineIndex = message.indexOf('\n');
const subject = newLineIndex !== -1
? `${message.substring(0, newLineIndex)}\u2026`
: message;
const avatarUrl = commitAvatars?.get(commit.hash);
const references = this._resolveHistoryItemRefs(commit);
historyItems.push({
id: commit.hash,
parentIds: commit.parents,
subject,
message: messageWithLinks,
author: commit.authorName,
authorEmail: commit.authorEmail,
authorIcon: avatarUrl ? Uri.parse(avatarUrl) : new ThemeIcon('account'),
displayId: getCommitShortHash(Uri.file(this.repository.root), commit.hash),
timestamp: commit.authorDate?.getTime(),
statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 },
references: references.length !== 0 ? references : undefined
} satisfies SourceControlHistoryItem);
}
return historyItems;
} catch (err) {
this.logger.error(`[GitHistoryProvider][provideHistoryItems] Failed to get history items with options '${JSON.stringify(options)}': ${err}`);
return [];
}
}
async provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise<SourceControlHistoryItemChange[]> {
historyItemParentId = historyItemParentId ?? await this.repository.getEmptyTree();
const historyItemChangesUri: Uri[] = [];
const historyItemChanges: SourceControlHistoryItemChange[] = [];
const changes = await this.repository.diffTrees(historyItemParentId, historyItemId);
for (const change of changes) {
const historyItemUri = change.uri.with({
query: `ref=${historyItemId}`
});
// History item change
historyItemChanges.push({
uri: historyItemUri,
...toMultiFileDiffEditorUris(change, historyItemParentId, historyItemId)
} satisfies SourceControlHistoryItemChange);
// History item change decoration
const letter = Resource.getStatusLetter(change.status);
const tooltip = Resource.getStatusText(change.status);
const color = Resource.getStatusColor(change.status);
const fileDecoration = new FileDecoration(letter, tooltip, color);
this.historyItemDecorations.set(historyItemUri.toString(), fileDecoration);
historyItemChangesUri.push(historyItemUri);
}
this._onDidChangeDecorations.fire(historyItemChangesUri);
return historyItemChanges;
}
async resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise<string | undefined> {
try {
if (historyItemRefs.length === 0) {
// TODO@lszomoru - log
return undefined;
} else if (historyItemRefs.length === 1 && historyItemRefs[0] === this.currentHistoryItemRef?.id) {
// Remote
if (this.currentHistoryItemRemoteRef) {
const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemRemoteRef.id);
return ancestor;
}
// Base
if (this.currentHistoryItemBaseRef) {
const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemBaseRef.id);
return ancestor;
}
// First commit
const commits = await this.repository.log({ maxParents: 0, refNames: ['HEAD'] });
if (commits.length > 0) {
return commits[0].hash;
}
} else if (historyItemRefs.length > 1) {
const ancestor = await this.repository.getMergeBase(historyItemRefs[0], historyItemRefs[1], ...historyItemRefs.slice(2));
return ancestor;
}
}
catch (err) {
this.logger.error(`[GitHistoryProvider][resolveHistoryItemRefsCommonAncestor] Failed to resolve common ancestor for ${historyItemRefs.join(',')}: ${err}`);
}
return undefined;
}
provideFileDecoration(uri: Uri): FileDecoration | undefined {
return this.historyItemDecorations.get(uri.toString());
}
private _resolveHistoryItemRefs(commit: Commit): SourceControlHistoryItemRef[] {
const references: SourceControlHistoryItemRef[] = [];
for (const ref of commit.refNames) {
if (ref === 'refs/remotes/origin/HEAD') {
continue;
}
switch (true) {
case ref.startsWith('HEAD -> refs/heads/'):
references.push({
id: ref.substring('HEAD -> '.length),
name: ref.substring('HEAD -> refs/heads/'.length),
revision: commit.hash,
category: l10n.t('branches'),
icon: new ThemeIcon('target')
});
break;
case ref.startsWith('refs/heads/'):
references.push({
id: ref,
name: ref.substring('refs/heads/'.length),
revision: commit.hash,
category: l10n.t('branches'),
icon: new ThemeIcon('git-branch')
});
break;
case ref.startsWith('refs/remotes/'):
references.push({
id: ref,
name: ref.substring('refs/remotes/'.length),
revision: commit.hash,
category: l10n.t('remote branches'),
icon: new ThemeIcon('cloud')
});
break;
case ref.startsWith('tag: refs/tags/'):
references.push({
id: ref.substring('tag: '.length),
name: ref.substring('tag: refs/tags/'.length),
revision: commit.hash,
category: l10n.t('tags'),
icon: new ThemeIcon('tag')
});
break;
}
}
return references.sort(compareSourceControlHistoryItemRef);
}
private async resolveHEADMergeBase(): Promise<Branch | undefined> {
try {
if (this.repository.HEAD?.type !== RefType.Head || !this.repository.HEAD?.name) {
return undefined;
}
const mergeBase = await this.repository.getBranchBase(this.repository.HEAD.name);
return mergeBase;
} catch (err) {
this.logger.error(`[GitHistoryProvider][resolveHEADMergeBase] Failed to resolve merge base for ${this.repository.HEAD?.name}: ${err}`);
return undefined;
}
}
dispose(): void {
dispose(this.disposables);
}
}
+45
View File
@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as http from 'http';
export class IPCClient {
private ipcHandlePath: string;
constructor(private handlerName: string) {
const ipcHandlePath = process.env['VSCODE_GIT_IPC_HANDLE'];
if (!ipcHandlePath) {
throw new Error('Missing VSCODE_GIT_IPC_HANDLE');
}
this.ipcHandlePath = ipcHandlePath;
}
call(request: any): Promise<any> {
const opts: http.RequestOptions = {
socketPath: this.ipcHandlePath,
path: `/${this.handlerName}`,
method: 'POST'
};
return new Promise((c, e) => {
const req = http.request(opts, res => {
if (res.statusCode !== 200) {
return e(new Error(`Bad status code: ${res.statusCode}`));
}
const chunks: Buffer[] = [];
res.on('data', d => chunks.push(d));
res.on('end', () => c(JSON.parse(Buffer.concat(chunks).toString('utf8'))));
});
req.on('error', err => e(err));
req.write(JSON.stringify(request));
req.end();
});
}
}
+126
View File
@@ -0,0 +1,126 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vscode';
import { ITerminalEnvironmentProvider } from '../terminal';
import { toDisposable } from '../util';
import * as path from 'path';
import * as http from 'http';
import * as os from 'os';
import * as fs from 'fs';
import * as crypto from 'crypto';
function getIPCHandlePath(id: string): string {
if (process.platform === 'win32') {
return `\\\\.\\pipe\\vscode-git-${id}-sock`;
}
if (process.platform !== 'darwin' && process.env['XDG_RUNTIME_DIR']) {
return path.join(process.env['XDG_RUNTIME_DIR'] as string, `vscode-git-${id}.sock`);
}
return path.join(os.tmpdir(), `vscode-git-${id}.sock`);
}
export interface IIPCHandler {
handle(request: any): Promise<any>;
}
export async function createIPCServer(context?: string): Promise<IPCServer> {
const server = http.createServer();
const hash = crypto.createHash('sha256');
if (!context) {
const buffer = await new Promise<Buffer>((c, e) => crypto.randomBytes(20, (err, buf) => err ? e(err) : c(buf)));
hash.update(buffer);
} else {
hash.update(context);
}
const ipcHandlePath = getIPCHandlePath(hash.digest('hex').substring(0, 10));
if (process.platform !== 'win32') {
try {
await fs.promises.unlink(ipcHandlePath);
} catch {
// noop
}
}
return new Promise((c, e) => {
try {
server.on('error', err => e(err));
server.listen(ipcHandlePath);
c(new IPCServer(server, ipcHandlePath));
} catch (err) {
e(err);
}
});
}
export interface IIPCServer extends Disposable {
readonly ipcHandlePath: string | undefined;
getEnv(): { [key: string]: string };
registerHandler(name: string, handler: IIPCHandler): Disposable;
}
export class IPCServer implements IIPCServer, ITerminalEnvironmentProvider, Disposable {
private handlers = new Map<string, IIPCHandler>();
get ipcHandlePath(): string { return this._ipcHandlePath; }
constructor(private server: http.Server, private _ipcHandlePath: string) {
this.server.on('request', this.onRequest.bind(this));
}
registerHandler(name: string, handler: IIPCHandler): Disposable {
this.handlers.set(`/${name}`, handler);
return toDisposable(() => this.handlers.delete(name));
}
private onRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
if (!req.url) {
console.warn(`Request lacks url`);
return;
}
const handler = this.handlers.get(req.url);
if (!handler) {
console.warn(`IPC handler for ${req.url} not found`);
return;
}
const chunks: Buffer[] = [];
req.on('data', d => chunks.push(d));
req.on('end', () => {
const request = JSON.parse(Buffer.concat(chunks).toString('utf8'));
handler.handle(request).then(result => {
res.writeHead(200);
res.end(JSON.stringify(result));
}, () => {
res.writeHead(500);
res.end();
});
});
}
getEnv(): { [key: string]: string } {
return { VSCODE_GIT_IPC_HANDLE: this.ipcHandlePath };
}
getTerminalEnv(): { [key: string]: string } {
return { VSCODE_GIT_IPC_HANDLE: this.ipcHandlePath };
}
dispose(): void {
this.handlers.clear();
this.server.close();
if (this._ipcHandlePath && process.platform !== 'win32') {
fs.unlinkSync(this._ipcHandlePath);
}
}
}
+320
View File
@@ -0,0 +1,320 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { env, ExtensionContext, workspace, window, Disposable, commands, Uri, version as vscodeVersion, WorkspaceFolder, LogOutputChannel, l10n, LogLevel, languages } from 'vscode';
import { findGit, Git, IGit } from './git';
import { Model } from './model';
import { CommandCenter } from './commands';
import { GitFileSystemProvider } from './fileSystemProvider';
import { GitDecorations } from './decorationProvider';
import { Askpass } from './askpass';
import { toDisposable, filterEvent, eventToPromise } from './util';
import TelemetryReporter from '@vscode/extension-telemetry';
import { GitExtension } from './api/git';
import { GitProtocolHandler } from './protocolHandler';
import { GitExtensionImpl } from './api/extension';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import { GitTimelineProvider } from './timelineProvider';
import { registerAPICommands } from './api/api1';
import { TerminalEnvironmentManager, TerminalShellExecutionManager } from './terminal';
import { createIPCServer, IPCServer } from './ipc/ipcServer';
import { GitEditor, GitEditorDocumentLinkProvider } from './gitEditor';
import { GitPostCommitCommandsProvider } from './postCommitCommands';
import { GitEditSessionIdentityProvider } from './editSessionIdentityProvider';
import { GitCommitInputBoxCodeActionsProvider, GitCommitInputBoxDiagnosticsManager } from './diagnostics';
import { GitBlameController } from './blame';
import { StagedResourceQuickDiffProvider } from './repository';
const deactivateTasks: { (): Promise<any> }[] = [];
export async function deactivate(): Promise<any> {
for (const task of deactivateTasks) {
await task();
}
}
async function createModel(context: ExtensionContext, logger: LogOutputChannel, telemetryReporter: TelemetryReporter, disposables: Disposable[]): Promise<Model> {
const pathValue = workspace.getConfiguration('git').get<string | string[]>('path');
let pathHints = Array.isArray(pathValue) ? pathValue : pathValue ? [pathValue] : [];
const { isTrusted, workspaceFolders = [] } = workspace;
const excludes = isTrusted ? [] : workspaceFolders.map(f => path.normalize(f.uri.fsPath).replace(/[\r\n]+$/, ''));
if (!isTrusted && pathHints.length !== 0) {
// Filter out any non-absolute paths
pathHints = pathHints.filter(p => path.isAbsolute(p));
}
const info = await findGit(pathHints, gitPath => {
logger.info(l10n.t('[main] Validating found git in: "{0}"', gitPath));
if (excludes.length === 0) {
return true;
}
const normalized = path.normalize(gitPath).replace(/[\r\n]+$/, '');
const skip = excludes.some(e => normalized.startsWith(e));
if (skip) {
logger.info(l10n.t('[main] Skipped found git in: "{0}"', gitPath));
}
return !skip;
}, logger);
let ipcServer: IPCServer | undefined = undefined;
try {
ipcServer = await createIPCServer(context.storagePath);
} catch (err) {
logger.error(`[main] Failed to create git IPC: ${err}`);
}
const askpass = new Askpass(ipcServer);
disposables.push(askpass);
const gitEditor = new GitEditor(ipcServer);
disposables.push(gitEditor);
const environment = { ...askpass.getEnv(), ...gitEditor.getEnv(), ...ipcServer?.getEnv() };
const terminalEnvironmentManager = new TerminalEnvironmentManager(context, [askpass, gitEditor, ipcServer]);
disposables.push(terminalEnvironmentManager);
logger.info(l10n.t('[main] Using git "{0}" from "{1}"', info.version, info.path));
const git = new Git({
gitPath: info.path,
userAgent: `git/${info.version} (${(os as any).version?.() ?? os.type()} ${os.release()}; ${os.platform()} ${os.arch()}) vscode/${vscodeVersion} (${env.appName})`,
version: info.version,
env: environment,
});
const model = new Model(git, askpass, context.globalState, context.workspaceState, logger, telemetryReporter);
disposables.push(model);
const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`);
model.onDidOpenRepository(onRepository, null, disposables);
model.onDidCloseRepository(onRepository, null, disposables);
onRepository();
const onOutput = (str: string) => {
const lines = str.split(/\r?\n/mg);
while (/^\s*$/.test(lines[lines.length - 1])) {
lines.pop();
}
logger.appendLine(lines.join('\n'));
};
git.onOutput.addListener('log', onOutput);
disposables.push(toDisposable(() => git.onOutput.removeListener('log', onOutput)));
const cc = new CommandCenter(git, model, context.globalState, logger, telemetryReporter);
disposables.push(
cc,
new GitFileSystemProvider(model, logger),
new GitDecorations(model),
new GitBlameController(model),
new GitTimelineProvider(model, cc),
new GitEditSessionIdentityProvider(model),
new StagedResourceQuickDiffProvider(model),
new TerminalShellExecutionManager(model, logger)
);
const postCommitCommandsProvider = new GitPostCommitCommandsProvider(model);
model.registerPostCommitCommandsProvider(postCommitCommandsProvider);
const diagnosticsManager = new GitCommitInputBoxDiagnosticsManager(model);
disposables.push(diagnosticsManager);
const codeActionsProvider = new GitCommitInputBoxCodeActionsProvider(diagnosticsManager);
disposables.push(codeActionsProvider);
const gitEditorDocumentLinkProvider = languages.registerDocumentLinkProvider('git-commit', new GitEditorDocumentLinkProvider(model));
disposables.push(gitEditorDocumentLinkProvider);
checkGitVersion(info);
commands.executeCommand('setContext', 'gitVersion2.35', git.compareGitVersionTo('2.35') >= 0);
return model;
}
async function isGitRepository(folder: WorkspaceFolder): Promise<boolean> {
if (folder.uri.scheme !== 'file') {
return false;
}
const dotGit = path.join(folder.uri.fsPath, '.git');
try {
const dotGitStat = await new Promise<fs.Stats>((c, e) => fs.stat(dotGit, (err, stat) => err ? e(err) : c(stat)));
return dotGitStat.isDirectory();
} catch (err) {
return false;
}
}
async function warnAboutMissingGit(): Promise<void> {
const config = workspace.getConfiguration('git');
const shouldIgnore = config.get<boolean>('ignoreMissingGitWarning') === true;
if (shouldIgnore) {
return;
}
if (!workspace.workspaceFolders) {
return;
}
const areGitRepositories = await Promise.all(workspace.workspaceFolders.map(isGitRepository));
if (areGitRepositories.every(isGitRepository => !isGitRepository)) {
return;
}
const download = l10n.t('Download Git');
const neverShowAgain = l10n.t('Don\'t Show Again');
const choice = await window.showWarningMessage(
l10n.t('Git not found. Install it or configure it using the "git.path" setting.'),
download,
neverShowAgain
);
if (choice === download) {
commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-download-git'));
} else if (choice === neverShowAgain) {
await config.update('ignoreMissingGitWarning', true, true);
}
}
export async function _activate(context: ExtensionContext): Promise<GitExtensionImpl> {
const disposables: Disposable[] = [];
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
const logger = window.createOutputChannel('Git', { log: true });
disposables.push(logger);
const onDidChangeLogLevel = (logLevel: LogLevel) => {
logger.appendLine(l10n.t('[main] Log level: {0}', LogLevel[logLevel]));
};
disposables.push(logger.onDidChangeLogLevel(onDidChangeLogLevel));
onDidChangeLogLevel(logger.logLevel);
const { aiKey } = require('../package.json') as { aiKey: string };
const telemetryReporter = new TelemetryReporter(aiKey);
deactivateTasks.push(() => telemetryReporter.dispose());
const config = workspace.getConfiguration('git', null);
const enabled = config.get<boolean>('enabled');
if (!enabled) {
const onConfigChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git'));
const onEnabled = filterEvent(onConfigChange, () => workspace.getConfiguration('git', null).get<boolean>('enabled') === true);
const result = new GitExtensionImpl();
eventToPromise(onEnabled).then(async () => result.model = await createModel(context, logger, telemetryReporter, disposables));
return result;
}
try {
const model = await createModel(context, logger, telemetryReporter, disposables);
return new GitExtensionImpl(model);
} catch (err) {
console.warn(err.message);
logger.warn(`[main] Failed to create model: ${err}`);
if (!/Git installation not found/.test(err.message || '')) {
throw err;
}
/* __GDPR__
"git.missing" : {
"owner": "lszomoru"
}
*/
telemetryReporter.sendTelemetryEvent('git.missing');
commands.executeCommand('setContext', 'git.missing', true);
warnAboutMissingGit();
return new GitExtensionImpl();
} finally {
disposables.push(new GitProtocolHandler(logger));
}
}
let _context: ExtensionContext;
export function getExtensionContext(): ExtensionContext {
return _context;
}
export async function activate(context: ExtensionContext): Promise<GitExtension> {
_context = context;
const result = await _activate(context);
context.subscriptions.push(registerAPICommands(result));
return result;
}
async function checkGitv1(info: IGit): Promise<void> {
const config = workspace.getConfiguration('git');
const shouldIgnore = config.get<boolean>('ignoreLegacyWarning') === true;
if (shouldIgnore) {
return;
}
if (!/^[01]/.test(info.version)) {
return;
}
const update = l10n.t('Update Git');
const neverShowAgain = l10n.t('Don\'t Show Again');
const choice = await window.showWarningMessage(
l10n.t('You seem to have git "{0}" installed. Code works best with git >= 2', info.version),
update,
neverShowAgain
);
if (choice === update) {
commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-download-git'));
} else if (choice === neverShowAgain) {
await config.update('ignoreLegacyWarning', true, true);
}
}
async function checkGitWindows(info: IGit): Promise<void> {
if (!/^2\.(25|26)\./.test(info.version)) {
return;
}
const config = workspace.getConfiguration('git');
const shouldIgnore = config.get<boolean>('ignoreWindowsGit27Warning') === true;
if (shouldIgnore) {
return;
}
const update = l10n.t('Update Git');
const neverShowAgain = l10n.t('Don\'t Show Again');
const choice = await window.showWarningMessage(
l10n.t('There are known issues with the installed Git "{0}". Please update to Git >= 2.27 for the git features to work correctly.', info.version),
update,
neverShowAgain
);
if (choice === update) {
commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-download-git'));
} else if (choice === neverShowAgain) {
await config.update('ignoreWindowsGit27Warning', true, true);
}
}
async function checkGitVersion(info: IGit): Promise<void> {
await checkGitv1(info);
if (process.platform === 'win32') {
await checkGitWindows(info);
}
}
File diff suppressed because it is too large Load Diff
+283
View File
@@ -0,0 +1,283 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable local/code-no-dangerous-type-assertions */
import { LogOutputChannel } from 'vscode';
export const enum OperationKind {
Add = 'Add',
Apply = 'Apply',
Blame = 'Blame',
Branch = 'Branch',
CheckIgnore = 'CheckIgnore',
Checkout = 'Checkout',
CheckoutTracking = 'CheckoutTracking',
CherryPick = 'CherryPick',
Clean = 'Clean',
Commit = 'Commit',
Config = 'Config',
DeleteBranch = 'DeleteBranch',
DeleteRef = 'DeleteRef',
DeleteRemoteRef = 'DeleteRemoteRef',
DeleteTag = 'DeleteTag',
Diff = 'Diff',
Fetch = 'Fetch',
FindTrackingBranches = 'GetTracking',
GetBranch = 'GetBranch',
GetBranches = 'GetBranches',
GetCommitTemplate = 'GetCommitTemplate',
GetObjectDetails = 'GetObjectDetails',
GetObjectFiles = 'GetObjectFiles',
GetRefs = 'GetRefs',
GetRemoteRefs = 'GetRemoteRefs',
HashObject = 'HashObject',
Ignore = 'Ignore',
Log = 'Log',
LogFile = 'LogFile',
Merge = 'Merge',
MergeAbort = 'MergeAbort',
MergeBase = 'MergeBase',
Move = 'Move',
PostCommitCommand = 'PostCommitCommand',
Pull = 'Pull',
Push = 'Push',
Remote = 'Remote',
RenameBranch = 'RenameBranch',
Remove = 'Remove',
Reset = 'Reset',
Rebase = 'Rebase',
RebaseAbort = 'RebaseAbort',
RebaseContinue = 'RebaseContinue',
Refresh = 'Refresh',
RevertFiles = 'RevertFiles',
RevList = 'RevList',
RevParse = 'RevParse',
SetBranchUpstream = 'SetBranchUpstream',
Show = 'Show',
Stage = 'Stage',
Status = 'Status',
Stash = 'Stash',
SubmoduleUpdate = 'SubmoduleUpdate',
Sync = 'Sync',
Tag = 'Tag',
}
export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchOperation | CheckIgnoreOperation | CherryPickOperation |
CheckoutOperation | CheckoutTrackingOperation | CleanOperation | CommitOperation | ConfigOperation | DeleteBranchOperation |
DeleteRefOperation | DeleteRemoteRefOperation | DeleteTagOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation |
GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetObjectFilesOperation | GetRefsOperation |
GetRemoteRefsOperation | HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation |
MergeBaseOperation | MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation |
RemoveOperation | ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RefreshOperation | RevertFilesOperation |
RevListOperation | RevParseOperation | SetBranchUpstreamOperation | ShowOperation | StageOperation | StatusOperation | StashOperation |
SubmoduleUpdateOperation | SyncOperation | TagOperation;
type BaseOperation = { kind: OperationKind; blocking: boolean; readOnly: boolean; remote: boolean; retry: boolean; showProgress: boolean };
export type AddOperation = BaseOperation & { kind: OperationKind.Add };
export type ApplyOperation = BaseOperation & { kind: OperationKind.Apply };
export type BlameOperation = BaseOperation & { kind: OperationKind.Blame };
export type BranchOperation = BaseOperation & { kind: OperationKind.Branch };
export type CheckIgnoreOperation = BaseOperation & { kind: OperationKind.CheckIgnore };
export type CherryPickOperation = BaseOperation & { kind: OperationKind.CherryPick };
export type CheckoutOperation = BaseOperation & { kind: OperationKind.Checkout; refLabel: string };
export type CheckoutTrackingOperation = BaseOperation & { kind: OperationKind.CheckoutTracking; refLabel: string };
export type CleanOperation = BaseOperation & { kind: OperationKind.Clean };
export type CommitOperation = BaseOperation & { kind: OperationKind.Commit };
export type ConfigOperation = BaseOperation & { kind: OperationKind.Config };
export type DeleteBranchOperation = BaseOperation & { kind: OperationKind.DeleteBranch };
export type DeleteRefOperation = BaseOperation & { kind: OperationKind.DeleteRef };
export type DeleteRemoteRefOperation = BaseOperation & { kind: OperationKind.DeleteRemoteRef };
export type DeleteTagOperation = BaseOperation & { kind: OperationKind.DeleteTag };
export type DiffOperation = BaseOperation & { kind: OperationKind.Diff };
export type FetchOperation = BaseOperation & { kind: OperationKind.Fetch };
export type FindTrackingBranchesOperation = BaseOperation & { kind: OperationKind.FindTrackingBranches };
export type GetBranchOperation = BaseOperation & { kind: OperationKind.GetBranch };
export type GetBranchesOperation = BaseOperation & { kind: OperationKind.GetBranches };
export type GetCommitTemplateOperation = BaseOperation & { kind: OperationKind.GetCommitTemplate };
export type GetObjectDetailsOperation = BaseOperation & { kind: OperationKind.GetObjectDetails };
export type GetObjectFilesOperation = BaseOperation & { kind: OperationKind.GetObjectFiles };
export type GetRefsOperation = BaseOperation & { kind: OperationKind.GetRefs };
export type GetRemoteRefsOperation = BaseOperation & { kind: OperationKind.GetRemoteRefs };
export type HashObjectOperation = BaseOperation & { kind: OperationKind.HashObject };
export type IgnoreOperation = BaseOperation & { kind: OperationKind.Ignore };
export type LogOperation = BaseOperation & { kind: OperationKind.Log };
export type LogFileOperation = BaseOperation & { kind: OperationKind.LogFile };
export type MergeOperation = BaseOperation & { kind: OperationKind.Merge };
export type MergeAbortOperation = BaseOperation & { kind: OperationKind.MergeAbort };
export type MergeBaseOperation = BaseOperation & { kind: OperationKind.MergeBase };
export type MoveOperation = BaseOperation & { kind: OperationKind.Move };
export type PostCommitCommandOperation = BaseOperation & { kind: OperationKind.PostCommitCommand };
export type PullOperation = BaseOperation & { kind: OperationKind.Pull };
export type PushOperation = BaseOperation & { kind: OperationKind.Push };
export type RemoteOperation = BaseOperation & { kind: OperationKind.Remote };
export type RenameBranchOperation = BaseOperation & { kind: OperationKind.RenameBranch };
export type RemoveOperation = BaseOperation & { kind: OperationKind.Remove };
export type ResetOperation = BaseOperation & { kind: OperationKind.Reset };
export type RebaseOperation = BaseOperation & { kind: OperationKind.Rebase };
export type RebaseAbortOperation = BaseOperation & { kind: OperationKind.RebaseAbort };
export type RebaseContinueOperation = BaseOperation & { kind: OperationKind.RebaseContinue };
export type RefreshOperation = BaseOperation & { kind: OperationKind.Refresh };
export type RevertFilesOperation = BaseOperation & { kind: OperationKind.RevertFiles };
export type RevListOperation = BaseOperation & { kind: OperationKind.RevList };
export type RevParseOperation = BaseOperation & { kind: OperationKind.RevParse };
export type SetBranchUpstreamOperation = BaseOperation & { kind: OperationKind.SetBranchUpstream };
export type ShowOperation = BaseOperation & { kind: OperationKind.Show };
export type StageOperation = BaseOperation & { kind: OperationKind.Stage };
export type StatusOperation = BaseOperation & { kind: OperationKind.Status };
export type StashOperation = BaseOperation & { kind: OperationKind.Stash };
export type SubmoduleUpdateOperation = BaseOperation & { kind: OperationKind.SubmoduleUpdate };
export type SyncOperation = BaseOperation & { kind: OperationKind.Sync };
export type TagOperation = BaseOperation & { kind: OperationKind.Tag };
export const Operation = {
Add: (showProgress: boolean): AddOperation => ({ kind: OperationKind.Add, blocking: false, readOnly: false, remote: false, retry: false, showProgress }),
Apply: { kind: OperationKind.Apply, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as ApplyOperation,
Blame: (showProgress: boolean) => ({ kind: OperationKind.Blame, blocking: false, readOnly: true, remote: false, retry: false, showProgress } as BlameOperation),
Branch: { kind: OperationKind.Branch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as BranchOperation,
CheckIgnore: { kind: OperationKind.CheckIgnore, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as CheckIgnoreOperation,
CherryPick: { kind: OperationKind.CherryPick, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as CherryPickOperation,
Checkout: (refLabel: string) => ({ kind: OperationKind.Checkout, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true, refLabel } as CheckoutOperation),
CheckoutTracking: (refLabel: string) => ({ kind: OperationKind.CheckoutTracking, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true, refLabel } as CheckoutTrackingOperation),
Clean: (showProgress: boolean) => ({ kind: OperationKind.Clean, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as CleanOperation),
Commit: { kind: OperationKind.Commit, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true } as CommitOperation,
Config: (readOnly: boolean) => ({ kind: OperationKind.Config, blocking: false, readOnly, remote: false, retry: false, showProgress: false } as ConfigOperation),
DeleteBranch: { kind: OperationKind.DeleteBranch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteBranchOperation,
DeleteRef: { kind: OperationKind.DeleteRef, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteRefOperation,
DeleteRemoteRef: { kind: OperationKind.DeleteRemoteRef, blocking: false, readOnly: false, remote: true, retry: false, showProgress: true } as DeleteRemoteRefOperation,
DeleteTag: { kind: OperationKind.DeleteTag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteTagOperation,
Diff: { kind: OperationKind.Diff, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as DiffOperation,
Fetch: (showProgress: boolean) => ({ kind: OperationKind.Fetch, blocking: false, readOnly: false, remote: true, retry: true, showProgress } as FetchOperation),
FindTrackingBranches: { kind: OperationKind.FindTrackingBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as FindTrackingBranchesOperation,
GetBranch: { kind: OperationKind.GetBranch, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetBranchOperation,
GetBranches: { kind: OperationKind.GetBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetBranchesOperation,
GetCommitTemplate: { kind: OperationKind.GetCommitTemplate, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetCommitTemplateOperation,
GetObjectDetails: { kind: OperationKind.GetObjectDetails, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectDetailsOperation,
GetObjectFiles: { kind: OperationKind.GetObjectFiles, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectFilesOperation,
GetRefs: { kind: OperationKind.GetRefs, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetRefsOperation,
GetRemoteRefs: { kind: OperationKind.GetRemoteRefs, blocking: false, readOnly: true, remote: true, retry: false, showProgress: false } as GetRemoteRefsOperation,
HashObject: { kind: OperationKind.HashObject, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as HashObjectOperation,
Ignore: { kind: OperationKind.Ignore, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as IgnoreOperation,
Log: (showProgress: boolean) => ({ kind: OperationKind.Log, blocking: false, readOnly: true, remote: false, retry: false, showProgress }) as LogOperation,
LogFile: { kind: OperationKind.LogFile, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as LogFileOperation,
Merge: { kind: OperationKind.Merge, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as MergeOperation,
MergeAbort: { kind: OperationKind.MergeAbort, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as MergeAbortOperation,
MergeBase: { kind: OperationKind.MergeBase, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as MergeBaseOperation,
Move: { kind: OperationKind.Move, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as MoveOperation,
PostCommitCommand: { kind: OperationKind.PostCommitCommand, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as PostCommitCommandOperation,
Pull: { kind: OperationKind.Pull, blocking: true, readOnly: false, remote: true, retry: true, showProgress: true } as PullOperation,
Push: { kind: OperationKind.Push, blocking: true, readOnly: false, remote: true, retry: false, showProgress: true } as PushOperation,
Remote: { kind: OperationKind.Remote, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RemoteOperation,
RenameBranch: { kind: OperationKind.RenameBranch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RenameBranchOperation,
Remove: { kind: OperationKind.Remove, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RemoveOperation,
Reset: { kind: OperationKind.Reset, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as ResetOperation,
Rebase: { kind: OperationKind.Rebase, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseOperation,
RebaseAbort: { kind: OperationKind.RebaseAbort, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseAbortOperation,
RebaseContinue: { kind: OperationKind.RebaseContinue, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseContinueOperation,
Refresh: { kind: OperationKind.Refresh, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RefreshOperation,
RevertFiles: (showProgress: boolean) => ({ kind: OperationKind.RevertFiles, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as RevertFilesOperation),
RevList: { kind: OperationKind.RevList, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as RevListOperation,
RevParse: { kind: OperationKind.RevParse, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as RevParseOperation,
SetBranchUpstream: { kind: OperationKind.SetBranchUpstream, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as SetBranchUpstreamOperation,
Show: { kind: OperationKind.Show, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as ShowOperation,
Stage: { kind: OperationKind.Stage, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StageOperation,
Status: { kind: OperationKind.Status, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StatusOperation,
Stash: { kind: OperationKind.Stash, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StashOperation,
SubmoduleUpdate: { kind: OperationKind.SubmoduleUpdate, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as SubmoduleUpdateOperation,
Sync: { kind: OperationKind.Sync, blocking: true, readOnly: false, remote: true, retry: true, showProgress: true } as SyncOperation,
Tag: { kind: OperationKind.Tag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as TagOperation
};
export interface OperationResult {
operation: Operation;
error: any;
}
interface IOperationManager {
getOperations(operationKind: OperationKind): Operation[];
isIdle(): boolean;
isRunning(operationKind: OperationKind): boolean;
shouldDisableCommands(): boolean;
shouldShowProgress(): boolean;
}
export class OperationManager implements IOperationManager {
private operations = new Map<OperationKind, Set<Operation>>();
constructor(private readonly logger: LogOutputChannel) { }
start(operation: Operation): void {
if (this.operations.has(operation.kind)) {
this.operations.get(operation.kind)!.add(operation);
} else {
this.operations.set(operation.kind, new Set([operation]));
}
this.logger.trace(`[OperationManager][start] ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`);
}
end(operation: Operation): void {
const operationSet = this.operations.get(operation.kind);
if (operationSet) {
operationSet.delete(operation);
if (operationSet.size === 0) {
this.operations.delete(operation.kind);
}
}
this.logger.trace(`[OperationManager][end] ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`);
}
getOperations(operationKind: OperationKind): Operation[] {
const operationSet = this.operations.get(operationKind);
return operationSet ? Array.from(operationSet) : [];
}
isIdle(): boolean {
const operationSets = this.operations.values();
for (const operationSet of operationSets) {
for (const operation of operationSet) {
if (!operation.readOnly) {
return false;
}
}
}
return true;
}
isRunning(operationKind: OperationKind): boolean {
return this.operations.has(operationKind);
}
shouldDisableCommands(): boolean {
const operationSets = this.operations.values();
for (const operationSet of operationSets) {
for (const operation of operationSet) {
if (operation.blocking) {
return true;
}
}
}
return false;
}
shouldShowProgress(): boolean {
const operationSets = this.operations.values();
for (const operationSet of operationSets) {
for (const operation of operationSet) {
if (operation.showProgress) {
return true;
}
}
}
return false;
}
}
+239
View File
@@ -0,0 +1,239 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Command, commands, Disposable, Event, EventEmitter, Memento, Uri, workspace, l10n } from 'vscode';
import { PostCommitCommandsProvider } from './api/git';
import { IRepositoryResolver, Repository } from './repository';
import { ApiRepository } from './api/api1';
import { dispose } from './util';
import { OperationKind } from './operation';
export interface IPostCommitCommandsProviderRegistry {
readonly onDidChangePostCommitCommandsProviders: Event<void>;
getPostCommitCommandsProviders(): PostCommitCommandsProvider[];
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
}
export class GitPostCommitCommandsProvider implements PostCommitCommandsProvider {
constructor(private readonly _repositoryResolver: IRepositoryResolver) { }
getCommands(apiRepository: ApiRepository): Command[] {
const repository = this._repositoryResolver.getRepository(apiRepository.rootUri);
if (!repository) {
return [];
}
const config = workspace.getConfiguration('git', Uri.file(repository.root));
// Branch protection
const isBranchProtected = repository.isBranchProtected();
const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;
const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt';
const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch';
// Icon
const isCommitInProgress = repository.operations.isRunning(OperationKind.Commit) || repository.operations.isRunning(OperationKind.PostCommitCommand);
const icon = isCommitInProgress ? '$(sync~spin)' : alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined;
// Tooltip (default)
let pushCommandTooltip = !alwaysCommitToNewBranch ?
l10n.t('Commit & Push Changes') :
l10n.t('Commit to New Branch & Push Changes');
let syncCommandTooltip = !alwaysCommitToNewBranch ?
l10n.t('Commit & Sync Changes') :
l10n.t('Commit to New Branch & Synchronize Changes');
// Tooltip (in progress)
if (isCommitInProgress) {
pushCommandTooltip = !alwaysCommitToNewBranch ?
l10n.t('Committing & Pushing Changes...') :
l10n.t('Committing to New Branch & Pushing Changes...');
syncCommandTooltip = !alwaysCommitToNewBranch ?
l10n.t('Committing & Synchronizing Changes...') :
l10n.t('Committing to New Branch & Synchronizing Changes...');
}
return [
{
command: 'git.push',
title: l10n.t('{0} Commit & Push', icon ?? '$(arrow-up)'),
tooltip: pushCommandTooltip
},
{
command: 'git.sync',
title: l10n.t('{0} Commit & Sync', icon ?? '$(sync)'),
tooltip: syncCommandTooltip
},
];
}
}
export class CommitCommandsCenter {
private _onDidChange = new EventEmitter<void>();
get onDidChange(): Event<void> { return this._onDidChange.event; }
private disposables: Disposable[] = [];
set postCommitCommand(command: string | null | undefined) {
if (command === undefined) {
// Commit WAS NOT initiated using the action button
// so there is no need to store the post-commit command
return;
}
this.globalState.update(this.getGlobalStateKey(), command)
.then(() => this._onDidChange.fire());
}
constructor(
private readonly globalState: Memento,
private readonly repository: Repository,
private readonly postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry
) {
const root = Uri.file(repository.root);
// Migrate post commit command storage
this.migratePostCommitCommandStorage()
.then(() => {
const onRememberPostCommitCommandChange = async () => {
const config = workspace.getConfiguration('git', root);
if (!config.get<boolean>('rememberPostCommitCommand')) {
await this.globalState.update(this.getGlobalStateKey(), undefined);
}
};
this.disposables.push(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git.rememberPostCommitCommand', root)) {
onRememberPostCommitCommandChange();
}
}));
onRememberPostCommitCommandChange();
this.disposables.push(postCommitCommandsProviderRegistry.onDidChangePostCommitCommandsProviders(() => this._onDidChange.fire()));
});
}
getPrimaryCommand(): Command {
const allCommands = this.getSecondaryCommands().map(c => c).flat();
const commandFromStorage = allCommands.find(c => c.arguments?.length === 2 && c.arguments[1] === this.getPostCommitCommandStringFromStorage());
const commandFromSetting = allCommands.find(c => c.arguments?.length === 2 && c.arguments[1] === this.getPostCommitCommandStringFromSetting());
return commandFromStorage ?? commandFromSetting ?? this.getCommitCommands()[0];
}
getSecondaryCommands(): Command[][] {
const commandGroups: Command[][] = [];
for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) {
const commands = provider.getCommands(new ApiRepository(this.repository));
commandGroups.push((commands ?? []).map(c => {
return { command: 'git.commit', title: c.title, tooltip: c.tooltip, arguments: [this.repository.sourceControl, c.command] };
}));
}
if (commandGroups.length > 0) {
commandGroups.splice(0, 0, this.getCommitCommands());
}
return commandGroups;
}
async executePostCommitCommand(command: string | null | undefined): Promise<void> {
try {
if (command === null) {
// No post-commit command
return;
}
if (command === undefined) {
// Commit WAS NOT initiated using the action button (ex: keybinding, toolbar action,
// command palette) so we have to honour the default post commit command (memento/setting).
const primaryCommand = this.getPrimaryCommand();
command = primaryCommand.arguments?.length === 2 ? primaryCommand.arguments[1] : null;
}
if (command !== null) {
await commands.executeCommand(command!.toString(), new ApiRepository(this.repository));
}
} catch (err) {
throw err;
}
finally {
if (!this.isRememberPostCommitCommandEnabled()) {
await this.globalState.update(this.getGlobalStateKey(), undefined);
this._onDidChange.fire();
}
}
}
private getGlobalStateKey(): string {
return `postCommitCommand:${this.repository.root}`;
}
private getCommitCommands(): Command[] {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
// Branch protection
const isBranchProtected = this.repository.isBranchProtected();
const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;
const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt';
const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch';
// Icon
const icon = alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined;
// Tooltip (default)
const branch = this.repository.HEAD?.name;
let tooltip = alwaysCommitToNewBranch ?
l10n.t('Commit Changes to New Branch') :
branch ?
l10n.t('Commit Changes on "{0}"', branch) :
l10n.t('Commit Changes');
// Tooltip (in progress)
if (this.repository.operations.isRunning(OperationKind.Commit)) {
tooltip = !alwaysCommitToNewBranch ?
l10n.t('Committing Changes...') :
l10n.t('Committing Changes to New Branch...');
}
return [
{ command: 'git.commit', title: l10n.t('{0} Commit', icon ?? '$(check)'), tooltip, arguments: [this.repository.sourceControl, null] },
{ command: 'git.commitAmend', title: l10n.t('{0} Commit (Amend)', icon ?? '$(check)'), tooltip, arguments: [this.repository.sourceControl, null] },
];
}
private getPostCommitCommandStringFromSetting(): string | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const postCommitCommandSetting = config.get<string>('postCommitCommand');
return postCommitCommandSetting === 'push' || postCommitCommandSetting === 'sync' ? `git.${postCommitCommandSetting}` : undefined;
}
private getPostCommitCommandStringFromStorage(): string | null | undefined {
return this.globalState.get<string | null>(this.getGlobalStateKey());
}
private async migratePostCommitCommandStorage(): Promise<void> {
const postCommitCommandString = this.globalState.get<string | null>(this.repository.root);
if (postCommitCommandString !== undefined) {
await this.globalState.update(this.getGlobalStateKey(), postCommitCommandString);
await this.globalState.update(this.repository.root, undefined);
}
}
private isRememberPostCommitCommandEnabled(): boolean {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
return config.get<boolean>('rememberPostCommitCommand') === true;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}
+97
View File
@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { UriHandler, Uri, window, Disposable, commands, LogOutputChannel, l10n } from 'vscode';
import { dispose, isWindows } from './util';
import * as querystring from 'querystring';
const schemes = isWindows ?
new Set(['git', 'http', 'https', 'ssh']) :
new Set(['file', 'git', 'http', 'https', 'ssh']);
const refRegEx = /^$|[~\^:\\\*\s\[\]]|^-|^\.|\/\.|\.\.|\.lock\/|\.lock$|\/$|\.$/;
export class GitProtocolHandler implements UriHandler {
private disposables: Disposable[] = [];
constructor(private readonly logger: LogOutputChannel) {
this.disposables.push(window.registerUriHandler(this));
}
handleUri(uri: Uri): void {
this.logger.info(`[GitProtocolHandler][handleUri] URI:(${uri.toString()})`);
switch (uri.path) {
case '/clone': this.clone(uri);
}
}
private async clone(uri: Uri): Promise<void> {
const data = querystring.parse(uri.query);
const ref = data.ref;
if (!data.url) {
this.logger.warn('[GitProtocolHandler][clone] Failed to open URI:' + uri.toString());
return;
}
if (Array.isArray(data.url) && data.url.length === 0) {
this.logger.warn('[GitProtocolHandler][clone] Failed to open URI:' + uri.toString());
return;
}
if (ref !== undefined && typeof ref !== 'string') {
this.logger.warn('[GitProtocolHandler][clone] Failed to open URI due to multiple references:' + uri.toString());
return;
}
let cloneUri: Uri;
try {
let rawUri = Array.isArray(data.url) ? data.url[0] : data.url;
// Handle SSH Uri
// Ex: git@github.com:microsoft/vscode.git
rawUri = rawUri.replace(/^(git@[^\/:]+)(:)/i, 'ssh://$1/');
cloneUri = Uri.parse(rawUri, true);
// Validate against supported schemes
if (!schemes.has(cloneUri.scheme.toLowerCase())) {
throw new Error('Unsupported scheme.');
}
// Validate the reference
if (typeof ref === 'string' && refRegEx.test(ref)) {
throw new Error('Invalid reference.');
}
}
catch (ex) {
this.logger.warn('[GitProtocolHandler][clone] Invalid URI:' + uri.toString());
return;
}
if (!(await commands.getCommands(true)).includes('git.clone')) {
this.logger.error('[GitProtocolHandler][clone] Could not complete git clone operation as git installation was not found.');
const errorMessage = l10n.t('Could not clone your repository as Git is not installed.');
const downloadGit = l10n.t('Download Git');
if (await window.showErrorMessage(errorMessage, { modal: true }, downloadGit) === downloadGit) {
commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-download-git'));
}
return;
} else {
const cloneTarget = cloneUri.toString(true);
this.logger.info(`[GitProtocolHandler][clone] Executing git.clone for ${cloneTarget}`);
commands.executeCommand('git.clone', cloneTarget, undefined, { ref: ref });
}
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}
+12
View File
@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vscode';
import { PushErrorHandler } from './api/git';
export interface IPushErrorHandlerRegistry {
registerPushErrorHandler(provider: PushErrorHandler): Disposable;
getPushErrorHandlers(): PushErrorHandler[];
}
+15
View File
@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event } from 'vscode';
import { RemoteSourcePublisher } from './api/git';
export interface IRemoteSourcePublisherRegistry {
readonly onDidAddRemoteSourcePublisher: Event<RemoteSourcePublisher>;
readonly onDidRemoveRemoteSourcePublisher: Event<RemoteSourcePublisher>;
getRemoteSourcePublishers(): RemoteSourcePublisher[];
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;
}
+17
View File
@@ -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.
*--------------------------------------------------------------------------------------------*/
import { PickRemoteSourceOptions, PickRemoteSourceResult } from './typings/git-base';
import { GitBaseApi } from './git-base';
export async function pickRemoteSource(options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise<string | undefined>;
export async function pickRemoteSource(options: PickRemoteSourceOptions & { branch: true }): Promise<PickRemoteSourceResult | undefined>;
export async function pickRemoteSource(options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {
return GitBaseApi.getAPI().pickRemoteSource(options);
}
export async function getRemoteSourceActions(url: string) {
return GitBaseApi.getAPI().getRemoteSourceActions(url);
}
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
echo ''
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
VSCODE_GIT_ASKPASS_PIPE=`mktemp`
ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" VSCODE_GIT_ASKPASS_TYPE="ssh" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $VSCODE_GIT_ASKPASS_EXTRA_ARGS $*
cat $VSCODE_GIT_ASKPASS_PIPE
rm $VSCODE_GIT_ASKPASS_PIPE
+217
View File
@@ -0,0 +1,217 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDocument, Range, Selection, Uri, TextEditor, TextEditorDiffInformation } from 'vscode';
import { fromGitUri, isGitUri } from './uri';
export interface LineChange {
readonly originalStartLineNumber: number;
readonly originalEndLineNumber: number;
readonly modifiedStartLineNumber: number;
readonly modifiedEndLineNumber: number;
}
export function applyLineChanges(original: TextDocument, modified: TextDocument, diffs: LineChange[]): string {
const result: string[] = [];
let currentLine = 0;
for (const diff of diffs) {
const isInsertion = diff.originalEndLineNumber === 0;
const isDeletion = diff.modifiedEndLineNumber === 0;
let endLine = isInsertion ? diff.originalStartLineNumber : diff.originalStartLineNumber - 1;
let endCharacter = 0;
// if this is a deletion at the very end of the document,then we need to account
// for a newline at the end of the last line which may have been deleted
// https://github.com/microsoft/vscode/issues/59670
if (isDeletion && diff.originalEndLineNumber === original.lineCount) {
endLine -= 1;
endCharacter = original.lineAt(endLine).range.end.character;
}
result.push(original.getText(new Range(currentLine, 0, endLine, endCharacter)));
if (!isDeletion) {
let fromLine = diff.modifiedStartLineNumber - 1;
let fromCharacter = 0;
// if this is an insertion at the very end of the document,
// then we must start the next range after the last character of the
// previous line, in order to take the correct eol
if (isInsertion && diff.originalStartLineNumber === original.lineCount) {
fromLine -= 1;
fromCharacter = modified.lineAt(fromLine).range.end.character;
}
result.push(modified.getText(new Range(fromLine, fromCharacter, diff.modifiedEndLineNumber, 0)));
}
currentLine = isInsertion ? diff.originalStartLineNumber : diff.originalEndLineNumber;
}
result.push(original.getText(new Range(currentLine, 0, original.lineCount, 0)));
return result.join('');
}
export function toLineRanges(selections: readonly Selection[], textDocument: TextDocument): Range[] {
const lineRanges = selections.map(s => {
const startLine = textDocument.lineAt(s.start.line);
const endLine = textDocument.lineAt(s.end.line);
return new Range(startLine.range.start, endLine.range.end);
});
lineRanges.sort((a, b) => a.start.line - b.start.line);
const result = lineRanges.reduce((result, l) => {
if (result.length === 0) {
result.push(l);
return result;
}
const [last, ...rest] = result;
const intersection = l.intersection(last);
if (intersection) {
return [intersection, ...rest];
}
if (l.start.line === last.end.line + 1) {
const merge = new Range(last.start, l.end);
return [merge, ...rest];
}
return [l, ...result];
}, [] as Range[]);
result.reverse();
return result;
}
export function getModifiedRange(textDocument: TextDocument, diff: LineChange): Range {
if (diff.modifiedEndLineNumber === 0) {
if (diff.modifiedStartLineNumber === 0) {
return new Range(textDocument.lineAt(diff.modifiedStartLineNumber).range.end, textDocument.lineAt(diff.modifiedStartLineNumber).range.start);
} else if (textDocument.lineCount === diff.modifiedStartLineNumber) {
return new Range(textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end, textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end);
} else {
return new Range(textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end, textDocument.lineAt(diff.modifiedStartLineNumber).range.start);
}
} else {
return new Range(textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.start, textDocument.lineAt(diff.modifiedEndLineNumber - 1).range.end);
}
}
export function intersectDiffWithRange(textDocument: TextDocument, diff: LineChange, range: Range): LineChange | null {
const modifiedRange = getModifiedRange(textDocument, diff);
const intersection = range.intersection(modifiedRange);
if (!intersection) {
return null;
}
if (diff.modifiedEndLineNumber === 0) {
return diff;
} else {
const modifiedStartLineNumber = intersection.start.line + 1;
const modifiedEndLineNumber = intersection.end.line + 1;
// heuristic: same number of lines on both sides, let's assume line by line
if (diff.originalEndLineNumber - diff.originalStartLineNumber === diff.modifiedEndLineNumber - diff.modifiedStartLineNumber) {
const delta = modifiedStartLineNumber - diff.modifiedStartLineNumber;
const length = modifiedEndLineNumber - modifiedStartLineNumber;
return {
originalStartLineNumber: diff.originalStartLineNumber + delta,
originalEndLineNumber: diff.originalStartLineNumber + delta + length,
modifiedStartLineNumber,
modifiedEndLineNumber
};
} else {
return {
originalStartLineNumber: diff.originalStartLineNumber,
originalEndLineNumber: diff.originalEndLineNumber,
modifiedStartLineNumber,
modifiedEndLineNumber
};
}
}
}
export function invertLineChange(diff: LineChange): LineChange {
return {
modifiedStartLineNumber: diff.originalStartLineNumber,
modifiedEndLineNumber: diff.originalEndLineNumber,
originalStartLineNumber: diff.modifiedStartLineNumber,
originalEndLineNumber: diff.modifiedEndLineNumber
};
}
export function toLineChanges(diffInformation: TextEditorDiffInformation): LineChange[] {
return diffInformation.changes.map(x => {
let originalStartLineNumber: number;
let originalEndLineNumber: number;
let modifiedStartLineNumber: number;
let modifiedEndLineNumber: number;
if (x.original.startLineNumber === x.original.endLineNumberExclusive) {
// Insertion
originalStartLineNumber = x.original.startLineNumber - 1;
originalEndLineNumber = 0;
} else {
originalStartLineNumber = x.original.startLineNumber;
originalEndLineNumber = x.original.endLineNumberExclusive - 1;
}
if (x.modified.startLineNumber === x.modified.endLineNumberExclusive) {
// Deletion
modifiedStartLineNumber = x.modified.startLineNumber - 1;
modifiedEndLineNumber = 0;
} else {
modifiedStartLineNumber = x.modified.startLineNumber;
modifiedEndLineNumber = x.modified.endLineNumberExclusive - 1;
}
return {
originalStartLineNumber,
originalEndLineNumber,
modifiedStartLineNumber,
modifiedEndLineNumber
};
});
}
export function getIndexDiffInformation(textEditor: TextEditor): TextEditorDiffInformation | undefined {
// Diff Editor (Index)
return textEditor.diffInformation?.find(diff =>
diff.original && isGitUri(diff.original) && fromGitUri(diff.original).ref === 'HEAD' &&
diff.modified && isGitUri(diff.modified) && fromGitUri(diff.modified).ref === '');
}
export function getWorkingTreeDiffInformation(textEditor: TextEditor): TextEditorDiffInformation | undefined {
// Working tree diff information. Diff Editor (Working Tree) -> Text Editor
return getDiffInformation(textEditor, '~') ?? getDiffInformation(textEditor, '');
}
export function getWorkingTreeAndIndexDiffInformation(textEditor: TextEditor): TextEditorDiffInformation | undefined {
return getDiffInformation(textEditor, 'HEAD');
}
function getDiffInformation(textEditor: TextEditor, ref: string): TextEditorDiffInformation | undefined {
return textEditor.diffInformation?.find(diff => diff.original && isGitUri(diff.original) && fromGitUri(diff.original).ref === ref);
}
export interface DiffEditorSelectionHunkToolbarContext {
mapping: unknown;
/**
* The original text with the selected modified changes applied.
*/
originalWithModifiedChanges: string;
modifiedUri: Uri;
originalUri: Uri;
}
+300
View File
@@ -0,0 +1,300 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Command, EventEmitter, Event, workspace, Uri, l10n } from 'vscode';
import { Repository } from './repository';
import { anyEvent, dispose, filterEvent } from './util';
import { Branch, RefType, RemoteSourcePublisher } from './api/git';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
import { CheckoutOperation, CheckoutTrackingOperation, OperationKind } from './operation';
interface CheckoutStatusBarState {
readonly isCheckoutRunning: boolean;
readonly isCommitRunning: boolean;
readonly isSyncRunning: boolean;
}
class CheckoutStatusBar {
private _onDidChange = new EventEmitter<void>();
get onDidChange(): Event<void> { return this._onDidChange.event; }
private disposables: Disposable[] = [];
private _state: CheckoutStatusBarState;
private get state() { return this._state; }
private set state(state: CheckoutStatusBarState) {
this._state = state;
this._onDidChange.fire();
}
constructor(private repository: Repository) {
this._state = {
isCheckoutRunning: false,
isCommitRunning: false,
isSyncRunning: false
};
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
repository.onDidRunGitStatus(this._onDidChange.fire, this._onDidChange, this.disposables);
repository.onDidChangeBranchProtection(this._onDidChange.fire, this._onDidChange, this.disposables);
}
get command(): Command | undefined {
const operationData = [
...this.repository.operations.getOperations(OperationKind.Checkout) as CheckoutOperation[],
...this.repository.operations.getOperations(OperationKind.CheckoutTracking) as CheckoutTrackingOperation[]
];
const rebasing = !!this.repository.rebaseCommit;
const label = operationData[0]?.refLabel ?? `${this.repository.headLabel}${rebasing ? ` (${l10n.t('Rebasing')})` : ''}`;
const command = (this.state.isCheckoutRunning || this.state.isCommitRunning || this.state.isSyncRunning) ? '' : 'git.checkout';
return {
command,
tooltip: `${label}, ${this.getTooltip()}`,
title: `${this.getIcon()} ${label}`,
arguments: [this.repository.sourceControl]
};
}
private getIcon(): string {
if (!this.repository.HEAD) {
return '';
}
// Checkout
if (this.state.isCheckoutRunning) {
return '$(loading~spin)';
}
// Branch
if (this.repository.HEAD.type === RefType.Head && this.repository.HEAD.name) {
return this.repository.isBranchProtected() ? '$(lock)' : '$(git-branch)';
}
// Tag
if (this.repository.HEAD.type === RefType.Tag) {
return '$(tag)';
}
// Commit
return '$(git-commit)';
}
private getTooltip(): string {
if (this.state.isCheckoutRunning) {
return l10n.t('Checking Out Branch/Tag...');
}
if (this.state.isCommitRunning) {
return l10n.t('Committing Changes...');
}
if (this.state.isSyncRunning) {
return l10n.t('Synchronizing Changes...');
}
return l10n.t('Checkout Branch/Tag...');
}
private onDidChangeOperations(): void {
const isCommitRunning = this.repository.operations.isRunning(OperationKind.Commit);
const isCheckoutRunning = this.repository.operations.isRunning(OperationKind.Checkout) ||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
const isSyncRunning = this.repository.operations.isRunning(OperationKind.Sync) ||
this.repository.operations.isRunning(OperationKind.Push) ||
this.repository.operations.isRunning(OperationKind.Pull);
this.state = { ...this.state, isCheckoutRunning, isCommitRunning, isSyncRunning };
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}
interface SyncStatusBarState {
readonly enabled: boolean;
readonly isCheckoutRunning: boolean;
readonly isCommitRunning: boolean;
readonly isSyncRunning: boolean;
readonly hasRemotes: boolean;
readonly HEAD: Branch | undefined;
readonly remoteSourcePublishers: RemoteSourcePublisher[];
}
class SyncStatusBar {
private _onDidChange = new EventEmitter<void>();
get onDidChange(): Event<void> { return this._onDidChange.event; }
private disposables: Disposable[] = [];
private _state: SyncStatusBarState;
private get state() { return this._state; }
private set state(state: SyncStatusBarState) {
this._state = state;
this._onDidChange.fire();
}
constructor(private repository: Repository, private remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry) {
this._state = {
enabled: true,
isCheckoutRunning: false,
isCommitRunning: false,
isSyncRunning: false,
hasRemotes: false,
HEAD: undefined,
remoteSourcePublishers: remoteSourcePublisherRegistry.getRemoteSourcePublishers()
};
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
anyEvent(remoteSourcePublisherRegistry.onDidAddRemoteSourcePublisher, remoteSourcePublisherRegistry.onDidRemoveRemoteSourcePublisher)
(this.onDidChangeRemoteSourcePublishers, this, this.disposables);
const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.enableStatusBarSync'));
onEnablementChange(this.updateEnablement, this, this.disposables);
this.updateEnablement();
}
private updateEnablement(): void {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const enabled = config.get<boolean>('enableStatusBarSync', true);
this.state = { ... this.state, enabled };
}
private onDidChangeOperations(): void {
const isCommitRunning = this.repository.operations.isRunning(OperationKind.Commit);
const isCheckoutRunning = this.repository.operations.isRunning(OperationKind.Checkout) ||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
const isSyncRunning = this.repository.operations.isRunning(OperationKind.Sync) ||
this.repository.operations.isRunning(OperationKind.Push) ||
this.repository.operations.isRunning(OperationKind.Pull);
this.state = { ...this.state, isCheckoutRunning, isCommitRunning, isSyncRunning };
}
private onDidRunGitStatus(): void {
this.state = {
...this.state,
hasRemotes: this.repository.remotes.length > 0,
HEAD: this.repository.HEAD
};
}
private onDidChangeRemoteSourcePublishers(): void {
this.state = {
...this.state,
remoteSourcePublishers: this.remoteSourcePublisherRegistry.getRemoteSourcePublishers()
};
}
get command(): Command | undefined {
if (!this.state.enabled) {
return;
}
if (!this.state.hasRemotes) {
if (this.state.remoteSourcePublishers.length === 0) {
return;
}
const command = (this.state.isCheckoutRunning || this.state.isCommitRunning) ? '' : 'git.publish';
const tooltip =
this.state.isCheckoutRunning ? l10n.t('Checking Out Changes...') :
this.state.isCommitRunning ? l10n.t('Committing Changes...') :
this.state.remoteSourcePublishers.length === 1
? l10n.t('Publish to {0}', this.state.remoteSourcePublishers[0].name)
: l10n.t('Publish to...');
return {
command,
title: `$(cloud-upload)`,
tooltip,
arguments: [this.repository.sourceControl]
};
}
const HEAD = this.state.HEAD;
let icon = '$(sync)';
let text = '';
let command = '';
let tooltip = '';
if (HEAD && HEAD.name && HEAD.commit) {
if (HEAD.upstream) {
if (HEAD.ahead || HEAD.behind) {
text += this.repository.syncLabel;
}
command = 'git.sync';
tooltip = this.repository.syncTooltip;
} else {
icon = '$(cloud-upload)';
command = 'git.publish';
tooltip = l10n.t('Publish Branch');
}
} else {
command = '';
tooltip = '';
}
if (this.state.isCheckoutRunning) {
command = '';
tooltip = l10n.t('Checking Out Changes...');
}
if (this.state.isCommitRunning) {
command = '';
tooltip = l10n.t('Committing Changes...');
}
if (this.state.isSyncRunning) {
icon = '$(sync~spin)';
command = '';
tooltip = l10n.t('Synchronizing Changes...');
}
return {
command,
title: [icon, text].join(' ').trim(),
tooltip,
arguments: [this.repository.sourceControl]
};
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}
export class StatusBarCommands {
readonly onDidChange: Event<void>;
private syncStatusBar: SyncStatusBar;
private checkoutStatusBar: CheckoutStatusBar;
private disposables: Disposable[] = [];
constructor(repository: Repository, remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry) {
this.syncStatusBar = new SyncStatusBar(repository, remoteSourcePublisherRegistry);
this.checkoutStatusBar = new CheckoutStatusBar(repository);
this.onDidChange = anyEvent(this.syncStatusBar.onDidChange, this.checkoutStatusBar.onDidChange);
}
get commands(): Command[] {
return [this.checkoutStatusBar.command, this.syncStatusBar.command]
.filter((c): c is Command => !!c);
}
dispose(): void {
this.syncStatusBar.dispose();
this.checkoutStatusBar.dispose();
this.disposables = dispose(this.disposables);
}
}
+92
View File
@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtensionContext, l10n, LogOutputChannel, TerminalShellExecutionEndEvent, window, workspace } from 'vscode';
import { dispose, filterEvent, IDisposable } from './util';
import { Model } from './model';
export interface ITerminalEnvironmentProvider {
featureDescription?: string;
getTerminalEnv(): { [key: string]: string };
}
export class TerminalEnvironmentManager {
private readonly disposable: IDisposable;
constructor(private readonly context: ExtensionContext, private readonly envProviders: (ITerminalEnvironmentProvider | undefined)[]) {
this.disposable = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git'))
(this.refresh, this);
this.refresh();
}
private refresh(): void {
const config = workspace.getConfiguration('git', null);
this.context.environmentVariableCollection.clear();
if (!config.get<boolean>('enabled', true)) {
return;
}
const features: string[] = [];
for (const envProvider of this.envProviders) {
const terminalEnv = envProvider?.getTerminalEnv() ?? {};
for (const name of Object.keys(terminalEnv)) {
this.context.environmentVariableCollection.replace(name, terminalEnv[name]);
}
if (envProvider?.featureDescription && Object.keys(terminalEnv).length > 0) {
features.push(envProvider.featureDescription);
}
}
if (features.length) {
this.context.environmentVariableCollection.description = l10n.t('Enables the following features: {0}', features.join(', '));
}
}
dispose(): void {
this.disposable.dispose();
}
}
export class TerminalShellExecutionManager {
private readonly subcommands = new Set<string>([
'add', 'branch', 'checkout', 'cherry-pick', 'clean', 'commit', 'fetch', 'merge',
'mv', 'rebase', 'reset', 'restore', 'revert', 'rm', 'pull', 'push', 'stash', 'switch']);
private readonly disposables: IDisposable[] = [];
constructor(
private readonly model: Model,
private readonly logger: LogOutputChannel
) {
window.onDidEndTerminalShellExecution(this.onDidEndTerminalShellExecution, this, this.disposables);
}
private onDidEndTerminalShellExecution(e: TerminalShellExecutionEndEvent): void {
const { execution, exitCode, shellIntegration } = e;
const [executable, subcommand] = execution.commandLine.value.split(/\s+/);
const cwd = execution.cwd ?? shellIntegration.cwd;
if (executable.toLowerCase() !== 'git' || !this.subcommands.has(subcommand?.toLowerCase()) || !cwd || exitCode !== 0) {
return;
}
this.logger.trace(`[TerminalShellExecutionManager][onDidEndTerminalShellExecution] Matched git subcommand: ${subcommand}`);
const repository = this.model.getRepository(cwd);
if (!repository) {
this.logger.trace(`[TerminalShellExecutionManager][onDidEndTerminalShellExecution] Unable to find repository for current working directory: ${cwd.toString()}`);
return;
}
repository.status();
}
dispose(): void {
dispose(this.disposables);
}
}
+666
View File
@@ -0,0 +1,666 @@
/*---------------------------------------------------------------------------------------------
* 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 { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles, parseGitRemotes } from '../git';
import * as assert from 'assert';
import { splitInChunks } from '../util';
suite('git', () => {
suite('GitStatusParser', () => {
test('empty parser', () => {
const parser = new GitStatusParser();
assert.deepStrictEqual(parser.status, []);
});
test('empty parser 2', () => {
const parser = new GitStatusParser();
parser.update('');
assert.deepStrictEqual(parser.status, []);
});
test('simple', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0');
assert.deepStrictEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('simple 2', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0');
parser.update('?? file2.txt\0');
parser.update('?? file3.txt\0');
assert.deepStrictEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('empty lines', () => {
const parser = new GitStatusParser();
parser.update('');
parser.update('?? file.txt\0');
parser.update('');
parser.update('');
parser.update('?? file2.txt\0');
parser.update('');
parser.update('?? file3.txt\0');
parser.update('');
assert.deepStrictEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('combined', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0?? file2.txt\0?? file3.txt\0');
assert.deepStrictEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('split 1', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0?? file2');
parser.update('.txt\0?? file3.txt\0');
assert.deepStrictEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('split 2', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt');
parser.update('\0?? file2.txt\0?? file3.txt\0');
assert.deepStrictEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('split 3', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0?? file2.txt\0?? file3.txt');
parser.update('\0');
assert.deepStrictEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('rename', () => {
const parser = new GitStatusParser();
parser.update('R newfile.txt\0file.txt\0?? file2.txt\0?? file3.txt\0');
assert.deepStrictEqual(parser.status, [
{ path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('rename split', () => {
const parser = new GitStatusParser();
parser.update('R newfile.txt\0fil');
parser.update('e.txt\0?? file2.txt\0?? file3.txt\0');
assert.deepStrictEqual(parser.status, [
{ path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('rename split 3', () => {
const parser = new GitStatusParser();
parser.update('?? file2.txt\0R new');
parser.update('file.txt\0fil');
parser.update('e.txt\0?? file3.txt\0');
assert.deepStrictEqual(parser.status, [
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
});
suite('parseGitmodules', () => {
test('empty', () => {
assert.deepStrictEqual(parseGitmodules(''), []);
});
test('sample', () => {
const sample = `[submodule "deps/spdlog"]
path = deps/spdlog
url = https://github.com/gabime/spdlog.git
`;
assert.deepStrictEqual(parseGitmodules(sample), [
{ name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' }
]);
});
test('big', () => {
const sample = `[submodule "deps/spdlog"]
path = deps/spdlog
url = https://github.com/gabime/spdlog.git
[submodule "deps/spdlog2"]
path = deps/spdlog2
url = https://github.com/gabime/spdlog.git
[submodule "deps/spdlog3"]
path = deps/spdlog3
url = https://github.com/gabime/spdlog.git
[submodule "deps/spdlog4"]
path = deps/spdlog4
url = https://github.com/gabime/spdlog4.git
`;
assert.deepStrictEqual(parseGitmodules(sample), [
{ name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' },
{ name: 'deps/spdlog2', path: 'deps/spdlog2', url: 'https://github.com/gabime/spdlog.git' },
{ name: 'deps/spdlog3', path: 'deps/spdlog3', url: 'https://github.com/gabime/spdlog.git' },
{ name: 'deps/spdlog4', path: 'deps/spdlog4', url: 'https://github.com/gabime/spdlog4.git' }
]);
});
test('whitespace #74844', () => {
const sample = `[submodule "deps/spdlog"]
path = deps/spdlog
url = https://github.com/gabime/spdlog.git
`;
assert.deepStrictEqual(parseGitmodules(sample), [
{ name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' }
]);
});
test('whitespace again #108371', () => {
const sample = `[submodule "deps/spdlog"]
path= deps/spdlog
url=https://github.com/gabime/spdlog.git
`;
assert.deepStrictEqual(parseGitmodules(sample), [
{ name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' }
]);
});
});
suite('parseGitRemotes', () => {
test('empty', () => {
assert.deepStrictEqual(parseGitRemotes(''), []);
});
test('single remote', () => {
const sample = `[remote "origin"]
url = https://github.com/microsoft/vscode.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), [
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode.git', isReadOnly: false }
]);
});
test('single remote (multiple urls)', () => {
const sample = `[remote "origin"]
url = https://github.com/microsoft/vscode.git
url = https://github.com/microsoft/vscode2.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), [
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode.git', isReadOnly: false }
]);
});
test('multiple remotes', () => {
const sample = `[remote "origin"]
url = https://github.com/microsoft/vscode.git
pushurl = https://github.com/microsoft/vscode1.git
fetch = +refs/heads/*:refs/remotes/origin/*
[remote "remote2"]
url = https://github.com/microsoft/vscode2.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), [
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode1.git', isReadOnly: false },
{ name: 'remote2', fetchUrl: 'https://github.com/microsoft/vscode2.git', pushUrl: 'https://github.com/microsoft/vscode2.git', isReadOnly: false }
]);
});
test('remotes (white space)', () => {
const sample = ` [remote "origin"]
url = https://github.com/microsoft/vscode.git
pushurl=https://github.com/microsoft/vscode1.git
fetch = +refs/heads/*:refs/remotes/origin/*
[ remote"remote2"]
url = https://github.com/microsoft/vscode2.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), [
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode1.git', isReadOnly: false },
{ name: 'remote2', fetchUrl: 'https://github.com/microsoft/vscode2.git', pushUrl: 'https://github.com/microsoft/vscode2.git', isReadOnly: false }
]);
});
test('remotes (invalid section)', () => {
const sample = `[remote "origin"
url = https://github.com/microsoft/vscode.git
pushurl = https://github.com/microsoft/vscode1.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), []);
});
});
suite('parseGitCommit', () => {
test('single parent commit', function () {
const GIT_OUTPUT_SINGLE_PARENT =
'52c293a05038d865604c2284aa8698bd087915a1\n' +
'John Doe\n' +
'john.doe@mail.com\n' +
'1580811030\n' +
'1580811031\n' +
'8e5a374372b8393906c7e380dbb09349c5385554\n' +
'main,branch\n' +
'This is a commit message.\x00';
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
authorDate: new Date(1580811030000),
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
refNames: ['main', 'branch'],
shortStat: undefined
}]);
});
test('multiple parent commits', function () {
const GIT_OUTPUT_MULTIPLE_PARENTS =
'52c293a05038d865604c2284aa8698bd087915a1\n' +
'John Doe\n' +
'john.doe@mail.com\n' +
'1580811030\n' +
'1580811031\n' +
'8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217\n' +
'main\n' +
'This is a commit message.\x00';
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217'],
authorDate: new Date(1580811030000),
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
refNames: ['main'],
shortStat: undefined
}]);
});
test('no parent commits', function () {
const GIT_OUTPUT_NO_PARENTS =
'52c293a05038d865604c2284aa8698bd087915a1\n' +
'John Doe\n' +
'john.doe@mail.com\n' +
'1580811030\n' +
'1580811031\n' +
'\n' +
'main\n' +
'This is a commit message.\x00';
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: [],
authorDate: new Date(1580811030000),
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
refNames: ['main'],
shortStat: undefined
}]);
});
test('commit with shortstat', function () {
const GIT_OUTPUT_SINGLE_PARENT =
'52c293a05038d865604c2284aa8698bd087915a1\n' +
'John Doe\n' +
'john.doe@mail.com\n' +
'1580811030\n' +
'1580811031\n' +
'8e5a374372b8393906c7e380dbb09349c5385554\n' +
'main,branch\n' +
'This is a commit message.\x00\n' +
' 1 file changed, 2 insertions(+), 3 deletion(-)';
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
authorDate: new Date(1580811030000),
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
refNames: ['main', 'branch'],
shortStat: {
deletions: 3,
files: 1,
insertions: 2
}
}]);
});
test('commit with shortstat (no insertions)', function () {
const GIT_OUTPUT_SINGLE_PARENT =
'52c293a05038d865604c2284aa8698bd087915a1\n' +
'John Doe\n' +
'john.doe@mail.com\n' +
'1580811030\n' +
'1580811031\n' +
'8e5a374372b8393906c7e380dbb09349c5385554\n' +
'main,branch\n' +
'This is a commit message.\x00\n' +
' 1 file changed, 3 deletion(-)';
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
authorDate: new Date(1580811030000),
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
refNames: ['main', 'branch'],
shortStat: {
deletions: 3,
files: 1,
insertions: 0
}
}]);
});
test('commit with shortstat (no deletions)', function () {
const GIT_OUTPUT_SINGLE_PARENT =
'52c293a05038d865604c2284aa8698bd087915a1\n' +
'John Doe\n' +
'john.doe@mail.com\n' +
'1580811030\n' +
'1580811031\n' +
'8e5a374372b8393906c7e380dbb09349c5385554\n' +
'main,branch\n' +
'This is a commit message.\x00\n' +
' 1 file changed, 2 insertions(+)';
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
authorDate: new Date(1580811030000),
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
refNames: ['main', 'branch'],
shortStat: {
deletions: 0,
files: 1,
insertions: 2
}
}]);
});
test('commit list', function () {
const GIT_OUTPUT_SINGLE_PARENT =
'52c293a05038d865604c2284aa8698bd087915a1\n' +
'John Doe\n' +
'john.doe@mail.com\n' +
'1580811030\n' +
'1580811031\n' +
'8e5a374372b8393906c7e380dbb09349c5385554\n' +
'main,branch\n' +
'This is a commit message.\x00\n' +
'52c293a05038d865604c2284aa8698bd087915a2\n' +
'Jane Doe\n' +
'jane.doe@mail.com\n' +
'1580811032\n' +
'1580811033\n' +
'8e5a374372b8393906c7e380dbb09349c5385555\n' +
'main,branch\n' +
'This is another commit message.\x00';
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [
{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
authorDate: new Date(1580811030000),
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
refNames: ['main', 'branch'],
shortStat: undefined,
},
{
hash: '52c293a05038d865604c2284aa8698bd087915a2',
message: 'This is another commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385555'],
authorDate: new Date(1580811032000),
authorName: 'Jane Doe',
authorEmail: 'jane.doe@mail.com',
commitDate: new Date(1580811033000),
refNames: ['main', 'branch'],
shortStat: undefined,
},
]);
});
test('commit list with shortstat', function () {
const GIT_OUTPUT_SINGLE_PARENT = '52c293a05038d865604c2284aa8698bd087915a1\n' +
'John Doe\n' +
'john.doe@mail.com\n' +
'1580811030\n' +
'1580811031\n' +
'8e5a374372b8393906c7e380dbb09349c5385554\n' +
'main,branch\n' +
'This is a commit message.\x00\n' +
' 5 file changed, 12 insertions(+), 13 deletion(-)\n' +
'52c293a05038d865604c2284aa8698bd087915a2\n' +
'Jane Doe\n' +
'jane.doe@mail.com\n' +
'1580811032\n' +
'1580811033\n' +
'8e5a374372b8393906c7e380dbb09349c5385555\n' +
'main,branch\n' +
'This is another commit message.\x00\n' +
' 6 file changed, 22 insertions(+), 23 deletion(-)';
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
authorDate: new Date(1580811030000),
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
refNames: ['main', 'branch'],
shortStat: {
deletions: 13,
files: 5,
insertions: 12
}
},
{
hash: '52c293a05038d865604c2284aa8698bd087915a2',
message: 'This is another commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385555'],
authorDate: new Date(1580811032000),
authorName: 'Jane Doe',
authorEmail: 'jane.doe@mail.com',
commitDate: new Date(1580811033000),
refNames: ['main', 'branch'],
shortStat: {
deletions: 23,
files: 6,
insertions: 22
}
}]);
});
});
suite('parseLsTree', function () {
test('sample', function () {
const input = `040000 tree 0274a81f8ee9ca3669295dc40f510bd2021d0043 - .vscode
100644 blob 1d487c1817262e4f20efbfa1d04c18f51b0046f6 491570 Screen Shot 2018-06-01 at 14.48.05.png
100644 blob 686c16e4f019b734655a2576ce8b98749a9ffdb9 764420 Screen Shot 2018-06-07 at 20.04.59.png
100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 4 boom.txt
100644 blob 86dc360dd25f13fa50ffdc8259e9653921f4f2b7 11 boomcaboom.txt
100644 blob a68b14060589b16d7ac75f67b905c918c03c06eb 24 file.js
100644 blob f7bcfb05af46850d780f88c069edcd57481d822d 201 file.md
100644 blob ab8b86114a051f6490f1ec5e3141b9a632fb46b5 8 hello.js
100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 4 what.js
100644 blob be859e3f412fa86513cd8bebe8189d1ea1a3e46d 24 what.txt
100644 blob 56ec42c9dc6fcf4534788f0fe34b36e09f37d085 261186 what.txt2`;
const output = parseLsTree(input);
assert.deepStrictEqual(output, [
{ mode: '040000', type: 'tree', object: '0274a81f8ee9ca3669295dc40f510bd2021d0043', size: '-', file: '.vscode' },
{ mode: '100644', type: 'blob', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', size: '491570', file: 'Screen Shot 2018-06-01 at 14.48.05.png' },
{ mode: '100644', type: 'blob', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', size: '764420', file: 'Screen Shot 2018-06-07 at 20.04.59.png' },
{ mode: '100644', type: 'blob', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', size: '4', file: 'boom.txt' },
{ mode: '100644', type: 'blob', object: '86dc360dd25f13fa50ffdc8259e9653921f4f2b7', size: '11', file: 'boomcaboom.txt' },
{ mode: '100644', type: 'blob', object: 'a68b14060589b16d7ac75f67b905c918c03c06eb', size: '24', file: 'file.js' },
{ mode: '100644', type: 'blob', object: 'f7bcfb05af46850d780f88c069edcd57481d822d', size: '201', file: 'file.md' },
{ mode: '100644', type: 'blob', object: 'ab8b86114a051f6490f1ec5e3141b9a632fb46b5', size: '8', file: 'hello.js' },
{ mode: '100644', type: 'blob', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', size: '4', file: 'what.js' },
{ mode: '100644', type: 'blob', object: 'be859e3f412fa86513cd8bebe8189d1ea1a3e46d', size: '24', file: 'what.txt' },
{ mode: '100644', type: 'blob', object: '56ec42c9dc6fcf4534788f0fe34b36e09f37d085', size: '261186', file: 'what.txt2' }
]);
});
});
suite('parseLsFiles', function () {
test('sample', function () {
const input = `100644 7a73a41bfdf76d6f793007240d80983a52f15f97 0 .vscode/settings.json
100644 1d487c1817262e4f20efbfa1d04c18f51b0046f6 0 Screen Shot 2018-06-01 at 14.48.05.png
100644 686c16e4f019b734655a2576ce8b98749a9ffdb9 0 Screen Shot 2018-06-07 at 20.04.59.png
100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 boom.txt
100644 86dc360dd25f13fa50ffdc8259e9653921f4f2b7 0 boomcaboom.txt
100644 a68b14060589b16d7ac75f67b905c918c03c06eb 0 file.js
100644 f7bcfb05af46850d780f88c069edcd57481d822d 0 file.md
100644 ab8b86114a051f6490f1ec5e3141b9a632fb46b5 0 hello.js
100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 what.js
100644 be859e3f412fa86513cd8bebe8189d1ea1a3e46d 0 what.txt
100644 56ec42c9dc6fcf4534788f0fe34b36e09f37d085 0 what.txt2`;
const output = parseLsFiles(input);
assert.deepStrictEqual(output, [
{ mode: '100644', object: '7a73a41bfdf76d6f793007240d80983a52f15f97', stage: '0', file: '.vscode/settings.json' },
{ mode: '100644', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', stage: '0', file: 'Screen Shot 2018-06-01 at 14.48.05.png' },
{ mode: '100644', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', stage: '0', file: 'Screen Shot 2018-06-07 at 20.04.59.png' },
{ mode: '100644', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', stage: '0', file: 'boom.txt' },
{ mode: '100644', object: '86dc360dd25f13fa50ffdc8259e9653921f4f2b7', stage: '0', file: 'boomcaboom.txt' },
{ mode: '100644', object: 'a68b14060589b16d7ac75f67b905c918c03c06eb', stage: '0', file: 'file.js' },
{ mode: '100644', object: 'f7bcfb05af46850d780f88c069edcd57481d822d', stage: '0', file: 'file.md' },
{ mode: '100644', object: 'ab8b86114a051f6490f1ec5e3141b9a632fb46b5', stage: '0', file: 'hello.js' },
{ mode: '100644', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', stage: '0', file: 'what.js' },
{ mode: '100644', object: 'be859e3f412fa86513cd8bebe8189d1ea1a3e46d', stage: '0', file: 'what.txt' },
{ mode: '100644', object: '56ec42c9dc6fcf4534788f0fe34b36e09f37d085', stage: '0', file: 'what.txt2' },
]);
});
});
suite('splitInChunks', () => {
test('unit tests', function () {
assert.deepStrictEqual(
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 6)],
[['hello'], ['there'], ['cool'], ['stuff']]
);
assert.deepStrictEqual(
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 10)],
[['hello', 'there'], ['cool', 'stuff']]
);
assert.deepStrictEqual(
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 12)],
[['hello', 'there'], ['cool', 'stuff']]
);
assert.deepStrictEqual(
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 14)],
[['hello', 'there', 'cool'], ['stuff']]
);
assert.deepStrictEqual(
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 2000)],
[['hello', 'there', 'cool', 'stuff']]
);
assert.deepStrictEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 1)],
[['0'], ['01'], ['012'], ['0'], ['01'], ['012'], ['0'], ['01'], ['012']]
);
assert.deepStrictEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 2)],
[['0'], ['01'], ['012'], ['0'], ['01'], ['012'], ['0'], ['01'], ['012']]
);
assert.deepStrictEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 3)],
[['0', '01'], ['012'], ['0', '01'], ['012'], ['0', '01'], ['012']]
);
assert.deepStrictEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 4)],
[['0', '01'], ['012', '0'], ['01'], ['012', '0'], ['01'], ['012']]
);
assert.deepStrictEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 5)],
[['0', '01'], ['012', '0'], ['01', '012'], ['0', '01'], ['012']]
);
assert.deepStrictEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 6)],
[['0', '01', '012'], ['0', '01', '012'], ['0', '01', '012']]
);
assert.deepStrictEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 7)],
[['0', '01', '012', '0'], ['01', '012', '0'], ['01', '012']]
);
assert.deepStrictEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 8)],
[['0', '01', '012', '0'], ['01', '012', '0', '01'], ['012']]
);
assert.deepStrictEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 9)],
[['0', '01', '012', '0', '01'], ['012', '0', '01', '012']]
);
});
});
});
+40
View File
@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* 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 options: import('mocha').MochaOptions = {
ui: 'tdd',
color: true,
timeout: 60000
};
// These integration tests is being run in multiple environments (electron, web, remote)
// so we need to set the suite name based on the environment as the suite name is used
// for the test results file name
let suite = '';
if (process.env.VSCODE_BROWSER) {
suite = `${process.env.VSCODE_BROWSER} Browser Integration Git Tests`;
} else if (process.env.REMOTE_VSCODE) {
suite = 'Remote Integration Git Tests';
} else {
suite = 'Integration Git Tests';
}
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
options.reporter = 'mocha-multi-reporters';
options.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
testRunner.configure(options);
export = testRunner;
+177
View File
@@ -0,0 +1,177 @@
/*---------------------------------------------------------------------------------------------
* 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 assert from 'assert';
import { workspace, commands, window, Uri, WorkspaceEdit, Range, TextDocument, extensions, TabInputTextDiff } from 'vscode';
import * as cp from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { GitExtension, API, Repository, Status } from '../api/git';
import { eventToPromise } from '../util';
suite('git smoke test', function () {
const cwd = fs.realpathSync(workspace.workspaceFolders![0].uri.fsPath);
function file(relativePath: string) {
return path.join(cwd, relativePath);
}
function uri(relativePath: string) {
return Uri.file(file(relativePath));
}
async function open(relativePath: string) {
const doc = await workspace.openTextDocument(uri(relativePath));
await window.showTextDocument(doc);
return doc;
}
async function type(doc: TextDocument, text: string) {
const edit = new WorkspaceEdit();
const end = doc.lineAt(doc.lineCount - 1).range.end;
edit.replace(doc.uri, new Range(end, end), text);
await workspace.applyEdit(edit);
}
let git: API;
let repository: Repository;
suiteSetup(async function () {
fs.writeFileSync(file('app.js'), 'hello', 'utf8');
fs.writeFileSync(file('index.pug'), 'hello', 'utf8');
cp.execSync('git init -b main', { cwd });
cp.execSync('git config user.name testuser', { cwd });
cp.execSync('git config user.email monacotools@example.com', { cwd });
cp.execSync('git config commit.gpgsign false', { cwd });
cp.execSync('git add .', { cwd });
cp.execSync('git commit -m "initial commit"', { cwd });
// make sure git is activated
const ext = extensions.getExtension<GitExtension>('vscode.git');
await ext?.activate();
git = ext!.exports.getAPI(1);
if (git.repositories.length === 0) {
const onDidOpenRepository = eventToPromise(git.onDidOpenRepository);
await commands.executeCommand('git.openRepository', cwd);
await onDidOpenRepository;
}
assert.strictEqual(git.repositories.length, 1);
assert.strictEqual(fs.realpathSync(git.repositories[0].rootUri.fsPath), cwd);
repository = git.repositories[0];
});
test('reflects working tree changes', async function () {
await commands.executeCommand('workbench.view.scm');
const appjs = await open('app.js');
await type(appjs, ' world');
await appjs.save();
await repository.status();
assert.strictEqual(repository.state.workingTreeChanges.length, 1);
assert.strictEqual(repository.state.workingTreeChanges[0].uri.path, appjs.uri.path);
assert.strictEqual(repository.state.workingTreeChanges[0].status, Status.MODIFIED);
fs.writeFileSync(file('newfile.txt'), '');
const newfile = await open('newfile.txt');
await type(newfile, 'hey there');
await newfile.save();
await repository.status();
assert.strictEqual(repository.state.workingTreeChanges.length, 2);
assert.strictEqual(repository.state.workingTreeChanges[0].uri.path, appjs.uri.path);
assert.strictEqual(repository.state.workingTreeChanges[0].status, Status.MODIFIED);
assert.strictEqual(repository.state.workingTreeChanges[1].uri.path, newfile.uri.path);
assert.strictEqual(repository.state.workingTreeChanges[1].status, Status.UNTRACKED);
});
test('opens diff editor', async function () {
const appjs = uri('app.js');
await commands.executeCommand('git.openChange', appjs);
assert(window.activeTextEditor);
assert.strictEqual(window.activeTextEditor!.document.uri.path, appjs.path);
assert(window.tabGroups.activeTabGroup.activeTab);
assert(window.tabGroups.activeTabGroup.activeTab!.input instanceof TabInputTextDiff);
});
test('stages correctly', async function () {
const appjs = uri('app.js');
const newfile = uri('newfile.txt');
await repository.add([appjs.fsPath]);
assert.strictEqual(repository.state.indexChanges.length, 1);
assert.strictEqual(repository.state.indexChanges[0].uri.path, appjs.path);
assert.strictEqual(repository.state.indexChanges[0].status, Status.INDEX_MODIFIED);
assert.strictEqual(repository.state.workingTreeChanges.length, 1);
assert.strictEqual(repository.state.workingTreeChanges[0].uri.path, newfile.path);
assert.strictEqual(repository.state.workingTreeChanges[0].status, Status.UNTRACKED);
await repository.revert([appjs.fsPath]);
assert.strictEqual(repository.state.indexChanges.length, 0);
assert.strictEqual(repository.state.workingTreeChanges.length, 2);
assert.strictEqual(repository.state.workingTreeChanges[0].uri.path, appjs.path);
assert.strictEqual(repository.state.workingTreeChanges[0].status, Status.MODIFIED);
assert.strictEqual(repository.state.workingTreeChanges[1].uri.path, newfile.path);
assert.strictEqual(repository.state.workingTreeChanges[1].status, Status.UNTRACKED);
});
test('stages, commits changes and verifies outgoing change', async function () {
const appjs = uri('app.js');
const newfile = uri('newfile.txt');
await repository.add([appjs.fsPath]);
await repository.commit('second commit');
assert.strictEqual(repository.state.workingTreeChanges.length, 1);
assert.strictEqual(repository.state.workingTreeChanges[0].uri.path, newfile.path);
assert.strictEqual(repository.state.workingTreeChanges[0].status, Status.UNTRACKED);
assert.strictEqual(repository.state.indexChanges.length, 0);
await repository.commit('third commit', { all: true });
assert.strictEqual(repository.state.workingTreeChanges.length, 0);
assert.strictEqual(repository.state.indexChanges.length, 0);
});
test('rename/delete conflict', async function () {
await commands.executeCommand('workbench.view.scm');
const appjs = file('app.js');
const renamejs = file('rename.js');
await repository.createBranch('test', true);
// Delete file (test branch)
fs.unlinkSync(appjs);
await repository.commit('commit on test', { all: true });
await repository.checkout('main');
// Rename file (main branch)
fs.renameSync(appjs, renamejs);
await repository.commit('commit on main', { all: true });
try {
await repository.merge('test');
} catch (e) { }
assert.strictEqual(repository.state.mergeChanges.length, 1);
assert.strictEqual(repository.state.mergeChanges[0].status, Status.DELETED_BY_THEM);
assert.strictEqual(repository.state.workingTreeChanges.length, 0);
assert.strictEqual(repository.state.indexChanges.length, 0);
});
});
+380
View File
@@ -0,0 +1,380 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, ConfigurationChangeEvent, Disposable, env, Event, EventEmitter, MarkdownString, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace, l10n, Command } from 'vscode';
import { Model } from './model';
import { Repository, Resource } from './repository';
import { debounce } from './decorators';
import { emojify, ensureEmojis } from './emoji';
import { CommandCenter } from './commands';
import { OperationKind, OperationResult } from './operation';
import { getCommitShortHash } from './util';
import { CommitShortStat } from './git';
import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider';
import { AvatarQuery, AvatarQueryCommit } from './api/git';
const AVATAR_SIZE = 20;
export class GitTimelineItem extends TimelineItem {
static is(item: TimelineItem): item is GitTimelineItem {
return item instanceof GitTimelineItem;
}
readonly ref: string;
readonly previousRef: string;
readonly message: string;
constructor(
ref: string,
previousRef: string,
message: string,
timestamp: number,
id: string,
contextValue: string
) {
const index = message.indexOf('\n');
const label = index !== -1 ? `${message.substring(0, index)} \u2026` : message;
super(label, timestamp);
this.ref = ref;
this.previousRef = previousRef;
this.message = message;
this.id = id;
this.contextValue = contextValue;
}
get shortRef() {
return this.shortenRef(this.ref);
}
get shortPreviousRef() {
return this.shortenRef(this.previousRef);
}
setItemDetails(uri: Uri, hash: string | undefined, avatar: string | undefined, author: string, email: string | undefined, date: string, message: string, shortStat?: CommitShortStat, remoteSourceCommands: Command[] = []): void {
this.tooltip = new MarkdownString('', true);
this.tooltip.isTrusted = true;
const avatarMarkdown = avatar
? `![${author}](${avatar}|width=${AVATAR_SIZE},height=${AVATAR_SIZE})`
: '$(account)';
if (email) {
const emailTitle = l10n.t('Email');
this.tooltip.appendMarkdown(`${avatarMarkdown} [**${author}**](mailto:${email} "${emailTitle} ${author}")`);
} else {
this.tooltip.appendMarkdown(`${avatarMarkdown} **${author}**`);
}
this.tooltip.appendMarkdown(`, $(history) ${date}\n\n`);
this.tooltip.appendMarkdown(`${message}\n\n`);
if (shortStat) {
this.tooltip.appendMarkdown(`---\n\n`);
const labels: string[] = [];
if (shortStat.insertions) {
labels.push(`<span style="color:var(--vscode-scmGraph-historyItemHoverAdditionsForeground);">${shortStat.insertions === 1 ?
l10n.t('{0} insertion{1}', shortStat.insertions, '(+)') :
l10n.t('{0} insertions{1}', shortStat.insertions, '(+)')}</span>`);
}
if (shortStat.deletions) {
labels.push(`<span style="color:var(--vscode-scmGraph-historyItemHoverDeletionsForeground);">${shortStat.deletions === 1 ?
l10n.t('{0} deletion{1}', shortStat.deletions, '(-)') :
l10n.t('{0} deletions{1}', shortStat.deletions, '(-)')}</span>`);
}
this.tooltip.appendMarkdown(`${labels.join(', ')}\n\n`);
}
if (hash) {
this.tooltip.appendMarkdown(`---\n\n`);
this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('Open Commit')}")`);
this.tooltip.appendMarkdown('&nbsp;');
this.tooltip.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`);
// Remote commands
if (remoteSourceCommands.length > 0) {
this.tooltip.appendMarkdown('&nbsp;&nbsp;|&nbsp;&nbsp;');
const remoteCommandsMarkdown = remoteSourceCommands
.map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify([...command.arguments ?? [], hash]))} "${command.tooltip}")`);
this.tooltip.appendMarkdown(remoteCommandsMarkdown.join('&nbsp;'));
}
}
}
private shortenRef(ref: string): string {
if (ref === '' || ref === '~' || ref === 'HEAD') {
return ref;
}
return ref.endsWith('^') ? `${ref.substr(0, 8)}^` : ref.substr(0, 8);
}
}
export class GitTimelineProvider implements TimelineProvider {
private _onDidChange = new EventEmitter<TimelineChangeEvent | undefined>();
get onDidChange(): Event<TimelineChangeEvent | undefined> {
return this._onDidChange.event;
}
readonly id = 'git-history';
readonly label = l10n.t('Git History');
private readonly disposable: Disposable;
private providerDisposable: Disposable | undefined;
private repo: Repository | undefined;
private repoDisposable: Disposable | undefined;
private repoOperationDate: Date | undefined;
constructor(private readonly model: Model, private commands: CommandCenter) {
this.disposable = Disposable.from(
model.onDidOpenRepository(this.onRepositoriesChanged, this),
workspace.onDidChangeConfiguration(this.onConfigurationChanged, this)
);
if (model.repositories.length) {
this.ensureProviderRegistration();
}
}
dispose() {
this.providerDisposable?.dispose();
this.disposable.dispose();
}
async provideTimeline(uri: Uri, options: TimelineOptions, _token: CancellationToken): Promise<Timeline> {
// console.log(`GitTimelineProvider.provideTimeline: uri=${uri}`);
const repo = this.model.getRepository(uri);
if (!repo) {
this.repoDisposable?.dispose();
this.repoOperationDate = undefined;
this.repo = undefined;
return { items: [] };
}
if (this.repo?.root !== repo.root) {
this.repoDisposable?.dispose();
this.repo = repo;
this.repoOperationDate = new Date();
this.repoDisposable = Disposable.from(
repo.onDidChangeRepository(uri => this.onRepositoryChanged(repo, uri)),
repo.onDidRunGitStatus(() => this.onRepositoryStatusChanged(repo)),
repo.onDidRunOperation(result => this.onRepositoryOperationRun(repo, result))
);
}
const config = workspace.getConfiguration('git.timeline');
// TODO@eamodio: Ensure that the uri is a file -- if not we could get the history of the repo?
let limit: number | undefined;
if (options.limit !== undefined && typeof options.limit !== 'number') {
try {
const result = await this.model.git.exec(repo.root, ['rev-list', '--count', `${options.limit.id}..`, '--', uri.fsPath]);
if (!result.exitCode) {
// Ask for 2 more (1 for the limit commit and 1 for the next commit) than so we can determine if there are more commits
limit = Number(result.stdout) + 2;
}
}
catch {
limit = undefined;
}
} else {
// If we are not getting everything, ask for 1 more than so we can determine if there are more commits
limit = options.limit === undefined ? undefined : options.limit + 1;
}
await ensureEmojis();
const commits = await repo.logFile(uri, {
maxEntries: limit,
hash: options.cursor,
follow: true,
shortStats: true,
// sortByAuthorDate: true
});
const paging = commits.length ? {
cursor: limit === undefined ? undefined : (commits.length >= limit ? commits[commits.length - 1]?.hash : undefined)
} : undefined;
// If we asked for an extra commit, strip it off
if (limit !== undefined && commits.length >= limit) {
commits.splice(commits.length - 1, 1);
}
const dateFormatter = new Intl.DateTimeFormat(env.language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
const dateType = config.get<'committed' | 'authored'>('date');
const showAuthor = config.get<boolean>('showAuthor');
const showUncommitted = config.get<boolean>('showUncommitted');
const openComparison = l10n.t('Open Comparison');
const emptyTree = await repo.getEmptyTree();
const unpublishedCommits = await repo.getUnpublishedCommits();
const remoteHoverCommands = await provideSourceControlHistoryItemHoverCommands(this.model, repo);
const avatarQuery = {
commits: commits.map(c => ({
hash: c.hash,
authorName: c.authorName,
authorEmail: c.authorEmail
}) satisfies AvatarQueryCommit),
size: 20
} satisfies AvatarQuery;
const avatars = await provideSourceControlHistoryItemAvatar(this.model, repo, avatarQuery);
const items: GitTimelineItem[] = [];
for (let index = 0; index < commits.length; index++) {
const c = commits[index];
const date = dateType === 'authored' ? c.authorDate : c.commitDate;
const message = emojify(c.message);
const previousRef = commits[index + 1]?.hash ?? emptyTree;
const item = new GitTimelineItem(c.hash, previousRef, message, date?.getTime() ?? 0, c.hash, 'git:file:commit');
item.iconPath = new ThemeIcon('git-commit');
if (showAuthor) {
item.description = c.authorName;
}
const commitRemoteSourceCommands = !unpublishedCommits.has(c.hash) ? remoteHoverCommands : [];
const messageWithLinks = await provideSourceControlHistoryItemMessageLinks(this.model, repo, message) ?? message;
item.setItemDetails(uri, c.hash, avatars?.get(c.hash), c.authorName!, c.authorEmail, dateFormatter.format(date), messageWithLinks, c.shortStat, commitRemoteSourceCommands);
const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri);
if (cmd) {
item.command = {
title: openComparison,
command: cmd.command,
arguments: cmd.arguments,
};
}
items.push(item);
}
if (options.cursor === undefined) {
const you = l10n.t('You');
const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
if (index) {
const date = this.repoOperationDate ?? new Date();
const item = new GitTimelineItem('~', 'HEAD', l10n.t('Staged Changes'), date.getTime(), 'index', 'git:file:index');
// TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
item.iconPath = new ThemeIcon('git-commit');
item.description = '';
item.setItemDetails(uri, undefined, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(index.type));
const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri);
if (cmd) {
item.command = {
title: openComparison,
command: cmd.command,
arguments: cmd.arguments,
};
}
items.splice(0, 0, item);
}
if (showUncommitted) {
const working = repo.workingTreeGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
if (working) {
const date = new Date();
const item = new GitTimelineItem('', index ? '~' : 'HEAD', l10n.t('Uncommitted Changes'), date.getTime(), 'working', 'git:file:working');
item.iconPath = new ThemeIcon('circle-outline');
item.description = '';
item.setItemDetails(uri, undefined, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type));
const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri);
if (cmd) {
item.command = {
title: openComparison,
command: cmd.command,
arguments: cmd.arguments,
};
}
items.splice(0, 0, item);
}
}
}
return {
items: items,
paging: paging
};
}
private ensureProviderRegistration() {
if (this.providerDisposable === undefined) {
this.providerDisposable = workspace.registerTimelineProvider(['file', 'git', 'vscode-remote', 'vscode-local-history'], this);
}
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
if (e.affectsConfiguration('git.timeline.date') || e.affectsConfiguration('git.timeline.showAuthor') || e.affectsConfiguration('git.timeline.showUncommitted')) {
this.fireChanged();
}
}
private onRepositoriesChanged(_repo: Repository) {
// console.log(`GitTimelineProvider.onRepositoriesChanged`);
this.ensureProviderRegistration();
// TODO@eamodio: Being naive for now and just always refreshing each time there is a new repository
this.fireChanged();
}
private onRepositoryChanged(_repo: Repository, _uri: Uri) {
// console.log(`GitTimelineProvider.onRepositoryChanged: uri=${uri.toString(true)}`);
this.fireChanged();
}
private onRepositoryStatusChanged(_repo: Repository) {
// console.log(`GitTimelineProvider.onRepositoryStatusChanged`);
const config = workspace.getConfiguration('git.timeline');
const showUncommitted = config.get<boolean>('showUncommitted') === true;
if (showUncommitted) {
this.fireChanged();
}
}
private onRepositoryOperationRun(_repo: Repository, _result: OperationResult) {
// console.log(`GitTimelineProvider.onRepositoryOperationRun`);
// Successful operations that are not read-only and not status operations
if (!_result.error && !_result.operation.readOnly && _result.operation.kind !== OperationKind.Status) {
// This is less than ideal, but for now just save the last time an
// operation was run and use that as the timestamp for staged items
this.repoOperationDate = new Date();
this.fireChanged();
}
}
@debounce(500)
private fireChanged() {
this._onDidChange.fire(undefined);
}
}
+86
View File
@@ -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[]>;
}
+87
View File
@@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri } from 'vscode';
import { Change, Status } from './api/git';
export interface GitUriParams {
path: string;
ref: string;
submoduleOf?: string;
}
export function isGitUri(uri: Uri): boolean {
return /^git$/.test(uri.scheme);
}
export function fromGitUri(uri: Uri): GitUriParams {
return JSON.parse(uri.query);
}
export interface GitUriOptions {
scheme?: string;
replaceFileExtension?: boolean;
submoduleOf?: string;
}
// As a mitigation for extensions like ESLint showing warnings and errors
// for git URIs, let's change the file extension of these uris to .git,
// when `replaceFileExtension` is true.
export function toGitUri(uri: Uri, ref: string, options: GitUriOptions = {}): Uri {
const params: GitUriParams = {
path: uri.fsPath,
ref
};
if (options.submoduleOf) {
params.submoduleOf = options.submoduleOf;
}
let path = uri.path;
if (options.replaceFileExtension) {
path = `${path}.git`;
} else if (options.submoduleOf) {
path = `${path}.diff`;
}
return uri.with({ scheme: options.scheme ?? 'git', path, query: JSON.stringify(params) });
}
/**
* Assuming `uri` is being merged it creates uris for `base`, `ours`, and `theirs`
*/
export function toMergeUris(uri: Uri): { base: Uri; ours: Uri; theirs: Uri } {
return {
base: toGitUri(uri, ':1'),
ours: toGitUri(uri, ':2'),
theirs: toGitUri(uri, ':3'),
};
}
export function toMultiFileDiffEditorUris(change: Change, originalRef: string, modifiedRef: string): { originalUri: Uri | undefined; modifiedUri: Uri | undefined } {
switch (change.status) {
case Status.INDEX_ADDED:
return {
originalUri: undefined,
modifiedUri: toGitUri(change.uri, modifiedRef)
};
case Status.DELETED:
return {
originalUri: toGitUri(change.uri, originalRef),
modifiedUri: undefined
};
case Status.INDEX_RENAMED:
return {
originalUri: toGitUri(change.originalUri, originalRef),
modifiedUri: toGitUri(change.uri, modifiedRef)
};
default:
return {
originalUri: toGitUri(change.uri, originalRef),
modifiedUri: toGitUri(change.uri, modifiedRef)
};
}
}
+789
View File
@@ -0,0 +1,789 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n, workspace, Uri, DiagnosticSeverity, env } from 'vscode';
import { dirname, sep, relative } from 'path';
import { Readable } from 'stream';
import { promises as fs, createReadStream } from 'fs';
import byline from 'byline';
export const isMacintosh = process.platform === 'darwin';
export const isWindows = process.platform === 'win32';
export const isRemote = env.remoteName !== undefined;
export const isLinux = process.platform === 'linux';
export const isLinuxSnap = isLinux && !!process.env['SNAP'] && !!process.env['SNAP_REVISION'];
export function log(...args: any[]): void {
console.log.apply(console, ['git:', ...args]);
}
export interface IDisposable {
dispose(): void;
}
export function dispose<T extends IDisposable>(disposables: T[]): T[] {
disposables.forEach(d => d.dispose());
return [];
}
export function toDisposable(dispose: () => void): IDisposable {
return { dispose };
}
export function combinedDisposable(disposables: IDisposable[]): IDisposable {
return toDisposable(() => dispose(disposables));
}
export const EmptyDisposable = toDisposable(() => null);
export function fireEvent<T>(event: Event<T>): Event<T> {
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(_ => (listener as any).call(thisArgs), null, disposables);
}
export function mapEvent<I, O>(event: Event<I>, map: (i: I) => O): Event<O> {
return (listener: (e: O) => any, thisArgs?: any, disposables?: Disposable[]) => event(i => listener.call(thisArgs, map(i)), null, disposables);
}
export function filterEvent<T>(event: Event<T>, filter: (e: T) => boolean): Event<T> {
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
}
export function runAndSubscribeEvent<T>(event: Event<T>, handler: (e: T) => any, initial: T): IDisposable;
export function runAndSubscribeEvent<T>(event: Event<T>, handler: (e: T | undefined) => any): IDisposable;
export function runAndSubscribeEvent<T>(event: Event<T>, handler: (e: T | undefined) => any, initial?: T): IDisposable {
handler(initial);
return event(e => handler(e));
}
export function anyEvent<T>(...events: Event<T>[]): Event<T> {
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
const result = combinedDisposable(events.map(event => event(i => listener.call(thisArgs, i))));
disposables?.push(result);
return result;
};
}
export function done<T>(promise: Promise<T>): Promise<void> {
return promise.then<void>(() => undefined);
}
export function onceEvent<T>(event: Event<T>): Event<T> {
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
const result = event(e => {
result.dispose();
return listener.call(thisArgs, e);
}, null, disposables);
return result;
};
}
export function debounceEvent<T>(event: Event<T>, delay: number): Event<T> {
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
let timer: NodeJS.Timeout;
return event(e => {
clearTimeout(timer);
timer = setTimeout(() => listener.call(thisArgs, e), delay);
}, null, disposables);
};
}
export function eventToPromise<T>(event: Event<T>): Promise<T> {
return new Promise<T>(c => onceEvent(event)(c));
}
export function once(fn: (...args: any[]) => any): (...args: any[]) => any {
const didRun = false;
return (...args) => {
if (didRun) {
return;
}
return fn(...args);
};
}
export function assign<T>(destination: T, ...sources: any[]): T {
for (const source of sources) {
Object.keys(source).forEach(key => (destination as any)[key] = source[key]);
}
return destination;
}
export function uniqBy<T>(arr: T[], fn: (el: T) => string): T[] {
const seen = Object.create(null);
return arr.filter(el => {
const key = fn(el);
if (seen[key]) {
return false;
}
seen[key] = true;
return true;
});
}
export function groupBy<T>(arr: T[], fn: (el: T) => string): { [key: string]: T[] } {
return arr.reduce((result, el) => {
const key = fn(el);
result[key] = [...(result[key] || []), el];
return result;
}, Object.create(null));
}
export async function mkdirp(path: string, mode?: number): Promise<boolean> {
const mkdir = async () => {
try {
await fs.mkdir(path, mode);
} catch (err) {
if (err.code === 'EEXIST') {
const stat = await fs.stat(path);
if (stat.isDirectory()) {
return;
}
throw new Error(`'${path}' exists and is not a directory.`);
}
throw err;
}
};
// is root?
if (path === dirname(path)) {
return true;
}
try {
await mkdir();
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
await mkdirp(dirname(path), mode);
await mkdir();
}
return true;
}
export function uniqueFilter<T>(keyFn: (t: T) => string): (t: T) => boolean {
const seen: { [key: string]: boolean } = Object.create(null);
return element => {
const key = keyFn(element);
if (seen[key]) {
return false;
}
seen[key] = true;
return true;
};
}
export function find<T>(array: T[], fn: (t: T) => boolean): T | undefined {
let result: T | undefined = undefined;
array.some(e => {
if (fn(e)) {
result = e;
return true;
}
return false;
});
return result;
}
export async function grep(filename: string, pattern: RegExp): Promise<boolean> {
return new Promise<boolean>((c, e) => {
const fileStream = createReadStream(filename, { encoding: 'utf8' });
const stream = byline(fileStream);
stream.on('data', (line: string) => {
if (pattern.test(line)) {
fileStream.close();
c(true);
}
});
stream.on('error', e);
stream.on('end', () => c(false));
});
}
export function readBytes(stream: Readable, bytes: number): Promise<Buffer> {
return new Promise<Buffer>((complete, error) => {
let done = false;
const buffer = Buffer.allocUnsafe(bytes);
let bytesRead = 0;
stream.on('data', (data: Buffer) => {
const bytesToRead = Math.min(bytes - bytesRead, data.length);
data.copy(buffer, bytesRead, 0, bytesToRead);
bytesRead += bytesToRead;
if (bytesRead === bytes) {
(stream as any).destroy(); // Will trigger the close event eventually
}
});
stream.on('error', (e: Error) => {
if (!done) {
done = true;
error(e);
}
});
stream.on('close', () => {
if (!done) {
done = true;
complete(buffer.slice(0, bytesRead));
}
});
});
}
export const enum Encoding {
UTF8 = 'utf8',
UTF16be = 'utf16be',
UTF16le = 'utf16le'
}
export function detectUnicodeEncoding(buffer: Buffer): Encoding | null {
if (buffer.length < 2) {
return null;
}
const b0 = buffer.readUInt8(0);
const b1 = buffer.readUInt8(1);
if (b0 === 0xFE && b1 === 0xFF) {
return Encoding.UTF16be;
}
if (b0 === 0xFF && b1 === 0xFE) {
return Encoding.UTF16le;
}
if (buffer.length < 3) {
return null;
}
const b2 = buffer.readUInt8(2);
if (b0 === 0xEF && b1 === 0xBB && b2 === 0xBF) {
return Encoding.UTF8;
}
return null;
}
export function truncate(value: string, maxLength = 20): string {
return value.length <= maxLength ? value : `${value.substring(0, maxLength)}\u2026`;
}
function normalizePath(path: string): string {
// Windows & Mac are currently being handled
// as case insensitive file systems in VS Code.
if (isWindows || isMacintosh) {
return path.toLowerCase();
}
return path;
}
export function isDescendant(parent: string, descendant: string): boolean {
if (parent === descendant) {
return true;
}
if (parent.charAt(parent.length - 1) !== sep) {
parent += sep;
}
return normalizePath(descendant).startsWith(normalizePath(parent));
}
export function pathEquals(a: string, b: string): boolean {
return normalizePath(a) === normalizePath(b);
}
/**
* Given the `repository.root` compute the relative path while trying to preserve
* the casing of the resource URI. The `repository.root` segment of the path can
* have a casing mismatch if the folder/workspace is being opened with incorrect
* casing which is why we attempt to use substring() before relative().
*/
export function relativePath(from: string, to: string): string {
// There are cases in which the `from` path may contain a trailing separator at
// the end (ex: "C:\", "\\server\folder\" (Windows) or "/" (Linux/macOS)) which
// is by design as documented in https://github.com/nodejs/node/issues/1765. If
// the trailing separator is missing, we add it.
if (from.charAt(from.length - 1) !== sep) {
from += sep;
}
if (isDescendant(from, to) && from.length < to.length) {
return to.substring(from.length);
}
// Fallback to `path.relative`
return relative(from, to);
}
export function* splitInChunks(array: string[], maxChunkLength: number): IterableIterator<string[]> {
let current: string[] = [];
let length = 0;
for (const value of array) {
let newLength = length + value.length;
if (newLength > maxChunkLength && current.length > 0) {
yield current;
current = [];
newLength = value.length;
}
current.push(value);
length = newLength;
}
if (current.length > 0) {
yield current;
}
}
/**
* @returns whether the provided parameter is defined.
*/
export function isDefined<T>(arg: T | null | undefined): arg is T {
return !isUndefinedOrNull(arg);
}
/**
* @returns whether the provided parameter is undefined or null.
*/
export function isUndefinedOrNull(obj: unknown): obj is undefined | null {
return (isUndefined(obj) || obj === null);
}
/**
* @returns whether the provided parameter is undefined.
*/
export function isUndefined(obj: unknown): obj is undefined {
return (typeof obj === 'undefined');
}
interface ILimitedTaskFactory<T> {
factory: () => Promise<T>;
c: (value: T | Promise<T>) => void;
e: (error?: any) => void;
}
export class Limiter<T> {
private runningPromises: number;
private maxDegreeOfParalellism: number;
private outstandingPromises: ILimitedTaskFactory<T>[];
constructor(maxDegreeOfParalellism: number) {
this.maxDegreeOfParalellism = maxDegreeOfParalellism;
this.outstandingPromises = [];
this.runningPromises = 0;
}
queue(factory: () => Promise<T>): Promise<T> {
return new Promise<T>((c, e) => {
this.outstandingPromises.push({ factory, c, e });
this.consume();
});
}
private consume(): void {
while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
const iLimitedTask = this.outstandingPromises.shift()!;
this.runningPromises++;
const promise = iLimitedTask.factory();
promise.then(iLimitedTask.c, iLimitedTask.e);
promise.then(() => this.consumed(), () => this.consumed());
}
}
private consumed(): void {
this.runningPromises--;
if (this.outstandingPromises.length > 0) {
this.consume();
}
}
}
type Completion<T> = { success: true; value: T } | { success: false; err: any };
export class PromiseSource<T> {
private _onDidComplete = new EventEmitter<Completion<T>>();
private _promise: Promise<T> | undefined;
get promise(): Promise<T> {
if (this._promise) {
return this._promise;
}
return eventToPromise(this._onDidComplete.event).then(completion => {
if (completion.success) {
return completion.value;
} else {
throw completion.err;
}
});
}
resolve(value: T): void {
if (!this._promise) {
this._promise = Promise.resolve(value);
this._onDidComplete.fire({ success: true, value });
}
}
reject(err: any): void {
if (!this._promise) {
this._promise = Promise.reject(err);
this._onDidComplete.fire({ success: false, err });
}
}
}
export namespace Versions {
declare type VersionComparisonResult = -1 | 0 | 1;
export interface Version {
major: number;
minor: number;
patch: number;
pre?: string;
}
export function compare(v1: string | Version, v2: string | Version): VersionComparisonResult {
if (typeof v1 === 'string') {
v1 = fromString(v1);
}
if (typeof v2 === 'string') {
v2 = fromString(v2);
}
if (v1.major > v2.major) { return 1; }
if (v1.major < v2.major) { return -1; }
if (v1.minor > v2.minor) { return 1; }
if (v1.minor < v2.minor) { return -1; }
if (v1.patch > v2.patch) { return 1; }
if (v1.patch < v2.patch) { return -1; }
if (v1.pre === undefined && v2.pre !== undefined) { return 1; }
if (v1.pre !== undefined && v2.pre === undefined) { return -1; }
if (v1.pre !== undefined && v2.pre !== undefined) {
return v1.pre.localeCompare(v2.pre) as VersionComparisonResult;
}
return 0;
}
export function from(major: string | number, minor: string | number, patch?: string | number, pre?: string): Version {
return {
major: typeof major === 'string' ? parseInt(major, 10) : major,
minor: typeof minor === 'string' ? parseInt(minor, 10) : minor,
patch: patch === undefined || patch === null ? 0 : typeof patch === 'string' ? parseInt(patch, 10) : patch,
pre: pre,
};
}
export function fromString(version: string): Version {
const [ver, pre] = version.split('-');
const [major, minor, patch] = ver.split('.');
return from(major, minor, patch, pre);
}
}
export function deltaHistoryItemRefs(before: SourceControlHistoryItemRef[], after: SourceControlHistoryItemRef[]): {
added: SourceControlHistoryItemRef[];
modified: SourceControlHistoryItemRef[];
removed: SourceControlHistoryItemRef[];
} {
if (before.length === 0) {
return { added: after, modified: [], removed: [] };
}
const added: SourceControlHistoryItemRef[] = [];
const modified: SourceControlHistoryItemRef[] = [];
const removed: SourceControlHistoryItemRef[] = [];
let beforeIdx = 0;
let afterIdx = 0;
while (true) {
if (beforeIdx === before.length) {
added.push(...after.slice(afterIdx));
break;
}
if (afterIdx === after.length) {
removed.push(...before.slice(beforeIdx));
break;
}
const beforeElement = before[beforeIdx];
const afterElement = after[afterIdx];
const result = beforeElement.id.localeCompare(afterElement.id);
if (result === 0) {
if (beforeElement.revision !== afterElement.revision) {
// modified
modified.push(afterElement);
}
beforeIdx += 1;
afterIdx += 1;
} else if (result < 0) {
// beforeElement is smaller -> before element removed
removed.push(beforeElement);
beforeIdx += 1;
} else if (result > 0) {
// beforeElement is greater -> after element added
added.push(afterElement);
afterIdx += 1;
}
}
return { added, modified, removed };
}
const minute = 60;
const hour = minute * 60;
const day = hour * 24;
const week = day * 7;
const month = day * 30;
const year = day * 365;
/**
* Create a l10n.td difference of the time between now and the specified date.
* @param date The date to generate the difference from.
* @param appendAgoLabel Whether to append the " ago" to the end.
* @param useFullTimeWords Whether to use full words (eg. seconds) instead of
* shortened (eg. secs).
* @param disallowNow Whether to disallow the string "now" when the difference
* is less than 30 seconds.
*/
export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTimeWords?: boolean, disallowNow?: boolean): string {
if (typeof date !== 'number') {
date = date.getTime();
}
const seconds = Math.round((new Date().getTime() - date) / 1000);
if (seconds < -30) {
return l10n.t('in {0}', fromNow(new Date().getTime() + seconds * 1000, false));
}
if (!disallowNow && seconds < 30) {
return l10n.t('now');
}
let value: number;
if (seconds < minute) {
value = seconds;
if (appendAgoLabel) {
if (value === 1) {
return useFullTimeWords
? l10n.t('{0} second ago', value)
: l10n.t('{0} sec ago', value);
} else {
return useFullTimeWords
? l10n.t('{0} seconds ago', value)
: l10n.t('{0} secs ago', value);
}
} else {
if (value === 1) {
return useFullTimeWords
? l10n.t('{0} second', value)
: l10n.t('{0} sec', value);
} else {
return useFullTimeWords
? l10n.t('{0} seconds', value)
: l10n.t('{0} secs', value);
}
}
}
if (seconds < hour) {
value = Math.floor(seconds / minute);
if (appendAgoLabel) {
if (value === 1) {
return useFullTimeWords
? l10n.t('{0} minute ago', value)
: l10n.t('{0} min ago', value);
} else {
return useFullTimeWords
? l10n.t('{0} minutes ago', value)
: l10n.t('{0} mins ago', value);
}
} else {
if (value === 1) {
return useFullTimeWords
? l10n.t('{0} minute', value)
: l10n.t('{0} min', value);
} else {
return useFullTimeWords
? l10n.t('{0} minutes', value)
: l10n.t('{0} mins', value);
}
}
}
if (seconds < day) {
value = Math.floor(seconds / hour);
if (appendAgoLabel) {
if (value === 1) {
return useFullTimeWords
? l10n.t('{0} hour ago', value)
: l10n.t('{0} hr ago', value);
} else {
return useFullTimeWords
? l10n.t('{0} hours ago', value)
: l10n.t('{0} hrs ago', value);
}
} else {
if (value === 1) {
return useFullTimeWords
? l10n.t('{0} hour', value)
: l10n.t('{0} hr', value);
} else {
return useFullTimeWords
? l10n.t('{0} hours', value)
: l10n.t('{0} hrs', value);
}
}
}
if (seconds < week) {
value = Math.floor(seconds / day);
if (appendAgoLabel) {
return value === 1
? l10n.t('{0} day ago', value)
: l10n.t('{0} days ago', value);
} else {
return value === 1
? l10n.t('{0} day', value)
: l10n.t('{0} days', value);
}
}
if (seconds < month) {
value = Math.floor(seconds / week);
if (appendAgoLabel) {
if (value === 1) {
return useFullTimeWords
? l10n.t('{0} week ago', value)
: l10n.t('{0} wk ago', value);
} else {
return useFullTimeWords
? l10n.t('{0} weeks ago', value)
: l10n.t('{0} wks ago', value);
}
} else {
if (value === 1) {
return useFullTimeWords
? l10n.t('{0} week', value)
: l10n.t('{0} wk', value);
} else {
return useFullTimeWords
? l10n.t('{0} weeks', value)
: l10n.t('{0} wks', value);
}
}
}
if (seconds < year) {
value = Math.floor(seconds / month);
if (appendAgoLabel) {
if (value === 1) {
return useFullTimeWords
? l10n.t('{0} month ago', value)
: l10n.t('{0} mo ago', value);
} else {
return useFullTimeWords
? l10n.t('{0} months ago', value)
: l10n.t('{0} mos ago', value);
}
} else {
if (value === 1) {
return useFullTimeWords
? l10n.t('{0} month', value)
: l10n.t('{0} mo', value);
} else {
return useFullTimeWords
? l10n.t('{0} months', value)
: l10n.t('{0} mos', value);
}
}
}
value = Math.floor(seconds / year);
if (appendAgoLabel) {
if (value === 1) {
return useFullTimeWords
? l10n.t('{0} year ago', value)
: l10n.t('{0} yr ago', value);
} else {
return useFullTimeWords
? l10n.t('{0} years ago', value)
: l10n.t('{0} yrs ago', value);
}
} else {
if (value === 1) {
return useFullTimeWords
? l10n.t('{0} year', value)
: l10n.t('{0} yr', value);
} else {
return useFullTimeWords
? l10n.t('{0} years', value)
: l10n.t('{0} yrs', value);
}
}
}
export function getCommitShortHash(scope: Uri, hash: string): string {
const config = workspace.getConfiguration('git', scope);
const shortHashLength = config.get<number>('commitShortHashLength', 7);
return hash.substring(0, shortHashLength);
}
export type DiagnosticSeverityConfig = 'error' | 'warning' | 'information' | 'hint' | 'none';
export function toDiagnosticSeverity(value: DiagnosticSeverityConfig): DiagnosticSeverity {
return value === 'error'
? DiagnosticSeverity.Error
: value === 'warning'
? DiagnosticSeverity.Warning
: value === 'information'
? DiagnosticSeverity.Information
: DiagnosticSeverity.Hint;
}
+22
View File
@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, RelativePattern, Uri, workspace } from 'vscode';
import { IDisposable, anyEvent } from './util';
export interface IFileWatcher extends IDisposable {
readonly event: Event<Uri>;
}
export function watch(location: string): IFileWatcher {
const watcher = workspace.createFileSystemWatcher(new RelativePattern(location, '*'));
return new class implements IFileWatcher {
event = anyEvent(watcher.onDidCreate, watcher.onDidChange, watcher.onDidDelete);
dispose() {
watcher.dispose();
}
};
}
+32
View File
@@ -0,0 +1,32 @@
{
"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.editSessionIdentityProvider.d.ts",
"../../src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts",
"../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts",
"../../src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts",
"../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts",
"../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts",
"../../src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts",
"../../src/vscode-dts/vscode.proposed.scmValidation.d.ts",
"../../src/vscode-dts/vscode.proposed.scmMultiDiffEditor.d.ts",
"../../src/vscode-dts/vscode.proposed.scmTextDocument.d.ts",
"../../src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts",
"../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts",
"../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts",
"../../src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts",
"../../src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts",
"../../src/vscode-dts/vscode.proposed.timeline.d.ts",
"../types/lib.textEncoder.d.ts"
]
}