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

Чтение истории

Чтение и практика ~18 мин

Ситуация

Пока вас не было, в репозиторий сайта коммитил коллега Борис. Утром администратор пишет: «Сервис сайта не запускается, ругается на PORT». Вчера всё работало. Что изменилось — и в каком коммите?

Главная мысль

История — это не только список сообщений. Каждый коммит хранит, что изменилось, и Git умеет искать: по файлу, по тексту изменения, по автору. Сначала сужаем поиск до коммита, потом читаем его целиком.

Новые слова

  • Хеш коммита — его имя; в командах хватает первых 7 символов (c633bc3).
  • HEAD~1 — коммит перед HEAD, HEAD~2 — ещё раньше.

Разбор: кто поменял порт

Папка ~/git-course/site. Исходное состояние — история из восьми коммитов, последние три сделаны после вашей главы 2. Коммиты Бориса добавляет скрипт из setup.md — запустите его, даже если прошли главу 2. Хеши и время ниже — пример, у вас будут другие.

Симптом и общий обзор

grep PORT site.conf
PORT=80x2

Порт не число — сервис прав. Посмотрим историю кратко:

git log --oneline
3b9cf8a Добавить контакты на страницу «О нас»
c633bc3 Обновить описание запуска в README
b08e705 Добавить ссылку на страницу «О нас»
e8d3fd7 Уточнить заголовок сайта
64ee811 Добавить расписание занятий на главную
4f67f87 Перенести сайт на порт 8082
4c8af3a Добавить страницу «О нас»
31d2ce3 Первая версия учебного сайта

Ни одно сообщение не говорит о порте после 4f67f87. Сообщения не помогут — ищем по содержимому.

Сужаем: коммиты, менявшие файл

git log --oneline -- site.conf
c633bc3 Обновить описание запуска в README
e8d3fd7 Уточнить заголовок сайта
4f67f87 Перенести сайт на порт 8082
31d2ce3 Первая версия учебного сайта

-- отделяет путь к файлу от остальных аргументов: «только коммиты, затронувшие site.conf». Самый свежий — с сообщением про README. Подозрительно.

Ещё точнее — найти коммит, где появился или исчез сам текст 80x2:

git log -S '80x2' --oneline
c633bc3 Обновить описание запуска в README

Читаем коммит

В командах ниже подставьте хеш из своего вывода git log -S — у вас он другой. Здесь это c633bc3:

git show --stat c633bc3
commit c633bc32b3d43a4d480f9eab1cd38fc067910020
Author: Борис (учебный) <boris@example.invalid>
Date:   Fri Oct 16 14:31:12 2026 +0300

    Обновить описание запуска в README

 README.md | 6 +++++-
 site.conf | 2 +-
 2 files changed, 6 insertions(+), 2 deletions(-)

Коммит про README, а изменил два файла. Посмотрим только site.conf:

git show c633bc3 -- site.conf
…
--- a/site.conf
+++ b/site.conf
@@ -1,4 +1,4 @@
 # Настройки учебного сайта
-PORT=8082
+PORT=80x2
 NAME=site
 TITLE=Учебный сайт курса Git

Диагноз: опечатка попала в коммит вместе с правкой README — та самая проблема перемешанных правок из главы 2.

Исправляем одну строку

Отменять коммит целиком нельзя: правка README в нём нужная. Исправляем только порт новым коммитом и пишем в сообщении, откуда взялась ошибка:

sed -i 's/^PORT=80x2/PORT=8082/' site.conf
git diff
-PORT=80x2
+PORT=8082
git add site.conf
git commit -m "Вернуть порт 8082" -m "Опечатка 80x2 попала в коммит «Обновить описание запуска в README»."
git log --oneline -3
cf37457 Вернуть порт 8082
3b9cf8a Добавить контакты на страницу «О нас»
c633bc3 Обновить описание запуска в README

История не переписана: и ошибка, и исправление видны. Как отменить коммит целиком, когда это уместно, — в главе 7.

Ещё способы смотреть

  • git log --stat --oneline -3 — последние три коммита со списком файлов;
  • git log -p -1 -- about.html — последний коммит, менявший файл, вместе с diff;
  • git log --format='%h %an %ad %s' --date=short -5 — хеш, автор, дата и сообщение в одну строку.

Длинный вывод git log открывается в программе просмотра: листайте пробелом, выход — q.

Практика: два вопроса к истории

Исходное состояние — конец разбора (коммит «Вернуть порт 8082» сделан). Ответьте командами Git, не открывая файлы редактором:

  1. В каком коммите заголовок сайта стал «Учебный сайт курса Git»? Хеш, автор, какие файлы он менял.
  2. Кто добавил на главную ссылку на страницу «О нас» и с каким сообщением?

Признак успеха: для каждого ответа — команда, которая его показывает, и строка вывода, на которую вы опираетесь.

Подсказка 1

Оба вопроса — «когда в файле появился такой-то текст». Есть поиск по тексту изменения.

