SQL Operations Studio Public Preview 1 (0.23) release source code

This commit is contained in:
Karl Burtram
2017-11-09 14:30:27 -08:00
parent b88ecb8d93
commit 3cdac41339
8829 changed files with 759707 additions and 286 deletions

View File

@@ -0,0 +1,2 @@
#!/bin/sh
echo ''

View 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);

View 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

View 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();
}
}

View 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());
}
}

File diff suppressed because it is too large Load Diff

View 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());
}
}

View 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

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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();
}
}

View 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);
}
}

View 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
};
}

View 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);
}
}

View 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
View 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
View 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
View 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;
}