Розділ 4. Чудеса розгалуження

Можливості миттєвого розгалуження і злиття — найкращі особливості Git.

Завдання: зовнішні фактори неминуче потребують переключення уваги. Серйозна помилка в уже випущеній версії виявляється без попередження. Термін здачі певної функціональності наближається. Розробник, допомога якого потрібна вам в роботі над ключовою частиною проекту, збирається у відпустку. Одним словом, вам потрібно терміново кинути все, над чим ви працюєте в даний момент, і переключитися на зовсім інші завдання.

Переривання ходу ваших думок може серйозно знизити ефективність роботи, і чим складніше перемикання між процесами, тим більшою буде втрата. При централізованому керуванні версіями ми змушені завантажувати свіжу робочу копію з центрального сервера. Розподілена система краще: ми можемо клонувати потрібну версію локально.

Проте клонування все ж передбачає копіювання всього робочого каталогу, як і всієї історії змін до теперішнього моменту. Хоча Git і знижує затратність цієї дії за рахунок можливості спільного використання файлів і жорстких посилань, але всі файли проекту доведеться повністю відтворити в новому робочому каталозі.

Розв'язання: у Git є більш зручний інструмент для таких випадків, який заощадить і час, і дисковий простір в порівнянні з клонуванням — це git branch (branch — гілка, прим. пер.).

Цим чарівним словом файли в вашому каталозі миттєво перетворяться від однієї версії до іншої. Ця зміна дозволяє зробити набагато більше, ніж просто повернутися назад або просунутися вперед в історії. Ваші файли можуть зміниться з останньої випущеної версії на експериментальну, з експериментальної — на поточну версію у розробці, з неї — на версію вашого друга і так далі.

Кнопка боса

Грали коли-небудь в одну з таких ігор, де при натисканні певної клавіші («кнопки боса»), на екрані миттєво відображається таблиця або щось на зразок того? Тобто, якщо в офіс зайшов начальник, а ви граєте в гру, ви можете швидко її приховати.

У якомусь каталозі:

$ echo "Я хитріший за мого боса" > myfile.txt
$ git init
$ git add .
$ git commit -m "Початковий комміт"

Ми створили сховище Git, що містить один текстовий файл з певним повідомленням. Тепер виконайте

$ git checkout -b boss # ймовірно, це остання зміна
$ echo "Мій бос розумніший за мене" > myfile.txt
$ git commit -a -m "Інший комміт"

Це виглядає так, ніби ми тільки що перезаписали файл і зробили комміт. Але це ілюзія. Наберіть

$ git checkout master # переключитися на оригінальну версію файлу

Вуаля! Текстовий файл відновлений. А якщо бос вирішить сунути ніс в цей каталог, запустіть

$ git checkout boss # перейти на версію, яка підходить для очей боса

Ви можете переключатися між двома версіями цього файлу так часто, як вам хочеться і робити комміти кожної з них незалежно.

Брудна робота

Припустимо, ви працюєте над якоюсь функцією, і вам навіщось знадобилося повернутися на три версії назад і тимчасово додати кілька операторів виводу, щоб подивитися як щось працює. Тоді введіть

$ git commit -a
$ git checkout HEAD~3

Тепер ви можете додавати тимчасовий чорновий код в будь-яких місцях. Можна навіть закоммітити ці зміни. Коли закінчите, виконайте

$ git checkout master

щоб повернутися до вихідної роботі. Зауважте, що будь-які зміни, які не внесені в комміт, будуть перенесені.

А що, якщо ви все-таки хотіли зберегти тимчасові зміни? Запросто:

$ git checkout -b dirty

а потім зробіть комміт перед поверненням в гілку master. Кожного разу, коли ви захочете повернутися до чорнових змін, просто виконайте

$ git checkout dirty

Ми говорили про цю команду в одному із попередніх розділів, коли обговорювали завантаження старих станів. Тепер у нас перед очима повна картина: файли змінилися до потрібного стану, але ми повинні залишити головну гілку. Будь-які комміти, зроблені з цього моменту, направлять файли по іншому шляху, до якого можна буде повернутися пізніше.

Іншими словами, після перемикання на більш старий стан Git автоматично направляє вас по новій безіменній гілці, якій можна дати ім'я і зберегти її за допомогою git checkout -b.

