git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: git push to a non-bare repository

From
Theodore Tso <tytso@mit.edu>
Date
Mar 19, 2007, 02:00 UTC
Message-ID
<20070319020053.GA11371@thunk.org>
In-Reply-To
<vpq648ye9w6.fsf@olympe.imag.fr>
On Sun, Mar 18, 2007 at 06:31:21PM +0100, Matthieu Moy wrote:
Show 6 quoted lines
> I have a repository with a working tree on a machine A, did a clone to
> another machine B and commited there locally.
> 
> I want my changes to get back into the first repository, so I did a
> "push". The new commit is in the history, I can see it with "git log",
> but the modifications are not in the working tree.

The general answer (which you've already received) is to tell folks is to simply don't use "git push" to remote trees; basically, if you ever have a non-bare repository, it doesn't do what you expect, and it will leave the novice user horribly confused. A much better answer is to simply go back to machine A, and pull from machine B.

I was exploring though to see if there was anything we could do better, and so I used my standard test repository of the GNU Hello, world program, and did the following:

	git clone hello r1
	git clone r1 r2
	cd r1
	<edit hello.c's headers to be GPL v2 only>
	git commit -a -m "GPL v2 only"
	cd ../r2
	<edit hello.c so that the message printed is "Hello, world!" 
		instead of "hello, world">
	cd ..

OK, so this sets up the standard test setup of repositories r1 and r2. r1 contains a committed change so that hello.c is GPLv2 only. r2 contains an uncommitted change to the actual text printed by hello.c. The changes are nicely seprated in distaince by over 100 lines, so there should be no problems with merges. Let's play...

Experiment #1.  Let's try pushing from r1 to r2.
	cd r1
	git push ../r2

This pushes the change GPLv2 change from r1 to r2. However, it leaves the working tree and the index untouched, which leads to some very unexpected and surprising behavior:

  a) If you do a "git commit" you will commit the current contents of
     the index, which is usually the contents of the head of r2 before 
     the push.
  b) If you do a "git commit -a" you will commit the modified changes to 
     the working directory --- based off of the state of r2 before the
     push.  What will therefore show up in the revision log is something
     which appears to be based off of the more recent change in r1, but 
     which is really based off of the old history as of r2 before the push.

