|
3 | 3 | * Licensed under the MIT License. See License.txt in the project root for license information.
|
4 | 4 | *--------------------------------------------------------------------------------------------*/
|
5 | 5 |
|
6 |
| -import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands, LogOutputChannel, l10n, ProgressLocation } from 'vscode'; |
| 6 | +import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands, LogOutputChannel, l10n, ProgressLocation, QuickPickItemKind } from 'vscode'; |
7 | 7 | import TelemetryReporter from '@vscode/extension-telemetry';
|
8 | 8 | import { Operation, Repository, RepositoryState } from './repository';
|
9 | 9 | import { memoize, sequentialize, debounce } from './decorators';
|
@@ -33,6 +33,31 @@ class RepositoryPick implements QuickPickItem {
|
33 | 33 | constructor(public readonly repository: Repository, public readonly index: number) { }
|
34 | 34 | }
|
35 | 35 |
|
| 36 | +class UnsafeRepositorySet extends Set<string> { |
| 37 | + constructor() { |
| 38 | + super(); |
| 39 | + this.updateContextKey(); |
| 40 | + } |
| 41 | + |
| 42 | + override add(value: string): this { |
| 43 | + const result = super.add(value); |
| 44 | + this.updateContextKey(); |
| 45 | + |
| 46 | + return result; |
| 47 | + } |
| 48 | + |
| 49 | + override delete(value: string): boolean { |
| 50 | + const result = super.delete(value); |
| 51 | + this.updateContextKey(); |
| 52 | + |
| 53 | + return result; |
| 54 | + } |
| 55 | + |
| 56 | + private updateContextKey(): void { |
| 57 | + commands.executeCommand('setContext', 'git.unsafeRepositoryCount', this.size); |
| 58 | + } |
| 59 | +} |
| 60 | + |
36 | 61 | export interface ModelChangeEvent {
|
37 | 62 | repository: Repository;
|
38 | 63 | uri: Uri;
|
@@ -110,6 +135,11 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
|
110 | 135 | private showRepoOnHomeDriveRootWarning = true;
|
111 | 136 | private pushErrorHandlers = new Set<PushErrorHandler>();
|
112 | 137 |
|
| 138 | + private _unsafeRepositories = new UnsafeRepositorySet(); |
| 139 | + get unsafeRepositories(): Set<string> { |
| 140 | + return this._unsafeRepositories; |
| 141 | + } |
| 142 | + |
113 | 143 | private disposables: Disposable[] = [];
|
114 | 144 |
|
115 | 145 | constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private logger: LogOutputChannel, private telemetryReporter: TelemetryReporter) {
|
@@ -145,6 +175,11 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
|
145 | 175 | await initialScanFn();
|
146 | 176 | }
|
147 | 177 |
|
| 178 | + // Show unsafe repositories notification if we cannot use a welcome view |
| 179 | + if (this.repositories.length > 0 && this._unsafeRepositories.size > 0) { |
| 180 | + await this.showUnsafeRepositoryNotification(); |
| 181 | + } |
| 182 | + |
148 | 183 | /* __GDPR__
|
149 | 184 | "git.repositoryInitialScan" : {
|
150 | 185 | "owner": "lszomoru",
|
@@ -394,6 +429,23 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
|
394 | 429 | this.open(repository);
|
395 | 430 | repository.status(); // do not await this, we want SCM to know about the repo asap
|
396 | 431 | } catch (ex) {
|
| 432 | + // Handle unsafe repository |
| 433 | + const match = /^fatal: detected dubious ownership in repository at \'([^']+)\'$/m.exec(ex.stderr); |
| 434 | + if (match && match.length === 2) { |
| 435 | + const unsafeRepositoryPath = match[1]; |
| 436 | + this.logger.trace(`Unsafe repository: ${unsafeRepositoryPath}`); |
| 437 | + |
| 438 | + // If the unsafe repository is opened after the initial repository scan, and we cannot use the welcome view |
| 439 | + // as there is already at least one opened repository, we will be showing a notification for the repository. |
| 440 | + if (this._state === 'initialized' && this.openRepositories.length > 0 && !this._unsafeRepositories.has(unsafeRepositoryPath)) { |
| 441 | + this.showUnsafeRepositoryNotification(unsafeRepositoryPath); |
| 442 | + } |
| 443 | + |
| 444 | + this._unsafeRepositories.add(unsafeRepositoryPath); |
| 445 | + |
| 446 | + return; |
| 447 | + } |
| 448 | + |
397 | 449 | // noop
|
398 | 450 | this.logger.trace(`Opening repository for path='${repoPath}' failed; ex=${ex}`);
|
399 | 451 | }
|
@@ -675,6 +727,75 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
|
675 | 727 | return [...this.pushErrorHandlers];
|
676 | 728 | }
|
677 | 729 |
|
| 730 | + async addSafeDirectoryAndOpenRepository() { |
| 731 | + const unsafeRepositories: string[] = []; |
| 732 | + |
| 733 | + if (this._unsafeRepositories.size === 1) { |
| 734 | + // One unsafe repository |
| 735 | + unsafeRepositories.push(this._unsafeRepositories.values().next().value); |
| 736 | + } else { |
| 737 | + // Multiple unsafe repositories |
| 738 | + const allRepositoriesLabel = l10n.t('All Repositories'); |
| 739 | + const allRepositoriesQuickPickItem: QuickPickItem = { label: allRepositoriesLabel }; |
| 740 | + const repositoriesQuickPickItems: QuickPickItem[] = Array.from(this._unsafeRepositories.values()).sort().map(r => ({ label: `$(repo) ${r}` })); |
| 741 | + |
| 742 | + const quickpick = window.createQuickPick(); |
| 743 | + quickpick.title = l10n.t('Mark Repository as Safe and Open'); |
| 744 | + quickpick.placeholder = l10n.t('Pick a repository to mark as safe and open'); |
| 745 | + quickpick.items = [...repositoriesQuickPickItems, { label: '', kind: QuickPickItemKind.Separator }, allRepositoriesQuickPickItem]; |
| 746 | + |
| 747 | + quickpick.show(); |
| 748 | + const repositoryItem = await new Promise<string | undefined>( |
| 749 | + resolve => { |
| 750 | + quickpick.onDidAccept(() => resolve(quickpick.activeItems[0].label)); |
| 751 | + quickpick.onDidHide(() => resolve(undefined)); |
| 752 | + }); |
| 753 | + quickpick.hide(); |
| 754 | + |
| 755 | + if (!repositoryItem) { |
| 756 | + return; |
| 757 | + } |
| 758 | + |
| 759 | + if (repositoryItem === allRepositoriesLabel) { |
| 760 | + // All Repositories |
| 761 | + unsafeRepositories.push(...this._unsafeRepositories.values()); |
| 762 | + } else { |
| 763 | + // One Repository |
| 764 | + unsafeRepositories.push(repositoryItem); |
| 765 | + } |
| 766 | + } |
| 767 | + |
| 768 | + for (const unsafeRepository of unsafeRepositories) { |
| 769 | + // Mark as Safe |
| 770 | + await this.git.addSafeDirectory(unsafeRepository); |
| 771 | + |
| 772 | + // Open Repository |
| 773 | + await this.openRepository(unsafeRepository); |
| 774 | + this._unsafeRepositories.delete(unsafeRepository); |
| 775 | + } |
| 776 | + } |
| 777 | + |
| 778 | + private async showUnsafeRepositoryNotification(path?: string): Promise<void> { |
| 779 | + const unsafeRepositoryPaths: string[] = path ? [path] : Array.from(this._unsafeRepositories.values()); |
| 780 | + const unsafeRepositoryPathLabels = unsafeRepositoryPaths.sort().map(m => `"${m}"`).join(', '); |
| 781 | + |
| 782 | + const message = unsafeRepositoryPaths.length === 1 ? |
| 783 | + l10n.t('The git repository in the following folder has been detected as potentially unsafe as the folder is owned by someone else other than the current user: {0}. Do you want to open the repository?', unsafeRepositoryPathLabels) : |
| 784 | + l10n.t('The git repositories in the following folders have been detected as potentially unsafe as the folder is owned by someone else other than the current user: {0}. Do you want to open the repositories?', unsafeRepositoryPathLabels); |
| 785 | + |
| 786 | + const openRepository = unsafeRepositoryPaths.length === 1 ? l10n.t('Open Repository') : l10n.t('Open Repositories'); |
| 787 | + const learnMore = l10n.t('Learn More'); |
| 788 | + |
| 789 | + const choice = await window.showErrorMessage(message, openRepository, learnMore); |
| 790 | + if (choice === openRepository) { |
| 791 | + // Open Repository |
| 792 | + await this.addSafeDirectoryAndOpenRepository(); |
| 793 | + } else if (choice === learnMore) { |
| 794 | + // Learn More |
| 795 | + commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-scm')); |
| 796 | + } |
| 797 | + } |
| 798 | + |
678 | 799 | dispose(): void {
|
679 | 800 | const openRepositories = [...this.openRepositories];
|
680 | 801 | openRepositories.forEach(r => r.dispose());
|
|
0 commit comments