Git знизу вверх

Матеріал з Вікіпідручника

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

Для кожного з прикладів, які ви побачите в даному документі, я використовував Git версії 1.5.4.5 (а перекладач — 1.7.9.5).

Джон Віґлі (John Wiegley)

http://ftp.newartisans.com/pub/git.from.bottom.up.pdf

Ліцензія[ред.]

Цей документ надається під умовами ліцензії Creative Commons Attribution-Share Alike 3.0, зміст якої можна переглянути за адресою:

http://creativecommons.org/licenses/by-sa/3.0/us/

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


Вступ[ред.]

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

Перед тим як ми почнемо, ось кілька термінів які потрібно згадати спершу, так як вони надалі з'являтимуться постійно :

Репозиторій
Репозиторій — це набір коммітів, кожен з яких є архівом того, як виглядало робоче дерево проекту в якийсь момент у минулому, не залежно на вашій машині, чи ще десь. Також в ньому означена голова (HEAD), яка ідентифікує гілку чи комміт, від якої відгалужується поточне робоче дерево. Ну й на останок, він містить множину гілок (branches) та міток (tags), які ідентифікують певні комміти за іменем.
Індекс
На відміну від інших, подібних інструментів які ви могли використовувати, Git не комітить зміни прямо з робочого дерева в репозиторій. Натомість, зміни спершу заносяться у реєстр що називається індексом. Думайте про нього як про спосіб "підтвердження" своїх змін, одну за одною, перед тим як зробити комміт (який запише всі ваші підтверджені зміни за раз). Декому легше називати це "staging area" замість індексу.
Робоче дерево
Робоче дерево — це будь-яка директорія на вашій файловій системі, яка має пов'язаний з собою репозиторій (на що зазвичай вказує присутність в ній піддиректорії яка називається .git). Воно включає всі файли та піддиректорії тієї директорії.
Комміт
Комміт - це миттєвий знімок стану вашого робочого дерева в певний момент часу. Комміт на який вказує HEAD в момент створення нового комміта стає його батьком. Таким чином утворюється поняття "історії ревізій".
Гілка
Гілка́ - це просто назва для комміта (про комміти скоро поговоримо ширше), яка також називається посиланням. Це батьки комміта що визначають його історію, і таким чином під терміном "гілка розробки" йдеться також про них.
Мітка
Мітка (tag) - це теж назва для комміта, подібно до гілки, окрім того що він завжди вказує на один і той же комміт, та може мати власний текст опису.
master
Головна лінія розробки в більшості репозиторіїв відбувається в гілці яку називають "master"́. Хоча це загальноприйнята домовленість, але ця гілка не є якоюсь особливою.
HEAD
HEAD використовується вашим репозиторієм для визначення того від якого комміта походитсь поточне дерево:
  • Якщо ви перейшли на гілку, HEAD символічно посилатиметься на ту гілку, вказуючи що назва гілки повинна оновитись після наступного комміта.
  • Якщо ви перейшли на конкретний комміт, HEAD посилатиметься лише на той комміт. Це називається detached HEAD, і стається, наприклад, коли ви переходите на тег.

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

Життєвий цикл проекту

З цією базовою картинкою в голові[1] наступні розділи спробують описати як кожна з цих сутностей бере участь в роботі Git.

Репозиторій: відстеження вмісту директорії[ред.]

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

Структура репозиторію Git багато в чому повторює структуру файлової системи UNIX. Файлова система починається з кореневої директорії, яка зазвичай містить інші директорії, більшість з яких мають листки, чи, по іншому, файли, які містять дані. Мета-дані цих файлів містяться як в директоріях (імена), так і в і-нодах які посилаються на вміст тих файлів (їхні типи, права доступу і т.п.). Кожен і-нод має унікальний номер що ідентифікує вміст пов'язаного з ним файлу. І хоча ви можете мати багато елементів директорії що вказують на один і-нод (хардлінків), власником вмісту що зберігається у файловій системі є і-нод.

Внутрішньо Git поділяє, на диво, подібну структуру, проте з однією-двома ключовими відмінностями. По-перше, він зберігає вміст ваших файлів в блобах (blob), які також є листками в чомусь, страшенно подібному на директорію, що називається деревом. Так як і-нод унікально ідентифікується номером присвоєним системою, кожен блоб ідентифікується SHA1 хешем його вмісту та розміру. Для більшості цілей це лише звичайне число, як і-нод, але воно має дві додаткові властивості: воно перевіряє що вміст блобу ніколи змінюється і що той самий вміст завжди буде записуватись в той самий блоб, не залежно де він буде з'являтись: в різних коммітах, в різних репозиторіях - навіть в різних "кінцях" Інтернету. Якщо багато дерев посилаються на ваш блог, це просто як хардлінкінг: блоб не зникне з вашого репозиторію, доки на нього буде хоч одне посилання.

Відмінністю блоба Git і файлу файлової системи є те, що блоб не містить жодних метаданих. Вся ця інформація зберігається в дереві що тримає блоб. Одне дерево може тримати вміст блобу як файл "foo", що був створений в серпні 2004-го, а інше - як файл "bar" що був створений на 9 років пізніше. В нормальній файловій системі два файли з однаковим вмістом, але з різними метаданими завжди зберігатимуться як два різні незалежні файли. Чому така різниця існує? В основному тому, що файлова система створена для зберігання файлів що змінюють свій вміст, а Git - ні. Факт того що дані в репозиторії Git є незмінними - це те на чому все працює і чому потрібен був інший дизайн. І як виявляється, такий дизайн дозволяє набагато компактніше зберігання, тому що всі об'єкти що мають ідентичний вміст зберігатимуться в одному блобі не залежно від того де знаходяться.

