Как делать11 октября 2026 г.7 мин чтения
Основы программированияШаг 10 из 15

Git с нуля: коммиты, ветки, слияние

Коротко

Git хранит историю проекта как цепочку коммитов — снимков всех файлов в момент сохранения. Рабочий цикл: изменить файлы, отметить нужные командой git add, зафиксировать git commit. Новую задачу делают в отдельной ветке и сливают обратно git merge; если обе ветки изменили одну строку, Git останавливается на конфликте, и вы сами выбираете итоговый текст.

Пошаговый урокУровень: НачальныйGit и GitHubПрактика: ~45–90 мин

После материала вы сможете

  • создать репозиторий, настроить .gitignore и сделать осмысленные коммиты через git add и git commit
  • создать ветку, переключиться на неё и слить обратно, отличая fast-forward от коммита слияния
  • разрешить конфликт слияния: найти маркеры, оставить итоговый текст и завершить merge
  • выбрать безопасную отмену: restore, revert или reset --soft

Проверено на

Git 2.55 (Git for Windows, Git Bash)Git 2.45 (Alpine Linux 3.20)

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01Зачем контроль версий
  2. 02Рабочая папка, индекс, коммит
  3. 03Первый репозиторий: init, status, add, commit, log
  4. 04.gitignore и что нельзя коммитить
  5. 05Ветки: создание, переключение и fast-forward
  6. 06Слияние с конфликтом: как его разрешить
  7. 07Отмена изменений: restore, revert, reset — без потерь
  8. 08Практика: репозиторий с намеренным конфликтом

Зачем контроль версий

Без системы контроля версий история проекта живёт в папках вида site-final, site-final-2, site-final-ПРАВКИ. Непонятно, что изменилось, кто это сделал и как вернуть вчерашнюю рабочую версию. Git решает эти задачи: каждое сохранение — коммит — получает автора, дату, описание и уникальный идентификатор, а к любому коммиту можно вернуться.

Pro Git, официальная книга проекта, подчёркивает, чем Git отличается от многих старых систем: он хранит не список правок, а снимки (snapshots) — при каждом коммите Git как бы фотографирует состояние всех файлов. Неизменённые файлы не копируются заново, Git ссылается на уже сохранённую версию. Почти все операции выполняются локально, поэтому работать с историей можно без интернета.

  • История: кто, когда и зачем изменил каждую строку.
  • Безопасные эксперименты: новая функция разрабатывается в ветке и не ломает рабочую версию.
  • Командная работа: изменения нескольких людей объединяются автоматически, а спорные места Git показывает явно.
  • Отмена: ошибочный коммит можно отменить, не теряя остальную работу.

Рабочая папка, индекс, коммит

Главное, что нужно запомнить, — у файла в Git три состояния. По Pro Git: изменённый (modified) — вы поменяли файл, но не зафиксировали; подготовленный (staged) — вы отметили текущую версию файла для следующего коммита; зафиксированный (committed) — данные надёжно сохранены в локальной базе. Отсюда три места, между которыми движутся изменения.

Рабочая папкаgit addИндекс (staging area)git commitРепозиторий (.git)
Путь изменения: от файла на диске до коммита в истории.

Рабочая папка

Что там лежит
Файлы, которые вы редактируете
Команда, чтобы увидеть
git status, git diff

Индекс

Что там лежит
Версии файлов, которые попадут в следующий коммит
Команда, чтобы увидеть
git diff --staged

Репозиторий

Что там лежит
Все коммиты в папке .git
Команда, чтобы увидеть
git log

Индекс позволяет собирать коммит осознанно: из пяти изменённых файлов отметить два, относящихся к одной задаче. Перед первым коммитом Git нужно представиться — имя и почта попадают в каждый коммит. Флаг --global сохраняет настройки для всех репозиториев пользователя; последняя строка делает main именем первой ветки по умолчанию (в документации git init сказано, что без настройки сейчас используется master, а в Git 3.0 станет main).

