mirror of
https://github.com/Microsoft/vscode
synced 2024-10-02 17:32:41 +00:00
Merge pull request #187638 from microsoft/hediet/b/chief-salmon
Fixes Diff Algorithm Bug
This commit is contained in:
commit
ec700fa456
|
@ -45,14 +45,16 @@ export function smoothenSequenceDiffs(sequence1: ISequence, sequence2: ISequence
|
|||
* Improved diff: [{Add ", Foo" after Bar}]
|
||||
*/
|
||||
export function joinSequenceDiffs(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
|
||||
const result: SequenceDiff[] = [];
|
||||
if (sequenceDiffs.length > 0) {
|
||||
result.push(sequenceDiffs[0]);
|
||||
if (sequenceDiffs.length === 0) {
|
||||
return sequenceDiffs;
|
||||
}
|
||||
|
||||
const result: SequenceDiff[] = [];
|
||||
result.push(sequenceDiffs[0]);
|
||||
|
||||
// First move them all to the left as much as possible and join them if possible
|
||||
for (let i = 1; i < sequenceDiffs.length; i++) {
|
||||
const prevResult = sequenceDiffs[i - 1];
|
||||
const prevResult = result[result.length - 1];
|
||||
let cur = sequenceDiffs[i];
|
||||
|
||||
if (cur.seq1Range.isEmpty || cur.seq2Range.isEmpty) {
|
||||
|
|
|
@ -40,7 +40,8 @@ suite('diff fixtures', () => {
|
|||
|
||||
const diffingAlgo = diffingAlgoName === 'legacy' ? new SmartLinesDiffComputer() : new StandardLinesDiffComputer();
|
||||
|
||||
const diff = diffingAlgo.computeDiff(firstContentLines, secondContentLines, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false });
|
||||
const ignoreTrimWhitespace = folder.indexOf('trimws') >= 0;
|
||||
const diff = diffingAlgo.computeDiff(firstContentLines, secondContentLines, { ignoreTrimWhitespace, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false });
|
||||
|
||||
function getDiffs(changes: readonly LineRangeMapping[]): IDetailedDiff[] {
|
||||
return changes.map<IDetailedDiff>(c => ({
|
||||
|
@ -112,7 +113,7 @@ suite('diff fixtures', () => {
|
|||
}
|
||||
|
||||
test(`test`, () => {
|
||||
runTest('issue-185779', 'advanced');
|
||||
runTest('invalid-diff-trimws', 'advanced');
|
||||
});
|
||||
|
||||
for (const folder of folders) {
|
||||
|
|
|
@ -0,0 +1,815 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands, LogOutputChannel, l10n, ProgressLocation, WorkspaceFolder } from 'vscode';
|
||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||
import { Repository, RepositoryState } from './repository';
|
||||
import { memoize, sequentialize, debounce } from './decorators';
|
||||
import { dispose, anyEvent, filterEvent, isDescendant, pathEquals, toDisposable, eventToPromise } from './util';
|
||||
import { Git } from './git';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { fromGitUri } from './uri';
|
||||
import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider } from './api/git';
|
||||
import { Askpass } from './askpass';
|
||||
import { IPushErrorHandlerRegistry } from './pushError';
|
||||
import { ApiRepository } from './api/api1';
|
||||
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
|
||||
import { IPostCommitCommandsProviderRegistry } from './postCommitCommands';
|
||||
import { IBranchProtectionProviderRegistry } from './branchProtection';
|
||||
|
||||
class ClosedRepositoriesManager {
|
||||
|
||||
private _repositories: Set<string>;
|
||||
get repositories(): string[] {
|
||||
return [...this._repositories.values()];
|
||||
}
|
||||
|
||||
constructor(private readonly workspaceState: Memento) {
|
||||
this._repositories = new Set<string>(workspaceState.get<string[]>('closedRepositories', []));
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
addRepository(repository: string): void {
|
||||
this._repositories.add(repository);
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
deleteRepository(repository: string): boolean {
|
||||
const result = this._repositories.delete(repository);
|
||||
if (result) {
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
isRepositoryClosed(repository: string): boolean {
|
||||
return this._repositories.has(repository);
|
||||
}
|
||||
|
||||
private onDidChangeRepositories(): void {
|
||||
this.workspaceState.update('closedRepositories', [...this._repositories.values()]);
|
||||
commands.executeCommand('setContext', 'git.closedRepositoryCount', this._repositories.size);
|
||||
}
|
||||
}
|
||||
|
||||
class ParentRepositoriesManager {
|
||||
|
||||
/**
|
||||
* Key - normalized path used in user interface
|
||||
* Value - value indicating whether the repository should be opened
|
||||
*/
|
||||
private _repositories = new Set<string>;
|
||||
get repositories(): string[] {
|
||||
return [...this._repositories.values()];
|
||||
}
|
||||
|
||||
constructor(private readonly globalState: Memento) {
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
addRepository(repository: string): void {
|
||||
this._repositories.add(repository);
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
deleteRepository(repository: string): boolean {
|
||||
const result = this._repositories.delete(repository);
|
||||
if (result) {
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
hasRepository(repository: string): boolean {
|
||||
return this._repositories.has(repository);
|
||||
}
|
||||
|
||||
openRepository(repository: string): void {
|
||||
this.globalState.update(`parentRepository:${repository}`, true);
|
||||
this.deleteRepository(repository);
|
||||
}
|
||||
|
||||
private onDidChangeRepositories(): void {
|
||||
commands.executeCommand('setContext', 'git.parentRepositoryCount', this._repositories.size);
|
||||
}
|
||||
}
|
||||
|
||||
class UnsafeRepositoriesManager {
|
||||
|
||||
/**
|
||||
* Key - normalized path used in user interface
|
||||
* Value - path extracted from the output of the `git status` command
|
||||
* used when calling `git config --global --add safe.directory`
|
||||
*/
|
||||
private _repositories = new Map<string, string>();
|
||||
get repositories(): string[] {
|
||||
return [...this._repositories.keys()];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
addRepository(repository: string, path: string): void {
|
||||
this._repositories.set(repository, path);
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
deleteRepository(repository: string): boolean {
|
||||
const result = this._repositories.delete(repository);
|
||||
if (result) {
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getRepositoryPath(repository: string): string | undefined {
|
||||
return this._repositories.get(repository);
|
||||
}
|
||||
|
||||
hasRepository(repository: string): boolean {
|
||||
return this._repositories.has(repository);
|
||||
}
|
||||
|
||||
private onDidChangeRepositories(): void {
|
||||
commands.executeCommand('setContext', 'git.unsafeRepositoryCount', this._repositories.size);
|
||||
}
|
||||
}
|
||||
|
||||
export class Model implements IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry {
|
||||
|
||||
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 _onDidChangeState = new EventEmitter<State>();
|
||||
readonly onDidChangeState = this._onDidChangeState.event;
|
||||
|
||||
private _onDidPublish = new EventEmitter<PublishEvent>();
|
||||
readonly onDidPublish = this._onDidPublish.event;
|
||||
|
||||
firePublishEvent(repository: Repository, branch?: string) {
|
||||
this._onDidPublish.fire({ repository: new ApiRepository(repository), branch: branch });
|
||||
}
|
||||
|
||||
private _state: State = 'uninitialized';
|
||||
get state(): State { return this._state; }
|
||||
|
||||
setState(state: State): void {
|
||||
this._state = state;
|
||||
this._onDidChangeState.fire(state);
|
||||
commands.executeCommand('setContext', 'git.state', state);
|
||||
}
|
||||
|
||||
@memoize
|
||||
get isInitialized(): Promise<void> {
|
||||
if (this._state === 'initialized') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return eventToPromise(filterEvent(this.onDidChangeState, s => s === 'initialized')) as Promise<any>;
|
||||
}
|
||||
|
||||
private remoteSourcePublishers = new Set<RemoteSourcePublisher>();
|
||||
|
||||
private _onDidAddRemoteSourcePublisher = new EventEmitter<RemoteSourcePublisher>();
|
||||
readonly onDidAddRemoteSourcePublisher = this._onDidAddRemoteSourcePublisher.event;
|
||||
|
||||
private _onDidRemoveRemoteSourcePublisher = new EventEmitter<RemoteSourcePublisher>();
|
||||
readonly onDidRemoveRemoteSourcePublisher = this._onDidRemoveRemoteSourcePublisher.event;
|
||||
|
||||
private postCommitCommandsProviders = new Set<PostCommitCommandsProvider>();
|
||||
|
||||
private _onDidChangePostCommitCommandsProviders = new EventEmitter<void>();
|
||||
readonly onDidChangePostCommitCommandsProviders = this._onDidChangePostCommitCommandsProviders.event;
|
||||
|
||||
private branchProtectionProviders = new Map<string, Set<BranchProtectionProvider>>();
|
||||
|
||||
private _onDidChangeBranchProtectionProviders = new EventEmitter<Uri>();
|
||||
readonly onDidChangeBranchProtectionProviders = this._onDidChangeBranchProtectionProviders.event;
|
||||
|
||||
private pushErrorHandlers = new Set<PushErrorHandler>();
|
||||
|
||||
private _unsafeRepositoriesManager: UnsafeRepositoriesManager;
|
||||
get unsafeRepositories(): string[] {
|
||||
return this._unsafeRepositoriesManager.repositories;
|
||||
}
|
||||
|
||||
private _parentRepositoriesManager: ParentRepositoriesManager;
|
||||
get parentRepositories(): string[] {
|
||||
return this._parentRepositoriesManager.repositories;
|
||||
}
|
||||
|
||||
private _closedRepositoriesManager: ClosedRepositoriesManager;
|
||||
get closedRepositories(): string[] {
|
||||
return [...this._closedRepositoriesManager.repositories];
|
||||
}
|
||||
|
||||
/**
|
||||
* We maintain a map containing both the path and the canonical path of the
|
||||
* workspace folders. We are doing this as `git.exe` expands the symbolic links
|
||||
* while there are scenarios in which VS Code does not.
|
||||
*
|
||||
* Key - path of the workspace folder
|
||||
* Value - canonical path of the workspace folder
|
||||
*/
|
||||
private _workspaceFolders = new Map<string, string>();
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, readonly workspaceState: Memento, private logger: LogOutputChannel, private telemetryReporter: TelemetryReporter) {
|
||||
// Repositories managers
|
||||
this._closedRepositoriesManager = new ClosedRepositoriesManager(workspaceState);
|
||||
this._parentRepositoriesManager = new ParentRepositoriesManager(globalState);
|
||||
this._unsafeRepositoriesManager = new UnsafeRepositoriesManager();
|
||||
|
||||
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
|
||||
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
|
||||
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.setState('uninitialized');
|
||||
this.doInitialScan().finally(() => this.setState('initialized'));
|
||||
}
|
||||
|
||||
private async doInitialScan(): Promise<void> {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
|
||||
const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt');
|
||||
|
||||
// Initial repository scan function
|
||||
const initialScanFn = () => Promise.all([
|
||||
this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] }),
|
||||
this.onDidChangeVisibleTextEditors(window.visibleTextEditors),
|
||||
this.scanWorkspaceFolders()
|
||||
]);
|
||||
|
||||
if (config.get<boolean>('showProgress', true)) {
|
||||
await window.withProgress({ location: ProgressLocation.SourceControl }, initialScanFn);
|
||||
} else {
|
||||
await initialScanFn();
|
||||
}
|
||||
|
||||
if (this.parentRepositories.length !== 0 &&
|
||||
parentRepositoryConfig === 'prompt') {
|
||||
// Parent repositories notification
|
||||
this.showParentRepositoryNotification();
|
||||
} else if (this.unsafeRepositories.length !== 0) {
|
||||
// Unsafe repositories notification
|
||||
this.showUnsafeRepositoryNotification();
|
||||
}
|
||||
|
||||
/* __GDPR__
|
||||
"git.repositoryInitialScan" : {
|
||||
"owner": "lszomoru",
|
||||
"autoRepositoryDetection": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Setting that controls the initial repository scan" },
|
||||
"repositoryCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of repositories opened during initial repository scan" }
|
||||
}
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('git.repositoryInitialScan', { autoRepositoryDetection: String(autoRepositoryDetection) }, { repositoryCount: this.openRepositories.length });
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans each workspace folder, looking for git repositories. By
|
||||
* default it scans one level deep but that can be changed using
|
||||
* the git.repositoryScanMaxDepth setting.
|
||||
*/
|
||||
private async scanWorkspaceFolders(): Promise<void> {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
|
||||
this.logger.trace(`[swsf] Scan workspace sub folders. autoRepositoryDetection=${autoRepositoryDetection}`);
|
||||
|
||||
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all((workspace.workspaceFolders || []).map(async folder => {
|
||||
const root = folder.uri.fsPath;
|
||||
this.logger.trace(`[swsf] Workspace folder: ${root}`);
|
||||
|
||||
// Workspace folder children
|
||||
const repositoryScanMaxDepth = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<number>('repositoryScanMaxDepth', 1);
|
||||
const repositoryScanIgnoredFolders = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<string[]>('repositoryScanIgnoredFolders', []);
|
||||
|
||||
const subfolders = new Set(await this.traverseWorkspaceFolder(root, repositoryScanMaxDepth, repositoryScanIgnoredFolders));
|
||||
|
||||
// Repository scan folders
|
||||
const scanPaths = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<string[]>('scanRepositories') || [];
|
||||
this.logger.trace(`[swsf] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`);
|
||||
|
||||
for (const scanPath of scanPaths) {
|
||||
if (scanPath === '.git') {
|
||||
this.logger.trace('[swsf] \'.git\' not supported in \'git.scanRepositories\' setting.');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (path.isAbsolute(scanPath)) {
|
||||
const notSupportedMessage = l10n.t('Absolute paths not supported in "git.scanRepositories" setting.');
|
||||
this.logger.warn(notSupportedMessage);
|
||||
console.warn(notSupportedMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
subfolders.add(path.join(root, scanPath));
|
||||
}
|
||||
|
||||
this.logger.trace(`[swsf] Workspace scan sub folders: [${[...subfolders].join(', ')}]`);
|
||||
await Promise.all([...subfolders].map(f => this.openRepository(f)));
|
||||
}));
|
||||
}
|
||||
|
||||
private async traverseWorkspaceFolder(workspaceFolder: string, maxDepth: number, repositoryScanIgnoredFolders: string[]): Promise<string[]> {
|
||||
const result: string[] = [];
|
||||
const foldersToTravers = [{ path: workspaceFolder, depth: 0 }];
|
||||
|
||||
while (foldersToTravers.length > 0) {
|
||||
const currentFolder = foldersToTravers.shift()!;
|
||||
|
||||
if (currentFolder.depth < maxDepth || maxDepth === -1) {
|
||||
const children = await fs.promises.readdir(currentFolder.path, { withFileTypes: true });
|
||||
const childrenFolders = children
|
||||
.filter(dirent =>
|
||||
dirent.isDirectory() && dirent.name !== '.git' &&
|
||||
!repositoryScanIgnoredFolders.find(f => pathEquals(dirent.name, f)))
|
||||
.map(dirent => path.join(currentFolder.path, dirent.name));
|
||||
|
||||
result.push(...childrenFolders);
|
||||
foldersToTravers.push(...childrenFolders.map(folder => {
|
||||
return { path: folder, depth: currentFolder.depth + 1 };
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private onPossibleGitRepositoryChange(uri: Uri): void {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
|
||||
|
||||
if (autoRepositoryDetection === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventuallyScanPossibleGitRepository(uri.fsPath.replace(/\.git.*$/, ''));
|
||||
}
|
||||
|
||||
private eventuallyScanPossibleGitRepository(path: string) {
|
||||
this.possibleGitRepositoryPaths.add(path);
|
||||
this.eventuallyScanPossibleGitRepositories();
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
private eventuallyScanPossibleGitRepositories(): void {
|
||||
for (const path of this.possibleGitRepositoryPaths) {
|
||||
this.openRepository(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[];
|
||||
|
||||
openRepositoriesToDispose.forEach(r => r.dispose());
|
||||
this.logger.trace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
|
||||
await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath)));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
this.logger.trace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
|
||||
possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath));
|
||||
openRepositoriesToDispose.forEach(r => r.dispose());
|
||||
}
|
||||
|
||||
private async onDidChangeVisibleTextEditors(editors: readonly TextEditor[]): Promise<void> {
|
||||
if (!workspace.isTrusted) {
|
||||
this.logger.trace('[svte] Workspace is not trusted.');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = workspace.getConfiguration('git');
|
||||
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
|
||||
this.logger.trace(`[svte] Scan visible text editors. autoRepositoryDetection=${autoRepositoryDetection}`);
|
||||
|
||||
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(editors.map(async editor => {
|
||||
const uri = editor.document.uri;
|
||||
|
||||
if (uri.scheme !== 'file') {
|
||||
return;
|
||||
}
|
||||
|
||||
const repository = this.getRepository(uri);
|
||||
|
||||
if (repository) {
|
||||
this.logger.trace(`[svte] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.trace(`[svte] Open repository for editor resource ${uri.fsPath}`);
|
||||
await this.openRepository(path.dirname(uri.fsPath));
|
||||
}));
|
||||
}
|
||||
|
||||
@sequentialize
|
||||
async openRepository(repoPath: string, openIfClosed = false): Promise<void> {
|
||||
this.logger.trace(`Opening repository: ${repoPath}`);
|
||||
const existingRepository = await this.getRepositoryExact(repoPath);
|
||||
if (existingRepository) {
|
||||
this.logger.trace(`Repository for path ${repoPath} already exists: ${existingRepository.root})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = workspace.getConfiguration('git', Uri.file(repoPath));
|
||||
const enabled = config.get<boolean>('enabled') === true;
|
||||
|
||||
if (!enabled) {
|
||||
this.logger.trace('Git is not enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!workspace.isTrusted) {
|
||||
// Check if the folder is a bare repo: if it has a file named HEAD && `rev-parse --show -cdup` is empty
|
||||
try {
|
||||
fs.accessSync(path.join(repoPath, 'HEAD'), fs.constants.F_OK);
|
||||
const result = await this.git.exec(repoPath, ['-C', repoPath, 'rev-parse', '--show-cdup']);
|
||||
if (result.stderr.trim() === '' && result.stdout.trim() === '') {
|
||||
this.logger.trace(`Bare repository: ${repoPath}`);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// If this throw, we should be good to open the repo (e.g. HEAD doesn't exist)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { repositoryRoot, unsafeRepositoryMatch } = await this.getRepositoryRoot(repoPath);
|
||||
this.logger.trace(`Repository root for path ${repoPath} is: ${repositoryRoot}`);
|
||||
|
||||
const existingRepository = await this.getRepositoryExact(repositoryRoot);
|
||||
if (existingRepository) {
|
||||
this.logger.trace(`Repository for path ${repositoryRoot} already exists: ${existingRepository.root}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shouldRepositoryBeIgnored(repositoryRoot)) {
|
||||
this.logger.trace(`Repository for path ${repositoryRoot} is ignored`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle git repositories that are in parent folders
|
||||
const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt');
|
||||
if (parentRepositoryConfig !== 'always' && this.globalState.get<boolean>(`parentRepository:${repositoryRoot}`) !== true) {
|
||||
const isRepositoryOutsideWorkspace = await this.isRepositoryOutsideWorkspace(repositoryRoot);
|
||||
if (isRepositoryOutsideWorkspace) {
|
||||
this.logger.trace(`Repository in parent folder: ${repositoryRoot}`);
|
||||
|
||||
if (!this._parentRepositoriesManager.hasRepository(repositoryRoot)) {
|
||||
// Show a notification if the parent repository is opened after the initial scan
|
||||
if (this.state === 'initialized' && parentRepositoryConfig === 'prompt') {
|
||||
this.showParentRepositoryNotification();
|
||||
}
|
||||
|
||||
this._parentRepositoriesManager.addRepository(repositoryRoot);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle unsafe repositories
|
||||
if (unsafeRepositoryMatch && unsafeRepositoryMatch.length === 3) {
|
||||
this.logger.trace(`Unsafe repository: ${repositoryRoot}`);
|
||||
|
||||
// Show a notification if the unsafe repository is opened after the initial scan
|
||||
if (this._state === 'initialized' && !this._unsafeRepositoriesManager.hasRepository(repositoryRoot)) {
|
||||
this.showUnsafeRepositoryNotification();
|
||||
}
|
||||
|
||||
this._unsafeRepositoriesManager.addRepository(repositoryRoot, unsafeRepositoryMatch[2]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle repositories that were closed by the user
|
||||
if (!openIfClosed && this._closedRepositoriesManager.isRepositoryClosed(repositoryRoot)) {
|
||||
this.logger.trace(`Repository for path ${repositoryRoot} is closed`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open repository
|
||||
const dotGit = await this.git.getRepositoryDotGit(repositoryRoot);
|
||||
const repository = new Repository(this.git.open(repositoryRoot, dotGit, this.logger), this, this, this, this, this.globalState, this.logger, this.telemetryReporter);
|
||||
|
||||
this.open(repository);
|
||||
this._closedRepositoriesManager.deleteRepository(repository.root);
|
||||
|
||||
// Do not await this, we want SCM
|
||||
// to know about the repo asap
|
||||
repository.status();
|
||||
} catch (err) {
|
||||
// noop
|
||||
this.logger.trace(`Opening repository for path='${repoPath}' failed; ex=${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async openParentRepository(repoPath: string): Promise<void> {
|
||||
await this.openRepository(repoPath);
|
||||
this._parentRepositoriesManager.openRepository(repoPath);
|
||||
}
|
||||
|
||||
private async getRepositoryRoot(repoPath: string): Promise<{ repositoryRoot: string; unsafeRepositoryMatch: RegExpMatchArray | null }> {
|
||||
try {
|
||||
const rawRoot = await this.git.getRepositoryRoot(repoPath);
|
||||
|
||||
// This can happen whenever `path` has the wrong case sensitivity in case
|
||||
// insensitive file systems https://github.com/microsoft/vscode/issues/33498
|
||||
return { repositoryRoot: Uri.file(rawRoot).fsPath, unsafeRepositoryMatch: null };
|
||||
} catch (err) {
|
||||
// Handle unsafe repository
|
||||
const unsafeRepositoryMatch = /^fatal: detected dubious ownership in repository at \'([^']+)\'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec(err.stderr);
|
||||
if (unsafeRepositoryMatch && unsafeRepositoryMatch.length === 3) {
|
||||
return { repositoryRoot: path.normalize(unsafeRepositoryMatch[1]), unsafeRepositoryMatch };
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRepositoryBeIgnored(repositoryRoot: string): boolean {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const ignoredRepos = config.get<string[]>('ignoredRepositories') || [];
|
||||
|
||||
for (const ignoredRepo of ignoredRepos) {
|
||||
if (path.isAbsolute(ignoredRepo)) {
|
||||
if (pathEquals(ignoredRepo, repositoryRoot)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
for (const folder of workspace.workspaceFolders || []) {
|
||||
if (pathEquals(path.join(folder.uri.fsPath, ignoredRepo), repositoryRoot)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private open(repository: Repository): void {
|
||||
this.logger.info(`Open repository: ${repository.root}`);
|
||||
|
||||
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 shouldDetectSubmodules = workspace
|
||||
.getConfiguration('git', Uri.file(repository.root))
|
||||
.get<boolean>('detectSubmodules') as boolean;
|
||||
|
||||
const submodulesLimit = workspace
|
||||
.getConfiguration('git', Uri.file(repository.root))
|
||||
.get<number>('detectSubmodulesLimit') as number;
|
||||
|
||||
const checkForSubmodules = () => {
|
||||
if (!shouldDetectSubmodules) {
|
||||
this.logger.trace('Automatic detection of git submodules is not enabled.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (repository.submodules.length > submodulesLimit) {
|
||||
window.showWarningMessage(l10n.t('The "{0}" repository has {1} submodules which won\'t be opened automatically. You can still open each one individually by opening a file within.', path.basename(repository.root), repository.submodules.length));
|
||||
statusListener.dispose();
|
||||
}
|
||||
|
||||
repository.submodules
|
||||
.slice(0, submodulesLimit)
|
||||
.map(r => path.join(repository.root, r.path))
|
||||
.forEach(p => {
|
||||
this.logger.trace(`Opening submodule: '${p}'`);
|
||||
this.eventuallyScanPossibleGitRepository(p);
|
||||
});
|
||||
};
|
||||
|
||||
const updateMergeChanges = () => {
|
||||
// set mergeChanges context
|
||||
const mergeChanges: Uri[] = [];
|
||||
for (const { repository } of this.openRepositories.values()) {
|
||||
for (const state of repository.mergeGroup.resourceStates) {
|
||||
mergeChanges.push(state.resourceUri);
|
||||
}
|
||||
}
|
||||
commands.executeCommand('setContext', 'git.mergeChanges', mergeChanges);
|
||||
};
|
||||
|
||||
const statusListener = repository.onDidRunGitStatus(() => {
|
||||
checkForSubmodules();
|
||||
updateMergeChanges();
|
||||
});
|
||||
checkForSubmodules();
|
||||
|
||||
const updateOperationInProgressContext = () => {
|
||||
let operationInProgress = false;
|
||||
for (const { repository } of this.openRepositories.values()) {
|
||||
if (repository.operations.shouldDisableCommands()) {
|
||||
operationInProgress = true;
|
||||
}
|
||||
}
|
||||
|
||||
commands.executeCommand('setContext', 'operationInProgress', operationInProgress);
|
||||
};
|
||||
|
||||
const operationEvent = anyEvent(repository.onDidRunOperation as Event<any>, repository.onRunOperation as Event<any>);
|
||||
const operationListener = operationEvent(() => updateOperationInProgressContext());
|
||||
updateOperationInProgressContext();
|
||||
|
||||
const dispose = () => {
|
||||
disappearListener.dispose();
|
||||
changeListener.dispose();
|
||||
originalResourceChangeListener.dispose();
|
||||
statusListener.dispose();
|
||||
operationListener.dispose();
|
||||
repository.dispose();
|
||||
|
||||
this.openRepositories = this.openRepositories.filter(e => e !== openRepository);
|
||||
this._onDidCloseRepository.fire(repository);
|
||||
};
|
||||
|
||||
const openRepository = { repository, dispose };
|
||||
this.openRepositories.push(openRepository);
|
||||
updateMergeChanges();
|
||||
this._onDidOpenRepository.fire(repository);
|
||||
}
|
||||
|
||||
close(repository: Repository): void {
|
||||
const openRepository = this.getOpenRepository(repository);
|
||||
|
||||
if (!openRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(`Close repository: ${repository.root}`);
|
||||
this._closedRepositoriesManager.addRepository(openRepository.repository.root);
|
||||
|
||||
openRepository.dispose();
|
||||
}
|
||||
|
||||
async pickRepository(): Promise<Repository | undefined> {
|
||||
if (this.openRepositories.length === 0) {
|
||||
throw new Error(l10n.t('There are no available repositories'));
|
||||
}
|
||||
|
||||
const picks = this.openRepositories.map((e, index) => new RepositoryPick(e.repository, index));
|
||||
const active = window.activeTextEditor;
|
||||
const repository = active && this.getRepository(active.document.fileName);
|
||||
const index = picks.findIndex(pick => pick.repository === repository);
|
||||
|
||||
// Move repository pick containing the active text editor to appear first
|
||||
if (index > -1) {
|
||||
picks.unshift(...picks.splice(index, 1));
|
||||
}
|
||||
|
||||
const placeHolder = l10n.t('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 async getRepositoryExact(repoPath: string): Promise<Repository | undefined> {
|
||||
const repoPathCanonical = await fs.promises.realpath(repoPath, { encoding: 'utf8' });
|
||||
const openRepository = this.openRepositories.find(async r => {
|
||||
const rootPathCanonical = await fs.promises.realpath(r.repository.root, { encoding: 'utf8' });
|
||||
return pathEquals(rootPathCanonical, repoPathCanonical);
|
||||
});
|
||||
return openRepository?.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 (hint instanceof ApiRepository) {
|
||||
return this.openRepositories.filter(r => r.repository === hint.repository)[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;
|
||||
}
|
||||
|
||||
outer:
|
||||
for (const liveRepository of this.openRepositories.sort((a, b) => b.repository.root.length - a.repository.root.length)) {
|
||||
if (!isDescendant(liveRepository.repository.root, resourcePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const submodule of liveRepository.repository.submodules) {
|
||||
const submoduleRoot = path.join(liveRepository.repository.root, submodule.path);
|
||||
|
||||
if (isDescendant(submoduleRoot, resourcePath)) {
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
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 || hint === repository.untrackedGroup) {
|
||||
return liveRepository;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,832 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands, LogOutputChannel, l10n, ProgressLocation, WorkspaceFolder } from 'vscode';
|
||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||
import { Repository, RepositoryState } from './repository';
|
||||
import { memoize, sequentialize, debounce } from './decorators';
|
||||
import { dispose, anyEvent, filterEvent, isDescendant, pathEquals, toDisposable, eventToPromise } from './util';
|
||||
import { Git } from './git';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { fromGitUri } from './uri';
|
||||
import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider } from './api/git';
|
||||
import { Askpass } from './askpass';
|
||||
import { IPushErrorHandlerRegistry } from './pushError';
|
||||
import { ApiRepository } from './api/api1';
|
||||
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
|
||||
import { IPostCommitCommandsProviderRegistry } from './postCommitCommands';
|
||||
import { IBranchProtectionProviderRegistry } from './branchProtection';
|
||||
|
||||
class ClosedRepositoriesManager {
|
||||
|
||||
private _repositories: Set<string>;
|
||||
get repositories(): string[] {
|
||||
return [...this._repositories.values()];
|
||||
}
|
||||
|
||||
constructor(private readonly workspaceState: Memento) {
|
||||
this._repositories = new Set<string>(workspaceState.get<string[]>('closedRepositories', []));
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
addRepository(repository: string): void {
|
||||
this._repositories.add(repository);
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
deleteRepository(repository: string): boolean {
|
||||
const result = this._repositories.delete(repository);
|
||||
if (result) {
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
isRepositoryClosed(repository: string): boolean {
|
||||
return this._repositories.has(repository);
|
||||
}
|
||||
|
||||
private onDidChangeRepositories(): void {
|
||||
this.workspaceState.update('closedRepositories', [...this._repositories.values()]);
|
||||
commands.executeCommand('setContext', 'git.closedRepositoryCount', this._repositories.size);
|
||||
}
|
||||
}
|
||||
|
||||
class ParentRepositoriesManager {
|
||||
|
||||
/**
|
||||
* Key - normalized path used in user interface
|
||||
* Value - value indicating whether the repository should be opened
|
||||
*/
|
||||
private _repositories = new Set<string>;
|
||||
get repositories(): string[] {
|
||||
return [...this._repositories.values()];
|
||||
}
|
||||
|
||||
constructor(private readonly globalState: Memento) {
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
addRepository(repository: string): void {
|
||||
this._repositories.add(repository);
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
deleteRepository(repository: string): boolean {
|
||||
const result = this._repositories.delete(repository);
|
||||
if (result) {
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
hasRepository(repository: string): boolean {
|
||||
return this._repositories.has(repository);
|
||||
}
|
||||
|
||||
openRepository(repository: string): void {
|
||||
this.globalState.update(`parentRepository:${repository}`, true);
|
||||
this.deleteRepository(repository);
|
||||
}
|
||||
|
||||
private onDidChangeRepositories(): void {
|
||||
commands.executeCommand('setContext', 'git.parentRepositoryCount', this._repositories.size);
|
||||
}
|
||||
}
|
||||
|
||||
class UnsafeRepositoriesManager {
|
||||
|
||||
/**
|
||||
* Key - normalized path used in user interface
|
||||
* Value - path extracted from the output of the `git status` command
|
||||
* used when calling `git config --global --add safe.directory`
|
||||
*/
|
||||
private _repositories = new Map<string, string>();
|
||||
get repositories(): string[] {
|
||||
return [...this._repositories.keys()];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
addRepository(repository: string, path: string): void {
|
||||
this._repositories.set(repository, path);
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
deleteRepository(repository: string): boolean {
|
||||
const result = this._repositories.delete(repository);
|
||||
if (result) {
|
||||
this.onDidChangeRepositories();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getRepositoryPath(repository: string): string | undefined {
|
||||
return this._repositories.get(repository);
|
||||
}
|
||||
|
||||
hasRepository(repository: string): boolean {
|
||||
return this._repositories.has(repository);
|
||||
}
|
||||
|
||||
private onDidChangeRepositories(): void {
|
||||
commands.executeCommand('setContext', 'git.unsafeRepositoryCount', this._repositories.size);
|
||||
}
|
||||
}
|
||||
|
||||
export class Model implements IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry {
|
||||
|
||||
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 _onDidChangeState = new EventEmitter<State>();
|
||||
readonly onDidChangeState = this._onDidChangeState.event;
|
||||
|
||||
private _onDidPublish = new EventEmitter<PublishEvent>();
|
||||
readonly onDidPublish = this._onDidPublish.event;
|
||||
|
||||
firePublishEvent(repository: Repository, branch?: string) {
|
||||
this._onDidPublish.fire({ repository: new ApiRepository(repository), branch: branch });
|
||||
}
|
||||
|
||||
private _state: State = 'uninitialized';
|
||||
get state(): State { return this._state; }
|
||||
|
||||
setState(state: State): void {
|
||||
this._state = state;
|
||||
this._onDidChangeState.fire(state);
|
||||
commands.executeCommand('setContext', 'git.state', state);
|
||||
}
|
||||
|
||||
@memoize
|
||||
get isInitialized(): Promise<void> {
|
||||
if (this._state === 'initialized') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return eventToPromise(filterEvent(this.onDidChangeState, s => s === 'initialized')) as Promise<any>;
|
||||
}
|
||||
|
||||
private remoteSourcePublishers = new Set<RemoteSourcePublisher>();
|
||||
|
||||
private _onDidAddRemoteSourcePublisher = new EventEmitter<RemoteSourcePublisher>();
|
||||
readonly onDidAddRemoteSourcePublisher = this._onDidAddRemoteSourcePublisher.event;
|
||||
|
||||
private _onDidRemoveRemoteSourcePublisher = new EventEmitter<RemoteSourcePublisher>();
|
||||
readonly onDidRemoveRemoteSourcePublisher = this._onDidRemoveRemoteSourcePublisher.event;
|
||||
|
||||
private postCommitCommandsProviders = new Set<PostCommitCommandsProvider>();
|
||||
|
||||
private _onDidChangePostCommitCommandsProviders = new EventEmitter<void>();
|
||||
readonly onDidChangePostCommitCommandsProviders = this._onDidChangePostCommitCommandsProviders.event;
|
||||
|
||||
private branchProtectionProviders = new Map<string, Set<BranchProtectionProvider>>();
|
||||
|
||||
private _onDidChangeBranchProtectionProviders = new EventEmitter<Uri>();
|
||||
readonly onDidChangeBranchProtectionProviders = this._onDidChangeBranchProtectionProviders.event;
|
||||
|
||||
private pushErrorHandlers = new Set<PushErrorHandler>();
|
||||
|
||||
private _unsafeRepositoriesManager: UnsafeRepositoriesManager;
|
||||
get unsafeRepositories(): string[] {
|
||||
return this._unsafeRepositoriesManager.repositories;
|
||||
}
|
||||
|
||||
private _parentRepositoriesManager: ParentRepositoriesManager;
|
||||
get parentRepositories(): string[] {
|
||||
return this._parentRepositoriesManager.repositories;
|
||||
}
|
||||
|
||||
private _closedRepositoriesManager: ClosedRepositoriesManager;
|
||||
get closedRepositories(): string[] {
|
||||
return [...this._closedRepositoriesManager.repositories];
|
||||
}
|
||||
|
||||
/**
|
||||
* We maintain a map containing both the path and the canonical path of the
|
||||
* workspace folders. We are doing this as `git.exe` expands the symbolic links
|
||||
* while there are scenarios in which VS Code does not.
|
||||
*
|
||||
* Key - path of the workspace folder
|
||||
* Value - canonical path of the workspace folder
|
||||
*/
|
||||
private _workspaceFolders = new Map<string, string>();
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, readonly workspaceState: Memento, private logger: LogOutputChannel, private telemetryReporter: TelemetryReporter) {
|
||||
// Repositories managers
|
||||
this._closedRepositoriesManager = new ClosedRepositoriesManager(workspaceState);
|
||||
this._parentRepositoriesManager = new ParentRepositoriesManager(globalState);
|
||||
this._unsafeRepositoriesManager = new UnsafeRepositoriesManager();
|
||||
|
||||
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
|
||||
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
|
||||
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.setState('uninitialized');
|
||||
this.doInitialScan().finally(() => this.setState('initialized'));
|
||||
}
|
||||
|
||||
private async doInitialScan(): Promise<void> {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
|
||||
const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt');
|
||||
|
||||
// Initial repository scan function
|
||||
const initialScanFn = () => Promise.all([
|
||||
this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] }),
|
||||
this.onDidChangeVisibleTextEditors(window.visibleTextEditors),
|
||||
this.scanWorkspaceFolders()
|
||||
]);
|
||||
|
||||
if (config.get<boolean>('showProgress', true)) {
|
||||
await window.withProgress({ location: ProgressLocation.SourceControl }, initialScanFn);
|
||||
} else {
|
||||
await initialScanFn();
|
||||
}
|
||||
|
||||
if (this.parentRepositories.length !== 0 &&
|
||||
parentRepositoryConfig === 'prompt') {
|
||||
// Parent repositories notification
|
||||
this.showParentRepositoryNotification();
|
||||
} else if (this.unsafeRepositories.length !== 0) {
|
||||
// Unsafe repositories notification
|
||||
this.showUnsafeRepositoryNotification();
|
||||
}
|
||||
|
||||
/* __GDPR__
|
||||
"git.repositoryInitialScan" : {
|
||||
"owner": "lszomoru",
|
||||
"autoRepositoryDetection": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Setting that controls the initial repository scan" },
|
||||
"repositoryCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of repositories opened during initial repository scan" }
|
||||
}
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('git.repositoryInitialScan', { autoRepositoryDetection: String(autoRepositoryDetection) }, { repositoryCount: this.openRepositories.length });
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans each workspace folder, looking for git repositories. By
|
||||
* default it scans one level deep but that can be changed using
|
||||
* the git.repositoryScanMaxDepth setting.
|
||||
*/
|
||||
private async scanWorkspaceFolders(): Promise<void> {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
|
||||
this.logger.trace(`[swsf] Scan workspace sub folders. autoRepositoryDetection=${autoRepositoryDetection}`);
|
||||
|
||||
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all((workspace.workspaceFolders || []).map(async folder => {
|
||||
const root = folder.uri.fsPath;
|
||||
this.logger.trace(`[swsf] Workspace folder: ${root}`);
|
||||
|
||||
// Workspace folder children
|
||||
const repositoryScanMaxDepth = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<number>('repositoryScanMaxDepth', 1);
|
||||
const repositoryScanIgnoredFolders = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<string[]>('repositoryScanIgnoredFolders', []);
|
||||
|
||||
const subfolders = new Set(await this.traverseWorkspaceFolder(root, repositoryScanMaxDepth, repositoryScanIgnoredFolders));
|
||||
|
||||
// Repository scan folders
|
||||
const scanPaths = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<string[]>('scanRepositories') || [];
|
||||
this.logger.trace(`[swsf] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`);
|
||||
|
||||
for (const scanPath of scanPaths) {
|
||||
if (scanPath === '.git') {
|
||||
this.logger.trace('[swsf] \'.git\' not supported in \'git.scanRepositories\' setting.');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (path.isAbsolute(scanPath)) {
|
||||
const notSupportedMessage = l10n.t('Absolute paths not supported in "git.scanRepositories" setting.');
|
||||
this.logger.warn(notSupportedMessage);
|
||||
console.warn(notSupportedMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
subfolders.add(path.join(root, scanPath));
|
||||
}
|
||||
|
||||
this.logger.trace(`[swsf] Workspace scan sub folders: [${[...subfolders].join(', ')}]`);
|
||||
await Promise.all([...subfolders].map(f => this.openRepository(f)));
|
||||
}));
|
||||
}
|
||||
|
||||
private async traverseWorkspaceFolder(workspaceFolder: string, maxDepth: number, repositoryScanIgnoredFolders: string[]): Promise<string[]> {
|
||||
const result: string[] = [];
|
||||
const foldersToTravers = [{ path: workspaceFolder, depth: 0 }];
|
||||
|
||||
while (foldersToTravers.length > 0) {
|
||||
const currentFolder = foldersToTravers.shift()!;
|
||||
|
||||
if (currentFolder.depth < maxDepth || maxDepth === -1) {
|
||||
const children = await fs.promises.readdir(currentFolder.path, { withFileTypes: true });
|
||||
const childrenFolders = children
|
||||
.filter(dirent =>
|
||||
dirent.isDirectory() && dirent.name !== '.git' &&
|
||||
!repositoryScanIgnoredFolders.find(f => pathEquals(dirent.name, f)))
|
||||
.map(dirent => path.join(currentFolder.path, dirent.name));
|
||||
|
||||
result.push(...childrenFolders);
|
||||
foldersToTravers.push(...childrenFolders.map(folder => {
|
||||
return { path: folder, depth: currentFolder.depth + 1 };
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private onPossibleGitRepositoryChange(uri: Uri): void {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
|
||||
|
||||
if (autoRepositoryDetection === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventuallyScanPossibleGitRepository(uri.fsPath.replace(/\.git.*$/, ''));
|
||||
}
|
||||
|
||||
private eventuallyScanPossibleGitRepository(path: string) {
|
||||
this.possibleGitRepositoryPaths.add(path);
|
||||
this.eventuallyScanPossibleGitRepositories();
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
private eventuallyScanPossibleGitRepositories(): void {
|
||||
for (const path of this.possibleGitRepositoryPaths) {
|
||||
this.openRepository(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[];
|
||||
|
||||
openRepositoriesToDispose.forEach(r => r.dispose());
|
||||
this.logger.trace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
|
||||
await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath)));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
this.logger.trace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
|
||||
possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath));
|
||||
openRepositoriesToDispose.forEach(r => r.dispose());
|
||||
}
|
||||
|
||||
private async onDidChangeVisibleTextEditors(editors: readonly TextEditor[]): Promise<void> {
|
||||
if (!workspace.isTrusted) {
|
||||
this.logger.trace('[svte] Workspace is not trusted.');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = workspace.getConfiguration('git');
|
||||
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
|
||||
this.logger.trace(`[svte] Scan visible text editors. autoRepositoryDetection=${autoRepositoryDetection}`);
|
||||
|
||||
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(editors.map(async editor => {
|
||||
const uri = editor.document.uri;
|
||||
|
||||
if (uri.scheme !== 'file') {
|
||||
return;
|
||||
}
|
||||
|
||||
const repository = this.getRepository(uri);
|
||||
|
||||
if (repository) {
|
||||
this.logger.trace(`[svte] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.trace(`[svte] Open repository for editor resource ${uri.fsPath}`);
|
||||
await this.openRepository(path.dirname(uri.fsPath));
|
||||
}));
|
||||
}
|
||||
|
||||
@sequentialize
|
||||
async openRepository(repoPath: string, openIfClosed = false): Promise<void> {
|
||||
this.logger.trace(`Opening repository: ${repoPath}`);
|
||||
const existingRepository = await this.getRepositoryExact(repoPath);
|
||||
if (existingRepository) {
|
||||
this.logger.trace(`Repository for path ${repoPath} already exists: ${existingRepository.root})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = workspace.getConfiguration('git', Uri.file(repoPath));
|
||||
const enabled = config.get<boolean>('enabled') === true;
|
||||
|
||||
if (!enabled) {
|
||||
this.logger.trace('Git is not enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!workspace.isTrusted) {
|
||||
// Check if the folder is a bare repo: if it has a file named HEAD && `rev-parse --show -cdup` is empty
|
||||
try {
|
||||
fs.accessSync(path.join(repoPath, 'HEAD'), fs.constants.F_OK);
|
||||
const result = await this.git.exec(repoPath, ['-C', repoPath, 'rev-parse', '--show-cdup']);
|
||||
if (result.stderr.trim() === '' && result.stdout.trim() === '') {
|
||||
this.logger.trace(`Bare repository: ${repoPath}`);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// If this throw, we should be good to open the repo (e.g. HEAD doesn't exist)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { repositoryRoot, unsafeRepositoryMatch } = await this.getRepositoryRoot(repoPath);
|
||||
this.logger.trace(`Repository root for path ${repoPath} is: ${repositoryRoot}`);
|
||||
|
||||
const existingRepository = await this.getRepositoryExact(repositoryRoot);
|
||||
if (existingRepository) {
|
||||
this.logger.trace(`Repository for path ${repositoryRoot} already exists: ${existingRepository.root}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shouldRepositoryBeIgnored(repositoryRoot)) {
|
||||
this.logger.trace(`Repository for path ${repositoryRoot} is ignored`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle git repositories that are in parent folders
|
||||
const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt');
|
||||
if (parentRepositoryConfig !== 'always' && this.globalState.get<boolean>(`parentRepository:${repositoryRoot}`) !== true) {
|
||||
const isRepositoryOutsideWorkspace = await this.isRepositoryOutsideWorkspace(repositoryRoot);
|
||||
if (isRepositoryOutsideWorkspace) {
|
||||
this.logger.trace(`Repository in parent folder: ${repositoryRoot}`);
|
||||
|
||||
if (!this._parentRepositoriesManager.hasRepository(repositoryRoot)) {
|
||||
// Show a notification if the parent repository is opened after the initial scan
|
||||
if (this.state === 'initialized' && parentRepositoryConfig === 'prompt') {
|
||||
this.showParentRepositoryNotification();
|
||||
}
|
||||
|
||||
this._parentRepositoriesManager.addRepository(repositoryRoot);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle unsafe repositories
|
||||
if (unsafeRepositoryMatch && unsafeRepositoryMatch.length === 3) {
|
||||
this.logger.trace(`Unsafe repository: ${repositoryRoot}`);
|
||||
|
||||
// Show a notification if the unsafe repository is opened after the initial scan
|
||||
if (this._state === 'initialized' && !this._unsafeRepositoriesManager.hasRepository(repositoryRoot)) {
|
||||
this.showUnsafeRepositoryNotification();
|
||||
}
|
||||
|
||||
this._unsafeRepositoriesManager.addRepository(repositoryRoot, unsafeRepositoryMatch[2]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle repositories that were closed by the user
|
||||
if (!openIfClosed && this._closedRepositoriesManager.isRepositoryClosed(repositoryRoot)) {
|
||||
this.logger.trace(`Repository for path ${repositoryRoot} is closed`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open repository
|
||||
const dotGit = await this.git.getRepositoryDotGit(repositoryRoot);
|
||||
const repository = new Repository(this.git.open(repositoryRoot, dotGit, this.logger), this, this, this, this, this.globalState, this.logger, this.telemetryReporter);
|
||||
|
||||
this.open(repository);
|
||||
this._closedRepositoriesManager.deleteRepository(repository.root);
|
||||
|
||||
// Do not await this, we want SCM
|
||||
// to know about the repo asap
|
||||
repository.status();
|
||||
} catch (err) {
|
||||
// noop
|
||||
this.logger.trace(`Opening repository for path='${repoPath}' failed; ex=${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async openParentRepository(repoPath: string): Promise<void> {
|
||||
await this.openRepository(repoPath);
|
||||
this._parentRepositoriesManager.openRepository(repoPath);
|
||||
}
|
||||
|
||||
private async getRepositoryRoot(repoPath: string): Promise<{ repositoryRoot: string; unsafeRepositoryMatch: RegExpMatchArray | null }> {
|
||||
try {
|
||||
const rawRoot = await this.git.getRepositoryRoot(repoPath);
|
||||
|
||||
// This can happen whenever `path` has the wrong case sensitivity in case
|
||||
// insensitive file systems https://github.com/microsoft/vscode/issues/33498
|
||||
return { repositoryRoot: Uri.file(rawRoot).fsPath, unsafeRepositoryMatch: null };
|
||||
} catch (err) {
|
||||
// Handle unsafe repository
|
||||
const unsafeRepositoryMatch = /^fatal: detected dubious ownership in repository at \'([^']+)\'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec(err.stderr);
|
||||
if (unsafeRepositoryMatch && unsafeRepositoryMatch.length === 3) {
|
||||
return { repositoryRoot: path.normalize(unsafeRepositoryMatch[1]), unsafeRepositoryMatch };
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRepositoryBeIgnored(repositoryRoot: string): boolean {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const ignoredRepos = config.get<string[]>('ignoredRepositories') || [];
|
||||
|
||||
for (const ignoredRepo of ignoredRepos) {
|
||||
if (path.isAbsolute(ignoredRepo)) {
|
||||
if (pathEquals(ignoredRepo, repositoryRoot)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
for (const folder of workspace.workspaceFolders || []) {
|
||||
if (pathEquals(path.join(folder.uri.fsPath, ignoredRepo), repositoryRoot)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private open(repository: Repository): void {
|
||||
this.logger.info(`Open repository: ${repository.root}`);
|
||||
|
||||
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 shouldDetectSubmodules = workspace
|
||||
.getConfiguration('git', Uri.file(repository.root))
|
||||
.get<boolean>('detectSubmodules') as boolean;
|
||||
|
||||
const submodulesLimit = workspace
|
||||
.getConfiguration('git', Uri.file(repository.root))
|
||||
.get<number>('detectSubmodulesLimit') as number;
|
||||
|
||||
const checkForSubmodules = () => {
|
||||
if (!shouldDetectSubmodules) {
|
||||
this.logger.trace('Automatic detection of git submodules is not enabled.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (repository.submodules.length > submodulesLimit) {
|
||||
window.showWarningMessage(l10n.t('The "{0}" repository has {1} submodules which won\'t be opened automatically. You can still open each one individually by opening a file within.', path.basename(repository.root), repository.submodules.length));
|
||||
statusListener.dispose();
|
||||
}
|
||||
|
||||
repository.submodules
|
||||
.slice(0, submodulesLimit)
|
||||
.map(r => path.join(repository.root, r.path))
|
||||
.forEach(p => {
|
||||
this.logger.trace(`Opening submodule: '${p}'`);
|
||||
this.eventuallyScanPossibleGitRepository(p);
|
||||
});
|
||||
};
|
||||
|
||||
const updateMergeChanges = () => {
|
||||
// set mergeChanges context
|
||||
const mergeChanges: Uri[] = [];
|
||||
for (const { repository } of this.openRepositories.values()) {
|
||||
for (const state of repository.mergeGroup.resourceStates) {
|
||||
mergeChanges.push(state.resourceUri);
|
||||
}
|
||||
}
|
||||
commands.executeCommand('setContext', 'git.mergeChanges', mergeChanges);
|
||||
};
|
||||
|
||||
const statusListener = repository.onDidRunGitStatus(() => {
|
||||
checkForSubmodules();
|
||||
updateMergeChanges();
|
||||
});
|
||||
checkForSubmodules();
|
||||
|
||||
const updateOperationInProgressContext = () => {
|
||||
let operationInProgress = false;
|
||||
for (const { repository } of this.openRepositories.values()) {
|
||||
if (repository.operations.shouldDisableCommands()) {
|
||||
operationInProgress = true;
|
||||
}
|
||||
}
|
||||
|
||||
commands.executeCommand('setContext', 'operationInProgress', operationInProgress);
|
||||
};
|
||||
|
||||
const operationEvent = anyEvent(repository.onDidRunOperation as Event<any>, repository.onRunOperation as Event<any>);
|
||||
const operationListener = operationEvent(() => updateOperationInProgressContext());
|
||||
updateOperationInProgressContext();
|
||||
|
||||
const dispose = () => {
|
||||
disappearListener.dispose();
|
||||
changeListener.dispose();
|
||||
originalResourceChangeListener.dispose();
|
||||
statusListener.dispose();
|
||||
operationListener.dispose();
|
||||
repository.dispose();
|
||||
|
||||
this.openRepositories = this.openRepositories.filter(e => e !== openRepository);
|
||||
this._onDidCloseRepository.fire(repository);
|
||||
};
|
||||
|
||||
const openRepository = { repository, dispose };
|
||||
this.openRepositories.push(openRepository);
|
||||
updateMergeChanges();
|
||||
this._onDidOpenRepository.fire(repository);
|
||||
}
|
||||
|
||||
close(repository: Repository): void {
|
||||
const openRepository = this.getOpenRepository(repository);
|
||||
|
||||
if (!openRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(`Close repository: ${repository.root}`);
|
||||
this._closedRepositoriesManager.addRepository(openRepository.repository.root);
|
||||
|
||||
openRepository.dispose();
|
||||
}
|
||||
|
||||
async pickRepository(): Promise<Repository | undefined> {
|
||||
if (this.openRepositories.length === 0) {
|
||||
throw new Error(l10n.t('There are no available repositories'));
|
||||
}
|
||||
|
||||
const picks = this.openRepositories.map((e, index) => new RepositoryPick(e.repository, index));
|
||||
const active = window.activeTextEditor;
|
||||
const repository = active && this.getRepository(active.document.fileName);
|
||||
const index = picks.findIndex(pick => pick.repository === repository);
|
||||
|
||||
// Move repository pick containing the active text editor to appear first
|
||||
if (index > -1) {
|
||||
picks.unshift(...picks.splice(index, 1));
|
||||
}
|
||||
|
||||
const placeHolder = l10n.t('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 async getRepositoryExact(repoPath: string): Promise<Repository | undefined> {
|
||||
const repoPathCanonical = await fs.promises.realpath(repoPath, { encoding: 'utf8' });
|
||||
|
||||
for (const openRepository of this.openRepositories) {
|
||||
const rootPathCanonical = await fs.promises.realpath(openRepository.repository.root, { encoding: 'utf8' });
|
||||
if (pathEquals(rootPathCanonical, repoPathCanonical)) {
|
||||
return openRepository.repository;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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 (hint instanceof ApiRepository) {
|
||||
return this.openRepositories.filter(r => r.repository === hint.repository)[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;
|
||||
}
|
||||
|
||||
outer:
|
||||
for (const liveRepository of this.openRepositories.sort((a, b) => b.repository.root.length - a.repository.root.length)) {
|
||||
if (!isDescendant(liveRepository.repository.root, resourcePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const submodule of liveRepository.repository.submodules) {
|
||||
const submoduleRoot = path.join(liveRepository.repository.root, submodule.path);
|
||||
|
||||
if (isDescendant(submoduleRoot, resourcePath)) {
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
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 || hint === repository.untrackedGroup) {
|
||||
return liveRepository;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getRepositoryForSubmodule(submoduleUri: Uri): Repository | undefined {
|
||||
for (const repository of this.repositories) {
|
||||
for (const submodule of repository.submodules) {
|
||||
const submodulePath = path.join(repository.root, submodule.path);
|
||||
|
||||
if (submodulePath === submoduleUri.fsPath) {
|
||||
return repository;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue