diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index dd5f2f9a029..c231c44ee94 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,7 +5,6 @@ import * as os from 'os'; import * as path from 'path'; -import * as picomatch from 'picomatch'; import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; @@ -27,24 +26,25 @@ const localize = nls.loadMessageBundle(); class CheckoutItem implements QuickPickItem { protected get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); } - get label(): string { return this.ref.name || this.shortCommit; } + get label(): string { return this.ref.name ? `${this.ref.name}${this.repository.isBranchProtected(this.ref.name) ? ' $(lock-small)' : ''}` : this.shortCommit; } get description(): string { return this.shortCommit; } - constructor(protected ref: Ref) { } + constructor(protected repository: Repository, protected ref: Ref) { } - async run(repository: Repository, opts?: { detached?: boolean }): Promise { + async run(opts?: { detached?: boolean }): Promise { const ref = this.ref.name; if (!ref) { return; } - await repository.checkout(ref, opts); + await this.repository.checkout(ref, opts); } } class CheckoutTagItem extends CheckoutItem { + override get label(): string { return this.ref.name || this.shortCommit; } override get description(): string { return localize('tag at', "Tag at {0}", this.shortCommit); } @@ -52,21 +52,22 @@ class CheckoutTagItem extends CheckoutItem { class CheckoutRemoteHeadItem extends CheckoutItem { + override get label(): string { return this.ref.name || this.shortCommit; } override get description(): string { return localize('remote branch at', "Remote branch at {0}", this.shortCommit); } - override async run(repository: Repository, opts?: { detached?: boolean }): Promise { + override async run(opts?: { detached?: boolean }): Promise { if (!this.ref.name) { return; } - const branches = await repository.findTrackingBranches(this.ref.name); + const branches = await this.repository.findTrackingBranches(this.ref.name); if (branches.length > 0) { - await repository.checkout(branches[0].name!, opts); + await this.repository.checkout(branches[0].name!, opts); } else { - await repository.checkoutTracking(this.ref.name, opts); + await this.repository.checkoutTracking(this.ref.name, opts); } } } @@ -219,7 +220,7 @@ function createCheckoutItems(repository: Repository): CheckoutItem[] { checkoutTypes = checkoutTypeConfig; } - const processors = checkoutTypes.map(getCheckoutProcessor) + const processors = checkoutTypes.map(type => getCheckoutProcessor(repository, type)) .filter(p => !!p) as CheckoutProcessor[]; for (const ref of repository.refs) { @@ -234,8 +235,8 @@ function createCheckoutItems(repository: Repository): CheckoutItem[] { class CheckoutProcessor { private refs: Ref[] = []; - get items(): CheckoutItem[] { return this.refs.map(r => new this.ctor(r)); } - constructor(private type: RefType, private ctor: { new(ref: Ref): CheckoutItem }) { } + get items(): CheckoutItem[] { return this.refs.map(r => new this.ctor(this.repository, r)); } + constructor(private repository: Repository, private type: RefType, private ctor: { new(repository: Repository, ref: Ref): CheckoutItem }) { } onRef(ref: Ref): void { if (ref.type === this.type) { @@ -244,14 +245,14 @@ class CheckoutProcessor { } } -function getCheckoutProcessor(type: string): CheckoutProcessor | undefined { +function getCheckoutProcessor(repository: Repository, type: string): CheckoutProcessor | undefined { switch (type) { case 'local': - return new CheckoutProcessor(RefType.Head, CheckoutItem); + return new CheckoutProcessor(repository, RefType.Head, CheckoutItem); case 'remote': - return new CheckoutProcessor(RefType.RemoteHead, CheckoutRemoteHeadItem); + return new CheckoutProcessor(repository, RefType.RemoteHead, CheckoutRemoteHeadItem); case 'tags': - return new CheckoutProcessor(RefType.Tag, CheckoutTagItem); + return new CheckoutProcessor(repository, RefType.Tag, CheckoutTagItem); } return undefined; @@ -1584,11 +1585,8 @@ export class CommandCenter { } // Branch protection - const branchProtection = config.get('branchProtection')!.map(bp => bp.trim()).filter(bp => bp !== ''); const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!; - const branchIsProtected = branchProtection.some(bp => picomatch.isMatch(repository.HEAD?.name ?? '', bp)); - - if (branchIsProtected && (branchProtectionPrompt === 'alwaysPrompt' || branchProtectionPrompt === 'alwaysCommitToNewBranch')) { + if (repository.isBranchProtected() && (branchProtectionPrompt === 'alwaysPrompt' || branchProtectionPrompt === 'alwaysCommitToNewBranch')) { const commitToNewBranch = localize('commit to branch', "Commit to a New Branch"); let pick: string | undefined = commitToNewBranch; @@ -1859,7 +1857,7 @@ export class CommandCenter { const item = choice as CheckoutItem; try { - await item.run(repository, opts); + await item.run(opts); } catch (err) { if (err.gitErrorCode !== GitErrorCodes.DirtyWorkTree) { throw err; @@ -1871,10 +1869,10 @@ export class CommandCenter { if (choice === force) { await this.cleanAll(repository); - await item.run(repository, opts); + await item.run(opts); } else if (choice === stash) { await this.stash(repository); - await item.run(repository, opts); + await item.run(opts); await this.stashPopLatest(repository); } } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 7a765bee11f..0190ce2c7be 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; +import * as picomatch from 'picomatch'; import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, Tab, TabInputTextDiff, TabInputNotebookDiff, RelativePattern } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; @@ -867,6 +868,7 @@ export class Repository implements Disposable { private isRepositoryHuge: false | { limit: number } = false; private didWarnAboutLimit = false; + private isBranchProtectedMatcher: picomatch.Matcher | undefined; private resourceCommandResolver = new ResourceCommandResolver(this); private disposables: Disposable[] = []; @@ -985,6 +987,10 @@ export class Repository implements Disposable { } }, null, this.disposables); + const onDidChangeBranchProtection = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.branchProtection', root)); + onDidChangeBranchProtection(this.updateBranchProtectionMatcher, this, this.disposables); + this.updateBranchProtectionMatcher(); + const statusBar = new StatusBarCommands(this, remoteSourcePublisherRegistry); this.disposables.push(statusBar); statusBar.onDidChange(() => this._sourceControl.statusBarCommands = statusBar.commands, null, this.disposables); @@ -2211,6 +2217,21 @@ export class Repository implements Disposable { } } + private updateBranchProtectionMatcher(): void { + const scopedConfig = workspace.getConfiguration('git', Uri.file(this.repository.root)); + const branchProtectionGlobs = scopedConfig.get('branchProtection')!.map(bp => bp.trim()).filter(bp => bp !== ''); + + if (branchProtectionGlobs.length === 0) { + this.isBranchProtectedMatcher = undefined; + } else { + this.isBranchProtectedMatcher = picomatch(branchProtectionGlobs); + } + } + + public isBranchProtected(name: string = this.HEAD?.name ?? ''): boolean { + return this.isBranchProtectedMatcher ? this.isBranchProtectedMatcher(name) : false; + } + dispose(): void { this.disposables = dispose(this.disposables); } diff --git a/extensions/git/src/statusbar.ts b/extensions/git/src/statusbar.ts index 7cf06e0ed4f..f5b2e3e87c8 100644 --- a/extensions/git/src/statusbar.ts +++ b/extensions/git/src/statusbar.ts @@ -24,7 +24,8 @@ class CheckoutStatusBar { get command(): Command | undefined { const rebasing = !!this.repository.rebaseCommit; - const title = `$(git-branch) ${this.repository.headLabel}${rebasing ? ` (${localize('rebasing', 'Rebasing')})` : ''}`; + const isBranchProtected = this.repository.isBranchProtected(); + const title = `$(git-branch) ${this.repository.headLabel}${rebasing ? ` (${localize('rebasing', 'Rebasing')})` : ''}${isBranchProtected ? ' $(lock-small)' : ''}`; return { command: 'git.checkout',