Git (теория)

Git (теория)

LINE

Это первая(теоретическая) из двух частей статьи про git.

До git

Прежде всего ответим на вопрос зачем нужен git в частности и система контроля версий вообще. На примере, который знаком многим студентам.

Диплом приходится часто исправлять и дополнять. Из за этого получается такая картина - один и тот же файл в нескольких версиях.

При разработке ПО картина обычно выглядит еще хуже, т.к. множество версий пересекаются с множеством файлов множества людей (возможно множества компаний). Система контроля версий нужна именно для этой цели - хранить все версии файлов, сохранять историю изменений во времени, следить за развитием проекта.

Основные понятия git

Для начала разберемся с основными понятиями git, на примере того же студента.

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

Вся дипломная работа состоит всего из 2-х файлов: сам диплом(diploma) и некоторой дополнительной, пояснительной информации(additional).

Вначале папка репозитория пуста, но через некоторое время работы над дипломом репозиторий будет состоять только из 1-ой этой версии v1

Когда студент хочет поработать над дипломом, он копирует к себе прошлую версию(папку v1) и работает с этой копией. Тот набор файлов, с которой он сейчас работает(т.е. копия), называется рабочей директорией (working directory)

При этом рабочим может стать любая версия, из сохраненных в репозитории. В примере выше это версия v1.

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

Таким образом нам нужна еще одна область, в которую мы включим только то, что хотим сохранить. Такая область называется staging area(что то вроде промежуточной области) или иногда ее называют индекс(index).

Можно представить себе staging area как еще одну директорию, в которую мы копируем те файлы, которые нужно сохранить.

Последнее, что нужно сделать - сохранить все из staging area в репозиторий. Такое сохранение называется коммитом (commit). После этого репозиторий будет содержать 2 версии диплома

Если в какой то момент студент понимает, что версия v2 его не устраивает, он может вновь "загрузить" версию v1, просто скопировав папку v1 в свою рабочую директорию и продолжать работать именно с этой версией. В терминах git такая операция называется checkout, т.е. приведение рабочего каталога в соответствии с файлами в определенном коммите.

Теперь соберем все вместе. В git есть 3 области.

  1. Git directory(или репозиторий, или .git) - это скрытая папка с названием .git, которая хранит все сохраненные версии(или все коммиты). Без этой папки git перестает быть системой контроля версий.
  2. Working directory - это то, как файлы выглядят в настоящий момент в файловой системе. Т.е. это тот проект, с которым мы работаем и то, что мы можем увидеть без git.
  3. Staging area - файлы, которые будут сохранены при следующем коммите. Все, что не находится здесь сохранено не будет, даже если они находятся в working directory.

Поняв, как эти 3 области работают вместе можно понять основную идею git. Все остальное - детали.

Имея репозиторий с версиями, мы можем выбрать любую версию(любой коммит) для работы (с помощью checkout), тем самым изменив working directory. Поработав над версией, мы меняем файлы. Если мы решили что эти изменения должны быть сохранены в следующем коммите, мы добавляем их в область staging area(с помощью add). Когда все сохранения готовы их нужно зафиксировать помощью commit. После коммита репозиторий хранит еще одну версию, которую можно выбрать для работы.

Файлы

Вторая важная схема git, это то, в каком состоянии может находиться отдельный файл. Мы можем попросить git следить за файлами, или не просить и он следить не будет. Таким образом файл может как быть под контролем git(о них знает git), так и не быть.

Если файл находится под контролем git, он может быть еще в некоторых состояниях

  • unmodified(или comitted)
  • modified
  • staged

Вот как это работает. Вы добавляете(создаете) новый файл в рабочей директории. Пока что за этим файлом git не следит (untracked). После вы добавляете этот файл в staging area с помощью add, таким образом git теперь знает о нем и следит за этим файлом, файл находится в staged состоянии. После вы можете сделать коммит. После коммита файл становится unmodified - он такой же, как в последнем коммите. В тоже время за ним следит git, но мы можем сказать чтобы git перестал за ним следить(например с помощью git rm --cached). Тогда файл снова станет untracked. С другой стороны, мы можем изменить этот файл - тогда файл все еще отслеживается git, но он изменен по сравнению с тем что было в последнем коммите. Такой файл находится в состоянии modified. Если мы хотим добавить его в следующий коммит, мы переносим его в staging area с помощью add. Если нет - оставляем как есть (и в следующий коммит то, что было изменено в файле не войдет).

В итоге возможны 4 состояния и несколько переходов, показанных на изображении.

Указатели и ветки

И последнее что стоит осознать и пропустить через себя - ветки(branch). В примере со студентом все версии хранились в директории repository, а каждая версия была сохранена как версия (v1, v2 и т.д.). В терминах git такая версия - это коммит. К коммиту тоже можно обратиться, но для этого используется hash. Например, вместо v1 git сохранит коммит как ba2123158657326ae6613ed35f9c4f7b20bc081d. В последствии по этому hash можно к нему обратиться также, как к папке v1.

Хотя предполагается что написание диплома последовательный процесс (от версии N к версии N + 1), иногда будет полезно иметь возможность пойти немного другим путем.

Что если мы к каждой папке с версией добавим дополнительный текстовый документ (previous), который будет содержать предыдущий номер версии? Тогда папка repository, при наличии 4-х версий, может выглядеть так.

Заметьте, что версии идут так: v1 -> v2 -> v3, а v4 имеет предка V1. Визуально такую ситуацию можно изобразить с помощью древовидной структуры

Версия v4 как бы ответвление от основной версии. Это может быть, например, когда студент начал писать диплом по одному шаблону(v1 -> v2 -> v3), а потом решил попробовать другой(v1 -> v4), но пока точно не знает какой в итоге будет использовать.

