How to find the nearest parent of a Git branch
Let's say I have the following local repository with a commit tree like this:
master --> a
\
\
develop c --> d
\
\
feature f --> g --> h
master
is my this is the latest stable release code, develop
is my this is the 'next' release code, and feature
is a new feature being prepared for develop
.
Using hooks, I want to be able to refuse pushes to feature
to my remote repository, unless commit f
is a direct descendant of develop
HEAD. I.e., the commit tree looks like this, because feature has been git rebase
on d
.
master --> a
\
\
develop c --> d
\
\
feature f --> g --> h
So is it possible to:
- Identify the parent branch of
feature
? - Identify the commit in parent branch which
f
is a descendant of?
From there I would check what HEAD of the parent branch is, and see if f
predecessor matches the parent branch HEAD, to determine if the feature needs to be rebased.
Solution 1:
Assuming that the remote repository has a copy of the develop branch (your initial description describes it in a local repository, but it sounds like it also exists in the remote), you should be able to achieve what I think you want, but the approach is a bit different from what you have envisioned.
Git’s history is based on a DAG of commits. Branches (and “refs” in general) are just transient labels that point to specific commits in the continually growing commit DAG. As such, the relationship between branches can vary over time, but the relationship between commits does not.
---o---1 foo
\
2---3---o bar
\
4
\
5---6 baz
It looks like baz
is based on (an old version of) bar
? But what if we delete bar
?
---o---1 foo
\
2---3
\
4
\
5---6 baz
Now it looks like baz
is based on foo
. But the ancestry of baz
did not change. We just removed a label (and the resulting dangling commit). And what if we add a new label at 4
?
---o---1 foo
\
2---3
\
4 quux
\
5---6 baz
Now it looks like baz
is based on quux
. Still, the ancestry did not change, only the labels changed.
If, however, we were asking “is commit 6
a descendent of commit 3
?” (assuming 3
and 6
are full SHA-1 commit names), then the answer would be “yes”, whether the bar
and quux
labels are present or not.
So, you could ask questions like “is the pushed commit a descendent of the current tip of the develop branch?”, but you can not reliably ask “what is the parent branch of the pushed commit?”.
A mostly reliable question that seems to get close to what you want is:
For all the pushed commit’s ancestors (excluding the current tip of develop and its ancestors), that have the current tip of develop as a parent:
- does at least one such commit exist?
- are all such commits single-parent commits?
Which could be implemented as:
pushedrev=...
basename=develop
if ! baserev="$(git rev-parse --verify refs/heads/"$basename" 2>/dev/null)"; then
echo "'$basename' is missing, call for help!"
exit 1
fi
parents_of_children_of_base="$(
git rev-list --pretty=tformat:%P "$pushedrev" --not "$baserev" |
grep -F "$baserev"
)"
case ",$parents_of_children_of_base" in
,) echo "must descend from tip of '$basename'"
exit 1 ;;
,*\ *) echo "must not merge tip of '$basename' (rebase instead)"
exit 1 ;;
,*) exit 0 ;;
esac
This will cover some of what you want restricted, but maybe not everything.
For reference, here is an extended example history:
A master
\
\ o-----J
\ / \
\ | o---K---L
\ |/
C--------------D develop
\ |\
F---G---H | F'--G'--H'
| |\
| | o---o---o---N
\ \ \ \
\ \ o---o---P
\ \
R---S
The above code could be used to reject H
and S
while accepting H'
, J
, K
, or N
, but it would also accept L
and P
(they involve merges, but they do not merge the tip of develop).
To also reject L
and P
, you can change the question and ask
For all the pushed commit’s ancestors (excluding the current tip of develop and its ancestors):
- are there any commits with two parents?
- if not, does at least one such commit have the current tip of develop its (only) parent?
pushedrev=...
basename=develop
if ! baserev="$(git rev-parse --verify refs/heads/"$basename" 2>/dev/null)"; then
echo "'$basename' is missing, call for help!"
exit 1
fi
parents_of_commits_beyond_base="$(
git rev-list --pretty=tformat:%P "$pushedrev" --not "$baserev" |
grep -v '^commit '
)"
case "$parents_of_commits_beyond_base" in
*\ *) echo "must not push merge commits (rebase instead)"
exit 1 ;;
*"$baserev"*) exit 0 ;;
*) echo "must descend from tip of '$basename'"
exit 1 ;;
esac
Solution 2:
A rephrasal
Another way to phrase the question is "What is the nearest commit that resides on a branch other than the current branch, and which branch is that?"
A solution
You can find it with a little bit of command-line magic
git show-branch \
| sed "s/].*//" \
| grep "\*" \
| grep -v "$(git rev-parse --abbrev-ref HEAD)" \
| head -n1 \
| sed "s/^.*\[//"
With AWK:
git show-branch -a \
| grep '\*' \
| grep -v `git rev-parse --abbrev-ref HEAD` \
| head -n1 \
| sed 's/[^\[]*//' \
| awk 'match($0, /\[[a-zA-Z0-9\/-]+\]/) { print substr( $0, RSTART+1, RLENGTH-2 )}'
Here's how it works:
- Display a textual history of all commits, including remote branches.
- Ancestors of the current commit are indicated by a star. Filter out everything else.
- Ignore all the commits in the current branch.
- The first result will be the nearest ancestor branch. Ignore the other results.
- Branch names are displayed [in brackets]. Ignore everything outside the brackets, and the brackets.
- Sometimes the branch name will include a
~#
or^#
to indicate how many commits are between the referenced commit and the branch tip. We don't care. Ignore them.
And the result
Running the above code on
A---B---D <-master
\
\
C---E---I <-develop
\
\
F---G---H <-topic
Will give you develop
if you run it from H and master
if you run it from I.
The code is available as a gist.
Solution 3:
Git parent
You can just run the command
git parent
to find the parent of the branch, if you add the Joe Chrysler's answer as a Git alias. It will simplify the usage.
Open the gitconfig file located at "~/.gitconfig"
by using any text editor (for Linux). And for Windows the ".gitconfig" path is generally located at C:\users\your-user\.gitconfig
.
vim ~/.gitconfig
Add the following alias command in the file:
[alias]
parent = "!git show-branch | grep '*' | grep -v \"$(git rev-parse --abbrev-ref HEAD)\" | head -n1 | sed 's/.*\\[\\(.*\\)\\].*/\\1/' | sed 's/[\\^~].*//' #"
Save and exit the editor.
Run the command git parent
.
That's it!
Solution 4:
You can also try:
git log --graph --decorate
Solution 5:
I have a solution to your overall problem (determine if feature
is descended from the tip of develop
), but it doesn't work using the method you outlined.
You can use git branch --contains
to list all the branches descended from the tip of develop
, then use grep
to make sure feature
is among them.
git branch --contains develop | grep "^ *feature$"
If it is among them, it will print " feature"
to standard output and have a return code of 0. Otherwise, it will print nothing and have a return code of 1.