Азы 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, что в общем случае очень сильно не рекомендуется (см Правки и изменения истории).
Специфика сообщества OpenStack (english)
- Общее описание Development Workflow
- http://docs.openstack.org/infra/manual/developers.html#development-workflow
- Как писать годный коммит-мессадж
- https://wiki.openstack.org/wiki/GitCommitMessages
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 (тут уж без вариантов)
- В остальных случаях - без ребейза.