Как начать понимать Git

На реддите в разделе "ProgrammerHumor" к бувально каждой шутке про git верхним, максимально заплюсованным комментом идёт ссылка на сайт https://ohshitgit.com/

Русская версия этой странички недавно была опубликована в виде поста на пикабу, что побудило меня написать данный текст. Он в первую очередь для тех, кто давно уже (месяцы или даже больше года) пользуется Git, но всё ещё регулярно копирует всю папку с проектом перед выполнением "страшных" команд. У меня этот период занял года два, а потом наконец случилось "просветление".

Причиной просветления послужил сайт think-like-a-git.net

Автор данного сайта обнаружил интересную закономерность. Люди в интернете, объясняя как работает Git, часто используют фразы наподобие: "Git становится намного понятнее, как только ты понимаешь, что...." - и далее часть фразы, которая вообще никак твоё понимание не увеличивает. То есть, существует странный понятийный разрыв - люди, которые поняли Git, не могут донести своё понимание, и это системная проблема. И тогда этот замечательный человек создал целый сайт, который коротко (относительно) и с юмором пробивает эту понятийную стену у тех, кто за много месяцев использования и чтения документации так и не пришёл в довольно очевидным вещам своим умом.

Когда я прочитал этот сайт, я сидел с открытым ртом и мыслью "Так вот оно что!". При этом, сайт не сообщает какой-то секретной новой информации. Он просто соединяет кусочки этой информации в голове читателя так, что происходит такой ощутимый щелчок, когда все части головоломки встают на свое место. И ты просто внезапно осознаешь, что фраза "git cтановится понятнее, когда ты его понял" перестаёт выглядеть для тебя бессмысленной тарабарщиной)))

Я долгое время пытался перевести этот сайт и оформить в виде поста на хабре, и у меня второй год валяется примерно десятая итерация черновика. Проблема в том, что у меня не получается сделать это короче, чем автор сайта, и при этом донести то, что он хочет донести. В итоге я отчаялся и так и не довел работу до конца, но по комментариям что на реддите, что на пикабу вижу, что проблема никуда не исчезла. Поэтому решил, что создам пост хотя-бы в таком виде.

Далее я кратко попробую сказать, о чем говорится на сайте "Think like a git", но не надейтесь что моё ультракороткое изложение может его заменить. ИДИТЕ И ЧИТАЙТЕ САЙТ.

Итак, о чем же Think-like-a-git?

Коммиты никуда не исчезают. Если вы сделали коммит - ваши данные в безопасности. Если вы в проекте из 1000 коммитов случайно сделали git reset --hard на первый коммит - вы не сделали ничего даже отдалённо ужасного. У вас есть определенная проблема - в выдаче команды git log вы видите один единственный начальный коммит, и не видите остальных коммитов, которые включают в себя (допустим) пару лет разработки. Но они там есть. Просто они недоступны. Но не в смысле "шеф, всё пропало!"-недоступны, а скорее "придется потратить полминутки, чтобы вернуть всё как было".

Представьте себе такой мысленный эксперимент. Мы берем Git-репозиторий, состоящий из файла text.txt. Копируем туда первую главу "Войны и мира", коммитим с комментарием "Глава 1". Потом берем, удаляем всё из файла, вставляем туда 2-ю главу и делаем git commit --amend, исправляя комментарий на "Глава 2". И так делаем 10 раз подряд. Команда git log покажет нам историю, состоящую из единственного сиротливого коммита, и если мы откроем файл - там одна только 10 глава. Потеряли ли мы первые девять глав? НЕТ! Они все остались в репозитории.

Можно подумать, что опция --amend переписывает последний коммит, но подумайте вот о чем. Имя коммита - это хэш от его содержимого+комментария+хэш родительского коммита. Как только вы редактируете хоть один символ и делает git commit --amend, вы получаете совершенно другой хэш. И совершенно другой коммит. Вы создаёте новый коммит каждый раз при --amend, но все предыдущие версии коммита никуда не пропадают и остаются лежать там же, рядышком, ожидая когда они вам понадобятся. И их содержимое реально (и несложно) достать обратно.

Вернемся к первому примеру, где мы сделали git reset --hard на первый коммит и "потеряли" 2 года разработки. Мы знаем, что где-то внутри репозитория есть второй коммит, который идет после первого, затем, третий, и так далее, вплоть до тысячного. Чтобы вернуть доступ ко всей истории, нам достаточно переключиться на последний коммит - и git немедленно "увидит" все предшествующие. Всё что нужно узнать - это имя, оно же хэш, последнего коммита.

А потом - прикрепить к нему ссылку с именем.

