Перейти к содержимому
RootPoligon

Осмысленный коммит

Чтение и практика ~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.

Сохраните правки двумя коммитами:

  1. «Добавить расписание занятий на главную» — только index.html.
  2. «Уточнить заголовок сайта» — только 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.

Проверка понимания

  1. git status --short показывает MM site.conf. Вы выполняете git commit -m "…". Какая версия файла сохранится в коммите?
  2. Почему сообщение «правки» хуже, чем «Перенести сайт на порт 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.

Проверка понимания

  1. Та, что лежит в индексе, — версия на момент последнего git add. Вторая правка (правая M) останется в рабочей папке и после коммита будет видна как M site.conf.
  2. «Правки» не отвечает ни на «что», ни на «зачем»: чтобы понять коммит, придётся читать diff, а причину (порт занят greeter) не восстановить вовсе. Конкретное сообщение помогает найти коммит в истории и решить, можно ли его отменить (главы 3 и 7).

Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.