Движок с нуля
Часть 1 · Старт · глава 1.8

История в Git

Заведёшь историю проекта в Git, выложишь её на GitHub и увидишь, какие команды Git стоят за кнопками Visual Studio.

Проект собран и настроен, и перед работой над движком стоит сохранить это состояние. Git ведёт историю проекта: каждый сохранённый шаг — коммит, к которому можно вернуться. Git ты установил отдельной программой, когда ставил Visual Studio и CMake.

Как работает Git

Прежде чем нажимать кнопки, посмотри, что Git делает с файлами.

Как работает Git: индекс, коммит и push

Git работает с четырьмя местами. Рабочая папка — твоя папка MyEngine, в которой ты правишь файлы. Индекс — черновик следующего коммита: сюда ты складываешь изменения, которые хочешь сохранить вместе. История — все коммиты проекта, Git хранит их в скрытой папке .git внутри папки проекта. GitHub — сайт, на котором лежит копия истории. Изменения проходят по этим местам слева направо, и каждый переход делает отдельная команда Git: git add переносит изменения из рабочей папки в индекс, git commit — из индекса в историю, git push — из истории на GitHub.
В рабочей папке лежат обычные файлы, Git их не меняет. Git только сравнивает эти файлы с последним коммитом и по этому сравнению видит, какие файлы изменились. Не всё, что лежит в папке, нужно хранить в истории. Папку out с результатом сборки и папку .vs со служебными файлами Visual Studio каждый компьютер создаёт сам, поэтому хранить такие папки незачем. Имена этих папок записывают в файл .gitignore, и Git эти папки пропускает: для Git этих папок как будто нет.
Команда git add кладёт изменённый файл в индекс. Индекс — снимок файлов в том виде, в каком файлы войдут в следующий коммит. Зачем нужен этот промежуточный шаг: допустим, ты изменил два файла, а работу закончил только над одним. Ты кладёшь в индекс законченный файл, и следующий коммит сохранит только этот файл. Второй файл остаётся изменённым в рабочей папке и попадёт в один из следующих коммитов.
Команда git commit сохраняет снимок из индекса в историю: появляется новый коммит. У коммита есть автор, время, сообщение о том, что сделано, и номер из 40 знаков, по которому Git находит коммит. Обычно показывают первые 7 знаков номера.
Коммиты складываются в цепочку: каждый коммит помнит, какой коммит был перед ним. Поэтому к любому коммиту можно вернуться, а между двумя коммитами Git покажет, что изменилось. Метка main — ветка: имя, которое указывает на последний коммит цепочки. Сама метка коммиты не хранит, она только показывает на один из них, на самый новый. Когда ты сохраняешь новый коммит, Git добавляет коммит в конец цепочки и переставляет метку main на новый коммит. Поэтому по имени main всегда находится последний коммит.
Команда git push копирует на GitHub коммиты, которых там ещё нет. Вся история при этом остаётся и на твоём компьютере: коммиты записываются на диск, поэтому сохранять их можно без интернета. Копия на GitHub нужна, чтобы проект не пропал, если сломается диск, и чтобы ты мог показать проект другим по ссылке.

Почему Visual Studio, а не консоль

В уроках и ответах в интернете Git почти всегда показывают командами в консоли: git add, git commit, git push. Такие команды ты будешь встречать везде, поэтому в этой главе рядом с каждым шагом в Visual Studio есть блок «То же в консоли». В блоке — команда, которая делает то же самое.

Кнопки Visual Studio запускают Git: свою копию Git, но эта копия читает и пишет ту же папку .git, что и Git в консоли. Своего способа хранить историю у Visual Studio нет. Поэтому история у консоли и у Visual Studio одна. Коммит, который ты сделал кнопкой, будет виден в консоли. Коммит, который ты сделал командой в консоли, будет виден в окне Visual Studio. Значит, работать можно в любом из двух мест. Мы выбираем Visual Studio, потому что в ней удобнее:

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

Поэтому после этой главы Git в курсе — только в окне Visual Studio.

Имя и почта

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

Эти настройки глобальные: следующий проект получит ту же подпись без новой настройки. Над полями написано, что значения хранятся в глобальных настройках Git, и это тот же файл, который меняет команда git config --global. Ниже этой надписи стоит MinGit и номер версии: строка показывает, что у Visual Studio своя копия Git. Копия и Git, который ты ставил сам, читают один и тот же файл настроек.

Что не хранить

В истории хранят только то, что пишет человек: код, CMakeLists.txt, CMakePresets.json. Папку сборки out хранить не нужно: CMake и сборщик создают её заново на любом компьютере. Папку .vs Visual Studio создала для своих служебных файлов, например открытых вкладок. Чтобы Git не предлагал сохранить эти папки, имена папок перечисляют в файле .gitignore.

