Азы Git и Gerrit workflow в проектах OpenStack

Posted on Tue 20 September 2016 in work

"git gets easier once you get the basic idea that branches are homeomorphic endofunctors mapping submanifolds of a Hilbert space" chi wai lau (@tabqwerty)

Осторожно - много англицизмов и моего личного мнения.

Самое Главное

Не лениться и вдумчиво читать то, что git или git-review выводит в командную строку.

Там очень часто весьма внятно описано что пошло или может пойти не так, и какие есть варианты действий, с примерами команд

Git

Чумовая книжечка - Git from the Bottom Up by John Wiegley (CC BY-ND 4.0). Картинки скопированы оттуда, и общая канва повествования немного тоже.

Основные конструкции

кирпичики Гита
  • Blob - объект хранящий “файл”. То, ради чего все затевалось. Определяется своим хэшем, который определяется только размером файла и его содержанием.
  • Tree - содержит ссылки на блобы и другие tree а также метадату для них. Тоже имеет хэш, определяемый хэшами содержимого и метадатой.
  • Commit - содержит одно дерево, и ссылки на родительские коммиты, формируя историю. Тоже имеет хэш, определяемый всем этим - содержанием дерева, коммит мессаджем и хэшами родительских коммитов.

Все остальное опирается на эти три базовых идеи.

Индекс

В отличие от (большинства?) других систем контроля версий в Git реализована двухступенчатая процедура внесения изменений - через Index (он же Staging Area).

основы основ
  • Working area - реальные файлы на файловой системе
  • Index -  изменения которые будут занесены в репозиторий как следующий коммит
  • Repository - хранилище коммитов

Бранчи и Теги

Бранчи и теги - не более чем символические ссылки на commit id имеющие человеко-читаемые имена.

HEAD - специальная ссылка на текущее (последнее чекаутное) состояние Working Tree.

<ref>^ - родитель коммита <ref>

<ref>^^ - второй родитель (есть однозначный типа MRO, но точно не помню какой). В принцие может быть больше двух родителей (^^^...) aka octopus merge, но в жизни пока не встречал/не приходилось делать.

<ref>~N - Nый родитель вдаль по истории

Основное отличие тегов в том что для них нельзя просто переопределить ссылку на коммит - надо только удалить старый тег и создать такой же новый. Но IMnsHO тому кто двигает теги надо отрубать руки.

Remotes

Ремоуты - это специальный вид бранчей, которые указывают на коммиты на удаленном  репозитории (точнее в его локальной копии).

Можно - и часто нужно! - иметь много remotes в своей локальной репе.

git remote -v
git remote add <remote-name> <url>
git fetch <remote-name>

Very Basic git workflow

# создаем новый репозиторий
mkdir <repo> && cd $_ && git init
# или стягиваем готовый
git clone <remote-url> <path> && cd $_
# hack on code
git add [--patch] # заносим изменения в индекс
git commit -m “My commit” # сохраняем индекс как новый коммит
# git push [remote] # выкладываем изменения в удаленный репозиторий

Интерактивный git add --patch позволяет заносить в индекс отдельные куски изменений (hunks), а не весь файл целиком.

Mergе и конфликты

Создание коммита с несколькими родителями.

Часта ситуация когда некоторые файлы отличаются в обоих родителях, и алгоритмы Git (на самом деле весьма продвинутые) не могут однозначно определить, как должна выглядеть их “сумма”. Появляются merge conflicts которые надо чинить.

Ищем в файлах строчки

<<<[some-ref]
# код того куда мержим
====
# код того что примерживаем
>>>[other-ref]

И выбираем какая версия больше нравится. А может и переписываем кусок совсем.

git mergetool
удобная штука, которая из коробки умеет работать со многими редакторами/сравнителями (vimdiff, meld, diffuse, WinMerge, kdiff3 и т.п.). Настраивается через git-config.

Обычно в редакторе будут файлы заканчивающиеся на:

  • BASE - версия файла из ближайшего общего предка
  • LOCAL - то куда мержится
  • REMOTE - то что мержится

Cherry-pick

Берет дифф данного коммита и пытается сделать коммит с таким же диффом поверх другого (по сути делает копию коммита, но с другим родителем). Возможны конфликты.

Rebase

Пересаживает коммит/ветку на нового родителя. Изменяет историю!

git rebase [-i] <target-ref>

Интерактивный ребейз - очень мощная штука. Позволяет выбрать какие коммиты и в каком порядке пересаживать, слепливать несколько коммитов в один, изменять их содержимое и мессаджи.

Конфликты возможны на каждом пересаживаемом патче.

В случае мерж конфликтов при ребейзе LOCAL относится к тому на что ребейзится, а REMOTE - то что ребейзится. Это иногда не интуитивно - например, у вас был старый master, от которого отбранчевана feature-branch. Вы обновили мастер и пытаетесь ребейзнуть свой feature-branch на него. С первого взгляда интуитивно кажется что LOCAL - это ваш код (вот, же он, локальный, еще может даже никуда не выложеный), а REMOTE - это тот код из master, что вы только что выкачали (из удаленного же хранилища, да?...). Надо просто запомнить.

Правки и изменения истории

В git есть несколько команд, которые изменяют историю коммитов, как минимум с точки зрения внешнего мира (список не полный):

  • git commit --amend - дополняет последний коммит изменениями находящимися в индексе. Поскольку id коммита определяется его содержимым, изменяет id коммита
  • git rebase - так как id коммита определяется также и id его родителей, ребейз меняет id всех коммитов попавших под ребейз.

