Знакомая картина. Ты три часа правил код, бегал между файлами, что-то чинил, что-то рефакторил, заодно поправил опечатку в README и закомментировал отладочный print. Пора коммитить. И вот тут новичок делает
Код: Выделить всё
git add . && git commit -m "fixes"Код: Выделить всё
fixesКод: Выделить всё
updateКод: Выделить всё
wipПрофессионал так не делает. Перед коммитом он точно знает, что изменилось, где это изменилось (в рабочем каталоге или уже в индексе) и кладёт в коммит ровно одну осмысленную порцию. Инструменты для этого - git status, git diff во всех его ипостасях и интерактивная индексация git add -p. Это не "посмотреть глазками", это снайперский контроль над тем, что попадёт в историю. Разберём механику до винтика.

Три дерева: почему diff бывает разный
Чтобы понять, посмотреть изменения git правильно, надо держать в голове три области (мы их называем "три дерева"):
- Рабочий каталог (working tree) - твои файлы на диске, как есть прямо сейчас.
- Индекс (index, он же staging area) - снимок того, что попадёт в следующий коммит. Физически это файл .
Код: Выделить всё
.git/index - HEAD - последний коммит текущей ветки, отправная точка.
Код: Выделить всё
git addКод: Выделить всё
git commitКод: Выделить всё
git addЗапомни три рабочие формы:
- - рабочий каталог против индекса. То, что ты ещё не заиндексировал.
Код: Выделить всё
git diff - (синоним
Код: Выделить всё
git diff --staged) - индекс против HEAD. То, что уже заиндексировано и уйдёт в коммит.Код: Выделить всё
--cached - - рабочий каталог против HEAD. Все изменения скопом, и заиндексированные, и нет.
Код: Выделить всё
git diff HEAD
Код: Выделить всё
git diff --stagedКод: Выделить всё
git commitgit status: карта местности
Код: Выделить всё
git diffКод: Выделить всё
git statusКод: Выделить всё
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: src/auth.py
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: src/auth.py
modified: README.md
Untracked files:
(use "git add <file>..." to include in what will be committed)
notes.txt
Код: Выделить всё
src/auth.pyКод: Выделить всё
README.mdКод: Выделить всё
notes.txtСовет:
Код: Выделить всё
git status -sКод: Выделить всё
$ git status -s
MM src/auth.py
M README.md
?? notes.txt
Код: Выделить всё
MMКод: Выделить всё
_MКод: Выделить всё
??Как читать унифицированный diff построчно
Вот сердце урока. Разберём реальный вывод
Код: Выделить всё
git diffКод: Выделить всё
$ git diff README.md
diff --git a/README.md b/README.md
index 3b18e51..d4a1c5f 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,8 @@ Installation
Clone the repo and run:
- pip install requirements.txt
+ pip install -r requirements.txt
+ python setup.py develop
Run tests with pytest.
- - заголовок.
Код: Выделить всё
diff --git a/README.md b/README.md- "до" (индекс),Код: Выделить всё
a/- "после" (рабочий каталог). Это служебные префиксы, а не реальные папки.Код: Выделить всё
b/ - - сокращённые хеши blob-объектов "до" и "после" плюс режим файла.
Код: Выделить всё
index 3b18e51..d4a1c5f 100644- обычный файл,Код: Выделить всё
100644- исполняемый. Если режим сменился, тут будет видно.Код: Выделить всё
100755 - и
Код: Выделить всё
--- a/README.md- какой файл считается старым (минус) и новым (плюс).Код: Выделить всё
+++ b/README.md - - это hunk header, граница куска изменений. Читается так: со старой стороны начиная со строки 10 показано 7 строк, с новой стороны начиная со строки 10 показано 8 строк. Строк стало на одну больше - мы добавили строку. Текст после второго
Код: Выделить всё
@@ -10,7 +10,8 @@(тутКод: Выделить всё
@@) - это "контекстный хвост", функция или заголовок, в котором живёт правка. Git угадывает его, чтобы ты понимал, куда смотреть.Код: Выделить всё
Installation - Строки с пробелом в начале - контекст (не менялись, даны для ориентира). - удалено.
Код: Выделить всё
-- добавлено. Замена строки в diff выглядит как удаление старой плюс добавление новой - отдельной операции "изменить строку" в построчном diff нет.Код: Выделить всё
+
Код: Выделить всё
git diff -U10Код: Выделить всё
-U0Код: Выделить всё
$ git diff --word-diff README.md
@@ -10,7 +10,7 @@ Installation
pip install [-requirements.txt-]{+-r requirements.txt+}
Код: Выделить всё
[-...-]Код: Выделить всё
{+...+}Код: Выделить всё
--word-diff=colorgit add -p: индексация изменений git по кускам
А вот и профи-навык, ради которого затевался урок. Индексация изменений git по частям - это
Код: Выделить всё
git add -pКод: Выделить всё
--patchКод: Выделить всё
$ git add -p src/auth.py
@@ -42,6 +42,7 @@ def login(user, password):
token = make_token(user)
+ log.debug("token issued for %s", user)
return token
@@ -88,7 +89,7 @@ def logout(session):
- session.clear()
+ session.invalidate()
return True
(1/2) Stage this hunk [y,n,q,a,d,s,e,?]?
- - да, заиндексировать этот hunk.
Код: Выделить всё
y - - нет, пропустить.
Код: Выделить всё
n - - выйти, ничего больше не спрашивать.
Код: Выделить всё
q - - этот и все оставшиеся hunk-и в этом файле.
Код: Выделить всё
a - - этот и все оставшиеся - пропустить.
Код: Выделить всё
d - - split, разбить hunk на более мелкие, если между правками есть строки контекста.
Код: Выделить всё
s - - edit, ручная правка hunk-а в редакторе: можно вручную решить, какие
Код: Выделить всё
e/Код: Выделить всё
+строки войдут.Код: Выделить всё
- - - справка прямо на месте.
Код: Выделить всё
?
Код: Выделить всё
nКод: Выделить всё
yКод: Выделить всё
git add -pПро
Код: Выделить всё
sКод: Выделить всё
eКод: Выделить всё
+Код: Выделить всё
-Код: Выделить всё
@@Старший брат -
Код: Выделить всё
git add -iКод: Выделить всё
git add -pКод: Выделить всё
-iКод: Выделить всё
-pКод: Выделить всё
addКод: Выделить всё
git restore -pКод: Выделить всё
git stash -pКод: Выделить всё
git checkout -pКод: Выделить всё
git reset -pgit restore: снять из индекса и откатить файл
Заиндексировал лишнее? git restore - современная команда (стабильна с Git 2.51, больше не экспериментальная) с чётким разделением ролей, в отличие от перегруженного
Код: Выделить всё
git checkout- - снять из индекса. Возвращает файл в индексе к состоянию HEAD, рабочий каталог не трогает. Твои правки на диске целы, просто разиндексированы. Это безопасная операция.
Код: Выделить всё
git restore --staged auth.py - - откатить рабочий каталог к состоянию индекса. Внимание: правки в рабочем каталоге теряются безвозвратно, отмены через reflog тут нет. Git даже подсказывает это в
Код: Выделить всё
git restore auth.py: "discard changes in working directory".Код: Выделить всё
git status - - вытащить версию файла из конкретного коммита.
Код: Выделить всё
git restore --source=HEAD~2 auth.py
Код: Выделить всё
git statusКод: Выделить всё
git reset HEAD auth.pyКод: Выделить всё
git checkout -- auth.pyКод: Выделить всё
restorediff между файлами, ветками и коммитами
Код: Выделить всё
git diffКод: Выделить всё
$ git diff src/auth.py # один файл, каталог vs индекс
$ git diff main feature # разница между двумя ветками
$ git diff main..feature # то же самое (двухточечная форма)
$ git diff main...feature # с момента, как ветки разошлись
$ git diff HEAD~3 HEAD # последние три коммита целиком
$ git diff abc123 def456 -- src/ # между коммитами, только папка src
$ git diff --stat main feature # сводка: сколько строк в каких файлах
Код: Выделить всё
main..featureКод: Выделить всё
main...featureКод: Выделить всё
git diff main...featureА
Код: Выделить всё
--statКод: Выделить всё
$ git diff --stat HEAD~3 HEAD
src/auth.py | 14 ++++++++------
README.md | 3 ++-
tests/test.py | 22 ++++++++++++++++++++++
3 files changed, 33 insertions(+), 6 deletions(-)
- "git diff пустой, а я менял файл". Ты уже сделал . Смотри
Код: Выделить всё
git addилиКод: Выделить всё
git diff --staged. Классика номер один.Код: Выделить всё
git diff HEAD - Путаница --staged и без него. - то, что не в коммите.
Код: Выделить всё
git diff- то, что уйдёт в коммит. Перепутал - закоммитил не то.Код: Выделить всё
git diff --staged - split не предлагается. доступен, только если внутри hunk-а есть неизменённые строки-контекст, разделяющие правки. Две соседние правки без контекста между ними Git разбить не сможет - тут спасёт только
Код: Выделить всё
s(ручная правка).Код: Выделить всё
e - git restore без --staged стёр работу. (без флага) выбрасывает правки рабочего каталога без права на возврат. Хотел разиндексировать - всегда добавляй
Код: Выделить всё
git restore file. Перепутал флаг - потерял час работы.Код: Выделить всё
--staged - CRLF и пробелы засоряют diff. Целые файлы краснеют из-за смены переводов строк. Лови пробельный шум флагом , а игнорируй пробелы при чтении через
Код: Выделить всё
git diff --check(но не коммить сКод: Выделить всё
git diff -w, это только для просмотра).Код: Выделить всё
-w - Бинарники. Git честно пишет и не лезет показывать байты. Это норма, а не ошибка.
Код: Выделить всё
Binary files a/logo.png and b/logo.png differ
Повтори руками по шагам, это десять минут и навык в пальцах:
Код: Выделить всё
mkdir diff-lab && cd diff-lab
git init -b main
printf 'line one\nline two\nline three\n' > app.txt
git add app.txt && git commit -m "init"
# Намеренно мешаем две независимые правки в один файл
printf 'line one CHANGED\nline two\nline three\nline four added\n' > app.txt
git status # видим modified, файл не в индексе
git diff # читаем hunk: где -, где +
git diff --word-diff # ту же правку первой строки видно по словам
git add -p app.txt # жмём s если предложит, индексируем порциями
git diff --staged # что уйдёт в коммит
git diff # что осталось в рабочем каталоге
git commit -m "rewrite line one"
git add -p app.txt # вторую правку - отдельным коммитом
git commit -m "add line four"
git log --oneline # два осмысленных коммита вместо одного "fixes"
# Бонус: разиндексировать и откатить
printf 'oops\n' >> app.txt
git add app.txt
git restore --staged app.txt # сняли из индекса, правка на диске цела
git diff # видим oops в рабочем каталоге
git restore app.txt # откатили - oops исчез
- Чем отличается от
Код: Выделить всё
git diffи отКод: Выделить всё
git diff --staged? Какие два дерева сравнивает каждая?Код: Выделить всё
git diff HEAD - В hunk header - что значат эти четыре числа?
Код: Выделить всё
@@ -20,5 +20,7 @@ - В ты нажал
Код: Выделить всё
git add -p, а Git не разбил hunk. Почему и что делать?Код: Выделить всё
s - Чем отличается от
Код: Выделить всё
git restore --staged fileи какое из них может стоить тебе работы?Код: Выделить всё
git restore file
Три дерева - рабочий каталог, индекс, HEAD - и три формы
Код: Выделить всё
git diffКод: Выделить всё
git statusКод: Выделить всё
git diffКод: Выделить всё
--word-diffКод: Выделить всё
--statКод: Выделить всё
main...featureКод: Выделить всё
git add -pКод: Выделить всё
git restoreКод: Выделить всё
--staged