Репозиторий и первые коммиты

Репозиторий — папка проекта, у которой есть история. Пока у MyEngine истории нет, поэтому сначала Git заводит папку .git, а потом сохраняет в историю первые коммиты.

Если не получилось3
Your solution contains files outside the solution folder which will not be included. Это сообщение приходит в окне Source Control - Git с вопросом Do you want to continue?.
Почему
Visual Studio заводит историю в папке из поля Local path. Если там осталась папка сборки D:\dev\MyEngine\out\build\vs2026, файлы проекта main.cpp и CMakeLists.txt лежат выше этой папки, то есть снаружи будущей истории. Visual Studio предупреждает, что эти файлы в историю не попадут.
Что сделать

Нажми No. В окне Create a Git repository нажми кнопку … справа от поля Local path, выбери D:\dev\MyEngine и снова нажми Create.

Внизу окна написано master, а не main.
Почему
Имя главной ветки берётся из глобальных настроек Git. Если поле Default branch name осталось пустым, Git называет первую ветку по старому правилу — master.
Что сделать

Проект от этого работает так же, имя ветки влияет только на подпись. Чтобы переименовать ветку, щёлкни по имени ветки в строке состояния, выбери Manage BranchesУправление ветвями, нажми правой кнопкой на master и выбери RenameПереименовать.

В первом коммите оказалась папка out или .vs.
Почему
Git проверяет .gitignore в момент коммита. Если файла ещё не было, был сохранён под другим именем, например .gitignore.txt, или лежит не в D:\dev\MyEngine, Git сохранил всё, что нашёл в папке.
Что сделать

Проще всего начать историю заново: закрой Visual Studio, удали в Проводнике скрытую папку D:\dev\MyEngine\.git, проверь имя и место .gitignore и повтори шаги этого раздела. Скрытые папки Проводник показывает после ViewПросмотреть → ShowПоказать → Hidden itemsСкрытые элементы.

После нажатия Create Visual Studio по очереди сделала три вещи:

  1. создала в папке проекта скрытую папку .git, в которой Git хранит историю;
  2. создала в папке проекта новый файл .gitattributes и сохранила только этот файл первым коммитом с сообщением Add .gitattributes.;
  3. сохранила вторым коммитом с сообщением Add project files. все остальные файлы проекта, кроме папок из .gitignore.

Файл .gitattributes хранит настройки Git для файлов проекта. Работает в файле одна строка, * text=auto: с ней Git записывает в историю концы строк одинаково, поэтому main.cpp, сохранённый в Windows, и тот же main.cpp, сохранённый в Linux, Git не считает разными. Файл .gitattributes оставляем в проекте как есть.

Подробнее: как работает * text=auto

Строки файла .gitattributes, которые начинаются с #, — комментарии с примерами, Git их пропускает. В строке * text=auto звёздочка * значит «все файлы проекта», а text=auto просит Git самому понять по содержимому, с чем он имеет дело. Файлы бывают двух видов. Текстовый файл — это код и настройки. Бинарный файл — это картинка, звук, app.exe: такой файл записан байтами для программы, а не для человека, и в Блокноте превращается в набор странных символов. Git смотрит на содержимое файла, и только в текстовых файлах следит за концом строки.

  • При коммите Git заменяет в текстовых файлах окончания CR LF на LF, поэтому в истории файл записан одинаково, на каком бы компьютере файл ни сохранили.
  • Когда Git кладёт файлы из истории в рабочую папку на Windows, Git снова ставит CR LF, и редактор в Windows видит привычные окончания.
  • Бинарные файлы, например картинки, Git сохраняет как есть, без замены: в картинке нет строк, и замена испортила бы файл.

Без строки * text=auto файл main.cpp, который кто-то сохранил в Linux или macOS, отличался бы от версии из Windows окончанием каждой строки, и сравнение показало бы, что изменились все строки файла.

Команды из блока «То же в консоли» сделали бы один коммит и без файла .gitattributes: этот файл создаёт Visual Studio, а git init такого файла не создаёт.

Историю видно в GitGit → View Branch HistoryПросмотреть журнал ветви: там два коммита с твоим именем и временем. Новый коммит стоит сверху, поэтому первым в списке идёт Add project files., а под ним Add .gitattributes.. В консоли историю печатает git log --oneline: по строке на коммит, первые 7 знаков номера и сообщение.

Окно Git Changes

Дальше каждый законченный шаг движка сохраняем коммитом, и в окне Git ChangesИзменения Git для этого есть всё. Окно открывается через меню ViewВид → Git ChangesИзменения Git или сочетанием Ctrl+0, затем G. Обычно окно встаёт вкладкой рядом с Solution ExplorerОбозреватель решений: переключаться между двумя окнами можно по вкладкам внизу панели. Если окно закрыто, тем же пунктом меню оно откроется снова.

