94

I am writing a pre-commit hook. I want to run php -l against all files with .php extension. However I am stuck.

I need to obtain a list of new/changed files that are staged. deleted files should be excluded.

I have tried using git diff and git ls-files, but I think I need a hand here.

igorw
  • 27,759
  • 5
  • 78
  • 90

6 Answers6

115

A slightly neater way of obtaining the same list is:

git diff --cached --name-only --diff-filter=ACM

This will return the list of files that need to be checked.

But just running php -l on your working copy may not be the right thing to do. If you are doing a partial commit i.e. just selecting a subset of the differences between your current working set and the HEAD for the commit, then the test will be run on your working set, but will be certifying a commit that has never existed on your disk.

To do it right you should extract the whole staged image to a temp area and perform the test there .

rm -rf $TEMPDIR
mkdir -p $TEMPDIR
git checkout-index --prefix=$TEMPDIR/ -af
git diff --cached --name-only --diff-filter=ACM | xargs -n 1 -I '{}' \bin\echo TEMPDIR/'{}' | grep \\.php | xargs -n 1 php -l

See Building a better pre-commit hook for Git for another implementation.

chim
  • 8,407
  • 3
  • 52
  • 60
LarryH
  • 1,718
  • 2
  • 12
  • 15
  • 4
    It is actually possible to pipe the file contents to `php -l`. And that's what we ended up with. See here: http://github.com/phpbb/phpbb3/blob/develop-olympus/git-tools/hooks/pre-commit – igorw Jun 19 '10 at 11:50
  • 2
    To check the syntax of a staged file, you can use `git show :FILENAME | php -l`. – Aad Mathijssen Nov 26 '14 at 09:38
  • 11
    --diff-filter should probably be "ACMR", because renamed files (R) can have changes too. – Droopycom Apr 20 '18 at 20:00
57

git diff --cached --name-status will show a summary of what's staged, so you can easily exclude removed files, e.g.:

M       wt-status.c
D       wt-status.h

This indicates that wt-status.c was modified and wt-status.h was removed in the staging area (index). So, to check only files that weren't removed:

steve@arise:~/src/git <master>$ git diff --cached --name-status | awk '$1 != "D" { print $2 }'
wt-status.c
wt-status.h

You will have to jump through extra hoops to deal with filenames with spaces in though (-z option to git diff and some more interesting parsing)

araqnid
  • 127,052
  • 24
  • 157
  • 134
  • Thanks, that's a good start. However, if I change a file without staging it, it's still displayed. I am running git version 1.7.0.1.147.g6d84b (recent custom build). Not sure if this is intended behavior. – igorw Mar 10 '10 at 16:10
  • That sounds odd. The "--cached" switch should make it only show files that have been staged: although I'm testing this with 1.6.5, it seems surprising that that would have changed... does "git diff --cached" on its own show the unstaged changes? – araqnid Mar 10 '10 at 17:18
  • After some debugging I was able to track it back to an other cause. Thanks a lot! – igorw Mar 10 '10 at 17:32
  • @igorw, I would be interested but the link is dead. – Simon Aug 26 '16 at 13:50
  • Just to note if the only thing wanted is the name of the file, there exists--name-only instead of --name-status. Could cut the extra awk hoop. – L.P. Apr 27 '17 at 14:06
  • 2
    The `print $2` part of the awk command only works correctly if the file name doesn't contain spaces. One possible fix for this: `... | awk '$1 != "D" { $1=""; sub(FS,""); print $0 }'`. Another note: for file renames, the `git diff` command prints OLD and NEW names. If only the new name is desired, add a `--no-renames` option to `git diff`, this will treat the rename as deletion of the old file and addition of the new file. – Gene Pavlovsky Apr 08 '19 at 13:10
26

None of the answers here support filenames with spaces. The best way for that is to add the -z flag in combination with xargs -0

git diff --cached --name-only --diff-filter=ACM -z | xargs -0 ...

This is what is given by git in built-in samples (see .git/hooks/pre-commit.sample)

eddygeek
  • 4,236
  • 3
  • 25
  • 32
17

Here is what I use for my Perl checks:

#!/bin/bash

while read st file; do
    # skip deleted files
    if [ "$st" == 'D' ]; then continue; fi

    # do a check only on the perl files
    if [[ "$file" =~ "(.pm|.pl)$" ]] && ! perl -c "$file"; then
        echo "Perl syntax check failed for file: $file"
        exit 1
    fi
done < <(git diff --cached --name-status)

for PHP it will look like this:

#!/bin/bash

while read st file; do
    # skip deleted files
    if [ "$st" == 'D' ]; then continue; fi
    # do a check only on the php files
    if [[ "$file" =~ ".php$" ]] && ! php -l "$file"; then
        echo "PHP syntax check failed for file: $file"
        exit 1
    fi
done < <(git diff --cached --name-status)
lcetinsoy
  • 58
  • 6
  • 2
    Pretty good, but doesn't work for partially staged files, because it reads the whole file. – igorw Nov 06 '10 at 11:00
  • Thanks ! I adapted your code and I put <<<$(git diff --cached --name-status) after the done instead using a pipe so that no subshell is started in the loop. It allows allowing variable updating in the loop to be used latter. Submitting an update of answer for review. Best – lcetinsoy Apr 25 '20 at 12:13
  • cant edit my comment again so synthax is actually '< <$(command) like https://stackoverflow.com/a/7390610/5203829 – lcetinsoy Apr 25 '20 at 12:53
2

git diff --cached is not sufficient if the commit call was specified with the -a flag, and there is no way to determine if that flag has been thrown in the hook. It would help if the arguments to commit should be available to the hook for examination.

mpersico
  • 766
  • 7
  • 19
  • git diff --cached DOES appear to be sufficient. However, I believe that if you run git status --porcelain inside your hook, all the files that will be processed will not have a blank or a ? in the first position of the output. I haven't fully tested it, but so far, it has held up in all the conditions I have in my repo, a mix of new, added, modified files where I try to commit explicit files, the default set of files, -a for everything. So why use git status instead of git diff? I think it's easier to parse. – mpersico Oct 31 '16 at 18:55
  • `git status --porcelain | grep -E -v '^[? ]'` – mpersico Oct 31 '16 at 19:02
  • `git status --porcelain | perl -ane 'print $F[1],qq(\n) if m/^[ACM] /'` is a better answer. It has the advantage of using a --porcelain option, guaranteed to never change. Use your own parser if perl is too heavyweight for you. – mpersico Oct 31 '16 at 19:15
0

to understand that the files have changed in a specific folder, I do this:

modifiedFrontendFiles=$(git diff --cached --name-status --relative=frontend)

if [ -n "$modifiedFrontendFiles" ]; then
    npm run lint
    npm run lint-css
    npm run format
    git add .
fi

in my case i check that the changes are in the frontend folder