Швидкі виправлення

Ваша робота в самому розпалі, коли раптом з'ясовується, що потрібно все кинути і виправити тільки що виявлену помилку в комміті 1b6d...:

$ git commit -a
$ git checkout -b fixes 1b6d

Після виправлення помилки виконайте

$ git commit -a -m "Помилка виправлена"
$ git checkout master

і поверніться до роботи над вашими початковими завданнями.

Ви можете навіть влити тільки що зроблене виправлення помилки в основну гілку:

$ git merge fixes

Злиття

У деяких системах керування версіями створювати гілки легко, а от зливати їх воєдино важко. У Git злиття настільки тривіальне, що ви можете його не помітити.

Насправді ми стикалися зі злиттями вже давно. Команда pull по суті отримує комміти, а потім зливає їх з вашою поточної гілкою. Якщо у вас немає локальних змін, злиття відбудеться само собою, як вироджений випадок на кшталт отримання останньої версії в централізованій системі управління версіями. Якщо ж у вас є локальні зміни, Git автоматично зробить злиття і повідомить про будь-які конфлікти.

Зазвичай у комміта є один батько, а саме попередній комміт. Злиття гілок призводить до комміту як мінімум з двома батьками. Звідси виникає питання: до якого комміту насправді відсилає HEAD~10? Комміт може мати кілька батьків, так за яким з них слідувати далі?

Виявляється, такий запис завжди вибирає першого батька. Це хороший вибір, тому що поточна гілка стає першим батьком під час злиття. Часто вас цікавлять тільки зміни, зроблені вами в поточній гілці, а не ті, які влилися з інших гілок.

Ви можете звертатися до конкретного батька за допомогою символу ^. Наприклад, щоб показати запис у журналі від другого батька, наберіть

$ git log HEAD^2

Для першого батька номер можна опустити. Наприклад, щоб показати різницю з першим батьком, введіть

$ git diff HEAD^

Ви можете поєднувати такий запис з іншими. Наприклад,

$ git checkout 1b6d^^2~10 -b ancient

створить нову гілку «ancient», що відображає стан на десять коммітов назад від другого батька першого батька комміта, що починається з 1b6d.

Неперервний робочий процес

У виробництві техніки часто буває, що другий крок плану повинен чекати завершення першого кроку. Автомобіль, що потребує ремонту, може тихо стояти в гаражі до прибуття з заводу конкретної деталі. Прототип може чекати виробництва чіпа, перш ніж розробка буде продовжена.

І в розробці ПЗ може бути те ж. Друга порція нової функціональності може бути змушена чекати випуску та тестування першої частини. Деякі проекти вимагають перевірки вашого коду перед його прийняттям, так що ви повинні дочекатися затвердження першої частини, перш ніж починати другу.

Завдяки безболісним галуженню і злиттю, ми можемо змінити правила і працювати над другою частиною до того, як перша офіційно буде готова. Припустимо, ви закоммітили першу частину і вислали її на перевірку. Скажімо, ви в гілці master. Тепер змініть гілку:

$ git checkout -b part2 # частина2

Потім працюйте над другою частиною, попутно вносячи комміти ваших змін. Людині властиво помилятися, і часто ви схочете повернутися і поправити щось в першій частині. Якщо ви везучі або дуже вправні, можете пропустити ці рядки.

$ git checkout master  # Повертаємося до першої частини.
$ вносимо_зміни
$ git commit -a        # Фіксуємо зміни
$ git checkout part2   # Повертаємося до другої частини.
$ git merge master     # Вливаємо зроблені виправлення.

Зрештою, перша частина затверджена:

$ git checkout master  # Повертаємося до першої частини.
$ отправка файлов        # Випускаємо в світ!
$ git merge part2      # Вливаємо другу частину.
$ git branch -d part2  # Видаляємо гілку part2.

Тепер ви знову в гілці master, а друга частина — у вашому робочому каталозі.

Цей прийом легко розширити на будь-яку кількість частин. Настільки ж легко змінити гілку заднім числом. Припустимо, ви надто пізно виявили, що повинні були створити гілку сім коммітів назад. Тоді введіть:

