Reputation: 39573
Consider this test script.
#!/bin/sh -x
rm -rf test clone*
# create a test repo containing one branch called "branch"
git init test
cd test
echo foo > foo
git add foo
git commit -am "initial commit"
git branch branch
# create three clones of the repo
cd ..
git clone test clone1
git clone test clone2
git clone test clone3
# push a commit on the branch in clone1
cd clone1
git checkout branch
echo bar > foo
git commit -am "bar"
git push origin branch
# from clone2, delete the branch in the origin
cd ../clone2
git push --delete origin branch
# from clone3, push a commit on the branch
cd ../clone3
git checkout branch
echo baz > foo
git commit -am "baz"
git push origin branch
This script creates a test repo with three clones. Clone 1 pushes a commit on a branch; Clone 2 pushes a deletion of the branch; Clone 3 pushes a commit on a stale version of the branch.
If Clone 2 hadn't deleted the branch, then Clone 3 would have gotten an error when trying to push the branch. e.g. if you comment out the "clone2" lines, Clone 3's push is correctly rejected.
But after Clone 2 deletes the branch, there's nothing preventing Clone 3 from pushing a stale copy of the branch; the Clone 3 user probably wouldn't even know that the branch had ever been deleted, or that the branch was stale at all.
I understand why this is happening: deleting git branches delete them from history. From git's perspective, Clone 3 is creating a fresh new branch that just happens to not include Clone 1's deleted commit.
But this is not what I want. I would like for Clone 3 to get some kind of error or warning message when trying to push to the deleted branch, something that says, "Hey, buddy! That branch was deleted! You'll need to --force the push if you want to recreate it."
Is that possible?
Upvotes: 2
Views: 640
Reputation: 488519
You can't quite get at the --force
or +
flag, but you can have a pre-receive
or update
hook on the bare repository to which pushes are done.
In either kind of script you are told which reference(s) is/are to be updated. A branch is simply a reference whose full name starts with refs/heads/
(the remainder of the name is the branch name, which may include more slashes). You can check whether this is in a table of "forbidden" names and if so, reject the push.
(Other names begin with, e.g., refs/tags/
if they are tags, refs/notes/
if they are notes, and so on.)
The key difference between the pre-receive
and update
hooks is that the former is given a list of all proposed reference updates, all at once, on its standard input, while the latter is given proposed reference updates one at a time. Either can say "allow" or "deny", but because the pre-receive
hook runs only once with all updates, its allow/deny is all-or-nothing: you inspect all updates once and determine at that time whether to allow all of them. The update
hook runs once per reference and can deny some individual updates while permitting others.
Both hooks receive three items per update: the name, the previous (aka old) SHA-1, and the proposed new SHA-1. In both cases, a reference name is being created if the old SHA-1 is all-zeros; it's being deleted if the new SHA-1 is all-zeros; and it exists but is being moved from one commit1 to another if both SHA-1s are not the special all-zeros one.
Neither hook gets any indication of whether this is a forced push. That means that if you want to allow some method of deliberately re-creating one of these forbidden branches, you must come up with some alternative.
One possibility is to have certain "blessed" users: those who (at least in theory) know what they're doing are allowed to re-create such a branch. (This is usually the preferred solution, I believe.)
Another (rather hacky) method is to use multiple hooks. In the pre-receive hook, if one of the references to be updated has some special name (e.g., "refs/force"), allow the re-creation of branches; then, in an update hook, reject that particular special name, or use a post-receive hook to delete the "force" ref.
It's OK to have the pre-receive hook allow all, and then the update hook deny some: the result is simply to allow those that are not individually denied. The output seen by the users may be a little scary/confusing, though: they'll get a rejection message and have to realize that it's only for some particular ref update, not for the push as a whole.
You'd have to store the list of "deleted" branches somewhere (perhaps in a file maintained by the hooks, and updated as branches are deleted).
Note that if you use gitolite it has a lot of this kind of control built in. It takes over some of the hooks, and requires that everyone push as a particular user, so it operates a bit differently than just using the raw hooks the way I described above.
1Or tag or other object, but branch names should always point to commits. Tag names normally point either to commits (lightweight tags) or tag objects (annotated tags). It's possible to have a reference point directly to a blob or tree object but neither branch nor tag names should do so.
Upvotes: 3