Как сделать коммит в окне Git Changes

Так выглядит окно Git ChangesИзменения Git, когда в проекте изменён один файл. Сверху список веток показывает ветку, в которую попадут новые коммиты, сейчас это main. Справа от списка кнопка Push: пока сервера нет, кнопка подключает проект к GitHub, а потом отправляет туда коммиты, чтобы там лежала копия истории. Ниже число коммитов говорит, сколько коммитов уже есть в истории ветки, а ссылка View all commits открывает эту историю списком. В середине поле для сообщения коммита: коротко пиши в поле, что сделано, и сообщение останется в истории рядом с коммитом. Под полем кнопка коммита сохраняет коммит, а флажок Amend нужен, чтобы заменить последний коммит, а не добавить новый. Внизу список Changes — файлы, которые изменились с последнего коммита и ждут сохранения.
В списке Changes лежат файлы, которые изменились с последнего коммита: Visual Studio спрашивает об этом Git, так же как команда git status. Буква справа от имени файла говорит, что с файлом случилось: M (modified) — файл изменён, A (added) — файл новый, D (deleted) — файл удалён. Двойной щелчок по файлу открывает сравнение: слева старая версия файла, справа новая. Строка, которую поменяли, слева красная со знаком минус, а справа зелёная со знаком плюс. Рамка внутри строки отмечает, что именно поменялось: красная рамка — то, что убрали, зелёная — то, что добавили. То же печатает команда git diff. Смотри изменения перед каждым коммитом: так в коммит не попадёт случайная правка.
Если в коммит нужна только часть изменений, наведи на файл и нажми «+» справа от имени файла. Файл переедет в группу Staged Changes: эта группа и есть индекс, а «+» делает то же, что команда git add main.cpp. Кнопка коммита тогда называется Commit Staged и сохраняет только файлы из группы Staged ChangesИндексированные изменения. Остальные файлы остаются в списке Changes до следующего коммита. «+» в строке Changes кладёт в индекс все изменённые файлы сразу.
Чтобы сохранить коммит, впиши в поле сообщение о том, что сделано, и нажми Commit AllЗафиксировать все. Кнопка делает две команды по порядку: сначала git add для всех файлов из списка ChangesИзменения, затем git commit с сообщением из поля. У кнопки есть стрелка, а в списке под стрелкой три пункта. Первый, Commit AllЗафиксировать все, делает то же, что сама кнопка, и у него есть сочетание Ctrl+Enter. Два других, Stash All и Stash All and Keep Staged, убирают изменения в отдельное хранилище Git без коммита, чтобы вернуть изменения позже. Флажок Amend нужен, если в последнем коммите ошибка, например забыт файл или опечатка в сообщении: со включённым флажком новый коммит не добавится к истории, а заменит последний, как команда git commit --amend. Заменять можно только коммит, который ещё не отправлен на GitHub: у того, кто уже скачал историю, остался бы старый коммит, и истории разошлись бы.
Пока проект не подключён к серверу, справа от списка веток стоит одна кнопка PushОтправить: она подключает проект к GitHub. После подключения вместо слова появляются кнопки обмена коммитами с сервером, и сцена на этой реплике показывает окно именно таким. Стрелка вверх, PushОтправить, отправляет на сервер коммиты, которых там ещё нет, как git push. Стрелка вниз, PullПолучить, забирает с сервера новые коммиты, например сделанные на другом компьютере, как git pull. Пунктирная стрелка вниз, FetchИзвлечь, только узнаёт, есть ли на сервере новые коммиты, и ничего не меняет в рабочей папке. Круговая стрелка, SyncСинхронизировать, делает Pull, а затем Push. Счётчик теперь показывает два числа: сколько твоих коммитов ещё не отправлено и сколько новых коммитов ждёт на сервере. После Push первое число становится 0. Ссылка View all commits открывает историю ветки.
Главное про Git видно и без окна Git ChangesИзменения Git: в самом низу окна Visual Studio, в правой части строки состояния, стоят четыре кнопки. Значок коммита с числом — сколько коммитов ещё не отправлено на сервер. Когда сервер подключён, значок становится стрелкой. Пока сервера нет, не отправлен ни один коммит, поэтому число равно числу коммитов в окне Git ChangesИзменения Git. Щелчок показывает список этих коммитов. Карандаш с числом — сколько файлов изменено с последнего коммита, щелчок открывает окно Git ChangesИзменения Git. Имя ветки — ветка, в которой ты сейчас работаешь, щелчок открывает меню с ветками проекта. Имя репозитория — папка с историей, которую открыла Visual Studio, щелчок открывает список других репозиториев и настройки. Значок ^ рядом с числом или именем значит, что по щелчку откроется список. Перед коммитом удобно взглянуть на карандаш: если число не 0, в проекте есть несохранённые в истории правки.

