Git на практике. Учимся поддерживать репозиторий в порядке
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті
Всем разработчикам приходится ежедневно работать с Git, но далеко не все уделяют стоящего внимания этому инструменту. Цель этой статьи ознакомить с некоторым полезным функционалом Git и показать, как это используется на практике. В первую очередь, эта статья может быть полезна начинающим разработчикам, которые хотят улучшить свою экспертизу в работе с Git, а также людям, которые хотят, но не знают, как держать свой репозиторий в чистоте и порядке.
Для начала хотелось бы описать мое видение «правильного» репозитория. Основным отличием такого репозитория является чистая история коммитов. Каждый из этих коммитов должен быть осмысленной атомарной единицей изменений в проекте. Это значит, что наша история не должна содержать коммиты с сообщениями по типу «feature in progress». Ваша задача как разработчика научиться делить все вносимые изменения на такие атомарные единицы. Описанный ниже материал поможет вам этого добиться.
Stash
Первая очень полезная команда, которую хотелось упомянуть, это git stash. Она позволяет сохранить наши изменения без создания коммитов.
Такая опция хороша, когда мы хотим переключиться на другую ветку, например, чтобы срочно пофиксить баг, но текущие изменения не готовы, чтобы сделать коммит. Мы, конечно, могли бы сделать коммит, но он бы содержал незаконченную работу и не нес бы в себе особого смысла и в довесок еще бы загрязнял наш репозиторий.
Допустим, мы внесли какие-то изменения:
git status On branch feature Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: file no changes added to commit (use "git add" and/or "git commit -a")
Сохраним наши изменения в stash:
git stash Saved working directory and index state WIP on feature: cdb8f82 Merge pull request #2 from germankhivrenko/feature
Также мы можем просмотреть список таких сохраненных изменений:
git stash list stash@{0}: WIP on feature: cdb8f82 Merge pull request #2 from germankhivrenko/feature
Чтобы вернуть изменения из stash:
git stash apply On branch feature Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: file no changes added to commit (use "git add" and/or "git commit -a")
По умолчанию применяются последние изменения из списка, и чтобы выбрать конкретные мы можем указать индекс:
git stash apply --index 0
Теперь мы знаем как переключать контекст задач и не загрязнять репозиторий.
Cherry-pick
Переходим к cherry-pick. Эта команда позволяет вставить выбранный вами коммит (и конечно же его изменения) в текущую ветку. Это полезно, когда мы хотим перенести конкретные изменения из другой ветки, но не хотим сливать ее полностью.
Представим, что у нас есть две ветки main и release с такой историей.
В main:
commit 818136b3eec70b04ca27b5de55abb1dc5de36cb9 (HEAD -> main) bad changes commit ffd31c519d29eae6f9573f42562708ae8034a0f4 cherry-pick me commit 7128c5d11ef768dc21b1171752502231fb274b21 Initial commit
В release:
commit 7128c5d11ef768dc21b1171752502231fb274b21 (HEAD -> release) Initial commit
Мы хотим получить изменения коммита с сообщением «cherry-pick me», но не хотим иметь изменения commit-а с сообщением «bad changes» в ветке release. Для этого мы должны сделать cherry-pick нужного коммита:
git cherry-pick ffd31c519d29eae6f9573f42562708ae8034a0f4
Для выбора коммита мы используем его hash. Теперь все готово, и мы можем увидеть нужные изменения в release:
commit e6a95034d392d295741a14ab0eaf084258116f5d (HEAD -> release) cherry-pick me commit 7128c5d11ef768dc21b1171752502231fb274b21 Initial commit
Примечание: во время выполнения cherry-pick могут возникнуть конфликты, после их решения Вы можете завершить cherry-pick c помощью команды git cherry-pick --continue
.
Сама суть этой команды заставляет задуматься над атомарностью коммита. Правильная история коммитов очень сильно помогает при использовании команды cherry-pick.
Rebase
В сети достаточно информации со сравнением merge и rebase (как мне кажется, один из самых распространенных вопросов по поводу Git), так что я не стану их сравнивать, а просто расскажу, как я использую rebase в повседневной рабочей жизни.
Для ясности кратко рассмотрим цикл разработки какого-то функционала с точки зрения Git. Разработчик берет за основу общую ветку (обычно она называется dev), создает новую ветку и вносит в нее свои изменения, потом создает merge/pull request, чтобы залить свои изменения в общую ветку.
Я советую делать rebase перед созданием merge/pull request-а своей ветки в общую. Все это делается для того, чтобы решить потенциально возможные конфликты, а также поддерживать чистоту истории в репозитории. Чистая и понятная история сильно облегчает потенциально возможные манипуляции над репозиторием в будущем.
Итак, зачем вообще что-то делать перед созданием merge/pull request-а? Главной целью для слияния своей ветки с общей перед созданием это решение возможных конфликтов. Держу пари, каждый разработчик когда-то видел сообщение по типу «There merge conflicts» или «Cannot merge automatically». Следовательно, перед нами стоит выбор: merge и rebase. Конечно, rebase не всегда уместен, но в данной ситуации стоит выбирать именно его. Для наглядности посмотрим на разницу между merge и rebase в следующем примере.
Допустим, нам нужно реализовать какой-то новый функционал. Для начала мы переключаемся на общую ветку (в нашем примере main) и делаем pull, чтобы подтянуть последние обновления:
git checkout main
git pull origin main
Создаем ветку feature из main для локальной разработки:
git checkout -b feature
Представим, что мы реализовали новый функционал и сделали новый коммит:
git add . git commit -m “implemented feature”
Поскольку над нашим репозиторием трудится множество людей разработчиков, пока мы реализовывали наш функционал, в общей ветке появились изменения, которые конфликтуют с нашими. Поэтому мы должны предотвратить возможные конфликты:
git pull --rebase origin main
Потом мы должны решить те самые конфликты (если они присутствуют) и затем продолжить:
git rebase --continue
Примечание: rebase перезаписывает Ваш созданный коммит, hash-ы старого и нового коммитов отличаются.
После чего мы можем отправлять наши изменения на сервер:
git push origin feature
И теперь можно спокойно создавать наш merge/pull request и не переживать по поводу конфликтов.
Теперь давайте посмотрим на историю коммитов нашей общей ветки после слияния нашего request-а (для этого переключитесь на main и сделайте pull):
git log --pretty=short commit eb560c0af5cfb63073592650bcb3e9050342b6bb (HEAD -> main, origin/main, origin/HEAD) Merge: 795882a 9f5011e Merge pull request #1 from germankhivrenko/feature commit 9f5011eeed9d6ffd5f693e7136599bc13f93a768 (origin/feature, feature) implemented feature commit 795882a02fc51fea1b2f46df8a87c2bec3d6dc86 added some changes
Коммит с сообщением «added some changes» — это те самые изменения, которые были добавлены другим разработчиком во время нашей работы над feature. Итого, мы с нашей стороны имеем два коммита. Один из них несет смысловую нагрузку (разработанный нами функционал), а другой — это merge-коммит двух других коммитов (мы можем это увидеть из его описания: Merge: 795882a 9f5011e), его за нас создала система.
Теперь давайте взглянем на историю коммитов, в случае если бы мы использовали git pull origin main
вместо git pull --rebase origin main
, то есть использовали merge вместо rebase. Прежде всего, после решения конфликтов, нам бы пришлось создать новый коммит:
git add . git commit -m “resolved merge conflicts”
Взглянем на историю коммитов main ветки:
commit cdb8f82bd78dfbcb9381147ad30a5f44f7c8072e (HEAD -> main, origin/main, origin/HEAD) Merge: 795882a eb36d47 Merge pull request #2 from germankhivrenko/feature commit eb36d479e83f899b5dcb7b25fc881c7fa5ff6f16 (origin/feature-1, feature-1) Merge: 69beef2 795882a resolved merge conflicts commit 69beef25fb0ad295fda1471c9c46f8e9d777b821 implemented feature commit 795882a02fc51fea1b2f46df8a87c2bec3d6dc86 added some changes
Вместо 3 коммитов получаем 4, где коммит с сообщением «resolved merge conflicts» не несет никакой смысловой нагрузки.
Если вам нужно синхронизировать вашу ветку и общую по ходу разработки, использование merge может существенно загрязнять историю, особенно если такая синхронизация проходит часто.
Reset & revert
Далее уделим немного внимания удалению и исправлению уже существующих в репозитории изменений и коммитов. Рассмотрим команды reset и revert.
Допустим, мы хотим добавить изменения к предыдущему коммиту. Для этого нам нужна команда reset с параметром —soft или —mixed (mixed используется по умолчанию, если указывать параметры). Вводим:
git reset --soft HEAD~1
Запись HEAD~1
означает, что мы хотим перейти на один коммит назад от текущего положения HEAD. Чтобы посмотреть, что же произошло, можно использовать git log
и git status
. В списке коммитов больше не будет коммита, который мы удалили, а изменения этого коммита остались в рабочей директории в статусе staged. Далее можно дополнить/исправить эти изменения и сделать новый коммит.
Если мы хотим просто удалить коммит и его изменения следует применять reset с параметром —hard, он удалит коммит их изменения не останутся в нашей рабочей директории.
Также для изменения последнего коммита в Git существует параметр —amend для команды commit, но мне почему то больше нравится пользоваться reset.
Стоит отметить, что описанные выше подходы именно изменяют историю коммитов в репозитории, что не всегда является безопасным. В общих ветках, которые обычно названы как master, dev или main не принято вносить изменения в историю коммитов, так как это может иметь негативные последствия. Также, если попробовать изменить коммит который уже есть на сервере, например, с помощью git reset
и попробовать отправить текущую ветку на сервер, то мы получим подобное сообщение:
git push origin main To github.com:germankhivrenko/git-in-practice.git ! [rejected] main -> main (non-fast-forward) error: failed to push some refs to '[email protected]:germankhivrenko/git-in-practice.git' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Integrate the remote changes (e.g. hint: 'git pull ...') before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Git советует нам сначала сделать pull, потом решить конфликты, создать новый коммит и уже потом попробовать отправить наши изменения еще раз. Все это делается для того, чтобы не допустить изменения истории коммитов на сервере. Конечно, это можно обойти с помощью параметра -f (—force) для команды push:
git push -f origin main
И если изменять историю на сервере в своих ветках, созданных для разработки конкретного функционала, до слияния их в общую вполне приемлемо, то изменять историю таких веток как master крайне не рекомендуется. Тут на помощь к нам приходит revert. Эта команда может удалить изменения выбранного коммита посредством создания нового коммита. Таким образом можно удалять изменения, но не изменять историю коммитов.
Примечание: revert может вызывать merge конфликты, после их решения нужно продолжить revert с помощью git revert --continue
.
Надеюсь, что данная статья хоть немного улучшить ваш опыт работы с Git. Спасибо, что дочитали до конца, буду рад любой критике и комментариям.
Найкращі коментарі пропустити