I'll take a stab at an answer, at this point, but I don't want to get bogged down in the details of what the book you're quoting says.
I think the way to view this is from the "receiving side", i.e., to think about what happens when you're an ordinary person using git and someone does a push into your repository (vs what you usually do, which is to fetch from theirs). Git provides a lot of mechanism for this, and a default policy that works ok, which is: "just say no". :-) Newer versions of git (2.3+) have added a few more policies that provide safety while allowing some of these pushes; whether you would call them "right" or "perfect" is more a matter of opinion. (Consider what happens if someone starts editing on your non-bare "deployed" host and then, say, falls asleep, so that no one else can push to it because it now "looks dirty" to git.)
Remember first that a repository can be set to "bare", which tells git not to look for a work-tree. (You can convert a non-bare repo to bare, or vice versa, but in my experience most people goof something up in the process. Using git clone --bare
sets up a bare clone initially, and avoids creating a work-tree, which means there's no possibility for confusion or error here.1) Given a repository that has no work-tree, no one ever goes into its work-tree and does any work, and the "push screws up in-progress work" case is impossible.
With that in mind, let's look at what happens when we're the receiver of a push, and we have a non-bare repository, which has an actual work-tree. Let's also keep two more items in mind:
Because we have a work-tree, we also have an index file (.git/index
) that is acting in its usual dual role of "place to stage the next commit" and "cache to speed up git status
and similar operations".
We also have a "current" branch, as stored in the HEAD
file,2 which we can look at directly, or use git symbolic-ref HEAD
to read (the latter is the formally approved method). If the current branch is br
, the HEAD
file contains a single line reading ref: refs/heads/br
, for instance, and git symbolic-ref HEAD
prints refs/heads/br
.
(If we're in "detached HEAD" mode, the HEAD
file contains a raw SHA-1, rather than ref: refs/heads/branch
. In this case, receiving a push won't screw up in-progress work, so we can safely ignore this case.)
Here's the underlying mechanism behind receiving a push:
As the receiver, we start by receiving objects to add to our repository.3 We add all those objects, even if we'll wind up rejecting one or more reference updates.
Now we get a list of proposals, whose general form looks like: "please set refs/heads/X
to 1234567...
", "forcefully set refs/heads/Y
to fedcba9...
", and so on. These correspond to the refspecs whoever started up their push
is using. (If the supplied SHA-1 is all-zeros, their git is asking us to delete these references.)
We take each of these reference update requests into consideration, partly one at a time and partly in whole gulps, applying the sub-rules listed below. For updates that pass, we set the supplied reference to the supplied SHA-1 and tell the other git "ok, done"; for updates that fail, we tell the other git "no, rejected" and supply a bit more "reason" message. (We also pass the stderr output, and sometimes stdout output, of hooks on to the other git. There's a small protocol to tell him which pieces are our own answers and which are just passed-on output, so that his git knows which updates we accepted.)
Once we're all done, we run a "post-receive hook" and pass it the successful updates (in the same form as for the pre-receive hook, but eliminating rejected updates).
Now let's cover (lightly) the rules used to accept or reject individual updates and/or the updates-as-a-whole. These are not necessarily in the actual internal order (and I'm going to ignore a few special cases like receive.shallowUpdates
for instance):
Some updates must pass various built-in tests, most commonly the "fast forward" test, and sometimes a "never change this" test. Exactly which refs get tested in which way depends on our version of git, our configuration, and the force flag for this update. For details on these, see the git config
documentation, paying particular attention to receive.denyDeletes
and receive.denyNonFastForwards
, and note that git used to apply fast-forward rules to tag updates (but not tag deletes) until git 1.8.2, when tags were changed to "never change" (but still allow deletes unless receive.denyDeletes
is set).
The entire set of updates is sent to the pre-receive
hook (on its standard input, as a series of lines). They're augmented with one more bit of information first: the current SHA-1 associated with each reference, or all-zeros if we don't already have that reference. If that hook exits non-zero, the entire set of updates—the whole push—is rejected. (If the hook does not exist, we consider this test to have passed.)
Each individual update is sent to the update
hook (as arguments). If that hook exits non-zero, this particular update is rejected, but we go on to verify the others. (As before, if the hook does not exist, the test is an automatic pass.)
Finally, we have the "non bare repository" rules, which are the ones you care about here, and I will separate out into their own section.
Updating a non-bare repository: what's allowed and why
(I see VonC beat me to this, but I'll go ahead with details.)
The new configuration entries or values in git 1.6.6, git 2.3, and git 2.4 are:
receive.denyDeleteCurrent
: this option was actually introduced in git 1.6.2 but didn't really do anything until git 1.6.6. Before then, receiving a push-to-delete of whatever the current branch was, deleted the current branch's reference. When it did so, HEAD
was left pointing to a non-existent branch (to fix it you had to use the plumbing tools or modify the file directly). In git 1.6.6, this stopped being allowed by default. (I haven't tested to see if the "bad HEAD" thing still happens.)
receive.denyCurrentBranch
: this also went in during 1.6.2 and was enabled (i.e., the default "refuse" action went live) in 1.6.6. However, it grew the new 'updateInstead' value in git 2.3.
Note that both of these are specific to "the current branch", i.e., the (single) reference refs/heads/br
that HEAD
refers to. And again, they apply only when core.bare
is not set. In this case, there is a work-tree and it's full of files that relate, in some way, to whatever SHA-1 is filed away in refs/heads/br
. There is also (probably4) an index file, which may or may not have things add
-ed, rm
-ed, and be holding merge state if you're in the middle of a conflicted merge.
Suppose that, via receive.denyCurrentBranch
, you allow someone's git push
to change your repository's stored SHA-1 for refs/heads/br
. Suppose further that you don't have any deployment hooks set up, and are not using the new (2.3+) features. Then, in this case, if someone else alters your refs/heads/br
, your own index and work-tree remain entirely unchanged. For concreteness, let's say that br
used to point to commit 2222222...
and someone else—Bob, for instance—has just successfully pushed and changed it to 3333333...
.
If you now finish your own editing / merging / whatever, git add
the results as usual, and run git commit
, git will make a new commit from your current index, which consists of "everything from commit 2222222...
except for your git add
and git rm
s". The stuff Bob did, which is in 3333333...
, is not in your index. The new commit that git makes, though, will have 3333333...
as its parent, while using the contents taken from your index, which is based on 2222222...
. The effect is that your commit reverts all Bob's changes while adding all your changes: diffing your new commit against 2222222...
will show you what you did, while diffing your new commit against its parent will show you backing out all Bob's work while keeping your own work.
If you do have a hook that does some deploying, the contents of your index and/or work-tree will depend on exactly what that hook does. For instance, if it does a git checkout -f
, everything Bob changed will replace what you've put in the index and work-tree.
Neither of these results is ever what anyone actually wants.
The new updateInstead
setting is closer to what people sometimes want: before allowing the reference update (Bob's change of refs/heads/br
from 2222222...
to 3333333...
), git checks whether your index and work-tree match commit 2222222...
. If they do,5 git allows Bob's push and applies that update to your index and work-tree, as if you had spotted Bob's push somehow and done git checkout br
, or anything equivalent to bring everything up to date.
There's still some potential danger here. For instance, suppose you've opened up README
to work on it. You've spent a while chasing down some reference URLs and typed them in, in your editor, but not written the result anywhere. Meanwhile, Bob has made a fix to README
and he runs his git push
. Your git sees that your work-tree is "clean" and that the update is "safe", so it updates your README
.
Depending on how clever your editor is, when you go to write out your README
, you may overwrite Bob's changes, or your editor might say "hey, README
changed, I'll grab the new one" and lose your work, etc. One might argue that this is bad behavior by that editor (and I'd buy that argument), but it's still a potential problem—and it's not limited to editors; you might be running some slow computational process that writes files that you keep source-controlled, which could produce the same sorts of issues.
Git doesn't try to decide how to handle all this. Git just gives you configuration options (more mechanisms) and leaves the final policy up to you. I'd say git's defaults here are correct; the fancier updateInstead
mode is not the default, because the "right" policy is unclear.
1There are still other possible errors, depending on whether you want group write mode and sharing for simple ssh pushes. At a previous workplace we eventually instituted a policy of configuring push-able repositories with a script: you would set up what you wanted to see in it as a private repo, then you run the script yourself or have an admin run it, giving it a URL for your private repo, to create the public-and-shared repo by cloning. After that we didn't care what you did with that private repo, but the main point here is that we would clone with --bare
rather than having to have someone—usually me—go fix up all the broken bits. :-)
2Even a bare repository has a HEAD
file and hence has a current branch. It also has an index, but with no work-dir the index is usually irrelevant. (Some deployment scripts wind up using the bare repo's index, which leads to bugs in some of those deployment scripts, but that's another question entirely.) The current branch is slightly relevant: it affects which branch someone else's git clone
will check out for them at the end of the clone process, provided they did not specify a particular branch name.
3Usually we get these as a "thin pack", which is to say, a pack that can delta-compress against objects we already have. In order for that to happen, there's a step before the "receive objects" step, where we tell the sender what SHA-1s we have. You can see what we tell the sender by, on the sender, using git ls-remote
. There's also some early protocol negotiation steps. These matter for lower level details, but not for the process described above.
4You can remove .git/index
and git will re-construct it later when it needs it. I don't particularly recommend removing it, but the effect is to lose all stored-up git add
and git rm
s, along with any merge information if you're in the middle of a merge.
5And additional tests pass (see VonC's answer). Some of those additional tests weren't in the initial stab at updateInstead
mode and, I think, were found the hard way.