The main problem with using git checkout HEAD -- . or git checkout HEAD -- path is that Git tries to optimize away the checkout, if Git thinks there's no need to do the checkout.1 As you noted in your own answer, git reset has the same optimization.
There are several ways to defeat the optimization. The one you chose—using git ls-files -z | xargs -0 rm -f—works fine. Another is to touch each file that you want Git to overwrite: this invalidates the cache data in Git's index. Since touch doesn't actually modify the file, this can be simpler: touch * is usually fairly painless. It doesn't handle subdirectories, though. You could run git ls-files -z | xargs -0 touch, which is almost the same as in your answer.
1Obviously, if CRLF-handling for index-to-work-tree copying has changed since the previous index-to-work-tree copy step, this optimization is incorrect: Git does need to do the checkout. But Git's cache doesn't realize that. Possibly there should be a plumbing command, git whack-cache or git frotz-index or something, that just invalidates each cache entry, so as to force a real checkout.