59

I'm trying to set up Git for staging my website so that I can git pull to get the current version to work on locally and then git push to push the changes to the remote server. I've got it set up so that it works the way I want it to, but after I push, I have to manually run git checkout -f or git reset --hard HEAD on the remote server.

I've tried putting these into a shell script as the post-receive hook on the server, but it just doesn't seem to have any effect. I know that the script is running because I'm seeing "Changes pushed to server" after I push. Here's the post-receive hook:

#!/bin/sh
git reset --hard HEAD
echo "Changes pushed to server."
tshepang
  • 10,772
  • 21
  • 84
  • 127
Matt
  • 605
  • 1
  • 6
  • 7
  • @VonC: The most important part of [your answer](http://stackoverflow.com/questions/3838727/git-post-receive-hook-for-website-staging/3838796#3838796) was written mostly in `bash` language, while the downvoter probably asserted his native one ;) – takeshin Oct 01 '10 at 13:15

6 Answers6

73

The answer to your question is here: http://toroid.org/ams/git-website-howto

In short, what you want to do is add a "detached work tree" to the bare repository. Normally you think of your work tree as containing the .git directory. Bare repositories do not have a work tree by definition, but you can create one as long as it's in a different directory than the bare repo.

The post-receive hook is just a simple git checkout -f to replicate the repository's HEAD into the work directory. Apache uses that as its document root, and you're all set. Any time you push to the bare repository, Apache will immediately start serving it.

I generally use this to automatically push to a staging server to see if the "real" environment will puke on my changes. Deploying to the live server is a completely different story. :-)

Paul
  • 2,488
  • 20
  • 25
  • Thank you, I'm still new to Git and when I got started with this project, I wasn't sure why there should be a bare repository when I could cd into the document root and just git init. I think understand now that it just keeps the git metadata out of the document root. Is that correct? – Matt Oct 01 '10 at 14:30
  • Also, the example in the link above above shows how to start from scratch and push to the server from a local repository. What's the best method of getting a bare repository (outside the web document root) from the one that's currently in there? – Matt Oct 01 '10 at 14:35
  • Matt, a bare repository is generally used on your central server. If you only have one copy of the repository and you are deploying directly from that, you will quickly run into issues. Create a bare repository in another directory with `git init --bare`. Then in your local clone of the repository, do `git origin add path_to_central_repo` to mark the new central repo as the original. Finally, `git push origin master` will push everything you have done to the master. Create a detached work tree from the central repo and you will stage your site with every `push` from your clone. – Paul Oct 04 '10 at 14:29
  • +1 ... This solution seems to be the most intelligent that I found thus far ... Allowing the ability to not only transport changes with few command steps ... but also maintaining chain-of-custody so to speak when work is performed directly on the server, requiring a pull when changes are made remotely ... – Edward J Beckett May 04 '12 at 20:02
  • 2
    [A web-focused Git workflow](http://joemaller.com/990/a-web-focused-git-workflow/) is another good article with a slightly different approach. – Ian Dunn Aug 14 '12 at 23:49
15

Update March 2015

As I mentioned in "What is this Git warning message when pushing changes to a remote repository?", you actually can push directly to a non-bare repo now (Git 2.3.0+, February 2015) with:

git config receive.denyCurrentBranch updateInstead

Update the working tree accordingly, but refuse to do so if there are any uncommitted changes.

That would allow you to avoid any post-receive hook.


(Original answer: Oct 2010)

The GitFAQ recommends for non-bare repo this post-update hook:
(it might give you more clue as to what is actually going on in the hook execution. Note this is a post-update hook, not a post-receive)

#!/bin/sh
#
# This hook does two things:
#
#  1. update the "info" files that allow the list of references to be
#     queries over dumb transports such as http
#
#  2. if this repository looks like it is a non-bare repository, and
#     the checked-out branch is pushed to, then update the working copy.
#     This makes "push" function somewhat similarly to darcs and bzr.
#
# To enable this hook, make this file executable by "chmod +x post-update".

git-update-server-info

is_bare=$(git-config --get --bool core.bare)

if [ -z "$is_bare" ]
then
    # for compatibility's sake, guess
    git_dir_full=$(cd $GIT_DIR; pwd)
    case $git_dir_full in */.git) is_bare=false;; *) is_bare=true;; esac
