Screenshot from Control Freak rejecting a foxtrot merge.
Table of Contents
Control Freak for Bitbucket Server allows Bitbucket admins to define and encourage consistent git policy across all projects and repositories within their Bitbucket Server and Bitbucket Datacenter installations.
The add-on accomplishes this through independent rule groups. Admins can define Jira policy, branch naming policy, rebase policy, commit authorship policy, etc. The add-on also includes rules to help prevent common nuisances in git repos such as foxtrot merges, empty commits, or accidental multi-rewrite pushes.
A master ruleset is defined once through the global config screen. By default the policy is enabled for all normal non-empty repos, but per-project and per-repo kill switches are available. A subset of the rules can also be overridden per-project or per-repo.
Bitbucket comes with many powerful configuration options and add-ons to help enforce repository policy (e.g., branch permissions, hooks, merge checks). But setting these up for each newly created repository is repetitive and error-prone, and not everyone with the "Create New Repository" permissions is always aware of their organization's preferred setup.
Control Freak for Bitbucket Server solves this problem by allowing admins to define a global default that is applied to all repositories. Projects and repos where the global settings are inappropriate can opt-out or override to suit their own needs. This results in more repositories that comply with the organization's preferred setup, thanks to this "opt-out" approach rather than Bitbucket's out-of-the-box "opt-in" approach.
Enabling Your Global Git Policy
The very top of the global config screen includes the enable/disable control:
By default Control Freak for Bitbucket Server is enabled for regular repositories and regular forks, and disabled for all other types. Note: repositories can also be moved between the personal and project areas of Bitbucket. After a move, the configured policy will apply to all new commits, but older commits are grandfathered. The repository types are:
Authorship PolicyAuthorship policy inspects the "Author" and "Committer" metadata for each incoming commit, and accepts or rejects the commit based on the selected rules. The user's email address and display name (as recorded in the commit metadata) must match exactly the values stored in Bitbucket to satisfy these rules. By default all three of these rules are disabled.
Tag Pushing PolicyBy default most companies allow tag creation/deletion via Bitbucket UI, REST, and "git push". Unfortunately, allowing tag management through "git push" makes it nearly impossible to permanently delete tags. New tags propagate out to team members naturally via "git pull" and "git fetch", but tag updates (moves) and tag deletes do not. This leads to an undesired scenario where all deleted tags come back from the dead whenever any team member runs "git push --tags". By default Control Freak allows all tag pushes.
Block Large FilesBy default git itself has no inherent limit for file sizes, and users sometimes push extremely large files into Bitbucket (often by accident). Since these files will exist forever in the repository's history (even if later commits delete them), many teams prefer to reject pushes that contain large files. Enable this control to block pushes of new large files. Note: all previously accepted large files are grandfathered, and moves/renames to those files are not blocked.
Our implementation has one weakness: if a large binary file is pushed that happens to be byte-identical to a file already present in the repository's history (including dead or unreachable branches), our logic cannot block the push. But such pushes do not increase the repository size, thanks to git's de-duplication logic under the hood.
remote: ----- remote: Push rejected. Files larger than 8000000 bytes are not permitted remote: in this repository. The following file is too large: remote: remote: path/to/bigFile.jar (8722752 bytes) remote: remote: You can push large files to this repository using 'git lfs': remote: git lfs track <path> remote: remote: -----
Block Empty CommitsEmpty commits contain no delta or patch (no changes to any files or directories), and so they are essentially pointless noise. It's rare for developers to explicitly create and push empty commits (e.g., "git commit --allow-empty"), although they can be used to re-trigger CI builds. Sometimes squashes cause empty commits. We recommend blocking empty commits, but by default Control Freak allows them.
remote: ----- remote: Commit rejected. Empty commits not allowed on "master". remote: remote: Commit 78cf7dce4519e9d3 is an empty commit. remote: remote: A rebase will remove empty commits: remote: remote: git rebase remote: remote: If the rebase complains that origin/master is up to date, remote: you can use the --force-rebase flag. remote: -----
Block Multi-RewritesThis setting keeps developers safe from a pair of dangerous git commands (a.k.a. "git footguns"). One day we'll write a blog post about this, but here's a very brief explanation of the two dangerous commands:
remote: ----- remote: Push rejected. Multiple history rewrites remote: in a single push not allowed. remote: remote: Is your "push.default" config is set to "matching"? remote: Try setting it to "simple" instead. remote: remote: Trying to mirror a repo? In that case, disable remote: "Control Freak for Bitbucket Server" for this repository. remote: (Repository Settings -> Control Freak) remote: -----
Foxtrot PolicyWe wrote a long blog post about Foxtrot merges. Here's the shorter version: default "git pull" behaviour often creates a merge object that swaps the mainline for the feature branch in the git history, making historical analysis of mainlines difficult. Try to get developers to use "git pull --rebase" instead of regular "git pull". But in case they forget, enable this control across all branches to keep mainlines safe from nuisance foxtrot merges. Control Freak Default: Foxtrot Merges Blocked On All Branches.
remote: ----- remote: Commit rejected. Foxtrot merges not allowed on "master". remote: remote: Commit f4294cff03e5654f contains a foxtrot merge. remote: A rebase will repair the problem: remote: remote: git rebase remote: remote: Alternatively, redo the merge in the opposite direction. remote: -----
Branch Naming PolicyControls how new branches get named. This control checks branches created via "git push", Bitbucket Web UI, Bitbucket REST, and even new branches created from the Jira Web UI.
Note: configure the branching model by clicking: [Repository] --> Settings --> Branching Model.
remote: ----- remote: Push rejected branch name: "abc". remote: New branches must comply with the repository's branching model. remote: Allowed names: bugfix/*, feature/*, hotfix/*, release/* remote: -----
remote: ----- remote: Push rejected branch name: "abc". remote: New branch names must contain a Jira ticket. remote: -----
remote: ----- remote: Push rejected new branch: "ABC-1" remote: Your Bitbucket instance does not have an application link to a Jira instance. remote: Please configure your Bitbucket instance. remote: (Or disable the Jira branch check of this global hook.) remote: -----
remote: ----- remote: New branch names must contain a Jira ticket, but Bitbucket remote: cannot access Jira to see if "[ABC-1]" exists. remote: Your Bitbucket account is missing OAUTH credentials into Jira. remote: remote: Use this URL to set that up: remote: http://localhost:7990/bitbucket/plugins/servlet/applinks/oauth/login-dance/authorize remote: -----
remote: ----- remote: Push rejected new branch: "ABC-1". remote: ticket "ABC-1" does not exist in Jira. remote: -----
remote: ----- remote: Push rejected branch name: "ABC-1" remote: New branches must satisfy the configured branch regex. remote: Failed Regex: (.*xyz) remote: -----
Branch Pushing Policy (a.k.a. Protected Branches)Bitbucket already supports Protected Branches out-of-the-box, so why re-invent the wheel with Control Freak? Three reasons:
remote: ----- remote: Push rejected. remote: You are not allowed to delete branch "master". remote: -----
remote: ----- remote: Push rejected. remote: History rewrites not allowed on "release/ABC-1". remote: -----
remote: ----- remote: Push rejected. Direct pushes not allowed on "release/ABC-1". remote: This branch can only be modified through pull requests. remote: -----
remote: ----- remote: Push rejected new branch "release/v1.2.X". remote: Commits on the new branch MUST arrive via pull-request, remote: and the starting-point (2e633566324f9087e93c49b9b0c5fc35601d5add) remote: for this new branch has never passed through a pull-request. remote: -----
Pull Request PolicyThe controls below only apply to pull-requests. Direct pushes do not trigger these controls, and so to properly enforce these policies on a given branch, you must also set the Always Require Pull-Requests control.
Rebase & Squash PolicyThese settings are used to enforce specific structural constraints on the branch-to-be-merged before it can be merged. The final merge itself will not necessarily be a "--ff" or "--squash" merge, since the underlying merge mechanics are controlled by the merge strategies Bitbucket is configured to use, and not by Control Freak.
For all practical purposes, "Require Fast-Forwardable Merges" is equivalent to enforcing a rebase-before-merge policy on the selected branches (especially if foxtrots are blocked). We recommend setting the "--ff" merge strategy as your default strategy if enabling this control. Set the "--ff" merge strategy per-project in Bitbucket and enjoy the resulting extra clean git commit history. Note: we do NOT recommend setting "--ff-only" as the default strategy, since that makes it impossible to merge pull-requests that span long-lived branches. We think "--ff" is the better choice.
Notice that both of these controls say "Merges-between not blocked." This is important, since it essentially allows admins to partition branches into two sets: long-lived and short-lived. The selected branches become the long-lived branches, and rebases/squashes between them are not required (and hopefully disallowed via other controls above).
There is 1 issue preventing you from merging this pull request.
There is 1 issue preventing you from merging this pull request.
Approval PolicyBitbucket natively provides similar functionality (via the "Minimum Approvals" merge check as well as the default reviewers feature), but Bitbucket's features cannot be configured or activated globally. Control Freak lets admins define a default global Approval Policy, and further enhances these checks to better account for self-approvals and read-only users.
Regarding self-approvals, out of the box, Bitbucket does not allow the user that created the pull-request to approve it, but that user might not be the same user who authored the commits. Administrators can check the "Ignore self-approvals" control here to further restrict the set of users who are allowed to approve the pull-request. Note: self-approvals are still permitted, but they don't count towards clearing the vote threshold (they become non-binding informational votes, similar to a +1 in Gerrit).
Control Freak examines the GIT_AUTHOR_EMAIL value inside each commit on the pull-request to determine if votes should be categorized as self-approvals or not. Control Freak also examines the GIT_COMMITTER_EMAIL value inside each commit. This means users that rebase or amend other people's work will also have their approvals on the pull-request ignored. This control works best if Authorship Policy is active with the "Committer must be current logged-in User" setting, otherwise users can circumvent Approval Policy by using coworker metadata in their commits instead of their own.
Read-only users can be a problem in organizations that allow some repositories to be "public" within their on-prem Bitbucket instance. Read-only users can contribute positive pull-request reviews to these "public" repositories, and Bitbucket's native "Minimum Approvals" merge-check allows those reviews to count towards clearing the threshold. Control Freak can be configured to either count such read-only reviews or ignore them.
All of the Approval Policy controls provided by Control Freak require co-installation of our paid PR-Booster plugin.
There is 1 issue preventing you from merging this pull request.
Disapproval PolicyControls whether pull-requests with "needs-work" reviews are allowed to merge. Essentially establishes a "Gerrit" style of code review, with "needs-work" reviews equivalent to a -2 vote in Gerrit. Unlike the Approval Policy (above), Control Freak does not offer an ability to ignore self-disapprovals since self-disapprovals should be taken seriously. Enabling disapproval policy requires co-installation of our paid PR-Booster plugin.
There is 1 issue preventing you from merging this pull request.
Jira PolicyWe call this section Jira Policy, but it could also be called "Commit Message Policy", since many of the controls here can be used without Jira. The top-level control ("Apply Jira Policy Against") specifies the branches that will be protected by this section. Branches that fall outside the selected choices are ignored and their commit messages are free to contain anything.
Notice also how this top-level control says "Merges-between not blocked." This is important, since it allows commits to merge back and forth between master, develop, production, and release/* branches (assuming those are the protected branches) without re-triggering additional Jira checks. Only when merges and pushes transition a commit from an unprotected branch to a protected branch do the Jira checks get triggered. Commits that exist only on a developer's workstation are always considered unprotected.
General Rules (No Connection To Jira Required)The "reference at least one Jira ticket" here is an offline check. It does not check if any identified tickets actually exist in Jira. As long as some text within the commit message looks like a Jira ticket (e.g., "TKT-123"), then the check is satisfied. The "Advanced Settings" below further specify exactly how Control Freak should look for these Jira tickets within the commit message (e.g., within brackets or not, first-line or anywhere, etc), but the underlying regex itself is hard-coded to "[A-Z][A-Z0-9]*-\\d+" and cannot be changed.
The 2nd control, "Match regex," checks that all commit messages satisfy the supplied regular expression, and has nothing to do with Jira.
Note: the "ignore submodule updates" exemption, when enabled, ignores commits that contain exclusively submodule updates. If a commit contains a mix of regular edits and submodule updates, then all checks under Jira Policy are applied.
Quick Note: Ignoring Clean Rebases / Cherry-PicksBy default the "Ignore clean rebases and cherry-picks" Jira exemption is enabled. This helps in situations where Jira online checks were initially satisfied (e.g., valid-status, valid-assignee), and the commits themselves have not been modified in any way, but for various reasons a rebase or cherry-pick is now required. As long as the rebase or cherry-pick is clean (the underlying patch is unchanged) this exemption allows the commits through, even if Jira status or assignee has now changed (e.g., perhaps the ticket is now "Closed" but the git commit needs to be back-ported to an older release through a cherry-pick).
Ticket Checks (Require A Connection To Jira)These are the standard online checks that Jira commit checkers are expected to do. Since v2020.09.21 Control-Freak queries the "Primary Jira" application link as well as all secondary application links.
Quick Note: Concerning Multi-Jira EnvironmentsControl-Freak maintains an internal mapping of Jira project affinities against the application links to minimize network traffic. This means that if a given Jira instance returns valid data for a given ticket (e.g., MYPROJ-123), all subsequent Jira ticket queries against that project will only ever be sent to that same Jira instance. For example a later query for MYPROJ-234 would only be sent to the same Jira instance that returned valid data for MYPROJ-123 - all other Jira application links would be ignored in this example. Control-Freak assumes a company would never run two distinct MYPROJ Jira projects in parallel on two different Jira instances.
Project Whitelist (Requires A Connection To Jira)A given "git push" might refer to several Jira tickets, but sometimes organizations want the Jira validation checks to only apply to a subset of those tickets. The Project Whitelist gives organizations a way to specify that subset. For example, a commit message might say:
"ABC-323 - fix for downstream problem in XYZ-2832"If the whitelist only contains project "ABC" then only the "ABC-323" ticket will have the online checks applied (e.g., match JQL, match assignee, valid status, etc). The "XYZ-2832" ticket will be ignored. This control is normally left blank at the Global level and overridden with specific projects at the Project and Repository levels. If the whitelist is empty (the default), then the "Ticket Checks" are applied to all Jira tickets found in the pertinent commit messages.
Advanced Settings (No Connection To Jira Required)
remote: ----- remote: Commit 347f2a9089f5b39a rejected. remote: It does not reference a Jira ticket in its commit message. remote: Note: Jira references must be in the first line of the commit message. remote: Also, they must be in brackets, e.g., [TKT-123]. remote: remote: To see the commit message: remote: remote: git show -s 347f2a9089f5b39a remote: remote: To edit the commit message: remote: remote: git rebase --interactive remote: remote: (Mark the commits to edit with 'reword' in the dialog). remote: -----
remote: ----- remote: Commit messages on "master" must reference valid Jira tickets, remote: but Bitbucket cannot access Jira to see if "[ABC-1]" exists. remote: Your Bitbucket account is missing OAUTH credentials into Jira. remote: remote: Use this URL to set that up: remote: http://localhost:7990/bitbucket/plugins/servlet/applinks/oauth/login-dance/authorize remote: -----
remote: ----- remote: Commit rejected. remote: Commits on "master" must satisfy Jira policy. remote: The following Jira tickets do not exist on your Jira server: remote: remote: [ABC-1]. remote: remote: Please remove references to those tickets from your commit messages. remote: -----
remote: ----- remote: Push rejected. Commits to "master" must satisfy Jira policy. remote: The following Jira tickets have invalid statuses for receiving commits: remote: remote: [ABC-1] - Done. remote: remote: Either remove references to those tickets from your commit messages, remote: or adjust the tickets in Jira so they satisfy the status check. remote: -----
remote: ----- remote: Push rejected. Commits to "master" must satisfy Jira policy. remote: Some referenced Jira tickets failed the JQL validation check. remote: remote: JQL: "TYPE != task" remote: remote: Failing Jira tickets: remote: remote: [ABC-1]. remote: remote: Either remove references to those tickets from your commit messages, remote: or adjust the tickets in Jira so they satisfy the JQL validation. remote: ----- remote: ----- remote: Push rejected. remote: Commits to "master" must satisfy Jira policy. remote: Commit 0001b282b756e1d1 was authored by [firstname.lastname@example.org], remote: but "TKT-1" is currently assigned to [unassigned]. remote: -----