mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-20 17:22:51 -05:00
* Initial 1.19 xcopy * Fix yarn build * Fix numerous build breaks * Next batch of build break fixes * More build break fixes * Runtime breaks * Additional post merge fixes * Fix windows setup file * Fix test failures. * Update license header blocks to refer to source eula
318 lines
11 KiB
TypeScript
318 lines
11 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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, Memento, ConfigurationChangeEvent } from 'vscode';
|
|
import { Repository, RepositoryState } from './repository';
|
|
import { memoize, sequentialize, debounce } from './decorators';
|
|
import { dispose, anyEvent, filterEvent, IDisposable, isDescendant } from './util';
|
|
import { Git, GitErrorCodes } from './git';
|
|
import * as path from 'path';
|
|
import * as fs from 'fs';
|
|
import * as nls from 'vscode-nls';
|
|
import { fromGitUri } from './uri';
|
|
|
|
const localize = nls.loadMessageBundle();
|
|
|
|
class RepositoryPick implements QuickPickItem {
|
|
@memoize get label(): string {
|
|
return path.basename(this.repository.root);
|
|
}
|
|
|
|
@memoize get description(): string {
|
|
return [this.repository.headLabel, this.repository.syncLabel]
|
|
.filter(l => !!l)
|
|
.join(' ');
|
|
}
|
|
|
|
constructor(public readonly repository: Repository) { }
|
|
}
|
|
|
|
export interface ModelChangeEvent {
|
|
repository: Repository;
|
|
uri: Uri;
|
|
}
|
|
|
|
export interface OriginalResourceChangeEvent {
|
|
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 _onDidChangeOriginalResource = new EventEmitter<OriginalResourceChangeEvent>();
|
|
readonly onDidChangeOriginalResource: Event<OriginalResourceChangeEvent> = this._onDidChangeOriginalResource.event;
|
|
|
|
private openRepositories: OpenRepository[] = [];
|
|
get repositories(): Repository[] { return this.openRepositories.map(r => r.repository); }
|
|
|
|
private possibleGitRepositoryPaths = new Set<string>();
|
|
|
|
private disposables: Disposable[] = [];
|
|
|
|
constructor(private git: Git, private globalState: Memento) {
|
|
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
|
|
this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] });
|
|
|
|
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
|
|
this.onDidChangeVisibleTextEditors(window.visibleTextEditors);
|
|
|
|
workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables);
|
|
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
.filter(r => !activeRepositories.has(r!.repository))
|
|
.filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[];
|
|
|
|
possibleRepositoryFolders.forEach(p => this.tryOpenRepository(p.uri.fsPath));
|
|
openRepositoriesToDispose.forEach(r => r.dispose());
|
|
}
|
|
|
|
private onDidChangeConfiguration(): void {
|
|
const possibleRepositoryFolders = (workspace.workspaceFolders || [])
|
|
.filter(folder => workspace.getConfiguration('git', folder.uri).get<boolean>('enabled') === true)
|
|
.filter(folder => !this.getOpenRepository(folder.uri));
|
|
|
|
const openRepositoriesToDispose = this.openRepositories
|
|
.map(repository => ({ repository, root: Uri.file(repository.repository.root) }))
|
|
.filter(({ root }) => workspace.getConfiguration('git', root).get<boolean>('enabled') !== true)
|
|
.map(({ repository }) => repository);
|
|
|
|
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;
|
|
}
|
|
|
|
const config = workspace.getConfiguration('git', Uri.file(path));
|
|
const enabled = config.get<boolean>('enabled') === true;
|
|
|
|
if (!enabled) {
|
|
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.globalState);
|
|
|
|
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 originalResourceChangeListener = repository.onDidChangeOriginalResource(uri => this._onDidChangeOriginalResource.fire({ repository, uri }));
|
|
|
|
const dispose = () => {
|
|
disappearListener.dispose();
|
|
changeListener.dispose();
|
|
originalResourceChangeListener.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);
|
|
}
|
|
|
|
close(repository: Repository): void {
|
|
const openRepository = this.getOpenRepository(repository);
|
|
|
|
if (!openRepository) {
|
|
return;
|
|
}
|
|
|
|
openRepository.dispose();
|
|
}
|
|
|
|
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) {
|
|
let resourcePath: string;
|
|
|
|
if (hint.scheme === 'git') {
|
|
resourcePath = fromGitUri(hint).path;
|
|
} else {
|
|
resourcePath = hint.fsPath;
|
|
}
|
|
|
|
for (const liveRepository of this.openRepositories) {
|
|
const relativePath = path.relative(liveRepository.repository.root, resourcePath);
|
|
|
|
if (isDescendant(liveRepository.repository.root, resourcePath)) {
|
|
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 {
|
|
const openRepositories = [...this.openRepositories];
|
|
openRepositories.forEach(r => r.dispose());
|
|
this.openRepositories = [];
|
|
|
|
this.possibleGitRepositoryPaths.clear();
|
|
this.disposables = dispose(this.disposables);
|
|
}
|
|
} |