Что тут надо учитывать:

  • Выложить такие изменения в уже существющую удаленную ветку на remote вы уже просто так не сможете - история разъехалась
    • Заставить выложить можно через git push --force
  • Но теперь при этом в вашей удаленной ветке тоже изменилась история. И теперь у любого, кто уже успел скачать через pull/fetch себе вашу удаленную ветку при попытке обновления возникнут проблемы - их "локальная" история разошлась с "апстримной", а это значит мерж-конфликты и прочая головная боль на пустом месте (они-то свой код не трогали..). Не надо так с людьми обходиться.

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

Но что примечательно - есть основаные на git системы, очень активно использующие amend и rebase в своей работе (см Gerrit Workflow в OpenStack).

Tools

Для любителей кнопочек и менюшечек

  • gitk/git-gui - на лицо ужасный (Tcl/Tk), добрый внутри “дефолтный” GUI для гита
    • gitk - браузер истории
    • git-gui - коммиты и проч.
  • gitg - весьма пристойный аскетичный Гуй на Gtk
  • git-cola - тоже неплохо, прикольная визуализация DAG (directed acyclic graph) дерева коммитов
  • SourceTree - для Win/Mac, не открытый но бесплатный, от Atlassian, на Яблоке красивый
  • Ваш IDE - наверное то же что-то есть (PyCharm, Eclipse+PyDev…)

Для ковбоев консоли

  • Git :)
  • tig - браузер, коммитер, диффер и проч на ncurses. Пользуюсь постоянно.
  • Vim плагины (у меня на нем профдеформация)
    • Vim-fugitive - весьма мощная штука, но пока я не очень пользуюсь, только для сложных интерактивных add. Надо переползать плотнее…
    • Vim-gitgutter - помечает добавленые/удаленные/измененные строчки, и может стейжить ханки. Так же интегрируется в vim-airline и показывает общее количество незакоммиченых изменений в открытом файле.

Fun

  • gource - визуализация развития гит репозитория в динамике. Просто красиво :)

    sudo apt install gource && cd <repo> && gource
    

Gerrit Workflow в OpenStack

Gerrit - система код-ревью основанная на Гите.

В cвое время отфоркался от Rietveld написанного Гуидо ван Россумом, создателем Python.

Основной принцип - содержит ченжи, внутри каждого патч-сеты. Каждый патч-сет - это отдельный бранч. Это позволяет вовсю пользоваться rebase и commit --amend, перезаписывая локальную историю и выкладывая ее на remote, что в общем случае очень сильно не рекомендуется (см Правки и изменения истории).

git-review

В принципе c Герритом можно работать через Git напрямую, но с git-review значительно удобнее.

sudo -H pip install -U git-review

Стоит почитать man git-review.

Настраивается через git-config:

$ cat ~/.gitconfig
…
[gitreview]
    username = <my-gerrit-user-name>
    rebase = false

Basic workflow

Change-Id

git-review добавляет пост-коммит хук, который добавляет в коммит-мессадж строчку (если её ещё там не было):

Change-Id: INNNNNN…
Change-Id
независимый, Gerrit-specific хэш, по которому Геррит определяет в какой change ему добавить новую версию коммита.
Очень важно
не изменять строчку c Change-Id при обновлении патчей. Новый Change-Id => новый change на Геррите.

Работа над новым, независимым изменением (баг, фича)

# git clone … && cd <repo>
git review -s # создает новый remote по имени gerrit
git checkout master
# git pull origin master
git checkout -b <new-feature-branch>
# hack on it
git add .
git commit
# проверяем что же мы закоммитили
git log -1 && git diff HEAD^..HEAD
# прогоняем юнит и прочие тесты
# tox [-e…]
git review

Правим свой старый патч

# если ветки нет - скачиваем ее с Gerrit:
git review -d NNNNNN # review.openstack.org/#/c/NNNNNN
# создалась новая ветка review/<user_name>/<topic>
# если ветка уже есть - переключаемся на нее:
git checkout <feature-branch>
# если мерж конфликт
git checkout master
git pull origin master
git fetch gerrit
git checkout <feature-branch>
git rebase -i master
# и резолвить конфликты
# hack on it, address reviewers comments
git add .
# не создаем новый коммит!
# а добавляем изменения из индекса в последний коммит
# (и естесственно при этом меняет его commid-id)
git commit --amend
git review

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

Rebase or not Rebase?

По дефолту, при выкладывании через git-review, его pre-push hook попытается сделать ребейз вашего change на ту ветку в которую вы выкладываете (по дефолту master).

Иногда это хорошо (вы забыли обновить мастер, и теперь есть мерж-конфликты - упадет сразу, не выложив).

Это поведение отменяется ключом -R.

Но чаще всего лучше делать осознанный ребейз руками - ревьюерам вашего кода проще сравнивать разные версии патч-сетов когда между ними не было ребейза.

Отдельная история - ваш change зависит от чужого, еще не вмерженого и находящегося на review. В таком случае не ребейзить чужие патчи - общее правило хорошего тона.

Поэтому мой личный алгоритм:

  • Отключить авторебейз по дефолту
  • Новый change - всегда от мастера.
  • Обновляю свой, независимый старый change - ребейз только если:
    • еще не было ни одного ревью, или
    • если merge conflict (тут уж без вариантов)
  • В остальных случаях - без ребейза.