model.ts 8.7 KB
Newer Older
J
Joao Moreno 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

J
Joao Moreno 已提交
8
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor } from 'vscode';
J
Joao 已提交
9 10 11
import { Repository, RepositoryState } from './repository';
import { memoize, sequentialize, debounce } from './decorators';
import { dispose, anyEvent, filterEvent } from './util';
J
Joao Moreno 已提交
12
import { Git, GitErrorCodes } from './git';
J
Joao Moreno 已提交
13
import * as path from 'path';
J
Joao 已提交
14
import * as fs from 'fs';
J
Joao Moreno 已提交
15 16 17 18 19
import * as nls from 'vscode-nls';

const localize = nls.loadMessageBundle();

class RepositoryPick implements QuickPickItem {
J
Joao Moreno 已提交
20 21 22
	@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) { }
J
Joao Moreno 已提交
23 24
}

J
Joao Moreno 已提交
25 26 27 28 29
export interface ModelChangeEvent {
	repository: Repository;
	uri: Uri;
}

J
Joao Moreno 已提交
30 31 32 33
interface OpenRepository extends Disposable {
	repository: Repository;
}

J
Joao Moreno 已提交
34
export class Model {
J
Joao Moreno 已提交
35

J
Joao Moreno 已提交
36 37 38 39 40 41
	private _onDidOpenRepository = new EventEmitter<Repository>();
	readonly onDidOpenRepository: Event<Repository> = this._onDidOpenRepository.event;

	private _onDidCloseRepository = new EventEmitter<Repository>();
	readonly onDidCloseRepository: Event<Repository> = this._onDidCloseRepository.event;

J
Joao Moreno 已提交
42 43 44
	private _onDidChangeRepository = new EventEmitter<ModelChangeEvent>();
	readonly onDidChangeRepository: Event<ModelChangeEvent> = this._onDidChangeRepository.event;

J
Joao Moreno 已提交
45 46
	private openRepositories: OpenRepository[] = [];
	get repositories(): Repository[] { return this.openRepositories.map(r => r.repository); }
J
Joao Moreno 已提交
47

J
Joao 已提交
48 49
	private possibleGitRepositoryPaths = new Set<string>();

J
Joao Moreno 已提交
50
	private disposables: Disposable[] = [];
51

J
Joao Moreno 已提交
52 53 54
	constructor(private git: Git) {
		workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
		this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] });
J
Joao Moreno 已提交
55

J
Joao Moreno 已提交
56 57
		window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
		this.onDidChangeVisibleTextEditors(window.visibleTextEditors);
J
Joao 已提交
58 59 60 61 62 63 64 65

		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);
J
Joao 已提交
66 67 68 69 70 71 72 73 74 75 76 77 78

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

J
Joao Moreno 已提交
79 80 81
			children
				.filter(child => child !== '.git')
				.forEach(child => this.tryOpenRepository(path.join(root, child)));
J
Joao 已提交
82
		}
J
Joao 已提交
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
	}

	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 = new Set<string>();
J
Joao Moreno 已提交
98
	}
J
Joao Moreno 已提交
99

J
Joao Moreno 已提交
100 101 102 103 104 105 106 107 108 109 110 111 112
	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[];

J
Joao Moreno 已提交
113
		possibleRepositoryFolders.forEach(p => this.tryOpenRepository(p.uri.fsPath));
J
Joao Moreno 已提交
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
		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;
			}
J
Joao Moreno 已提交
130

J
Joao Moreno 已提交
131
			this.tryOpenRepository(path.dirname(uri.fsPath));
J
Joao Moreno 已提交
132 133 134
		});
	}

J
Joao Moreno 已提交
135
	@sequentialize
J
Joao Moreno 已提交
136
	async tryOpenRepository(path: string): Promise<void> {
J
Joao 已提交
137
		if (this.getRepository(path)) {
J
Joao Moreno 已提交
138 139 140
			return;
		}

J
Joao Moreno 已提交
141
		try {
J
Joao Moreno 已提交
142
			const repositoryRoot = await this.git.getRepositoryRoot(path);
J
Joao 已提交
143 144 145 146 147 148 149 150

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

J
Joao Moreno 已提交
151 152 153 154 155 156 157 158
			const repository = new Repository(this.git.open(repositoryRoot));

			this.open(repository);
		} catch (err) {
			if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) {
				return;
			}

J
Joao Moreno 已提交
159
			// console.error('Failed to find repository:', err);
J
Joao Moreno 已提交
160 161 162 163
		}
	}

	private open(repository: Repository): void {
J
Joao 已提交
164 165
		const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed);
		const disappearListener = onDidDisappearRepository(() => dispose());
