mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-28 17:23:19 -05:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
2
extensions/git/src/askpass-empty.sh
Normal file
2
extensions/git/src/askpass-empty.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo ''
|
||||
74
extensions/git/src/askpass-main.ts
Normal file
74
extensions/git/src/askpass-main.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as http from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
function fatal(err: any): void {
|
||||
console.error(localize('missOrInvalid', "Missing or invalid credentials."));
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function main(argv: string[]): void {
|
||||
if (argv.length !== 5) {
|
||||
return fatal('Wrong number of arguments');
|
||||
}
|
||||
|
||||
if (!process.env['VSCODE_GIT_ASKPASS_HANDLE']) {
|
||||
return fatal('Missing handle');
|
||||
}
|
||||
|
||||
if (!process.env['VSCODE_GIT_ASKPASS_PIPE']) {
|
||||
return fatal('Missing pipe');
|
||||
}
|
||||
|
||||
if (process.env['VSCODE_GIT_COMMAND'] === 'fetch') {
|
||||
return fatal('Skip fetch commands');
|
||||
}
|
||||
|
||||
const output = process.env['VSCODE_GIT_ASKPASS_PIPE'];
|
||||
const socketPath = process.env['VSCODE_GIT_ASKPASS_HANDLE'];
|
||||
const request = argv[2];
|
||||
const host = argv[4].substring(1, argv[4].length - 2);
|
||||
const opts: http.RequestOptions = {
|
||||
socketPath,
|
||||
path: '/',
|
||||
method: 'POST'
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
main(process.argv);
|
||||
5
extensions/git/src/askpass.sh
Normal file
5
extensions/git/src/askpass.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
VSCODE_GIT_ASKPASS_PIPE=`mktemp`
|
||||
VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $*
|
||||
cat $VSCODE_GIT_ASKPASS_PIPE
|
||||
rm $VSCODE_GIT_ASKPASS_PIPE
|
||||
114
extensions/git/src/askpass.ts
Normal file
114
extensions/git/src/askpass.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Disposable, window, InputBoxOptions } from 'vscode';
|
||||
import { denodeify } from './util';
|
||||
import * as path from 'path';
|
||||
import * as http from 'http';
|
||||
import * as os from 'os';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
const randomBytes = denodeify<Buffer>(crypto.randomBytes);
|
||||
|
||||
export interface AskpassEnvironment {
|
||||
GIT_ASKPASS: string;
|
||||
ELECTRON_RUN_AS_NODE?: string;
|
||||
VSCODE_GIT_ASKPASS_NODE?: string;
|
||||
VSCODE_GIT_ASKPASS_MAIN?: string;
|
||||
VSCODE_GIT_ASKPASS_HANDLE?: string;
|
||||
}
|
||||
|
||||
function getIPCHandlePath(nonce: string): string {
|
||||
if (process.platform === 'win32') {
|
||||
return `\\\\.\\pipe\\vscode-git-askpass-${nonce}-sock`;
|
||||
}
|
||||
|
||||
if (process.env['XDG_RUNTIME_DIR']) {
|
||||
return path.join(process.env['XDG_RUNTIME_DIR'], `vscode-git-askpass-${nonce}.sock`);
|
||||
}
|
||||
|
||||
return path.join(os.tmpdir(), `vscode-git-askpass-${nonce}.sock`);
|
||||
}
|
||||
|
||||
export class Askpass implements Disposable {
|
||||
|
||||
private server: http.Server;
|
||||
private ipcHandlePathPromise: Promise<string>;
|
||||
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);
|
||||
|
||||
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.ServerRequest, 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> {
|
||||
const options: InputBoxOptions = {
|
||||
password: /password/i.test(request),
|
||||
placeHolder: request,
|
||||
prompt: `Git: ${host}`,
|
||||
ignoreFocusOut: true
|
||||
};
|
||||
|
||||
return await window.showInputBox(options) || '';
|
||||
}
|
||||
|
||||
async getEnv(): Promise<AskpassEnvironment> {
|
||||
if (!this.enabled) {
|
||||
return {
|
||||
GIT_ASKPASS: path.join(__dirname, 'askpass-empty.sh')
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.server.close();
|
||||
}
|
||||
}
|
||||
62
extensions/git/src/autofetch.ts
Normal file
62
extensions/git/src/autofetch.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { workspace, Disposable } from 'vscode';
|
||||
import { GitErrorCodes } from './git';
|
||||
import { Repository } from './repository';
|
||||
import { throttle } from './decorators';
|
||||
|
||||
export class AutoFetcher {
|
||||
|
||||
private static Period = 3 * 60 * 1000 /* three minutes */;
|
||||
private disposables: Disposable[] = [];
|
||||
private timer: NodeJS.Timer;
|
||||
|
||||
constructor(private repository: Repository) {
|
||||
workspace.onDidChangeConfiguration(this.onConfiguration, this, this.disposables);
|
||||
this.onConfiguration();
|
||||
}
|
||||
|
||||
private onConfiguration(): void {
|
||||
const gitConfig = workspace.getConfiguration('git');
|
||||
|
||||
if (gitConfig.get<boolean>('autofetch') === false) {
|
||||
this.disable();
|
||||
} else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
enable(): void {
|
||||
if (this.timer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetch();
|
||||
this.timer = setInterval(() => this.fetch(), AutoFetcher.Period);
|
||||
}
|
||||
|
||||
disable(): void {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
@throttle
|
||||
private async fetch(): Promise<void> {
|
||||
try {
|
||||
await this.repository.fetch();
|
||||
} catch (err) {
|
||||
if (err.gitErrorCode === GitErrorCodes.AuthenticationFailed) {
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disable();
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
1412
extensions/git/src/commands.ts
Normal file
1412
extensions/git/src/commands.ts
Normal file
File diff suppressed because it is too large
Load Diff
117
extensions/git/src/contentProvider.ts
Normal file
117
extensions/git/src/contentProvider.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { workspace, Uri, Disposable, Event, EventEmitter, window } from 'vscode';
|
||||
import { debounce } from './decorators';
|
||||
import { fromGitUri } from './uri';
|
||||
import { Model, ModelChangeEvent } from './model';
|
||||
|
||||
interface CacheRow {
|
||||
uri: Uri;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface Cache {
|
||||
[uri: string]: CacheRow;
|
||||
}
|
||||
|
||||
const THREE_MINUTES = 1000 * 60 * 3;
|
||||
const FIVE_MINUTES = 1000 * 60 * 5;
|
||||
|
||||
export class GitContentProvider {
|
||||
|
||||
private onDidChangeEmitter = new EventEmitter<Uri>();
|
||||
get onDidChange(): Event<Uri> { return this.onDidChangeEmitter.event; }
|
||||
|
||||
private changedRepositoryRoots = new Set<string>();
|
||||
private cache: Cache = Object.create(null);
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(private model: Model) {
|
||||
this.disposables.push(
|
||||
model.onDidChangeRepository(this.onDidChangeRepository, this),
|
||||
workspace.registerTextDocumentContentProvider('git', this)
|
||||
);
|
||||
|
||||
setInterval(() => this.cleanup(), FIVE_MINUTES);
|
||||
}
|
||||
|
||||
private onDidChangeRepository({ repository }: ModelChangeEvent): void {
|
||||
this.changedRepositoryRoots.add(repository.root);
|
||||
this.eventuallyFireChangeEvents();
|
||||
}
|
||||
|
||||
@debounce(1100)
|
||||
private eventuallyFireChangeEvents(): void {
|
||||
this.fireChangeEvents();
|
||||
}
|
||||
|
||||
private fireChangeEvents(): void {
|
||||
Object.keys(this.cache).forEach(key => {
|
||||
const uri = this.cache[key].uri;
|
||||
const fsPath = uri.fsPath;
|
||||
|
||||
for (const root of this.changedRepositoryRoots) {
|
||||
if (fsPath.startsWith(root)) {
|
||||
this.onDidChangeEmitter.fire(uri);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.changedRepositoryRoots.clear();
|
||||
}
|
||||
|
||||
async provideTextDocumentContent(uri: Uri): Promise<string> {
|
||||
const repository = this.model.getRepository(uri);
|
||||
|
||||
if (!repository) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const cacheKey = uri.toString();
|
||||
const timestamp = new Date().getTime();
|
||||
const cacheValue = { uri, timestamp };
|
||||
|
||||
this.cache[cacheKey] = cacheValue;
|
||||
|
||||
let { path, ref } = fromGitUri(uri);
|
||||
|
||||
if (ref === '~') {
|
||||
const fileUri = Uri.file(path);
|
||||
const uriString = fileUri.toString();
|
||||
const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.original.toString() === uriString);
|
||||
ref = indexStatus ? '' : 'HEAD';
|
||||
}
|
||||
|
||||
try {
|
||||
return await repository.show(ref, path);
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
const now = new Date().getTime();
|
||||
const cache = Object.create(null);
|
||||
|
||||
Object.keys(this.cache).forEach(key => {
|
||||
const row = this.cache[key];
|
||||
const isOpen = window.visibleTextEditors.some(e => e.document.uri.fsPath === row.uri.fsPath);
|
||||
|
||||
if (isOpen || now - row.timestamp < THREE_MINUTES) {
|
||||
cache[row.uri.toString()] = row;
|
||||
}
|
||||
});
|
||||
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
103
extensions/git/src/decorators.ts
Normal file
103
extensions/git/src/decorators.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
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 (...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 (...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<T>(fn: Function, key: string): Function {
|
||||
const currentKey = `__$sequence$${key}`;
|
||||
|
||||
return function (...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 (...args: any[]) {
|
||||
clearTimeout(this[timerKey]);
|
||||
this[timerKey] = setTimeout(() => fn.apply(this, args), delay);
|
||||
};
|
||||
});
|
||||
}
|
||||
1067
extensions/git/src/git.ts
Normal file
1067
extensions/git/src/git.ts
Normal file
File diff suppressed because it is too large
Load Diff
57
extensions/git/src/iterators.ts
Normal file
57
extensions/git/src/iterators.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
function* filter<T>(it: IterableIterator<T>, condition: (t: T, i: number) => boolean): IterableIterator<T> {
|
||||
let i = 0;
|
||||
for (let t of it) {
|
||||
if (condition(t, i++)) {
|
||||
yield t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* map<T, R>(it: IterableIterator<T>, fn: (t: T, i: number) => R): IterableIterator<R> {
|
||||
let i = 0;
|
||||
for (let t of it) {
|
||||
yield fn(t, i++);
|
||||
}
|
||||
}
|
||||
|
||||
export interface FunctionalIterator<T> extends Iterable<T> {
|
||||
filter(condition: (t: T, i: number) => boolean): FunctionalIterator<T>;
|
||||
map<R>(fn: (t: T, i: number) => R): FunctionalIterator<R>;
|
||||
toArray(): T[];
|
||||
}
|
||||
|
||||
class FunctionalIteratorImpl<T> implements FunctionalIterator<T> {
|
||||
|
||||
constructor(private iterator: IterableIterator<T>) { }
|
||||
|
||||
filter(condition: (t: T, i: number) => boolean): FunctionalIterator<T> {
|
||||
return new FunctionalIteratorImpl(filter(this.iterator, condition));
|
||||
}
|
||||
|
||||
map<R>(fn: (t: T, i: number) => R): FunctionalIterator<R> {
|
||||
return new FunctionalIteratorImpl(map<T, R>(this.iterator, fn));
|
||||
}
|
||||
|
||||
toArray(): T[] {
|
||||
return Array.from(this.iterator);
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<T> {
|
||||
return this.iterator;
|
||||
}
|
||||
}
|
||||
|
||||
export function iterate<T>(obj: T[] | IterableIterator<T>): FunctionalIterator<T> {
|
||||
if (Array.isArray(obj)) {
|
||||
return new FunctionalIteratorImpl(obj[Symbol.iterator]());
|
||||
}
|
||||
|
||||
return new FunctionalIteratorImpl(obj);
|
||||
}
|
||||
104
extensions/git/src/main.ts
Normal file
104
extensions/git/src/main.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.config(process.env.VSCODE_NLS_CONFIG)();
|
||||
import { ExtensionContext, workspace, window, Disposable, commands, Uri } from 'vscode';
|
||||
import { findGit, Git, IGit } from './git';
|
||||
import { Model } from './model';
|
||||
import { CommandCenter } from './commands';
|
||||
import { GitContentProvider } from './contentProvider';
|
||||
import { Askpass } from './askpass';
|
||||
import { toDisposable } from './util';
|
||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||
|
||||
async function init(context: ExtensionContext, disposables: Disposable[]): Promise<void> {
|
||||
const { name, version, aiKey } = require(context.asAbsolutePath('./package.json')) as { name: string, version: string, aiKey: string };
|
||||
const telemetryReporter: TelemetryReporter = new TelemetryReporter(name, version, aiKey);
|
||||
disposables.push(telemetryReporter);
|
||||
|
||||
const outputChannel = window.createOutputChannel('Git');
|
||||
disposables.push(outputChannel);
|
||||
|
||||
const config = workspace.getConfiguration('git');
|
||||
const enabled = config.get<boolean>('enabled') === true;
|
||||
const pathHint = workspace.getConfiguration('git').get<string>('path');
|
||||
const info = await findGit(pathHint);
|
||||
const askpass = new Askpass();
|
||||
const env = await askpass.getEnv();
|
||||
const git = new Git({ gitPath: info.path, version: info.version, env });
|
||||
const model = new Model(git);
|
||||
disposables.push(model);
|
||||
|
||||
const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`);
|
||||
model.onDidOpenRepository(onRepository, null, disposables);
|
||||
model.onDidCloseRepository(onRepository, null, disposables);
|
||||
onRepository();
|
||||
|
||||
if (!enabled) {
|
||||
const commandCenter = new CommandCenter(git, model, outputChannel, telemetryReporter);
|
||||
disposables.push(commandCenter);
|
||||
return;
|
||||
}
|
||||
|
||||
outputChannel.appendLine(localize('using git', "Using git {0} from {1}", info.version, info.path));
|
||||
|
||||
const onOutput = str => outputChannel.append(str);
|
||||
git.onOutput.addListener('log', onOutput);
|
||||
disposables.push(toDisposable(() => git.onOutput.removeListener('log', onOutput)));
|
||||
|
||||
disposables.push(
|
||||
new CommandCenter(git, model, outputChannel, telemetryReporter),
|
||||
new GitContentProvider(model),
|
||||
);
|
||||
|
||||
await checkGitVersion(info);
|
||||
}
|
||||
|
||||
export function activate(context: ExtensionContext): any {
|
||||
const disposables: Disposable[] = [];
|
||||
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
|
||||
|
||||
init(context, disposables)
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
async function checkGitVersion(info: IGit): Promise<void> {
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
// remove Git version check on since for Carbon
|
||||
|
||||
return;
|
||||
|
||||
/*
|
||||
const config = workspace.getConfiguration('git');
|
||||
const shouldIgnore = config.get<boolean>('ignoreLegacyWarning') === true;
|
||||
|
||||
if (shouldIgnore) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[01]/.test(info.version)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const update = localize('updateGit', "Update Git");
|
||||
const neverShowAgain = localize('neverShowAgain', "Don't show again");
|
||||
|
||||
const choice = await window.showWarningMessage(
|
||||
localize('git20', "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://git-scm.com/'));
|
||||
} else if (choice === neverShowAgain) {
|
||||
await config.update('ignoreLegacyWarning', true, true);
|
||||
}
|
||||
*/
|
||||
}
|
||||
291
extensions/git/src/model.ts
Normal file
291
extensions/git/src/model.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor } from 'vscode';
|
||||
import { Repository, RepositoryState } from './repository';
|
||||
import { memoize, sequentialize, debounce } from './decorators';
|
||||
import { dispose, anyEvent, filterEvent } from './util';
|
||||
import { Git, GitErrorCodes } from './git';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
class RepositoryPick implements QuickPickItem {
|
||||
@memoize get label(): string { return path.basename(this.repository.root); }
|
||||
@memoize get description(): string { return path.dirname(this.repository.root); }
|
||||
constructor(public readonly repository: Repository) { }
|
||||
}
|
||||
|
||||
export interface ModelChangeEvent {
|
||||
repository: Repository;
|
||||
uri: Uri;
|
||||
}
|
||||
|
||||
interface OpenRepository extends Disposable {
|
||||
repository: Repository;
|
||||
}
|
||||
|
||||
export class Model {
|
||||
|
||||
private _onDidOpenRepository = new EventEmitter<Repository>();
|
||||
readonly onDidOpenRepository: Event<Repository> = this._onDidOpenRepository.event;
|
||||
|
||||
private _onDidCloseRepository = new EventEmitter<Repository>();
|
||||
readonly onDidCloseRepository: Event<Repository> = this._onDidCloseRepository.event;
|
||||
|
||||
private _onDidChangeRepository = new EventEmitter<ModelChangeEvent>();
|
||||
readonly onDidChangeRepository: Event<ModelChangeEvent> = this._onDidChangeRepository.event;
|
||||
|
||||
private openRepositories: OpenRepository[] = [];
|
||||
get repositories(): Repository[] { return this.openRepositories.map(r => r.repository); }
|
||||
|
||||
private possibleGitRepositoryPaths = new Set<string>();
|
||||
|
||||
private enabled = false;
|
||||
private configurationChangeDisposable: Disposable;
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(private git: Git) {
|
||||
const config = workspace.getConfiguration('git');
|
||||
this.enabled = config.get<boolean>('enabled') === true;
|
||||
|
||||
this.configurationChangeDisposable = workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this);
|
||||
|
||||
if (this.enabled) {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
private onDidChangeConfiguration(): void {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const enabled = config.get<boolean>('enabled') === true;
|
||||
|
||||
if (enabled === this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
this.enable();
|
||||
} else {
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
private enable(): void {
|
||||
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
|
||||
this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] });
|
||||
|
||||
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
|
||||
this.onDidChangeVisibleTextEditors(window.visibleTextEditors);
|
||||
|
||||
const fsWatcher = workspace.createFileSystemWatcher('**');
|
||||
this.disposables.push(fsWatcher);
|
||||
|
||||
const onWorkspaceChange = anyEvent(fsWatcher.onDidChange, fsWatcher.onDidCreate, fsWatcher.onDidDelete);
|
||||
const onGitRepositoryChange = filterEvent(onWorkspaceChange, uri => /\/\.git\//.test(uri.path));
|
||||
const onPossibleGitRepositoryChange = filterEvent(onGitRepositoryChange, uri => !this.getRepository(uri));
|
||||
onPossibleGitRepositoryChange(this.onPossibleGitRepositoryChange, this, this.disposables);
|
||||
|
||||
this.scanWorkspaceFolders();
|
||||
}
|
||||
|
||||
private disable(): void {
|
||||
const openRepositories = [...this.openRepositories];
|
||||
openRepositories.forEach(r => r.dispose());
|
||||
this.openRepositories = [];
|
||||
|
||||
this.possibleGitRepositoryPaths.clear();
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the first level of each workspace folder, looking
|
||||
* for git repositories.
|
||||
*/
|
||||
private async scanWorkspaceFolders(): Promise<void> {
|
||||
for (const folder of workspace.workspaceFolders || []) {
|
||||
const root = folder.uri.fsPath;
|
||||
const children = await new Promise<string[]>((c, e) => fs.readdir(root, (err, r) => err ? e(err) : c(r)));
|
||||
|
||||
children
|
||||
.filter(child => child !== '.git')
|
||||
.forEach(child => this.tryOpenRepository(path.join(root, child)));
|
||||
}
|
||||
}
|
||||
|
||||
private onPossibleGitRepositoryChange(uri: Uri): void {
|
||||
const possibleGitRepositoryPath = uri.fsPath.replace(/\.git.*$/, '');
|
||||
this.possibleGitRepositoryPaths.add(possibleGitRepositoryPath);
|
||||
this.eventuallyScanPossibleGitRepositories();
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
private eventuallyScanPossibleGitRepositories(): void {
|
||||
for (const path of this.possibleGitRepositoryPaths) {
|
||||
this.tryOpenRepository(path);
|
||||
}
|
||||
|
||||
this.possibleGitRepositoryPaths.clear();
|
||||
}
|
||||
|
||||
private async onDidChangeWorkspaceFolders({ added, removed }: WorkspaceFoldersChangeEvent): Promise<void> {
|
||||
const possibleRepositoryFolders = added
|
||||
.filter(folder => !this.getOpenRepository(folder.uri));
|
||||
|
||||
const activeRepositoriesList = window.visibleTextEditors
|
||||
.map(editor => this.getRepository(editor.document.uri))
|
||||
.filter(repository => !!repository) as Repository[];
|
||||
|
||||
const activeRepositories = new Set<Repository>(activeRepositoriesList);
|
||||
const openRepositoriesToDispose = removed
|
||||
.map(folder => this.getOpenRepository(folder.uri))
|
||||
.filter(r => !!r && !activeRepositories.has(r.repository)) as OpenRepository[];
|
||||
|
||||
possibleRepositoryFolders.forEach(p => this.tryOpenRepository(p.uri.fsPath));
|
||||
openRepositoriesToDispose.forEach(r => r.dispose());
|
||||
}
|
||||
|
||||
private onDidChangeVisibleTextEditors(editors: TextEditor[]): void {
|
||||
editors.forEach(editor => {
|
||||
const uri = editor.document.uri;
|
||||
|
||||
if (uri.scheme !== 'file') {
|
||||
return;
|
||||
}
|
||||
|
||||
const repository = this.getRepository(uri);
|
||||
|
||||
if (repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tryOpenRepository(path.dirname(uri.fsPath));
|
||||
});
|
||||
}
|
||||
|
||||
@sequentialize
|
||||
async tryOpenRepository(path: string): Promise<void> {
|
||||
if (this.getRepository(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const repositoryRoot = await this.git.getRepositoryRoot(path);
|
||||
|
||||
// This can happen whenever `path` has the wrong case sensitivity in
|
||||
// case insensitive file systems
|
||||
// https://github.com/Microsoft/vscode/issues/33498
|
||||
if (this.getRepository(repositoryRoot)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const repository = new Repository(this.git.open(repositoryRoot));
|
||||
|
||||
this.open(repository);
|
||||
} catch (err) {
|
||||
if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.error('Failed to find repository:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private open(repository: Repository): void {
|
||||
const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed);
|
||||
const disappearListener = onDidDisappearRepository(() => dispose());
|
||||
const changeListener = repository.onDidChangeRepository(uri => this._onDidChangeRepository.fire({ repository, uri }));
|
||||
const dispose = () => {
|
||||
disappearListener.dispose();
|
||||
changeListener.dispose();
|
||||
repository.dispose();
|
||||
this.openRepositories = this.openRepositories.filter(e => e !== openRepository);
|
||||
this._onDidCloseRepository.fire(repository);
|
||||
};
|
||||
|
||||
const openRepository = { repository, dispose };
|
||||
this.openRepositories.push(openRepository);
|
||||
this._onDidOpenRepository.fire(repository);
|
||||
}
|
||||
|
||||
async pickRepository(): Promise<Repository | undefined> {
|
||||
if (this.openRepositories.length === 0) {
|
||||
throw new Error(localize('no repositories', "There are no available repositories"));
|
||||
}
|
||||
|
||||
const picks = this.openRepositories.map(e => new RepositoryPick(e.repository));
|
||||
const placeHolder = localize('pick repo', "Choose a repository");
|
||||
const pick = await window.showQuickPick(picks, { placeHolder });
|
||||
|
||||
return pick && pick.repository;
|
||||
}
|
||||
|
||||
getRepository(sourceControl: SourceControl): Repository | undefined;
|
||||
getRepository(resourceGroup: SourceControlResourceGroup): Repository | undefined;
|
||||
getRepository(path: string): Repository | undefined;
|
||||
getRepository(resource: Uri): Repository | undefined;
|
||||
getRepository(hint: any): Repository | undefined {
|
||||
const liveRepository = this.getOpenRepository(hint);
|
||||
return liveRepository && liveRepository.repository;
|
||||
}
|
||||
|
||||
private getOpenRepository(repository: Repository): OpenRepository | undefined;
|
||||
private getOpenRepository(sourceControl: SourceControl): OpenRepository | undefined;
|
||||
private getOpenRepository(resourceGroup: SourceControlResourceGroup): OpenRepository | undefined;
|
||||
private getOpenRepository(path: string): OpenRepository | undefined;
|
||||
private getOpenRepository(resource: Uri): OpenRepository | undefined;
|
||||
private getOpenRepository(hint: any): OpenRepository | undefined {
|
||||
if (!hint) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (hint instanceof Repository) {
|
||||
return this.openRepositories.filter(r => r.repository === hint)[0];
|
||||
}
|
||||
|
||||
if (typeof hint === 'string') {
|
||||
hint = Uri.file(hint);
|
||||
}
|
||||
|
||||
if (hint instanceof Uri) {
|
||||
const resourcePath = hint.fsPath;
|
||||
|
||||
for (const liveRepository of this.openRepositories) {
|
||||
const relativePath = path.relative(liveRepository.repository.root, resourcePath);
|
||||
|
||||
if (!/^\.\./.test(relativePath)) {
|
||||
return liveRepository;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const liveRepository of this.openRepositories) {
|
||||
const repository = liveRepository.repository;
|
||||
|
||||
if (hint === repository.sourceControl) {
|
||||
return liveRepository;
|
||||
}
|
||||
|
||||
if (hint === repository.mergeGroup || hint === repository.indexGroup || hint === repository.workingTreeGroup) {
|
||||
return liveRepository;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disable();
|
||||
this.configurationChangeDisposable.dispose();
|
||||
}
|
||||
}
|
||||
841
extensions/git/src/repository.ts
Normal file
841
extensions/git/src/repository.ts
Normal file
@@ -0,0 +1,841 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, Disposable, ProgressLocation, window, workspace, WorkspaceEdit } from 'vscode';
|
||||
import { Repository as BaseRepository, Ref, Branch, Remote, Commit, GitErrorCodes, Stash } from './git';
|
||||
import { anyEvent, filterEvent, eventToPromise, dispose, find } 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 { StatusBarCommands } from './statusbar';
|
||||
|
||||
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const iconsRootPath = path.join(path.dirname(__dirname), 'resources', 'icons');
|
||||
|
||||
function getIconUri(iconName: string, theme: string): Uri {
|
||||
return Uri.file(path.join(iconsRootPath, theme, `${iconName}.svg`));
|
||||
}
|
||||
|
||||
export enum RepositoryState {
|
||||
Idle,
|
||||
Disposed
|
||||
}
|
||||
|
||||
export enum Status {
|
||||
INDEX_MODIFIED,
|
||||
INDEX_ADDED,
|
||||
INDEX_DELETED,
|
||||
INDEX_RENAMED,
|
||||
INDEX_COPIED,
|
||||
|
||||
MODIFIED,
|
||||
DELETED,
|
||||
UNTRACKED,
|
||||
IGNORED,
|
||||
|
||||
ADDED_BY_US,
|
||||
ADDED_BY_THEM,
|
||||
DELETED_BY_US,
|
||||
DELETED_BY_THEM,
|
||||
BOTH_ADDED,
|
||||
BOTH_DELETED,
|
||||
BOTH_MODIFIED
|
||||
}
|
||||
|
||||
export enum ResourceGroupType {
|
||||
Merge,
|
||||
Index,
|
||||
WorkingTree
|
||||
}
|
||||
|
||||
export class Resource implements SourceControlResourceState {
|
||||
|
||||
@memoize
|
||||
get resourceUri(): Uri {
|
||||
if (this.renameResourceUri && (this._type === Status.MODIFIED || this._type === Status.DELETED || this._type === Status.INDEX_RENAMED || this._type === Status.INDEX_COPIED)) {
|
||||
return this.renameResourceUri;
|
||||
}
|
||||
|
||||
return this._resourceUri;
|
||||
}
|
||||
|
||||
@memoize
|
||||
get command(): Command {
|
||||
return {
|
||||
command: 'git.openResource',
|
||||
title: localize('open', "Open"),
|
||||
arguments: [this]
|
||||
};
|
||||
}
|
||||
|
||||
get resourceGroupType(): ResourceGroupType { return this._resourceGroupType; }
|
||||
get type(): Status { return this._type; }
|
||||
get original(): Uri { return this._resourceUri; }
|
||||
get renameResourceUri(): Uri | undefined { return this._renameResourceUri; }
|
||||
|
||||
private static Icons = {
|
||||
light: {
|
||||
Modified: getIconUri('status-modified', 'light'),
|
||||
Added: getIconUri('status-added', 'light'),
|
||||
Deleted: getIconUri('status-deleted', 'light'),
|
||||
Renamed: getIconUri('status-renamed', 'light'),
|
||||
Copied: getIconUri('status-copied', 'light'),
|
||||
Untracked: getIconUri('status-untracked', 'light'),
|
||||
Ignored: getIconUri('status-ignored', 'light'),
|
||||
Conflict: getIconUri('status-conflict', 'light'),
|
||||
},
|
||||
dark: {
|
||||
Modified: getIconUri('status-modified', 'dark'),
|
||||
Added: getIconUri('status-added', 'dark'),
|
||||
Deleted: getIconUri('status-deleted', 'dark'),
|
||||
Renamed: getIconUri('status-renamed', 'dark'),
|
||||
Copied: getIconUri('status-copied', 'dark'),
|
||||
Untracked: getIconUri('status-untracked', 'dark'),
|
||||
Ignored: getIconUri('status-ignored', 'dark'),
|
||||
Conflict: getIconUri('status-conflict', 'dark')
|
||||
}
|
||||
};
|
||||
|
||||
private getIconPath(theme: string): Uri | undefined {
|
||||
switch (this.type) {
|
||||
case Status.INDEX_MODIFIED: return Resource.Icons[theme].Modified;
|
||||
case Status.MODIFIED: return Resource.Icons[theme].Modified;
|
||||
case Status.INDEX_ADDED: return Resource.Icons[theme].Added;
|
||||
case Status.INDEX_DELETED: return Resource.Icons[theme].Deleted;
|
||||
case Status.DELETED: return Resource.Icons[theme].Deleted;
|
||||
case Status.INDEX_RENAMED: return Resource.Icons[theme].Renamed;
|
||||
case Status.INDEX_COPIED: return Resource.Icons[theme].Copied;
|
||||
case Status.UNTRACKED: return Resource.Icons[theme].Untracked;
|
||||
case Status.IGNORED: return Resource.Icons[theme].Ignored;
|
||||
case Status.BOTH_DELETED: return Resource.Icons[theme].Conflict;
|
||||
case Status.ADDED_BY_US: return Resource.Icons[theme].Conflict;
|
||||
case Status.DELETED_BY_THEM: return Resource.Icons[theme].Conflict;
|
||||
case Status.ADDED_BY_THEM: return Resource.Icons[theme].Conflict;
|
||||
case Status.DELETED_BY_US: return Resource.Icons[theme].Conflict;
|
||||
case Status.BOTH_ADDED: return Resource.Icons[theme].Conflict;
|
||||
case Status.BOTH_MODIFIED: return Resource.Icons[theme].Conflict;
|
||||
default: return void 0;
|
||||
}
|
||||
}
|
||||
|
||||
private get tooltip(): string {
|
||||
switch (this.type) {
|
||||
case Status.INDEX_MODIFIED: return localize('index modified', "Index Modified");
|
||||
case Status.MODIFIED: return localize('modified', "Modified");
|
||||
case Status.INDEX_ADDED: return localize('index added', "Index Added");
|
||||
case Status.INDEX_DELETED: return localize('index deleted', "Index Deleted");
|
||||
case Status.DELETED: return localize('deleted', "Deleted");
|
||||
case Status.INDEX_RENAMED: return localize('index renamed', "Index Renamed");
|
||||
case Status.INDEX_COPIED: return localize('index copied', "Index Copied");
|
||||
case Status.UNTRACKED: return localize('untracked', "Untracked");
|
||||
case Status.IGNORED: return localize('ignored', "Ignored");
|
||||
case Status.BOTH_DELETED: return localize('both deleted', "Both Deleted");
|
||||
case Status.ADDED_BY_US: return localize('added by us', "Added By Us");
|
||||
case Status.DELETED_BY_THEM: return localize('deleted by them', "Deleted By Them");
|
||||
case Status.ADDED_BY_THEM: return localize('added by them', "Added By Them");
|
||||
case Status.DELETED_BY_US: return localize('deleted by us', "Deleted By Us");
|
||||
case Status.BOTH_ADDED: return localize('both added', "Both Added");
|
||||
case Status.BOTH_MODIFIED: return localize('both modified', "Both Modified");
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
private get strikeThrough(): boolean {
|
||||
switch (this.type) {
|
||||
case Status.DELETED:
|
||||
case Status.BOTH_DELETED:
|
||||
case Status.DELETED_BY_THEM:
|
||||
case Status.DELETED_BY_US:
|
||||
case Status.INDEX_DELETED:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get faded(): boolean {
|
||||
// TODO@joao
|
||||
return false;
|
||||
// const workspaceRootPath = this.workspaceRoot.fsPath;
|
||||
// return this.resourceUri.fsPath.substr(0, workspaceRootPath.length) !== workspaceRootPath;
|
||||
}
|
||||
|
||||
get decorations(): SourceControlResourceDecorations {
|
||||
const light = { iconPath: this.getIconPath('light') };
|
||||
const dark = { iconPath: this.getIconPath('dark') };
|
||||
const tooltip = this.tooltip;
|
||||
const strikeThrough = this.strikeThrough;
|
||||
const faded = this.faded;
|
||||
|
||||
return { strikeThrough, faded, tooltip, light, dark };
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _resourceGroupType: ResourceGroupType,
|
||||
private _resourceUri: Uri,
|
||||
private _type: Status,
|
||||
private _renameResourceUri?: Uri
|
||||
) { }
|
||||
}
|
||||
|
||||
export enum Operation {
|
||||
Status = 1 << 0,
|
||||
Add = 1 << 1,
|
||||
RevertFiles = 1 << 2,
|
||||
Commit = 1 << 3,
|
||||
Clean = 1 << 4,
|
||||
Branch = 1 << 5,
|
||||
Checkout = 1 << 6,
|
||||
Reset = 1 << 7,
|
||||
Fetch = 1 << 8,
|
||||
Pull = 1 << 9,
|
||||
Push = 1 << 10,
|
||||
Sync = 1 << 11,
|
||||
Show = 1 << 12,
|
||||
Stage = 1 << 13,
|
||||
GetCommitTemplate = 1 << 14,
|
||||
DeleteBranch = 1 << 15,
|
||||
Merge = 1 << 16,
|
||||
Ignore = 1 << 17,
|
||||
Tag = 1 << 18,
|
||||
Stash = 1 << 19
|
||||
}
|
||||
|
||||
// function getOperationName(operation: Operation): string {
|
||||
// switch (operation) {
|
||||
// case Operation.Status: return 'Status';
|
||||
// case Operation.Add: return 'Add';
|
||||
// case Operation.RevertFiles: return 'RevertFiles';
|
||||
// case Operation.Commit: return 'Commit';
|
||||
// case Operation.Clean: return 'Clean';
|
||||
// case Operation.Branch: return 'Branch';
|
||||
// case Operation.Checkout: return 'Checkout';
|
||||
// case Operation.Reset: return 'Reset';
|
||||
// case Operation.Fetch: return 'Fetch';
|
||||
// case Operation.Pull: return 'Pull';
|
||||
// case Operation.Push: return 'Push';
|
||||
// case Operation.Sync: return 'Sync';
|
||||
// case Operation.Init: return 'Init';
|
||||
// case Operation.Show: return 'Show';
|
||||
// case Operation.Stage: return 'Stage';
|
||||
// case Operation.GetCommitTemplate: return 'GetCommitTemplate';
|
||||
// default: return 'unknown';
|
||||
// }
|
||||
// }
|
||||
|
||||
function isReadOnly(operation: Operation): boolean {
|
||||
switch (operation) {
|
||||
case Operation.Show:
|
||||
case Operation.GetCommitTemplate:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldShowProgress(operation: Operation): boolean {
|
||||
switch (operation) {
|
||||
case Operation.Fetch:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Operations {
|
||||
isIdle(): boolean;
|
||||
isRunning(operation: Operation): boolean;
|
||||
}
|
||||
|
||||
class OperationsImpl implements Operations {
|
||||
|
||||
constructor(private readonly operations: number = 0) {
|
||||
// noop
|
||||
}
|
||||
|
||||
start(operation: Operation): OperationsImpl {
|
||||
return new OperationsImpl(this.operations | operation);
|
||||
}
|
||||
|
||||
end(operation: Operation): OperationsImpl {
|
||||
return new OperationsImpl(this.operations & ~operation);
|
||||
}
|
||||
|
||||
isRunning(operation: Operation): boolean {
|
||||
return (this.operations & operation) !== 0;
|
||||
}
|
||||
|
||||
isIdle(): boolean {
|
||||
return this.operations === 0;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommitOptions {
|
||||
all?: boolean;
|
||||
amend?: boolean;
|
||||
signoff?: boolean;
|
||||
signCommit?: boolean;
|
||||
}
|
||||
|
||||
export interface GitResourceGroup extends SourceControlResourceGroup {
|
||||
resourceStates: Resource[];
|
||||
}
|
||||
|
||||
export class Repository implements Disposable {
|
||||
|
||||
private _onDidChangeRepository = new EventEmitter<Uri>();
|
||||
readonly onDidChangeRepository: Event<Uri> = this._onDidChangeRepository.event;
|
||||
|
||||
private _onDidChangeState = new EventEmitter<RepositoryState>();
|
||||
readonly onDidChangeState: Event<RepositoryState> = this._onDidChangeState.event;
|
||||
|
||||
private _onDidChangeStatus = new EventEmitter<void>();
|
||||
readonly onDidChangeStatus: Event<void> = this._onDidChangeStatus.event;
|
||||
|
||||
private _onRunOperation = new EventEmitter<Operation>();
|
||||
readonly onRunOperation: Event<Operation> = this._onRunOperation.event;
|
||||
|
||||
private _onDidRunOperation = new EventEmitter<Operation>();
|
||||
readonly onDidRunOperation: Event<Operation> = this._onDidRunOperation.event;
|
||||
|
||||
@memoize
|
||||
get onDidChangeOperations(): Event<void> {
|
||||
return anyEvent(this.onRunOperation as Event<any>, this.onDidRunOperation as Event<any>);
|
||||
}
|
||||
|
||||
private _sourceControl: SourceControl;
|
||||
get sourceControl(): SourceControl { return this._sourceControl; }
|
||||
|
||||
get inputBox(): SourceControlInputBox { return this._sourceControl.inputBox; }
|
||||
|
||||
private _mergeGroup: SourceControlResourceGroup;
|
||||
get mergeGroup(): GitResourceGroup { return this._mergeGroup as GitResourceGroup; }
|
||||
|
||||
private _indexGroup: SourceControlResourceGroup;
|
||||
get indexGroup(): GitResourceGroup { return this._indexGroup as GitResourceGroup; }
|
||||
|
||||
private _workingTreeGroup: SourceControlResourceGroup;
|
||||
get workingTreeGroup(): GitResourceGroup { return this._workingTreeGroup as GitResourceGroup; }
|
||||
|
||||
private _HEAD: Branch | undefined;
|
||||
get HEAD(): Branch | undefined {
|
||||
return this._HEAD;
|
||||
}
|
||||
|
||||
private _refs: Ref[] = [];
|
||||
get refs(): Ref[] {
|
||||
return this._refs;
|
||||
}
|
||||
|
||||
private _remotes: Remote[] = [];
|
||||
get remotes(): Remote[] {
|
||||
return this._remotes;
|
||||
}
|
||||
|
||||
private _operations = new OperationsImpl();
|
||||
get operations(): Operations { return this._operations; }
|
||||
|
||||
private _state = RepositoryState.Idle;
|
||||
get state(): RepositoryState { return this._state; }
|
||||
set state(state: RepositoryState) {
|
||||
this._state = state;
|
||||
this._onDidChangeState.fire(state);
|
||||
|
||||
this._HEAD = undefined;
|
||||
this._refs = [];
|
||||
this._remotes = [];
|
||||
this.mergeGroup.resourceStates = [];
|
||||
this.indexGroup.resourceStates = [];
|
||||
this.workingTreeGroup.resourceStates = [];
|
||||
this._sourceControl.count = 0;
|
||||
}
|
||||
|
||||
get root(): string {
|
||||
return this.repository.root;
|
||||
}
|
||||
|
||||
private isRepositoryHuge = false;
|
||||
private didWarnAboutLimit = false;
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly repository: BaseRepository
|
||||
) {
|
||||
const fsWatcher = workspace.createFileSystemWatcher('**');
|
||||
this.disposables.push(fsWatcher);
|
||||
|
||||
const onWorkspaceChange = anyEvent(fsWatcher.onDidChange, fsWatcher.onDidCreate, fsWatcher.onDidDelete);
|
||||
const onRepositoryChange = filterEvent(onWorkspaceChange, uri => !/^\.\./.test(path.relative(repository.root, uri.fsPath)));
|
||||
const onRelevantRepositoryChange = filterEvent(onRepositoryChange, uri => !/\/\.git\/index\.lock$/.test(uri.path));
|
||||
onRelevantRepositoryChange(this.onFSChange, this, this.disposables);
|
||||
|
||||
const onRelevantGitChange = filterEvent(onRelevantRepositoryChange, uri => /\/\.git\//.test(uri.path));
|
||||
onRelevantGitChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables);
|
||||
|
||||
const label = `${path.basename(repository.root)} (Git)`;
|
||||
|
||||
this._sourceControl = scm.createSourceControl('git', label);
|
||||
this._sourceControl.acceptInputCommand = { command: 'git.commitWithInput', title: localize('commit', "Commit"), arguments: [this._sourceControl] };
|
||||
this._sourceControl.quickDiffProvider = this;
|
||||
this.disposables.push(this._sourceControl);
|
||||
|
||||
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.mergeGroup.hideWhenEmpty = true;
|
||||
this.indexGroup.hideWhenEmpty = true;
|
||||
|
||||
this.disposables.push(this.mergeGroup);
|
||||
this.disposables.push(this.indexGroup);
|
||||
this.disposables.push(this.workingTreeGroup);
|
||||
|
||||
this.disposables.push(new AutoFetcher(this));
|
||||
|
||||
const statusBar = new StatusBarCommands(this);
|
||||
this.disposables.push(statusBar);
|
||||
statusBar.onDidChange(() => this._sourceControl.statusBarCommands = statusBar.commands, null, this.disposables);
|
||||
this._sourceControl.statusBarCommands = statusBar.commands;
|
||||
|
||||
this.updateCommitTemplate();
|
||||
this.status();
|
||||
}
|
||||
|
||||
provideOriginalResource(uri: Uri): Uri | undefined {
|
||||
if (uri.scheme !== 'file') {
|
||||
return;
|
||||
}
|
||||
|
||||
return toGitUri(uri, '', true);
|
||||
}
|
||||
|
||||
private async updateCommitTemplate(): Promise<void> {
|
||||
try {
|
||||
this._sourceControl.commitTemplate = await this.repository.getCommitTemplate();
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
// @throttle
|
||||
// async init(): Promise<void> {
|
||||
// if (this.state !== State.NotAGitRepository) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// await this.git.init(this.workspaceRoot.fsPath);
|
||||
// await this.status();
|
||||
// }
|
||||
|
||||
@throttle
|
||||
async status(): Promise<void> {
|
||||
await this.run(Operation.Status);
|
||||
}
|
||||
|
||||
async add(resources: Uri[]): Promise<void> {
|
||||
await this.run(Operation.Add, () => this.repository.add(resources.map(r => r.fsPath)));
|
||||
}
|
||||
|
||||
async stage(resource: Uri, contents: string): Promise<void> {
|
||||
const relativePath = path.relative(this.repository.root, resource.fsPath).replace(/\\/g, '/');
|
||||
await this.run(Operation.Stage, () => this.repository.stage(relativePath, contents));
|
||||
}
|
||||
|
||||
async revert(resources: Uri[]): Promise<void> {
|
||||
await this.run(Operation.RevertFiles, () => this.repository.revert('HEAD', resources.map(r => r.fsPath)));
|
||||
}
|
||||
|
||||
async commit(message: string, opts: CommitOptions = Object.create(null)): Promise<void> {
|
||||
await this.run(Operation.Commit, async () => {
|
||||
if (opts.all) {
|
||||
await this.repository.add([]);
|
||||
}
|
||||
|
||||
await this.repository.commit(message, opts);
|
||||
});
|
||||
}
|
||||
|
||||
async clean(resources: Uri[]): Promise<void> {
|
||||
await this.run(Operation.Clean, async () => {
|
||||
const toClean: string[] = [];
|
||||
const toCheckout: string[] = [];
|
||||
|
||||
resources.forEach(r => {
|
||||
const raw = r.toString();
|
||||
const scmResource = find(this.workingTreeGroup.resourceStates, sr => sr.resourceUri.toString() === raw);
|
||||
|
||||
if (!scmResource) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (scmResource.type) {
|
||||
case Status.UNTRACKED:
|
||||
case Status.IGNORED:
|
||||
toClean.push(r.fsPath);
|
||||
break;
|
||||
|
||||
default:
|
||||
toCheckout.push(r.fsPath);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
if (toClean.length > 0) {
|
||||
promises.push(this.repository.clean(toClean));
|
||||
}
|
||||
|
||||
if (toCheckout.length > 0) {
|
||||
promises.push(this.repository.checkout('', toCheckout));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
});
|
||||
}
|
||||
|
||||
async branch(name: string): Promise<void> {
|
||||
await this.run(Operation.Branch, () => this.repository.branch(name, true));
|
||||
}
|
||||
|
||||
async deleteBranch(name: string, force?: boolean): Promise<void> {
|
||||
await this.run(Operation.DeleteBranch, () => this.repository.deleteBranch(name, force));
|
||||
}
|
||||
|
||||
async merge(ref: string): Promise<void> {
|
||||
await this.run(Operation.Merge, () => this.repository.merge(ref));
|
||||
}
|
||||
|
||||
async tag(name: string, message?: string): Promise<void> {
|
||||
await this.run(Operation.Tag, () => this.repository.tag(name, message));
|
||||
}
|
||||
|
||||
async checkout(treeish: string): Promise<void> {
|
||||
await this.run(Operation.Checkout, () => this.repository.checkout(treeish, []));
|
||||
}
|
||||
|
||||
async getCommit(ref: string): Promise<Commit> {
|
||||
return await this.repository.getCommit(ref);
|
||||
}
|
||||
|
||||
async reset(treeish: string, hard?: boolean): Promise<void> {
|
||||
await this.run(Operation.Reset, () => this.repository.reset(treeish, hard));
|
||||
}
|
||||
|
||||
@throttle
|
||||
async fetch(): Promise<void> {
|
||||
try {
|
||||
await this.run(Operation.Fetch, () => this.repository.fetch());
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
@throttle
|
||||
async pullWithRebase(): Promise<void> {
|
||||
await this.run(Operation.Pull, () => this.repository.pull(true));
|
||||
}
|
||||
|
||||
@throttle
|
||||
async pull(rebase?: boolean, remote?: string, name?: string): Promise<void> {
|
||||
await this.run(Operation.Pull, () => this.repository.pull(rebase, remote, name));
|
||||
}
|
||||
|
||||
@throttle
|
||||
async push(): Promise<void> {
|
||||
await this.run(Operation.Push, () => this.repository.push());
|
||||
}
|
||||
|
||||
async pullFrom(rebase?: boolean, remote?: string, branch?: string): Promise<void> {
|
||||
await this.run(Operation.Pull, () => this.repository.pull(rebase, remote, branch));
|
||||
}
|
||||
|
||||
async pushTo(remote?: string, name?: string, setUpstream: boolean = false): Promise<void> {
|
||||
await this.run(Operation.Push, () => this.repository.push(remote, name, setUpstream));
|
||||
}
|
||||
|
||||
async pushTags(remote?: string): Promise<void> {
|
||||
await this.run(Operation.Push, () => this.repository.push(remote, undefined, false, true));
|
||||
}
|
||||
|
||||
@throttle
|
||||
async sync(): Promise<void> {
|
||||
await this.run(Operation.Sync, async () => {
|
||||
await this.repository.pull();
|
||||
|
||||
const shouldPush = this.HEAD && typeof this.HEAD.ahead === 'number' ? this.HEAD.ahead > 0 : true;
|
||||
|
||||
if (shouldPush) {
|
||||
await this.repository.push();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async show(ref: string, filePath: string): Promise<string> {
|
||||
return await this.run(Operation.Show, async () => {
|
||||
const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/');
|
||||
const configFiles = workspace.getConfiguration('files');
|
||||
const encoding = configFiles.get<string>('encoding');
|
||||
|
||||
return await this.repository.buffer(`${ref}:${relativePath}`, encoding);
|
||||
});
|
||||
}
|
||||
|
||||
async getStashes(): Promise<Stash[]> {
|
||||
return await this.repository.getStashes();
|
||||
}
|
||||
|
||||
async createStash(message?: string): Promise<void> {
|
||||
return await this.run(Operation.Stash, () => this.repository.createStash(message));
|
||||
}
|
||||
|
||||
async popStash(index?: number): Promise<void> {
|
||||
return await this.run(Operation.Stash, () => this.repository.popStash(index));
|
||||
}
|
||||
|
||||
async getCommitTemplate(): Promise<string> {
|
||||
return await this.run(Operation.GetCommitTemplate, async () => this.repository.getCommitTemplate());
|
||||
}
|
||||
|
||||
async ignore(files: Uri[]): Promise<void> {
|
||||
return await this.run(Operation.Ignore, async () => {
|
||||
const ignoreFile = `${this.repository.root}${path.sep}.gitignore`;
|
||||
const textToAppend = files
|
||||
.map(uri => path.relative(this.repository.root, uri.fsPath).replace(/\\/g, '/'))
|
||||
.join('\n');
|
||||
|
||||
const document = await new Promise(c => fs.exists(ignoreFile, c))
|
||||
? await workspace.openTextDocument(ignoreFile)
|
||||
: await workspace.openTextDocument(Uri.file(ignoreFile).with({ scheme: 'untitled' }));
|
||||
|
||||
await window.showTextDocument(document);
|
||||
|
||||
const edit = new WorkspaceEdit();
|
||||
const lastLine = document.lineAt(document.lineCount - 1);
|
||||
const text = lastLine.isEmptyOrWhitespace ? `${textToAppend}\n` : `\n${textToAppend}\n`;
|
||||
|
||||
edit.insert(document.uri, lastLine.range.end, text);
|
||||
workspace.applyEdit(edit);
|
||||
});
|
||||
}
|
||||
|
||||
private async run<T>(operation: Operation, runOperation: () => Promise<T> = () => Promise.resolve<any>(null)): Promise<T> {
|
||||
if (this.state !== RepositoryState.Idle) {
|
||||
throw new Error('Repository not initialized');
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
this._operations = this._operations.start(operation);
|
||||
this._onRunOperation.fire(operation);
|
||||
|
||||
try {
|
||||
const result = await this.retryRun(runOperation);
|
||||
|
||||
if (!isReadOnly(operation)) {
|
||||
await this.updateModelState();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) {
|
||||
this.state = RepositoryState.Disposed;
|
||||
}
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
this._operations = this._operations.end(operation);
|
||||
this._onDidRunOperation.fire(operation);
|
||||
}
|
||||
};
|
||||
|
||||
return shouldShowProgress(operation)
|
||||
? window.withProgress({ location: ProgressLocation.SourceControl }, run)
|
||||
: run();
|
||||
}
|
||||
|
||||
private async retryRun<T>(runOperation: () => Promise<T> = () => Promise.resolve<any>(null)): Promise<T> {
|
||||
let attempt = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
attempt++;
|
||||
return await runOperation();
|
||||
} catch (err) {
|
||||
if (err.gitErrorCode === GitErrorCodes.RepositoryIsLocked && attempt <= 10) {
|
||||
// quatratic backoff
|
||||
await timeout(Math.pow(attempt, 2) * 50);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@throttle
|
||||
private async updateModelState(): Promise<void> {
|
||||
const { status, didHitLimit } = await this.repository.getStatus();
|
||||
const config = workspace.getConfiguration('git');
|
||||
const shouldIgnore = config.get<boolean>('ignoreLimitWarning') === true;
|
||||
|
||||
this.isRepositoryHuge = didHitLimit;
|
||||
|
||||
if (didHitLimit && !shouldIgnore && !this.didWarnAboutLimit) {
|
||||
const ok = { title: localize('ok', "OK"), isCloseAffordance: true };
|
||||
const neverAgain = { title: localize('neveragain', "Never Show Again") };
|
||||
|
||||
window.showWarningMessage(localize('huge', "The git repository at '{0}' has too many active changes, only a subset of Git features will be enabled.", this.repository.root), ok, neverAgain).then(result => {
|
||||
if (result === neverAgain) {
|
||||
config.update('ignoreLimitWarning', true, false);
|
||||
}
|
||||
});
|
||||
|
||||
this.didWarnAboutLimit = true;
|
||||
}
|
||||
|
||||
let HEAD: Branch | undefined;
|
||||
|
||||
try {
|
||||
HEAD = await this.repository.getHEAD();
|
||||
|
||||
if (HEAD.name) {
|
||||
try {
|
||||
HEAD = await this.repository.getBranch(HEAD.name);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
|
||||
const [refs, remotes] = await Promise.all([this.repository.getRefs(), this.repository.getRemotes()]);
|
||||
|
||||
this._HEAD = HEAD;
|
||||
this._refs = refs;
|
||||
this._remotes = remotes;
|
||||
|
||||
const index: Resource[] = [];
|
||||
const workingTree: Resource[] = [];
|
||||
const merge: 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;
|
||||
|
||||
switch (raw.x + raw.y) {
|
||||
case '??': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.UNTRACKED));
|
||||
case '!!': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.IGNORED));
|
||||
case 'DD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_DELETED));
|
||||
case 'AU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.ADDED_BY_US));
|
||||
case 'UD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.DELETED_BY_THEM));
|
||||
case 'UA': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.ADDED_BY_THEM));
|
||||
case 'DU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.DELETED_BY_US));
|
||||
case 'AA': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_ADDED));
|
||||
case 'UU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_MODIFIED));
|
||||
}
|
||||
|
||||
let isModifiedInIndex = false;
|
||||
|
||||
switch (raw.x) {
|
||||
case 'M': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_MODIFIED)); isModifiedInIndex = true; break;
|
||||
case 'A': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_ADDED)); break;
|
||||
case 'D': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_DELETED)); break;
|
||||
case 'R': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_RENAMED, renameUri)); break;
|
||||
case 'C': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_COPIED, renameUri)); break;
|
||||
}
|
||||
|
||||
switch (raw.y) {
|
||||
case 'M': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.MODIFIED, renameUri)); break;
|
||||
case 'D': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.DELETED, renameUri)); break;
|
||||
}
|
||||
});
|
||||
|
||||
// set resource groups
|
||||
this.mergeGroup.resourceStates = merge;
|
||||
this.indexGroup.resourceStates = index;
|
||||
this.workingTreeGroup.resourceStates = workingTree;
|
||||
|
||||
// set count badge
|
||||
const countBadge = workspace.getConfiguration('git').get<string>('countBadge');
|
||||
let count = merge.length + index.length + workingTree.length;
|
||||
|
||||
switch (countBadge) {
|
||||
case 'off': count = 0; break;
|
||||
case 'tracked': count = count - workingTree.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED).length; break;
|
||||
}
|
||||
|
||||
this._sourceControl.count = count;
|
||||
|
||||
// set context key
|
||||
let stateContextKey = '';
|
||||
|
||||
switch (this.state) {
|
||||
case RepositoryState.Idle: stateContextKey = 'idle'; break;
|
||||
case RepositoryState.Disposed: stateContextKey = 'norepo'; break;
|
||||
}
|
||||
|
||||
this._onDidChangeStatus.fire();
|
||||
}
|
||||
|
||||
private onFSChange(uri: Uri): void {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const autorefresh = config.get<boolean>('autorefresh');
|
||||
|
||||
if (!autorefresh) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isRepositoryHuge) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.operations.isIdle()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventuallyUpdateWhenIdleAndWait();
|
||||
}
|
||||
|
||||
@debounce(1000)
|
||||
private eventuallyUpdateWhenIdleAndWait(): void {
|
||||
this.updateWhenIdleAndWait();
|
||||
}
|
||||
|
||||
@throttle
|
||||
private async updateWhenIdleAndWait(): Promise<void> {
|
||||
await this.whenIdleAndFocused();
|
||||
await this.status();
|
||||
await timeout(5000);
|
||||
}
|
||||
|
||||
private async whenIdleAndFocused(): Promise<void> {
|
||||
while (true) {
|
||||
if (!this.operations.isIdle()) {
|
||||
await eventToPromise(this.onDidRunOperation);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!window.state.focused) {
|
||||
const onDidFocusWindow = filterEvent(window.onDidChangeWindowState, e => e.focused);
|
||||
await eventToPromise(onDidFocusWindow);
|
||||
continue;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
}
|
||||
108
extensions/git/src/staging.ts
Normal file
108
extensions/git/src/staging.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { TextDocument, Range, LineChange, Selection } from 'vscode';
|
||||
|
||||
export function applyLineChanges(original: TextDocument, modified: TextDocument, diffs: LineChange[]): string {
|
||||
const result: string[] = [];
|
||||
let currentLine = 0;
|
||||
|
||||
for (let diff of diffs) {
|
||||
const isInsertion = diff.originalEndLineNumber === 0;
|
||||
const isDeletion = diff.modifiedEndLineNumber === 0;
|
||||
|
||||
result.push(original.getText(new Range(currentLine, 0, isInsertion ? diff.originalStartLineNumber : diff.originalStartLineNumber - 1, 0)));
|
||||
|
||||
if (!isDeletion) {
|
||||
let fromLine = diff.modifiedStartLineNumber - 1;
|
||||
let fromCharacter = 0;
|
||||
|
||||
if (isInsertion && diff.originalStartLineNumber === original.lineCount) {
|
||||
fromLine = original.lineCount - 1;
|
||||
fromCharacter = original.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: 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;
|
||||
}
|
||||
|
||||
function getModifiedRange(textDocument: TextDocument, diff: LineChange): Range {
|
||||
return diff.modifiedEndLineNumber === 0
|
||||
? new Range(textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end, textDocument.lineAt(diff.modifiedStartLineNumber).range.start)
|
||||
: 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 {
|
||||
return {
|
||||
originalStartLineNumber: diff.originalStartLineNumber,
|
||||
originalEndLineNumber: diff.originalEndLineNumber,
|
||||
modifiedStartLineNumber: intersection.start.line + 1,
|
||||
modifiedEndLineNumber: intersection.end.line + 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function invertLineChange(diff: LineChange): LineChange {
|
||||
return {
|
||||
modifiedStartLineNumber: diff.originalStartLineNumber,
|
||||
modifiedEndLineNumber: diff.originalEndLineNumber,
|
||||
originalStartLineNumber: diff.modifiedStartLineNumber,
|
||||
originalEndLineNumber: diff.modifiedEndLineNumber
|
||||
};
|
||||
}
|
||||
189
extensions/git/src/statusbar.ts
Normal file
189
extensions/git/src/statusbar.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Disposable, Command, EventEmitter, Event } from 'vscode';
|
||||
import { RefType, Branch } from './git';
|
||||
import { Repository, Operation } from './repository';
|
||||
import { anyEvent, dispose } from './util';
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
class CheckoutStatusBar {
|
||||
|
||||
private _onDidChange = new EventEmitter<void>();
|
||||
get onDidChange(): Event<void> { return this._onDidChange.event; }
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(private repository: Repository) {
|
||||
repository.onDidChangeStatus(this._onDidChange.fire, this._onDidChange, this.disposables);
|
||||
}
|
||||
|
||||
get command(): Command | undefined {
|
||||
const HEAD = this.repository.HEAD;
|
||||
|
||||
if (!HEAD) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tag = this.repository.refs.filter(iref => iref.type === RefType.Tag && iref.commit === HEAD.commit)[0];
|
||||
const tagName = tag && tag.name;
|
||||
const head = HEAD.name || tagName || (HEAD.commit || '').substr(0, 8);
|
||||
const title = '$(git-branch) '
|
||||
+ head
|
||||
+ (this.repository.workingTreeGroup.resourceStates.length > 0 ? '*' : '')
|
||||
+ (this.repository.indexGroup.resourceStates.length > 0 ? '+' : '')
|
||||
+ (this.repository.mergeGroup.resourceStates.length > 0 ? '!' : '');
|
||||
|
||||
return {
|
||||
command: 'git.checkout',
|
||||
tooltip: localize('checkout', 'Checkout...'),
|
||||
title,
|
||||
arguments: [this.repository.sourceControl]
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
interface SyncStatusBarState {
|
||||
isSyncRunning: boolean;
|
||||
hasRemotes: boolean;
|
||||
HEAD: Branch | undefined;
|
||||
}
|
||||
|
||||
class SyncStatusBar {
|
||||
|
||||
private static StartState: SyncStatusBarState = {
|
||||
isSyncRunning: false,
|
||||
hasRemotes: false,
|
||||
HEAD: undefined
|
||||
};
|
||||
|
||||
private _onDidChange = new EventEmitter<void>();
|
||||
get onDidChange(): Event<void> { return this._onDidChange.event; }
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
private _state: SyncStatusBarState = SyncStatusBar.StartState;
|
||||
private get state() { return this._state; }
|
||||
private set state(state: SyncStatusBarState) {
|
||||
this._state = state;
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
constructor(private repository: Repository) {
|
||||
repository.onDidChangeStatus(this.onModelChange, this, this.disposables);
|
||||
repository.onDidChangeOperations(this.onOperationsChange, this, this.disposables);
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
private onOperationsChange(): void {
|
||||
this.state = {
|
||||
...this.state,
|
||||
isSyncRunning: this.repository.operations.isRunning(Operation.Sync)
|
||||
};
|
||||
}
|
||||
|
||||
private onModelChange(): void {
|
||||
this.state = {
|
||||
...this.state,
|
||||
hasRemotes: this.repository.remotes.length > 0,
|
||||
HEAD: this.repository.HEAD
|
||||
};
|
||||
}
|
||||
|
||||
get command(): Command | undefined {
|
||||
if (!this.state.hasRemotes) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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 += `${HEAD.behind}↓ ${HEAD.ahead}↑`;
|
||||
}
|
||||
command = 'git.sync';
|
||||
tooltip = localize('sync changes', "Synchronize Changes");
|
||||
} else {
|
||||
icon = '$(cloud-upload)';
|
||||
command = 'git.publish';
|
||||
tooltip = localize('publish changes', "Publish Changes");
|
||||
}
|
||||
} else {
|
||||
command = '';
|
||||
tooltip = '';
|
||||
}
|
||||
|
||||
if (this.state.isSyncRunning) {
|
||||
icon = '$(sync~spin)';
|
||||
command = '';
|
||||
tooltip = localize('syncing changes', "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 {
|
||||
|
||||
private syncStatusBar: SyncStatusBar;
|
||||
private checkoutStatusBar: CheckoutStatusBar;
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(repository: Repository) {
|
||||
this.syncStatusBar = new SyncStatusBar(repository);
|
||||
this.checkoutStatusBar = new CheckoutStatusBar(repository);
|
||||
}
|
||||
|
||||
get onDidChange(): Event<void> {
|
||||
return anyEvent(
|
||||
this.syncStatusBar.onDidChange,
|
||||
this.checkoutStatusBar.onDidChange
|
||||
);
|
||||
}
|
||||
|
||||
get commands(): Command[] {
|
||||
const result: Command[] = [];
|
||||
|
||||
const checkout = this.checkoutStatusBar.command;
|
||||
|
||||
if (checkout) {
|
||||
result.push(checkout);
|
||||
}
|
||||
|
||||
const sync = this.syncStatusBar.command;
|
||||
|
||||
if (sync) {
|
||||
result.push(sync);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.syncStatusBar.dispose();
|
||||
this.checkoutStatusBar.dispose();
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
}
|
||||
137
extensions/git/src/test/git.test.ts
Normal file
137
extensions/git/src/test/git.test.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { GitStatusParser } from '../git';
|
||||
import * as assert from 'assert';
|
||||
|
||||
suite('git', () => {
|
||||
suite('GitStatusParser', () => {
|
||||
test('empty parser', () => {
|
||||
const parser = new GitStatusParser();
|
||||
assert.deepEqual(parser.status, []);
|
||||
});
|
||||
|
||||
test('empty parser 2', () => {
|
||||
const parser = new GitStatusParser();
|
||||
parser.update('');
|
||||
assert.deepEqual(parser.status, []);
|
||||
});
|
||||
|
||||
test('simple', () => {
|
||||
const parser = new GitStatusParser();
|
||||
parser.update('?? file.txt\0');
|
||||
assert.deepEqual(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.deepEqual(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.deepEqual(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.deepEqual(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.deepEqual(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.deepEqual(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.deepEqual(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.deepEqual(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.deepEqual(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.deepEqual(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: '?' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
9
extensions/git/src/typings/refs.d.ts
vendored
Normal file
9
extensions/git/src/typings/refs.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
/// <reference types='@types/mocha'/>
|
||||
26
extensions/git/src/uri.ts
Normal file
26
extensions/git/src/uri.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Uri } from 'vscode';
|
||||
|
||||
export function fromGitUri(uri: Uri): { path: string; ref: string; } {
|
||||
return JSON.parse(uri.query);
|
||||
}
|
||||
|
||||
// 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, replaceFileExtension = false): Uri {
|
||||
return uri.with({
|
||||
scheme: 'git',
|
||||
path: replaceFileExtension ? `${uri.path}.git` : uri.path,
|
||||
query: JSON.stringify({
|
||||
path: uri.fsPath,
|
||||
ref
|
||||
})
|
||||
});
|
||||
}
|
||||
191
extensions/git/src/util.ts
Normal file
191
extensions/git/src/util.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Event } from 'vscode';
|
||||
import { dirname } from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
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 mapEvent<I, O>(event: Event<I>, map: (i: I) => O): Event<O> {
|
||||
return (listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables);
|
||||
}
|
||||
|
||||
export function filterEvent<T>(event: Event<T>, filter: (e: T) => boolean): Event<T> {
|
||||
return (listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
|
||||
}
|
||||
|
||||
export function anyEvent<T>(...events: Event<T>[]): Event<T> {
|
||||
return (listener, thisArgs = null, disposables?) => {
|
||||
const result = combinedDisposable(events.map(event => event(i => listener.call(thisArgs, i))));
|
||||
|
||||
if (disposables) {
|
||||
disposables.push(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export function done<T>(promise: Promise<T>): Promise<void> {
|
||||
return promise.then<void>(() => void 0);
|
||||
}
|
||||
|
||||
export function onceEvent<T>(event: Event<T>): Event<T> {
|
||||
return (listener, thisArgs = null, disposables?) => {
|
||||
const result = event(e => {
|
||||
result.dispose();
|
||||
return listener.call(thisArgs, e);
|
||||
}, null, disposables);
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
let 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[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 function denodeify<R>(fn: Function): (...args) => Promise<R> {
|
||||
return (...args) => new Promise<R>((c, e) => fn(...args, (err, r) => err ? e(err) : c(r)));
|
||||
}
|
||||
|
||||
export function nfcall<R>(fn: Function, ...args): Promise<R> {
|
||||
return new Promise<R>((c, e) => fn(...args, (err, r) => 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);
|
||||
} catch (err) {
|
||||
if (err.code === 'EEXIST') {
|
||||
const stat = await nfcall<fs.Stats>(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;
|
||||
}
|
||||
Reference in New Issue
Block a user