One possible solution is a client side hook (pre-rebase
):
One other thing we can do here is make sure the user doesn’t push non-fast-forwarded references. To get a reference that isn’t a fast-forward, you either have to rebase past a commit you’ve already pushed up or try pushing a different local branch up to the same remote branch.
Presumably, the server is already configured with receive.denyDeletes
and receive.denyNonFastForwards
to enforce this policy, so the only accidental thing you can try to catch is rebasing commits that have already been pushed.
An example pre-rebase script that checks for that: It gets a list of all the commits you’re about to rewrite and checks whether they exist in any of your remote references.
If it sees one that is reachable from one of your remote references, it aborts the rebase.
#!/usr/bin/env ruby
base_branch = ARGV[0]
if ARGV[1]
topic_branch = ARGV[1]
else
topic_branch = "HEAD"
end
target_shas = `git rev-list #{base_branch}..#{topic_branch}`.split("\n")
remote_refs = `git branch -r`.split("\n").map { |r| r.strip }
target_shas.each do |sha|
remote_refs.each do |remote_ref|
shas_pushed = `git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
if shas_pushed.split("\n").include?(sha)
puts "[POLICY] Commit #{sha} has already been pushed to #{remote_ref}"
exit 1
end
end
end
But that remains a client-side solution, which can be circumvented, and has to be deployed/activated repos by repos.
On the server side, this is not easy to enforce (beside the config receive.denyNonFastForwards
).
But since git 1.8.5, instead of a simple git push --force
(after rebase), you now can do a git push --force-with-lease.
You assume you took the lease on the ref when you fetched to decide what the rebased history should be, and you can push back only if the lease has not been broken.
At least, you know if your force push will cause problem or not.
But that remains something a user has to think to do.