J
Joao Moreno 已提交
166 167
		const changeListener = repository.onDidChangeRepository(uri => this._onDidChangeRepository.fire({ repository, uri }));
		const dispose = () => {
J
Joao 已提交
168
			disappearListener.dispose();
J
Joao Moreno 已提交
169
			changeListener.dispose();
J
Joao Moreno 已提交
170 171
			repository.dispose();
			this.openRepositories = this.openRepositories.filter(e => e !== openRepository);
J
Joao Moreno 已提交
172
			this._onDidCloseRepository.fire(repository);
J
Joao Moreno 已提交
173
		};
J
Joao Moreno 已提交
174

J
Joao Moreno 已提交
175 176
		const openRepository = { repository, dispose };
		this.openRepositories.push(openRepository);
J
Joao Moreno 已提交
177
		this._onDidOpenRepository.fire(repository);
J
Joao Moreno 已提交
178 179 180
	}

	async pickRepository(): Promise<Repository | undefined> {
J
Joao Moreno 已提交
181
		if (this.openRepositories.length === 0) {
182 183 184
			throw new Error(localize('no repositories', "There are no available repositories"));
		}

J
Joao Moreno 已提交
185
		const picks = this.openRepositories.map(e => new RepositoryPick(e.repository));
J
Joao Moreno 已提交
186 187 188 189 190 191
		const placeHolder = localize('pick repo', "Choose a repository");
		const pick = await window.showQuickPick(picks, { placeHolder });

		return pick && pick.repository;
	}

J
Joao Moreno 已提交
192 193
	getRepository(sourceControl: SourceControl): Repository | undefined;
	getRepository(resourceGroup: SourceControlResourceGroup): Repository | undefined;
J
Joao Moreno 已提交
194
	getRepository(path: string): Repository | undefined;
J
Joao Moreno 已提交
195 196
	getRepository(resource: Uri): Repository | undefined;
	getRepository(hint: any): Repository | undefined {
J
Joao Moreno 已提交
197 198 199 200
		const liveRepository = this.getOpenRepository(hint);
		return liveRepository && liveRepository.repository;
	}

J
Joao 已提交
201
	private getOpenRepository(repository: Repository): OpenRepository | undefined;
J
Joao Moreno 已提交
202 203
	private getOpenRepository(sourceControl: SourceControl): OpenRepository | undefined;
	private getOpenRepository(resourceGroup: SourceControlResourceGroup): OpenRepository | undefined;
J
Joao Moreno 已提交
204
	private getOpenRepository(path: string): OpenRepository | undefined;
J
Joao Moreno 已提交
205 206
	private getOpenRepository(resource: Uri): OpenRepository | undefined;
	private getOpenRepository(hint: any): OpenRepository | undefined {
J
Joao Moreno 已提交
207 208
		if (!hint) {
			return undefined;
J
Joao Moreno 已提交
209 210
		}

J
Joao 已提交
211 212 213 214
		if (hint instanceof Repository) {
			return this.openRepositories.filter(r => r.repository === hint)[0];
		}

J
Joao Moreno 已提交
215 216 217 218
		if (typeof hint === 'string') {
			hint = Uri.file(hint);
		}

J
Joao Moreno 已提交
219 220
		if (hint instanceof Uri) {
			const resourcePath = hint.fsPath;
J
Joao Moreno 已提交
221

J
Joao Moreno 已提交
222 223
			for (const liveRepository of this.openRepositories) {
				const relativePath = path.relative(liveRepository.repository.root, resourcePath);
224

225
				if (!/^\.\./.test(relativePath)) {
J
Joao Moreno 已提交
226
					return liveRepository;
J
Joao Moreno 已提交
227 228
				}
			}
229

J
Joao Moreno 已提交
230 231
			return undefined;
		}
J
Joao Moreno 已提交
232

J
Joao Moreno 已提交
233 234 235
		for (const liveRepository of this.openRepositories) {
			const repository = liveRepository.repository;

J
Joao Moreno 已提交
236
			if (hint === repository.sourceControl) {
J
Joao Moreno 已提交
237
				return liveRepository;
J
Joao Moreno 已提交
238
			}
J
Joao Moreno 已提交
239

J
Joao Moreno 已提交
240
			if (hint === repository.mergeGroup || hint === repository.indexGroup || hint === repository.workingTreeGroup) {
J
Joao Moreno 已提交
241
				return liveRepository;
J
Joao Moreno 已提交
242 243 244 245 246
			}
		}

		return undefined;
	}
247 248

	dispose(): void {
J
Joao Moreno 已提交
249 250
		[...this.openRepositories].forEach(r => r.dispose());
		this.disposables = dispose(this.disposables);
251
	}
J
Joao Moreno 已提交
252
}