Question

I'm working on a pre-commit hook that uses YUI Compressor to minify any CSS and JavaScript files that have been staged for commit. After the files get minified, the minified versions automatically get staged for commit. I've read that it's generally not a good idea to automatically add machine-generated files to the commit, but I think in this case it's OK. This is what it looks like:

Output of git status:

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   _site-wide.css
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   _subpage.css
#

Output of git commit -m "Updated site-wide styling":

1 CSS file was minified and added to the Git repository
0 JavaScript files were minified and added to the Git repository
[master 41f1815] Updated site-wide styling
 2 files changed, 2 insertions(+), 2 deletions(-)

What happened here is the pre-commit hook used YUI Compressor to minify _site-wide.css, outputting the result to site-wide.css (no leading underscore). It then staged site-wide.css for commit. The pre-commit hook skipped over _subpage.css because, although it had been modified, it was not staged for commit.

Since the CSS and JavaScript files on disk might not be the same as the CSS and JavaScript files staged for commit, I run git stash -q --keep-index before minifying the files and then run git stash pop -q after. This pre-commit hook works fine on repositories that already have a commit, but if I put the pre-commit hook in place before the first commit is made, I get this:

Output of git status:

# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#   new file:   _site-wide.css
#   new file:   _subpage.css
#

Output of git commit -m "Initial commit":

fatal: bad revision 'HEAD'
fatal: bad revision 'HEAD'
fatal: Needed a single revision
You do not have the initial commit yet
2 CSS files were minified and added to the Git repository
0 JavaScript files were minified and added to the Git repository
No stash found.

Output of git status:

# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#   new file:   _site-wide.css
#   new file:   _subpage.css
#   new file:   site-wide.css
#   new file:   subpage.css
#

I will now paste the pre-commit hook's code. Keep in mind that in order to be flexible, I wrote this script so that it could be run in any CakePHP project, not just the ones that have a Git repository. I also wrote it so that you can force it to minify all of the CSS and JavaScript files and not just the ones staged for commit. This is done by running .git/hooks/pre-commit force. Here is the code:

#!/bin/bash

css_files_to_ignore=(
    #"_do_not_minify.css"
)

js_files_to_ignore=(
    #"_do_not_minify.js"
)

if git rev-parse --git-dir > /dev/null 2>&1; then
    git_repository=true
    base_folder="$(git rev-parse --show-toplevel)/app/webroot"

    if [ "$1" == "force" ]; then
        process_unstaged_files=true
    else
        process_unstaged_files=false
    fi
else
    git_repository=false
    base_folder="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/webroot"
fi

if [ -f /Applications/yuicompressor.jar ]; then
    # Mac

    yuicompressor_path=/Applications/yuicompressor.jar
else
    # Linux

    yuicompressor_path=$(command -v yui-compressor)
fi

function process_assets()
{
    extension=$1
    files_minified=0

    for infile in $(echo "$base_folder/$extension/*.$extension")
    do
        # Only process files.

        [[ -f $infile ]] || continue

        filename=${infile##*/}

        # If the filename starts with an underscore, that means that the file is
        # eligible for minification.

        [[ ${filename:0:1} == "_" ]] || continue

        ignore_this_file=false

        files_to_ignore=$extension"_files_to_ignore"

        for i in $(eval echo \${$files_to_ignore[@]})
        do
            if [[ $i == $filename ]]; then
                ignore_this_file=true
                break
            fi
        done

        if [ $git_repository == true ] && [ $process_unstaged_files == false ] && git diff --quiet --cached $infile; then
            # This file is NOT staged for commit.

            ignore_this_file=true
        fi

        if [ $ignore_this_file == false ]; then
            minified_file="$base_folder/$extension/${filename:1}"

            if [ ! -f "$minified_file" ] || test $infile -nt $minified_file; then
                $yuicompressor_command "$infile" -o "$minified_file"

                if [ $git_repository == true ] && [ $process_unstaged_files == false ]; then
                    git add "$minified_file"
                fi

                ((files_minified++))
            fi
        fi
    done

    # Output a summary of what was done.

    if [ $extension == "css" ]; then
        file_type="CSS"
    else
        file_type="JavaScript"
    fi

    echo -n "$files_minified $file_type file"

    if [ $files_minified -eq 1 ]; then
        echo -n " was"
    else
        echo -n "s were"
    fi

    echo -n " minified"

    if [ $git_repository == true ] && [ $process_unstaged_files == false ]; then
        echo " and added to the Git repository"
    else
        echo
    fi
}

if [ -f "$yuicompressor_path" ]; then
    if [ ${yuicompressor_path: -4} == ".jar" ]; then
        yuicompressor_command="java -jar $yuicompressor_path"
    else
        yuicompressor_command=$yuicompressor_path
    fi

    if [ $git_repository == true ] && [ $process_unstaged_files == false ] && ! git diff --quiet --cached; then
        # The staging area is what should be processed rather than what is currently
        # on disk.

        git stash -q --keep-index

        stashed=true
    else
        stashed=false
    fi

    process_assets css
    process_assets js

    if [ $stashed == true ]; then
        git stash pop -q
    fi
else
    echo "YUI Compressor was not found. Aborting."
    exit 1
fi

How can I make this work? Any help would be appreciated.

Was it helpful?

Solution

I've come to the conclusion that there is no solution to this because some Git commands simply will not work when there is no HEAD. Also, after thinking about it some more, I decided that this is kind of an edge case anyway, at least for me, because my very first commit is usually something basic such as the files for the framework that I use. So I can just implement the hook after the first commit.

I appreciate Hassek's time as well as the time of anybody else who read this. Thank you!

OTHER TIPS

With Git 2.22 (Q2 2019, 7 years later), there is one, with git stash as rewritten in C.

See commit 1ac528c (25 Feb 2019) by Paul-Sebastian Ungureanu (weekly-digest[bot]).

stash: make push -q quiet

There is a change in behaviour with this commit.
When there was no initial commit, the shell version of stash would still display a message.
This commit makes push to not display any message if --quiet or -q is specified.

So you should not see "You do not have the initial commit yet" with a git stash -q <command>, starting with the new rewritten-in-C stash command of Git 2.22.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top