All of this is bad, which is why "git push" to a non-bare repository is extremely surprising. (As an aside, what Bitkeeper would do is to update the working tree, but it added the constraint that it would only allow the "bk push" if the push resulted in a fast-forward merge, thus guaranteeing no conflicts, and if the none of the files that would need to be updated in the working tree had been locally modified; if either constraint were modified, the push would be aborted, and the remote repository not modified at all. It would be nice if we could enforce these constraints using the appropriate hooks, but from what I can tell the hooks aren't in the right place for us to be able to do that, or to be able to undo or recover from a bad push. More on that in a bit.)

OK, so suppose we do the push anyway. As you suggested, one possible solution is:

> $ git reset --soft <commit-id-before-the-push>
> $ git merge <commit-id-after-the-push>
> 
> But it means I have to remember <commit-id-before-the-push>.

Is it at all possible to figure out <commit-id-before-the-push>? It seems the answer is no, and I suspect that's a bug. Maybe it doesn't make sense to save the original HEAD in ORIG_HEAD in r2, but surely the original HEAD should be saved in the reflog, right? Well, at the moment it saves it in neither case.

The problem is without doing this, it is as far as I can tell impossible to determine what revision the index is currently corresponding to, and without this information, it's very limited in what you can do.

What possible solution which you *can* do is:
	git diff > /tmp/stage-patch
	git reset --hard
	patch -p1 < /tmp/stage-patch

But that seems a bit manual and somewhat kludgy. If we had the revision of r2 before the push, it would be possible to do a 3-way merge, which in some cases might result in a cleaner merge of the modified files in the working tree.

Experiment #2.  Let's try pulling from r2 to r1

So this is what we tell people they should do; so how well does it work in this case?

	<do the above experimental setup>
	cd r2
	git pull ../r1
What do we get?
	Updating f2e3cc0..37508dc
	hello.c: needs update
	fatal: Entry 'hello.c' not uptodate. Cannot merge.
	 hello.c |    7 +++----
	 1 files changed, 3 insertions(+), 4 deletions(-)

Oh, dear. Since hello.c was locally modified, git-merge refused to do a 3-way merge to the local file. That's unfortunate, since if it had, it would have succeeded, and in this case it would have done the right thing. git-checkout will do something similar, but it at has an -m option which will do a 3-way merge to update the local working file. Unfortunately git-merge and git-pull do not have such an option. 'twould be nice if it did, but that's going to have to wait someone wanting to scratch that particular itch...

The failure leaves the index and the working tree in the same confused state as the "git push" scenario, though --- the index is still referring to original state of the tree before the pull, but the HEAD has been updated to after the pull, so "git commit" will lead to the same confusing behavior. Fortunately, though, when we do a pull, ORIG_HEAD and the reflog are updated, so we can get back to our original state via a command like this:

	git update-ref HEAD ORIG_HEAD

So ok, let's break down the git pull into its two constiuent parts. The git-fetch and the git-merge.

	git fetch
	git merge FETCH_HEAD
This fails in the same way:
   Updating f2e3cc0..37508dc
   hello.c: needs update
   fatal: Entry 'hello.c' not uptodate. Cannot merge.
    hello.c |    7 +++----
    1 files changed, 3 insertions(+), 4 deletions(-)

And just as before, it leaves HEAD updated to FETCH_HEAD, even though it failed. So it's consistent, but that's not what the documentation for git-merge states:

      You  may  have  local modifications in the working tree files. In other
      words, git-diff is allowed to report changes. However, the  merge  uses
      your  working  tree  as  the  working area, and in order to prevent the
      merge operation from losing such changes, it makes sure  that  they  do
      not  interfere  with the merge. Those complex tables in read-tree docu-
      mentation define what it means  for  a  path  to  "interfere  with  the
      merge".  And  if  your  local  modifications  interfere with the merge,
      again, it stops before touching anything.
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Well, it certainly stops before touching any local files, but it has already updated HEAD, which leaves things in a very confusion situation. Maybe it would be better if git-merge atomically failed and left HEAD back pointing at the original revision?

						- Ted
Previous: Junio C HamanoNext: Junio C Hamano
Message 6 of 27 in “git push to a non-bare repository”
  1. Matthieu MoyMar 18, 2007
  2. Junio C HamanoMar 18, 2007
  3. Sam VilainMar 18, 2007
  4. Jakub NarebskiMar 18, 2007
  5. Junio C HamanoMar 18, 2007
  6. Theodore TsoMar 19, 2007
  7. Junio C HamanoMar 19, 2007
  8. Shawn O. PearceMar 19, 2007
  9. Theodore TsoMar 19, 2007
  10. Shawn O. PearceMar 19, 2007
  11. Theodore TsoMar 19, 2007
  12. Shawn O. PearceMar 19, 2007
  13. Nicolas PitreMar 19, 2007
  14. Theodore TsoMar 19, 2007
  15. Junio C HamanoMar 19, 2007
  16. Nicolas PitreMar 19, 2007
  17. Nicolas PitreMar 19, 2007
  18. Sam VilainMar 19, 2007
  19. Junio C HamanoMar 20, 2007
  20. Junio C HamanoMar 20, 2007
  21. Theodore TsoMar 19, 2007
  22. Shawn O. PearceMar 19, 2007
  23. Junio C HamanoMar 19, 2007
  24. Matthieu MoyMar 19, 2007
  25. Jakub NarebskiMar 19, 2007
  26. Neil SchemenauerMar 21, 2007
  27. Sergio CallegariMar 19, 2007

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.