Browse Source

Adds plumbing for showing unsafe repos state

main
Eric Amodio 1 year ago
committed by Keith Daulton
parent
commit
3a6a1d0030
9 changed files with 81 additions and 44 deletions
  1. +9
    -7
      src/env/node/git/git.ts
  2. +12
    -1
      src/env/node/git/localGitProvider.ts
  3. +7
    -1
      src/env/node/git/vslsGitProvider.ts
  4. +1
    -0
      src/git/gitProvider.ts
  5. +8
    -0
      src/git/gitProviderService.ts
  6. +7
    -7
      src/webviews/apps/home/components/plus-banner.ts
  7. +12
    -9
      src/webviews/apps/home/home.ts
  8. +15
    -13
      src/webviews/home/homeWebview.ts
  9. +10
    -6
      src/webviews/home/protocol.ts

+ 9
- 7
src/env/node/git/git.ts View File

@ -1604,7 +1604,7 @@ export class Git {
return { path: dotGitPath }; return { path: dotGitPath };
} }
async rev_parse__show_toplevel(cwd: string): Promise<string | undefined> {
async rev_parse__show_toplevel(cwd: string): Promise<[safe: true, repoPath: string] | [safe: false] | []> {
let data; let data;
if (!workspace.isTrusted) { if (!workspace.isTrusted) {
@ -1618,7 +1618,7 @@ export class Git {
); );
if (data.trim() === '') { if (data.trim() === '') {
Logger.log(`Skipping (untrusted workspace); bare clone repository detected in '${cwd}'`); Logger.log(`Skipping (untrusted workspace); bare clone repository detected in '${cwd}'`);
return undefined;
return emptyArray as [];
} }
} catch { } catch {
// If this throw, we should be good to open the repo (e.g. HEAD doesn't exist) // If this throw, we should be good to open the repo (e.g. HEAD doesn't exist)
@ -1629,7 +1629,9 @@ export class Git {
data = await this.git<string>({ cwd: cwd, errors: GitErrorHandling.Throw }, 'rev-parse', '--show-toplevel'); data = await this.git<string>({ cwd: cwd, errors: GitErrorHandling.Throw }, 'rev-parse', '--show-toplevel');
// Make sure to normalize: https://github.com/git-for-windows/git/issues/2478 // Make sure to normalize: https://github.com/git-for-windows/git/issues/2478
// Keep trailing spaces which are part of the directory name // Keep trailing spaces which are part of the directory name
return data.length === 0 ? undefined : normalizePath(data.trimStart().replace(/[\r|\n]+$/, ''));
return data.length === 0
? (emptyArray as [])
: [true, normalizePath(data.trimStart().replace(/[\r|\n]+$/, ''))];
} catch (ex) { } catch (ex) {
const unsafeMatch = const unsafeMatch =
/^fatal: detected dubious ownership in repository at '([^']+)'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec( /^fatal: detected dubious ownership in repository at '([^']+)'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec(
@ -1639,7 +1641,7 @@ export class Git {
Logger.log( Logger.log(
`Skipping; unsafe repository detected in '${unsafeMatch[1]}'; run 'git config --global --add safe.directory ${unsafeMatch[2]}' to allow it`, `Skipping; unsafe repository detected in '${unsafeMatch[1]}'; run 'git config --global --add safe.directory ${unsafeMatch[2]}' to allow it`,
); );
return undefined;
return [false];
} }
const inDotGit = /this operation must be run in a work tree/.test(ex.stderr); const inDotGit = /this operation must be run in a work tree/.test(ex.stderr);
@ -1659,7 +1661,7 @@ export class Git {
); );
data = data.trim(); data = data.trim();
if (data.length) { if (data.length) {
return normalizePath((data === '.' ? cwd : data).trimStart().replace(/[\r|\n]+$/, ''));
return [true, normalizePath((data === '.' ? cwd : data).trimStart().replace(/[\r|\n]+$/, ''))];
} }
} }
} }
@ -1670,7 +1672,7 @@ export class Git {
if (!exists) { if (!exists) {
do { do {
const parent = dirname(cwd); const parent = dirname(cwd);
if (parent === cwd || parent.length === 0) return undefined;
if (parent === cwd || parent.length === 0) return emptyArray as [];
cwd = parent; cwd = parent;
exists = await fsExists(cwd); exists = await fsExists(cwd);
@ -1679,7 +1681,7 @@ export class Git {
return this.rev_parse__show_toplevel(cwd); return this.rev_parse__show_toplevel(cwd);
} }
} }
return undefined;
return emptyArray as [];
} }
} }