fi

update_wc() {
    ref=$1
    echo "Push to checked out branch $ref" >&2
    if [ ! -f $GIT_DIR/logs/HEAD ]
    then
        echo "E:push to non-bare repository requires a HEAD reflog" >&2
        exit 1
    fi
    if (cd $GIT_WORK_TREE; git-diff-files -q --exit-code >/dev/null)
    then
        wc_dirty=0
    else
        echo "W:unstaged changes found in working copy" >&2
        wc_dirty=1
        desc="working copy"
    fi
    if git diff-index --cached HEAD@{1} >/dev/null
    then
        index_dirty=0
    else
        echo "W:uncommitted, staged changes found" >&2
        index_dirty=1
        if [ -n "$desc" ]
        then
            desc="$desc and index"
        else
            desc="index"
        fi
    fi
    if [ "$wc_dirty" -ne 0 -o "$index_dirty" -ne 0 ]
    then
        new=$(git rev-parse HEAD)
        echo "W:stashing dirty $desc - see git-stash(1)" >&2
        ( trap 'echo trapped $$; git symbolic-ref HEAD "'"$ref"'"' 2 3 13 15 ERR EXIT
        git-update-ref --no-deref HEAD HEAD@{1}
        cd $GIT_WORK_TREE
        git stash save "dirty $desc before update to $new";
        git-symbolic-ref HEAD "$ref"
        )
    fi

    # eye candy - show the WC updates :)
    echo "Updating working copy" >&2
    (cd $GIT_WORK_TREE
    git-diff-index -R --name-status HEAD >&2
    git-reset --hard HEAD)
}

if [ "$is_bare" = "false" ]
then
    active_branch=`git-symbolic-ref HEAD`
    export GIT_DIR=$(cd $GIT_DIR; pwd)
    GIT_WORK_TREE=${GIT_WORK_TREE-..}
    for ref
    do
        if [ "$ref" = "$active_branch" ]
        then
            update_wc $ref
        fi
    done
fi

For this to work, you would still need to specifically allow pushing changes to the current branch by using either one of these configuration settings:

git config receive.denyCurrentBranch ignore

or

git config receive.denyCurrentBranch warn
Community
  • 1
  • 1
VonC
  • 1,042,979
  • 435
  • 3,649
  • 4,283
  • 4
    +1: The script may seem bulky or verbose, but it is that way for a good reason; unlike the blunt approaches of using a plain `git reset --hard` or `git checkout -f`, it will preserve any uncommitted changes in a stash. – Chris Johnsen Oct 01 '10 at 13:39
  • This script is totally fubar and it doesn't do anything. I managed to repair it (hopefully correctly), see my answer... – Tronic Jul 27 '12 at 16:52
  • With Git 2.3 you don't need this hook anymore you can do `git config receive.denyCurrentBranch updateInstead`. See https://github.com/blog/1957-git-2-3-has-been-released . – Aurelien Mar 15 '15 at 20:37
  • @Aurelien True, good point. I have edited the answer, since I actually had that option covered in http://stackoverflow.com/a/28262104/6309 last February. – VonC Mar 15 '15 at 20:51
11

I had the exact same issue. In a reply to this link: http://toroid.org/ams/git-website-howto - The following command has done it:

sudo chmod +x hooks/post-receive

We missed a sudo permission first configured the stuff.

ido
  • 763
  • 1
  • 7
  • 20
6

Fixed version of VonC's script, works for me (absolutely no guarantees).

#!/bin/sh
#
# This hook does two things:
#
#  1. update the "info" files that allow the list of references to be
#     queries over dumb transports such as http
#
#  2. if this repository looks like it is a non-bare repository, and
#     the checked-out branch is pushed to, then update the working copy.
#     This makes "push" function somewhat similarly to darcs and bzr.
#
# To enable this hook, make this file executable by "chmod +x post-update".