Если свести весь сайт "Think-like-a-git" к одной короткой фразе, то она будет следующей: "Ссылка обеспечивает к коммиту доступ". Создали ветку на коммите - и всё, информация никуда не денется и останется доступной. Снова вернемся ко второму примеру, с 10 главами "Войны и мира". Через git reflog получаем список всех 10 коммитов, которые находятся в репозитории. В каждом - по одной главе книги. Переключаемся на коммит, помеченный как "Глава 1", по его хэшу - git checkout %хэш_1%, и прикрепляем к нему ссылку любого типа. Можно создать ветку: git switch -c chapter_1. Можно создать тэг: git tag chapter_1

После повторения процедуры для всех десяти коммитов, мы можем перейти на ветку "master" и продолжить работу, но если нам вдруг понадобится текст глав с 1 по 9, достаточно посмотреть список веток/тегов и переключиться на нужный.

Обычно, если пользуешься Git из консоли - достаточно регулярно делать git log. Если даже случайно что-то напортачишь - достаточно пролистать историю консоли выше, скопипастить там имя нужного коммита, к которому потерян доступ, и всё починить в течение буквально нескольких секунд. И где-то на этом моменте страх что-то испортить начинает исчезать.

Проблема "запутанной рабочей копии"

Ещё одна вещь, о которой я хотел бы рассказать. Опять же, тщательно и подробно она описана вот тут (ОПЯТЬ ЖЕ, ИДИТЕ И ЧИТАЙТЕ!). Если коротко и сумбурно - опция git add --patch, или же коротко git add -p - позволяет добавлять в индекс файл "покусочно". И если вы не пробовали эту опцию - с высокой степенью вероятности, вы полюбите её с первого взгляда и больше git add без неё использовать не будете. Она прогоняет все изменения в каждом файле, кусок за куском, и спрашивает вас - "добавить этот кусок в index?". Вы просто отвечаете y/n. Очень часто это позволяет выявить (и не добавлять в коммит) ненужные вещи, вроде лишних вставок пустых строк или debug-код, который вы в процессе работы написали, но в коммите он строго говоря не нужен.

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

По-умолчанию, для нового файла в проекте опция --patch не работает, Git добавляет такой файл в index только целиком. Если вы хотите новый файл разбить по нескольким коммитам - необходимо сперва этот файл добавить с опцией -N, она же --intent-to-add - после чего опция --patch для него заработает.

Использование git diff / git show для быстрого включения в работу

Это просто маленький лайфхак, пишу его для тех, кто не в курсе. Допустим, вы работали над проектом, вечером в пятницу не успели ничего доделать (и в рабочем каталоге творится какая-то каша, и программа даже не компилится - ай яй яй, нехорошо так делать!).

Придя на работу в понедельник, первым делом выполняете git diff и вдумчиво листаете - перед глазами оказываются те вещи, над которыми вы работали, и ничего лишнего. Это позволяет намного быстрее включиться в работу, особенно если изменения разбросаны по разным файлам.

Второй пример. Вам надо продолжить работу над проектом после перерыва в несколько месяцев. Вы открываете проект, там несколько десятков файлов лютого, запутанного кода. Вы не понимаете, как он работает и что он делает, несмотря на то, что это ваш собственный код. git diff пишет, что рабочий каталог без изменений (к счастью!).

Первое действие - git log, и читаем комментарии к коммитам. В памяти постепено начинает всплывать, над какими функциями вы работали, когда последний раз трогали этот проект.

Второе действие - git show HEAD и читаем последний коммит. Затем git show HEAD~1 и читаем предпоследний. И ещё один. Если коммиты сделаны по всем правилам - небольшие и содержат только одно небольшое изменение - то проект читается как открытая книга. И через короткий промежуток времени в голове восстанавливается тот рабочий контекст, в котором вы вносили последние изменения в проект. Можно продолжать работать.

Как можно потерять сделанную работу?

Главная опасность - это потерять несохранённые изменения. Если у вас в рабочем каталоге накопилось изменений на кучу коммитов, в процессе разбивки кода по коммитам можно знатно обосраться и случайно сбросить несохранённые изменения. Если и есть причина перед работой с Git сделать копию проекта - то это она :) Я предпочитаю в таких случаях сделать огромный коммит со всеми изменениями в кучу, добавить ему ветку или тэг типа saved_commit, после чего сделать git reset HEAD~ (при этом все изменения снова окажутся в рабочем каталоге) и начать распихивать всё по коммитам. В случае проблем:
1) git reset --hard сбрасывает рабочий каталог (уничтожит всю сделанную работу, если не сохранить её в коммит предварительно!)
2) git checkout saved_commit заново заполняет рабочий каталог плодами многочасового труда
3) git reset master возвращает указатель на ветку master, рабочий каталог содержит все изменения из saved_commit,

Вторая опасность, о которой неоднократно писали в комментариях - это делать git push --force. Но это выходит за рамки данной статьи.