How to do a "local-only commit" in git?

I'm using git, and I'd like to be able to create a commit which isn't synced with the remote repository. Such a commit would have to "float" atop all other commits in the local repository to avoid affecting the history. I could use such a commit to store local-specific changes (configuration changes, debugging flags, local workarounds, etc.).

Currently, I manually rebase when I commit to reorder the commit back to the top, and I push using HEAD^ to avoid pushing the local changes. I've also considered putting the changes in the stash, but that's less convenient because it precludes normal use of the stash. Another alternative is to simply leave all those local changes unstaged, and use git add -p every time I want to commit. However, with a large number of trivial local changes, that becomes a hassle.

Here's an example of my current workflow:

My repository initially looks like

A---B---C---F master

where "F" is my floating commit.

I make a commit:

A---B---C---F---D master

then git rebase -i HEAD~2 to reorder:

A---B---C---D---F master

then git push remote HEAD~1... to push everything but the local F commit.

The change F contains changes to existing versioned files, and may contain an arbitrary amount of changes. (If I can make more than one commit "floating", that would be even better, since I could then separate my local changes).


Solution 1:

How about putting those changes into a local branch which you rebase/merge regularly from your main dev branch? That way there wouldn't be any danger of committing them upstream.

Solution 2:

So, it sounds like you want two things:

  • Some commits should be kept private (on a local branch, for example) and never pushed or merged when you pull; they should remain "after" the shared commits.

  • This should be mostly transparent to you; you want to work on master, and have the local branch maintained automatically. You just decide which commits should be local, and commands that interact with remote repositories will ignore those.

So you want to write a script (name it git-something and put it on your path, so it's an extra git command) to identify and deal with these commits. You need some trigger for the script to recognize the local commits by. The easy way to do this is to put a magic word in the commit description -- one you'll never use in a real/shared commit -- for the script to recognize. (If that's too flaky-sounding for you, you could also use a special file in the commit's tree, like .THIS_COMMIT_IS_LOCAL_ONLY; I haven't done that in the examples because it's a little harder.)

You'll need a command to make a local commit from the current index/workdir; this is easy, it just calls git commit $@ -m "__LOCAL_COMMIT_ONLY__" (that's an example; the point is it does something to mark the commit being created as local-only, and then defers to git commit). You'll also need a command to temporarily pop all the local commits, do some other git command (pull, push, fetch, merge, whatever), and then reapply the local commits. You will also use this command for creating local commits that you do intend to share, so that they always appear "under" the local-only commits in your history.

Here's one example script that gives you both in one:

#!/bin/sh
if [[ $1 eq 'new' ]]; then
  shift
  exec git commit $@ -m "__LOCAL_COMMIT_ONLY__"
elif [[ $1 eq

OLD_HEAD=$(git rev-parse HEAD)
OLD_REAL_HEAD="$(git rev-list HEAD --grep=__LOCAL_COMMIT_ONLY__ | tail -n1)^"
git reset --soft $OLD_REAL_HEAD
git $@
git rebase --onto HEAD $OLD_REAL_HEAD $OLD_HEAD

Now, assuming you call the script git-local, use git local new to create a new local-only commit from the index (or git local new -a to create on from modified files in the workdir), git local commit (the name is imperfect, sadly) to create a new "real" commit, git local push to push, git local pull to pull, etc.

The primary downside of this is that it requires you to remember that most commands now get prefixed with local. If you forget to do this once, you're a little bit hosed, but not too badly -- a quick git rebase -i will allow you to easily move your local commits back to the top and you're off and running again. The biggest risk is that you accidentally use git push instead of git local push and send all your private changes upstream, which will annoy everyone. For that, you might want to actually write a little wrapper script to invoke instead of git itself (call it ~/bin/git and make sure ~/bin is on your path):

#!/bin/sh
if [[ $1 = 'push' ]]; then
  if /usr/bin/git rev-list HEAD --grep=__LOCAL_COMMIT_ONLY__ | grep -q .; then
    echo "Can't push with local changes still active!"
    echo "Try using `git local push' instead."
    exit 1
  fi
fi
exec /usr/bin/git "$@"

You could also make a pre-receive hook on the server that automatically rejects any commit containing __LOCAL_COMMIT_ONLY__ in its message.