set -e

git update-server-info

is_bare=$(git config --get --bool core.bare)

if [ -z "${is_bare}" ]
then
    # for compatibility's sake, guess
    git_dir_full=$(cd $GIT_DIR; pwd)
    case $git_dir_full in */.git) is_bare=false;; *) is_bare=true;; esac
fi

update_wc() {
    ref=$1
    echo "Push to checked out branch $ref" >&2
    if [ ! -f ${GIT_DIR}/logs/HEAD ]
    then
        echo "E:push to non-bare repository requires a HEAD reflog" >&2
        exit 1
    fi
    if (cd ${GIT_WORK_TREE}; git diff-files -q --exit-code >/dev/null)
    then
        wc_dirty=0
    else
        echo "W:unstaged changes found in working copy" >&2
        wc_dirty=1
        desc="working copy"
    fi
    if git diff-index --cached HEAD@{1} >/dev/null
    then
        index_dirty=0
    else
        echo "W:uncommitted, staged changes found" >&2
        index_dirty=1
        if [ -n "$desc" ]
        then
            desc="$desc and index"
        else
            desc="index"
        fi
    fi
    if [ "$wc_dirty" -ne 0 -o "$index_dirty" -ne 0 ]
    then
        new=$(git rev-parse HEAD)
        echo "W:stashing dirty $desc - see git-stash(1)" >&2
        ( trap 'echo trapped $$; git symbolic-ref HEAD "'"$ref"'"' 2 3 13 15 ERR EXIT
        git update-ref --no-deref HEAD HEAD@{1}
        cd ${GIT_WORK_TREE}
        git stash save "dirty $desc before update to $new";
        git symbolic-ref HEAD "$ref"
        )
    fi

    # eye candy - show the WC updates :)
    echo "Updating working copy" >&2
    (cd ${GIT_WORK_TREE}
    git diff-index -R --name-status HEAD >&2
    git reset --hard HEAD
    # need to touch some files or restart the application? do that here:
    # touch *.wsgi
    )

}

if [ x"${is_bare}" = x"false" ]
then
    active_branch=$(git symbolic-ref HEAD)
    export GIT_DIR=$(cd ${GIT_DIR}; pwd)
    GIT_WORK_TREE="${GIT_DIR}/.."
    for ref in $(cat)
    do
        if [ x"$ref" = x"${active_branch}" ]
        then
            update_wc $ref
        fi
    done
fi
Tronic
  • 9,852
  • 2
  • 38
  • 50
6

Simple script for setting this git deployment:

Preparing post-receive hook:

echo '#!/bin/sh'        >  .git/hooks/post-receive
echo 'git checkout -f'  >> .git/hooks/post-receive
echo 'git reset --hard' >> .git/hooks/post-receive
chmod +x .git/hooks/post-receive

Allowing push into this repository, though it is not bare:

git config receive.denycurrentbranch false
Honza
  • 930
  • 10
  • 17
  • 1
    You can also make use of external worktree: `git config core.worktree /path/to/workdir`. You can turn bare repository into one with worktree for that (`git config core.bare false`) – Vi. Sep 21 '13 at 01:22
  • Why do you need `git reset --hard` BTW? – Vi. Sep 21 '13 at 01:23
  • 3
    You can also add `git diff -R --cached --name-status` before checkout to get nice listing of what files are being updated on the pushing side. – Vi. Sep 21 '13 at 01:23
  • Vi: When I try it without "git reset --hard", sametime worktree was not changed when I push into repository and git behaved as if I changed files in worktree of the server manually back. – Honza Sep 21 '13 at 15:31
1

I'm just guessing, but this may be some permission issue (full path needed? cd?). Check what is really happening in the log files.

However publishing the files via git is always just one tasks of the publishing process. You usually need to copy some files, delete other, setup, update permissions, generate docs etc.

For a complex solution a build script might be better than any git hook. Tools which can handle those tasks very well:

(I realize this is not the answer you are expecting, but it's too long to post as a comment)

takeshin
  • 44,484
  • 28
  • 112
  • 160