+ 12
- 1
src/env/node/git/localGitProvider.ts View File

@ -1066,6 +1066,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
private readonly toCanonicalMap = new Map<string, Uri>(); private readonly toCanonicalMap = new Map<string, Uri>();
private readonly fromCanonicalMap = new Map<string, Uri>(); private readonly fromCanonicalMap = new Map<string, Uri>();
protected readonly unsafePaths = new Set<string>();
@gate() @gate()
@debug() @debug()
@ -1084,7 +1085,13 @@ export class LocalGitProvider implements GitProvider, Disposable {
uri = Uri.joinPath(uri, '..'); uri = Uri.joinPath(uri, '..');
} }
repoPath = await this.git.rev_parse__show_toplevel(uri.fsPath);
let safe;
[safe, repoPath] = await this.git.rev_parse__show_toplevel(uri.fsPath);
if (safe) {
this.unsafePaths.delete(uri.fsPath);
} else {
this.unsafePaths.add(uri.fsPath);
}
if (!repoPath) return undefined; if (!repoPath) return undefined;
const repoUri = Uri.file(repoPath); const repoUri = Uri.file(repoPath);
@ -4103,6 +4110,10 @@ export class LocalGitProvider implements GitProvider, Disposable {
return this.git.merge_base__is_ancestor(repoPath, ref, '@{u}'); return this.git.merge_base__is_ancestor(repoPath, ref, '@{u}');
} }
hasUnsafeRepositories(): boolean {
return this.unsafePaths.size !== 0;
}
isTrackable(uri: Uri): boolean { isTrackable(uri: Uri): boolean {
return this.supportedSchemes.has(uri.scheme); return this.supportedSchemes.has(uri.scheme);
} }

+ 7
- 1
src/env/node/git/vslsGitProvider.ts View File

@ -93,7 +93,13 @@ export class VslsGitProvider extends LocalGitProvider {
uri = Uri.joinPath(uri, '..'); uri = Uri.joinPath(uri, '..');
} }
repoPath = await this.git.rev_parse__show_toplevel(uri.fsPath);
let safe;
[safe, repoPath] = await this.git.rev_parse__show_toplevel(uri.fsPath);
if (safe) {
this.unsafePaths.delete(uri.fsPath);
} else {
this.unsafePaths.add(uri.fsPath);
}
if (!repoPath) return undefined; if (!repoPath) return undefined;
return repoPath ? Uri.parse(repoPath, true) : undefined; return repoPath ? Uri.parse(repoPath, true) : undefined;

+ 1
- 0
src/git/gitProvider.ts View File

@ -404,6 +404,7 @@ export interface GitProvider extends Disposable {
): Promise<boolean>; ): Promise<boolean>;
hasCommitBeenPushed(repoPath: string, ref: string): Promise<boolean>; hasCommitBeenPushed(repoPath: string, ref: string): Promise<boolean>;
hasUnsafeRepositories?(): boolean;
isTrackable(uri: Uri): boolean; isTrackable(uri: Uri): boolean;
isTracked(uri: Uri): Promise<boolean>; isTracked(uri: Uri): Promise<boolean>;

+ 8
- 0
src/git/gitProviderService.ts View File

