Reputation: 8587
is it possible to make it impossible to create a branch from any branch except master?
Here is how I work:
This morning I've had a bad surprise, when I merged a ticket branch into master a lot of other stuff got merged: apparently I had created the ticket-branch from preprod.
So, is there a way to avoid this? I need to ensure that the branches I create are created from master only.
Upvotes: 2
Views: 314
Reputation: 488163
As asked, the answer is "no". Branches—more precisely, branch labels—in git are flitty ephemeral things: they come and go as they please, as it were, and anyone at any time can create a new label attached to any commit anywhere in the repository. For instance, if you have this:
A - B - C - D <-- main
\
E - F - G <-- sub
\
H <-- two
as your (entire) repository, where each single letter represents a commit and there are three named branches (main
, sub
, and two
), I (or you) can do this:
$ git branch foo main~1
and now there is a new branch-label foo
pointing to commit C
:
D <-- main
/
A - B - C <-- foo
\
E - F - G <-- sub
\
H <-- two
Note that the commit graph is exactly the same, I just levered D
up onto a new line to make it easier to see where the new label foo
points.
It's also important to note here that branch labels, when they exist, are attached to a specific commit, not to (another) label. (There is such a thing as an indirect label but normally that's just for HEAD
, and it doesn't really work if you try to use it as a "branch".) What makes a local branch label "a branch" is that git will move this label for you, when you make new commits.
What I think Zeeker is suggesting in a comment is to check, at the time you are about to make a new commit (before actually doing it, i.e., in a pre-commit
hook):
Writing hooks is a bit tricky. If you're experienced with shell scripting (sh/bash), that helps a bunch. You can use any language you like but git commands are most easily run from a shell script. In any case, you then get into all the git "plumbing" commands, because they're meant to be used from scripts—in fact, many git commands are just shell scripts that invoke the plumbing commands. (For instance, git stash
and the variations of git rebase
are all just scripts.)
Here's an outline, entirely untested. We start with some boilerplate...
#! /bin/sh
#
# Are we on a branch, or do we have a detached HEAD?
# If detached HEAD, just exit 0 = ok to commit. Otherwise
# set $branch to the short name of the branch.
branch=$(git symbolic-ref -q --abbrev-ref HEAD) || exit 0
Now we check which branch the new commit goes on, i.e., which label is to be moved by the new commit, if we allow it (exit 0). (Exit 1, with a message about why to disallow the commit, to prevent it.) You probably want to modify this section, e.g., to check for ticket-* as a name for instance. I just need to verify that the name is neither master
nor preprod
, for the remaining parts. I also need to compare the SHA-1s of master
and preprod
, since if they are identical, the rule "must be descendent of master
but not of preprod
" is literally impossible to satisfy (all descendents of master
are also descendents of preprod
by definition).
case "$branch" in
master) exit 0;; # branch master, just allow (or deny=exit 1)
preprod) exit 0;; # branch preprod, just allow
esac # all others: fall through to next checks
# Must be a ticket branch.
# If there are no branches named preprod and master, skip all this.
# Otherwise, get their SHA-1 values.
master=$(git rev-parse -q --verify preprod) || exit 0
preprod=$(git rev-parse -q --verify master) || exit 0
# Ensure that the new commit, once made, will not be a descendant
# of branch preprod, and will be a descendent of master. That is,
# if we add a new commit node to the graph, ancestors(new) will
# NOT include the commit labeled preprod but WILL include the
# commit labeled master.
#
# Note that if preprod and master point to the *same* commit,
# this condition can never be satisfied. In this particular
# case (preprod == master) just allow the commit. (We're not
# committing *on* master or preprod -- we checked that above --
# so this commit will not move either of *those* two labels.)
[ $master = $preprod ] && exit 0
Last, we enforce the rule:
# A new commit's immediate parent will be whatever the SHA-1 of HEAD
# is, so we simply check whether HEAD satisfies our conditions.
if git merge-base --is-ancestor $preprod HEAD; then
echo "ERROR: new commit will be a descendent of" 1>&2
echo " branch preprod (commit $preprod)" 1>&2
exit 0
fi
if ! git merge-base --is-ancestor $master HEAD; then
echo "ERROR: new commit will not be a descendent of" 1>&2
echo " branch master (commit $master)" 1>&2
exit 0
fi
There are some holes in the above checking. The most obvious is when both master
and preprod
point to the same commit:
... - o - o - o <-- master, preprod, branch3
\
o - o <-- branch4
If you're on branch3
(HEAD
is branch3
) and add a new commit, you get:
o <-- HEAD=branch3
/
... - o - o - o <-- master, preprod
\
o - o <-- branch4
This is how you have to think about handling this: all these operations, in git, modify the commit graph, and drag the current branch label (the one HEAD
has in it) along with them. (In the "detached HEAD" case, HEAD
points directly to a commit, rather than to a branch label that in turn points to the commit. You can at that time, or really any time, add a new label, and make HEAD
point to it, with git checkout -b
.)
Less obvious, but much bigger, is the hole knittl mentioned in a comment: git rebase
will copy a series of commits to a new series and then move the label; and the git reset
command moves labels as instructed (while also updating index and working tree, depending on arguments).
Upvotes: 3