From fe423bbdba5ed6b6d84c37e2f6ab6f65a2ff503d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 18 Jan 2023 15:27:58 +0100 Subject: [PATCH] Git - Improvements to opening git repositories in parent folders (#171617) * Initial implementation for external repositories * Added setting * Add basic welcome views * Replaced "Always Open" with "Configure" * Remove code duplication * Polish based on feedback * Language consistency * Update notification severity * Move away from the "external repository" terminology * Refactor notification logic * Saving my changes * Further improvements * Refactor parent repository notification * Update message and fix edge case when setting is set to `never` --- extensions/git/package.json | 49 +++++++-- extensions/git/package.nls.json | 23 +++++ extensions/git/src/commands.ts | 40 +++++++- extensions/git/src/model.ts | 169 +++++++++++++++++++++++--------- extensions/github/package.json | 16 +-- 5 files changed, 230 insertions(+), 67 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 013d1375ea3..fdfa3452de9 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -712,6 +712,11 @@ "command": "git.manageUnsafeRepositories", "title": "%command.manageUnsafeRepositories%", "category": "Git" + }, + { + "command": "git.openRepositoriesInParentFolders", + "title": "%command.openRepositoriesInParentFolders%", + "category": "Git" } ], "continueEditSession": [ @@ -1162,6 +1167,10 @@ { "command": "git.manageUnsafeRepositories", "when": "config.git.enabled && !git.missing && git.unsafeRepositoryCount != 0" + }, + { + "command": "git.openRepositoriesInParentFolders", + "when": "config.git.enabled && !git.missing && git.parentRepositoryCount != 0" } ], "scm/title": [ @@ -2674,6 +2683,22 @@ "experimental" ] }, + "git.openRepositoryInParentFolders": { + "type": "string", + "enum": [ + "always", + "never", + "prompt" + ], + "enumDescriptions": [ + "%config.openRepositoryInParentFolders.always%", + "%config.openRepositoryInParentFolders.never%", + "%config.openRepositoryInParentFolders.prompt%" + ], + "default": "prompt", + "markdownDescription": "%config.openRepositoryInParentFolders%", + "scope": "resource" + }, "git.publishBeforeContinueOn": { "type": "boolean", "default": true, @@ -2827,14 +2852,14 @@ { "view": "scm", "contents": "%view.workbench.scm.empty%", - "when": "config.git.enabled && !git.missing && workbenchState == empty && git.unsafeRepositoryCount == 0", + "when": "config.git.enabled && !git.missing && workbenchState == empty && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 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.unsafeRepositoryCount == 0", + "when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount == 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0", "enablement": "git.state == initialized", "group": "2_open@1" }, @@ -2851,15 +2876,25 @@ { "view": "scm", "contents": "%view.workbench.scm.folder%", - "when": "config.git.enabled && !git.missing && git.state == initialized && workbenchState == folder && scmRepositoryCount == 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 && 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.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 && remoteName != 'codespaces'", "group": "5_scm@1" }, + { + "view": "scm", + "contents": "%view.workbench.scm.repositoryInParentFolders%", + "when": "config.git.enabled && !git.missing && git.state == initialized && git.parentRepositoryCount == 1" + }, + { + "view": "scm", + "contents": "%view.workbench.scm.repositoriesInParentFolders%", + "when": "config.git.enabled && !git.missing && git.state == initialized && git.parentRepositoryCount > 1" + }, { "view": "scm", "contents": "%view.workbench.scm.unsafeRepository%", @@ -2873,15 +2908,13 @@ { "view": "explorer", "contents": "%view.workbench.cloneRepository%", - "when": "config.git.enabled", - "enablement": "git.state == initialized", + "when": "config.git.enabled && git.state == initialized && scmRepositoryCount == 0", "group": "5_scm@1" }, { "view": "explorer", "contents": "%view.workbench.learnMore%", - "when": "config.git.enabled", - "enablement": "git.state == initialized", + "when": "config.git.enabled && git.state == initialized && scmRepositoryCount == 0", "group": "5_scm@10" } ] diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 24acb32bac3..728ab7f2870 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -104,6 +104,7 @@ "command.timelineSelectForCompare": "Select for Compare", "command.timelineCompareWithSelected": "Compare with Selected", "command.manageUnsafeRepositories": "Manage Unsafe Repositories", + "command.openRepositoriesInParentFolders": "Open Repositories In Parent Folders", "command.api.getRepositories": "Get Repositories", "command.api.getRepositoryState": "Get Repository State", "command.api.getRemoteSources": "Get Remote Sources", @@ -242,6 +243,10 @@ "config.useIntegratedAskPass": "Controls whether GIT_ASKPASS should be overwritten to use the integrated version.", "config.mergeEditor": "Open the merge editor for files that are currently under conflict.", "config.optimisticUpdate": "Controls whether to optimistically update the state of the Source Control view after running git commands.", + "config.openRepositoryInParentFolders": "Control whether a repository in the parent folders of the workspace, open file(s) should be opened.", + "config.openRepositoryInParentFolders.always": "Always open a repository in the parent folders of the workspace, open file(s).", + "config.openRepositoryInParentFolders.never": "Never open a repository in the parent folders of the workspace, open file(s).", + "config.openRepositoryInParentFolders.prompt": "Prompt before opening a repository in the parent folders of the workspace, open file(s).", "config.publishBeforeContinueOn": "Controls whether to publish unpublished git state when using Continue Working On from a git repository.", "submenu.explorer": "Git", "submenu.commit": "Commit", @@ -334,6 +339,24 @@ "view.workbench.scm.scanWorkspaceForRepositories": { "message": "Scanning workspace for git repositories..." }, + "view.workbench.scm.repositoryInParentFolders": { + "message": "A git repository was found in one of the parent directories of the workspace, open file(s).\n[Open Repository](command:git.openRepositoriesInParentFolders)\nUse [this setting](command:workbench.action.openSettings?%5B%22git.openRepositoryInParentFolders%22%5D) to control how git repositories in parent directories of the workspace, open file(s) are opened. To learn more [read our docs](https://aka.ms/vscode-git-external-repository).", + "comment": [ + "{Locked='](command:git.openRepositoriesInParentFolders'}", + "{Locked='](command:workbench.action.openSettings?%5B%22git.openRepositoryInParentFolders%22%5D'}", + "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.repositoriesInParentFolders": { + "message": "Git repositories were found in one of the parent directories of the workspace, open file(s).\n[Open Repository](command:git.openRepositoriesInParentFolders)\nUse [this setting](command:workbench.action.openSettings?%5B%22git.openRepositoryInParentFolders%22%5D) to control how git repositories in parent directories of the workspace, open file(s) are opened. To learn more [read our docs](https://aka.ms/vscode-git-external-repository).", + "comment": [ + "{Locked='](command:git.openRepositoriesInParentFolders'}", + "{Locked='](command:workbench.action.openSettings?%5B%22git.openRepositoryInParentFolders%22%5D'}", + "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.unsafeRepository": { "message": "The detected git repository is potentially unsafe as the folder is owned by someone other than the current user.\n[Manage Unsafe Repositories](command:git.manageUnsafeRepositories)\nTo learn more about unsafe repositories [read our docs](https://aka.ms/vscode-git-unsafe-repository).", "comment": [ diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 6c2877a4af8..9fe7ff2b275 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -191,7 +191,7 @@ class FetchAllRemotesItem implements QuickPickItem { } } -class UnsafeRepositoryItem implements QuickPickItem { +class RepositoryItem implements QuickPickItem { get label(): string { return `$(repo) ${this.path}`; } @@ -3335,6 +3335,38 @@ export class CommandCenter { repository.closeDiffEditors(undefined, undefined, true); } + @command('git.openRepositoriesInParentFolders') + async openRepositoriesInParentFolders(): Promise { + const parentRepositories: string[] = []; + + const title = l10n.t('Open Repositories In Parent Folders'); + const placeHolder = l10n.t('Pick a repository to open'); + + const allRepositoriesLabel = l10n.t('All Repositories'); + const allRepositoriesQuickPickItem: QuickPickItem = { label: allRepositoriesLabel }; + const repositoriesQuickPickItems: QuickPickItem[] = Array.from(this.model.parentRepositories.keys()).sort().map(r => new RepositoryItem(r)); + + const items = this.model.parentRepositories.size === 1 ? [...repositoriesQuickPickItems] : + [...repositoriesQuickPickItems, { label: '', kind: QuickPickItemKind.Separator }, allRepositoriesQuickPickItem]; + + const repositoryItem = await window.showQuickPick(items, { title, placeHolder }); + if (!repositoryItem) { + return; + } + + if (repositoryItem === allRepositoriesQuickPickItem) { + // All Repositories + parentRepositories.push(...this.model.parentRepositories.keys()); + } else { + // One Repository + parentRepositories.push((repositoryItem as RepositoryItem).path); + } + + for (const parentRepository of parentRepositories) { + await this.model.openParentRepository(parentRepository); + } + } + @command('git.manageUnsafeRepositories') async manageUnsafeRepositories(): Promise { const unsafeRepositories: string[] = []; @@ -3345,13 +3377,13 @@ export class CommandCenter { const allRepositoriesLabel = l10n.t('All Repositories'); const allRepositoriesQuickPickItem: QuickPickItem = { label: allRepositoriesLabel }; - const repositoriesQuickPickItems: QuickPickItem[] = Array.from(this.model.unsafeRepositories.keys()).sort().map(r => new UnsafeRepositoryItem(r)); + const repositoriesQuickPickItems: QuickPickItem[] = Array.from(this.model.unsafeRepositories.keys()).sort().map(r => new RepositoryItem(r)); quickpick.items = this.model.unsafeRepositories.size === 1 ? [...repositoriesQuickPickItems] : [...repositoriesQuickPickItems, { label: '', kind: QuickPickItemKind.Separator }, allRepositoriesQuickPickItem]; quickpick.show(); - const repositoryItem = await new Promise( + const repositoryItem = await new Promise( resolve => { quickpick.onDidAccept(() => resolve(quickpick.activeItems[0])); quickpick.onDidHide(() => resolve(undefined)); @@ -3367,7 +3399,7 @@ export class CommandCenter { unsafeRepositories.push(...this.model.unsafeRepositories.keys()); } else { // One Repository - unsafeRepositories.push((repositoryItem as UnsafeRepositoryItem).path); + unsafeRepositories.push((repositoryItem as RepositoryItem).path); } for (const unsafeRepository of unsafeRepositories) { diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 4e2878b4d48..133877a09de 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -34,18 +34,13 @@ class RepositoryPick implements QuickPickItem { constructor(public readonly repository: Repository, public readonly index: number) { } } -/** - * 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` - */ -class UnsafeRepositoryMap extends Map { +abstract class RepositoryMap extends Map { constructor() { super(); this.updateContextKey(); } - override set(key: string, value: string): this { + override set(key: string, value: T): this { const result = super.set(key, value); this.updateContextKey(); @@ -59,11 +54,30 @@ class UnsafeRepositoryMap extends Map { return result; } - private updateContextKey(): void { + abstract updateContextKey(): void; +} + +/** + * 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` + */ +class UnsafeRepositoryMap extends RepositoryMap { + updateContextKey(): void { commands.executeCommand('setContext', 'git.unsafeRepositoryCount', this.size); } } +/** + * Key - normalized path used in user interface + * Value - value indicating whether the repository should be opened + */ +class ParentRepositoryMap extends RepositoryMap { + updateContextKey(): void { + commands.executeCommand('setContext', 'git.parentRepositoryCount', this.size); + } +} + export interface ModelChangeEvent { repository: Repository; uri: Uri; @@ -138,14 +152,18 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand private _onDidChangePostCommitCommandsProviders = new EventEmitter(); readonly onDidChangePostCommitCommandsProviders = this._onDidChangePostCommitCommandsProviders.event; - private showRepoOnHomeDriveRootWarning = true; private pushErrorHandlers = new Set(); private _unsafeRepositories = new UnsafeRepositoryMap(); - get unsafeRepositories(): Map { + get unsafeRepositories(): UnsafeRepositoryMap { return this._unsafeRepositories; } + private _parentRepositories = new ParentRepositoryMap(); + get parentRepositories(): ParentRepositoryMap { + return this._parentRepositories; + } + private disposables: Disposable[] = []; constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private logger: LogOutputChannel, private telemetryReporter: TelemetryReporter) { @@ -168,6 +186,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand private async doInitialScan(): Promise { const config = workspace.getConfiguration('git'); const autoRepositoryDetection = config.get('autoRepositoryDetection'); + const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt'); const initialScanFn = () => Promise.all([ this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] }), @@ -181,8 +200,12 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand await initialScanFn(); } - // Unsafe repositories notification - if (this._unsafeRepositories.size !== 0) { + if (this._parentRepositories.size !== 0 && + parentRepositoryConfig === 'prompt') { + // Parent repositories notification + this.showParentRepositoryNotification(); + } else if (this._unsafeRepositories.size !== 0) { + // Unsafe repositories notification this.showUnsafeRepositoryNotification(); } @@ -394,65 +417,87 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand } 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 - const repositoryRoot = Uri.file(rawRoot).fsPath; - this.logger.trace(`Repository root: ${repositoryRoot}`); + const { repositoryRoot, unsafeRepositoryMatch } = await this.getRepositoryRoot(repoPath); if (this.getRepositoryExact(repositoryRoot)) { this.logger.trace(`Repository for path ${repositoryRoot} already exists`); return; } - if (this.shouldRepositoryBeIgnored(rawRoot)) { + if (this.shouldRepositoryBeIgnored(repositoryRoot)) { this.logger.trace(`Repository for path ${repositoryRoot} is ignored`); return; } - // On Window, opening a git repository from the root of the HOMEDRIVE poses a security risk. - // We will only a open git repository from the root of the HOMEDRIVE if the user explicitly - // opens the HOMEDRIVE as a folder. Only show the warning once during repository discovery. - if (process.platform === 'win32' && process.env.HOMEDRIVE && pathEquals(`${process.env.HOMEDRIVE}\\`, repositoryRoot)) { - const isRepoInWorkspaceFolders = (workspace.workspaceFolders ?? []).find(f => pathEquals(f.uri.fsPath, repositoryRoot))!!; + // Handle git repositories that are in parent folders + const isRepositoryOutsideWorkspace = (workspace.workspaceFolders ?? []) + .find(f => pathEquals(f.uri.fsPath, repositoryRoot) || isDescendant(f.uri.fsPath, repositoryRoot)) === undefined; + const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt'); - if (!isRepoInWorkspaceFolders) { - if (this.showRepoOnHomeDriveRootWarning) { - window.showWarningMessage(l10n.t('Unable to automatically open the git repository at "{0}". To open that git repository, open it directly as a folder in VS Code.', repositoryRoot)); - this.showRepoOnHomeDriveRootWarning = false; + if (isRepositoryOutsideWorkspace && parentRepositoryConfig !== 'always' && this.globalState.get(`parentRepository:${repositoryRoot}`) !== true) { + this.logger.trace(`Repository in parent folder: ${repositoryRoot}`); + + if (!this._parentRepositories.has(repositoryRoot)) { + // Show a notification if the parent repository is opened after the initial scan + if (this.state === 'initialized' && parentRepositoryConfig === 'prompt') { + this.showParentRepositoryNotification(); } - this.logger.trace(`Repository for path ${repositoryRoot} is on the root of the HOMEDRIVE`); - return; + this._parentRepositories.set(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._unsafeRepositories.has(repositoryRoot)) { + this.showUnsafeRepositoryNotification(); + } + + this._unsafeRepositories.set(repositoryRoot, unsafeRepositoryMatch[2]); + + 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.globalState, this.logger, this.telemetryReporter); this.open(repository); repository.status(); // do not await this, we want SCM to know about the repo asap - } catch (ex) { + } catch (err) { + // noop + this.logger.trace(`Opening repository for path='${repoPath}' failed; ex=${err}`); + } + } + + async openParentRepository(repoPath: string): Promise { + // Mark the repository to be opened from the parent folders + this.globalState.update(`parentRepository:${repoPath}`, true); + + await this.openRepository(repoPath); + this.parentRepositories.delete(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 match = /^fatal: detected dubious ownership in repository at \'([^']+)\'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec(ex.stderr); - if (match && match.length === 3) { - const unsafeRepositoryPath = path.normalize(match[1]); - this.logger.trace(`Unsafe repository: ${unsafeRepositoryPath}`); - - // Show a notification if the unsafe repository is opened after the initial repository scan - if (this._state === 'initialized' && !this._unsafeRepositories.has(unsafeRepositoryPath)) { - this.showUnsafeRepositoryNotification(); - } - - this._unsafeRepositories.set(unsafeRepositoryPath, match[2]); - - return; + 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 }; } - // noop - this.logger.trace(`Opening repository for path='${repoPath}' failed; ex=${ex}`); + throw err; } } @@ -737,6 +782,36 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand return [...this.pushErrorHandlers]; } + private async showParentRepositoryNotification(): Promise { + const message = this.parentRepositories.size === 1 ? + workspace.workspaceFolders !== undefined ? + l10n.t('We found a git repository in one of the parent folders of this workspace. Would you like to open the repository?') : + l10n.t('We found a git repository in one of the parent folders of the open file(s). Would you like to open the repository?') : + workspace.workspaceFolders !== undefined ? + l10n.t('We found git repositories in one of the parent folders of this workspace. Would you like to open the repositories?') : + l10n.t('We found git repositories in one of the parent folders of the open file(s). Would you like to open the repositories?'); + + const yes = l10n.t('Yes'); + const always = l10n.t('Always'); + const never = l10n.t('Never'); + + const choice = await window.showWarningMessage(message, yes, always, never); + if (choice === yes) { + // Open Parent Repositories + commands.executeCommand('git.openRepositoriesInParentFolders'); + } else if (choice === always || choice === never) { + // Update setting + const config = workspace.getConfiguration('git'); + await config.update('openRepositoryInParentFolders', choice === always ? 'always' : 'never', true); + + if (choice === always) { + for (const parentRepository of [...this.parentRepositories.keys()]) { + await this.openParentRepository(parentRepository); + } + } + } + } + private async showUnsafeRepositoryNotification(): Promise { // If no repositories are open, we will use a welcome view to inform the user // that a potentially unsafe repository was found so we do not have to show diff --git a/extensions/github/package.json b/extensions/github/package.json index 91d49c77e8a..018c28a1f7f 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -80,16 +80,16 @@ ], "file/share": [ { - "command": "github.copyVscodeDevLinkFile", - "when": "github.hasGitHubRepo", - "group": "0_vscode@0" + "command": "github.copyVscodeDevLinkFile", + "when": "github.hasGitHubRepo", + "group": "0_vscode@0" } ], "editor/context/share": [ { - "command": "github.copyVscodeDevLink", - "when": "github.hasGitHubRepo && resourceScheme != untitled", - "group": "0_vscode@0" + "command": "github.copyVscodeDevLink", + "when": "github.hasGitHubRepo && resourceScheme != untitled", + "group": "0_vscode@0" } ] }, @@ -119,12 +119,12 @@ { "view": "scm", "contents": "%welcome.publishFolder%", - "when": "config.git.enabled && git.state == initialized && workbenchState == folder && git.unsafeRepositoryCount == 0" + "when": "config.git.enabled && git.state == initialized && workbenchState == folder && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0" }, { "view": "scm", "contents": "%welcome.publishWorkspaceFolder%", - "when": "config.git.enabled && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0 && git.unsafeRepositoryCount == 0" + "when": "config.git.enabled && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0" } ], "markdown.previewStyles": [