可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
A foxtrot merge is a merge where 'origin/master' merges in as a 2nd (or later) parent, like so:

Commit 'D' is a foxtrot merge because 'origin/master' is its 2nd parent. Notice how first-parent history from 'origin/master' contains commit 'B' at this moment.
But in my git repo I need all merges involving 'origin/master' to keep 'origin/master' as the 1st parent. Unfortunately git doesn't care about parent-order when it evaluates whether a commit is eligible for fast-forward. This causes the first parent history on my master branch to sometimes lose commits that used to be there (e.g., output of "git log --first-parent").
Here's what happens when commit 'D' from the earlier diagram is pushed:

How can I prevent this push? First-parent history of 'origin/master' no longer contains commit 'B' after the foxtrot merge is pushed!
Obviously no commits or work are actually lost, it's just that in my environment I really need "git log --first-parent" to be a stable accumulative record of commits - if you like, a kind of "Write-Once Read-Many" (WORM) database. I have scripts and processes that use "git log --first-parent" to generate changelogs and release notes, as well as to manage ticket transitions in my ticketing system (JIRA). Foxtrot merges are breaking my scripts!
Is there some kind of pre-receive hook I could install in my git repositories to prevent foxtrot merges from getting pushed?
p.s. The commit graphs in this stackoverflow question were generated using http://bit-booster.com/graph.html.
回答1:
The following pre-receive hook will block those:
#/bin/bash # Copyright (c) 2016 G. Sylvie Davies. http://bit-booster.com/ # Copyright (c) 2016 torek. http://stackoverflow.com/users/1256452/torek # License: MIT license. https://opensource.org/licenses/MIT while read oldrev newrev refname do if [ "$refname" = "refs/heads/master" ]; then MATCH=`git log --first-parent --pretty='%H %P' $oldrev..$newrev | grep $oldrev | awk '{ print \$2 }'` if [ "$oldrev" = "$MATCH" ]; then exit 0 else echo "*** PUSH REJECTED! FOXTROT MERGE BLOCKED!!! ***" exit 1 fi fi done
If you're using Github / Gitlab / Bitbucket Cloud, you may need to look into creating some kind of call into their commit status apis (here's api docs for: bitbucket, github; not sure if gitlab has one), because you don't have access to the pre-receive hooks, and even if you did, you'd still have to deal with people clicking the "merge" button directly in the web ui of those products (in which case there is no "push").
With Bitbucket Server you can install the add-on I created.
Once it's installed you click "enable" on the "Protect First Parent Hook" in a given repository's "hook" settings:

It will block foxtrot merges via push and via the "merge" button in the Bitbucket Server UI. It does this even if its license is expired, making the "Protect First-Parent Hook" a free component of the larger add-on.
Here's an example of my Bit-Booster "Protect First Parent" pre-receive hook in action:
For more background on foxtrot merges I wrote a blog post.
回答2:
Here is a hook code which will do what you are asking for:
#!/bin/sh # Check to see if this is the first commit in the repository or not if git rev-parse --verify HEAD >/dev/null 2>&1 then # We compare our changes against the previous commit against=HEAD^ else # Initial commit: diff against an empty tree object against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 fi # Redirect output to screen. exec 1>&2 # Check to see if we have updated the master branch if [ "$refname" eq "refs/heads/master" ]; then # Output colors red='\033[0;31m'; green='\033[0;32m'; yellow='\033[0;33m'; default='\033[0;m'; # personal touch :-) echo "${red}" echo " " echo " |ZZzzz " echo " | " echo " | " echo " |ZZzzz /^\ |ZZzzz " echo " | |~~~| | " echo " | |- -| / \ " echo " /^\ |[]+ | |^^^| " echo " |^^^^^^^| | +[]| | | " echo " | +[]|/\/\/\/\^/\/\/\/\/|^^^^^^^| " echo " |+[]+ |~~~~~~~~~~~~~~~~~~| +[]| " echo " | | [] /^\ [] |+[]+ | " echo " | +[]+| [] || || [] | +[]+| " echo " |[]+ | || || |[]+ | " echo " |_______|------------------|_______| " echo " " echo " " echo " ${green}You have just committed code ${red} " echo " Your code ${yellow}is bad.!!! " echo " ${red} Do not ever commit again " echo " " echo "${default}" fi; # set the exit code to 0 or 1 based upon your needs # 0 = good to push # 1 = exit without pushing. exit 0;
回答3:
I wrote this to provide feedback early (pre-receive
hook is also needed). I use post-merge
and pre-push
hooks for this. It's not possible to prevent a foxtrot merge in commit-msg hook as the information to detect one is available only after. In post-merge
hook, I simply warn. In pre-push
hook, I throw and block the push. See d3f1821 ("foxtrot: Add sub-hooks to detect foxtrot merges", 2017-08-05).
~/.git-hooks/helpers/foxtrot-merge-detector:
#!/bin/sh #usage: # foxtrot-merge-detector [<branch>] # # If foxtrot merge detected for branch (current branch if no branch), # exit with 1. # foxtrot merges: # See http://bit-booster.blogspot.cz/2016/02/no-foxtrots-allowed.html # https://stackoverflow.com/questions/35962754/git-how-can-i-prevent-foxtrot-merges-in-my-master-branch remoteBranch=$(git rev-parse --abbrev-ref "$1"@{u} 2>/dev/null) # no remote tracking branch, exit if [[ -z "$remoteBranch" ]]; then exit 0 fi branch=$(git rev-parse --abbrev-ref "${1-@}" 2>/dev/null) # branch commit does not cover remote branch commit, exit if ! $(git merge-base --is-ancestor $remoteBranch $branch); then exit 0 fi remoteBranchCommit=$(git rev-parse $remoteBranch) # branch commit is same as remote branch commit, exit if [[ $(git rev-parse $branch) == $remoteBranchCommit ]]; then exit 0 fi # remote branch commit is first-parent of branch, exit if [[ $(git log --first-parent --pretty='%P' $remoteBranchCommit..$branch | \ cut -d' ' -f1 | \ grep $remoteBranchCommit | wc -l) -eq 1 ]]; then exit 0 fi # foxtrot merge detected if here exit 1
And then use it as
pre-push hook:
#!/bin/sh remote="$1" url="$2" z40=0000000000000000000000000000000000000000 while read local_ref local_sha remote_ref remote_sha do if [ "$local_sha" = $z40 ]; then # handle delete, do nothing : else # ex $local_ref as "refs/heads/dev" branch=$(git rev-parse --abbrev-ref "$local_ref") ~/.git-hooks/helpers/foxtrot-merge-detector "$branch" # check exit code and exit if needed exitcode=$? if [ $exitcode -ne 0 ]; then echo 1>&2 "fatal: foxtrot merge detected, aborting push" echo 1>&2 "fatal: branch $branch" exit $exitcode fi fi done
post-merge:
#!/bin/sh ~/.git-hooks/helpers/foxtrot-merge-detector # check exit code and exit if needed exitcode=$? if [ $exitcode -ne 0 ]; then echo -e " ${Yellow}WARNING:${None} foxtrot merge detected" # swallow exit code fi