Git: making pushes to non-bare repositories safe
Asked Answered
V

2

3

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!

Vashtivashtia answered 20/5, 2011 at 17:30 Comment(0)
D
5

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.

Disloyalty answered 20/5, 2011 at 19:40 Comment(6)
Sorry, I think I did a bad job at articulating the motivation behind this crazy scheme to allow pushes to non-bare repos. We have at least two use cases where it seems to make sense: 1. We have a "stable" branch and an "unstable" branch, but for various reasons we want these to be in separate repositories. People clone off of and push to both stable and unstable, but oftentimes we also need to merge from unstable into stable, or pull bug fixes from stable into unstable. This is easy to do directly if stable and unstable are non-bare, but much more of a pain to do if they are bare.Vashtivashtia
(continued from previous comment) Since no one is doing work in either stable or unstable directly, it seems safe to keep the working directory in sync with HEAD via a hook. 2. For various reaons, our integration tests can only be run on a server. People want to be able to have a clone on the server of stable/unstable, then create a clone on their local machine of their server-side clone. A typical workflow would involve pulling from the central repo into their server-side clone, then again into their local clone, doing work, then pushing up to the server-side clone to run integration tests.Vashtivashtia
(continued from previous comment) If the tests pass, another push back to the central repo would be done. The "intermediary" server-side repo that sits between the central repo and the local clone would be unable to pull updates from the central repo if it were bare. Since all work is being done on the local clone, it seems safe to make the server-side repo non-bare and have its working directory kept in sync with HEAD. Sorry for the lengthy comments -- I'd be grateful for your thoughts on these two use cases!Vashtivashtia
@David, for use case 1, rather than doing merges directly in the central repos, you generally should do merges in a developer's local directory, then push to the central repos. Otherwise, you end up with periods of time that the central repos are unusable.Disloyalty
Use case 2 is the exception to the rule because you are "deploying" to your integration test server. There should be no uncommitted changes in that repo, so either the checkout or reset methods should work just fine without ever requiring merges.Disloyalty
Augh, that is an excellent point about use case 1 -- I hadn't considered that! As for use case 2, I'll have to think carefully about whether the post-receive hook should really be auto-installed in every repo in anticipation of this use case, or installed manually as needed -- the consequences of forgetting to add the hook manually are potentially serious, but so are the consequences of having one's working tree inappropriately reset. Thanks for taking the time to reply, Karl!Vashtivashtia
M
4

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
Milieu answered 20/5, 2011 at 17:46 Comment(5)
Thanks for pointing me at that script! I hadn't considered stashing existing changes away like that. Do you know whether "checkout -f" would work as well as "reset --hard"?Vashtivashtia
@David: git checkout -f and git reset --hard are equivalent: both will update the index to exactly match HEAD and update the working tree to match the (new) index. Note that because git checkout has several modes of operation, it may be easier for people to immediately understand git reset --hard rather than reasoning out what this checkout command will do (no path arguments means that it will “switch branches”, since there is no commit or branch argument, it defaults to HEAD (which implies no effective “branch switch”) and will update the index and working tree to match that).Milieu
That FAQ answer also links to another way to push to a bare repository, which is what I'd always do if I have to push to a non-bare repository.Triplenerved
Thanks for the pointers to the FAQ entry on pushing to a branch in /refs/remotes/ instead of master -- that is an interesting option! The use cases I am mainly concerned about here, though, are cases in which: 1. no work is being done directly in the repo (ie., the working directory will never be dirty) 2. the repo needs to be pushed to 3. the repo also needs to be able to do pulls/merges. I gave a few examples in my reply to Karl above (separate central stable/unstable repos, "intermediary" repos used for integration testing) -- I'd be grateful for your thoughts on these cases as well! :)Vashtivashtia
@David: It seems to me that your points 1 and 3 are contradictory. When ever you do a merge (or pull), you run the risk of having to deal with conflicts. Having to resolve conflicts means that you will have a dirty working tree and index. As Karl pointed out (in comments on his answer), such merge work is usually done in a normal development repository instead and pushed into your central repository instead of trying to do it directly in the central repository.Milieu

© 2022 - 2024 — McMap. All rights reserved.