My home directory is a checked-out worktree of my dotfiles repo, and for seven years the way changes reached other machines was a direct git push into the live, checked-out $HOME on the far end. Two pieces made that work:
[receive]
denyCurrentBranch = ignoreplus a post-update hook. receive.denyCurrentBranch is the setting behind git’s refusal to accept a push into a checked-out non-bare branch. Set to ignore—committed as “allow updating HEAD worktree”—git accepts the push and updates the branch ref, leaving the worktree stale. The hook did the rest.
It extended the classic contrib sample (whose comment promises push will work “somewhat similarly to darcs and bzr”). The interesting part is what happens when the receiving worktree is dirty. The hook checked git diff-files for unstaged changes and git diff-index --cached for staged ones, composed a description, then stashed under a signal trap:
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"
)It temporarily rewinds HEAD to the pre-push commit so the stash captures the dirty state against the tree it was dirty against, then restores the symbolic ref—and the trap restores it even if the stash step dies mid-flight. But the staged-state detector was broken: git diff-index --cached HEAD@{1} >/dev/null omitted --quiet or --exit-code, so differences still produced status zero and index_dirty stayed false. Staged changes were preserved when unstaged changes happened to trigger the stash too; staged-only changes skipped the stash and the later reset --hard could discard them.
The default git stash save also omitted untracked and ignored files. An untracked path obstructing an incoming tracked file could be removed by git reset --hard. Only tracked changes that reached the stash were preserved.
Then the sync, with a report:
# 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)This runs before the reset, so diff-index compares the incoming HEAD against the still-old worktree; -R reverses the comparison so the report reads as what the update is about to do—a name and a status per file, printed to stderr and therefore into the pushing terminal. The dash-form commands (git-reset, git-diff-index) are another artifact of its era.
The hook was eventually deleted with the commit message “No longer used.” By then the flow had inverted: instead of pushing at machines, every machine fetches and fast-forwards itself at login, which needs no listening end and no hook.