bash
git config --global user.name "Aigerim Dev"
git config --global user.email "aigerim@example.com"
git config --global init.defaultBranch main
Однократная настройка. Подставьте своё имя и почту. Проверено: Git 2.55, git config --global --list показывает три значения.

Первый репозиторий: init, status, add, commit, log

Сделаем репозиторий для лендинга кофейни. git init создаёт в текущей папке скрытый каталог .git — это и есть репозиторий. В папке два файла: README.md и страница index.html:

index.html
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Coffee Point</title></head>
<body>
  <h1>Coffee Point</h1>
  <p>Open daily 8:00-22:00</p>
</body>
</html>
Исходная страница — создайте её в редакторе.
terminal
$ git --version
git version 2.55.0.windows.1
$ mkdir landing && cd landing
$ git init -b main
Initialized empty Git repository in C:/Users/you/landing/.git/
$ git config user.name "Aigerim Dev"
$ git config user.email "aigerim@example.com"
$ printf '# Coffee Point landing\n' > README.md
$ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	README.md
	index.html

nothing added to commit but untracked files present (use "git add" to track)
$ git add index.html README.md
$ git status --short
A  README.md
A  index.html
$ git commit -m "Add landing page skeleton"
[main (root-commit) 01210cb] Add landing page skeleton
 2 files changed, 9 insertions(+)
 create mode 100644 README.md
 create mode 100644 index.html
$ git log --oneline
01210cb Add landing page skeleton
Git 2.55, Git Bash (реальный вывод; путь в строке Initialized сокращён, index.html создан в редакторе). В примере имя и почта заданы только для этого репозитория — без --global.
  1. git status

    Главная команда новичка: на какой вы ветке, что изменено, что подготовлено. Новые файлы Git называет untracked — он их видит, но не отслеживает.

  2. git add

    Переносит текущую версию файлов в индекс. git status --short показывает это компактно: ?? — неотслеживаемый файл, A — добавлен в индекс, M — изменён. Левая колонка — индекс, правая — рабочая папка.

  3. git commit -m

    Фиксирует содержимое индекса. Git печатает ветку, короткий хеш коммита (01210cb) и статистику. root-commit — первый коммит без родителя.

  4. git log --oneline

    История по одному коммиту в строке: хеш и сообщение. Без флага --oneline выводятся автор и дата.

.gitignore и что нельзя коммитить

В любом проекте появляются файлы, которым не место в истории: установленные зависимости (node_modules), сборки, логи и, главное, локальные настройки с секретами (.env). Файл .gitignore в корне репозитория перечисляет шаблоны путей, которые Git должен игнорировать.

terminal
$ printf 'API_KEY=YOUR_API_KEY\n' > .env
$ mkdir node_modules && touch node_modules/cache.js
$ printf 'debug\n' > npm-debug.log
$ git status --short
?? .env
?? node_modules/
?? npm-debug.log
$ printf 'node_modules/\n.env\n*.log\n' > .gitignore
$ git status --short
?? .gitignore
$ git add .gitignore
$ git commit -m "Ignore dependencies, logs and local secrets"
[main 8e7365c] Ignore dependencies, logs and local secrets
 1 file changed, 3 insertions(+)
 create mode 100644 .gitignore
$ git check-ignore -v .env npm-debug.log
.gitignore:2:.env	.env
.gitignore:3:*.log	npm-debug.log
Git 2.55, Git Bash (реальный вывод). git check-ignore -v показывает, какое правило из какой строки сработало.

Тот же .gitignore с комментариями — так его удобнее поддерживать:

.gitignore
# dependencies — installed by npm install
node_modules/
# local settings and secrets
.env
# logs
*.log
Проверено: git check-ignore -v находит правила для node_modules/, .env и *.log.

node_modules/

Что игнорирует
Папку с таким именем на любом уровне (косая черта в конце — только папки)

*.log

Что игнорирует
Все файлы с расширением .log; * не захватывает /

/build

Что игнорирует
Только build в корне — косая черта в начале привязывает шаблон к папке .gitignore

