Skip to content

ololobster/git-notes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 

Repository files navigation

Заметки по Git

Оригинал — github.com/ololobster/git-notes.

  1. Введение
  2. Работа с репозиториями: настройки, удалённые репозитории, дочерние репозитории
  3. Работа с ветками: merge vs rebase
  4. Работа с коммитами: reflog
  5. Работа с тегами
  6. Анализ истории
  7. Внутреннее устройство: объекты, прочее
  8. Подходы к выпуску релизов: GitHub flow, GitLab flow, Git flow
  9. 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⟩

Примечания:

  1. Можно не указывать каталог, тогда будет использовано название исходного репа.
  2. Если клонируем другой локальный реп (т.е. ⟨repo⟩ — это каталог), то используем --no-hardlinks.
  3. Можно сразу переключится на нужную ветку при помощи -b ⟨branch⟩.

Создать локальный репозиторий с нуля:

$ git init ⟨target directory⟩

Примечания:

  1. Каталог под реп будет создан, если его не существует.
  2. Можно не указывать каталог, тогда будет использован текущий.

Настройки

Вывести/изменить значение параметра для текущего репа (хранится в .git/config):

$ git config ⟨param⟩
$ git config ⟨param⟩ ⟨value⟩

Вывести/изменить глобальное значение параметра (хранится в ~/.gitconfig):

$ git config --global ⟨param⟩
$ git config --global ⟨param⟩ ⟨value⟩

Вывести все настройки:

$ git config --list --show-origin

Некоторые параметры:

  1. user.email и user.name — информация по автору.
  2. core.autocrlf (true, false или input) — надо ли править переводы строк в зависимости от ОС. Просто используем false в сочетании с UTF-8 и \n.
  3. 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

Добавить дочерний репозиторий:

  1. Перейти в нужную папку.
  2. $ git submodule add ⟨repo⟩
    

Работа с ветками

Ветка — это просто указатель на коммит, который является кончиком этой ветки. У ветки под названием ⟨branch⟩ может быть 3 версии:

  1. Ветка ⟨branch⟩ в удалённом репе.
  2. Tracking-ветка под названием ⟨repo name⟩/⟨branch⟩, находящаяся в локальном репе. Это копия ветки в удалённом репе.
  3. Локальная ветка ⟨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⟩

Примечания:

  1. В локальном репе появится (обновится) tracking-ветка под названием ⟨repo name⟩/⟨branch⟩.
  2. При этом локальная ветка ⟨branch⟩ не обновится.
  3. Если локальной ветки ⟨branch⟩ ещё нет — то и не появится. Для этого ещё надо сделать git checkout ⟨branch⟩.

Скачать из удалённого репа ветку, которой ещё нет в локальном репе, создать соответствующую локальную ветку и переключиться на неё:

$ git checkout -b ⟨branch⟩ ⟨repo name⟩/⟨branch⟩

Допустим, мы накоммитили в локальную ветку ⟨branch⟩ какую-то ерунду. Надо выпилить эти коммиты и привести ветку к состоянию как на удалённом репе. На этой ветке выполняем:

$ git fetch ⟨repo name⟩ ⟨branch⟩
$ git reset --hard ⟨repo name⟩/⟨branch⟩

Если отправили неправильный коммит в удалённый реп:

  1. Правим историю (например, при помощи git commit --amend или git rebase -i).
  2. Перезаписываем ветку в удалённом репе:
    $ git push --force ⟨repo name⟩ ⟨branch⟩
    

Примечание: НЕ надо так делать, если с этой веткой работают другие люди (см. золотое правило rebase).

Стащить коммит из другой ветки:

$ git cherry-pick ⟨commit⟩

Примечания:

  1. В текущей ветке будет создана копия коммита, содержащая те же изменения.
  2. В gitk можно жмакнуть правой кнопкой на какой-нибудь коммит и выбрать в меню «Cherry-pick this commit».

Merge vs rebase