Представляємо блоб[ред.]

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

   $ mkdir sample; cd sample
   $ echo 'Hello, world!' > greeting

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

   $ git hash-object greeting
   af5626b4a114abcb82d63db7c8082c3c4756e51b

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

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

   $ git init
   $ git add greeting
   $ git commit -m "Added my greeting"

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

   $ git cat-file -t af5626b
   blob
   $ git cat-file blob af5626b
   Hello, world!

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

Таким чином блоб в Git являє собою фундаментальну одиницю даних в Git. Насправді, вся система стосується управління блобами.

Блоби зберігаються в деревах[ред.]

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

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

   $ git ls-tree HEAD
   100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b greeting

Ось воно! Перший комміт додав мій файл "greeting" до репозиторію. Цей комміт містить одне дерево Git, яке має один листок: блоб з вмістом файлу "greeting".

Хоча я можу побачити дерево передаючи HEAD команді git ls-tree, я ще не бачив об'єкт дерева на який посилається наш комміт. Ось кілька інших команд для того щоб підкреслити відмінність між деревом та коммітом і відкрити моє дерево:

   $ git rev-parse HEAD
   374bf425a1a1383fd8e0dea4aba877898627749c # буде іншим на вашій системі.
   $ git cat-file -t HEAD
   commit
   $ git cat-file commit HEAD
   tree 0563f77d884e4f79ce95117e2d686d7d6e282887
   author Bunyk <tbunyk@gmail.com> 1388923552 +0200
   committer Bunyk <tbunyk@gmail.com> 1388923552 +0200
   Added my greeting

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

Давайте перевіримо що це насправді один і той самий об'єкт дерева:

   $ git ls-tree 0563f77
   100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b greeting

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


   $ find .git/objects/ -type f | sort
   .git/objects/05/63f77d884e4f79ce95117e2d686d7d6e282887
   .git/objects/37/4bf425a1a1383fd8e0dea4aba877898627749c
   .git/objects/af/5626b4a114abcb82d63db7c8082c3c4756e51b

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

   $ git cat-file -t 0563f77d884e4f79ce95117e2d686d7d6e282887
   tree
   $ git cat-file -t 374bf425a1a1383fd8e0dea4aba877898627749c
   commit
   $ git cat-file -t af5626b4a114abcb82d63db7c8082c3c4756e51b
   blob

Я міг би використати команду show щоб переглянути вміст кожного з цих об’єктів, але залишу це як вправу для читача.

Як створюються дерева[ред.]

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

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

   $ rm -fr greeting .git
   $ echo 'Hello, world!' > greeting
   $ git init
   $ git add greeting

Все розпочинається коли ви вперше додаєте файл до індексу. Для даного випадку, давайте скажемо що індекс - це те що ми використовуємо щоб початково створювати блоби з файлів. Коли я додав файл "greeting", в моєму репозиторії сталася зміна. Я поки що не можу бачити цю зміну як комміт, але є один спосіб з'ясувати що сталось:

   $ git log # не спрацює, тому що ще немає ніяких коммітів.
   fatal: bad default revision 'HEAD'
   $ git ls-files --stage # перелічити блоби на які посилається індекс
   100644 af5626b4a114abcb82d63db7c8082c3c4756e51b 0	greeting

Що це? Я ще нічого не закомітив в репозиторій, а вже почав існувати якийсь об'єкт. Він має такий самий ідентифікатор як той з якого все це почалось, тому я знаю що він являє собою вміст мого файлу "greetings". Я можу використати git cat-file -t для цього хеша, і побачити що це blob. Це, насправді, той самий блоб який я отримав коли вперше створив цей учбовий репозиторій. Один і той же файл завжди буде давати один і той же блоб (на випадок, якщо я ще не достатньо на цьому наголосив).

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

   $ git write-tree
   0563f77d884e4f79ce95117e2d686d7d6e282887

Це число також повинне бути знайомим: дерево що містить такі ж блоби (і піддерева) завжди матиме такий же хеш. Я все ще не маю об'єкту комміта, але зараз в мене є об'єкт дерева в репозиторії що містить блоб. Метою низькорівневої команди write-tree є взяти все що знаходиться в індексі, і створити на основі цього дерево з метою створення коміту.

Я можу вручну створити новий об'єкт комміту, використовуючи це дерево прямо, за допомогою команди commit-tree:

   $ echo "Initial commit" | git commit-tree 0563f77
   c81e4e2e19c80030ae10e4b601429591ed5ef99d

Гола команда commit-tree приймає ідентифікатор дерева, і створює об'єкт комміту що містить це дерево. Якщо б я хотів щоб комміт мав батька, я повинен був би задати ідентифікатор батьківського комміта явно, використовуючи опцію -p. Також, зауважте що ідентифікатор хешу відрізняється від того що з'явиться на вашій системі. Це тому що мій об'єкт комміту містить як моє ім'я так і час створення комміту, а ці дві деталі завжди відрізнятимуться від ваших.

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

   echo "c81e4e2e19c80030ae10e4b601429591ed5ef99d"  > .git/refs/heads/master

