Dealing With Compiled Files in Git

Why might you need this?

If you have file A, and every time you change A, file B gets rebuilt, and you have to commit file B, these steps will help you. These examples are written with Sass in mind.

I know what you're thinking, you shouldn't commit built CSS. That's not the point. Sometimes you have to. Let's make that process better.

1. Don't Show Minified CSS In Your Diffs

Why: We don't want to see huge blobs of minified CSS when we run git diff. We only care about diffs of the source (Sass) files.

How: List built files in a .gitattributes file, in your repository's root directory:

path/to/your/built/file1.css    -diff  
path/to/your/built/file2.css    -diff  

Explanation: This tells Git not to use a diff program when looking at your built files, essentially marking them as binary. You can learn more at the gitattributes documentation.

Now when we run git diff:

# On branch dingus
#
#   modified:   path/to/your/built/file.css

Binary files a/path/to/your/built/file.css and b/path/to/your/built/file.css differ  

Ah, so nice.

To get the diff back, just pass the -a or --text flag. They do the same thing, which is treat all files as text.

git diff -a -- path/to/your/built/file.css

index 57eb03f..278a4a2 100644  
--- a/path/to/your/built/file.css
+++ b/path/to/your/built/file.css
@@ -17,7 +17,7 @@
- */.clearfix{*zoom:1}.clearfix:before...
+ */.clearfix{*zoom:1}.clearfix:before...

Caveats:

Github does not respect binary flags, so you will still see full diffs on the "files changed" tab.

2. Don't Let Compiled Files Conflict In A Rebase

Why: When rebasing branches containing minified CSS, the blobs can conflict. We don't want these files to merge, we just want their source files to merge cleanly.

How: Set up a merge driver in your .git/config file:

[merge "ours"]
    name = "Keep ours merge"
    driver = true

Then tell your built files to use that merge driver in your .gitattributes file:

path/to/your/built/file2.css merge=ours  

Explanation: When merging built CSS files, use --ours, as in git checkout --ours file, which will use their branch's version of the file. That's not a typo, Git is just weird. Instead of trying to merge the files, it just picks one version over the other. The next section tells us how to automatically rebuild the files so they aren't out of sync after a rebase.

Caveats

You can't commit your .git/config. You must add the merge driver for every repo setup.

3. Rebuild Files Automatically After A Rebase

Why: Remember, the above step will just pick one version of the file, not attempt to merge them. After you have merged the source files (Sass) cleanly in a rebase, let's automatically rebuild the output (CSS) files and sneak them into the commit.

How: Create a Git hook in a file called exactly: .git/hooks/post-rewrite

if [[ $1 = "rebase" ]]; then  
    echo "\nRebuiling compiled files post $1..."

    YOUR SASS BUILD SCRIPT HERE

    echo "Adding built files to the last commit"
    git add -u
    git commit --amend --no-edit
fi  

Then chmod +x .git/hooks/post-rewrite

Explanation: Every time you rebase code, ever, it will run your Sass build script and add the changes to the previous commit. In this script $1 will be one of rebase or amend. We don't want to run it on amend because it will loop infinitely, since the last step is amending the commit.

For non-Sass examples, just run your build script.

Caveats

  • You can't commit Git hooks. Damn you, Linus.
  • If you can't run your build script command line (let's say your IDE generates these files automagically), then…uh…go home.

4. Prevent Yourself From Committing Unbuilt Files

Why: This is more trivial, but I find it useful. For example, our Jenkins build server will fail a build if you didn't compile your Sass before committing, to let you know you were working without of date styles locally. I would rather know this at commit time than at build time.

How: Create a pre-commit Git hook in .git/hooks/pre-commit

#!/bin/sh
# Check for built files, and yell at user if not

# Find source files that are marked as modified
sources=`git status -s | grep 'M.*PATTERN\/.*'`

# Find built files that have changed (we don't use M because they could be deleted, etc)
built=`git status -s | grep 'BUILT_FILE'`

# If source files have changed, but not built files, block the commit
if [ -n "$sources" ] &&  [ -z "$built" ]; then  
    echo "Build the files, you clod!"
    exit 1
fi  

Then chmod +x .git/hooks/pre-commit

For Sass, Replace PATTERN with the name of your Sass directory, and replace BUILT_FILE with path/to/your/built/file1.css.

Explanation: Before Git accepts your commit, it will run this hook. If you have changed source file A, and compiled file B that depends on A is not changed, it will prevent you from committing. We use exit 1 (quit script and return status code of 1) which means there was an error, and Git will not let you commit.

You can learn more about bash's horrifying conditional syntax at Introduction to if.

That's it!

If this article helped you improve your Git setup, consider following me on Twitter or buying me a coffee :).

comments powered by Disqus