diff --git a/extensions/git/package.json b/extensions/git/package.json index 10d4faf8d63..c3c93123829 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1854,6 +1854,29 @@ "description": "%config.branchPrefix%", "default": "" }, + "git.branchProtection": { + "type": "array", + "markdownDescription": "%config.branchProtection%", + "items": { + "type": "string" + }, + "default": [] + }, + "git.branchProtectionPrompt": { + "type": "string", + "description": "%config.branchProtectionPrompt%", + "enum": [ + "alwaysCommit", + "alwaysCommitToNewBranch", + "alwaysPrompt" + ], + "enumDescriptions": [ + "%config.branchProtectionPrompt.alwaysCommit%", + "%config.branchProtectionPrompt.alwaysCommitToNewBranch%", + "%config.branchProtectionPrompt.alwaysPrompt%" + ], + "default": "alwaysPrompt" + }, "git.branchValidationRegex": { "type": "string", "description": "%config.branchValidationRegex%", @@ -2602,6 +2625,7 @@ "byline": "^5.0.0", "file-type": "^7.2.0", "jschardet": "3.0.0", + "picomatch": "2.3.1", "vscode-nls": "^4.0.0", "vscode-uri": "^2.0.0", "which": "^1.3.0" @@ -2611,6 +2635,7 @@ "@types/file-type": "^5.2.1", "@types/mocha": "^9.1.1", "@types/node": "16.x", + "@types/picomatch": "2.3.0", "@types/which": "^1.0.28" }, "repository": { diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 0562d14e52e..ec691130508 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -120,6 +120,11 @@ "config.checkoutType.tags": "Tags", "config.checkoutType.remote": "Remote branches", "config.branchPrefix": "Prefix used when creating a new branch.", + "config.branchProtection": "List of protected branches. By default, a prompt is shown before changes are committed to a protected branch. The prompt can be controlled using the `#git.branchProtectionPrompt#` setting.", + "config.branchProtectionPrompt": "Controls whether a prompt is being before changes are committed to a protected branch.", + "config.branchProtectionPrompt.alwaysCommit": "Always commit changes to the protected branch.", + "config.branchProtectionPrompt.alwaysCommitToNewBranch": "Always commit changes to a new branch.", + "config.branchProtectionPrompt.alwaysPrompt": "Always prompt before changes are committed to a protected branch.", "config.branchRandomNameDictionary": "List of dictionaries used when the branch name that is randomly generated. Supported values: `adjectives`, `animals`, `colors`, `numbers`.", "config.branchRandomNameEnable": "Controls whether a random name is generated when creating a new branch.", "config.branchValidationRegex": "A regular expression to validate new branch names.", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 3864142817c..a3199cffc3a 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,6 +5,7 @@ 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 } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; @@ -1496,6 +1497,36 @@ export class CommandCenter { opts.all = 'tracked'; } + // 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')) { + const commitToNewBranch = localize('commit to branch', "Commit to a New Branch"); + + let pick: string | undefined = commitToNewBranch; + + if (branchProtectionPrompt === 'alwaysPrompt') { + const message = localize('confirm branch protection commit', "You are trying to commit to a protected branch and you might not have permission to push your commits to the remote.\n\nHow would you like to proceed?"); + const commit = localize('commit changes', "Commit Anyway"); + + pick = await window.showWarningMessage(message, { modal: true }, commitToNewBranch, commit); + } + + if (!pick) { + return false; + } else if (pick === commitToNewBranch) { + const branchName = await this.promptForBranchName(repository); + + if (!branchName) { + return false; + } + + await repository.branch(branchName, true); + } + } + await repository.commit(message, opts); const postCommitCommand = config.get<'none' | 'push' | 'sync'>('postCommitCommand'); diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index cfeee5c745e..60b91dbb320 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -36,6 +36,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== +"@types/picomatch@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@types/picomatch/-/picomatch-2.3.0.tgz#75db5e75a713c5a83d5b76780c3da84a82806003" + integrity sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g== + "@types/which@^1.0.28": version "1.0.28" resolved "https://registry.yarnpkg.com/@types/which/-/which-1.0.28.tgz#016e387629b8817bed653fe32eab5d11279c8df6" @@ -71,6 +76,11 @@ jschardet@3.0.0: resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +picomatch@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + vscode-nls@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002"