Merge from vscode a5cf1da01d5db3d2557132be8d30f89c38019f6c (#8525)

* Merge from vscode a5cf1da01d5db3d2557132be8d30f89c38019f6c

* remove files we don't want

* fix hygiene

* update distro

* update distro

* fix hygiene

* fix strict nulls

* distro

* distro

* fix tests

* fix tests

* add another edit

* fix viewlet icon

* fix azure dialog

* fix some padding

* fix more padding issues
This commit is contained in:
Anthony Dresser
2019-12-04 19:28:22 -08:00
committed by GitHub
parent a8818ab0df
commit f5ce7fb2a5
1507 changed files with 42813 additions and 27370 deletions

View File

@@ -8,6 +8,7 @@ import { Repository as BaseRepository, Resource } from '../repository';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState } from './git';
import { Event, SourceControlInputBox, Uri, SourceControl } from 'vscode';
import { mapEvent } from '../util';
import { toGitUri } from '../uri';
class ApiInputBox implements InputBox {
set value(value: string) { this._inputBox.value = value; }
@@ -234,5 +235,9 @@ export class ApiImpl implements API {
return this._model.repositories.map(r => new ApiRepository(r));
}
toGitUri(uri: Uri, ref: string): Uri {
return toGitUri(uri, ref);
}
constructor(private _model: Model) { }
}

View File

@@ -185,6 +185,8 @@ export interface API {
readonly repositories: Repository[];
readonly onDidOpenRepository: Event<Repository>;
readonly onDidCloseRepository: Event<Repository>;
toGitUri(uri: Uri, ref: string): Uri;
}
export interface GitExtension {

View File

@@ -3,9 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as http from 'http';
import * as fs from 'fs';
import * as nls from 'vscode-nls';
import { IPCClient } from './ipc/ipcClient';
const localize = nls.loadMessageBundle();
@@ -20,10 +20,6 @@ function main(argv: string[]): void {
return fatal('Wrong number of arguments');
}
if (!process.env['VSCODE_GIT_ASKPASS_HANDLE']) {
return fatal('Missing handle');
}
if (!process.env['VSCODE_GIT_ASKPASS_PIPE']) {
return fatal('Missing pipe');
}
@@ -33,40 +29,14 @@ function main(argv: string[]): void {
}
const output = process.env['VSCODE_GIT_ASKPASS_PIPE'] as string;
const socketPath = process.env['VSCODE_GIT_ASKPASS_HANDLE'] as string;
const request = argv[2];
const host = argv[4].substring(1, argv[4].length - 2);
const opts: http.RequestOptions = {
socketPath,
path: '/',
method: 'POST'
};
const ipcClient = new IPCClient('askpass');
const req = http.request(opts, res => {
if (res.statusCode !== 200) {
return fatal(`Bad status code: ${res.statusCode}`);
}
const chunks: string[] = [];
res.setEncoding('utf8');
res.on('data', (d: string) => chunks.push(d));
res.on('end', () => {
const raw = chunks.join('');
try {
const result = JSON.parse(raw);
fs.writeFileSync(output, result + '\n');
} catch (err) {
return fatal(`Error parsing response`);
}
setTimeout(() => process.exit(0), 0);
});
});
req.on('error', () => fatal('Error in request'));
req.write(JSON.stringify({ request, host }));
req.end();
ipcClient.call({ request, host }).then(res => {
fs.writeFileSync(output, res + '\n');
setTimeout(() => process.exit(0), 0);
}).catch(err => fatal(err));
}
main(process.argv);

View File

