Skip to content

Instantly share code, notes, and snippets.

@canton7
Last active November 7, 2023 08:16
Show Gist options
  • Save canton7/1423106 to your computer and use it in GitHub Desktop.
Save canton7/1423106 to your computer and use it in GitHub Desktop.
Local versions of tracked config files

How to have local versions of tracked config files in git

This is a fairly common question, and there isn't a One True Answer.

These are the most common techniques:

If you can modify your application

  1. Modify your app such that, before loading its config from e.g. config.ini, it first looks for e.g. config.mine.ini, and loads it instead if it exists. Track a standard config.ini in your repo, but if you need to override the values in it, copy it to config.mine.ini and modify. Then configure git to ignore config.mine.ini.
  2. Modify your app such that it first loads e.g. config.ini, but then attempts to load e.g. config.mine.ini, and if it exists, uses keys from config.mine.ini to override those obtained from config.ini. This is similar to the first approach, but means that config.mine.ini needs to specify only options that you want to override. Thanks to shrugger for this one.
  3. Have your app look at environmental variables for its config, before using the values from its config file (or hardcoded defaults). Then set those variables as appropriate (e.g. in your .bashrc for shell scripts, or somewhere in your webserver's config for web apps). Only works if your app doesn't need much config, but can be a useful behaviour for simple apps, and is particularly useful for things like API keys.

If you can't modify your application

  1. Have your app just look for e.g. config.ini, but don't track this file. Instead create and track e.g. config.sample.ini. Tell git to ignore config.ini. Everyone who clones the repo has to copy config.sample.ini to config.ini, but they can modify config.ini as much as they want.
  2. Use some gitattributes clean/smudge magic as suggested by SethRobertson, see the other file in this gist.
  3. Use a local configuration branch. This is a more complex setup, but allows the config files to be stored within, and tracked by, git.
  4. Not recommended: Avoid git update-index --assume-inchanged and --skip-worktree for this purpose (despite what some blogs say): they're meant for other things, and do not work as expected here. See "Notes" in man git update-index.

If your local config must be committed, e.g. for deployment to Heroku

If you follow one of the above suggestions -- where tracked and untracked config are stored in different files, with the latter ignored -- but need to commit the untracked config for deploying to somewhere, consider an extra deployment step that does something like the following:

# Assume the remote we're deploying to is called 'heroku', although this isn't heroku-specific
# We'll create a commit on master containing the normally-untracked config, merge that into heroku/master, push, then remove the commit from master
# Require a clean working directory, possibly using `git diff-index --quiet HEAD --` to check for uncommitted stuff
git fetch heroku
git checkout <branch_to_deploy_from> # e.g. master in git-flow
# Add our normally-untracked confing
git add -f <normally_untracked_config_file>
# Create a deploy commit on master
git commit -m "Deploy: Some useful commit message"
# Checkout the branch we're going to push to, and merge that deploy commit in
# We avoid having to have a local version of the heroku/master branch by detaching the HEAD
git checkout -q heroku/master
git merge --no-ff -m "Merge deploy commit" <branch_to_deploy_from>
# Push our deploy commit+merge to heroku
git push heroku HEAD:master
# Checkout <branch_to_deploy_from>, and remove the deploy commit
git checkout <branch_to_deploy_from>
git reset --mixed HEAD^

This creates a nice visual history, with a set of merges from <branch_to_deploy_from> to heroku/master (if you need to roll back), but master branch doesn't have a record of the deployments.

Having per-repo untracked config using gitattributes

This approach was presented by SethRobertson on #git. I haven't had a chance to play with it, and it has some caveats (see below), but it's a nice idea.

First, read up on gitattributes. One Two. Pay attention to the smudge/clean filters.

Create a local branch called "private", which contains your personal config.

Setup script

The following script sets up the the smudge/clean filters for all files in SUBDIR

#!/bin/sh
#
#
SUBDIR=z
x=0
find $SUBDIR -type f | while read f
do
  echo "/$f     filter=private_$x" >> .gitattributes
  git config filter.private_$x.clean "/tmp/clean $f"
  git config filter.private_$x.smudge "/tmp/dirty $f"
  x=$(($x+1))
done

/tmp/dirty

The smudge filter is executed on git checkout, and replaces the contents of every file identified by the setup script with the corresponding file from the "private" branch.

#!/bin/sh
git show "private:$1"

/tmp/clean

The clean filter is executed on git add, and replaces the contents of the (locally modified) files identified by the setup script with their tracked versions from the current branch.

#!/bin/sh
git show "HEAD:$1"

Notes

Note that there's no way to tell which branch is being checked out. Therefore, before checking out out the "private" branch, you have to disable the filters. Renaming .gitattributes is one way of doing this.

Also, SethRobertson alluded to the presence of an undocumented "%f", which would remove the need to create a separate smudge/clean filter pair for every file.

@merusso
Copy link

merusso commented Jun 26, 2015

What about "git update-index --skip-worktree"?

@tobiasvl
Copy link

tobiasvl commented Jul 4, 2016

What about .git/info/exclude?

@kadoban
Copy link

kadoban commented Jul 27, 2016

@TobiasVi: Neither .gitignore nor .git/info/exclude have any effect at all on already-tracked files, so that will do nothing.

@ultraSsak
Copy link

So, git can't handle as simple situation as :
"Yes, i know i have changes in this file, yes, i know, just switch to different branch and leave my file alone"
SVN 1:0 GIT

@nirizr
Copy link

nirizr commented Aug 6, 2017

@ultraSsak sure it can. most of the times git checkout will just work with unstaged files. When it doesn't, and then there's a good reason for it, you can stash, checkout, pop (and handle the conflicts if there are any).

@eigen-value
Copy link

Hi,
Just fyi, "local configuration branch" link is broken

@ggarcia24
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment