git diff: все формы сравнения
git diff — это инструмент сравнения двух snapshot’ов чего угодно: working tree, index, коммит, ветка. По дефолту git diff без аргументов показывает неstaged изменения в working tree (то, что ты ещё не сделал git add). Но это лишь одна из десятка форм.
В этом уроке мы разбираемся, какие сравнения возможны, как работают --staged, --cached, range syntax с .. и ..., и как использовать --stat для быстрого обзора.
Три области и их сравнения
Помнишь три области Git: working tree, index (staging area), repository (HEAD коммит). git diff может сравнить любую пару:
Разберём каждый вариант.
git diff: working tree vs index
Без флагов — показывает изменения, которые ты сделал, но ещё не добавил в staging.
$ echo "new line" >> src/etl.py
$ git diff
diff --git a/src/etl.py b/src/etl.py
index a1b2c3d..f4e5d6c 100644
--- a/src/etl.py
+++ b/src/etl.py
@@ -10,3 +10,4 @@ def process():
return data
# end of file
+new line
Это то, что ты сделал в файле, но Git ещё этого не “видит” в смысле “готов к коммиту”.
После git add src/etl.py:
$ git diff
# (пусто)
Файл теперь в index. git diff без флагов больше не показывает его — потому что working tree и index теперь совпадают.
git diff --staged: index vs HEAD
Показывает изменения, готовые к коммиту. То есть в index, но ещё не в репозитории.
$ git add src/etl.py
$ git diff --staged
diff --git a/src/etl.py b/src/etl.py
index a1b2c3d..f4e5d6c 100644
--- a/src/etl.py
+++ b/src/etl.py
@@ -10,3 +10,4 @@ def process():
return data
# end of file
+new line
--staged и --cached — синонимы:
git diff --staged
git diff --cached
Используются они для “проверки перед коммитом”: “то, что я сейчас закоммичу, выглядит правильно?”.
Привычка: перед каждым git commit сделать git diff --staged. Это catches много багов: случайно добавил console.log(), оставил print() for debug, забыл удалить TODO, и так далее.
git diff HEAD: всё против HEAD
Показывает все изменения относительно последнего коммита — и staged, и unstaged.
$ git diff HEAD
То есть git diff HEAD = git diff + git diff --staged. Полный обзор “что я ещё не закоммитил”.
Сравнение коммитов: git diff <A> <B>
$ git diff a1b2c3d f4e5d6c
Показывает изменения от коммита A к коммиту B. Можно использовать любые refs: SHA, имена веток, теги, относительные.
git diff HEAD~3 HEAD # последние 3 коммита
git diff main feature/x # ветка vs ветка
git diff v1.0 v1.1 # между тегами
git diff @{yesterday} HEAD # что было вчера vs сейчас (если работал)
Один аргумент:
git diff main # diff текущей ветки относительно main
Это эквивалент git diff main HEAD (от main к HEAD).
Range syntax: .. vs ...
В git log тоже были эти операторы — в git diff они работают чуть иначе.
git diff A..B — то же, что git diff A B
git diff main..feature/x
git diff main feature/x # эквивалент
Показывает изменения от верхушки main до верхушки feature/x.
git diff A...B — diff от merge base до B
git diff main...feature/x
Это отличается от ... Тут Git находит merge base двух веток и показывает изменения только в feature/x относительно этой точки разделения.
Когда что использовать:
A..B— “что будет, если я попробую полностью заменить A на B”.A...B— “что я (в branch B) добавил после того, как разошёлся с A”. Это то, что показывает PR на GitHub.
В подавляющем большинстве случаев для code review используют A...B. Это симметричнее и понятнее.
Только имена файлов: --name-only и --name-status
Часто не нужен полный diff, а только список изменённых файлов.
$ git diff --name-only HEAD~3 HEAD
src/etl.py
tests/test_etl.py
docs/README.md
С статусом изменений (added, modified, deleted):
$ git diff --name-status HEAD~3 HEAD
M src/etl.py
A tests/test_etl.py
M docs/README.md
D src/old.py
Буквы: M = modified, A = added, D = deleted, R = renamed, C = copied.
Полезно для скриптов: “запусти линтер на всех изменённых файлах”:
git diff --name-only origin/main...HEAD | xargs flake8
Суммарная статистика: --stat
Аналог git log --stat, но для произвольного diff:
$ git diff --stat HEAD~3 HEAD
src/etl.py | 25 +++++++++++++++++++++++--
tests/test_etl.py | 18 ++++++++++++++++++
docs/README.md | 5 +++--
3 files changed, 44 insertions(+), 4 deletions(-)
Полезный обзор перед PR: “сколько строк я меняю?”.
--shortstat — только итог:
$ git diff --shortstat HEAD~3 HEAD
3 files changed, 44 insertions(+), 4 deletions(-)
--word-diff: построчно, но по словам
Для текстовых файлов (документация, markdown) построчный diff неудобен. --word-diff показывает изменения на уровне слов:
$ git diff --word-diff
diff --git a/README.md b/README.md
@@ -3,5 +3,5 @@
This is [-old-]{+new+} documentation.
Удобно для рецензирования прозы. Для кода — обычно нет, там важна структура построчно.
Игнорировать whitespace
Если коллега переформатировал файл, и теперь весь diff — это white-space changes:
git diff -w # игнорировать все whitespace
git diff -b # игнорировать изменения количества пробелов
git diff --ignore-blank-lines # игнорировать пустые строки
-w особенно полезен при rebase больших файлов после форматирования.
Output options
Цвет
По дефолту в терминале — цветно. Для копирования / скриптов:
git diff --no-color # ASCII без цвета
git diff --color=always # всегда цвет (даже в pipe)
Context lines
По дефолту diff показывает 3 строки контекста до и после изменения:
git diff -U10 # 10 строк контекста
git diff -U0 # только сами изменения, без контекста
git diff --unified=10 # длинная форма
Сравнения с подмодулями
git diff --submodule=log # показать diff из submodules как логи коммитов
git diff --submodule=diff # показать diff внутри submodules
Полезно когда работаешь с проектами, где есть git submodules — иначе diff показывает только “submodule updated from X to Y”.
Сравнение в IDE: git difftool
Как мы видели в Module 06, git difftool запускает diff в GUI вместо терминала:
git difftool # working tree vs index
git difftool --staged # index vs HEAD
git difftool HEAD~3 HEAD # между коммитами
Полезно для крупных diff’ов или когда нужна подсветка синтаксиса.
Реальные сценарии
Перед коммитом
git diff --staged # точно ли это то, что я хочу закоммитить?
Перед PR
# Полный обзор того, что я добавил
git diff origin/main...HEAD
# Только список файлов
git diff --name-only origin/main...HEAD
# Статистика
git diff --stat origin/main...HEAD
При ревью коммита коллеги
# Показать конкретный коммит как diff
git show <sha>
# Эквивалент
git diff <sha>~1 <sha>
Когда что-то сломалось
# Что изменилось с момента последнего рабочего состояния
git diff <last-known-good-sha> HEAD
# Только определённый файл
git diff <sha> HEAD -- src/etl.py
Локальные ещё-не-pushed изменения
git diff origin/feature/x..HEAD
Цветовая кастомизация
В ~/.gitconfig:
[color "diff"]
meta = yellow
frag = magenta bold
func = white bold
old = red bold
new = green bold
commit = yellow bold
whitespace = red reverse
Дефолтные цвета обычно ок, но это для тех, кто хочет.
Diff highlight tools
Для лучшего восприятия diff’ов есть утилиты:
diff-so-fancy— pretty pipe дляgit diff.delta(написан на Rust) — современный pager с подсветкой синтаксиса.
# Установка delta
brew install git-delta
# или
cargo install git-delta
# Настройка
git config --global core.pager delta
git config --global interactive.diffFilter "delta --color-only"
git config --global delta.navigate true
git config --global delta.side-by-side true
delta особенно полезен для больших diff’ов: подсвечивает изменения внутри строк, поддерживает side-by-side view.
Попробуй сам
mkdir diff-demo && cd diff-demo
git init
# Базовое состояние
cat > app.py <<EOF
def hello():
print("Hello, world!")
def goodbye():
print("Bye")
EOF
git add . && git commit -m "init"
# Сделай изменения
sed -i.bak 's/Hello, world!/Hello, Universe!/' app.py
echo "" >> app.py
echo "def main():" >> app.py
echo " hello()" >> app.py
rm app.py.bak
# Сравни working tree vs HEAD
git diff
# Видишь diff
# Stage частично
git add -p app.py
# Интерактивно: примешь одни блоки, не другие
# Сравни три варианта
git diff # unstaged changes
git diff --staged # staged changes
git diff HEAD # все changes
# Сделай коммит
git commit -m "update greeting and add main"
# Diff с предыдущим коммитом
git diff HEAD~1 HEAD
# Полный diff коммита
# Сделай ветку с другими изменениями
git switch -c feature
sed -i.bak 's/main()/run_main()/' app.py && rm app.py.bak
git commit -am "rename main to run_main"
git switch -
sed -i.bak 's/Bye/Goodbye, friend!/' app.py && rm app.py.bak
git commit -am "longer goodbye"
# Сравни ветки
git diff main feature # diff между верхушками
git diff main...feature # только feature changes относительно merge base
# Только имена файлов
git diff --name-only main...feature
Что смотреть при code review dbt-моделей