If I try to remove a branch from an Azure DevOps GIT repository, the Force Push permission is required, according to the error message that is shown:
However, allowing "force pushes" also allows rewriting history. This is even explicitly stated at the place these rights are configured.
In my understanding of Git, branches are nothing more than bookmarks/shortcuts to changes. In a way, they're a special kind of tag. If these are discarded at during a merge, no force push right is required.
I'd like to authorize people to be able to delete branches without giving them the right to mess with history. They shouldn't be able to edit/remove actual change set, but they can do all the want to these special kinds of tags. How do I configure that?
Or am I misunderstanding the amount of history one can rewrite by allowing force pushes? All I'd like preserved is what code change was checked in by whom. Can this history be broken by allowing force pushes?
I'd like to authorize people to be able to delete branches without giving them the right to mess with history.
Based on the available permission options, you can't achieve exactly what you're asking, however, it turns out that there's probably a better way anyway. If tl;dr applies then skip to the bottom section: "Where you probably want to land".
Question: What is a "force push"?
We usually say "a force push rewrites history." Another way to think about this which may be more useful in the context of this question is: A "force push" is required anytime you wish to remove one or more commits from the branch's reachable history. If you aren't doing that, and the tip of the remote branch is reachable by the new tip commit you are pushing, then a regular push will suffice.
Question: Why are "force push" and "delete branches" lumped together into the same permission?
Consider a branch that reaches 3 commits: A-B-C
Add one commit: A-B-C-D
= regular push.
Remove one commit: A-B
= force push.
Replace one commit completely (i.e. remove one and add one commit): A-B-X
= force push.
Modify one commit slightly (i.e. remove one and add one commit): A-B-C'
= force push.
Remove all commits (i.e. delete the branch) = force push.
If we define a "force push" as "removing one or more commits from a branch", then deleting a branch is conceptually a subset of that (removing all commits), and there is a special syntax for deleting a branch: git push -d origin my-branch
or git push origin :my-branch
.
Now let's suppose we did what you asked: We could define "force push" as "remove one or more commits, but not all commits", and then separate the permissions such that you enable "delete a branch" but do not allow "force push". As pointed out in Romain Valeri's comment, there's nothing to stop someone from deleting a branch, and then re-using that branch name to push out a new commit- effectively achieving a "force push" anyway. That being said, if you added a third permission for "Create a new branch", then perhaps your desire would work. However, allowing people to delete a branch but not create a branch would likely cause more pain than it solves in most workflows. (This doesn't mean no one would find value in it though...)
Question: When is it OK to use "force push"?
This is highly dependent on your situation and preferences. Some people believe the answer is "never", though that strict dogma is probably in the minority view. The majority position is to disallow force pushing shared branches, and allowing force pushing otherwise. In general, and especially in private repos, it's pretty common for people to have their own personal branches that they are working on, and therefore regularly rebasing those branches against a shared target is encouraged, so force pushing personal branches would be considered a normal and frequent occurrence. In a private AzDO repo it therefore makes sense to allow both deleting and force pushing a branch, as long as it's a personal branch.
For all users that will contribute to any AzDO Git repos, by default, set:
Allow for "Contribute" and "Force Push".
Not set for everything else.
Note these settings will apply to most branches.
For specific protected branches, use branch policies. This will require a Pull Request (with many configurable options) in order to merge into protected branches, and effectively also prevents anyone from force pushing or deleting these branches.
For protected branches, if needed, you can optionally add additional security by turning off inheritance, and then setting explicit security if required. In that case you could add "Contribute" for the set of users that can complete PRs into the branch, but (probably) don't include "Force Push" this time1. Additional permission examples might include:
I think it's important to point out that behind the scenes, AzDO actually assigns meaning to the creator of a branch. This concept doesn't actually exist in Git, but (by trial and error I've confirmed that) AzDO defines it as:
The branch creator is the most recent authenticated user to push a branch name when it didn't exist.
Note this means that the branch creator can change, for example if the branch is deleted and then someone else pushes a branch of the same name- the last person to push it while it doesn't exist becomes the creator. Also, the person to push is the person that is authenticated in AzDO (either in the client or in the UI); this person has nothing to do with the author of the commits on the branch.
The interesting part here is that the "creator" of a branch automatically gets additional permissions to that branch, including force-push permissions, which means that if you don't enable force-push for everyone by default like I recommend above, this person (or an admin or group of people specifically given force push permissions) can still delete their own branch. IMHO this was a really good attempt to avoid giving everyone force-push permissions, but in practice I've found it doesn't work. The problem is that when you select the option to delete your branch upon completing a PR, it will only work if the "creator" of that branch is the person to complete the PR. If anyone else completes the PR, the branch deletion will fail. This is a significant enough problem in my workflow with many people completing other people's PRs that we have to give everyone force-push permission by default.
1 Note that once branch policies are enabled, in order to force push you must have "Allow" set for two permissions: "Force Push" and "Bypass polices when pushing". This means that it's OK for any user to have one or the other, as long as they don't have both. This is why you "probably" want to remove "force push" permission from everyone on protected branches, for sanity purposes, but it may not be actually necessary to do so.