gitgit-pushgit-non-bare-repository

Why does my remote Git repository have uncommitted changes after pushing to it?


I set up a new Git repository using the following commands:

mkdir plans-for-world-domination
cd plans-for-world-domination
git init
echo "MWA HA HA HA HA!" > plans.txt
git add .
git commit -m "Beginning my plans..."

Then I made a clone of this repository, made some changes, committed them, and then tried to push:

cd ..
git clone plans-for-world-domination clone
cd clone
echo "Step 1: set up super secret spy base in Cleveland, Ohio" >> plans.txt
git commit -am "Update plans"
git push origin master

When I cd back into the plans-for-world-domination repository though, there are changes staged in the staging-area/index that are the reverse of the changes that I just pushed:

$ cd ../plans-for-world-domination
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   plans.txt

$ git diff --staged
diff --git a/plans.txt b/plans.txt
index febb495..ce01362 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1,2 +1 @@
 MWA HA HA HA HA!
-Step 1: set up super secret spy base in Cleveland, Ohio

Why does my first repo have these unstaged changes that are the reverse of what I just pushed, and how can I fix this?


Solution

  • Pushes don't update the working-copy and staging-area in non-bare repositories

    The staging area in the first repository appears to contain the reverse of the changes that were just pushed because it's a non-bare repository, meaning that it contains a working copy, which is also frequently referred to as a working (directory) tree in the Git documentation. Bare repositories, on the other hand, don't have a working copy directory.

    Since the repository is non-bare, when you push to it, the push only updates branch references, and the symbolic HEAD reference, because git push doesn't operate on the working-copy and staging area that are present in non-bare repos.

    As a consequence of this, the working-copy and staging-area of the non-bare repo are still left on the same state of the repository that was present before the push that updated HEAD. In other words, the actual state of the working-copy and staging-area do not match the state of the commit pointed to by HEAD. This is why those differences between the two states show up when git status and git diff are run:

    $ git status
    On branch master
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
            modified:   plans.txt
    
    $ git diff --staged
    diff --git a/plans.txt b/plans.txt
    index febb495..ce01362 100644
    --- a/hello.txt
    +++ b/hello.txt
    @@ -1,2 +1 @@
     MWA HA HA HA HA!
    -Step 1: set up super secret spy base in Cleveland, Ohio
    

    (Sub-optimal) solution: hard reset

    Since the working-copy and staging-area are out of sync with HEAD, a solution to get them to match again is to simply use

    git reset --hard HEAD
    git reset --hard
    

    to reset the working-coy and staging-area to the commit pointed to by HEAD.

    However, this is not the ideal solution...

    Ideal solution: push to bare repositories instead

    You're not really supposed to push to non-bare repositories, because of this exact issue of their working-copies and staging-areas de-syncing with the repository references. Instead, unless you have an unusual reason to push to a non-bare repository, you really ought to push to bare repositories instead, which don't have a working copy.

    To create a bare repository, simply use the --bare flag:

    # Initialize a bare repo
    mkdir bare
    cd bare
    git init --bare
    
    # Push changes to the bare repo
    cd ..
    mkdir project
    cd project
    # Make some changes and commit
    git remote add origin ../bare
    git push origin master
    
    # Or create a bare clone from another bare or non-bare repo
    git clone --bare <repo-path-or-uri>
    

    Pushing to non-bare repositories is denied by default since Git 1.6.2

    Note that since Git version 1.6.2, pushing to non-bare repositories has been denied by default:

    With the next major release, git push into a branch that is currently checked out will be refused by default. You can choose what should happen upon such a push by setting the configuration variable receive.denyCurrentBranch in the receiving repository.

    In fact, when you try to push to a non-bare repo with current versions of Git, you're push should be denied with the following error message (slightly modified for brevity):

    $ git push origin master
    Total 0 (delta 0), reused 0 (delta 0)
    error: refusing to update checked out branch: refs/heads/master
    error: By default, updating the current branch in a non-bare repository
    error: is denied, because it will make the index and work tree inconsistent
    error: with what you pushed, and will require 'git reset --hard' to match
    error: the work tree to HEAD.
    error:
    error: You can set 'receive.denyCurrentBranch' configuration variable to
    error: 'ignore' or 'warn' in the remote repository to allow pushing into
    error: its current branch; however, this is not recommended unless you
    error: arranged to update its work tree to match what you pushed in some
    error: other way.
    error:
    error: To squelch this message and still keep the default behaviour, set
    error: 'receive.denyCurrentBranch' configuration variable to 'refuse'.
    To non-bare
     ! [remote rejected] master -> master (branch is currently checked out)
    error: failed to push some refs to 'non-bare'
    

    As the error message above explains, you can disable the safety checks that prevent you from pushing into a non-bare repo by disabling the receive.denyCurrentBranch config setting in the remote non-bare repo:

    git config receive.denyCurrentBranch warn   # Warn when pushing to non-bare repo
    git config receive.denyCurrentBranch ignore # Don't even bother warning