@@ -3,15 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, window, InputBoxOptions } from 'vscode';
import { denodeify } from './util';
import { window, InputBoxOptions } from 'vscode';
import { IDisposable } 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';
const randomBytes = denodeify<Buffer>(crypto.randomBytes);
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
export interface AskpassEnvironment {
GIT_ASKPASS: string;
@@ -21,68 +16,21 @@ export interface AskpassEnvironment {
VSCODE_GIT_ASKPASS_HANDLE?: string;
}
function getIPCHandlePath(nonce: string): string {
if (process.platform === 'win32') {
return `\\\\.\\pipe\\vscode-git-askpass-${nonce}-sock`;
export class Askpass implements IIPCHandler {
private disposable: IDisposable;
static getDisabledEnv(): AskpassEnvironment {
return {
GIT_ASKPASS: path.join(__dirname, 'askpass-empty.sh')
};
}
if (process.env['XDG_RUNTIME_DIR']) {
return path.join(process.env['XDG_RUNTIME_DIR'] as string, `vscode-git-askpass-${nonce}.sock`);
constructor(ipc: IIPCServer) {
this.disposable = ipc.registerHandler('askpass', this);
}
return path.join(os.tmpdir(), `vscode-git-askpass-${nonce}.sock`);
}
export class Askpass implements Disposable {
private server: http.Server;
private ipcHandlePathPromise: Promise<string>;
private ipcHandlePath: string | undefined;
private enabled = true;
constructor() {
this.server = http.createServer((req, res) => this.onRequest(req, res));
this.ipcHandlePathPromise = this.setup().catch(err => {
console.error(err);
return '';
});
}
private async setup(): Promise<string> {
const buffer = await randomBytes(20);
const nonce = buffer.toString('hex');
const ipcHandlePath = getIPCHandlePath(nonce);
this.ipcHandlePath = ipcHandlePath;
try {
this.server.listen(ipcHandlePath);
this.server.on('error', err => console.error(err));
} catch (err) {
console.error('Could not launch git askpass helper.');
this.enabled = false;
}
return ipcHandlePath;
}
private onRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
const chunks: string[] = [];
req.setEncoding('utf8');
req.on('data', (d: string) => chunks.push(d));
req.on('end', () => {
const { request, host } = JSON.parse(chunks.join(''));
this.prompt(host, request).then(result => {
res.writeHead(200);
res.end(JSON.stringify(result));
}, () => {
res.writeHead(500);
res.end();
});
});
}
private async prompt(host: string, request: string): Promise<string> {
async handle({ request, host }: { request: string, host: string }): Promise<string> {
const options: InputBoxOptions = {
password: /password/i.test(request),
placeHolder: request,
@@ -93,27 +41,16 @@ export class Askpass implements Disposable {
return await window.showInputBox(options) || '';
}
async getEnv(): Promise<AskpassEnvironment> {
if (!this.enabled) {
return {
GIT_ASKPASS: path.join(__dirname, 'askpass-empty.sh')
};
}
getEnv(): AskpassEnvironment {
return {
ELECTRON_RUN_AS_NODE: '1',
GIT_ASKPASS: path.join(__dirname, 'askpass.sh'),
VSCODE_GIT_ASKPASS_NODE: process.execPath,
VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js'),
VSCODE_GIT_ASKPASS_HANDLE: await this.ipcHandlePathPromise
VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js')
};
}
dispose(): void {
this.server.close();
if (this.ipcHandlePath && process.platform !== 'win32') {
fs.unlinkSync(this.ipcHandlePath);
}
this.disposable.dispose();
}
}
}

View File

@@ -3,19 +3,19 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor, MessageOptions, WorkspaceFolder } from 'vscode';
import { Git, CommitOptions, Stash, ForcePushMode } from './git';
import { Repository, Resource, ResourceGroupType } from './repository';
import { Model } from './model';
import { toGitUri, fromGitUri } from './uri';
import { grep, isDescendant, pathEquals } from './util';
import { applyLineChanges, intersectDiffWithRange, toLineRanges, invertLineChange, getModifiedRange } from './staging';
import * as path from 'path';
import { lstat, Stats } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder } from 'vscode';
import TelemetryReporter from 'vscode-extension-telemetry';
import * as nls from 'vscode-nls';
import { Ref, RefType, Branch, GitErrorCodes, Status } from './api/git';
import { Branch, GitErrorCodes, Ref, RefType, Status } from './api/git';
import { CommitOptions, ForcePushMode, Git, Stash } from './git';
import { Model } from './model';
import { Repository, Resource, ResourceGroupType } from './repository';
import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging';
import { fromGitUri, toGitUri, isGitUri } from './uri';
import { grep, isDescendant, pathEquals } from './util';
const localize = nls.loadMessageBundle();
@@ -99,7 +99,7 @@ class CreateBranchItem implements QuickPickItem {
constructor(private cc: CommandCenter) { }
get label(): string { return localize('create branch', '$(plus) Create new branch...'); }
get label(): string { return '$(plus) ' + localize('create branch', 'Create new branch...'); }
get description(): string { return ''; }
get alwaysShow(): boolean { return true; }
@@ -113,7 +113,7 @@ class CreateBranchFromItem implements QuickPickItem {
constructor(private cc: CommandCenter) { }
get label(): string { return localize('create branch from', '$(plus) Create new branch from...'); }
get label(): string { return '$(plus) ' + localize('create branch from', 'Create new branch from...'); }
get description(): string { return ''; }
get alwaysShow(): boolean { return true; }
@@ -136,7 +136,7 @@ class AddRemoteItem implements QuickPickItem {
constructor(private cc: CommandCenter) { }
get label(): string { return localize('add remote', '$(plus) Add a new remote...'); }
get label(): string { return '$(plus) ' + localize('add remote', 'Add a new remote...'); }
get description(): string { return ''; }
get alwaysShow(): boolean { return true; }
@@ -170,14 +170,14 @@ function command(commandId: string, options: CommandOptions = {}): Function {
};
}
const ImageMimetypes = [
'image/png',
'image/gif',
'image/jpeg',
'image/webp',
'image/tiff',
'image/bmp'
];
// const ImageMimetypes = [
// 'image/png',
// 'image/gif',
// 'image/jpeg',
// 'image/webp',
// 'image/tiff',
// 'image/bmp'
// ];
async function categorizeResourceByResolution(resources: Resource[]): Promise<{ merge: Resource[], resolved: Resource[], unresolved: Resource[], deletionConflicts: Resource[] }> {
const selection = resources.filter(s => s instanceof Resource) as Resource[];
@@ -213,6 +213,12 @@ function createCheckoutItems(repository: Repository): CheckoutItem[] {
return [...heads, ...tags, ...remoteHeads];
}
class TagItem implements QuickPickItem {
get label(): string { return this.ref.name ?? ''; }
get description(): string { return this.ref.commit?.substr(0, 8) ?? ''; }
constructor(readonly ref: Ref) { }
}
enum PushType {
Push,
PushTo,
@@ -289,10 +295,10 @@ export class CommandCenter {
}
} else {
if (resource.type !== Status.DELETED_BY_THEM) {
left = await this.getLeftResource(resource);
left = this.getLeftResource(resource);
}
right = await this.getRightResource(resource);
right = this.getRightResource(resource);
}
const title = this.getTitle(resource);
@@ -324,79 +330,40 @@ export class CommandCenter {
}
}
private async getURI(uri: Uri, ref: string): Promise<Uri | undefined> {
const repository = this.model.getRepository(uri);
if (!repository) {
return toGitUri(uri, ref);
}
try {
let gitRef = ref;
if (gitRef === '~') {
const uriString = uri.toString();
const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
gitRef = indexStatus ? '' : 'HEAD';
}
const { size, object } = await repository.getObjectDetails(gitRef, uri.fsPath);
const { mimetype } = await repository.detectObjectType(object);
if (mimetype === 'text/plain') {
return toGitUri(uri, ref);
}
if (size > 1000000) { // 1 MB
return Uri.parse(`data:;label:${path.basename(uri.fsPath)};description:${gitRef},`);
}
if (ImageMimetypes.indexOf(mimetype) > -1) {
const contents = await repository.buffer(gitRef, uri.fsPath);
return Uri.parse(`data:${mimetype};label:${path.basename(uri.fsPath)};description:${gitRef};size:${size};base64,${contents.toString('base64')}`);
}
return Uri.parse(`data:;label:${path.basename(uri.fsPath)};description:${gitRef},`);
} catch (err) {
return toGitUri(uri, ref);
}
}
private async getLeftResource(resource: Resource): Promise<Uri | undefined> {
private getLeftResource(resource: Resource): Uri | undefined {
switch (resource.type) {
case Status.INDEX_MODIFIED:
case Status.INDEX_RENAMED:
case Status.INDEX_ADDED:
return this.getURI(resource.original, 'HEAD');
return toGitUri(resource.original, 'HEAD');
case Status.MODIFIED:
case Status.UNTRACKED:
return this.getURI(resource.resourceUri, '~');
return toGitUri(resource.resourceUri, '~');
case Status.DELETED_BY_THEM:
return this.getURI(resource.resourceUri, '');
return toGitUri(resource.resourceUri, '');
}
return undefined;
}
private async getRightResource(resource: Resource): Promise<Uri | undefined> {
private getRightResource(resource: Resource): Uri | undefined {
switch (resource.type) {
case Status.INDEX_MODIFIED:
case Status.INDEX_ADDED:
case Status.INDEX_COPIED:
case Status.INDEX_RENAMED:
return this.getURI(resource.resourceUri, '');
return toGitUri(resource.resourceUri, '');
case Status.INDEX_DELETED:
case Status.DELETED:
return this.getURI(resource.resourceUri, 'HEAD');
return toGitUri(resource.resourceUri, 'HEAD');
case Status.DELETED_BY_US:
return this.getURI(resource.resourceUri, '~3');
return toGitUri(resource.resourceUri, '~3');
case Status.DELETED_BY_THEM:
return this.getURI(resource.resourceUri, '~2');
return toGitUri(resource.resourceUri, '~2');
case Status.MODIFIED:
case Status.UNTRACKED:
@@ -453,7 +420,7 @@ export class CommandCenter {
}
@command('git.clone')
async clone(url?: string): Promise<void> {
async clone(url?: string, parentPath?: string): Promise<void> {
if (!url) {
url = await window.showInputBox({
prompt: localize('repourl', "Repository URL"),
@@ -473,31 +440,33 @@ export class CommandCenter {
url = url.trim().replace(/^git\s+clone\s+/, '');
const config = workspace.getConfiguration('git');
let defaultCloneDirectory = config.get<string>('defaultCloneDirectory') || os.homedir();
defaultCloneDirectory = defaultCloneDirectory.replace(/^~/, os.homedir());
if (!parentPath) {
const config = workspace.getConfiguration('git');
let defaultCloneDirectory = config.get<string>('defaultCloneDirectory') || os.homedir();
defaultCloneDirectory = defaultCloneDirectory.replace(/^~/, os.homedir());
const uris = await window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
defaultUri: Uri.file(defaultCloneDirectory),
openLabel: localize('selectFolder', "Select Repository Location")
});
const uris = await window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
defaultUri: Uri.file(defaultCloneDirectory),
openLabel: localize('selectFolder', "Select Repository Location")
});
if (!uris || uris.length === 0) {
/* __GDPR__
"clone" : {
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
return;
if (!uris || uris.length === 0) {
/* __GDPR__
"clone" : {
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
return;
}
const uri = uris[0];
parentPath = uri.fsPath;
}
const uri = uris[0];
const parentPath = uri.fsPath;
try {
const opts = {
location: ProgressLocation.Notification,
@@ -507,7 +476,7 @@ export class CommandCenter {
const repositoryPath = await window.withProgress(
opts,
(progress, token) => this.git.clone(url!, parentPath, progress, token)
(progress, token) => this.git.clone(url!, parentPath!, progress, token)
);
let message = localize('proposeopen', "Would you like to open the cloned repository?");
@@ -686,7 +655,7 @@ export class CommandCenter {
let uris: Uri[] | undefined;
if (arg instanceof Uri) {
if (arg.scheme === 'git') {
if (isGitUri(arg)) {
uris = [Uri.file(fromGitUri(arg).path)];
} else if (arg.scheme === 'file') {
uris = [arg];
@@ -765,7 +734,7 @@ export class CommandCenter {
return;
}
const HEAD = await this.getLeftResource(resource);
const HEAD = this.getLeftResource(resource);
const basename = path.basename(resource.resourceUri.fsPath);
const title = `${basename} (HEAD)`;
@@ -866,7 +835,8 @@ export class CommandCenter {
}
const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree);
const scmResources = [...workingTree, ...resolved, ...unresolved];
const untracked = selection.filter(s => s.resourceGroupType === ResourceGroupType.Untracked);
const scmResources = [...workingTree, ...untracked, ...resolved, ...unresolved];
this.outputChannel.appendLine(`git.stage.scmResources ${scmResources.length}`);
if (!scmResources.length) {
@@ -907,7 +877,9 @@ export class CommandCenter {
}
}
await repository.add([]);
const config = workspace.getConfiguration('git', Uri.file(repository.root));
const untrackedChanges = config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
await repository.add([], untrackedChanges === 'mixed' ? undefined : { update: true });
}
private async _stageDeletionConflict(repository: Repository, uri: Uri): Promise<void> {
@@ -945,6 +917,24 @@ export class CommandCenter {
}
}
@command('git.stageAllTracked', { repository: true })
async stageAllTracked(repository: Repository): Promise<void> {
const resources = repository.workingTreeGroup.resourceStates
.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
const uris = resources.map(r => r.resourceUri);
await repository.add(uris);
}
@command('git.stageAllUntracked', { repository: true })
async stageAllUntracked(repository: Repository): Promise<void> {
const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates]
.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);
const uris = resources.map(r => r.resourceUri);
await repository.add(uris);
}
@command('git.stageChange')
async stageChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];
@@ -1090,7 +1080,7 @@ export class CommandCenter {
const modifiedDocument = textEditor.document;
const modifiedUri = modifiedDocument.uri;
if (modifiedUri.scheme !== 'git') {
if (!isGitUri(modifiedUri)) {
return;
}
@@ -1131,8 +1121,8 @@ export class CommandCenter {
resourceStates = [resource];
}
const scmResources = resourceStates
.filter(s => s instanceof Resource && s.resourceGroupType === ResourceGroupType.WorkingTree) as Resource[];
const scmResources = resourceStates.filter(s => s instanceof Resource
&& (s.resourceGroupType === ResourceGroupType.WorkingTree || s.resourceGroupType === ResourceGroupType.Untracked)) as Resource[];
if (!scmResources.length) {
return;
@@ -1189,41 +1179,11 @@ export class CommandCenter {
const untrackedResources = resources.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);
if (untrackedResources.length === 0) {
const message = resources.length === 1
? localize('confirm discard all single', "Are you sure you want to discard changes in {0}?", path.basename(resources[0].resourceUri.fsPath))
: localize('confirm discard all', "Are you sure you want to discard ALL changes in {0} files?\nThis is IRREVERSIBLE!\nYour current working set will be FOREVER LOST.", resources.length);
const yes = resources.length === 1
? localize('discardAll multiple', "Discard 1 File")
: localize('discardAll', "Discard All {0} Files", resources.length);
const pick = await window.showWarningMessage(message, { modal: true }, yes);
if (pick !== yes) {
return;
}
await repository.clean(resources.map(r => r.resourceUri));
return;
await this._cleanTrackedChanges(repository, resources);
} else if (resources.length === 1) {
const message = localize('confirm delete', "Are you sure you want to DELETE {0}?\nThis is IRREVERSIBLE!\nThis file will be FOREVER LOST.", path.basename(resources[0].resourceUri.fsPath));
const yes = localize('delete file', "Delete file");
const pick = await window.showWarningMessage(message, { modal: true }, yes);
if (pick !== yes) {
return;
}
await repository.clean(resources.map(r => r.resourceUri));
await this._cleanUntrackedChange(repository, resources[0]);
} else if (trackedResources.length === 0) {
const message = localize('confirm delete multiple', "Are you sure you want to DELETE {0} files?", resources.length);
const yes = localize('delete files', "Delete Files");
const pick = await window.showWarningMessage(message, { modal: true }, yes);
if (pick !== yes) {
return;
}
await repository.clean(resources.map(r => r.resourceUri));
await this._cleanUntrackedChanges(repository, resources);
} else { // resources.length > 1 && untrackedResources.length > 0 && trackedResources.length > 0
const untrackedMessage = untrackedResources.length === 1
? localize('there are untracked files single', "The following untracked file will be DELETED FROM DISK if discarded: {0}.", path.basename(untrackedResources[0].resourceUri.fsPath))
@@ -1248,6 +1208,74 @@ export class CommandCenter {
}
}
@command('git.cleanAllTracked', { repository: true })
async cleanAllTracked(repository: Repository): Promise<void> {
const resources = repository.workingTreeGroup.resourceStates
.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
if (resources.length === 0) {
return;
}
await this._cleanTrackedChanges(repository, resources);
}
@command('git.cleanAllUntracked', { repository: true })
async cleanAllUntracked(repository: Repository): Promise<void> {
const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates]
.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);
if (resources.length === 0) {
return;
}
if (resources.length === 1) {
await this._cleanUntrackedChange(repository, resources[0]);
} else {
await this._cleanUntrackedChanges(repository, resources);
}
}
private async _cleanTrackedChanges(repository: Repository, resources: Resource[]): Promise<void> {
const message = resources.length === 1
? localize('confirm discard all single', "Are you sure you want to discard changes in {0}?", path.basename(resources[0].resourceUri.fsPath))
: localize('confirm discard all', "Are you sure you want to discard ALL changes in {0} files?\nThis is IRREVERSIBLE!\nYour current working set will be FOREVER LOST.", resources.length);
const yes = resources.length === 1
? localize('discardAll multiple', "Discard 1 File")
: localize('discardAll', "Discard All {0} Files", resources.length);
const pick = await window.showWarningMessage(message, { modal: true }, yes);
if (pick !== yes) {
return;
}
await repository.clean(resources.map(r => r.resourceUri));
}
private async _cleanUntrackedChange(repository: Repository, resource: Resource): Promise<void> {
const message = localize('confirm delete', "Are you sure you want to DELETE {0}?\nThis is IRREVERSIBLE!\nThis file will be FOREVER LOST.", path.basename(resource.resourceUri.fsPath));
const yes = localize('delete file', "Delete file");
const pick = await window.showWarningMessage(message, { modal: true }, yes);
if (pick !== yes) {
return;
}
await repository.clean([resource.resourceUri]);
}
private async _cleanUntrackedChanges(repository: Repository, resources: Resource[]): Promise<void> {
const message = localize('confirm delete multiple', "Are you sure you want to DELETE {0} files?", resources.length);
const yes = localize('delete files', "Delete Files");
const pick = await window.showWarningMessage(message, { modal: true }, yes);
if (pick !== yes) {
return;
}
await repository.clean(resources.map(r => r.resourceUri));
}
private async smartCommit(
repository: Repository,
getCommitMessage: () => Promise<string | undefined>,
@@ -1271,7 +1299,7 @@ export class CommandCenter {
if (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) {
documents = documents
.filter(d => repository.indexGroup.resourceStates.some(s => s.resourceUri.path === d.uri.fsPath));
.filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath)));
}
if (documents.length > 0) {
@@ -1356,6 +1384,10 @@ export class CommandCenter {
opts.all = 'tracked';
}
if (opts.all && config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges') !== 'mixed') {
opts.all = 'tracked';
}
await repository.commit(message, opts);
const postCommitCommand = config.get<'none' | 'push' | 'sync'>('postCommitCommand');
@@ -1376,6 +1408,7 @@ export class CommandCenter {
const message = repository.inputBox.value;
const getCommitMessage = async () => {
let _message: string | undefined = message;
if (!_message) {
let value: string | undefined = undefined;
@@ -1400,7 +1433,7 @@ export class CommandCenter {
});
}
return _message ? repository.cleanUpCommitEditMessage(_message) : _message;
return _message;
};
const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
@@ -1485,7 +1518,7 @@ export class CommandCenter {
if (commit.parents.length > 1) {
const yes = localize('undo commit', "Undo merge commit");
const result = await window.showWarningMessage(localize('merge commit', "The last commit was a merge commit. Are you sure you want to undo it?"), yes);
const result = await window.showWarningMessage(localize('merge commit', "The last commit was a merge commit. Are you sure you want to undo it?"), { modal: true }, yes);
if (result !== yes) {
return;
@@ -1705,6 +1738,26 @@ export class CommandCenter {
await repository.tag(name, message);
}
@command('git.deleteTag', { repository: true })
async deleteTag(repository: Repository): Promise<void> {
const picks = repository.refs.filter(ref => ref.type === RefType.Tag)
.map(ref => new TagItem(ref));
if (picks.length === 0) {
window.showWarningMessage(localize('no tags', "This repository has no tags."));
return;
}
const placeHolder = localize('select a tag to delete', 'Select a tag to delete');
const choice = await window.showQuickPick(picks, { placeHolder });
if (!choice) {
return;
}
await repository.deleteTag(choice.label);
}
@command('git.fetch', { repository: true })
async fetch(repository: Repository): Promise<void> {
if (repository.remotes.length === 0) {
@@ -2073,9 +2126,14 @@ export class CommandCenter {
return;
}
const branchName = repository.HEAD && repository.HEAD.name || '';
if (remotes.length === 1) {
return await repository.pushTo(remotes[0].name, branchName, true);
}
const addRemote = new AddRemoteItem(this);
const picks = [...repository.remotes.map(r => ({ label: r.name, description: r.pushUrl })), addRemote];
const branchName = repository.HEAD && repository.HEAD.name || '';
const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
const choice = await window.showQuickPick(picks, { placeHolder });
@@ -2133,7 +2191,8 @@ export class CommandCenter {
}
private async _stash(repository: Repository, includeUntracked = false): Promise<void> {
const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0
&& (!includeUntracked || repository.untrackedGroup.resourceStates.length === 0);
const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
if (noUnstagedChanges && noStagedChanges) {
@@ -2215,6 +2274,18 @@ export class CommandCenter {
await repository.applyStash();
}
@command('git.stashDrop', { repository: true })
async stashDrop(repository: Repository): Promise<void> {
const placeHolder = localize('pick stash to drop', "Pick a stash to drop");
const stash = await this.pickStash(repository, placeHolder);
if (!stash) {
return;
}
await repository.dropStash(stash.index);
}
private async pickStash(repository: Repository, placeHolder: string): Promise<Stash | undefined> {
const stashes = await repository.getStashes();
@@ -2352,7 +2423,7 @@ export class CommandCenter {
return undefined;
}
if (uri.scheme === 'git') {
if (isGitUri(uri)) {
const { path } = fromGitUri(uri);
uri = Uri.file(path);
}

View File

@@ -147,4 +147,4 @@ export class GitContentProvider {
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}
}

View File

@@ -113,6 +113,7 @@ class GitDecorationProvider implements DecorationProvider {
this.collectSubmoduleDecorationData(newDecorations);
this.collectDecorationData(this.repository.indexGroup, newDecorations);
this.collectDecorationData(this.repository.untrackedGroup, newDecorations);
this.collectDecorationData(this.repository.workingTreeGroup, newDecorations);
this.collectDecorationData(this.repository.mergeGroup, newDecorations);

View File

@@ -5,8 +5,6 @@
import * as jschardet from 'jschardet';
jschardet.Constants.MINIMUM_THRESHOLD = 0.2;
function detectEncodingByBOM(buffer: Buffer): string | null {
if (!buffer || buffer.length < 2) {
return null;

View File

@@ -0,0 +1,199 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, Uri, Disposable, Event, EventEmitter, window, FileSystemProvider, FileChangeEvent, FileStat, FileType, FileChangeType, FileSystemError } 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';
interface CacheRow {
uri: Uri;
timestamp: number;
}
const THREE_MINUTES = 1000 * 60 * 3;
const FIVE_MINUTES = 1000 * 60 * 5;
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 model: Model) {
this.disposables.push(
model.onDidChangeRepository(this.onDidChangeRepository, this),
model.onDidChangeOriginalResource(this.onDidChangeOriginalResource, this),
workspace.registerFileSystemProvider('gitfs', this, { isReadonly: true, isCaseSensitive: true }),
workspace.registerResourceLabelFormatter({
scheme: 'gitfs',
formatting: {
label: '${path} (git)',
separator: '/'
}
})
);
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 gitUri = toGitUri(uri, '', { replaceFileExtension: true });
this.mtime = new Date().getTime();
this._onDidChangeFile.fire([{ type: FileChangeType.Changed, uri: gitUri }]);
}
@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;
}
stat(uri: Uri): FileStat {
const { submoduleOf } = fromGitUri(uri);
const repository = submoduleOf ? this.model.getRepository(submoduleOf) : this.model.getRepository(uri);
if (!repository) {
throw FileSystemError.FileNotFound();
}
return { type: FileType.File, size: 0, mtime: this.mtime, ctime: 0 };
}
readDirectory(): Thenable<[string, FileType][]> {
throw new Error('Method not implemented.');
}
createDirectory(): void {
throw new Error('Method not implemented.');
}
async readFile(uri: Uri): Promise<Uint8Array> {
let { 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) {
throw FileSystemError.FileNotFound();
}
const timestamp = new Date().getTime();
const cacheValue: CacheRow = { uri, timestamp };
this.cache.set(uri.toString(), cacheValue);
if (ref === '~') {
const fileUri = Uri.file(path);
const uriString = fileUri.toString();
const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
ref = indexStatus ? '' : 'HEAD';
} else if (/^~\d$/.test(ref)) {
ref = `:${ref[1]}`;
}
try {
return await repository.buffer(ref, path);
} catch (err) {
return new Uint8Array(0);
}
}
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());
}
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { promises as fs, exists } from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as cp from 'child_process';
@@ -11,7 +11,7 @@ import * as which from 'which';
import { EventEmitter } from 'events';
import iconv = require('iconv-lite');
import * as filetype from 'file-type';
import { assign, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util';
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util';
import { CancellationToken, Progress } from 'vscode';
import { URI } from 'vscode-uri';
import { detectEncoding } from './encoding';
@@ -22,8 +22,6 @@ import { StringDecoder } from 'string_decoder';
// https://github.com/microsoft/vscode/issues/65693
const MAX_CLI_LENGTH = 30000;
const readfile = denodeify<string, string | null, string>(fs.readFile);
export interface IGit {
path: string;
version: string;
@@ -196,13 +194,13 @@ async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToke
}),
new Promise<Buffer>(c => {
const buffers: Buffer[] = [];
on(child.stdout, 'data', (b: Buffer) => buffers.push(b));
once(child.stdout, 'close', () => c(Buffer.concat(buffers)));
on(child.stdout!, 'data', (b: Buffer) => buffers.push(b));
once(child.stdout!, 'close', () => c(Buffer.concat(buffers)));
}),
new Promise<string>(c => {
const buffers: Buffer[] = [];
on(child.stderr, 'data', (b: Buffer) => buffers.push(b));
once(child.stderr, 'close', () => c(Buffer.concat(buffers).toString('utf8')));
on(child.stderr!, 'data', (b: Buffer) => buffers.push(b));
once(child.stderr!, 'close', () => c(Buffer.concat(buffers).toString('utf8')));
})
]) as Promise<[number, Buffer, string]>;
@@ -350,7 +348,7 @@ export class Git {
let folderPath = path.join(parentPath, folderName);
let count = 1;
while (count < 20 && await new Promise(c => fs.exists(folderPath, c))) {
while (count < 20 && await new Promise(c => exists(folderPath, c))) {
folderName = `${baseFolderName}-${count++}`;
folderPath = path.join(parentPath, folderName);
}
@@ -360,7 +358,7 @@ export class Git {
const onSpawn = (child: cp.ChildProcess) => {
const decoder = new StringDecoder('utf8');
const lineStream = new byline.LineStream({ encoding: 'utf8' });
child.stderr.on('data', (buffer: Buffer) => lineStream.write(decoder.write(buffer)));
child.stderr!.on('data', (buffer: Buffer) => lineStream.write(decoder.write(buffer)));
let totalProgress = 0;
let previousProgress = 0;
@@ -438,7 +436,7 @@ export class Git {
}
if (options.input) {
child.stdin.end(options.input, 'utf8');
child.stdin!.end(options.input, 'utf8');
}
const bufferResult = await exec(child, options.cancellationToken);
@@ -763,9 +761,10 @@ export class Repository {
async log(options?: LogOptions): Promise<Commit[]> {
const maxEntries = options && typeof options.maxEntries === 'number' && options.maxEntries > 0 ? options.maxEntries : 32;
const args = ['log', '-' + maxEntries, `--pretty=format:${COMMIT_FORMAT}%x00%x00`];
const gitResult = await this.run(args);
if (gitResult.exitCode) {
// An empty repo.
// An empty repo
return [];
}
@@ -882,7 +881,7 @@ export class Repository {
async detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> {
const child = await this.stream(['show', object]);
const buffer = await readBytes(child.stdout, 4100);
const buffer = await readBytes(child.stdout!, 4100);
try {
child.kill();
@@ -1151,7 +1150,7 @@ export class Repository {
async stage(path: string, data: string): Promise<void> {
const child = this.stream(['hash-object', '--stdin', '-w', '--path', path], { stdio: [null, null, null] });
child.stdin.end(data, 'utf8');
child.stdin!.end(data, 'utf8');
const { exitCode, stdout } = await exec(child);
const hash = stdout.toString('utf8');
@@ -1163,11 +1162,12 @@ export class Repository {
});
}
const treeish = await this.getCommit('HEAD').then(() => 'HEAD', () => '');
let mode: string;
let add: string = '';
try {
const details = await this.getObjectDetails('HEAD', path);
const details = await this.getObjectDetails(treeish, path);
mode = details.mode;
} catch (err) {
if (err.gitErrorCode !== GitErrorCodes.UnknownPath) {
@@ -1327,6 +1327,11 @@ export class Repository {
await this.run(args);
}
async deleteTag(name: string): Promise<void> {
let args = ['tag', '-d', name];
await this.run(args);
}
async clean(paths: string[]): Promise<void> {
const pathsByGroup = groupBy(paths, p => path.dirname(p));
const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]);
@@ -1592,6 +1597,24 @@ export class Repository {
}
}
async dropStash(index?: number): Promise<void> {
const args = ['stash', 'drop'];
if (typeof index === 'number') {
args.push(`stash@{${index}}`);
}
try {
await this.run(args);
} catch (err) {
if (/No stash found/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoStashFound;
}
throw err;
}
}
getStatus(limit = 5000): Promise<{ status: IFileStatus[]; didHitLimit: boolean; }> {
return new Promise<{ status: IFileStatus[]; didHitLimit: boolean; }>((c, e) => {
const parser = new GitStatusParser();
@@ -1618,19 +1641,19 @@ export class Repository {
if (parser.status.length > limit) {
child.removeListener('exit', onExit);
child.stdout.removeListener('data', onStdoutData);
child.stdout!.removeListener('data', onStdoutData);
child.kill();
c({ status: parser.status.slice(0, limit), didHitLimit: true });
}
};
child.stdout.setEncoding('utf8');
child.stdout.on('data', onStdoutData);
child.stdout!.setEncoding('utf8');
child.stdout!.on('data', onStdoutData);
const stderrData: string[] = [];
child.stderr.setEncoding('utf8');
child.stderr.on('data', raw => stderrData.push(raw as string));
child.stderr!.setEncoding('utf8');
child.stderr!.on('data', raw => stderrData.push(raw as string));
child.on('error', cpErrorHandler(e));
child.on('exit', onExit);
@@ -1789,18 +1812,17 @@ export class Repository {
}
}
cleanupCommitEditMessage(message: string): string {
//TODO: Support core.commentChar
// TODO: Support core.commentChar
stripCommitMessageComments(message: string): string {
return message.replace(/^\s*#.*$\n?/gm, '').trim();
}
async getMergeMessage(): Promise<string | undefined> {
const mergeMsgPath = path.join(this.repositoryRoot, '.git', 'MERGE_MSG');
try {
const raw = await readfile(mergeMsgPath, 'utf8');
return raw.trim();
const raw = await fs.readFile(mergeMsgPath, 'utf8');
return this.stripCommitMessageComments(raw);
} catch {
return undefined;
}
@@ -1823,9 +1845,8 @@ export class Repository {
templatePath = path.join(this.repositoryRoot, templatePath);
}
const raw = await readfile(templatePath, 'utf8');
return raw.trim();
const raw = await fs.readFile(templatePath, 'utf8');
return this.stripCommitMessageComments(raw);
} catch (err) {
return '';
}
@@ -1848,7 +1869,7 @@ export class Repository {
const gitmodulesPath = path.join(this.root, '.gitmodules');
try {
const gitmodulesRaw = await readfile(gitmodulesPath, 'utf8');
const gitmodulesRaw = await fs.readFile(gitmodulesPath, 'utf8');
return parseGitmodules(gitmodulesRaw);
} catch (err) {
if (/ENOENT/.test(err.message)) {

View File

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

View File

@@ -0,0 +1,106 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vscode';
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(nonce: string): string {
if (process.platform === 'win32') {
return `\\\\.\\pipe\\vscode-git-ipc-${nonce}-sock`;
}
if (process.env['XDG_RUNTIME_DIR']) {
return path.join(process.env['XDG_RUNTIME_DIR'] as string, `vscode-git-ipc-${nonce}.sock`);
}
return path.join(os.tmpdir(), `vscode-git-ipc-${nonce}.sock`);
}
export interface IIPCHandler {
handle(request: any): Promise<any>;
}
export async function createIPCServer(): Promise<IIPCServer> {
const server = http.createServer();
const buffer = await new Promise<Buffer>((c, e) => crypto.randomBytes(20, (err, buf) => err ? e(err) : c(buf)));
const nonce = buffer.toString('hex');
const ipcHandlePath = getIPCHandlePath(nonce);
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(): any;
registerHandler(name: string, handler: IIPCHandler): Disposable;
}
class IPCServer implements IIPCServer, 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(): any {
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);
}
}
}

View File

@@ -11,6 +11,7 @@ import { findGit, Git, IGit } from './git';
import { Model } from './model';
import { CommandCenter } from './commands';
import { GitContentProvider } from './contentProvider';
import { GitFileSystemProvider } from './fileSystemProvider';
import { GitDecorations } from './decorationProvider';
import { Askpass } from './askpass';
import { toDisposable, filterEvent, eventToPromise } from './util';
@@ -18,9 +19,9 @@ import TelemetryReporter from 'vscode-extension-telemetry';
import { GitExtension } from './api/git';
import { GitProtocolHandler } from './protocolHandler';
import { GitExtensionImpl } from './api/extension';
// {{SQL CARBON EDIT}} - remove unused imports
// import * as path from 'path';
// import * as fs from 'fs';
import { createIPCServer, IIPCServer } from './ipc/ipcServer';
const deactivateTasks: { (): Promise<any>; }[] = [];
@@ -33,10 +34,26 @@ export async function deactivate(): Promise<any> {
async function createModel(context: ExtensionContext, outputChannel: OutputChannel, telemetryReporter: TelemetryReporter, disposables: Disposable[]): Promise<Model> {
const pathHint = workspace.getConfiguration('git').get<string>('path');
const info = await findGit(pathHint, path => outputChannel.appendLine(localize('looking', "Looking for git in: {0}", path)));
const askpass = new Askpass();
disposables.push(askpass);
const env = await askpass.getEnv();
let env: any = {};
let ipc: IIPCServer | undefined;
try {
ipc = await createIPCServer();
disposables.push(ipc);
env = { ...env, ...ipc.getEnv() };
} catch {
// noop
}
if (ipc) {
const askpass = new Askpass(ipc);
disposables.push(askpass);
env = { ...env, ...askpass.getEnv() };
} else {
env = { ...env, ...Askpass.getDisabledEnv() };
}
const git = new Git({ gitPath: info.path, version: info.version, env });
const model = new Model(git, context.globalState, outputChannel);
disposables.push(model);
@@ -63,6 +80,7 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann
disposables.push(
new CommandCenter(git, model, outputChannel, telemetryReporter),
new GitContentProvider(model),
new GitFileSystemProvider(model),
new GitDecorations(model),
new GitProtocolHandler()
);
@@ -198,4 +216,4 @@ async function checkGitVersion(_info: IGit): Promise<void> {
// await config.update('ignoreLegacyWarning', true, true);
// }
// {{SQL CARBON EDIT}} - End
}
}

View File

@@ -6,7 +6,7 @@
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, OutputChannel } from 'vscode';
import { Repository, RepositoryState } from './repository';
import { memoize, sequentialize, debounce } from './decorators';
import { dispose, anyEvent, filterEvent, isDescendant, firstIndex } from './util';
import { dispose, anyEvent, filterEvent, isDescendant, firstIndex, pathEquals } from './util';
import { Git } from './git';
import * as path from 'path';
import * as fs from 'fs';
@@ -240,10 +240,7 @@ export class Model {
return;
}
const config = workspace.getConfiguration('git');
const ignoredRepos = new Set(config.get<Array<string>>('ignoredRepositories'));
if (ignoredRepos.has(rawRoot)) {
if (this.shouldRepositoryBeIgnored(rawRoot)) {
return;
}
@@ -261,6 +258,27 @@ export class Model {
}
}
private shouldRepositoryBeIgnored(repositoryRoot: string): boolean {
const config = workspace.getConfiguration('git');
const ignoredRepos = config.get<string[]>('ignoredRepositories') || [];
for (const ignoredRepo of ignoredRepos) {
if (path.isAbsolute(ignoredRepo)) {
if (pathEquals(ignoredRepo, repositoryRoot)) {
return true;
}
} else {
for (const folder of workspace.workspaceFolders || []) {
if (pathEquals(path.join(folder.uri.fsPath, ignoredRepo), repositoryRoot)) {
return true;
}
}
}
}
return false;
}
private open(repository: Repository): void {
this.outputChannel.appendLine(`Open repository: ${repository.root}`);

View File

@@ -3,17 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, SourceControlInputBoxValidation, Disposable, ProgressLocation, window, workspace, WorkspaceEdit, ThemeColor, Decoration, Memento, SourceControlInputBoxValidationType, OutputChannel, LogLevel, env, ProgressOptions, CancellationToken } from 'vscode';
import { Repository as BaseRepository, Commit, Stash, GitError, Submodule, CommitOptions, ForcePushMode } from './git';
import { anyEvent, filterEvent, eventToPromise, dispose, find, isDescendant, IDisposable, onceEvent, EmptyDisposable, debounceEvent, combinedDisposable } from './util';
import { memoize, throttle, debounce } from './decorators';
import { toGitUri } from './uri';
import { AutoFetcher } from './autofetch';
import * as path from 'path';
import * as nls from 'vscode-nls';
import * as fs from 'fs';
import * as path from 'path';
import { CancellationToken, Command, Disposable, env, Event, EventEmitter, LogLevel, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, Decoration } from 'vscode';
import * as nls from 'vscode-nls';
import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status } from './api/git';
import { AutoFetcher } from './autofetch';
import { debounce, memoize, throttle } from './decorators';
import { Commit, CommitOptions, ForcePushMode, GitError, Repository as BaseRepository, Stash, Submodule } from './git';
import { StatusBarCommands } from './statusbar';
import { Branch, Ref, Remote, RefType, GitErrorCodes, Status, LogOptions, Change } from './api/git';
import { toGitUri } from './uri';
import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent } from './util';
import { IFileWatcher, watch } from './watch';
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
@@ -33,7 +33,8 @@ export const enum RepositoryState {
export const enum ResourceGroupType {
Merge,
Index,
WorkingTree
WorkingTree,
Untracked
}
export class Resource implements SourceControlResourceState {
@@ -292,6 +293,7 @@ export const enum Operation {
Merge = 'Merge',
Ignore = 'Ignore',
Tag = 'Tag',
DeleteTag = 'DeleteTag',
Stash = 'Stash',
CheckIgnore = 'CheckIgnore',
GetObjectDetails = 'GetObjectDetails',
@@ -569,6 +571,9 @@ export class Repository implements Disposable {
private _workingTreeGroup: SourceControlResourceGroup;
get workingTreeGroup(): GitResourceGroup { return this._workingTreeGroup as GitResourceGroup; }
private _untrackedGroup: SourceControlResourceGroup;
get untrackedGroup(): GitResourceGroup { return this._untrackedGroup as GitResourceGroup; }
private _HEAD: Branch | undefined;
get HEAD(): Branch | undefined {
return this._HEAD;
@@ -641,6 +646,7 @@ export class Repository implements Disposable {
this.mergeGroup.resourceStates = [];
this.indexGroup.resourceStates = [];
this.workingTreeGroup.resourceStates = [];
this.untrackedGroup.resourceStates = [];
this._sourceControl.count = 0;
}
@@ -708,6 +714,7 @@ export class Repository implements Disposable {
this._mergeGroup = this._sourceControl.createResourceGroup('merge', localize('merge changes', "MERGE CHANGES"));
this._indexGroup = this._sourceControl.createResourceGroup('index', localize('staged changes', "STAGED CHANGES"));
this._workingTreeGroup = this._sourceControl.createResourceGroup('workingTree', localize('changes', "CHANGES"));
this._untrackedGroup = this._sourceControl.createResourceGroup('untracked', localize('untracked changes', "UNTRACKED CHANGES"));
const updateIndexGroupVisibility = () => {
const config = workspace.getConfiguration('git', root);
@@ -721,11 +728,16 @@ export class Repository implements Disposable {
const onConfigListenerForBranchSortOrder = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.branchSortOrder', root));
onConfigListenerForBranchSortOrder(this.updateModelState, this, this.disposables);
const onConfigListenerForUntracked = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.untrackedChanges', root));
onConfigListenerForUntracked(this.updateModelState, this, this.disposables);
this.mergeGroup.hideWhenEmpty = true;
this.untrackedGroup.hideWhenEmpty = true;
this.disposables.push(this.mergeGroup);
this.disposables.push(this.indexGroup);
this.disposables.push(this.workingTreeGroup);
this.disposables.push(this.untrackedGroup);
this.disposables.push(new AutoFetcher(this, globalState));
@@ -911,8 +923,8 @@ export class Repository implements Disposable {
return this.run(Operation.HashObject, () => this.repository.hashObject(data));
}
async add(resources: Uri[]): Promise<void> {
await this.run(Operation.Add, () => this.repository.add(resources.map(r => r.fsPath)));
async add(resources: Uri[], opts?: { update?: boolean }): Promise<void> {
await this.run(Operation.Add, () => this.repository.add(resources.map(r => r.fsPath), opts));
}
async rm(resources: Uri[]): Promise<void> {
@@ -957,6 +969,7 @@ export class Repository implements Disposable {
const toClean: string[] = [];
const toCheckout: string[] = [];
const submodulesToUpdate: string[] = [];
const resourceStates = [...this.workingTreeGroup.resourceStates, ...this.untrackedGroup.resourceStates];
resources.forEach(r => {
const fsPath = r.fsPath;
@@ -969,7 +982,7 @@ export class Repository implements Disposable {
}
const raw = r.toString();
const scmResource = find(this.workingTreeGroup.resourceStates, sr => sr.resourceUri.toString() === raw);
const scmResource = find(resourceStates, sr => sr.resourceUri.toString() === raw);
if (!scmResource) {
return;
@@ -1021,6 +1034,10 @@ export class Repository implements Disposable {
await this.run(Operation.Tag, () => this.repository.tag(name, message));
}
async deleteTag(name: string): Promise<void> {
await this.run(Operation.DeleteTag, () => this.repository.deleteTag(name));
}
async checkout(treeish: string): Promise<void> {
await this.run(Operation.Checkout, () => this.repository.checkout(treeish, []));
}
@@ -1249,6 +1266,10 @@ export class Repository implements Disposable {
return await this.run(Operation.Stash, () => this.repository.popStash(index));
}
async dropStash(index?: number): Promise<void> {
return await this.run(Operation.Stash, () => this.repository.dropStash(index));
}
async applyStash(index?: number): Promise<void> {
return await this.run(Operation.Stash, () => this.repository.applyStash(index));
}
@@ -1257,10 +1278,6 @@ export class Repository implements Disposable {
return await this.run(Operation.GetCommitTemplate, async () => this.repository.getCommitTemplate());
}
async cleanUpCommitEditMessage(editMessage: string): Promise<string> {
return this.repository.cleanupCommitEditMessage(editMessage);
}
async ignore(files: Uri[]): Promise<void> {
return await this.run(Operation.Ignore, async () => {
const ignoreFile = `${this.repository.root}${path.sep}.gitignore`;
@@ -1298,7 +1315,7 @@ export class Repository implements Disposable {
// https://git-scm.com/docs/git-check-ignore#git-check-ignore--z
const child = this.repository.stream(['check-ignore', '-v', '-z', '--stdin'], { stdio: [null, null, null] });
child.stdin.end(filePaths.join('\0'), 'utf8');
child.stdin!.end(filePaths.join('\0'), 'utf8');
const onExit = (exitCode: number) => {
if (exitCode === 1) {
@@ -1320,12 +1337,12 @@ export class Repository implements Disposable {
data += raw;
};
child.stdout.setEncoding('utf8');
child.stdout.on('data', onStdoutData);
child.stdout!.setEncoding('utf8');
child.stdout!.on('data', onStdoutData);
let stderr: string = '';
child.stderr.setEncoding('utf8');
child.stderr.on('data', raw => stderr += raw);
child.stderr!.setEncoding('utf8');
child.stderr!.on('data', raw => stderr += raw);
child.on('error', reject);
child.on('exit', onExit);
@@ -1427,6 +1444,7 @@ export class Repository implements Disposable {
private async updateModelState(): Promise<void> {
const { status, didHitLimit } = await this.repository.getStatus();
const config = workspace.getConfiguration('git');
const scopedConfig = workspace.getConfiguration('git', Uri.file(this.repository.root));
const shouldIgnore = config.get<boolean>('ignoreLimitWarning') === true;
const useIcons = !config.get<boolean>('decorations.enabled', true);
this.isRepositoryHuge = didHitLimit;
@@ -1487,17 +1505,29 @@ export class Repository implements Disposable {
this._submodules = submodules!;
this.rebaseCommit = rebaseCommit;
const untrackedChanges = scopedConfig.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
const index: Resource[] = [];
const workingTree: Resource[] = [];
const merge: Resource[] = [];
const untracked: Resource[] = [];
status.forEach(raw => {
const uri = Uri.file(path.join(this.repository.root, raw.path));
const renameUri = raw.rename ? Uri.file(path.join(this.repository.root, raw.rename)) : undefined;
const renameUri = raw.rename
? Uri.file(path.join(this.repository.root, raw.rename))
: undefined;
switch (raw.x + raw.y) {
case '??': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.UNTRACKED, useIcons));
case '!!': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.IGNORED, useIcons));
case '??': switch (untrackedChanges) {
case 'mixed': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.UNTRACKED, useIcons));
case 'separate': return untracked.push(new Resource(ResourceGroupType.Untracked, uri, Status.UNTRACKED, useIcons));
default: return undefined;
}
case '!!': switch (untrackedChanges) {
case 'mixed': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.IGNORED, useIcons));
case 'separate': return untracked.push(new Resource(ResourceGroupType.Untracked, uri, Status.IGNORED, useIcons));
default: return undefined;
}
case 'DD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_DELETED, useIcons));
case 'AU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.ADDED_BY_US, useIcons));
case 'UD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.DELETED_BY_THEM, useIcons));
@@ -1520,6 +1550,7 @@ export class Repository implements Disposable {
case 'D': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.DELETED, useIcons, renameUri)); break;
case 'A': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.INTENT_TO_ADD, useIcons, renameUri)); break;
}
return undefined;
});
@@ -1527,6 +1558,7 @@ export class Repository implements Disposable {
this.mergeGroup.resourceStates = merge;
this.indexGroup.resourceStates = index;
this.workingTreeGroup.resourceStates = workingTree;
this.untrackedGroup.resourceStates = untracked;
// set count badge
this.setCountBadge();
@@ -1537,12 +1569,27 @@ export class Repository implements Disposable {
}
private setCountBadge(): void {
const countBadge = workspace.getConfiguration('git').get<string>('countBadge');
let count = this.mergeGroup.resourceStates.length + this.indexGroup.resourceStates.length + this.workingTreeGroup.resourceStates.length;
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const countBadge = config.get<'all' | 'tracked' | 'off'>('countBadge');
const untrackedChanges = config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
let count =
this.mergeGroup.resourceStates.length +
this.indexGroup.resourceStates.length +
this.workingTreeGroup.resourceStates.length;
switch (countBadge) {
case 'off': count = 0; break;
case 'tracked': count = count - this.workingTreeGroup.resourceStates.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED).length; break;
case 'tracked':
if (untrackedChanges === 'mixed') {
count -= this.workingTreeGroup.resourceStates.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED).length;
}
break;
case 'all':
if (untrackedChanges === 'separate') {
count += this.untrackedGroup.resourceStates.length;
}
break;
}
this._sourceControl.count = count;
@@ -1644,7 +1691,7 @@ export class Repository implements Disposable {
const head = HEAD.name || tagName || (HEAD.commit || '').substr(0, 8);
return head
+ (this.workingTreeGroup.resourceStates.length > 0 ? '*' : '')
+ (this.workingTreeGroup.resourceStates.length + this.untrackedGroup.resourceStates.length > 0 ? '*' : '')
+ (this.indexGroup.resourceStates.length > 0 ? '+' : '')
+ (this.mergeGroup.resourceStates.length > 0 ? '!' : '');
}

View File

@@ -1,11 +0,0 @@
declare module 'jschardet' {
export interface IDetectedMap {
encoding: string,
confidence: number
}
export function detect(buffer: Buffer): IDetectedMap;
export const Constants: {
MINIMUM_THRESHOLD: number,
}
}

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Uri } from 'vscode';
import * as qs from 'querystring';
export interface GitUriParams {
path: string;
@@ -11,8 +12,26 @@ export interface GitUriParams {
submoduleOf?: string;
}
export function isGitUri(uri: Uri): boolean {
return /^git(fs)?$/.test(uri.scheme);
}
export function fromGitUri(uri: Uri): GitUriParams {
return JSON.parse(uri.query);
const result = qs.parse(uri.query) as any;
if (!result) {
throw new Error('Invalid git URI: empty query');
}
if (typeof result.path !== 'string') {
throw new Error('Invalid git URI: missing path');
}
if (typeof result.ref !== 'string') {
throw new Error('Invalid git URI: missing ref');
}
return result;
}
export interface GitUriOptions {
@@ -42,8 +61,8 @@ export function toGitUri(uri: Uri, ref: string, options: GitUriOptions = {}): Ur
}
return uri.with({
scheme: 'git',
scheme: 'gitfs',
path,
query: JSON.stringify(params)
query: qs.stringify(params as any)
});
}
}

View File

@@ -6,7 +6,7 @@
import { Event } from 'vscode';
import { dirname, sep } from 'path';
import { Readable } from 'stream';
import * as fs from 'fs';
import { promises as fs, createReadStream } from 'fs';
import * as byline from 'byline';
export function log(...args: any[]): void {
@@ -140,25 +140,14 @@ export function groupBy<T>(arr: T[], fn: (el: T) => string): { [key: string]: T[
}, Object.create(null));
}
export function denodeify<A, B, C, R>(fn: Function): (a: A, b: B, c: C) => Promise<R>;
export function denodeify<A, B, R>(fn: Function): (a: A, b: B) => Promise<R>;
export function denodeify<A, R>(fn: Function): (a: A) => Promise<R>;
export function denodeify<R>(fn: Function): (...args: any[]) => Promise<R>;
export function denodeify<R>(fn: Function): (...args: any[]) => Promise<R> {
return (...args) => new Promise<R>((c, e) => fn(...args, (err: any, r: any) => err ? e(err) : c(r)));
}
export function nfcall<R>(fn: Function, ...args: any[]): Promise<R> {
return new Promise<R>((c, e) => fn(...args, (err: any, r: any) => err ? e(err) : c(r)));
}
export async function mkdirp(path: string, mode?: number): Promise<boolean> {
const mkdir = async () => {
try {
await nfcall(fs.mkdir, path, mode);
await fs.mkdir(path, mode);
} catch (err) {
if (err.code === 'EEXIST') {
const stat = await nfcall<fs.Stats>(fs.stat, path);
const stat = await fs.stat(path);
if (stat.isDirectory()) {
return;
@@ -232,7 +221,7 @@ export function find<T>(array: T[], fn: (t: T) => boolean): T | undefined {
export async function grep(filename: string, pattern: RegExp): Promise<boolean> {
return new Promise<boolean>((c, e) => {
const fileStream = fs.createReadStream(filename, { encoding: 'utf8' });
const fileStream = createReadStream(filename, { encoding: 'utf8' });
const stream = byline(fileStream);
stream.on('data', (line: string) => {
if (pattern.test(line)) {