@ -2515,6 +2515,14 @@ export class GitProviderService implements Disposable {
return repository.hasUpstreamBranch(); return repository.hasUpstreamBranch();
} }
@log()
hasUnsafeRepositories(): boolean {
for (const provider of this._providers.values()) {
if (provider.hasUnsafeRepositories?.()) return true;
}
return false;
}
@log<GitProviderService['isRepositoryForEditor']>({ @log<GitProviderService['isRepositoryForEditor']>({
args: { args: {
0: r => r.uri.toString(true), 0: r => r.uri.toString(true),

+ 7
- 7
src/webviews/apps/home/components/plus-banner.ts View File

@ -16,7 +16,7 @@ const template = html`
</h3> </h3>
${when( ${when(
y => y.extensionEnabled,
y => y.hasRepositories,
html<PlusBanner>` html<PlusBanner>`
<p class="mb-1"> <p class="mb-1">
<vscode-button @click="${x => x.fireAction('command:gitlens.plus.startPreviewTrial')}" <vscode-button @click="${x => x.fireAction('command:gitlens.plus.startPreviewTrial')}"
@ -49,7 +49,7 @@ const template = html`
private repos. private repos.
</p> </p>
${when( ${when(
y => y.extensionEnabled,
y => y.hasRepositories,
html<PlusBanner>` html<PlusBanner>`
<p class="mb-1"> <p class="mb-1">
<vscode-button @click="${x => x.fireAction('command:gitlens.plus.purchase')}" <vscode-button @click="${x => x.fireAction('command:gitlens.plus.purchase')}"
@ -81,7 +81,7 @@ const template = html`
GitLens+ features on private repos. GitLens+ features on private repos.
</p> </p>
${when( ${when(
y => y.extensionEnabled,
y => y.hasRepositories,
html<PlusBanner>` html<PlusBanner>`
<p class="mb-1"> <p class="mb-1">
<vscode-button @click="${x => x.fireAction('command:gitlens.plus.loginOrSignUp')}" <vscode-button @click="${x => x.fireAction('command:gitlens.plus.loginOrSignUp')}"
@ -101,7 +101,7 @@ const template = html`
private repos. private repos.
</p> </p>
${when( ${when(
y => y.extensionEnabled,
y => y.hasRepositories,
html<PlusBanner>` html<PlusBanner>`
<p class="mb-1"> <p class="mb-1">
<vscode-button @click="${x => x.fireAction('command:gitlens.plus.purchase')}" <vscode-button @click="${x => x.fireAction('command:gitlens.plus.purchase')}"
@ -137,7 +137,7 @@ const template = html`
SubscriptionState.Free, SubscriptionState.Free,
SubscriptionState.FreePreviewTrialExpired, SubscriptionState.FreePreviewTrialExpired,
SubscriptionState.FreePlusTrialExpired, SubscriptionState.FreePlusTrialExpired,
].includes(x.state) && x.extensionEnabled,
].includes(x.state) && x.hasRepositories,
html<PlusBanner>` html<PlusBanner>`
<p class="mb-0"> <p class="mb-0">
${when( ${when(
@ -152,7 +152,7 @@ const template = html`
`, `,
)} )}
${when( ${when(
x => !x.extensionEnabled,
x => !x.hasRepositories,
html<PlusBanner>` html<PlusBanner>`
<p class="mb-0"> <p class="mb-0">
To use GitLens+, open a folder containing a git repository or clone from a URL from the Explorer. To use GitLens+, open a folder containing a git repository or clone from a URL from the Explorer.
@ -230,7 +230,7 @@ export class PlusBanner extends FASTElement {
plus = true; plus = true;
@observable @observable
extensionEnabled = true;
hasRepositories = false;
get daysRemaining() { get daysRemaining() {
if (this.days < 1) { if (this.days < 1) {

+ 12
- 9
src/webviews/apps/home/home.ts View File

@ -7,8 +7,8 @@ import type { State } from '../../home/protocol';
import { import {
CompleteStepCommandType, CompleteStepCommandType,
DidChangeConfigurationType, DidChangeConfigurationType,
DidChangeExtensionEnabledType,
DidChangeLayoutType, DidChangeLayoutType,
DidChangeRepositoriesType,
DidChangeSubscriptionNotificationType, DidChangeSubscriptionNotificationType,
DismissBannerCommandType, DismissBannerCommandType,
DismissSectionCommandType, DismissSectionCommandType,
@ -95,11 +95,11 @@ export class HomeApp extends App {
this.updateState(); this.updateState();
}); });
break; break;
case DidChangeExtensionEnabledType.method:
case DidChangeRepositoriesType.method:
this.log(`onMessageReceived(${msg.id}): name=${msg.method}`); this.log(`onMessageReceived(${msg.id}): name=${msg.method}`);
onIpc(DidChangeExtensionEnabledType, msg, params => {
this.state.extensionEnabled = params.extensionEnabled;
onIpc(DidChangeRepositoriesType, msg, params => {
this.state.repositories = { ...params };
this.updateNoRepo(); this.updateNoRepo();
}); });
break; break;
@ -242,13 +242,16 @@ export class HomeApp extends App {
} }
private updateNoRepo() { private updateNoRepo() {
const { extensionEnabled } = this.state;
const { repositories } = this.state;
const hasRepos = repositories.count > 0;
const value = extensionEnabled ? 'true' : 'false';
// TODO@d13 provide better feedback if there are unsafe repos (maybe even if there are no "open" repos?)
const value = hasRepos ? 'true' : 'false';
let $el = document.getElementById('no-repo'); let $el = document.getElementById('no-repo');
$el?.setAttribute('aria-hidden', value); $el?.setAttribute('aria-hidden', value);
if (extensionEnabled) {
if (hasRepos) {
$el?.setAttribute('hidden', value); $el?.setAttribute('hidden', value);
} else { } else {
$el?.removeAttribute('hidden'); $el?.removeAttribute('hidden');
@ -256,7 +259,7 @@ export class HomeApp extends App {
$el = document.getElementById('no-repo-alert'); $el = document.getElementById('no-repo-alert');
$el?.setAttribute('aria-hidden', value); $el?.setAttribute('aria-hidden', value);
if (extensionEnabled) {
if (hasRepos) {
$el?.setAttribute('hidden', value); $el?.setAttribute('hidden', value);
} else { } else {
$el?.removeAttribute('hidden'); $el?.removeAttribute('hidden');
@ -283,7 +286,7 @@ export class HomeApp extends App {
$plusContent.setAttribute('visibility', visibility); $plusContent.setAttribute('visibility', visibility);
$plusContent.setAttribute('plan', subscription.plan.effective.name); $plusContent.setAttribute('plan', subscription.plan.effective.name);
$plusContent.setAttribute('plus', plusEnabled.toString()); $plusContent.setAttribute('plus', plusEnabled.toString());
($plusContent as PlusBanner).extensionEnabled = this.state.extensionEnabled;
($plusContent as PlusBanner).hasRepositories = this.state.repositories.count > 0;
} }
$plusContent = document.getElementById('plus-content'); $plusContent = document.getElementById('plus-content');

+ 15
- 13
src/webviews/home/homeWebview.ts View File

@ -8,7 +8,6 @@ import type { SubscriptionChangeEvent } from '../../plus/subscription/subscripti
import type { Subscription } from '../../subscription'; import type { Subscription } from '../../subscription';
import { executeCoreCommand, registerCommand } from '../../system/command'; import { executeCoreCommand, registerCommand } from '../../system/command';
import { configuration } from '../../system/configuration'; import { configuration } from '../../system/configuration';
import { getContext, onDidChangeContext } from '../../system/context';
import type { Deferrable } from '../../system/function'; import type { Deferrable } from '../../system/function';
import { debounce } from '../../system/function'; import { debounce } from '../../system/function';
import type { StorageChangeEvent } from '../../system/storage'; import type { StorageChangeEvent } from '../../system/storage';
@ -20,8 +19,8 @@ import {
CompletedActions, CompletedActions,
CompleteStepCommandType, CompleteStepCommandType,
DidChangeConfigurationType, DidChangeConfigurationType,
DidChangeExtensionEnabledType,
DidChangeLayoutType, DidChangeLayoutType,
DidChangeRepositoriesType,
DidChangeSubscriptionNotificationType, DidChangeSubscriptionNotificationType,
DismissBannerCommandType, DismissBannerCommandType,
DismissSectionCommandType, DismissSectionCommandType,
@ -34,10 +33,7 @@ export class HomeWebviewProvider implements WebviewProvider {
constructor(private readonly container: Container, private readonly host: WebviewController<State>) { constructor(private readonly container: Container, private readonly host: WebviewController<State>) {
this._disposable = Disposable.from( this._disposable = Disposable.from(
this.container.subscription.onDidChange(this.onSubscriptionChanged, this), this.container.subscription.onDidChange(this.onSubscriptionChanged, this),
onDidChangeContext(key => {
if (key !== 'gitlens:disabled') return;
this.notifyExtensionEnabled();
}),
this.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this),
configuration.onDidChange(this.onConfigurationChanged, this), configuration.onDidChange(this.onConfigurationChanged, this),
this.container.storage.onDidChange(this.onStorageChanged, this), this.container.storage.onDidChange(this.onStorageChanged, this),
); );
@ -55,6 +51,10 @@ export class HomeWebviewProvider implements WebviewProvider {
this.notifyDidChangeConfiguration(); this.notifyDidChangeConfiguration();
} }
private onRepositoriesChanged() {
this.notifyDidChangeRepositories();
}
private onStorageChanged(e: StorageChangeEvent) { private onStorageChanged(e: StorageChangeEvent) {
if (e.key !== 'views:layout') return; if (e.key !== 'views:layout') return;
@ -223,7 +223,7 @@ export class HomeWebviewProvider implements WebviewProvider {
const dismissedBanners = this.container.storage.get('home:banners:dismissed', []); const dismissedBanners = this.container.storage.get('home:banners:dismissed', []);
return { return {
extensionEnabled: this.getExtensionEnabled(),
repositories: this.getRepositoriesState(),
webroot: this.host.getWebRoot(), webroot: this.host.getWebRoot(),
subscription: sub.subscription, subscription: sub.subscription,
completedActions: sub.completedActions, completedActions: sub.completedActions,
@ -250,16 +250,18 @@ export class HomeWebviewProvider implements WebviewProvider {
}); });
} }
private getExtensionEnabled() {
return !getContext('gitlens:disabled', false);
private getRepositoriesState() {
return {
count: this.container.git.repositoryCount,
openCount: this.container.git.openRepositoryCount,
hasUnsafe: this.container.git.hasUnsafeRepositories(),
};
} }
private notifyExtensionEnabled() {
private notifyDidChangeRepositories() {
if (!this.host.ready) return; if (!this.host.ready) return;
void this.host.notify(DidChangeExtensionEnabledType, {
extensionEnabled: this.getExtensionEnabled(),
});
void this.host.notify(DidChangeRepositoriesType, this.getRepositoriesState());
} }
private getPlusEnabled() { private getPlusEnabled() {

+ 10
- 6
src/webviews/home/protocol.ts View File

@ -9,7 +9,11 @@ export const enum CompletedActions {
} }
export interface State { export interface State {
extensionEnabled: boolean;
repositories: {
count: number;
openCount: number;
hasUnsafe: boolean;
};
webroot?: string; webroot?: string;
subscription: Subscription; subscription: Subscription;
completedActions: CompletedActions[]; completedActions: CompletedActions[];
@ -51,12 +55,12 @@ export const DidChangeSubscriptionNotificationType = new IpcNotificationType
'subscription/didChange', 'subscription/didChange',
); );
export interface DidChangeExtensionEnabledParams {
extensionEnabled: boolean;
export interface DidChangeRepositoriesParams {
count: number;
openCount: number;
hasUnsafe: boolean;
} }
export const DidChangeExtensionEnabledType = new IpcNotificationType<DidChangeExtensionEnabledParams>(
'extensionEnabled/didChange',
);
export const DidChangeRepositoriesType = new IpcNotificationType<DidChangeRepositoriesParams>('repositories/didChange');
export interface DidChangeConfigurationParams { export interface DidChangeConfigurationParams {
plusEnabled: boolean; plusEnabled: boolean;

Loading…
Cancel
Save