Оригинал — github.com/ololobster/git-notes.
- Введение
- Работа с репозиториями: настройки, удалённые репозитории, дочерние репозитории
- Работа с ветками: merge vs rebase
- Работа с коммитами: reflog
- Работа с тегами
- Анализ истории
- Внутреннее устройство: объекты, прочее
- Подходы к выпуску релизов: GitHub flow, GitLab flow, Git flow
- GitLab CI/CD: настройка задач, настройка runner'ов
Git — это распределённая система контроля версий (VCS ака version control system, SCM ака source code management) от Линуса Торвальдса, созданная для работы над ядром Linux. Распределённость означает, что все копии проекта (репозитории) равноправны и могут невозбранно подключаться друг к другу, чтобы скачать или отправить изменения. На практике один из репов назначается главным, и через него члены команды обмениваются результатами своей работы.
История изменений представляет собой ориентированный граф из коммитов. Каждый коммит — это снимок (snapshot) файлов и каталогов проекта.
Локальный реп — это каталог, в котором лежит дочерний каталог .git
со служебными файлами Git.
Рабочее дерево (working tree) — это файлы и каталоги в локальном репе, всё кроме .git
.
Зная ид коммита, можно выгрузить соответствующий снимок в рабочее дерево:
$ git checkout ⟨commit⟩
Рабочее дерево этого репозитория.
Staging area ака index — это новые файлы и модификации отслеживаемых файлов, которые помечены (при помощи git add
) для внесения в следующий коммит.
3 области локального репа. Staging area и история изменений живут в .git
.
HEAD
— это указатель на коммит, который сейчас выгружен в рабочее древо.
Вывести ид коммита, на который указывает HEAD
:
$ git rev-parse HEAD
Git предусматривает для файлов лишь 3 варианта прав: 100644
(обычный файл), 100755
(выполняемый файл), 120000
(символьная ссылка).
Создать локальную копию другого репа:
$ git clone ⟨repo⟩ ⟨target directory⟩
Примечания:
- Можно не указывать каталог, тогда будет использовано название исходного репа.
- Если клонируем другой локальный реп (т.е.
⟨repo⟩
— это каталог), то используем--no-hardlinks
. - Можно сразу переключится на нужную ветку при помощи
-b ⟨branch⟩
.
Создать локальный репозиторий с нуля:
$ git init ⟨target directory⟩
Примечания:
- Каталог под реп будет создан, если его не существует.
- Можно не указывать каталог, тогда будет использован текущий.
Вывести/изменить значение параметра для текущего репа (хранится в .git/config
):
$ git config ⟨param⟩
$ git config ⟨param⟩ ⟨value⟩
Вывести/изменить глобальное значение параметра (хранится в ~/.gitconfig
):
$ git config --global ⟨param⟩
$ git config --global ⟨param⟩ ⟨value⟩
Вывести все настройки:
$ git config --list --show-origin
Некоторые параметры:
user.email
иuser.name
— информация по автору.core.autocrlf
(true
,false
илиinput
) — надо ли править переводы строк в зависимости от ОС. Просто используемfalse
в сочетании с UTF-8 и\n
.init.defaultBranch
— название ветки по умолчанию. Обычноmaster
илиmain
.
Удалённый реп (remote, upstream) — это другая копия проекта, к которой может подключаться локальный реп. Может находиться на удалённом сервере (например, на GitHub или на GitLab), а может — в соседнем каталоге на той же ЭВМ.
Вывести список удалённых репов:
$ git remote -v
Добавить удалённый реп:
$ git remote add ⟨repo name⟩ ⟨repo URL⟩
Изменить URL удалённого репа:
$ git remote set-url ⟨repo name⟩ ⟨repo URL⟩
Выпилить удалённый реп:
$ git remote remove ⟨repo name⟩
Клонировать репозиторий и сразу подтянуть его дочерние репозитории:
$ git clone --recurse-submodules ⟨repo⟩
Подтянуть дочерние репозитории:
$ git submodule update --init --recursive
Добавить дочерний репозиторий:
- Перейти в нужную папку.
-
$ git submodule add ⟨repo⟩
Ветка — это просто указатель на коммит, который является кончиком этой ветки.
У ветки под названием ⟨branch⟩
может быть 3 версии:
- Ветка
⟨branch⟩
в удалённом репе. - Tracking-ветка под названием
⟨repo name⟩/⟨branch⟩
, находящаяся в локальном репе. Это копия ветки в удалённом репе. - Локальная ветка
⟨branch⟩
, куда мы коммитим.
Все 3 версии могут совпадать, а могут и отличаться (если в локальную и в удалённую ветку были добавлены новые коммиты).
Вывести список локальных веток:
$ git branch
Типовой сценарий — создать новую локальную ветку, переключиться на неё, сделать что-то хорошее, отправить эту ветку на удалённый реп origin
для последующего оформления merge-реквеста:
$ git branch ⟨branch⟩
$ git checkout ⟨branch⟩
...
$ git push origin ⟨branch⟩
Создать новую локальную ветку от конкретного коммита:
$ git branch ⟨branch⟩ ⟨commit⟩
Удалить локальную ветку, которая уже залита в удалённый реп:
$ git branch -d ⟨branch⟩
Удалить локальную ветку, которая ещё не залита в удалённый реп:
$ git branch -D ⟨branch⟩
git fetch
скачивает изменения, git merge
сливает ветки, git pull
= git fetch
+ git merge
.
Скачать ветку из удалённого репа:
$ git fetch ⟨repo name⟩ ⟨branch⟩
Примечания:
- В локальном репе появится (обновится) tracking-ветка под названием
⟨repo name⟩/⟨branch⟩
. - При этом локальная ветка
⟨branch⟩
не обновится. - Если локальной ветки
⟨branch⟩
ещё нет — то и не появится. Для этого ещё надо сделатьgit checkout ⟨branch⟩
.
Скачать из удалённого репа ветку, которой ещё нет в локальном репе, создать соответствующую локальную ветку и переключиться на неё:
$ git checkout -b ⟨branch⟩ ⟨repo name⟩/⟨branch⟩
Допустим, мы накоммитили в локальную ветку ⟨branch⟩
какую-то ерунду.
Надо выпилить эти коммиты и привести ветку к состоянию как на удалённом репе.
На этой ветке выполняем:
$ git fetch ⟨repo name⟩ ⟨branch⟩
$ git reset --hard ⟨repo name⟩/⟨branch⟩
Если отправили неправильный коммит в удалённый реп:
- Правим историю (например, при помощи
git commit --amend
илиgit rebase -i
). - Перезаписываем ветку в удалённом репе:
$ git push --force ⟨repo name⟩ ⟨branch⟩
Примечание: НЕ надо так делать, если с этой веткой работают другие люди (см. золотое правило rebase).
Стащить коммит из другой ветки:
$ git cherry-pick ⟨commit⟩
Примечания:
- В текущей ветке будет создана копия коммита, содержащая те же изменения.
- В
gitk
можно жмакнуть правой кнопкой на какой-нибудь коммит и выбрать в меню «Cherry-pick this commit».
Походы к слиянию веток: merge-коммит vs rebase + fast-forward merge.
У merge-коммита 2 родителя вместо одного.
При fast-forward merge кончик ветки просто переставляется на другой коммит, новых коммитов не создаётся.
Залить ветку ⟨branch⟩
в текущую ветку:
$ git merge ⟨branch⟩
Примечания:
--ff
повелевает использовать fast-forward merge если это возможно. Это поведение по умолчанию.--no-ff
— создать merge-коммит, даже если возможен fast-forward merge.--ff-only
— либо fast-forward merge либо никак.
Выполнить rebase — взять из текущей ветки коммиты, которых нет в ⟨branch⟩
, и прикрепить их к кончику ⟨branch⟩
(см. картинку выше):
$ git rebase ⟨branch⟩
Примечания:
- Теперь текущая ветка растёт прямо из кончика
⟨branch⟩
, можно делать fast-forward merge. - Можно предварительно посмотреть какие коммиты будут затронуты:
$ git log ⟨branch⟩..HEAD
-i
ака--interactive
повелевает запустить интерактивный rebase, чтобы причесать историю.- По умолчанию выбранные коммиты нанизываются на всё ту же ветку
⟨branch⟩
, но при помощи--onto ⟨target branch⟩
можно выбрать другую целевую ветку (см. пример ниже).
Пример rebase с заданием целевой ветки: на new-feature
всё готово (надо заливать в main
), но на родительской ветке feature
ещё идёт работа.
Находясь на new-feature
выполнить:
$ git log --pretty=format:"%h" feature..HEAD
c9d7dde
dbb5c68
$ git rebase --onto main feature
Пример rebase с заданием целевой ветки.
Можно запустить интерактивный rebase на локальной ветке (просто чтобы причесать историю):
$ git rebase -i ⟨commit⟩
Примечание: под rebase попадут все коммиты начиная с ⟨commit⟩
НЕвключительно.
Профит от rebase — это красивая история в одну линию.
git rebase
создаёт абсолютно новые коммиты, копируя изменения, которые содержатся в старых коммитах.
Отсюда вытекает золотое правило rebase: НЕ делай rebase на общей ветке, которая существует в удалённом репе и используется другими людьми.
Если у разных людей в их локальных репах будут разные истории для одной и той же ветки, то это приведёт к геморрою при merge и бессмысленным кольцам в истории.
Вывести изменения в рабочем древе относительно HEAD
:
$ git diff
Примечание: изменения, находящиеся в staging area, не будут отображены.
Вывести содержимое staging area:
$ git diff --staged
Примечание: --staged
= --cached
.
Создать коммит из того, что находится в staging area:
$ git commit
Примечания:
-a
ака--all
повелевает перенести все модификации отслеживаемых файлов в staging area, чтобы они тоже попали в коммит.--amend
повелевает перезаписать предыдущий коммит (его изменения сохранятся, к ним добавятся новые).
Удалить отслеживаемый файл:
$ git rm ⟨file⟩
Примечания:
- Файл будет удалён из рабочего древа + его удаление будет занесено в staging area.
- Можно просто удалить файл из рабочего древа, а потом вызвать
git commit
с-a
. - При помощи шаблона можно выпилить сразу много файлов.
Пример:
log/\*.log
.
Если по ошибке занесли неотслеживаемый файл в staging area, но ещё не закоммитили:
$ git rm --cached ⟨file⟩
Сбросить все изменения (и в staging area и в рабочем древе):
$ git checkout HEAD -f
Сбросить все изменения файла (и в staging area и в рабочем древе):
$ git checkout HEAD -- ⟨file⟩
Примечание: также поможет если по ошибке вызвали git rm
для отслеживаемого файла, но ещё не закоммитили.
Удалить из рабочего древа неотслеживаемые файлы и каталоги:
$ git clean -f -d
Примечание: -n
повелевает сделать холостой проход и вывести что будет удалено.
Reflog (reference log) — это записи о перемещениях кончиков веток и HEAD
.
Например, HEAD@{2}
означает «куда HEAD
указывал 2 перемещения назад».
Простое переключение с ветки на ветку при помощи git checkout
также добавляет новые записи для HEAD
.
Вывести последние N записей reflog для HEAD
:
$ git reflog show -n ⟨N⟩
Вывести записи reflog для заданной ветки:
$ git reflog show ⟨branch⟩
Git ничего не забывает и не прощает, например, git commit --amend
не перезаписывает последний коммит, а создаёт новый ему на замену (старый остаётся в недрах Git).
Это позволяет много чего отменить.
Отменить последний коммит, если нужно сохранить изменения:
$ git reset --soft HEAD@{1}
Примечание: эти изменения будут лежать в staging area.
Отменить последний коммит, если изменения не нужны:
$ git reset --hard HEAD@{1}
Отменить ошибочное применение git commit --amend
:
$ git reset --soft HEAD@{1}
$ git commit -C HEAD@{1}
Вывести все теги:
$ git tag
Искать теги по шаблону:
$ git tag -l "poppler-20.*"
Вывести последний тег:
$ git describe --abbrev=0 --tags
Создать простой (lightweight) тег на последнем коммите:
$ git tag ⟨tag⟩
Создать простой тег на заданном коммите:
$ git tag ⟨tag⟩ ⟨commit⟩
По умолчанию git push
не заливает теги в удалённый репозиторий.
Для этого надо вызвать:
$ git push ⟨repo name⟩ ⟨tag⟩
Вывести последние N коммитов:
$ git log -n ⟨N⟩
Найти коммиты, где был добавлен/удалён фрагмент кода:
$ git log -S ⟨piece of code⟩
Найти коммиты, где в конкретном файле был добавлен/удалён фрагмент кода:
$ git log -S ⟨piece of code⟩ -- ⟨file⟩
Примечания:
- Если была просто изменена строка, куда входит
⟨piece of code⟩
, то такой коммит не считается. - Можно скормить целый блок кода в несколько строк, чтобы найти коммит, в котором этот блок был добавлен.
Вывести Unix timestamp последнего коммита:
$ git log -n 1 --format=%ct
Вывести изменения коммита:
$ git diff ⟨commit⟩^!
Посмотреть разницу между 2 ветками:
$ git diff ⟨branch 1⟩..⟨branch 2⟩
Посмотреть разницу между 2 ветками для конкретного файла:
$ git diff ⟨branch 1⟩..⟨branch 2⟩ -- ⟨file⟩
Примечания:
- Можно указать tracking-ветку, например,
origin/debian
. - Если
git diff
заменить наgit log
, то выдаст ту же разницу в виде списка коммитов.
Единицей хранения данных является объект, который идентифицируется 40-символьным хешем SHA1.
Т.е. Git является key-value хранилищем.
Объекты хранятся в .git/objects
.
Типы объектов:
- BLOB — это содержимое файла (ни имени файла, ни прав доступа тут нет). Если добавить в репозиторий 2 копии одного файла, то BLOB-объект будет один, т.к. хеш SHA1 одинаковый.
- Tree — это каталог, т.е. список дочерних файлов и каталогов, их прав и соответствующих им BLOB-объектов.
- Коммит, имеющий следующие свойства:
tree
— корневой каталог репозитория (коммит не хранит дельту);parent
— предшествующий коммит (2 шт. если это merge-коммит);author
,committer
— информация по автору;message
.
- Тег.
Узнать тип объекта:
$ git cat-file -t ⟨id⟩
Пример создания коммита без высокоуровневых команд git add
и git commit
:
- Создаём BLOB-объект:
В
$ echo 'my blob' | git hash-object -w --stdin 0b729d77bcd6e98e02910a24d781736df537ef13 $ git cat-file -t 0b729d77 blob $ git cat-file -p 0b729d77 my blob
.git/objects
появился новый файл:$ ls .git/objects/0b 729d77bcd6e98e02910a24d781736df537ef13
- Добавляем в корневой каталог новый файл
my_blob.txt
с правами100644
:$ git update-index --add --cacheinfo 100644 0b729d77bcd6e98e02910a24d781736df537ef13 my_blob.txt $ git write-tree 6fd058384276fe29baced4899f2a06f671989d6e $ git cat-file -t 6fd05838 tree $ git cat-file -p 6fd05838 ... 100644 blob 0b729d77bcd6e98e02910a24d781736df537ef13 my_blob.txt
- Создаём коммит на основе нового дерева
6fd05838
(предшествующий коммит —3b64db71
):$ git commit-tree 6fd05838 -p 3b64db71 -m 'test' 73008467263c517658273f774b992ac135d019a6 $ git cat-file -t 73008467 commit
- Переставляем кончик ветки
main
на новый коммит:$ git update-ref refs/heads/main 73008467
git log
показывает всё корректно, но файлаmy_blob.txt
нету в working tree...$ git checkout HEAD -- my_blob.txt
Ветка — это просто ид коммита, который является кончиком данной ветки, тег — аналогично.
Всё это добро лежит в каталоге каталоге .git/refs
и в файле .git/packed-refs
.
Можно посмотреть этот ид коммита:
$ git rev-parse ⟨branch or tag⟩
Примечание: для tracking-ветки указываем ⟨repo name⟩/⟨branch⟩
.
Текстовый файл .git/HEAD
указывает на текущий коммит.
Если мы на ветке main
, то .git/HEAD
выглядит так:
ref: refs/heads/main
Если мы переключились на конкретный коммит c61f0c0a
, то .git/HEAD
выглядит так:
c61f0c0afdf2ae48db1f0b15cf1cc0023d88c203
Reflog хранится в .git/logs
:
- история
HEAD
— в файле.git/logs/HEAD
, - история ветки — в файле
.git/logs/refs/heads/⟨branch⟩
.
С т.з. пользователя и высокоуровневого API коммит не хранит дельту относительно прошлого коммита.
Но это не значит, что если в большом файле поменять 1 строчку и закоммитить, то в .git
будут храниться 2 полновесные версии этого файла.
На более низком уровне абстракции Git хранит объекты одним из 2 способов:
- undeltified — объект хранится в сжатом виде;
- deltified — объект хранится в виде дельты относитьно другого объекта (эта дельта также сжимается).
Работа над фичами идёт в отдельных ветках.
Ветка main
всегда должна оставаться стабильной.
Если что-то залито в main
, то можно и должно деплоить новую версию (по заветам continuous delivery).
Очень простая схема. Подойдёт для SaaS-приложения, которое деплоится каждый день или чаще.
См. также:
- Understanding the GitHub flow.
- GitHub Flow — Scott Chacon.
1-я схема: усовершенствованный GitHub flow с ветками main
(или development
) и production
.
Новым релизом считается заливание main
в production
.
Сложность: просто.
Удобнее GitHub flow, когда мы не можем мгновенно задеплоить новую версию в любой момент (например, есть deployment windows, есть модерация мобильных приложений в Apple App Store и т.д.), а разработка в main
не должна останавливаться.
2-я схема: каждая ветка соответствует какому-либо окружению.
При обновлении ветки происходит деплой в соответствующее окружение.
Пример: тестовые окружения для веток main
и pre-production
, а ветка production
деплоится на боевые сервера.
GitLab flow с ветками, соответствующими окружениям.
3-я схема: main
предназначена для разработки нового релиза.
Когда релиз пора выпускать, из main
отпочковывается специальная ветка, где его будут тестировать, шлифовать и поддерживать.
Выпуском новой версии считается изменение в релизной ветке (опционально — добавление тега в релизную ветку).
Схема предназначена для ситуаций, когда
- надо долго поддерживать старые версии в работоспособном состоянии, выпуская обновления;
- есть разные вариации продукта, например, для Windows и для Linux, для iOS и для Android.
GitLab flow с ветками, соответствующими релизам.
См. также:
Используемые ветки:
develop
предназначена для разработки нового релиза.main
всегда должна оставаться супер-стабильной. Любая заливка вmain
означает выпуск новой версии, соответствующий коммит помечается тегом.- Работа над новыми фичами идёт в отдельных ветках, они вливаются в
develop
. - Когда релиз почти готов, из
develop
отпочковывается специальная ветка, где его будут тестировать и шлифовать (вdevelop
продолжается работа над следующим релизом). Когда релиз готов, эта ветка вливается вmain
. - Ветки для оперативного исправления серьёзных багов, которые вливаются и в
main
и вdevelop
.
Очень сложно. Может подойти, если есть хорошая документация и команда придерживается месячного или квартального цикла выпуска релизов.
См. также:
- A successful Git branching model — Vincent Driessen.
CD — это когда ты только пушнул, а у клиента уже всё упало?
GitLab CI/CD предоставляет средства для сборки, тестирования и деплоя приложений в автоматическом режиме. Для каждого нового изменения запускается pipeline, в pipeline может быть несколько стадий (stage), а в каждой стадии — несколько задач (job). Runner — это процесс, который выполняет задачи. Можно использовать runner'ы, предоставленные GitLab, а можно поднять runner'ы на собственной инфраструктуре.
Чтобы настроить CI/CD надо:
- Выбрать runner'ы в настройках проекта на GitLab. При необходимости — поднять свои runner'ы.
- Настроить стадии и задачи, положив в корень проекта файл
.gitlab-ci.yml
.
Пример .gitlab-ci.yml
с 2 задачами:
stages:
- test
- build
unit-tests:
stage: test
script:
- echo "Stage 'test', job 'unit-tests'..."
packing:
stage: build
script:
- echo "Stage 'build', job 'packing'..."
GitLab предоставляет кучу переменных окружения, к которым могут невозбранно обращаться задачи.
Например, CI_COMMIT_SHORT_SHA
, CI_JOB_ID
, CI_PIPELINE_ID
, CI_COMMIT_REF_NAME
и т.д.
Можно задавать собственные переменные в .gitlab-ci.yml
:
variables:
LANG: en_US.UTF-8
Также можно задавать переменные в настройках проекта на GitLab.
Заметки по запуску pipeline:
- Если залить в реп сразу цепочку из нескольких коммитов, то pipeline запустится лишь для последнего коммита.
- Pipeline создаются не только для новых коммитов но и для новых тегов.
Например, если залить в реп 1 коммит и тег к нему, то для одного и того же коммита будет запущено 2 pipeline:
- pipeline для коммита, при этом
CI_COMMIT_REF_NAME
— это название ветки; - pipeline для тега, при этом
CI_COMMIT_REF_NAME
— это название тега.
- pipeline для коммита, при этом
Заметки по запуску задач:
- При помощи секции
rules
можно задать условия создания задачи. GitLab обходит условия по порядку до 1-го сработавшего, далее задача либо создаётся, либо нет (если для этого условия указаноwhen: never
). when: manual
позволяет создать задачу, которую можно запустить только вручную.- По умолчанию задача запустится лишь после того, как успешно завершатся задачи на предыдущей стадии.
Но можно явно указать для задачи зависимости при помощи
needs
.
Пример: у нас есть важная задача, которая чего-то куда-то деплоит.
Она должна запускаться только на ветке debian
и только если в настройках данного проекта на GitLab заданы переменные NEXUS_LOGIN
и NEXUS_PASSWORD
.
debian-build:
stage: build
rules:
- if: $NEXUS_LOGIN == null || $NEXUS_PASSWORD == null
when: never
- if: $CI_COMMIT_REF_NAME != "debian"
when: never
- when: always
script:
- echo "Packing and deploying using ${NEXUS_LOGIN} and ${NEXUS_PASSWORD}..."
Виды runner'ов:
- Shared (например, runner'ы, предоставляемые GitLab).
- Групповые — для конкретной группы (доступен для дочерних проектов и групп).
- Specific — для конкретного проекта.
По состоянию на октябрь 2021, можно расшарить specific-runner, убрав галочку ``Lock to current projects''. Это единственный адекватный способ использовать тот же runner для персональных форков разработчиков.
Вывести список runner'ов, запущенных на ЭВМ:
$ gitlab-runner list
Запустить интерактивное создание runner'а:
# gitlab-runner register
Примечания:
- Настройки живут в
/etc/gitlab-runner/config.toml
. - По состоянию на октябрь 2021, нельзя просто обновить токен в
config.toml
и перезапустить сервисgitlab-runner
, чтобы перевести runner на другой проект. Надо выпиливать runner и создавать по новой.
Файлы .bashrc
и .bash_logout
могут выполняться в рамках задачи.
Если задача падает на «Prepare environment», то можно начать с выпиливания .bash_logout
.
Клонировать с аутентификацией по токену:
$ git clone https://gitlab-ci-token:⟨token⟩@gitlab.com/⟨project⟩