Вообще-то как раз гит хранит целые копии файла.
Для каждого файла создается его хеш, и файл-объект хранится под именем с этим кешом.
If you again examine your objects directory, you can see that it now contains a file for that new content. This is how Git stores the content initially — as a single file per piece of content, named with the SHA-1 checksum of the content and its header. The subdirectory is named with the first 2 characters of the SHA-1, and the filename is the remaining 38 characters.
То есть любое изменение файла - создает еще один файл-объект.
Два одинаковых файла не будут занимать два места, даже если они хранятся под разными именами.
Каждый коммит - содержит список файлов и хеш для содержимого.
А ветка - это просто ссылка на конкретный коммит и немного метаданных.
Также файлы-объекты хранятся упакованными, а периодически файлы-объекты могут быть объеденены в отдельный пакет.
Чтобы посмотреть содержимое любого гит-объекта, юзай
git cat-file -p ID_объекта (где айди объекта это как раз его хеш)
И собственно именно эта фича - хранение каждого изменения файла отдельным объектом и позволило создать легковесные ветки, где переключение на любой коммит любой ветки - быстрая проверка и копирование файлов, в отличие от CVS и SVN, где любое переключение ветки - куча пересчетов диффов как назад так и вперед.
Но, поскольку SVN - централизированная система, где все изменения хранятся только на сервере, можно менять формат хранения между версиями, так как это не нужно согласовывать со всеми пользователями репозитория.
Например кроме диффов, в поздних SVN периодически сохраняются полные снепшоты, например каждые 1000 коммитов делается полный слепок, что ускоряет перерасчеты.