Теперь ему нужно помнить, что версия v3 это старый шаблон, а v4 - новый. Возможно потом появится еще один. Чтобы не запутаться во всех версиях (ведь в repository студента они представлены как просто список файлов), можно добавить в repository еще один текстовый файл branches, который будет хранить название ветки и название версии(или указатель на версию, ведь с помощью этого указателя можно найти версию).

Тогда repository будет выглядеть примерно так.

Теперь можно найти все ветки в репозитории в одном месте и узнать на какое сохранение(коммит) она ссылается. Что же такое ветка в git? Это просто указатель на коммит(а точнее hash коммита).

Однако никто не мешает нам загрузить в рабочую директорию отдельный коммит, без ветки (например v2). Для этой цели служит еще один указатель HEAD, т.е. это указатель на тот коммит, с которым мы сейчас работаем. HEAD может указывать на определенный коммит, но в большинстве случаев он совпадает с определенной веткой.

Визуально версии можно теперь представить так: 2 ветки указывают на 2 версии, т.к. HEAD указывает на oldPattern, значит сейчас мы работаем именно с этой версией. Стрелки указывают на то, как развивалась отдельная ветка, хотя ссылки в действительности должны быть в обратную сторону (каждый коммит хранит ссылку на своего предка). Зачем могут быть нужны эти ссылки? Чтобы посмотреть историю ветки. Мы берем коммит по ветке и пока есть предыдущие переходим на предыдущий коммит (в git для просмотра истории используется команда log). При таком взгляде можно сказать что ветка - это односвязный список коммитов.

В git каждый коммит кроме hash и предка хранит еще дополнительную информацию: кем сделан, когда, сообщение коммита и т.д.

Есть еще весомая причина использовать ветки. Для этого возьмем 2 отдельных коммита (commit1 и commit2). Эти коммиты различаются файлом file. В первом коммите файл содержит "line 1 abc >>>", а второй "line 1 another >>> 12345". Если у нас есть 2 этих значения, мы можем найти дельту - то, как изменился файл file, т.е. различия этого файла в первом и втором коммите.


Теперь представьте ситуацию, что 2 человека работают над проектом. У них есть главная ветка main, в которую они договорились коммитить только "качественные" версии своего проекта. А пока каждый делает свою часть, и для этого каждый создал ветку(feature_1 и feature_2).

Первый разработчик закончил со своей частью быстрее и хочет чтобы ветка feature_1 была добавлена в main. Что для этого нужно? Просто поменять 1 указатель main на feature_1. Это так, потому что main потомок feature_1. А напомню, что ветка - это указатель на коммит. Мы хотим, чтобы main была в том же состоянии, что и feature_1. В итоге мы можем слить все что было в ветке feature_1 в main почти мгновенно. Теперь проект выгляди так

Теперь второй разработчик тоже закончил. Как теперь мы должны слить ветку feature_2 в main? Это зависит от наших предпочтений. Мы можем взять 3 коммита: коммит первой ветки, второй ветки и общего предка этих веток(в данном случае v1). Дальше найти все изменения, которые есть во второй(feature_2) ветке относительно предка, все изменения в первой ветке(main) относительно предка. Если вы нашли изменения, которые есть во второй(feature_2) но нет в первой(main) - их можно применить. Если есть изменения в обоих ветках и они одинаковые - можно взять любой. Если изменения есть в обоих ветках и они различны - это говорит о конфликте слияния и в таком случае git не сможет сделать слияние, т.к. не понятно какая версия должна быть в итоге.

В итоге мы можем сделать что то вроде такого(объединить 2 ветки или выполнить merge). У такого коммита(коммита слияния) будет 2 предка.

А можем взять все коммиты, начиная от общего предка ветки feature_2 и main(т.е. коммиты v3, v5, v7) и последовательно применить к ветке main(найти последовательно дельты main и v3, применить, потом main и v5, применить и т.д.) В таком случае мы изменили базу ветки feature_2, т.е. сделали перебазирование(rebase). Тогда это будет выглядеть так

Затем можно просто переместить ветку main на feature_2. Более того, мы можем выборочно взять коммит и попросить применить его поверх ветки(cherry-pick). Например взять только v5 коммит

Мы можем взять все коммиты ветки и перебазировать их, но попросить git "ужать" всю ветку в один коммит(squash). Выглядеть это будет так


Мы также можем по разному сделать отмену коммита. Просто переместить ветку на несколько коммитов назад, оставив при этом working directory в том же состоянии. Или полностью удалить последние коммиты и все что в них было сделано. И да, ветки не обязательно должны быть на одном устройстве.

Мы можем попросить git сделать коммит, в котором будет дельта, обратная предыдущей дельте(revert). Т.е. если у нас есть коммит v1, мы добавляем коммит v2, а после такой коммит v3, что коммиты v1 и v3 будут визуально одинаковыми.

Возможностей у git по работе с ветками множество.

История проекта

Важный момент, о котором стоит сказать, что меняя коммиты в ветках мы переписываем историю. И здесь есть 2 взгляда по этому поводу.

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

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

Однако важный момент, который стоит запомнить

!!!Никогда не менять историю ветки, если она используется(даже возможно используется) другими людьми!!!

Это приведет к множеству конфликтов и недопонимания.

Полезные ссылки

Несколько полезных ссылкок

Заключение

Основные части git

  • Области(staging area, working directory и repository)
  • состояния файлов(untracked, unmodified, staged, modified)
  • ветки
  • коммиты

Это не все, что есть в git, но пожалуй это самое важное.

В следующей (практической) части мы посмотрим на все это на практике и разберем многие команд git.

Report Page