Ця команда вкаже Git, що ім'я гілки "master", тепер повинно посилатись на наш недавній комміт. Інший, більш безпечний спосіб зробити таке саме - це використати команду update-ref:

   $ git update-ref refs/heads/master c81e4

Після створення гілки "master" ми повинні асоціювати з нею наше робоче дерево, як зазвичай це відбувається, коли ми робимо чекаут гілки:

   $ git symbolic-ref HEAD refs/heads/master

Ця команда символічно пов'язує HEAD, та гілку master. Це важливо, тому що всі наступні комміти з робочого дерева, будуть автоматично оновлювати refs/heads/master.

Важко повірити, що все так просто, але так, тепер я можу використовувати log, щоб переглядати мій щойно відчеканений комміт:

   $ git log
   commit c81e4e2e19c80030ae10e4b601429591ed5ef99d
   Author: Bunyk <tbunyk@gmail.com>
   Date:   Sun Jan 5 22:19:19 2014 +0200
       Initial commit

Побічне зауваження: якщо б я не змусив refs/heads/master вказувати на новий комміт, цей комміт вважався б "недосяжним", так як ніщо на нього не посилалося б, і він не був би батьком комміта на який є посилання. Коли трапляється така ситуація, об'єкт комміта в певний момент буде видаленим з репозиторію, разом зі своїм деревом і блобами. (Це автоматично виконується командою яка називається gc і яку рідко потрібно буде викликати вручну). Якщо прив'язувати комміт з іменем в refs/heads, як ми робили вище, він стає досяжним коммітом, що гарантує що він віднині й надалі зберігатиметься.

Краса комітів[ред.]

Деякі системи контролю версій роблять "гілки" (branches) магічними речима, часто відокремлюючи їх від "головної лінії" чи "стовбура", в той час як інші розглядають ідею гілок як щось дуже відмінне від коммітів. Але в Git немає гілок як окремих сутностей: є тільки блоби, дерева та комміти[2]. Так як комміт може мати одного чи кількох батьків, і ці комміти також можуть мати батьків, це те що дозволяє окремому комміту бути розглянутим як гілка: він знає всю історію яка до нього привела.

Ви можете переглянути всі комміти верхнього рівня, на які є посилання, використовуючи команду branch:

   $ git branch -v
   * master c81e4e2 Initial commit

Повторюйте за мною: гілка це ніщо інше як іменоване посилання на комміт. Таким чином, гілки та мітки є ідентичними, з єдиним винятком, що мітки мають власні описи, так як і коміти на які вони посилаються. Гілки це лише посилання, а мітки це описові, нуу... "мітки".

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

   $ git reset --hard c81e4e2

Опція --hard, наказує витирати всі зміни в моєму поточному робочому дереві, незалежно від того чи їх реєстрували в індексі чи ні (про цю опцію буде трохи більше далі). Безпечнішим способом зробити те саме буде:

   $ git checkout c81e4e2

Єдина відмінність - файли в моєму робочому дереві зберігаються. Якщо я передам команді checkout опцію -f, він спрацює майже так само як reset --hard, тільки зачепить лише робоче дерево, в той час як reset --hard, змінить HEAD поточної гілки так щоб та вказувала на задану версію дерева.

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

А ось картинка того як всі ці частини тримаються разом:

  • кола - це об’єкти коммітів, які посилаються на один чи більше батьківських коммітів. І так аж до їх початкових предків, таким чином формуючи "історію".
  • трикутники - це об’єкти дерев, які містяться в коммітах або інших деревах.
  • квадрати - це об’єкти блобів, які власне містять дані файлів

Кожен комміт містить дерево, і кожне дерево може містити в своєму листі довільну кількість інших дерев та блобів.

Коміт будь-яким іншим іменем...[ред.]

Розуміння коммітів - це ключ до просвітлення в Git. Ви знатимете що досягли вершини мудрості в контролі версі, коли ваш розум міститиме лише топології коммітів, залишаючи позаду плутанину гілок, тегів, локальних і віддалених репозиторіїв і т.п.

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