Убрать случайный коммит

В истории сейчас три коммита: два от Visual Studio и твой Change greeting. Допустим, коммит Change greeting сделан случайно: менять приветствие не требовалось. Пока коммит лежит только на твоём компьютере, его можно убрать из истории совсем: вернуть ветку на коммит, который стоит перед ним.

Теперь разберём, что произошло с историей.

Как отменить последний коммит через Reset

До твоего Reset в истории было три коммита. Верхний коммит, Change greeting, был лишним. Метка main стояла на коммите Change greeting, потому что этот коммит самый новый.
Ты щёлкнул правой кнопкой по коммиту Add project files.: это последний нужный коммит, он стоит прямо под лишним. Щелчком по этому коммиту ты показал, на какой коммит вернуть ветку.
Коммиты в Git лежат цепочкой, а ветка — это метка main, которая показывает на самый новый коммит цепочки. Reset переставил метку main на выбранный коммит, на Add project files.. Коммит Change greeting остался выше метки, а историей ветки считается всё, что стоит от метки вниз к старым коммитам. Поэтому в журнале коммита Change greeting больше нет.
Пункт Reset → Delete Changes (--hard) делает то же, что команда git reset --hard HEAD~1. Visual Studio подставляет в команду номер коммита, по которому ты щёлкнул, а в консоли тот же коммит проще назвать через HEAD~1. Это одна команда, а не две: git reset — название команды, а --hard и HEAD~1 — её параметры. В Visual Studio вводить команду не нужно: ты выбираешь пункт меню, и Visual Studio запускает команду сама. В консоли команду набирают руками. Параметр HEAD~1 говорит, куда переставить метку. Слово HEAD значит «ты сейчас здесь»: так Git называет ветку, в которой ты работаешь, то есть main, а через метку main находит самый новый коммит. Запись ~1 отсчитывает от самого нового коммита один коммит назад. Флаг --hard говорит, что вместе с меткой нужно вернуть и файлы в папке проекта: в main.cpp снова прежний текст приветствия. Без этого флага метка переехала бы, а main.cpp остался бы с новым текстом, и новый текст снова считался бы несохранённой правкой.

Как и флаг Amend, Reset годится только для коммитов, которые ещё не отправлены на GitHub.

То же в консоли
PowerShell
PS D:\dev\MyEngine> git reset --hard HEAD~1

git reset --hard HEAD~1 переставляет метку ветки main на один коммит назад и возвращает в рабочую папку файлы из коммита, на который теперь показывает метка, то есть из Add project files..

А если коммит уже отправлен на GitHub

Тогда историю не переписывают, а добавляют к ней коммит, который отменяет правки лишнего. В журнале это пункт Revert в меню правой кнопки на нужном коммите: Visual Studio запишет новый коммит с сообщением Revert “Change greeting”, а старый коммит останется в истории. В консоли то же делает git revert --no-edit HEAD.

На GitHub

Пока история лежит только на твоём компьютере. GitHub хранит копию истории: если диск сломается, проект не пропадёт, а ссылку на репозиторий можно показать другим. Для этого нужна учётная запись на github.com: зарегистрируйся там, если ещё не сделал этого.

Когда у проекта появился сервер, окно Git ChangesИзменения Git выглядит иначе: у строки с названием ветки вместо слова Push стоит ряд значков со стрелками, а ниже числа коммитов. Кнопка Push теперь стрелка вверх. Дальше после коммита нажимай эту кнопку. Можно и одной кнопкой: в списке под стрелкой у кнопки Commit AllЗафиксировать все теперь есть пункт Commit All and PushЗафиксировать все и отправить.

Итог

У проекта есть история: два коммита на компьютере и та же история на GitHub. Каждый следующий шаг движка — коммит в окне Git ChangesИзменения Git и PushОтправить.

Шпаргалка: Visual Studio и консоль
Что сделать В Visual Studio В консоли
задать имя и почту Git → Settings → Git Global Config git config --global user.name "…"
завести историю Git → Create Git Repository git init
посмотреть, что изменилось окно Git Changes, двойной щелчок по файлу git status, git diff
положить файл в индекс + у файла в Git Changes git add main.cpp
сохранить коммит Commit All или Commit Staged git add ., затем git commit -m "…"
заменить последний коммит флажок Amend git commit --amend
убрать последний коммит Reset → Delete Changes (--hard) в View Branch History git reset --hard HEAD~1
посмотреть историю Git → View Branch History git log --oneline
отправить на GitHub Push в Git Changes git push