loading...

Don't commit that file!

bronzehedwick profile image Chris DeLuca Originally published at chrisdeluca.me on ・3 min read

I wrote a small git pre-commit hook to prevent committing certain files. There are more words to this, but if you're impatient, you can skip right to the goods.

At work, we have some configuration files tracked in git that we modify locally to enable debugging options. We don't want to ignore these files and have to manage them in a different system outside of git, but we also don't want the debugging options checked in.

So we keep the files tracked in git, and modify them on our local systems, and try to remember not to check in those debugging options.

After the debugging changes ended up in a pull request of mine, I had an idea: since I'm a computer programmer, what if I could use my computer to save myself from myself? It was just crazy enough to work.

What I really wanted was for git to prevent me from committing changes to these files, physically if necessary. The answer: git hooks.

Git hooks are custom scripts that run inside your local repository when one of several actions is taken, like committing, and merging, and the like. They're very powerful, since they can be any script that runs in a shell, but like most things in computer science, they still can't throw a punch. That meant my script would need to throw an error instead to keep me from committing those debugging changes.

A few minutes later I had cobbled together a git pre-commit hook script that prevents any of the unwanted files from being changed. The pre commit hook runs, as the name heavily implies, before the commit happens, so if one of the no-no files is in the changeset, I get a nice big error when I run git commit.

Here's what I came up with:


#!/bin/sh
#
# This script prevents specific file modifications from taking place.
# We want certain config files checked into git so that builds work on a clone,
# *and* we need to modify these files locally to enable debug options.
# This leads to a scenario where we can accidentally check in the config files
# with our local debug options checked in. This script prevents that.

# Get current revision to check against.
if git rev-parse --verify HEAD >/dev/null 2>&1
then
  against=HEAD
else
  # Initial commit: diff against an empty tree object
  against="$(git hash-object -t tree /dev/null)"
fi

# Redirect output to stderr.
exec 1>&2

# Test staged files against the files we don't want to check in,
# and abort if found.
git diff --cached --name-only "$against" | while read -r file;
do
  if test "$file" == "path/to/my/unchanagble/file.yml";
  then
    echo "Don't check in file.yml. Aborting!"
    exit 1
  fi

  if test "$file" == "some/other/file.php";
  then
    echo "Don't check in file.php. Aborting!"
    exit 1
  fi

  # Repeat pattern as necessary.
done

The magic sauce is near the end; I loop over the output of git diff --cached --name-only, which shows the name of each staged file, and check if the file name matches one of the files I don't want to commit. If the file matches, exit with a non-zero status, and git will happily prevent me from making that commit. Hooray!

Posted on by:

bronzehedwick profile

Chris DeLuca

@bronzehedwick

Programming, writing, comedy.

Discussion

pic
Editor guide
 

I've noticed one effect when adding a file to .gitignore. It wasn't removed from repo.

Steps To Reproduce:

  1. Create and complete a file
  2. git add <file> && git commit -m "Initialize <file>"
  3. List the file among exclusion rules in .gitignore

Expected effect:
The file is committed while any further changes thereto get ignored.

There might be problems. Like what happens when we switch branches? How to ensure that if we DO want to change the file for everybody, how to ensure this?

I didn't test the above. Just for your consideration.

 

Adding a file to .gitignore doesn't remove it from git. It just ignores it in the "working tree", and when doing bulk adds (IE: git add . or git commit -a).

You still can stage such changes with git add -f.

 

Correct.

If you need to remove the file from the git cache but not from the file system (aka, if you had a file checked in and then added it to .gitignore), you can run:

git rm --cached path/to/file

git rm --cached path/to/file

Cool I learned something new today

But I'm a little confused about your use case. It seems to me that adding the file to .gitignore would do the trick.

The use case in my post is admittedly pretty specific, but we don't want to add the config files to .gitignore because the files are needed for the codebase to run properly. These config files also happen to have debugging options in them, so you have to modify the git working tree to enable them.

 

You are perfectly correct. And that's the point. One can "freeze" the file at the state it's been at the moment of adding to .gitignore.

I was addressing the following fragment from TS:

we have some configuration files tracked in git that we modify locally to enable debugging options. We don't want to ignore these files and have to manage them in a different system outside of git, but we also don't want the debugging options checked in.

 

I think this is fun, but that the correct solution is to stage files one at a time, after confirming they contain what you expect. This allows you to be incredibly hectic in your development, because you can trust yourself to reel it all back in at commit time. It also means you won't accidentally commit secrets (eg keys) or random files that accidentally wound up in the directory. It also means your commits will probably be much more coherent, b/c you'll be much more aware of what's in them, and you can break them into sensible chunks (eg git add -p)

 

Hmm, like to commit often, and use git commit -av. The verbose option gives me a diff, which I use to make my pre-flight checks.

 

Nice, I didn't know about commit -v I still have to do it one file at a time, though. If I don't think about what's in the file before looking at the diff, I wind up glossing over it and not being in tune with my commits.

 

Hi,

At work, we had the same problem, but we ended trying a different approach.
What we do is having a .dist version of every file that we want to keep tracked "as is" but don´t want to track changes that people do over them, and in the .gitgnore we simply ignore the "non .dist files". So in the end we have a tracked file structure like this:

├── app
│   ├── config
│   │   ├── some-general-config.yml
│   |   ├── local-parameters.yml.dist
│   |   ├── variable-config.yml.dist
|-- .gitignore

and in the .gitignore we just add:

/app/config/local-parameters.yml
/app/config/variable-config.yml

That way everyone (including our testing and production environments) get a copy of the config files that provide the exact info to each app instance that we have. Right now when you clone an instance, you would have to manually create the "real" copies of the config files and edit them, but we´re making a script that we intend to run on our composer install phase that will add a stage that does this automatically.

 

Hey Germán, that's a nice solution. Unfortunately we can't tell our app to recognize a different config file, but you gave me an alternate idea. Perhaps there's a path in YAML where we have the regular config file checked in, then have a local config file that overrides any values set in the general that's in .gitignore. The YAML in the general file would be something like:

source: my-local-config.yml
 

For some reason this made me think about the Google movie where they made an app to prevent yourself from sending drunk texts..be careful before you send that file out ON THE LINE!

 

Oh wow, I completely forgot about that. Wasn't that a Gmail Labs thing at one point? Mail Goggles? I guess I have to cop to re-inventing a weird old gmail filter haha.