назва_гілки
як я вже казав раніше, ім’я будь-якої гілки - це просто псевдонім для найновішого комміта в цій "гілці". Це подібно до використання слова HEAD при чекауті комміта.
назва_тегу
Назва тегу - це псевдонім аналогічний до гілки. Головною відмінністю між ними двома є те що тег ніколи не змінюється, в той час як назва гілки змінюється щоразу коли в гілку додається новий комміт.
HEAD
комміт який ми зачекаутили. Якщо ви робите чекаут конкретного комміта (а не гілки), тоді HEAD посилається саме на цей комміт і не на ніяку гілку. Такий стан називається використанням "detached HEAD".
c82a22c39cbc32...
До комміта завжди можна звернутись використовуючи його повний, 40-символьний хеш SHA1. Зазвичай це використовують копіпастячи хеш, тому що щоб звернутись до комміта вручну існують зручніші способи.
c82a22c
Достатньо використовувати стільки цифр ідентифікатора хешу, скільки потрібно для унікального посилання всередині репозиторію. В більшості випадків шести чи семи символів буде достатньо.
назва^
батько будь-якого комміта може бути отриманий з використанням символа "карет". Якщо комміт має більше ніж одного батька - буде використаний перший.
назва^^
карет може застосовуватись кілька разів. Цей псевдонім посилається на батька батька комміта "назва".
назва^2
якщо комміт має кілька батьків, ви можете вказати n-того батька, використовуючи синтаксис назва^n.
назва~10
звернутись до n-того предка комміта можна використовуючи тильду (~) за якою йде номер предка. Це так само як написати назва^^^^^^^^^^.
назва:шлях/до/файлу
щоб звернутись до певного файлу всередині дерева комміту, задайте шлях до того файлу після двокрапки. Це корисно з командою show, або щоб показати відмінності між двома версіями файлу:
$ git diff HEAD^1:Makefile HEAD^2:Makefile
назва^{дерево}
Ви можете звернутись прямо до дерева яке міститься в комміті, замість прямо до комміту.
назва1..назва2
Цей та наступні псевдоніми вказують на діапазони коммітів, які є надзвичайно корисними з командами на зразок log, для того аби подивитись що сталось протягом певного відрізку часу.

Синтаксис назва1..назва2 посилається на всі комміти досяжні від назва2, назад, але не включно до назва1. Якщо назва1 або назва2 пропущені, замість них використовується HEAD.

назва1...назва2
Діапазон з трьома крапками відрізняється від версії з двома крапками вище. Для команд на зразок log, він посилається на всі комміти на які є посилання з назва1 або назва2, але не з обох. В результаті, виходить список всіх унікальних коммітів з обох гілок.

Для команд на зразок diff, діапазон задає комміти між назва1 та спільним предком назва1 та назва2. Це відрізняється від випадку з командою log тим що зміни внесені name1 не показуються.

master..
Це використання еквівалентне master..HEAD. Я додам його тут, незважаючи на те що воно вже описано вище, тому що я використовую цей псевдонім постійно, коли переглядаю зміни зроблені в поточній гілці.
..master
Це теж особливо корисне після того як ви зробили fetch і хочете подивитись, які зміни стались після вашого останнього rebase чи merge.
--since=2.weeks
Всі комміти зроблені до двох тижнів тому.
--until=2.weeks
Всі комміти старіші за два тижні.
--grep=pattern
Всі комміти чиї описи відповідають регулярному виразу pattern.
--committer=pattern
Всі комміти чий коммітер відповідає регулярці pattern.
--author=pattern
Всі комміти чий автор відповідає регулярці pattern. Автор комміту - це той хто створив зміни які той представляє. При локальній розробці це завжди та ж особа що й коммітер, але коли патчі відсилаються по e-mail, автор і коммітер зазвичай відрізняються.
--no-merges
Вибирає лише ті комміти в діапазоні які мають одного батька, таким чином ігноруючи всі комміти створені мерджем.

Більшість з цих опцій можуть використовуватись в перемішку. Ось приклад що показує наступні комміти: зміни зроблені в поточну гілку (відгалужену від master), мною, протягом останнього місяця, які в описі містять текст "foo":

$ git log --grep='foo' --author='johnw' --since="1 month ago" master..

Розгалуження та сила rebase[ред.]

Одна з найбільш вмілих в маніпулюванні коммітами команд в Git називається rebase. Кожна гілка на якій ви працюєте, має один чи більше "базових коммітів": коммітів з яких ця гілка з’явилась. Візьмімо наприклад наступний типовий сценарій. Зауважте що стрілки показують назад в часі, тому що кожен комміт має посилання на свого батька (чи батьків) але не на дітей. Тому комміти D та Z є головами відповідних гілок:

В цьому випадку, виконання git branch покаже дві "голови": D та Z, спільним предком для яких є A. Це саме нам скаже і show-branch:

$ git branch
  Z
* D

$ git show-branch
! [Z] Z
 * [D] D
--
 * [D] D
 * [D^] C
 * [D~2] B
+  [Z] Z
+  [Z^] Y
+  [Z~2] X
+  [Z~3] W
+* [D~3] A

Щоб читати вивід show-branch, треба трохи призвичаїтись до оформлення, але по суті, він не відрізняється від діаграми вище. Ось що він нам каже:

  • Гілка на якій ми зараз перебуваємо, вперше відгалужилась від комміту A (також відомого як комміт D~3, і навіть Z~4, якщо вам так захочеться). Синтаксис комміт^ використовується щоб послатись на батька комміта, а комміт~3 - на третього предка, тобто прадіда.
  • Якщо читати знизу вверх, перша колонка (знак плюс) показує гілку яка називається Z з чотирма коммітами: W, X, Y та Z.
  • Друга колонка (зірочки) показує комміти які були зроблені на поточній гілці, а саме три комміти: B, C та D.
  • Заголовок виводу, відділений рискою від всього що знизу, ідентифікує зображені гілки, в яких колонках позначаються їхні комміти, і символ обраний для позначення.

Дія яку ми б хотіли здійснити - оновити робочу гілку Z коммітами з головної - D. Іншими словами - перенести роботу зроблену в коммітах B, C та D в Z.