!keep.log

Что игнорирует
Возвращает файл, исключённый предыдущим правилом

# текст

Что игнорирует
Комментарий

Ветки: создание, переключение и fast-forward

Ветка в Git — подвижный указатель на коммит. Новую работу ведут в отдельной ветке: основная (main) остаётся рабочей, а незаконченная функция не мешает остальным. git switch -c имя создаёт ветку и переключается на неё (то же самое делают две команды: git branch имя и git switch имя). По Pro Git, начиная с версии 2.23 для переключения и создания веток можно использовать git switch вместо прежнего git checkout.

terminal
$ git switch -c feature/footer
Switched to a new branch 'feature/footer'
$ git diff
diff --git a/index.html b/index.html
index 8e73a21..4683878 100644
--- a/index.html
+++ b/index.html
@@ -4,5 +4,6 @@
 <body>
   <h1>Coffee Point</h1>
   <p>Open daily 8:00-22:00</p>
+  <footer>Almaty, Abay Ave 1</footer>
 </body>
 </html>
$ git commit -am "Add footer with address"
[feature/footer 47ae5bd] Add footer with address
 1 file changed, 1 insertion(+)
$ git switch main
Switched to branch 'main'
$ git merge feature/footer
Updating 8e7365c..47ae5bd
Fast-forward
 index.html | 1 +
 1 file changed, 1 insertion(+)
$ git branch -d feature/footer
Deleted branch feature/footer (was 47ae5bd).
Git 2.55, Git Bash (реальный вывод; строка footer добавлена в index.html в редакторе перед git diff).

git diff показывает неподготовленные изменения: строки с + добавлены, с - удалены. git commit -am подготавливает все уже отслеживаемые изменённые файлы и сразу коммитит — новые файлы этот флаг не добавляет. После переключения на main слияние прошло как fast-forward: в main не было своих новых коммитов, и Git просто передвинул указатель вперёд. Слитую ветку удаляют git branch -d — по документации, ветка должна быть полностью слита, иначе Git откажется её удалять.

Слияние с конфликтом: как его разрешить

Если обе ветки получили свои коммиты, Git выполняет трёхстороннее слияние: сравнивает два последних снимка с их общим предком и создаёт коммит слияния с двумя родителями. Когда ветки изменили одну и ту же строку по-разному, автоматически выбрать нельзя — это конфликт. Воспроизведём его: в ветке feature/hours поменяем часы работы, а в main — ту же строку иначе.

terminal
$ git branch feature/hours
$ git switch feature/hours
Switched to branch 'feature/hours'
$ sed -i 's/8:00-22:00/9:00-23:00/' index.html
$ git commit -am "Change opening hours to 9-23"
[feature/hours bd9d4cd] Change opening hours to 9-23
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git switch main
Switched to branch 'main'
$ sed -i 's/8:00-22:00/8:00-22:00, weekends till 23:00/' index.html
$ git commit -am "Mention weekend hours"
[main 437a99b] Mention weekend hours
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph --all
* bd9d4cd Change opening hours to 9-23
| * 437a99b Mention weekend hours
|/  
* 47ae5bd Add footer with address
* 8e7365c Ignore dependencies, logs and local secrets
* 01210cb Add landing page skeleton
$ git merge feature/hours
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   index.html

no changes added to commit (use "git add" and/or "git commit -a")
$ cat index.html
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Coffee Point</title></head>
<body>
  <h1>Coffee Point</h1>
<<<<<<< HEAD
  <p>Open daily 8:00-22:00, weekends till 23:00</p>
=======
  <p>Open daily 9:00-23:00</p>
>>>>>>> feature/hours
  <footer>Almaty, Abay Ave 1</footer>
</body>
</html>
Git 2.55, Git Bash (реальный вывод). sed меняет строку в файле — так же можно отредактировать её в редакторе.

