Git - close repository improvements (#184708)

* Initial implementation

* Move ObservableSet into a separate file

* Add quick pick for reopening closed repositories

* Fix issue with initializing the context key

* Add welcome views
This commit is contained in:
Ladislau Szomoru 2023-06-09 13:19:57 +02:00 committed by GitHub
parent e913a2e547
commit 9979f9cc3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 177 additions and 11 deletions

View file

@ -84,6 +84,12 @@
"category": "Git",
"enablement": "!operationInProgress"
},
{
"command": "git.reopenClosedRepositories",
"title": "%command.reopenClosedRepositories%",
"category": "Git",
"enablement": "!operationInProgress && git.ClosedRepositoryCount != 0"
},
{
"command": "git.close",
"title": "%command.close%",
@ -2860,14 +2866,14 @@
{
"view": "scm",
"contents": "%view.workbench.scm.empty%",
"when": "config.git.enabled && !git.missing && workbenchState == empty && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0",
"when": "config.git.enabled && !git.missing && workbenchState == empty && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0",
"enablement": "git.state == initialized",
"group": "2_open@1"
},
{
"view": "scm",
"contents": "%view.workbench.scm.emptyWorkspace%",
"when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount == 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0",
"when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount == 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0",
"enablement": "git.state == initialized",
"group": "2_open@1"
},
@ -2884,13 +2890,13 @@
{
"view": "scm",
"contents": "%view.workbench.scm.folder%",
"when": "config.git.enabled && !git.missing && git.state == initialized && workbenchState == folder && scmRepositoryCount == 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && remoteName != 'codespaces'",
"when": "config.git.enabled && !git.missing && git.state == initialized && workbenchState == folder && scmRepositoryCount == 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0 && remoteName != 'codespaces'",
"group": "5_scm@1"
},
{
"view": "scm",
"contents": "%view.workbench.scm.workspace%",
"when": "config.git.enabled && !git.missing && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0 && scmRepositoryCount == 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && remoteName != 'codespaces'",
"when": "config.git.enabled && !git.missing && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0 && scmRepositoryCount == 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0 && remoteName != 'codespaces'",
"group": "5_scm@1"
},
{
@ -2913,6 +2919,16 @@
"contents": "%view.workbench.scm.unsafeRepositories%",
"when": "config.git.enabled && !git.missing && git.state == initialized && git.unsafeRepositoryCount > 1"
},
{
"view": "scm",
"contents": "%view.workbench.scm.closedRepository%",
"when": "config.git.enabled && !git.missing && git.state == initialized && git.closedRepositoryCount == 1"
},
{
"view": "scm",
"contents": "%view.workbench.scm.closedRepositories%",
"when": "config.git.enabled && !git.missing && git.state == initialized && git.closedRepositoryCount > 1"
},
{
"view": "explorer",
"contents": "%view.workbench.cloneRepository%",

View file

@ -7,6 +7,7 @@
"command.cloneRecursive": "Clone (Recursive)",
"command.init": "Initialize Repository",
"command.openRepository": "Open Repository",
"command.reopenClosedRepositories": "Reopen Closed Repositories...",
"command.close": "Close Repository",
"command.refresh": "Refresh",
"command.openChange": "Open Changes",
@ -378,6 +379,22 @@
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.closedRepository": {
"message": "A git repository was found that was previously closed.\n[Reopen Closed Repository](command:git.reopenClosedRepositories)\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).",
"comment": [
"{Locked='](command:git.reopenClosedRepositories'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.closedRepositories": {
"message": "Git repositories were found that were previously closed.\n[Reopen Closed Repositories](command:git.reopenClosedRepositories)\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).",
"comment": [
"{Locked='](command:git.reopenClosedRepositories'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.cloneRepository": {
"message": "You can clone a repository locally.\n[Clone Repository](command:git.clone 'Clone a repository once the git extension has activated')",
"comment": [

View file

@ -880,7 +880,43 @@ export class CommandCenter {
path = result[0].fsPath;
}
await this.model.openRepository(path);
await this.model.openRepository(path, true);
}
@command('git.reopenClosedRepositories', { repository: false })
async reopenClosedRepositories(): Promise<void> {
if (this.model.closedRepositories.length === 0) {
return;
}
const closedRepositories: string[] = [];
const title = l10n.t('Reopen Closed Repositories');
const placeHolder = l10n.t('Pick a repository to reopen');
const allRepositoriesLabel = l10n.t('All Repositories');
const allRepositoriesQuickPickItem: QuickPickItem = { label: allRepositoriesLabel };
const repositoriesQuickPickItems: QuickPickItem[] = this.model.closedRepositories.sort().map(r => new RepositoryItem(r));
const items = this.model.closedRepositories.length === 1 ? [...repositoriesQuickPickItems] :
[...repositoriesQuickPickItems, { label: '', kind: QuickPickItemKind.Separator }, allRepositoriesQuickPickItem];
const repositoryItem = await window.showQuickPick(items, { title, placeHolder });
if (!repositoryItem) {
return;
}
if (repositoryItem === allRepositoriesQuickPickItem) {
// All Repositories
closedRepositories.push(...this.model.closedRepositories.values());
} else {
// One Repository
closedRepositories.push((repositoryItem as RepositoryItem).path);
}
for (const repository of closedRepositories) {
await this.model.openRepository(repository, true);
}
}
@command('git.close', { repository: true })

View file

@ -86,7 +86,7 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel,
version: info.version,
env: environment,
});
const model = new Model(git, askpass, context.globalState, logger, telemetryReporter);
const model = new Model(git, askpass, context.globalState, context.workspaceState, logger, telemetryReporter);
disposables.push(model);
const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`);

View file

@ -19,6 +19,7 @@ import { ApiRepository } from './api/api1';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
import { IPostCommitCommandsProviderRegistry } from './postCommitCommands';
import { IBranchProtectionProviderRegistry } from './branchProtection';
import { ObservableSet } from './observable';
class RepositoryPick implements QuickPickItem {
@memoize get label(): string {
@ -169,6 +170,11 @@ export class Model implements IBranchProtectionProviderRegistry, IRemoteSourcePu
return this._parentRepositories;
}
private _closedRepositories: ObservableSet<string>;
get closedRepositories(): string[] {
return [...this._closedRepositories.values()];
}
/**
* 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
@ -181,7 +187,11 @@ export class Model implements IBranchProtectionProviderRegistry, IRemoteSourcePu
private disposables: Disposable[] = [];
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private logger: LogOutputChannel, private telemetryReporter: TelemetryReporter) {
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private workspaceState: Memento, private logger: LogOutputChannel, private telemetryReporter: TelemetryReporter) {
this._closedRepositories = new ObservableSet<string>(workspaceState.get<string[]>('closedRepositories', []));
this._closedRepositories.onDidChange(this.onDidChangeClosedRepositories, this, this.disposables);
this.onDidChangeClosedRepositories();
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables);
@ -369,6 +379,11 @@ export class Model implements IBranchProtectionProviderRegistry, IRemoteSourcePu
openRepositoriesToDispose.forEach(r => r.dispose());
}
private onDidChangeClosedRepositories(): void {
this.workspaceState.update('closedRepositories', [...this._closedRepositories.values()]);
commands.executeCommand('setContext', 'git.closedRepositoryCount', this._closedRepositories.size);
}
private async onDidChangeVisibleTextEditors(editors: readonly TextEditor[]): Promise<void> {
if (!workspace.isTrusted) {
this.logger.trace('[svte] Workspace is not trusted.');
@ -403,7 +418,7 @@ export class Model implements IBranchProtectionProviderRegistry, IRemoteSourcePu
}
@sequentialize
async openRepository(repoPath: string): Promise<void> {
async openRepository(repoPath: string, openIfClosed = false): Promise<void> {
this.logger.trace(`Opening repository: ${repoPath}`);
if (this.getRepositoryExact(repoPath)) {
this.logger.trace(`Repository for path ${repoPath} already exists`);
@ -480,12 +495,22 @@ export class Model implements IBranchProtectionProviderRegistry, IRemoteSourcePu
return;
}
// Handle repositories that were closed by the user
if (!openIfClosed && this._closedRepositories.has(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);
repository.status(); // do not await this, we want SCM to know about the repo asap
this._closedRepositories.delete(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}`);
@ -633,6 +658,8 @@ export class Model implements IBranchProtectionProviderRegistry, IRemoteSourcePu
}
this.logger.info(`Close repository: ${repository.root}`);
this._closedRepositories.add(openRepository.repository.root.toString());
openRepository.dispose();
}

View file

@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EventEmitter } from 'vscode';
export class ObservableSet<T> implements Set<T> {
readonly [Symbol.toStringTag]: string = 'ObservableSet';
private _set: Set<T>;
private _onDidChange = new EventEmitter<void>();
readonly onDidChange = this._onDidChange.event;
constructor(values?: readonly T[] | null) {
this._set = new Set(values);
}
get size(): number {
return this._set.size;
}
add(value: T): this {
this._set.add(value);
this._onDidChange.fire();
return this;
}
clear(): void {
if (this._set.size > 0) {
this._set.clear();
this._onDidChange.fire();
}
}
delete(value: T): boolean {
const result = this._set.delete(value);
if (result) {
this._onDidChange.fire();
}
return result;
}
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void {
this._set.forEach((_value, key) => callbackfn.call(thisArg, key, key, this));
}
has(value: T): boolean {
return this._set.has(value);
}
entries(): IterableIterator<[T, T]> {
return this._set.entries();
}
keys(): IterableIterator<T> {
return this._set.keys();
}
values(): IterableIterator<T> {
return this._set.keys();
}
[Symbol.iterator](): IterableIterator<T> {
return this.keys();
}
}

View file

@ -160,12 +160,12 @@
{
"view": "scm",
"contents": "%welcome.publishFolder%",
"when": "config.git.enabled && git.state == initialized && workbenchState == folder && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0"
"when": "config.git.enabled && git.state == initialized && workbenchState == folder && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0"
},
{
"view": "scm",
"contents": "%welcome.publishWorkspaceFolder%",
"when": "config.git.enabled && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0"
"when": "config.git.enabled && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0"
}
],
"markdown.previewStyles": [