$ git branch -m master part2 # Перейменовуємо гілку master в part2.
$ git branch master HEAD~7   # Створюємо нову гілку master сімома коммітами вище.

Тепер гілка master містить тільки першу частину, а гілка part2 — все інше. В останній ми і знаходимося. Ми створили гілку master, не перемикаючись на неї, тому що хочемо продовжити роботу над part2. Це незвично: досі ми переключалися на гілки відразу ж після їх створення, ось так:

$ git checkout HEAD~7 -b master  # Створюємо гілку і переключаємося на неї.

Змінюємо склад суміші

Припустимо, вам подобається працювати над всіма аспектами проекту в одній і тій же гілці. Ви хочете закрити свій робочий процес від інших, щоб всі бачили ваші комміти тільки після того, як вони будуть добре оформлені. Створіть пару гілок:

$ git branch sanitized    # Створюємо гілку для очищених коммітів.
$ git checkout -b medley  # Створюємо гілку для роботи і переключаємося на неї.

Далі робіть все що потрібно: виправляйте помилки, додавайте нові функції, додавайте тимчасовий код і так далі, при цьому частіше виконуючи комміти. Після цього

$ git checkout sanitized
$ git cherry-pick medley^^

застосує комміт пра-батька голови гілки «medley» до гілки «sanitized». Правильно підбираючи елементи, ви зможете створити гілку, в якій буде лише остаточний код, а пов'язані між собою комміти будуть зібрані разом.

Управління гілками

Для перегляду списку всіх гілок наберіть

$ git branch

За замовчуванням ви починаєте з гілки під назвою «master». Комусь подобається залишати гілку «master» недоторканою і створювати нові гілки зі своїми змінами.

Опції -d і -m дозволяють видаляти і переміщати (перейменовувати) гілки. Дивіться git help branch.

Гілка «master» — це зручна традиція. Інші можуть припускати, що у вашому сховищі є гілка з таким ім'ям і що вона містить офіційну версію проекту. Хоча ви можете перейменувати або знищити гілку «master», краще дотриматися загальної угоди.

Тимчасові гілки

Через якийсь час ви можете виявити, що створюєте безліч тимчасових гілок для однієї і тієї ж короткострокової мети: кожна така гілка лише зберігає поточний стан, щоб ви могли повернутися назад і виправити серйозну помилку або зробити щось ще.

Це схоже на те, як ви перемикаєте телевізійні канали, щоб подивитися що показують по іншим. Але замість того, щоб натиснути на пару кнопок, вам потрібно створювати, вибирати (checkout), зливати (merge) а потім видаляти тимчасові гілки. На щастя, в Git є скорочена команда, настільки ж зручна, як пульт дистанційного керування.

$ git stash

Ця команда збереже поточний стан в у тимчасовому місці (схованці, stash) і відновить попередній стан. Ваш каталог стає точно таким, яким був до початку редагування, і ви можете виправити помилки, завантажити віддалені зміни тощо. Коли ви хочете повернутися назад в стан схованки, наберіть:

$ git stash apply # Можливо, знадобиться усунути конфлікти, що виникнули.

Можна створювати кілька схованок, використовуючи їх по-різному. Дивіться git help stash. Як ви могли здогадатися, Git залишає гілки за кадром при виконанні цього чудового прийому.

Працюйте як вам подобається

Можливо, ви сумніваєтеся, чи варті гілки таких клопотів. Зрештою, клони майже настільки ж швидкі і ви можете перемикатися між ними за допомогою cd замість загадкових команд Git.

Поглянемо на веб-браузери. Навіщо потрібна підтримка вкладок на додаток до вікон? Підтримка і тих, і інших дозволяє пристосуватися до широкої різноманітності стилів роботи. Деяким користувачам подобається тримати відкритим єдине вікно і використовувати вкладки для безлічі веб-сторінок. Інші можуть впасти в іншу крайність: безліч вікон без вкладок взагалі. Треті віддадуть перевагу чомусь середньому.

Гілки схожі на вкладки для робочого каталогу, а клони — на нові вікна браузера. Ці операції швидкі і виконуються локально, так чому б не поекспериментувати і не знайти найбільш зручну для себе комбінацію? Git дозволяє працювати в так, як вам подобається.