Походы к слиянию веток: merge-коммит vs rebase + fast-forward merge. У merge-коммита 2 родителя вместо одного. При fast-forward merge кончик ветки просто переставляется на другой коммит, новых коммитов не создаётся.

Залить ветку ⟨branch⟩ в текущую ветку:

$ git merge ⟨branch⟩

Примечания:

  1. --ff повелевает использовать fast-forward merge если это возможно. Это поведение по умолчанию.
  2. --no-ff — создать merge-коммит, даже если возможен fast-forward merge.
  3. --ff-only — либо fast-forward merge либо никак.

Выполнить rebase — взять из текущей ветки коммиты, которых нет в ⟨branch⟩, и прикрепить их к кончику ⟨branch⟩ (см. картинку выше):

$ git rebase ⟨branch⟩

Примечания:

  1. Теперь текущая ветка растёт прямо из кончика ⟨branch⟩, можно делать fast-forward merge.
  2. Можно предварительно посмотреть какие коммиты будут затронуты:
    $ git log ⟨branch⟩..HEAD
    
  3. -i ака --interactive повелевает запустить интерактивный rebase, чтобы причесать историю.
  4. По умолчанию выбранные коммиты нанизываются на всё ту же ветку ⟨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

Примечания:

  1. -a ака --all повелевает перенести все модификации отслеживаемых файлов в staging area, чтобы они тоже попали в коммит.
  2. --amend повелевает перезаписать предыдущий коммит (его изменения сохранятся, к ним добавятся новые).

Удалить отслеживаемый файл:

$ git rm ⟨file⟩

Примечания:

  1. Файл будет удалён из рабочего древа + его удаление будет занесено в staging area.
  2. Можно просто удалить файл из рабочего древа, а потом вызвать git commit с -a.
  3. При помощи шаблона можно выпилить сразу много файлов. Пример: 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

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⟩

Примечания:

  1. Если была просто изменена строка, куда входит ⟨piece of code⟩, то такой коммит не считается.
  2. Можно скормить целый блок кода в несколько строк, чтобы найти коммит, в котором этот блок был добавлен.

Вывести 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⟩

Примечания:

  1. Можно указать tracking-ветку, например, origin/debian.
  2. Если git diff заменить на git log, то выдаст ту же разницу в виде списка коммитов.

Внутреннее устройство

Объекты

Единицей хранения данных является объект, который идентифицируется 40-символьным хешем SHA1. Т.е. Git является key-value хранилищем. Объекты хранятся в .git/objects.

Типы объектов:

  1. BLOB — это содержимое файла (ни имени файла, ни прав доступа тут нет). Если добавить в репозиторий 2 копии одного файла, то BLOB-объект будет один, т.к. хеш SHA1 одинаковый.
  2. Tree — это каталог, т.е. список дочерних файлов и каталогов, их прав и соответствующих им BLOB-объектов.
  3. Коммит, имеющий следующие свойства:
    • tree — корневой каталог репозитория (коммит не хранит дельту);
    • parent — предшествующий коммит (2 шт. если это merge-коммит);
    • author, committer — информация по автору;
    • message.
  4. Тег.

Узнать тип объекта:

$ git cat-file -t ⟨id⟩

Пример создания коммита без высокоуровневых команд git add и git commit:

  1. Создаём 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
    
  2. Добавляем в корневой каталог новый файл 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
    
  3. Создаём коммит на основе нового дерева 6fd05838 (предшествующий коммит — 3b64db71):
    $ git commit-tree 6fd05838 -p 3b64db71 -m 'test'
    73008467263c517658273f774b992ac135d019a6
    $ git cat-file -t 73008467
    commit
    
  4. Переставляем кончик ветки main на новый коммит:
    $ git update-ref refs/heads/main 73008467
    
  5. 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 — объект хранится в виде дельты относитьно другого объекта (эта дельта также сжимается).

Подходы к выпуску релизов

GitHub flow

Работа над фичами идёт в отдельных ветках. Ветка main всегда должна оставаться стабильной. Если что-то залито в main, то можно и должно деплоить новую версию (по заветам continuous delivery).

