I have a protected branch that should only be promoted/fast-forwarded once the integration build on the integration build passes.
I currently try to solve this by having an integration build on pull requests to integration branch, that, once it succeeds, simply fast forwards the release branch to the tip of the integration branch.
However, when I build a branch on the TFS build system, it will checkout the commit at the head of the integration branch, leaving the build server in detached head state.
That should all be fine, but for some reason I can not do that without using multiple statements. To me, intuitively, there should be a simple command for this. I only want to fast-forward a branch point to the current commit.
For now though, I have found 2 ways to do it:
Saving HEAD commit with scripting
- Save HEAD commit to variable in current shell scripting language (here saved to %HEAD_commit_id%)
git checkout release
git merge %HEAD_commit_id%
Using throw-away branch
git branch current-head
git checkout release
git merge current-head
git branch -d current-head
Looking for better solution
Is it correct that the above is "the best" I can do to accomplish this? I would think a one-line exists for this? Both solution 1 and 2 has its caveats, so I would rather go with neither, but I will probably end up with using number 2 in the end.
Steps to reproduce scenario
To reproduce the exact scenario I have, you need 2 commits (C1 and C2) and 2 branches (release and integration).
Point release to C1, integration to C2 and HEAD to C2 (detached head state).
The end result should allow me to push the release branch, now pointing to the same commit as integration.
You have many options; which to use depends what you can guarantee about your environment.
You could use (tip of the hat to jthill):
(with anything your shell requires to protect the braces from expansion). That's equivalent to your first method, but does not need a temporary variable since
@{1}
means "the previous value ofHEAD
before the checkout".You can, however, also use either of these rather tricky operations:
or:
as we'll see below. This is more efficient in some ways. Its main drawback is that almost no one groks it.
Background
Let's first note that each repository is self-contained, by which I mean each repository has its own branch names, and its own
HEAD
and index and work-tree. (The latter three—HEAD
, index, and work-tree—function as a unit. If you have Git version 2.5 or later, you can usegit worktree add
to add independent work-trees, each of which comes with its ownHEAD
and index.)git checkout name-or-ID
will, assuming all goes well:HEAD
pointing toID
, usually throughname
(an "attached HEAD"), but directly if necessary (when you give it an ID, or use--detach
to make sure it detaches HEAD from thename
you give it).HEAD
now names.Now, let's consider what
git merge
does when it's possible to fast-forward instead of actually merging. This occurs whenHEAD
names a commit that is an ancestor of the target commit and you tellgit merge
to merge with anything that identifies that target commit. The identifier you pass togit merge
can be a raw hash ID (as in your%HEAD_commit_id%
method), or a branch name (as in yourgit merge current-head
method). The only thing that is necessary is thatgit merge
:HEAD
and the target commit; andHEAD
identifies.These are the conditions under which a fast-forward merge is possible. If a fast-forward merge is possible, and you have not forbidden it explicitly (
git merge --no-ff
) or implicitly (git merge -s ours
for instance, or certain cases of merging using an annotated tag),git merge
will do the fast-forward operation in this case. You can even add--ff-only
to the arguments togit merge
to tell Git: Do a fast-forward if possible, and if it's not possible, give me an error instead of doing a real merge.The fast-forward merge operation
Now let's look at what the fast-forward merge actually does. There are multiple parts to this, because
git merge
affects the current (HEAD-attached) branch, or ifHEAD
is detached,HEAD
itself. Note that if something goes wrong partway through, Git attempts to back out the entire operation, so that it looks like either everything succeeds, or the fast-forward never even starts. (In some particularly nasty cases, such as when the computer has caught fire, the messy internal state may show through, provided your computer survives the conflagration.)Git needs to make the index and work-tree match the merge result, which is of course the target commit. So in effect, Git checks out the target commit. However,
HEAD
remains connected to the current branch, if it's attached. This checkout of the target commit updates the index and work-tree.If
HEAD
is attached, Git now changes the branch name to whichHEAD
is attached so that this name identifies the commit just checked-out—the target commit. That is, if we had something we might draw this way:we now have:
This is the fast-forward operation in play, when we are on the given branch.
(If
HEAD
is detached, simply imagine the same action without the namebranch
in front of it.)Consider
git fetch
andgit push
When you use
git fetch
to obtain new commits from another Git, orgit push
to give your own commits to another Git, you have the two Gits talk to each other and transfer any new commits. Assume for the moment that this is agit fetch
operation:becomes:
where
Y--Z
are the new commits that link up with the tip commitX
of our branch. Quite typically,X
has two names pointing to it, such asmaster
andorigin/master
, at the start of the operation:and at the end, the corresponding
origin/master
name has moved, so we might draw the commits like this:Consider how the name
origin/master
just moved, and compare it to howbranch
moved when we did a fast-forward merge. These are the same motions: the nameorigin/master
moved in a fast-forward fashion, so that instead of pointing to commitX
, it now points to commitZ
.This, too, is what happens during
git push
, except that instead of having our Git retrieve commits from their Git, we have our commit give commits to their Git. But once we have done that, we don't want our Git to remember their Git's master, we want their Git to remember, as theirmaster
, our final commit. So if they had:and we gave them
--Y--Z
that connect toX
, we'd like them to make theirmaster
point toZ
:This, too, is a fast-forward operation.
Note that if the request we make isn't a fast-forward, the other Git generally rejects our request:
In this case, if they moved their name
master
to point toZ
, they would "forget" commitW
. That's a non-fast-forward operation, and is the kind of thing that when we rungit merge
, causes us to get a true merge. A Git that is receiving agit push
won't rungit merge
, so it requires that our request be a fast-forward operation—well, requires this unless we tell it to force the change.As long as we don't use
--force
or equivalent, this kind ofgit push
won't make any change that is not a fast-forward operation. The other Git will simply say: No, I won't do that, because it's not a fast-forward.Note that unlike our fast-forward merge case, their Git—the other Git to which we are
git push
ing—doesn't rungit checkout
on any updated commit hashes. This is why we must either push to a--bare
repository, which has no work-tree, or push to a branch that the other Git doesn't have checked out.1This means we can (ab)use fetch or push
We know that:
git push
will fast-forward a branch, as long as it's not checked out, andrelease
checked out, sorelease
to some specific commit hash?Our Git will ask their Git—which is really our Git, just wearing a different hat—to take any new commits—there aren't any; our Git has all the commits that our Git has—and then update its (our)
release
in a fast-forward manner, since we're not asking it to force anything. Hence we can fast-forward our ownrelease
to the commit identified byHEAD
if we just say: Hey, other Git, set yourrefs/heads/release
to the commit identified by myHEAD
! We do that with a push refspec of the form:The only trick now is to call up ourselves. Normally we might need a URL, but Git allows path names, including relative path names; and the relative path name
.
means the current directory. So if we:we'll have our Git call up own own Git, transfer nothing (no new commits), and ask our Git to set our
refs/heads/release
in a fast-forward fashion, to match our currentHEAD
. Since ourrelease
is not checked out, this is allowed if and only if the operation is a fast-forward.The fetch trick is exactly the same: we fetch from ourselves (which transfers no commits), then request that our own Git set our own
refs/heads/release
to a specific commit hash. Since there is no leading+
force-flag, this too verifies that the operation is fast-forward-only. Because we do not use--update-head-ok
, this does the same check thatgit push
does, that the currentHEAD
does not name branchrelease
. Curiously, we get the hash ID from the other Git this time (but that's ourselves so it's the same hash ID either way).1This actually depends on the setting of
receive.denyCurrentBranch
. See alsoreceive.denyNonFastForwards
. Newer Gits also supportupdateInstead
with which we can have the target Git rungit checkout
, but only in certain circumstances; this allows non-bare repositories to be used withgit push
even with a branch occupying their work-tree-and-index pair.