В інших системах контролю версій, таке може бути зроблено лише з використанням "об’єднання гілок". Насправді об’єднання може бути зроблено і в Git, викликом команди merge, і залишається потрібним, для випадків коли Z - опублікована гілка, і ми не хочемо змінювати її історію коммітів. Ось які команди запускають для об’єднання:

$ git checkout Z # перейти на гілку Z
$ git merge D # влити комміти B, C and D в Z

І ось так репозиторій виглядатиме після виконання цих команд:

Якщо б ми перевірили гілку Z зараз, вона б містила вміст попередньої гілки Z (тепер доступної як Z^), обєднаний з вмістом D. (Зауваження: справжня операція об’єднання вимагала б вирішення будь-яких конфліктів між станами D та Z).

Хоча нова Z зараз містить зміни з D, вона також містить новий комміт який представляє собою об’єднання Z з D: показаний тут як Z'. Цей комміт не додає нічого нового, але зберігає роботу здійснену для об’єднання D та Z. Це в певному сенсі "метакомміт", тому що його вміст стосується роботи здійсненої в репозиторії, а не якихось нових змін в робочому дереві.

Щоправда, є спосіб пересадити гілку Z прямо на D, так ніби переміщуючи її вперед в часі: використовуючи могутню команду rebase. Ось граф який ми хочемо отримати:

Такий стан справ найбільш прямо представляє те що ми б хотіли зробити: щоб наша локальна, розробницька гілка Z базувалась на останній роботі виконаній в головній гілці D. І саме тому команда називається rebase, бо вона змінює базовий комміт для гілки для якої її викликали. Якщо постійно її запускати, можна просувати вперед набір патчів протягом довільного часу, завжди крокуючи в ногу зі змінами в головній гілці, але без додавання необов’язкових коммітів з об’єднаннями в головну гілку[3]. Ось команди які треба запустити, на противагу merge показаному вище:

$ git checkout Z # перейти на гілку Z
$ git rebase D # змусити базовий комміт Z показувати на D

Чому це тільки для локальних гілок? Тому що щоразу коли ви змінюєте базу, ви потенційно змінюєте кожен комміт в гілці. Раніше, коли W базувався на A, він містив лише зміни потрібні для того щоб перетворити A в W. Після запуску rebase, W буде перезаписаним щоб містити зміни потрібні щоб перетворити D в W'. Навіть перетворення з W в X змінилось, тому що ланцюжок коммітів A+W+X тепер стає D+W'+X' - і так далі. Якщо б це була гілка зміни в якій вже бачили інші люди, і будь-хто з тих хто клонував ваш репозиторій створив собі локальні гілки що відгалужуються від Z, їхні гілки тепер показуватимуть на стару Z, а не на нову Z'.

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

Інтерактивний rebase[ред.]

Коли rebase був запущений вище, він автоматично переписав всі комміти від W до Z щоб перенести гілку Z на комміт D (тобто голову гілки D). Але ви можете отримати повний контроль над тим як це переписування відбувається. Якщо ви передасте команді rebase опцію -i, вона перенесе вас у буфер редагування, де ви зможете обрати що повинно бути зроблено для кожного комміта в локальній гілці Z:

pic
Це поведінка за замочуванням, яка обирається для кожного комміта в гілці якщо ви не використовуєте інтерактивний режим. Вона означає що комміт який розглядається, повинен застосовуватись до його (вже переписаного) батьківського комміта. Для кожного комміта що викликає конфлікти, rebase дає вам можливість їх вирішити.
squash
Розчавлений (squashed) комміт "складе" ввесь свій вміст у вміст комміта що передував йому. Це можна зробити будь-яку кількість разів. Якщо ви візьмете для прикладу гілку яку ми розглядали вище, і "розчавите" всі її комміти (окрім першого, який повинен бути pic, щоб мати можливість куди робити squash), ви отримаєте нову гілку Z, яка містить лише один комміт поверх D. Це корисно коли ви маєте зміни в багатьох коммітах, але хочете переписати історію так щоб показувати їх в одному комміті.
edit
Якщо ви позначите комміт як edit, процес ребейсу зупиниться на тому комміті і поверне вас в командний рядок з поточним робочим деревом яке відображає поточний комміт. Індекс матиме всі зміни комміту що застосуються коли ви викличете commit. Таким чином ви зможете зробити які завгодно зміни, і після комміта та запуску rebase --continue комміт буде переписаний так ніби ті зміни були здійснені оригінально.
(Видалити)
Якщо ви видалите комміт з інтерактивного файлу rebase, або якщо ви його закоментуєте, комміт просто зникне, так ніби його ніколи й не робили. Зауважте, що це може створювати конфлікти при мерджі, якщо будь-який з наступних коммітів в гілці буде залежати від змін у видаленому.

Потужність цієї команди важко оцінити спочатку, але вона дає вам майже необмежений контроль над виглядом будь-якої гілки. Її можна використовувати щоб:

  • Об’єднувати багато коммітів в один.
  • Перевпорядковувати комміти
  • Видаляти неправильні зміни
  • Переміщувати базу вашої гілки на будь-який інший комміт репозиторію.
  • Редагувати кожен комміт, щоб змінити його вміст через довгий час після створення.

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

