|
Professor Seleznov
|
Введение Для меня Git относится к тем технологиям про которые все слышали, многие пользуются, но про которые всегда узнаёшь что-то новое. Git - система контроля версий, которую используют все IT-специалисты. Конечно, кто-то использует и другие, но я таких не встречал. И так, как же построен Git и как с ним работать, чтобы не было мучительно больно за случайно уничтоженные наработки. Часть 1. Философия и устройство Компоненты Git, в типичной своей конфигурации, состоит из двух частей - сервер и клиент. Сервер обеспечивает доступность единого достоверного источника истины для репозиториев, а клиенты взаимодействуют с локальными репозиториями. Также клиент позволяет получать доступ к удалённым репозиториям и передавать изменения на сервер. Характеристики Git можно охарактеризовать следующим:
- Сохраняет изменения ревизий в виде снимков (сохраняется множество ревизий файла над которым идёт работа. Git делает снапшоты внесённых изменений - коммиты);
- Расширен под локальную разработку (делается локальный клон удалённого репозитория. Все ресурсы и снимки ревизий хранятся в одном месте - история коммитов);
- Действует в соответствии с инструкциями (явные команды. Ничего не будет сделано, пока пользователь не предоставит инструкции о том что сделать и в какой момент);
- Поддержка нелинейной разработки (можно отклоняться от основной линии разработки и работать параллельно с ней - ветвление).
Ветвление считается легковесной операцией, так как является всего лишь указателем на последний коммит в серии связанных коммитов.
Командная строка CLI интерфейс git очень простой и даёт пользователю полный контроль над репозиторием. Вот список некоторых распространённых команд. Настройка и создание репозитория:
- git config: Используется для задания имени пользователя и email, которые будут привязаны к вашим коммитам;
- git init: Превращает текущую папку в Git-репозиторий, создавая скрытую директорию .git;
- git clone url: Копирует существующий удаленный репозиторий на ваш компьютер.
Работа с изменениями:
- git status: Показывает состояние рабочего каталога: какие файлы изменены, а какие подготовлены к сохранению;
- git add file: Добавляет изменения из файла в «индекс» (staging area) для последующего сохранения. Чтобы добавить всё сразу, используйте git add .;
- git commit -m "сообщение": Сохраняет проиндексированные изменения, создавая своего рода «снимок» проекта с описанием;
- git diff: Показывает разницу между текущими изменениями в файлах и последним сохраненным состоянием;
- git rm file: Удаляем файл из тех для которых отслеживаются изменения.
История и отмена:
- git log: Выводит список всех совершенных коммитов в обратном хронологическом порядке;
- git restore file: Отменяет незафиксированные изменения в файле, возвращая его к состоянию последнего коммита;
- git rm file: Удаляет файл из проекта и из индекса Git.
Ветки и слияние:
- git branch: Показывает список всех локальных веток или создает новую ветку;
- git checkout branch-name: Переключает вас на указанную ветку (в новых версиях Git также часто используют git switch);
- git merge branch-name: Объединяет изменения из указанной ветки в ту, на которой вы находитесь.
Работа с удаленным репозиторием (GitHub, GitLab и др.):
- git remote add origin url: Привязывает локальный репозиторий к удаленному;
- git push: Отправляет ваши локальные коммиты на удаленный сервер;
- git pull: Загружает последние изменения из удаленного репозитория и сразу объединяет их с вашим кодом.
Для просмотра всего доступного списка можно ввести git help --all. Принимаются как “короткие”, так и “длинные” опции, например:
git commit -m "Test" равно git commit --message="Test"
Опции от списка аргументов можно отделять с помощью двойного тире, например
git diff -w main origin -- tools/MakeFile
Файлы конфигурации Есть три уровня, откуда берутся конфигурации. Вот они, в порядке убывания приоритета:
- .git/config(относятся к репозиторию, управляются через --file. Также запись в него происходит с помощью опции --local);
- ~/.gitconfig (пользовательские настройки, управляются через опцию --global);
- /etc/gitconfig (системные настройки, управляются через опцию -system) Например, чтобы иметь возможность делать коммиты необходимо настроить подпись для них, делается это с помощью команд
# Для одного репозитория git config --local user.name "Ivan Ivanov" git config --local user.email "ivanov@example.com" # Для всего git git config --global user.name "Ivanych666" git config --global user.email "ivanych666@example.com"
Псевдонимы Сложные команды git можно заворачивать в алиасы. Выглядит это так
git config --global alias.show-graph 'log --graph --abbrev-commit --pretty=oneline'
И после данной манипуляции строку log --graph --abbrev-commit --pretty=oneline можно заменить лаконичным show-graph. Часть 2. Фундамент Репозиторий По своей сути репозиторий - это база данных “ключ-значение” (находится в директории .git), а также полная копия всего проекта за всю его историю. Внутри репозитория Git поддерживает две структуры - хранилище объектов и индексы. Индекс - это временная информация, которая относится конкретно к репозиторию и при необходимости может создаваться и изменяться. Хранилище объектов нужно, чтобы его можно было копировать во время клонирования. Хранилище объектов (object store) Хранилище содержит в себе всю необходимую информацию для воссоздания или восстановления любой версии или ветки проекта. Оно содержит в себе четыре типа объектов - BLOB, деревья, коммиты и теги. BLOB Binary Large Object. Этот термин используется для обозначения переменной или файла, который может содержать любые данные и внутренняя структура которого игнорируется программой. Он содержит данные файла, но не содержит ни метаданных, ни имени этого файла. В виде BLOB представлена каждая версия файла. Деревья Один объект дерева представляет собой один уровень информации каталога. Он регистрирует идентификаторы BLOB-объектов, пути, необходимые метаданные для всех файлов в каталоге. Может рекурсивно ссылаться на другие поддеревья, создавая полную иерархию файлов и подкаталогов. Коммиты Содержит метаданные для каждого внесённого в репозиторий изменения, включая автора, коммитера, дату и сообщение журнала. Каждый коммит указывает на свой объект дерева в одном полном снимке. Корневой коммит родителя не имеет, а у большинства коммитов есть один родительский. Теги Обычно присваивается коммиту, чтобы тот имел человекочитаемое имя, а не только набор цифр и букв. Все эти объекты являются неизменяемыми, но есть НО. Теги могут быть аннотированными и легковесными, так вот аннотированный может меняться. Каждый объект в этом хранилище связан с уникальным именем, которое вычисляется путём примененения к содержимому объекта алгоритма SHA-1. Малейшее изменение файла ведёт к изменению хэша и как итог новая версия индексируется отдельно. В итоге получается такой стек:
- BLOB-объект находится в основании. Он не ссылается ни на что, а просто есть;
- Деревья указывают на BLOB-объекты и, при необходимости, на другие деревья;
- Коммит указывает на одно конкретное дерево;
- Тег указывает максимум на один коммит.
Индекс Или каталог индексирования, сохраняет двоичные данные и является приватным для репозитория. Содержимое индекса временное и описывает структуру репозитория в заданный момент времени. Обновление происходит в момент выполнения команды git add. Файлы packfile Git не хранит всё содержимое каждой версии файла, это было бы слишком затратно. Вместо этого используется механизм под названием pack-файл для которого используется алгоритм DEFLATE (Сначала данные обрабатываются алгоритмом LZ77, который заменяет повторяющиеся последовательности ссылками на их предыдущее появление. Затем полученный результат сжимается алгоритмом Хаффмана для уменьшения размера кода). То есть в каждой следующей версии сохраняется разница между файлами. Часть 3. Как это работает Если мы создадим пустую директорию и инициализируем её с помощью git init, то сожержимое её будет таким
tree .git .git ├── config ├── description ├── HEAD ├── hooks │ ├── applypatch-msg.sample -> /usr/share/git-core/hooks/applypatch-msg │ ├── commit-msg.sample -> /usr/share/git-core/hooks/commit-msg │ ├── post-update.sample -> /usr/share/git-core/hooks/post-update │ ├── pre-applypatch.sample -> /usr/share/git-core/hooks/pre-applypatch │ ├── pre-commit.sample -> /usr/share/git-core/hooks/pre-commit │ ├── pre-merge-commit.sample -> /usr/share/git-core/hooks/pre-merge-commit │ ├── prepare-commit-msg.sample -> /usr/share/git-core/hooks/prepare-commit-msg │ ├── pre-push.sample -> /usr/share/git-core/hooks/pre-push │ ├── pre-rebase.sample -> /usr/share/git-core/hooks/pre-rebase │ ├── pre-receive.sample -> /usr/share/git-core/hooks/pre-receive │ ├── push-to-checkout.sample -> /usr/share/git-core/hooks/push-to-checkout │ ├── sendemail-validate.sample -> /usr/share/git-core/hooks/sendemail-validate │ └── update.sample -> /usr/share/git-core/hooks/update ├── info │ └── exclude ├── objects │ ├── info │ └── pack └── refs ├── heads └── tags
Далее мы можем создать файл hello.txt с содержиммым hello и проиндексировать его
echo hello > hello.txt git add .
В итоге в .git у нас появится новый BLOB-объект
├── objects │ ├── ce │ │ └── 013625030ba8dba906f756967f9e9ca394464a
Узнать, что это именно наш файл можно с помощью команды
echo "hello" | git hash-object --stdin ce013625030ba8dba906f756967f9e9ca394464a
Превращение первого байта хэша в каталог создаёт фиксированное 256-стороннее пространство имён с равным распределением для всех возможных объектов
Также можем посмотреть что у нас есть в индексе
git ls-files -s 100644 ce013625030ba8dba906f756967f9e9ca394464a 0 hello.txt
Сразу видно связь BLOB и файла. Также для наглядности можно выполнить снимок состояния индекса и сохранение его в дереве
git write-tree aaa96ced2d9a1c8e72c56b253a0e2fe78393feb7
Отображение в списке файлов сразу меняется
├── objects │ ├── aa │ │ └── a96ced2d9a1c8e72c56b253a0e2fe78393feb7 │ ├── ce │ │ └── 013625030ba8dba906f756967f9e9ca394464a │ ├── info │ └── pack
И содержимое дерева можно также посмотреть
git cat-file -p aaa96 100644 blob ce013625030ba8dba906f756967f9e9ca394464a hello.txt
Теперь коммит. Сделаем его также низкоуровневой командой и посмотрим на результат.
echo -n "Commit for hello" | git commit-tree aaa96 6042f3b460685619480f48aacac1f5fa9529f377 git cat-file -p 6042f tree aaa96ced2d9a1c8e72c56b253a0e2fe78393feb7 author Magnus Root 1776848397 +0300 committer Magnus Root 1776848397 +0300 Commit for hello%
Видим и коммит, и дерево. Теперь теги. Как уже упоминалось, они могут быть двух видов - легковесные и аннотированные. Легковесные - это по сути ссылки на объект коммита, они не хранятся как постоянные объекты в хранилище. Обычно применяются когда требуется временная метка. Аннотированные - создают объект. Они содержат сообщение, которое можно подписать цифровым ключом. Используются для создания конкретной версии релиза объекта. Аннотированный неподписанный тег создаётся командой git tag
git tag -a V1.0 6042f # Узнаем его SHA-1 git rev-parse V1.0 db4725b944b0a39a942fe573a6ab69e6edbf94f7 # Проверим git cat-file -p db4725b object 6042f3b460685619480f48aacac1f5fa9529f377 type commit tag V1.0 tagger Magnus Root 1776848847 +0300 Write a message for tag: V1.0
Часть 4. Основы Git Ветки Ветка, она же бранч(branch), позволяет запустить отдельную линию разработки в рамках того же проекта. Как уже упоминалось, операция эта дешёвая, так как ветка - это просто указатель на объект коммита откуда начинать строить новую дельту. Ветки можно использовать для:
- Разных этапов разработки;
- Для новых версий проекта;
- Для фиксов багов и разработки каких-либо фич;
- Для отделения разработчиков.
Чтобы успешно использовать этот инструмент рекомендуется опираться на определённые практики:
- Имена веток не могут начинаться с точки, знака минус, не может включать в себя пробелы;
- Не могут включать в себя две последовательные точки ..;
- Не может включать в себя ~ ^ : ? * [;
- Не может включать в себя управляющий ASCII символ. Основная ветка по умолчанию будет назваться master или main. Поменять это можно при инициализации командой:
git init -b banch-name или git config --global init.defaultBranch branch-name
Переименовать ветку, сохранив историю, можно командой:
git branch -m old-name new-name
Ветки могут быть иерархически вложенными. Вот некоторые команды, для управления ветками:
# Создание ветки git branch features # Просмотр списка веток git branch # Просмотр веток и их коммитов git show-branch ! [features] Change graph * [master] Change graph -- +* [features] Change graph # В выводе над -- отображаются ветки. Знаком * обозначена активная ветка. # В выводе под -- отображаются коммиты. Знак + означает, что коммит есть в перечисленной ветке. Знак *, что коммит есть в текущей ветке. Может быть ещё и знак -, который означает, что это коммит слияния для этой ветки.
Без дополнительных параметров выводятся только ветки в локальном репозитории. Для вывода веток из удалённого нужно добавить опцию -r. А с опцией -a можно вывести и те, и другие. Одновременно можно работать только с одной веткой. Если нужно переключиться на другую, то используется команда
git checkout branch-name # Или если ветки ещё нет, то её можно сразу и создать git checkout -b branch-name
Если в ветке, на которую происходит переключение, содержатся файлы и папки, которых нет в текущей, то они появятся. Наоборот это правило тоже работает. Если файлы есть и там, и там, но с разным содержимым, то оно будет изменяться. Но если в ветке имеются несохранённые данные, то Git не даст переключить ветку, чтобы не потерять их. Если вы уверены, что эти данные не нужны, то можно при переключении использовать опцию -f. Также можно использовать опцию -m, чтобы перенести сделанные изменения в ветку куда происходит переключение.
# Команду checkout можно использовать также для восстановления файлов rm -rf test.txt git checkout -- test.txt # Или для перемещения по коммитам. Например, если нужно 4 коммита назад git checkout dev~4
Удалить ветку можно с помощью опции -d. Но по умолчанию Git не даст удалить ветку, в которой пользователь находится сейчас, и ветку, в которой есть коммиты, которых нет в текущей ветке. Коммиты Коммит - это снимок отражающий текущее состояние репозитория в определённый момент времени. Каждый новый коммит указывает на предыдущий. При создании нового коммита Git сравнивает текущее состояние индекса с предыдущим снимком, определяет список изменений в файлах и каталогах и уже, исходя из этого, создаёт новые BLOB-объекты для всех изменившихся файлов и объекты деревьева для изменившихся каталогов. Ссылки Каждый коммит имеет уникальный шестнадцатиричный ID, то есть хэш SHA1, который является явной ссылкой. Также существует неявная ссылка HEAD, которая всегда указывает на последний коммит. Помимо HEAD есть ещё несколько символических ссылок:
- ORIG_HEAD - используется для восстановления предыдущего состояния, отката или сравнения при операциях слияния;
- FETCH_HEAD - используется для удалённых репозиториев. git fetch записывает вершины всех веток, указанных в файле .git/FETCH_HEAD. Эта ссылка сокращение для вершины последней извлекаемой ветки, оно валидно только после операции извлечения;
- MERGE_HEAD - используется пр слиянии. Вершина другой ветки временно записыватся сюда;
- CHERRY_PICK_HEAD - используется для записи коммитов при выполнении команды git cherry-pick. Данными ссылками можно управлять с помощью git symbolic-ref.
История Просмотреть история коммитов можно с помощью git log. Если не включать в эту команду дополнительные опции, то она будет равнозначна git log HEAD, то есть выведет все коммиты, в обратном направлении, достижимые вдоль графа коммитов. Для просмотра истории ветки рекомендуется явно указывать её и стартовую точку git log test -2. Также можно использовать дополнительные опции для форматирования и указания диапазонов:
- --pretty=short - корректирует количество информации о коммите (может быть oneline, short, medium и full);
- --abbrev-commit - запрашивает сокращение хэша;
- -n - позволяет ограничить размер вывода git log;
- -p - покажет изменения внесённые коммитом;
- --stat - покажет какие файлы изменились и в каком количестве были эти изменения;
- --graph - выводит текстовое представление истории коммитов;
- main~9..main~7 - показывает коммиты ветки main между 9 и 7.
Управление файлами и индексом Индекс можно рассматривать как кэш текущего состояния рабочего каталога. В него заносится то, что будет помещено в коммит. Увидеть состояние индекса на текущий момент можно командой git status. Эта команда явно сообщит какие файлы проиндексированы. Для этого Git сравнит виртуальное состояние дерева с рабочим деревом и покажет различия. Также можно использовать команды git diff(покажет отличия между текущей и прошлой версией файла) и git diff --cached(покажет какие изменения будут участвовать в следующем коммите). Git делит файлы на три группы:
- Tracked (отслеживаемые) - файл уже находящийся в репозитории или внесйнный в индекс. Добавляется через git add filename или git add .;
- Ignored (игнорируемые) - файлы, которые не должны попасть в индекс, например файлы конфигурации, собранные бинарники или заметки. Указываются в .gitignore;
- Untracked (неотслеживаемые) - файлы, которые не входят в первые две категории.
# Чтобы внести все отслеживаемые файлы в индекс можно использовать команду git commit -a # но это работает только при изменении файла, но не удалении или перемещении.
Команда git rm делает ровно портивоположное git add, а именно удаляет файл или директорию из отслеживаемых и удаляет сам файл. Поэтому если его нужно сохранить, то используем git rm --cached. Если же файл нужно переименовать или перенести, то используем git mv. Про .gitignore Несколько правил для данного файла:
- Пустые строки игнорируются;
- Обычное имя файла соответствует имени файла в каталоге;
- Если имя оканчивается слешем, то оно читается как каталог;
- Поддерживаются шаблоны с подстановочными символами, например со *;
- ! в начале строки инвертирует её и имеет приоритет над всем остальным.
Слияние В Git слияние обозначает объединение двух или более историй коммитов веток. Выполняется командой git merge. Слияние может проходить без проблем, но может и приводить к конфликтам. Обычно это происходит когда в ветку куда применяются изменения уже были внесены каки-то данные и они могут быть потеряны. Чтобы понять в чём проблема нужно использовать команду git diff. Некоторые конфликты могут быть разрешены автоматически, но далеко не все. Если конфликтующих файлов много, то можно использовать команду git status или git ls-files -u для вывода набора файлов, которые ещё не объединены.git status покажет что нужно сделать для разрешения конфликта. Всю информацию о конфликтах Git держит в нескольких метсах:
- .git/MERGE_HEAD - содержит хэш коммита, в который происходит слияние;
- .git/MERGE_MSG - содержит сообщение о слиянии;
- Индекс Git содержит три копии файла (базовую, нашу версию, и версию конфликтующих изменений). Если конфликт не удалось разрешить и есть желание вернуться к исходному состоянию, то поможет команда git checkout -m.
Если операция слияния начата, но нужно её отменить, то выолняется команда git merge --abort. Если слияние нужно отменить после его завершения (то есть после завершения коммита), то нужно выполнить git reset --hard ORIG_HEAD. Часть 5. Базовые навыки в Git Работа с коммитами Поиск коммитов git bisect Представим ситуацию, что наша утилита, которую мы активно разрабатываем и храним в Git перестала работать как нужно и произошло это не в последнем коммите, а где-то раньше, но внимание не обратили. Найти виновника поможет команда git bisect. Это инструмент для выделения конкретного коммита на основе произвольного критерия поиска. Например, последняя версия не работает, укажем это git bisect bad, а версия 1.5 работала правильно git bisect good v1.5. В диапазоне между двумя этими коммитами и будет идти вся работа. Будет показан коммит, для которого нужно указать рабочий он или уже нет. При каждом запросе пространство поиска будет сужаться вдвое, пока не останется только виновник. С помощью git bisect log можно посмотреть как проходил поиск. git blame Эта команда сообщает кто последним изменял каждую отдельную строку файла и какой коммит это изменение внёс. Опция -L позволяет указать диапазон поиска, например git blame -L start-line-no, end-line-no. Также можно посмотреть кто вносил изменения в файл в определённый промежуток времени, например git blame --since=1.weeks -- hello.txt pickaxe Если добавить в команде git log опцию -S, то получится инструмент, который выполнить грубый поиск в коммитах по слову. Например, git log -Sinclude покажет все коммиты, в которых содержалось слово include, но количество добавления и удаления этого слова не должно совпадать в одном коммите. Изменение коммитов amend Если нужно внести изменения в последний коммит, например исправить опечатку, то можно выполнить команду git commit --amend. git revert Команда git revert commit_sha1 выполняет инверсию указанного коммита. Это действие не изменяет историю, а добавляет в неё новый коммит. git reset У данной команды есть три опции - soft,hard и mixed. git reset --soft commit переадресует HEAD на указанный коммит, сохранив содержимое и рабочего каталога, и индекса. git reset --mixed commit сделает то же, что и soft, но изменит индекс. Именно этот режим используется в git reset по умолчанию. git reset --hard commit не сохраняет даже содержимое каталога. Если нужно отменить сделанный коммит, но при этом сохранить данные, то поможет команда git reset --soft HEAD. Если данные не нужны, то используем опцию --hard вместо --soft. А если коммит был уже отправлен в удалённый репозиторий, то следом вводим git push -f git cherry-pick Данная команда применяет изменения, внесённые именованным коммитом в текущей ветке. Она добавляет новый коммит в историю. Данная команда работает с помощью git diff, а значит нужно будет, при необходимости, разрешать конфликты. Типичным применением cherry-pick является выборочный отбор коммитов из одной ветки в другу. git rebase Используется для изменения последовательности коммитов и применяется для поддержания актуальности находящихся в разработке коммитов относительно другой ветки, например master. Так команда git rebase master features перенесёт все недостающие коммиты в ветку features, это называется прямым переносом. Если ваша ветка features основана на ветке test, но её нужно перенести на master, то это можно сделать выполнив git rebase --onto master features. Локальное хранилище и журнал ссылок Локальное хранилище, или stash - это способ фиксировать текущую работу. Представим, что появляется веская причина остановить разработку одной фичи и переключиться на другую, то делается просто git stash push или git stash, или git stash -m "WIP". Это сохранит текущий индекс и состояние рабочего каталога, а после очистит их для работы над новым функционалом. Когда появится необходимость вернуться к работе над отложенным ранее, то выполняется команда git stash pop, которая восстановит весь контекст. Это может в некотрых случаях к конфликтам, которые будет необходимо разрешать и после разрешения применить git stash drop для очистки хранилища… Если сохранённое состояние удалять из хранилища не нужно, то можно использовать git stash apply. Просмотреть стек сохранённых контекстов можно с помощью команды git stash list. Также может возникнуть ситуация, когда нужно проверить какие опреации выполнялись в Git. В этом может помочь команда git reflog show. Данный журнал хранит следующую информацию:
- клонирование;
- отправка изменений;
- создание новых коммитов;
- изменение и создание веток;
- перебазирование;
- сброс. Данный журнал есть только у непустых репозиториев, то есть у тех, которые имеют рабочий каталог.
Журнал ссылок не разрастается до бесконечности. По умолчанию срок жизни коммита, который не имеет ссылок и недостижим, истекает через 30 дней. Срок жизни достижимых - 90 дней. Удалённые репозитории Хранить данные только локально неудобно для командной работы и небезопасно. Для решения этих проблем используются удалённые репозитории. Если необходимо скачать себе чей-то репозиторий, то используется команда git clone. Также эта команда позволяет сделать клон локального репозитория git clone hello new_hello. По умолчанию каждый клон сохраняет ссылку на родительский репозиторий через подключение с именем origin. Помимо клонирования доступны операции:
- git fetch - извлекает из удалённого репозитория объекты и метаданные;
- git pull - то же, что и fetch, но также вливает изменения в локальную ветку. По сути состоит из git fetch и git merge или git rebase;
- git push - переносит объекты и метаданные в удалённый репозиторий;
- git ls-remote - показывает список ссылок вместе с хэшами их коммитов в удалённом репозитории. Позволяет определить доступно ли обновление.
Если нужно подключить локальный репозиторий к удалённому, то сделать это несложно
git remote add origin url
Вместо origin может быть любое имя. Для разрыва связи с репозиторием можно использовать команду git remote rm. А команда git remote update позволит скачать в локальный репозиторий все доступные обновления. Заключение Пожалуй этого достаточно, чтобы можно было понимать как работает Git и успешно работать в нём. Но не стоит забывать, что это лишь базис и есть множество различных опций, лучших практик и команд, которые могут ускорять и улучшать работу с этим инструментом. Помимо этого Git постоянно развивается, но базовые функции остаются неизменными и для болшего числа пользователей вполне достаточными. Также здесь не учтены вопросы создания репозитория, работа с GitHub и прочими подобными сервисами, но это уже отдельные темы заслуживающие отдельных статей.-Источник
|