Розділ 5. Уроки історії

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

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

Залишаючись коректним

Щойно зробили комміт і зрозуміли, що повинні були ввести інший опис? Запустіть

$ git commit --amend

щоб змінити останній опис. Усвідомили, що забули додати файл? Запустіть git add, щоб це зробити, потім виконайте вищевказану команду.

Захотілося додати ще трохи змін в останній комміт? Так зробіть їх і запустіть

$ git commit --amend -a

…І ще дещо

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

$ git rebase -i HEAD~10

і останні десять коммітів з’являться у вашому улюбленому редакторі (задається змінною оточення $EDITOR). Наприклад:

pick 5c6eb73 Додав посилання repo.or.cz
pick a311a64 Переставив аналогії в „Працюй як хочеш“
pick 100834f Додав ціль для push в Makefile

Старі комміти передують новим коммітам у цьому списку, на відміну від команди log. Тут, 5c6eb73 є найстарішим коммітом і 100834f є найновішим. Тепер ви можете:

  • Видаляти комміти шляхом видалення рядків. Подібно команді revert, але видаляє запис: це буде так ніби комміта ніколи не існувало.
  • Міняти порядок коммітів, переставляючи рядки.
  • Заміняти pick на:

    • edit, щоб позначати комміт для внесення правок;
    • reword, щоб змінити опис у журналі;
    • squash, щоб злити комміт з попереднім;
    • fixup, щоб злити комміт з попереднім і відкинути його опис.

Наприклад, ми могли б замінити другий pick з squash:

pick 5c6eb73 Додав посилання repo.or.cz
squash a311a64 Переставив аналогії в „Працюй як хочеш“
pick 100834f Додав ціль для push в Makefile

Після того, як ми збережемо і вийдемо, Git зіллє a311a64 у 5c6eb73. Так squash зливає у наступний комміт вгорі: думайте «squash up».

Тоді Git об’єднує повідомлення журналу і подає їх для редагування. Команда fixup пропускає цей етап; злиті повідомлення журналу просто відкидаються.

Якщо ви позначили комміт командою edit, Git поверне вас в минуле, до найстарішого такого комміта. Ви можете відкоректувати старий комміт як описано в попередньому параграфі і, навіть, створити нові комміти, які знаходяться тут. Як тільки ви будете задоволені «retcon», йдіть вперед у часі, виконавши:

$ git rebase --continue

Git виводить комміти до наступного edit або до поточного, якщо не залишиться нічого.

Ви також можете відмовитися від перебазування (rebase) з:

$ git rebase --abort

Одним словом, робіть комміти раніше і частіше — ви завжди зможете навести порядок за допомогою rebase.

Локальні зміни зберігаються

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

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

Це робота для команди git rebase, як описано вище. Найчастіше, має сенс використовувати опцію --onto, щоб прибрати переплетення.

Також дивіться git help rebase для отримання детальних прикладів використання цієї чудової команди. Ви можете розщеплювати комміти. Ви можете навіть змінювати порядок гілок у дереві.

Будьте обережні: rebase — це потужна команда. Для складних rebases, спочатку зробіть резервну копію за допомогою git clone.

Переписуючи історію

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

$ git filter-branch --tree-filter 'rm цілком/таємний/файл' HEAD

Дивіться git help filter-branch, де обговорюється цей приклад і пропонується більш швидкий спосіб вирішення. Взагалі, filter-branch дозволяє змінювати істотні частини історії за допомогою однієї-єдиної команди.

Після цієї команди каталог .git/refs/original буде описувати стан, який був до її виклику. Переконайтеся, що команда filter-branch зробила те, що ви хотіли, і якщо хочете знову використовувати цю команду, видаліть цей каталог.

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

Створюючи історію

Хочете перевести проект під управління Git? Якщо зараз він знаходиться під управлінням якоїсь із добре відомих систем керування версіями, то цілком імовірно, що хтось вже написав необхідні скрипти для експорту всієї історії проекту в Git.

Якщо ні, то дивіться в сторону команди git fast-import, яка зчитує текст в спеціальному форматі для створення історії Git з нуля. Зазвичай скрипт, який використовує цю команду, буває зліплений похапцем для одиничного запуску, що переносить весь проект за один раз.

В якості прикладу вставте такі рядки в тимчасовий файл, на зразок /tmp/history:

commit refs/heads/master
committer Alice <alice@example.com> Thu, 01 Jan 1970 00:00:00 +0000
data <<EOT
Початковий комміт.
EOT

M 100644 inline hello.c
data <<EOT
#include <stdio.h>

int main() {
  printf("Hello, world!\n");
  return 0;
}
EOT

commit refs/heads/master
committer Bob <bob@example.com> Tue, 14 Mar 2000 01:59:26 -0800
data <<EOT
Замінений printf() на write()
EOT

M 100644 inline hello.c
data <<EOT
#include <unistd.h>

int main() {
  write(1, "Hello, world!\n", 14);
  return 0;
}
EOT

Потім створіть сховище Git з цього тимчасового файлу за допомогою команд:

$ mkdir project; cd project; git init
$ git fast-import --date-format=rfc2822 < /tmp/history

Ви можете витягти останню версію проекту за допомогою

$ git checkout master .

Команда git fast-export перетворює будь-яке сховище в формат, зрозумілий для команди git fast-import. Її результат можна використовувати як зразок для написання скриптів перетворення або для перенесення сховищ в зрозумілому для людини форматі. Звичайно, за допомогою цих команд можна пересилати сховища текстових файлів через канали передачі тексту.

Коли ж все пішло не так?

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

У будь-якому випадку, вже надто пізно. Однак, якщо ви фіксували свої зміни досить часто, то Git зможе точно вказати проблему:

$ git bisect start
$ git bisect bad HEAD
$ git bisect good 1b6d

Git витягне стан рівно посередині. Перевірте чи працює те, що зламалося, і якщо все ще ні:

$ git bisect bad

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

$ git bisect reset

Замість ручного тестування кожної зміни автоматизуйте пошук, запустивши

$ git bisect run my_script

За поверненим значенням заданої команди, зазвичай одноразового скрипта, Git буде відрізняти хороший стан від поганого. Скрипт повинен повернути 0, якщо теперішній комміт хороший; 125, якщо його треба пропустити, і будь-яке інше число від 1 до 127, якщо він поганий. Від'ємне значення перериває команду bisect.

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

Через кого все пішло не так?

Як і в багатьох інших системах керування версіями, в Git є команда blame:

$ git blame bug.c

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

Особистий досвід

У централізованих системах керування версіями зміни історії — досить складна операція, і доступна вона лише адміністраторам. Клонування, розгалуження і злиття неможливі без взаємодії по мережі. Так само йдуть справи і з базовими операціями, такими як перегляд історії або фіксація змін. У деяких системах мережеве з’єднання потрібне навіть для перегляду власних змін, або відкриття файлу для редагування.

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

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

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

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

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