Картинка показує наступне: ми маємо одну гілку розробки D, яка три комміти тому була відгалужена щоб почати розробку ризикованого функціоналу на Z. В певний момент посеред розробки, коли C та X були головами своїх гілок, ми вирішили почати інший експеримент який в кінцевому результаті дав L. Тепер ми вважаємо що код в L задовільний, але не достатньо для того щоб об’єднати його назад з головною гілкою, тому ми вирішили перемістити ті зміни в розробницьку гілку Z, так щоб виглядало що обидві зміни ми робили на одній гілці. О, і поки ми на ній, ми хочемо швиденько відредагуавти J, щоб змінити дату в копірайті, бо ми забули що це був 2015-тий коли ми робили зміну! Ось команди потрібні щоб розплутати цей вузол:

$ git checkout L
$ git rebase -i Z

Після вирішення всіх конфліктів що могли виникнути, тепер ми матимемо такий репозиторій:

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

Індекс: зустрічайте посередника[ред.]

Між файлами що знаходяться в файловій системі та блобами що знаходяться в репозиторії, знаходиться дещо дивна сутність - індекс. Його важко зрозуміти зокрема й тому, що ім'я, обране для нього - не зовсім вдале. Це індекс в тому розумінні що стосується недавно створених дерев та блобів, які були створені командою add. Скоро вони будуть прив'язані до дерева через створення комміту в репозиторії, але до того часу на них вказує лише індекс. Це означає що коли ви витрете реєстрацію зміни в індексі за допомогою команди reset, то відповідний блоб залишиться сиротою, і буде рано чи пізно видалений.

Індекс є просто зоною збирання (staging area), і має дуже гарну причину для свого існування: він дозволяє модель роботи незрозумілу користувачам CSV чи Subversion, і дуже знайому користувачам Darcs: можливість будувати комміт покроково.

Стан індексу стає деревом наступного комміту.

Файли, директорії та "hunk-и" (окремі зміни в файлах) додаються до індекса за допомогою git add та git add --patch

Спершу, дозвольте мені сказати що є спосіб майже повністю ігнорувати індекс: передаючи команді commit ключ -a. Наприклад погляньмо на те як працює Subversion. Коли ви набираєте svn status ви побачите набір дій що будуть застосовані до репозиторія при наступному виклику svn commit. Цей список дій певним чином є "неформальним індексом" що створюється порівнянням стану голови репозиторію зі станом поточного робочого дерева. Якщо файл foo.c було змінено, при наступному комміті ті зміни буде збережено. Якщо невідомий файл має навпроти свого імені в статусі знак запитання, цей файл буде проігноровано, але нові файли додані командою svn add буде додано до репозиторію.

Це не відрізняється від того що відбувається коли ви використовуєте git commit -a: нові невідомі файли ігноруються, але ті нові файли що були додані команою git add додаються до репозиторію, як і зміни в існуючих файлах. Така взаємодія майже ідентична способу роботи Subversion.

Справжньою відмінністю є те що в Subversion "список наступних дій" завжди визначається станом поточного робочого дерева. В Git, "список наступних дій" є вмістом індексу, який показує наступний стан HEAD, і яким можна маніпулювати до виклику commit. Це дає додатковий шар контролю за тим що має статись, дозволяючи накопичувати зміни покроково.

Якщо це ще не зрозуміло, то ось інший приклад: нехай у вас є файл foo.c і ви зробили в ньому два набори непов’язаних змін. Тепер ви хочете розтягнути ці зміни в два окремі комміти, кожен з своїм описом. Ось як це можна зробити в Subversion:

   $ svn diff foo.c > foo.patch
   $ vi foo.patch
   <відредагувати foo.patch залишивши лише зміни які я хочу закомітити пізніше>
   $ patch -p1 -R < foo.patch # відкотити другий набір змін
   $ svn commit -m "опис першого комміту"
   $ patch -p1 < foo.patch # застосувати другий набір змін знову
   $ svn commit -m "опис другого комміту"

Виглядає весело? А якщо повторити це кілька разів для складних наборів змін? Ось версія цього в Git, яка використовує індекс:

   $ git add --patch foo.c
   <вибрати частини що я хочу закомітити першими>
   $ git commit -m "опис першого комміту" 
   $ git add foo.c # додати решту змін
   $ git commit -m "опис другого комміту" 


Розглядаємо індекс ширше[ред.]

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

Якщо ви думаєте "Quilt!", ви цілком праві. Насправді індекс трошки відрізняється від Quilt[4], він має обмеження в тому що дозволяє будувати лише один патч за раз.

Але що, якщо замість двох наборів змін в foo.c, я мав би чотири? Зі звичайним Git я мав би вичесати зміни кожного поокремо, закомітити і перейти до вибирання змін наступного. Це стало набагато легшим з індексом, але що якщо я хочу перевірити ті зміни в різних комбінаціях одне з одним, перед тим як реєструвати їх в індексі? Тобто, якщо б я позначив патчі А, Б, В та Г, що якщо я б спершу хотів випробувати А + Б, потім А + В, а потім А + Г, і.т.д, перед тим як вирішити що набір змін повний?

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

Що потрібно для того щоб здійснювати щось таке, так це індекс, який дає більше ніж один комміт за раз. Це саме те що Stacked Git[5] надає.