Подсказка 2

git log -S 'текст' --oneline находит коммит; git show --stat <хеш> показывает автора и файлы. Для второго вопроса текст ссылки — about.html, файл — index.html.

Подсказка 3

Если -S 'about.html' находит слишком много, добавьте путь: -- index.html. Автора в одну строку покажет --format='%h %an %s'.

Решение — в answers/03-reading-history.md.

Если результат другой

  • git log -S ничего не нашёл. Текст ищется с учётом регистра и целиком. Проверьте написание: скопируйте строку из файла (grep TITLE site.conf).
  • Терминал «завис» после git log. Это программа просмотра. Нажмите q.
  • fatal: ambiguous argument. Git не понял, хеш это или файл. Поставьте -- перед путём к файлу.

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

  1. Почему поиск по сообщениям коммитов не нашёл поломку, а git log -S нашёл?
  2. Почему порт исправлен новым коммитом, а не отменой c633bc3 целиком?

Ответы — в answers/03-reading-history.md.

Дальше: 4. Ветки.

Проверка автора и источники

  • Воспроизведено: исходное состояние скриптом из setup.md, разбор и практика — Git 2.43.0, Ubuntu 24.04, bash 5.2. Хеши и время — из этого запуска.
  • Коммиты Бориса в учебном состоянии созданы с git -c user.name=… -c user.email=…: так скрипт подписывает коммит другим автором, не меняя настройки репозитория.
  • Источники: git-log (-S, --stat, -p, --format, пути после --), git-show, gitrevisions (HEAD~1).
Подготовка учебного репозитория для этой главы

Перед главой 3

Глава 3 начинается после главы 2 и трёх коммитов Бориса. Если папки ~/git-course/site нет, сначала создайте конец главы 2:

bash <<'SETUP'
rp_before3_fresh() {
  # --- Проверки: ничего не меняют ---
  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 "Добавить страницу «О нас»"
cd ~/git-course/site
sed -i 's/^PORT=8081/PORT=8082/' site.conf
git add site.conf
git commit -q -m "Перенести сайт на порт 8082" -m "Порт 8081 на учебном сервере занят сервисом greeter."
sed -i 's|<p>Здесь будет расписание занятий.</p>|<p>Занятия: вторник и четверг, 19:00.</p>|' index.html
git add index.html
git commit -q -m "Добавить расписание занятий на главную"
sed -i 's/^TITLE=.*/TITLE=Учебный сайт курса Git/' site.conf
git add site.conf
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 -1
  echo "Готово: конец главы 2."
}
rp_before3_fresh </dev/null
SETUP

Затем — коммиты Бориса (этот блок нужен всем, в том числе прошедшим главу 2):

bash <<'SETUP'
rp_before3() {
  # --- Проверки: ничего не меняют ---
  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
B=(-c user.name="Борис (учебный)" -c user.email="boris@example.invalid")
sed -i 's|<p>Занятия: вторник и четверг, 19:00.</p>|&\n<p><a href="about.html">О нас</a></p>|' index.html
git "${B[@]}" commit -q -am "Добавить ссылку на страницу «О нас»"
cat > README.md <<'EOF'
# Учебный сайт

Запуск для проверки из папки сайта:

    python3 -m http.server 8082

Порт указан в site.conf.
EOF
sed -i 's/^PORT=8082/PORT=80x2/' site.conf
git "${B[@]}" commit -q -am "Обновить описание запуска в README"
sed -i 's|<p>Учебный сайт курса «Git: основы».</p>|&\n<p>Вопросы: через форму на странице курса.</p>|' about.html
git commit -q -am "Добавить контакты на страницу «О нас»"
  )
  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 -3
  echo "Готово: добавлены три коммита Бориса."
}
rp_before3 </dev/null
SETUP
Разбор и ответы — откройте после своей попытки

Практика

1. Заголовок «Учебный сайт курса Git».

git log -S 'курса Git' --oneline
e8d3fd7 Уточнить заголовок сайта
git show --stat e8d3fd7

Автор — Анна (учебный), изменён один файл: site.conf | 2 +-.

Другой путь: git log -p -- site.conf и найти глазами строку +TITLE=Учебный сайт курса Git.

2. Ссылка на «О нас».

git log --format='%h %an %s' -S 'about.html' -- index.html
b08e705 Борис (учебный) Добавить ссылку на страницу «О нас»

Без -- index.html поиск тоже найдёт этот коммит, но может добавить и другие, где текст about.html встречался в других файлах.

Воспроизведено: Git 2.43.0, Ubuntu 24.04.

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

  1. Сообщение пишет человек, и оно описывает намерение («обновить README»), а не все изменения. Опечатка попала в коммит случайно, поэтому в сообщении её нет. git log -S ищет по самим изменениям — по тому, что действительно произошло с текстом.
  2. Отмена коммита целиком убрала бы и нужную правку README. Исправляем ровно то, что сломано, и оставляем в истории объяснение со ссылкой на коммит-источник.

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