Git - branch protection (#152218)

Co-authored-by: João Moreno <joao.moreno@microsoft.com>
This commit is contained in:
Ladislau Szomoru
2022-06-16 16:25:03 +02:00
committed by GitHub
parent c3424ba3f6
commit 46d9d7acda
3 changed files with 44 additions and 24 deletions

View File

@@ -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<void> {
async run(opts?: { detached?: boolean }): Promise<void> {
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<void> {
override async run(opts?: { detached?: boolean }): Promise<void> {
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<string[]>('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);
}
}

View File

@@ -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<string[]>('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);
}

View File

@@ -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',