Данное повествование подразумевает знакомство читателей с такими командами git как: add, pull, push, commit.
В случае многопользовательской работы с репозиторием зачастую получается такая ситуация: сделали мы git pull, правим код, коммитим, хотим пропихнуть код в общий репозиторий, а тут-то нам и говорят: фигу вам, а не push—там уже всё поменялось. В итоге приходится делать git pull, получается вынужденный автоматический merge и с этим уродливым merge (который был по сути и необязателен!) мы запихиваем наши изменения обратно в общий репозиторий. История изменений получается не самая красивая.
Ниже будет рассказано о том, как же можно избежать подобных конфузов и прослыть знатоком git kung-fu.
Существует замечательная команда: git stash. Дословно, stash— припрятывать, тайник. Команда позволяет сохранять локальные незакоммиченные изменения, сбрасывая при этом состояние рабочей копии до HEAD’а. То есть, сказав git stash мы “припрятываем” все локальные изменения, получая чистую рабочую копию, в которую можно смело делать git pull не опасаясь возникновения ненужных merge’й.
После получения из репозитория-origin’а всех новых изменений мы можем смело “достать” “припрятанные” изменения и наложить их на текущую рабочую копию командой git stash apply.
Убедившись в успешном наложении изменений, можно сделать долгожданный git add -u, git commit и git push, после чего (или сразу после git stash apply) можно со спокойной совестью грохать сохранённые изменения командой git stash drop.
Более подробно о команде stash:
git stashдобавить текущие незакоммиченные изменения в стек изменений и сбросить текущую рабочую копию до HEAD’а репозитория;
git stash listпоказать все изменения в стеке;
git stash showпоказать последнее измененеие в стеке (патч);
git stash applyприменить последнее изменение из стека к текущей рабочей копии;
git stash dropудалить последнее изменение в стеке;
git stash popприменить последнее изменение из стека к текущей рабочей копии и удалить его из стека;
git stash clearочистить стек изменений.
Полный синтаксис смотреть командой git help stash (или man git-stash).
Ещё более удобной возможностью одновременной работы над множеством разных фич является использованием бранчей в git. Бранчи там реализованы просто божественно, использовать их—одно удовольствие.
Как это делаю я: когда надо закрыть какой-нибудь баг, или реализовать определённую фичу, я просто завожу под это дело отдельный бранч. Бранч можно создать двумя способами, какой из них использовать—вопрос персональных предпочтений. Расскажу об обоих по порядку.
Первый вариант создания—команда git branch имя_бранча. Создаёт локальный бранч с указанным именем. Просмотреть существующие бранчи в репозитории можно командой git branch (можно добавить опцию -v для вывода также текущего HEAD’а каждого из бранчей).
После создания бранча надо переключиться на него, то есть, привести рабочую копию в соответствии с HEAD’ом этого бранча и указать репозиторию, что все действия необходимо вести именно с этим бранчем. Такое переключение осуществляется командой git checkout имя_бранча.
Для создания нового бранча и переключения на него можно воспользоваться удобной опцией команды checkout, а именно: git checkout -b имя_нового_бранча —такой вызов одновременно создаст новый бранч и переключится на него.
Основная ветка репозитория традиционно называется master, и вернуться к ней из любого бранча можно также, как и переключение на любой другой бранч, то есть git checkout master.
Работа с бранчем рано или поздно должна завершиться переносом всех изменений из этого бранча в master. Для начала рассмотрим простейший вариант: есть основная ветка master с ревизиями a-b-c, мы создаём бранч bug1, вносим в него изменения и коммитим их: в бранче bug1 порядок ревизий стал a-b-c-d. Сейчас, чтобы перенести ревизию d в бранч master, мы выполняем следующую последовательность команд:
git checkout master # переключились на бранч master
git merge bug1 # выполняем слияние с бранчем bug1
Так как последовательность изменений из бранча bug1 однозначно накладывается на последовательность из бранча master, выполняемый merge будет определён как fast-forward, то есть история будет без лишних ветвлений, а итоговая последовательность изменений в master’е выглядит так: a-b-c-d.
После успешного наложения всех изменений из бранча bug1 его можно безболезненно удалить командой git branch -d bug1. Полезной особенностью этой команды является проверка на наличие всех изменений удаляемой ветки в master’е—если изменения перенесены не все, команда откажется удалять ветку.
Рассмотрим более сложный, но более близкий к реальности вариант: параллельное внесение изменений в основную ветку репозитория и отдельном бранче. Исходные данные аналогичны предыдущему примеру: master с историей изменений a-b-c, и ветка bug1 с добавленным изменением d, то есть история для неё a-b-c-d.
Предположим, пока мы занимались работой над багом, кто-то другой успел реализовать какую-нибудь фичу и пропихнуть её в основной репозиторий. Теперь, переключившись из бранча bug1 в master, и выполнив в master’е git pull, мы получаем в нём такую историю: a-b-c-E, где E—это коммит с реализацией той фичи, которую успел закоммитить другой программист.
Если сейчас находясь в ветке master выполнить git merge bug1, мы получим историю изменений раздваивающуюся после коммита c на 2 ветви и заканчивающуюся автоматическим слиянием веток, родителями этого merge-коммита будут ревизии E (из master’а) и d (из бранча bug1). Избежать подобной ветвистости помогает волшебная команда git rebase.
git rebase—инструмент для “пересадки” веток истории изменений. В нашем примере, имея в master историю a-b-c-E, мы можем “пересадить” кусок истории c-d из бранча bug1 после коммита E. Проделать это можно так:
git checkout bug1 # переключаемся на бранч bug1
git rebase master # пересадить историю изменений
Остановимся на последней команде подробнее. git rebase берёт ветку истории из текущего бранча, отличающуюся от истории бранча, указанного в качестве параметра (master), и “пересаживает” эту ветку поверх подтянутой истории (master’а). То есть, история изменений для бранча bug1 после выполнения этой команды будет выглядеть так: a-b-c-E-D.
Необходимо обратить внимание на то, что получившееся изменение D отличается от исходного изменения d своим идентификатором, хотя и сохраняет такие атрибуты как дата, текст и патч. То есть, это уже фактически другая ревизия, а с номерами ревизий “играть” по-хорошему допускается только локально.
Имея в master’е историю a-b-c-E, а в бранче bug1 историю a-b-c-E-D, несложно выполнить fast-forward merge, сохранив “прямую” историю без ненужных ветвлений:
git checkout master
git merge bug1
Результатом получаем master с красивой историей a-b-c-E-D.
Более подробная информация с картинками есть в увлекательной документации: git help rebase (или man git-rebase).