Reputation: 1312
I have branch, which I want to allow go get merged, from another branch. BUT I don't allow it to merge to another branch(e.g Dev, master).
Thanks for help
Upvotes: 2
Views: 2751
Reputation: 483
You can get close to a solution by adapting this answer into either a pre-push
hook locally (in which case all your devs will need the hook installed, so you might also consider writing a script that lets you keep your hooks in your repo to make it easier for your devs to install), or you can reformulate the solution into a pre-receive
hook on the origin, for more strict enforcement.
The idea is to use a tag to identify a long-ago "bad" commit (tag the first commit on the "bad" branch) that should never be allowed to be merged into your clean branches, and then the hook looks at git merge-base
between the ref you're pushing and the "bad" tags, rejecting the push if they coincide.
Here's my adaptation of that answer for use as a pre-push
hook. It includes some logic to allow the branches that should contain the bad commits to be pushed, and also to not bother checking pushing tags and other non-branch refs.
#!/bin/bash
#
# This prevents a push to the named remote if the push has any "forbidden"
# commits (indicated by a tag matching the forbidden_pat variable) as any
# ancestors. It prevents a known-bad branch from having its history
# intermingled with other branches.
#
# Adapted from http://stackoverflow.com/a/13384768
# which remote should we protect?
protected_remote="origin"
# which branch should allow forbidden commits?
skip_branch_regex='^test[1-3]$'
# what do our forbidden tags look like?
forbidden_pat="forbidden/*"
# only check if this is the remote we care about
if [ "$1" != "$protected_remote" ]; then
exit 0
fi
# we might be trying to push multiple branches (e.g. push.default = matching).
# See http://git-scm.com/docs/githooks#_pre_push for the format
while read line
do
words=($line)
branch="${words[0]##refs/heads/}"
ref="${words[1]}"
# don't proceed if this is not a branch (IOW, if our substitution didn't
# change anything; we only want to check pushing branch pointers, not tags or
# other refs
if [ "$branch" = "${words[0]}" ]; then
continue
fi
# don't proceed if we're trying to push a branch that can receive the bad
# commit
if [[ "$branch" =~ $skip_branch_regex ]]; then
continue
fi
# check each forbidden tag to see if it's an ancestor of the ref we're
# trying to push; reject if it is.
for forbidden in $(git tag -l "$forbidden_pat"); do
if [ $(git merge-base "$forbidden" "$ref") = $(git rev-parse "$forbidden") ]; then
echo "Push to $branch contains BAD commit $forbidden." >&2
exit 1
fi
done
done
exit 0
This has worked acceptably in a multi-developer environment.
All that being said, I agree with Mark Adelsberger's answer that this is not a problem to be solved programatically, and instead is one that should be solved in other ways (training, automated tests, a simplified branching workflow, etc.).
Upvotes: 1
Reputation: 45819
You'll find no easy way to do this. I can tell you, roughly, what you'd need to do; but there are some significant hurdles you'll have to overcome, and for those you're on your own because I'm not dumping enough time to fully develop what I believe is a terrible idea.
The only potential solution is to set up git hooks. You can put a pre-receive hook on the origin repo, so that if anyone tries a push that includes a merge you don't like, the push can be rejected. (The challenge is to identify the merges you don't like; I'll come back to that.)
Of course by the time someone pushes, they might have a considerable amount of work they'd have to redo. So your developers, knowing that they can't push refs that break your rule, might want to install the same script as a pre-commit hook to avoid mistakes. (Assuming they don't just decide it's not worth working on a project with this sort of constraint, that is.)
The tricky part is how you're going to detect the type of merge you're after. Your script will have to analyze any new merges, to see what the 1st and 2nd parent are. So you might start by saying, if the 2nd (or subsequent) parent of a merge is equal to master
, reject the push. So then if you have
x -- x -- A <--(master)
\
x -- x -- B <--(dev)
someone can't simply say
git checkout dev
git merge master
git push
because your script sees that the 2nd child of the new merge commit is A
, which is master
. So they can't push
x ----- x ---- A <--(master)
\ \
x -- x -- B -- M <--(dev)
But in that case you probably also don't want them to push
x ----- x ---- A -- C <--(master)
\ \
x -- x -- B -- M <--(dev)
And if the hook only looks at master
, they could get to this by saying
git checkout master
# stage some changes, or make the following commit with the allow-empty flag
git commit
git push
git checkout dev
git merge master^
git push
The developer has to go out of his way to merge master to dev, but can still do it. And since you apparently want to make this a rule, meaning your developers see the value of these merges even though you don't, you can expect that you'll need to enforce the rule more stringently. So your script actually needs to look for any merge whose second (or subsequent) parent is reachable from master
.
So you'd use something like git rev-list
to build a list of revisions that can't be merged from, and check each new merge looking for the 2nd (or subsequent) parent to be in that list.
But that could have some unintended consequences. What if you have
x -- x -- x -- M -- x <--(master)
\ \ /
\ x --- A <--(hotfix)
\
x -- x -- B <--(dev)
You should probably allow that hotfix to be merged to dev, but in this picture it's reachable from master
. (You could require merging the hotfix to dev
first and then to master
, but at best that's another arbitrary constraint contrary to what people are likely to do.)
So maybe when you use rev-list
to build the list of forbidden merge targets, you'd give it the first-parent
argument. This starts to get tough to reason about, so it's unclear whether this is now bullet-proof, but it's getting closer I suppose.
Except the next thing is, what if someone uses reset
to move the master
ref around, so that at the time of the push you can't tell that what they're merging is actually from the master
branch? Are you going to forbid all force-push operations? (To be fair, force-push should be used carefully. The fact that this restriction might push developers to consider a workflow that uses it routinely is another sign that it's a bad idea. But completely eliminating force pushes may not work out so well.)
Oh, and the bigger your repo gets (and the deeper the history of master
) the more resource-intensive your hook will become (slowing down push
operations). I guess you could set a maximum number of commits in master
to look at, on the assumption that nobody will go back too far looking for an allowable segment of history to merge from master
. But that's one more complexity for your script, that could potentially lead to it being out-smarted.
The point of all of this is, you can spend a huge amount of effort trying to control your devs, or you can set agreed-upon team practices that people follow because they're good devs.
Upvotes: 4