Git остановился посреди слияния: файл помечен как both modified, а в нём появились маркеры. Между <<<<<<< HEAD и ======= — версия текущей ветки (main), между ======= и >>>>>>> feature/hours — версия сливаемой ветки. Разрешить конфликт — значит оставить итоговый текст и удалить все три маркера. Здесь правильный результат объединяет обе правки: новые часы и упоминание выходных.

index.html
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Coffee Point</title></head>
<body>
  <h1>Coffee Point</h1>
  <p>Open daily 9:00-23:00, weekends till 23:00</p>
  <footer>Almaty, Abay Ave 1</footer>
</body>
</html>
index.html после разрешения конфликта — маркеров не осталось.
terminal
$ git diff
diff --cc index.html
index d216469,5d082e3..0000000
--- a/index.html
+++ b/index.html
@@@ -3,7 -3,7 +3,7 @@@
  <head><meta charset="utf-8"><title>Coffee Point</title></head>
  <body>
    <h1>Coffee Point</h1>
-   <p>Open daily 8:00-22:00, weekends till 23:00</p>
 -  <p>Open daily 9:00-23:00</p>
++  <p>Open daily 9:00-23:00, weekends till 23:00</p>
    <footer>Almaty, Abay Ave 1</footer>
  </body>
  </html>
$ git add index.html
$ git status --short
M  index.html
$ git commit --no-edit
[main c954b42] Merge branch 'feature/hours'
$ git log --oneline --graph
*   c954b42 Merge branch 'feature/hours'
|\  
| * bd9d4cd Change opening hours to 9-23
* | 437a99b Mention weekend hours
|/  
* 47ae5bd Add footer with address
* 8e7365c Ignore dependencies, logs and local secrets
* 01210cb Add landing page skeleton
$ git branch -d feature/hours
Deleted branch feature/hours (was bd9d4cd).
Git 2.55, Git Bash (реальный вывод). git diff во время слияния показывает комбинированный формат: две колонки знаков — относительно каждой из веток.
  1. Найдите файлы с конфликтом

    git status перечисляет их в разделе Unmerged paths.

  2. Отредактируйте каждый файл

    Оставьте нужный вариант или объедините оба, удалите маркеры <<<<<<<, =======, >>>>>>>. Многие редакторы кода подсвечивают такие блоки и помогают выбрать вариант.

  3. Отметьте как разрешённое

    git add файл — по Pro Git, добавление в индекс и означает «конфликт разрешён».

  4. Завершите слияние

    git commit (с --no-edit — со стандартным сообщением Merge branch …). Передумали — git merge --abort вернёт всё к состоянию до слияния.

Отмена изменений: restore, revert, reset — без потерь

Для отмены есть несколько команд, и выбор зависит от того, где находится ошибка: в рабочей папке, в индексе или уже в коммите. Начиная с Git 2.23 для файлов есть отдельная команда git restore — её же подсказывает git status.

terminal
$ printf 'oops\n' >> README.md
$ git status --short
 M README.md
$ git restore README.md
$ git status --short
$ printf 'draft\n' > notes.txt
$ git add notes.txt
$ git status --short
A  notes.txt
$ git restore --staged notes.txt
$ git status --short
?? notes.txt
$ rm notes.txt
$ sed -i 's#  <footer>#  <p class="promo">-50% today</p>\n  <footer>#' index.html
$ git commit -am "Add promo banner"
[main c8a8b36] Add promo banner
 1 file changed, 1 insertion(+)
$ git revert --no-edit HEAD
[main 6bc2589] Revert "Add promo banner"
 Date: Sun Oct 11 16:11:34 2026 -0600
 1 file changed, 1 deletion(-)
$ git log --oneline -3
6bc2589 Revert "Add promo banner"
c8a8b36 Add promo banner
c954b42 Merge branch 'feature/hours'
$ grep -c promo index.html
0
$ printf 'typo\n' >> README.md
$ git commit -am "WIP"
[main 7ca022b] WIP
 1 file changed, 1 insertion(+)
