chore: import upstream snapshot with attribution
@@ -0,0 +1,2 @@
|
||||
legacy-peer-deps="true"
|
||||
timeout=180000
|
||||
@@ -0,0 +1,7 @@
|
||||
src/**
|
||||
test/**
|
||||
out/**
|
||||
tsconfig.json
|
||||
build/**
|
||||
extension.webpack.config.js
|
||||
package-lock.json
|
||||
@@ -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"
|
||||
]
|
||||
```
|
||||
@@ -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'
|
||||
}
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)."
|
||||
}
|
||||
@@ -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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" fill="white">
|
||||
U
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 431 B |
|
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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" 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, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback";" fill="white">
|
||||
U
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 431 B |
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo ''
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 ? `` : '$(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(`, <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(`, <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(' ');
|
||||
markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`);
|
||||
|
||||
// Remote hover commands
|
||||
if (remoteHoverCommands.length > 0) {
|
||||
markdownString.appendMarkdown(' | ');
|
||||
|
||||
const remoteCommandsMarkdown = remoteHoverCommands
|
||||
.map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify([...command.arguments ?? [], hash]))} "${command.tooltip}")`);
|
||||
markdownString.appendMarkdown(remoteCommandsMarkdown.join(' '));
|
||||
}
|
||||
|
||||
markdownString.appendMarkdown(' | ');
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
#!/bin/sh
|
||||
@@ -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);
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
ELECTRON_RUN_AS_NODE="1" \
|
||||
"$VSCODE_GIT_EDITOR_NODE" "$VSCODE_GIT_EDITOR_MAIN" $VSCODE_GIT_EDITOR_EXTRA_ARGS "$@"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo ''
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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']]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
? ``
|
||||
: '$(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(' ');
|
||||
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(' | ');
|
||||
|
||||
const remoteCommandsMarkdown = remoteSourceCommands
|
||||
.map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify([...command.arguments ?? [], hash]))} "${command.tooltip}")`);
|
||||
this.tooltip.appendMarkdown(remoteCommandsMarkdown.join(' '));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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[]>;
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||