Skip to content

Commit bd0bf62

Browse files
Adds access step to some features
1 parent c283896 commit bd0bf62

File tree

7 files changed

+167
-17
lines changed

7 files changed

+167
-17
lines changed

contributions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4906,7 +4906,7 @@
49064906
}
49074907
},
49084908
"gitlens.views.ai.generateChangelog": {
4909-
"label": "Generate Changelog",
4909+
"label": "Generate Changelog (Preview)",
49104910
"icon": "$(sparkle)",
49114911
"menus": {
49124912
"view/item/context": [

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7833,7 +7833,7 @@
78337833
},
78347834
{
78357835
"command": "gitlens.views.ai.generateChangelog",
7836-
"title": "Generate Changelog",
7836+
"title": "Generate Changelog (Preview)",
78377837
"icon": "$(sparkle)"
78387838
},
78397839
{

src/features.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,17 @@ export type RepoFeatureAccess =
3030
visibility?: RepositoryVisibility;
3131
};
3232

33-
export type PlusFeatures = 'timeline' | 'worktrees' | 'graph' | 'launchpad' | 'startWork' | 'associateIssueWithBranch';
33+
export type PlusFeatures =
34+
| 'timeline'
35+
| 'worktrees'
36+
| 'graph'
37+
| 'launchpad'
38+
| 'startWork'
39+
| 'associateIssueWithBranch'
40+
| 'generateStashMessage'
41+
| 'explainCommit'
42+
| 'cloudPatchGenerateTitleAndDescription';
43+
export type AdvancedFeatures = 'generateChangelog';
3444

3545
export type FeaturePreviews = 'graph';
3646
export const featurePreviews: FeaturePreviews[] = ['graph'];

src/git/gitProviderService.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { GlyphChars, Schemes } from '../constants';
1616
import { SubscriptionPlanId } from '../constants.subscription';
1717
import type { Container } from '../container';
1818
import { AccessDeniedError, ProviderNotFoundError, ProviderNotSupportedError } from '../errors';
19-
import type { FeatureAccess, Features, PlusFeatures, RepoFeatureAccess } from '../features';
19+
import type { AdvancedFeatures, FeatureAccess, Features, PlusFeatures, RepoFeatureAccess } from '../features';
2020
import type { Subscription } from '../plus/gk/models/subscription';
2121
import type { SubscriptionChangeEvent } from '../plus/gk/subscriptionService';
2222
import { isSubscriptionPaidPlan } from '../plus/gk/utils/subscription.utils';
@@ -722,17 +722,26 @@ export class GitProviderService implements Disposable {
722722
return this._subscription ?? (this._subscription = await this.container.subscription.getSubscription());
723723
}
724724

725-
private _accessCache = new Map<PlusFeatures | undefined, Promise<FeatureAccess>>();
725+
private _accessCache = new Map<PlusFeatures | AdvancedFeatures | undefined, Promise<FeatureAccess>>();
726726
private _accessCacheByRepo = new Map<string /* path */, Promise<RepoFeatureAccess>>();
727727
private clearAccessCache(): void {
728728
this._accessCache.clear();
729729
this._accessCacheByRepo.clear();
730730
}
731731

732-
async access(feature: PlusFeatures | undefined, repoPath: string | Uri): Promise<RepoFeatureAccess>;
733-
async access(feature?: PlusFeatures, repoPath?: string | Uri): Promise<FeatureAccess | RepoFeatureAccess>;
732+
async access(
733+
feature: PlusFeatures | AdvancedFeatures | undefined,
734+
repoPath: string | Uri,
735+
): Promise<RepoFeatureAccess>;
736+
async access(
737+
feature?: PlusFeatures | AdvancedFeatures,
738+
repoPath?: string | Uri,
739+
): Promise<FeatureAccess | RepoFeatureAccess>;
734740
@debug({ exit: true })
735-
async access(feature?: PlusFeatures, repoPath?: string | Uri): Promise<FeatureAccess | RepoFeatureAccess> {
741+
async access(
742+
feature?: PlusFeatures | AdvancedFeatures,
743+
repoPath?: string | Uri,
744+
): Promise<FeatureAccess | RepoFeatureAccess> {
736745
if (repoPath == null) {
737746
let access = this._accessCache.get(feature);
738747
if (access == null) {
@@ -754,14 +763,17 @@ export class GitProviderService implements Disposable {
754763
return access;
755764
}
756765

757-
private async accessCore(feature: PlusFeatures | undefined, repoPath: string | Uri): Promise<RepoFeatureAccess>;
758766
private async accessCore(
759-
feature?: PlusFeatures,
767+
feature: PlusFeatures | AdvancedFeatures | undefined,
768+
repoPath: string | Uri,
769+
): Promise<RepoFeatureAccess>;
770+
private async accessCore(
771+
feature?: PlusFeatures | AdvancedFeatures,
760772
repoPath?: string | Uri,
761773
): Promise<FeatureAccess | RepoFeatureAccess>;
762774
@debug({ exit: true })
763775
private async accessCore(
764-
feature?: PlusFeatures,
776+
feature?: PlusFeatures | AdvancedFeatures,
765777
repoPath?: string | Uri,
766778
): Promise<FeatureAccess | RepoFeatureAccess> {
767779
const subscription = await this.getSubscription();
@@ -775,7 +787,15 @@ export class GitProviderService implements Disposable {
775787
return { allowed: subscription.account?.verified !== false, subscription: { current: subscription } };
776788
}
777789

778-
if (feature === 'launchpad' || feature === 'startWork' || feature === 'associateIssueWithBranch') {
790+
if (
791+
feature === 'launchpad' ||
792+
feature === 'startWork' ||
793+
feature === 'associateIssueWithBranch' ||
794+
feature === 'generateStashMessage' ||
795+
feature === 'explainCommit' ||
796+
feature === 'cloudPatchGenerateTitleAndDescription' ||
797+
feature === 'generateChangelog'
798+
) {
779799
return { allowed: false, subscription: { current: subscription, required: SubscriptionPlanId.Pro } };
780800
}
781801

@@ -850,7 +870,7 @@ export class GitProviderService implements Disposable {
850870
return getRepoAccess.call(this, repoPath, true);
851871
}
852872

853-
async ensureAccess(feature: PlusFeatures, repoPath?: string): Promise<void> {
873+
async ensureAccess(feature: PlusFeatures | AdvancedFeatures, repoPath?: string): Promise<void> {
854874
const { allowed, subscription } = await this.access(feature, repoPath);
855875
if (allowed === false) throw new AccessDeniedError(subscription.current, subscription.required);
856876
}

src/plus/ai/aiProviderService.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { primaryAIProviders } from '../../constants.ai';
55
import type { AIGenerateDraftEventData, Source, TelemetryEvents } from '../../constants.telemetry';
66
import type { Container } from '../../container';
77
import { CancellationError } from '../../errors';
8+
import type { AdvancedFeatures, PlusFeatures } from '../../features';
89
import type { GitCommit } from '../../git/models/commit';
910
import { isCommit } from '../../git/models/commit';
1011
import type { GitRevisionReference } from '../../git/models/reference';
@@ -13,6 +14,7 @@ import { uncommitted, uncommittedStaged } from '../../git/models/revision';
1314
import { assertsCommitHasFullDetails } from '../../git/utils/commit.utils';
1415
import { showAIModelPicker } from '../../quickpicks/aiModelPicker';
1516
import { configuration } from '../../system/-webview/configuration';
17+
import { getContext } from '../../system/-webview/context';
1618
import type { Storage } from '../../system/-webview/storage';
1719
import { supportedInVSCodeVersion } from '../../system/-webview/vscode';
1820
import { debounce } from '../../system/function/debounce';
@@ -22,6 +24,7 @@ import { lazy } from '../../system/lazy';
2224
import type { Deferred } from '../../system/promise';
2325
import { getSettledValue } from '../../system/promise';
2426
import type { ServerConnection } from '../gk/serverConnection';
27+
import { ensureFeatureAccess } from '../gk/utils/-webview/acount.utils';
2528
import type { AIActionType, AIModel, AIModelDescriptor } from './models/model';
2629
import type { PromptTemplateContext } from './models/promptTemplates';
2730
import type { AIProvider, AIRequestResult } from './models/provider';
@@ -284,11 +287,56 @@ export class AIProviderService implements Disposable {
284287
return model;
285288
}
286289

290+
private async ensureOrgAccess(): Promise<boolean> {
291+
const orgEnabled = getContext('gitlens:gk:organization:ai:enabled');
292+
if (orgEnabled === false) {
293+
await window.showErrorMessage(`AI features have been disabled for your organization.`);
294+
return false;
295+
}
296+
297+
return true;
298+
}
299+
300+
private async ensureFeatureAccess(
301+
feature:
302+
| 'generateStashMessage'
303+
| 'explainCommit'
304+
| 'cloudPatchGenerateTitleAndDescription'
305+
| 'generateChangelog',
306+
title: string,
307+
source: Source,
308+
): Promise<boolean> {
309+
if (!(await this.ensureOrgAccess())) return false;
310+
311+
if (
312+
!(await ensureFeatureAccess(
313+
this.container,
314+
title,
315+
feature satisfies PlusFeatures | AdvancedFeatures,
316+
source,
317+
))
318+
) {
319+
return false;
320+
}
321+
322+
return true;
323+
}
324+
287325
async explainCommit(
288326
commitOrRevision: GitRevisionReference | GitCommit,
289327
sourceContext: Source & { type: TelemetryEvents['ai/explain']['changeType'] },
290328
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
291329
): Promise<AISummarizeResult | undefined> {
330+
if (
331+
!(await this.ensureFeatureAccess(
332+
'explainCommit' satisfies PlusFeatures,
333+
'Explaining commits requires an account with Pro access.',
334+
sourceContext,
335+
))
336+
) {
337+
return undefined;
338+
}
339+
292340
const diff = await this.container.git.diff(commitOrRevision.repoPath).getDiff?.(commitOrRevision.ref);
293341
if (!diff?.contents) throw new Error('No changes found to explain.');
294342

@@ -339,6 +387,8 @@ export class AIProviderService implements Disposable {
339387
progress?: ProgressOptions;
340388
},
341389
): Promise<AISummarizeResult | undefined> {
390+
if (!(await this.ensureOrgAccess())) return undefined;
391+
342392
const changes: string | undefined = await this.getChanges(changesOrRepo);
343393
if (changes == null) return undefined;
344394

@@ -377,6 +427,18 @@ export class AIProviderService implements Disposable {
377427
codeSuggestion?: boolean;
378428
},
379429
): Promise<AISummarizeResult | undefined> {
430+
if (
431+
!(await this.ensureFeatureAccess(
432+
'cloudPatchGenerateTitleAndDescription' satisfies PlusFeatures,
433+
`Generating ${
434+
options?.codeSuggestion ? 'code suggestion' : 'cloud patch'
435+
} descriptions requires an account with Pro access.`,
436+
sourceContext,
437+
))
438+
) {
439+
return undefined;
440+
}
441+
380442
const changes: string | undefined = await this.getChanges(changesOrRepo);
381443
if (changes == null) return undefined;
382444

@@ -423,6 +485,16 @@ export class AIProviderService implements Disposable {
423485
progress?: ProgressOptions;
424486
},
425487
): Promise<AISummarizeResult | undefined> {
488+
if (
489+
!(await this.ensureFeatureAccess(
490+
'generateStashMessage' satisfies PlusFeatures,
491+
'Generating stash messages requires an account with Pro access.',
492+
source,
493+
))
494+
) {
495+
return undefined;
496+
}
497+
426498
const changes: string | undefined = await this.getChanges(changesOrRepo);
427499
if (changes == null) {
428500
options?.generating?.cancel();
@@ -458,6 +530,16 @@ export class AIProviderService implements Disposable {
458530
source: Source,
459531
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
460532
): Promise<AIResult | undefined> {
533+
if (
534+
!(await this.ensureFeatureAccess(
535+
'generateChangelog' satisfies AdvancedFeatures,
536+
'Generating changelogs requires an account with Pro access.',
537+
source,
538+
))
539+
) {
540+
return undefined;
541+
}
542+
461543
const result = await this.sendRequest(
462544
'generate-changelog',
463545
async () => ({

src/plus/gk/subscriptionService.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -882,10 +882,10 @@ export class SubscriptionService implements Disposable {
882882
}
883883

884884
@log()
885-
async upgrade(source: Source | undefined, plan?: SubscriptionPlanId): Promise<void> {
885+
async upgrade(source: Source | undefined, plan?: SubscriptionPlanId): Promise<boolean> {
886886
const scope = getLogScope();
887887

888-
if (!(await ensurePlusFeaturesEnabled())) return;
888+
if (!(await ensurePlusFeaturesEnabled())) return false;
889889

890890
let aborted = false;
891891
const promo = await this.container.productConfig.getApplicablePromo(this._subscription.state);
@@ -919,7 +919,7 @@ export class SubscriptionService implements Disposable {
919919
getSubscriptionPlanPriority(this._subscription.plan.effective.id) >=
920920
getSubscriptionPlanPriority(plan ?? SubscriptionPlanId.Pro)
921921
) {
922-
return;
922+
return true;
923923
}
924924
}
925925
} catch {}
@@ -982,7 +982,7 @@ export class SubscriptionService implements Disposable {
982982
aborted = !(await openUrl(this.container.urls.getGkDevUrl('purchase/checkout', query)));
983983

984984
if (aborted) {
985-
return;
985+
return false;
986986
}
987987

988988
telemetry?.dispose();
@@ -1016,6 +1016,8 @@ export class SubscriptionService implements Disposable {
10161016
if (refresh) {
10171017
void this.checkUpdatedSubscription(source);
10181018
}
1019+
1020+
return true;
10191021
}
10201022

10211023
@gate<SubscriptionService['validate']>(o => `${o?.force ?? false}`)

src/plus/gk/utils/-webview/acount.utils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import type { Uri } from 'vscode';
12
import { window } from 'vscode';
23
import type { Source } from '../../../../constants.telemetry';
34
import type { Container } from '../../../../container';
5+
import type { AdvancedFeatures, PlusFeatures } from '../../../../features';
46

57
export async function ensureAccount(container: Container, title: string, source: Source): Promise<boolean> {
68
while (true) {
@@ -52,3 +54,37 @@ export async function ensureAccount(container: Container, title: string, source:
5254

5355
return true;
5456
}
57+
58+
export async function ensureFeatureAccess(
59+
container: Container,
60+
title: string,
61+
feature: PlusFeatures | AdvancedFeatures,
62+
source: Source,
63+
repoPath?: string | Uri,
64+
): Promise<boolean> {
65+
if (!(await ensureAccount(container, title, source))) return false;
66+
67+
while (true) {
68+
const access = await container.git.access(feature, repoPath);
69+
if (access.allowed) break;
70+
71+
const upgrade = { title: 'Upgrade to Pro' };
72+
const cancel = { title: 'Cancel', isCloseAffordance: true };
73+
const result = await window.showWarningMessage(
74+
`${title}\n\nUpgrade to Pro to access this feature.`,
75+
{ modal: true },
76+
upgrade,
77+
cancel,
78+
);
79+
80+
if (result === upgrade) {
81+
if (await container.subscription.upgrade(source)) {
82+
continue;
83+
}
84+
}
85+
86+
return false;
87+
}
88+
89+
return true;
90+
}

0 commit comments

Comments
 (0)