mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-25 04:36:23 +00:00
Git - branch protection (#152218)
Co-authored-by: João Moreno <joao.moreno@microsoft.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user