Осмысленный коммит
Чтение и практика ~20 мин
Ситуация
Утром вы поменяли порт сайта — так попросил администратор. Днём добавили расписание на главную. Вечером поправили заголовок. Всё лежит в рабочей папке вперемешку. Если сохранить одним коммитом «правки», через месяц никто не поймёт, почему поменялся порт, а откатить только его без расписания будет трудно.
Главная мысль
Между папкой и историей есть промежуточное место — индекс. git add кладёт в индекс текущую версию файла,
git commit сохраняет только индекс. Поэтому коммит можно собрать по частям и проверить до сохранения.
Три места и команды, которые их сравнивают:
рабочая папка ──git add──▶ индекс ──git commit──▶ последний коммит
└──── git diff ────┘ └── git diff --staged ──┘
git diff— чем папка отличается от индекса: что вы изменили, но ещё не отметили.git diff --staged— чем индекс отличается от последнего коммита: что войдёт в следующий коммит.
Новые слова
- Индекс (staging area, «подготовленное») — набор версий файлов, который станет следующим коммитом.
- diff — список различий: строки с
-убраны, с+добавлены, без знака — контекст вокруг. - HEAD — последний коммит текущей ветки, «где вы сейчас».
Разбор: две задачи — два коммита
Папка ~/git-course/site. Исходное состояние — конец главы 1: два коммита, working tree clean.
Если вы начинаете с этой главы, создайте его скриптом из setup.md.
Две правки
Меняем порт и текст главной. sed -i правит файл на месте (знакомо по курсу Linux):
sed -i 's/^PORT=8081/PORT=8082/' site.conf
sed -i 's|<p>Здесь будет расписание занятий.</p>|<p>Занятия: вторник и четверг, 19:00.</p>|' index.html
git status
On branch main
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: index.html
modified: site.conf
no changes added to commit (use "git add" and/or "git commit -a")
Обе правки — в рабочей папке, в индексе пусто. git diff покажет обе (фрагмент):
-<p>Здесь будет расписание занятий.</p>
+<p>Занятия: вторник и четверг, 19:00.</p>
…
-PORT=8081
+PORT=8082
В индекс — только порт
git add site.conf
git status --short
M index.html
M site.conf
Две колонки кода: левая — индекс, правая — рабочая папка. M у site.conf — изменение в индексе,
M у index.html — изменение только в папке.
Предскажите, что теперь покажут git diff и git diff --staged.
git diff
diff --git a/index.html b/index.html
index 77467ab..5d49af6 100644
--- a/index.html
+++ b/index.html
@@ -3,6 +3,6 @@
<head><meta charset="utf-8"><title>Учебный сайт</title></head>
<body>
<h1>Учебный сайт</h1>
-<p>Здесь будет расписание занятий.</p>
+<p>Занятия: вторник и четверг, 19:00.</p>
</body>
</html>
git diff --staged
diff --git a/site.conf b/site.conf
index 23fa383..d881499 100644
--- a/site.conf
+++ b/site.conf
@@ -1,4 +1,4 @@
# Настройки учебного сайта
-PORT=8081
+PORT=8082
NAME=site
TITLE=Учебный сайт
Правка порта «переехала» из git diff в git diff --staged.
Один файл — две версии
Теперь, пока порт лежит в индексе, поменяем заголовок в том же site.conf:
sed -i 's/^TITLE=.*/TITLE=Учебный сайт курса Git/' site.conf
git status --short
M index.html
MM site.conf
MM — у site.conf есть изменение в индексе и другое изменение в папке. git add положил в индекс версию
файла на момент команды; новая правка туда не попала. Сравните:
git diff site.conf
@@ -1,4 +1,4 @@
# Настройки учебного сайта
PORT=8082
NAME=site
-TITLE=Учебный сайт
+TITLE=Учебный сайт курса Git
git diff --staged site.conf
@@ -1,4 +1,4 @@
# Настройки учебного сайта
-PORT=8081
+PORT=8082
NAME=site
TITLE=Учебный сайт
Это и есть разница между staged и unstaged: в коммит пойдёт только порт.
Сообщение коммита
Хорошее сообщение отвечает «что сделано и зачем». Первая строка — коротко, повелительно, до 50–70 символов:
«Перенести сайт на порт 8082», а не «правки» или «fix». Причина — во втором абзаце. Каждый -m — отдельный абзац:
git commit -m "Перенести сайт на порт 8082" -m "Порт 8081 на учебном сервере занят сервисом greeter."
[main e722969] Перенести сайт на порт 8082
1 file changed, 1 insertion(+), 1 deletion(-)
Проверяем результат
git show --stat HEAD
commit e722969d7ed196634b40d4c6c0d8d737c2387c18
Author: Анна (учебный) <anna@example.invalid>
Date: Fri Oct 16 14:28:31 2026 +0300
Перенести сайт на порт 8082
Порт 8081 на учебном сервере занят сервисом greeter.
site.conf | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
В коммите один файл и одна изменённая строка. А в папке остались две другие правки:
git status --short
M index.html
M site.conf
Файл site.conf в папке по-прежнему содержит новый заголовок — коммит ничего не стёр.
Практика: ещё два коммита
Исходное состояние — сразу после разбора: в папке изменены index.html (расписание) и site.conf (заголовок),
индекс пуст. Если начинаете здесь — скрипт setup.md.
Сохраните правки двумя коммитами:
- «Добавить расписание занятий на главную» — только
index.html. - «Уточнить заголовок сайта» — только
site.conf.
Перед каждым git commit проверьте командой, что именно войдёт в коммит.
Признаки успеха:
git log --onelineпоказывает пять коммитов;git show --stat HEAD~1— толькоindex.html,git show --stat HEAD— толькоsite.conf;git status—working tree clean.
Подсказка 1
Индекс собирают по одному файлу. Какой командой отметить только index.html?
Подсказка 2
Что войдёт в коммит, показывает сравнение индекса с последним коммитом. Короткий вариант — с ключом --stat.
Подсказка 3
git add index.html → git diff --staged --stat → git commit -m "…"; затем то же для site.conf.
HEAD~1 — коммит перед HEAD.
Решение — в answers/02-meaningful-commit.md.
Если результат другой
- Оба файла попали в один коммит. Вы выполнили
git add .илиgit commit -a: первое кладёт в индекс всё изменённое в папке, второе — все отслеживаемые изменённые файлы. Для этой задачи оба способа слишком широкие. Как поправить последний коммит, не теряя работы, — в главе 7; пока можно оставить так и сделать вывод. - Отметили не тот файл. Уберите его из индекса, не трогая правку в папке:
git restore --staged site.conf. Правка останется в рабочей папке — проверьтеgit diff. git diffничего не показывает, а изменения точно были. Они уже в индексе — смотритеgit diff --staged.
Проверка понимания
git status --shortпоказываетMM site.conf. Вы выполняетеgit commit -m "…". Какая версия файла сохранится в коммите?- Почему сообщение «правки» хуже, чем «Перенести сайт на порт 8082»?
Ответы — в answers/02-meaningful-commit.md.
Дальше: 3. Чтение истории.
Проверка автора и источники
- Воспроизведено: разбор и практика целиком в отдельном временном репозитории (Git 2.43.0, Ubuntu 24.04, bash 5.2).
Хеши и даты — из этого запуска. Полные выводы
git diffв разборе показаны целиком там, где важна шапка, и с пропуском (…) там, где важны только строки изменений. git add -p(выбор отдельных фрагментов файла) не разбирается: он интерактивный; для учебных задач хватает выбора по файлам.- Источники: git-add, git-diff
(
--staged— синоним--cached), git-status (колонки X и Y краткого формата), git-commit (несколько-m— несколько абзацев), Pro Git, 2.2 «Запись изменений в репозиторий».
Подготовка учебного репозитория для этой главы
Перед главой 2
Конец главы 1: два коммита, working tree clean. Папки ~/git-course/site быть не должно.
bash <<'SETUP'
rp_before2() {
# --- Проверки: ничего не меняют ---
fail() { echo "Отказ: $1"; echo "Ничего не изменено."; return 1; }
course_repo() {
[ -d "$1/.git" ] || { fail "$1 — нет такого репозитория Git"; return 1; }
git -C "$1" log --max-parents=0 --format=%s 2>/dev/null | grep -qx 'Первая версия учебного сайта' \
|| { fail "$1 — не учебный репозиторий курса"; return 1; }
}
quiet() { # папка, ожидаемая ветка, разрешённые неотслеживаемые файлы через пробел
local x f
[ "$(git -C "$1" symbolic-ref --short -q HEAD)" = "$2" ] || { fail "в $1 текущая ветка не $2"; return 1; }
for x in MERGE_HEAD REVERT_HEAD CHERRY_PICK_HEAD REBASE_HEAD; do
git -C "$1" rev-parse -q --verify "$x" >/dev/null && { fail "в $1 не завершена операция Git ($x) — завершите или отмените её"; return 1; }
done
for x in rebase-merge rebase-apply; do
[ -e "$(git -C "$1" rev-parse --git-path "$x")" ] && { fail "в $1 не завершён rebase"; return 1; }
done
[ -z "$(git -C "$1" status --porcelain --untracked-files=no)" ] || { fail "в $1 есть незакоммиченные изменения"; return 1; }
for f in $(git -C "$1" ls-files --others --exclude-standard); do
case " $3 " in *" $f "*) ;; *) fail "в $1 лишний неотслеживаемый файл $f"; return 1 ;; esac
done
}
last_is() { [ "$(git -C "$1" log -1 --format=%s)" = "$2" ] || { fail "в $1 последний коммит не «$2»"; return 1; }; }
no_branch() { ! git -C "$1" show-ref --verify -q "refs/heads/$2" || { fail "в $1 уже есть ветка $2"; return 1; }; }
absent() { [ ! -e "$1" ] || { fail "$1 уже существует — переименуйте её сами, если хотите начать заново"; return 1; }; }
command -v git >/dev/null || { fail "не найден git"; return 1; }
absent ~/git-course/site || return 1
# --- Подготовка: после первой записи при ошибке скрипт останавливается ---
(
set -e
mkdir -p ~/git-course/site
cd ~/git-course/site
git init -q -b main
git config user.name "Анна (учебный)"
git config user.email "anna@example.invalid"
cat > index.html <<'EOF'
<!doctype html>
<html lang="ru">
<head><meta charset="utf-8"><title>Учебный сайт</title></head>
<body>
<h1>Учебный сайт</h1>
<p>Здесь будет расписание занятий.</p>
</body>
</html>
EOF
cat > site.conf <<'EOF'
# Настройки учебного сайта
PORT=8081
NAME=site
TITLE=Учебный сайт
EOF
cat > README.md <<'EOF'
# Учебный сайт
Запуск для проверки: python3 -m http.server 8081
EOF
git add index.html site.conf README.md
git commit -q -m "Первая версия учебного сайта"
cat > about.html <<'EOF'
<!doctype html>
<html lang="ru">
<head><meta charset="utf-8"><title>О нас</title></head>
<body>
<h1>О нас</h1>
<p>Учебный сайт курса «Git: основы».</p>
</body>
</html>
EOF
git add about.html
git commit -q -m "Добавить страницу «О нас»"
)
if [ $? -ne 0 ]; then
echo "Ошибка после начала подготовки: часть шагов могла выполниться."
echo "Посмотрите git status и git log --oneline --all -5. Начать заново: переименуйте ~/git-course"
echo "(mv ~/git-course ~/git-course-old) и выполните блоки этого файла с начала."
return 1
fi
git -C ~/git-course/site log --oneline
echo "Готово: конец главы 1, два коммита."
}
rp_before2 </dev/null
SETUP
Практика главы 2
Конец разбора главы 2: три коммита, в папке изменены index.html и site.conf, индекс пуст.
Выполняется после блока «Перед главой 2».
bash <<'SETUP'
rp_practice2() {
# --- Проверки: ничего не меняют ---
fail() { echo "Отказ: $1"; echo "Ничего не изменено."; return 1; }
course_repo() {
[ -d "$1/.git" ] || { fail "$1 — нет такого репозитория Git"; return 1; }
git -C "$1" log --max-parents=0 --format=%s 2>/dev/null | grep -qx 'Первая версия учебного сайта' \
|| { fail "$1 — не учебный репозиторий курса"; return 1; }
}
quiet() { # папка, ожидаемая ветка, разрешённые неотслеживаемые файлы через пробел
local x f
[ "$(git -C "$1" symbolic-ref --short -q HEAD)" = "$2" ] || { fail "в $1 текущая ветка не $2"; return 1; }
for x in MERGE_HEAD REVERT_HEAD CHERRY_PICK_HEAD REBASE_HEAD; do
git -C "$1" rev-parse -q --verify "$x" >/dev/null && { fail "в $1 не завершена операция Git ($x) — завершите или отмените её"; return 1; }
done
for x in rebase-merge rebase-apply; do
[ -e "$(git -C "$1" rev-parse --git-path "$x")" ] && { fail "в $1 не завершён rebase"; return 1; }
done
[ -z "$(git -C "$1" status --porcelain --untracked-files=no)" ] || { fail "в $1 есть незакоммиченные изменения"; return 1; }
for f in $(git -C "$1" ls-files --others --exclude-standard); do
case " $3 " in *" $f "*) ;; *) fail "в $1 лишний неотслеживаемый файл $f"; return 1 ;; esac
done
}
last_is() { [ "$(git -C "$1" log -1 --format=%s)" = "$2" ] || { fail "в $1 последний коммит не «$2»"; return 1; }; }
no_branch() { ! git -C "$1" show-ref --verify -q "refs/heads/$2" || { fail "в $1 уже есть ветка $2"; return 1; }; }
absent() { [ ! -e "$1" ] || { fail "$1 уже существует — переименуйте её сами, если хотите начать заново"; return 1; }; }
command -v git >/dev/null || { fail "не найден git"; return 1; }
course_repo ~/git-course/site || return 1
quiet ~/git-course/site main || return 1
last_is ~/git-course/site "Добавить страницу «О нас»" || return 1
# --- Подготовка: после первой записи при ошибке скрипт останавливается ---
(
set -e
cd ~/git-course/site
sed -i 's/^PORT=8081/PORT=8082/' site.conf
git commit -q -am "Перенести сайт на порт 8082" -m "Порт 8081 на учебном сервере занят сервисом greeter."
sed -i 's|<p>Здесь будет расписание занятий.</p>|<p>Занятия: вторник и четверг, 19:00.</p>|' index.html
sed -i 's/^TITLE=.*/TITLE=Учебный сайт курса Git/' site.conf
git status --short
)
if [ $? -ne 0 ]; then
echo "Ошибка после начала подготовки: часть шагов могла выполниться."
echo "Посмотрите git status и git log --oneline --all -5. Начать заново: переименуйте ~/git-course"
echo "(mv ~/git-course ~/git-course-old) и выполните блоки этого файла с начала."
return 1
fi
echo "Готово: изменены index.html и site.conf, индекс пуст."
}
rp_practice2 </dev/null
SETUP
Ожидаемый результат — Готово, затем git status --short покажет M index.html и M site.conf.
Разбор и ответы — откройте после своей попытки
Практика
Папка: ~/git-course/site.
git add index.html
git diff --staged --stat
index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
В индексе только index.html — можно сохранять.
git commit -m "Добавить расписание занятий на главную"
git add site.conf
git diff --staged
diff --git a/site.conf b/site.conf
index d881499..537eb36 100644
--- a/site.conf
+++ b/site.conf
@@ -1,4 +1,4 @@
# Настройки учебного сайта
PORT=8082
NAME=site
-TITLE=Учебный сайт
+TITLE=Учебный сайт курса Git
Порт уже в истории, поэтому в diff только заголовок.
git commit -m "Уточнить заголовок сайта"
git log --oneline
ba1c983 Уточнить заголовок сайта
a4045c5 Добавить расписание занятий на главную
e722969 Перенести сайт на порт 8082
f6d4ea3 Добавить страницу «О нас»
be0a128 Первая версия учебного сайта
Хеши у вас другие. git status → working tree clean.
Воспроизведено: Git 2.43.0, Ubuntu 24.04.
Проверка понимания
- Та, что лежит в индексе, — версия на момент последнего
git add. Вторая правка (праваяM) останется в рабочей папке и после коммита будет видна какM site.conf. - «Правки» не отвечает ни на «что», ни на «зачем»: чтобы понять коммит, придётся читать diff, а причину (порт занят greeter) не восстановить вовсе. Конкретное сообщение помогает найти коммит в истории и решить, можно ли его отменить (главы 3 и 7).
Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.