$ git reset --soft HEAD~1
$ git status --short
M  README.md
$ git log --oneline -1
6bc2589 Revert "Add promo banner"
$ git restore --staged README.md && git restore README.md
$ git status
On branch main
nothing to commit, working tree clean
Git 2.55, Git Bash (реальный вывод).

Испортили файл, ещё не делали add

Команда
git restore файл
Что происходит
Файл возвращается к версии из индекса; правки в рабочей папке теряются

Лишний файл попал в индекс

Команда
git restore --staged файл
Что происходит
Файл убирается из индекса, содержимое на диске остаётся

Ошибочный коммит уже в общей истории

Команда
git revert <коммит>
Что происходит
Создаётся новый коммит, отменяющий изменения; история не переписывается

Последний коммит ещё никому не отправлен

Команда
git reset --soft HEAD~1
Что происходит
Коммит снимается, его изменения остаются подготовленными в индексе

Практика: репозиторий с намеренным конфликтом

Скрипт ниже создаёт учебный репозиторий: файл настроек сайта, ветку feature/hours и разные правки одной строки в обеих ветках. Сохраните его как conflict-lab.sh и запустите bash conflict-lab.sh в bash, zsh или Git Bash. Флаг -i.bak у sed нужен, чтобы команда одинаково работала в GNU sed (Linux, Git Bash) и BSD sed (macOS).

conflict-lab.sh
#!/usr/bin/env bash
# conflict-lab.sh — creates a practice repo with two branches and a deliberate merge conflict
set -e
dir="${1:-git-conflict-lab}"
mkdir "$dir" && cd "$dir"
git init -q -b main
git config user.name "Git Learner"
git config user.email "learner@example.com"
printf 'title: Coffee Point\ncity: Almaty\nhours: 8:00-22:00\n' > site.yml
git add site.yml
git commit -q -m "Add site settings"
git switch -q -c feature/hours
sed -i.bak 's/8:00-22:00/9:00-23:00/' site.yml && rm site.yml.bak
git commit -q -am "Change hours on feature branch"
git switch -q main
sed -i.bak 's/8:00-22:00/8:00-21:00/' site.yml && rm site.yml.bak
git commit -q -am "Change hours on main"
echo "Ready: cd $dir && git merge feature/hours"
Проверено: Git 2.55 в Git Bash и Git 2.45 в Alpine Linux — одинаковый результат.

Следующий шаг — удалённый репозиторий: git push, pull request и код-ревью на GitHub.

Как проверено4 · пройденосверено 11 октября 2026 г.
  • запущено, работает

    Основной сценарий: init, status, add, commit, log, .gitignore + check-ignore, switch -c, fast-forward merge, конфликт и его разрешение, restore / restore --staged / revert / reset --soft (shellsession-блоки, index.html)

    реальный git 2.55.0.windows.1 в Git Bash во временной папке, изолированный конфиг (GIT_CONFIG_NOSYSTEM=1, GIT_CONFIG_GLOBAL=/dev/null); блоки кода вырезаны скриптом из транскрипта (code.mjs), содержимое index.html сверено с командами printf из транскрипта; в выводе сокращён только путь временной папки в строке git init (помечено)

  • запущено, работает

    Тот же сценарий на Linux

    docker run --rm alpine:3.20 + apk git 2.45.4: вывод совпал с Git Bash, кроме хешей, дат, путей и порядка двух строк графа с одинаковой меткой времени

  • запущено, работает

    git config --global (3 строки) и .gitignore с комментариями

    Git 2.55 с GIT_CONFIG_GLOBAL на временный файл: git config --global --list показал значения, git init без -b создал ветку main; git check-ignore -v нашёл правила для node_modules/, .env, *.log

  • запущено, работает

    conflict-lab.sh и упражнения (разрешение конфликта, .gitignore с !keep.log)

    Git Bash (Git 2.55) и alpine:3.20 (Git 2.45, bash) — одинаковый результат; дополнительно проверены git branch -d на неслитой ветке (ошибка not fully merged) и git merge --abort

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.