Ось як я закоммічу два різні патчі в моє робоче дерево використовуючи звичайний Git:

   $ git add -i # обрати перший набір змін
   $ git commit -m "опис першого комміту" 
   $ git add -i # обрати другий набір змін
   $ git commit -m "опис другого комміту" 

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

   $ git log # знайти ідентифікатор першого комміта
   $ git checkout -b work <ідентифікатор першого комміта>
   $ git cherry-pick <ідентифікатор другого комміта>
   < запустити тести>
   $ git checkout master # знову перейти до гілки master
   $ git branch -D work # видалити тимчасову гілку

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

   $ stg new patch1
   $ git add -i # обрати перший набір змін
   $ stg refresh --index 
   $ stg new patch2
   $ git add -i # обрати другий набір змін
   $ stg refresh --index 

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

   $ stg applied
   patch1
   patch2
   < зробити тести використовуючи обидва патчі >
   $ stg pop patch1
   < зробити тести використовуючи лише другий патч >
   $ stg pop patch2
   $ stg push patch1
   < зробити тести використовуючи лише перший патч >
   $ stg push -a
   $ stg commit -a # закомітити всі патчі

Це набагато простіше ніж створювати тимчасові гілки, використовувати cherry-pick щоб обирати конкретні комміти та видаляти тимчасові гілки.

reset, чи не reset[ред.]

Однією з більш складних до опанування команд в Git є reset, яка здається кусає людей набагато частіше ніж інші команди. Що зрозуміло, так як вона має потенціал змінювати як ваше поточне робоче дерево, так і посилання HEAD. Тому я подумав що короткий огляд цієї команди буде корисним.

Загалом, reset є редактором посилань, редактором індексу та редактором робочого дерева. Частково через це він такий заплутаний, бо може виконувати так багато різних завдань. Давайте розглянемо відмінності між цими трьома режимами, та як вони вписуються в модель коммітів Git.

Виконання mixed reset[ред.]

Якщо ви використаєте опцію --mixed (або не пропишете жодної опції, так як вона використовується за замовчуванням), reset поверне частини вашого індексу та посилання HEAD до стану які відповідають заданому комміту. Єдина відмінність від --soft в тому, що --soft змінює значення HEAD і не чіпає індекс.

$ git add foo.c  # додати зміни в індекс як новий блоб
$ git reset HEAD # видалити будь-які зміни в індексі
$ git add foo.c  # ми помилились, додати назад

Виконання soft reset[ред.]

Якщо ви використаєте опцію --soft, то це буде так само як просто змінити посилання HEAD на інший комміт. Зміни в робочому дереві залишаться не торканими. Це означає що наступні дві команди еквівалентні:

$ git reset --soft HEAD^       # оновлює HEAD значенням його батька
                               # що еквівалентно ігноруванню останнього комміта.
$ git update-ref HEAD HEAD^    # робить те саме, але вручну

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

Але майте на увазі: якщо хтось клонував ваш репозиторій, і вони здійснили роботу на вершині вашого попереднього HEAD - того що ви відкинули - то зміни HEAD які ми здійснили змусять мердж відбутись автоматично після їхнього наступного пулу. Нижче зображено як виглядатиме репозиторій після soft reset та нового комміту:

А ось як виглядатиме клон вашого репозиторію, після того як його власник знову зробить пул, кольори показуюь наскільки різні комміти співпадають:

Виконання hard reset[ред.]

hard reset (опція --hard) має потенціал бути дуже небезпечним, там як він здатен робити двоє різних речей водночас. Найперше, якщо ви робите hard reset до вашого поточного HEAD, воно стре всі зміни в робочому дереві, так що ваші файли будуть відповідати вмісту HEAD.

Існує інша команда, checkout, яка працює так само як reset --hard якщо індекс порожній. В іншому випадку вона приводить ваше робоче дерево у відповідність до індексу.

Ну а якщо ви робитимете hard reset до старішого комміту, це буде так само як спершу зробити soft reset а тоді використати reset --hard щоб оновити стан робочого дерева. Таким чином, наступні команди еквівалентні:

$ git reset --hard HEAD~3   # Повернутись назад в часі відкидаючи зміни

$ git reset --soft HEAD~3   # Встановити HEAD вказівником на старіший комміт
$ git reset --hard          # Витерти всі відмінності в робочому дереві

Як ви можете бачити, виконання hard reset може бути дуже деструктивним. На щастя, існує безпечніший спосіб досягти того ж ефекту, використовуючи git stash (детальніше в наступному розділі):

$ git stash
$ git checkout -b new-branch HEAD~3 # голову надад в часі

Цей підхід має дві окремі переваги, якщо ви не цілком впевнені чи ви хочете змінити поточну гілку прямо зараз:

  1. Він зберігає вашу поточну роботу в сховок (stash), і ви можете повернутись назад до неї в будь-який час. Зауважте що сховок не пов’язаний з гілками, тому ви можете зберегти в ньому стан робочого дерева на одній гілці, а потім застосувати його до іншої.
  2. Він відкочує ваше робоче дерево до минулого стану, але на новій гілці, тож коли ви вирішите закомітити зміни старого стану, ви не зачепите оригінальну гілку.

Якщо ви додасте зміни в гілку new-branch а потім вирішите що хочете щоб вона була вашою новою гілкою master, виконайте наступні команди:

$ git branch -D master             # прощавай, стара master (зберігатиметься в reflog)
$ git branch -m new-branch master  # new-branch стає новою master