Очень простая схема. Подойдёт для SaaS-приложения, которое деплоится каждый день или чаще.

См. также:

  1. Understanding the GitHub flow.
  2. GitHub Flow — Scott Chacon.

GitLab flow

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 с ветками, соответствующими релизам.

См. также:

  1. Introduction to GitLab Flow.

Git flow

Используемые ветки:

  1. develop предназначена для разработки нового релиза.
  2. main всегда должна оставаться супер-стабильной. Любая заливка в main означает выпуск новой версии, соответствующий коммит помечается тегом.
  3. Работа над новыми фичами идёт в отдельных ветках, они вливаются в develop.
  4. Когда релиз почти готов, из develop отпочковывается специальная ветка, где его будут тестировать и шлифовать (в develop продолжается работа над следующим релизом). Когда релиз готов, эта ветка вливается в main.
  5. Ветки для оперативного исправления серьёзных багов, которые вливаются и в main и в develop.


Git flow.

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

См. также:

  1. A successful Git branching model — Vincent Driessen.

GitLab CI/CD

CD — это когда ты только пушнул, а у клиента уже всё упало?

GitLab CI/CD предоставляет средства для сборки, тестирования и деплоя приложений в автоматическом режиме. Для каждого нового изменения запускается pipeline, в pipeline может быть несколько стадий (stage), а в каждой стадии — несколько задач (job). Runner — это процесс, который выполняет задачи. Можно использовать runner'ы, предоставленные GitLab, а можно поднять runner'ы на собственной инфраструктуре.

Чтобы настроить CI/CD надо:

  1. Выбрать runner'ы в настройках проекта на GitLab. При необходимости — поднять свои runner'ы.
  2. Настроить стадии и задачи, положив в корень проекта файл .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:

  1. Если залить в реп сразу цепочку из нескольких коммитов, то pipeline запустится лишь для последнего коммита.
  2. Pipeline создаются не только для новых коммитов но и для новых тегов. Например, если залить в реп 1 коммит и тег к нему, то для одного и того же коммита будет запущено 2 pipeline:
    • pipeline для коммита, при этом CI_COMMIT_REF_NAME — это название ветки;
    • pipeline для тега, при этом CI_COMMIT_REF_NAME — это название тега.

Заметки по запуску задач:

  1. При помощи секции rules можно задать условия создания задачи. GitLab обходит условия по порядку до 1-го сработавшего, далее задача либо создаётся, либо нет (если для этого условия указано when: never).
  2. when: manual позволяет создать задачу, которую можно запустить только вручную.
  3. По умолчанию задача запустится лишь после того, как успешно завершатся задачи на предыдущей стадии. Но можно явно указать для задачи зависимости при помощи 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'ов

Виды runner'ов:

  1. Shared (например, runner'ы, предоставляемые GitLab).
  2. Групповые — для конкретной группы (доступен для дочерних проектов и групп).
  3. Specific — для конкретного проекта.

По состоянию на октябрь 2021, можно расшарить specific-runner, убрав галочку ``Lock to current projects''. Это единственный адекватный способ использовать тот же runner для персональных форков разработчиков.

Вывести список runner'ов, запущенных на ЭВМ:

$ gitlab-runner list

Запустить интерактивное создание runner'а:

# gitlab-runner register

Примечания:

  1. Настройки живут в /etc/gitlab-runner/config.toml.
  2. По состоянию на октябрь 2021, нельзя просто обновить токен в config.toml и перезапустить сервис gitlab-runner, чтобы перевести runner на другой проект. Надо выпиливать runner и создавать по новой.

Файлы .bashrc и .bash_logout могут выполняться в рамках задачи. Если задача падает на «Prepare environment», то можно начать с выпиливания .bash_logout.

Прочее

Клонировать с аутентификацией по токену:

$ git clone https://gitlab-ci-token:⟨token⟩@gitlab.com/⟨project⟩

About

Заметки по Git для самых маленьких.

Topics

Resources

License

Stars

Watchers

Forks