Reputation: 23975
I've seen a few questions about "how can I check whether a branch X
has been rebased onto Y
", but I haven't found one that has the particular flavor that I want.
I'd like to check whether X
is a "simple branch descending from Y
". Strictly, only allow this pattern between X
and Y
, not the pattern between Z
and Y
:
In other words, if every commit in branch X
has Y
as an ancestor (or is equal to Y
or is one of Y
's ancestors) -- not just whether the tip of X
has Y
as an ancestor.
This is desirable for helping people achieve a rebase-based merging pattern, where X
is a feature branch and Y
is the main development branch. (If someone really knows what they're doing, and a non-strict-ff branch is what they really want, we can easily grant special dispensation and allow the merge. But we want them to at least realize they're doing it, which keeps not happening.)
Note that this is not the same as "can be merged with --ff-only
". git merge --ff-only
will happily update Y
to point to X
if Y
is a descendant of X
, even if some ancestors of X
are neither descendants nor ancestors of Y
.
I know I can loop through all the ancestors of X
and check whether Y
is an ancestor of each one (and stop when I get to Y
itself, and make sure there's nothing with multiple parents), but I'm wondering if there's something better built-in to Git that I've missed.
Addendum:
As @TTT pointed out, another common scenario this would catch is someone messing up a rebase by pull
ing before their push
, instead of doing push --force-with-lease
. I created the following graphic for our internal "how to use Git" documentation, it would be great to have this check automated:
Upvotes: 4
Views: 621
Reputation: 51850
One way to check whether all commits in Y..X
are descendants of Y
is to check the boundary of that range using git log --boundary Y..X
or git rev-list --boundary Y..X
.
Starting from this history :
$ git log --graph --oneline --all
* 036a9f9 (HEAD -> X) create d.txt
* cadd199 create c.txt
| * 0680934 (Z) Merge commit '22a23fe' into Z
|/|
| * 22a23fe create b.txt
* | 8dec744 (Y) create a.txt
|/
* 878ac8b first commit
You will get :
$ git log --oneline --boundary Y..X
036a9f9 (HEAD -> X) create d.txt
cadd199 create c.txt
- 8dec744 (Y) create a.txt # <- one single boundary commit, pointing at Y
$ git log --oneline --boundary Y..Z
0680934 (Z) Merge commit '22a23fe' into Z
22a23fe create b.txt
- 8dec744 (Y) create a.txt # <- two commits on the boundary
- 878ac8b first commit # <-
A scriptable way to check if you are in this situation is :
# 'git rev-list' prints full hashes, boundary commits are prefixed with '-'
boundary=$(git rev-list --boundary Y..X | grep -e '^-')
want=$(git rev-parse Y)
want="-$want"
# the boundary should consist of "-<hash of Y>" only:
if [ "$boundary" = "$want" ]; then
echo "all commits in X are descendants of Y"
fi
The above just checks that all commits come after Y
. You could also be faced with the following situation :
* 036a9f9 (HEAD -> X) create d.txt
* 0680934 Merge 'origin/X' into X # <- someone created a merge commit in between
|\
| * cadd199 create c.txt
* | 22a23fe create b.txt
|/
* 8dec744 (Y) create a.txt
* 878ac8b first commit
and this would also get into the way of a rebase workflow.
If you also want to rule this out, use the --merges
option of git log
or git rev-list
:
# git rev-list also has a --count option, which will output the count
# rather than the complete list of commits
merges=$(git rev-list --count --merges Y..X)
if [ "$merges" -eq 0 ]; then
echo "all good, no merges between Y and X"
fi
The documentation for --boundary
does not give a good explanation of what a "boundary commit" is.
I would say this SO answer has a decent definition:
A boundary commit is the commit that limits a revision range but does not belong to that range. For example the revision range HEAD~3..HEAD consists of 3 commits (HEAD~2, HEAD~1, and HEAD), and the commit HEAD~3 serves as a boundary commit for it.
More formally, git processes a revision range by starting at the specified commit and getting at other commits through the parent links. It stops at commits that don't meet the selection criteria (and therefore should be excluded) - those are the boundary commits.
Upvotes: 6
Reputation: 30868
I think you are looking for something like (in Bash)
[[ $(git merge-base X Y) = $(git rev-parse Y) ]] && echo yes || echo no
git merge-base X Y
finds the best common ancestor and prints its full sha1 value. As to best, the doc says that
One common ancestor is better than another common ancestor if the latter is an ancestor of the former. A common ancestor that does not have any better common ancestor is a best common ancestor.
In most cases, the best common ancestor is the nearest common commit of the 2 branches away from both heads.
git rev-parse Y
prints the sha1 value of Y's head. If the best common ancestor is the same with Y's head, it meets that every commit in branch X has Y as an ancestor (or is equal to Y or is one of Y's ancestors)
. In other words, the set of Y's commits is a subset of the set of X's commits.
But in practice, Y in the remote repository could be being updated by others and the local Y could get updated unintentionally. The test would say no even if X is really descendent of Y during the period after X has been created and before Y is updated.
Upvotes: 2