Мораль цієї історії така: хоча ви можете робити серйозні операційні втручання в вашу поточну гілку використовуючи reset --soft та reset --hard (яка також змінить стан робочого дерева), навіщо вам це робити? Git робить роботу з гілками такою простою і дешевою, що майже завжди варто робити будь-які деструктивні зміни в окремій гілці, і тільки потім замінити основну гілку цією.

І що коли ви випадково виконаєте reset --hard, втративши не тільки ваші поточні зміни, а й видаливши комміти з гілки master? Ну, якщо у вас не було звички використовувати stash, щоб зробити знімки робочого дерева (про це в наступному розділі), ви вже нічого не зробите щоб відновити втрачене робоче дерево. Але ви можете відновити стан гілки master, знову використовуючи reset --hard до reflog (про це також в наступному розділі):

$ git reset --hard HEAD@{1}   # відновити з reflog перед останньою зміною

Щоб залишатись в безпечній зоні, ніколи не використовуйте reset --hard не виконавши перед цим stash. Це збереже вам багато нервів в майбутньому. Якщо ви таки виконали stash, то ви можете використати його щоб відновити також зміни робочого дерева:

$ git stash                   # тому що це завжди гарна ідея
$ git reset --hard HEAD~3     # повернутись назад в часі
$ git reset --hard HEAD@{1}   # ой, це була помилка, відмінити її!
$ git stash apply             # і повернути назад мої зміни робочого дерева

Останні ланки ланцюга: Stash та reflog[ред.]

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

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

   $ git reflog
   5f1bc85...  HEAD@{0}: commit (initial): Initial commit

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

Інше місце де можуть існувати блоби, хоча й непрямо - в вашому робочому дереві. Ось що я маю на увазі: нехай ви змінили файл foo.c але ще не додали ці зміни до індексу. Git може й не створив для вас блоб, але ці зміни існують, як і їх вміст, просто лише у вашій файловій системі замість репозиторію Git. Файл навіть має свій власний хеш SHA1, незважаючи на те що не існує реального блобу. Його можна переглянути наступною командою:

   $ git hash-object foo.c
   <якийсь хеш>

Що це дає нам? Ну, якщо ви працюєте над робочим деревом, і робочий день підходить до кінця, гарною звичкою буде сташити ваші зміни:

   $ git stash

Це забере ввесь вміст вашої директорії - включаючи як і робоче дерево, так і стан індексу - і створить блоби для них в репозиторії git, дерево для того щоб тримати ті блоби і пару сташ-комітів щоб зберігати робоче дерево та індекс і записати час коли ви зробили сташ.

Це гарна практика, тому що хоча й ви витягнете всі зміни назад наступного робочого дня за допомогою stash apply, ви будете мати рефлог всіх своїх сташнутих змін наприкінці кожного дня. Ось що ви зробите після того як прийдете на роботу наступного ранку (WIP тут означає Work in progress (роботу в процесі)):

   $ git stash list
   stash@{0}: WIP on master: 5f1bc85...  Initial commit
   
   $ git reflog show stash # такий самий вивід, плюс хеш сташ комміта
   2add13e...  stash@{0}: WIP on master: 5f1bc85...  Initial commit
   
   $ git stash apply

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


   $ git stash list
   stash@{0}: WIP on master: 1ca9d63 Initial commit
   ...
   stash@{32}: WIP on master: с7a9083 Initial commit
   $ git log stash@{32}         # коли я це зробив?
   $ git show stash@{32}        # покажіть над чим я працював
   $ git checkout -b temp stash@{32} # давайте подивимось на те старе робоче дерево

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

Якщо ви колись захочете очистити свій список сташів - наприклад щоб зберігати лише останні 30 днів активності - не використовуйте stash clear, використовуйте натомість reflog expire:

   $ git stash clear # НІ!!!! Ви втратите всю цю історію
   $ git reflog expire --expire=30.days refs/stash
   <виводить сташі що були залишені>

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

   $ cat <<EOF > /usr/local/bin/git-snapshot
   #!/bin/sh
   git stash && git stash apply
   EOF
   $ chmod +x $_
   $ git snapshot

Немає причини через яку ви не могли б запускати це як cron job щогодини, разом з запуском команди reflog expire щотижня чи місяця.

Висновки[ред.]

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

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

Зноски[ред.]

  1. Насправді команда checkout копіює вміст з репозиторію в індекс, а потім в робоче дерево. Але так як користувач ніколи не бачить таке використання індексу під час отримання стану робочого дерева, я вирішив що буде більше сенсу не відображати це на діаграмі.
  2. Ну й мітки, але мітки це лише вигадливі посилання на комміти, і можуть бути проігнорованими на даний момент
  3. Зауважте, що є вагомі причини не робити так, і використовувати натомість merge. Вибір залежить від вашої ситуації. Одним з недоліків зміни базового комміту є те, що навіть якщо робоче дерево після зміни бази компілюється, не гарантовано що проміжні комміти будуть все ще компілюватись, тому що вони ніколи не компілювались в своєму зміненому стані нової бази. Якщо історична достовірність для вас важлива - віддавайте перевагу об’єднанню.
  4. http://savannah.nongnu.org/projects/quilt
  5. http://procode.org/stgit