Git: making pushes to non-bare repositories safe

2019-01-19 08:08发布

问题:

I could use some guidance from the git experts out there regarding making push operations to non-bare repositories safe. Basically I have a plan about how to do this, and could use some advice about whether the plan is sane or not :)

Normally when you push to a non-bare repository in git, its working copy and index are not updated. As I've discovered, this can cause serious problems if you forget to later update them manually!

In our group, we have a few "central" repositories that people clone off of and push back to, but many people also want to be able to make clones of their clones and push/pull between them as needed in true distributed fashion. In order to make this safe, I want to ensure that every repository created via either "clone" or "init" has a post-receive hook automatically installed that will update the working directory and index after a push operation to be in sync with the new HEAD.

I've found that I can make this happen by creating a template directory with my post-receive hook in a hooks subdirectory, then having everyone in my group do a:

git config --global init.templatedir /path/to/template/dir

Currently my post-receive hook looks like this:

export GIT_WORK_TREE=..
git checkout -f HEAD

This SEEMS to work as desired, but I have some uncertainty about the checkout command. For the purposes of syncing the working directory and index with the state in HEAD, are "git checkout -f HEAD" and "git reset --hard HEAD" equivalent?

I ask because although I know that "git reset --hard HEAD" will do what I want, using it in a post-receive hook slows down push operations considerably in my testing (it seems to do a fresh check out of all files, regardless of whether a file is dirty or clean in the working dir). "git checkout -f HEAD" SEEMS to do the same thing much faster (get me a clean working directory and index in sync with HEAD), but I am a little nervous given the propensity of the checkout command to do on-the-fly merges with uncommitted working directory changes. Will this command really give me a working dir & index that exactly match the state in HEAD in all cases (including, eg., file deletions, renames, etc.)?

Thanks in advance for any advice!

回答1:

I might have misunderstood the problem. Do you really want to set it up so anyone can push to a developer's working directory while he has uncommitted changes he's working on? That can't be made "safe" by any stretch of the word.

What most people do is have their normal working directory, which is private, and make a bare clone of that into a public repo on the same disk, and share that one. That uses hard links so you barely use any additional space. You push your changes to your colleague's public bare repo, and he does a pull into his working directory when he's ready to receive the changes. Alternately, you push your changes into your public bare repo, and your colleague pulls from there when he's ready. There's a reason pushing to non-bare repositories is difficult to set up.



回答2:

I suggest using the post-update hook indicated by the Git FAQ entry ”Why won't I see changes in the remote repo after "git push"?”.

It will stash away staged and unstaged changes (to tracked files) before doing the hard reset. It is safer than a plain hard reset, but as the FAQ entry says, it still does not cover all the situations that might come up (e.g. an index with a pre-existing conflict can not be stashed; it does not auto-merge changes to unmodified files like git checkout does, etc.).


However, …
if at all possible, …
you should probably just avoid pushing to any checked out branch in the first place.

Pushing to a non-bare repository is okay as long as you are not pushing to the checked out branch (after all, the involved configuration variable is receive.denyCurrentBranch, not “receive.denyNonBare”).

The last paragraph of the above-linked FAQ entry links to (as, in a comment below, Mark Longair mentions) another entry that outlines an approach for pushing to a non-bare repository. The motivation for the entry is an asymmetric network connection between two non-bare repositories, but the technique can be applied to any situation where you need/want to push to a non-bare repository.

This latter FAQ entry gives an example of pushing to a remote-tracking branch (under refs/remotes/). Only the refs under refs/heads/ can be checked out without detaching HEAD (without the use of git symoblic-ref), so pushing to anything outside refs/heads/ should be safe for avoiding “pushing to the checked out branch”.

Since you are working in a centralized environment, you might be able to make a policy for the destination of such pushes. For example:

When you need to push commits to someone else’s non-bare repository, push them to refs/remotes/from/<your-username>/<branch>. To avoid conflicts with normal remote-tracking branches, no one should ever define a remote named from. Branches pushed like this will show up in git branch -a (or -r) and, accordingly, can be referenced without the refs/remotes/ prefix. However, the from pseudo-remote will not show up in git remote because there is no remote.from.url configuration variable.

Example:

alice$ remote add betty bettys-machine:path/to/some/non-bare/repository
alice$ git push betty master:refs/remotes/from/alice/bug/123/master

betty$ git log --reverse -p origin/master..from/alice/bug/123/master