-
1. Вступ
- 1.1 Про систему контролю версій
- 1.2 Коротка історія Git
- 1.3 Основи Git
- 1.4 Git, зазвичай, тільки додає дані
- 1.5 Три стани
- 1.6 Командний рядок
- 1.7 Інсталяція Git
- 1.8 Початкове налаштування Git
- 1.9 Отримання допомоги
- 1.10 Підсумок
-
2. Основи Git
- 2.1 Створення Git-репозиторія
- 2.2 Запис змін до репозиторія
- 2.3 Перегляд історії комітів
- 2.4 Скасування речей
- 2.5 Взаємодія з віддаленими сховищами
- 2.6 Теґування
- 2.7 Псевдоніми Git
- 2.8 Підсумок
-
3. Галуження в git
- 3.1 Гілки у кількох словах
- 3.2 Основи галуження та зливання
- 3.3 Управління гілками
- 3.4 Процеси роботи з гілками
- 3.5 Віддалені гілки
- 3.6 Перебазовування
- 3.7 Підсумок
-
4. Git на сервері
- 4.1 Протоколи
- 4.2 Отримання Git на сервері
- 4.3 Генерація вашого публічного ключа SSH
- 4.4 Налаштування Серверу
- 4.5 Демон Git
- 4.6 Розумний HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 Варіанти стороннього хостингу
- 4.10 Підсумок
-
5. Розподілений Git
-
6. GitHub
-
7. Інструменти Git
- 7.1 Вибір ревізій
- 7.2 Інтерактивне індексування
- 7.3 Ховання та чищення
- 7.4 Підписання праці
- 7.5 Пошук
- 7.6 Переписування історії
- 7.7 Усвідомлення скидання (reset)
- 7.8 Складне злиття
- 7.9 Rerere
- 7.10 Зневадження з Git
- 7.11 Підмодулі
- 7.12 Пакування
- 7.13 Заміна
- 7.14 Збереження посвідчення (credential)
- 7.15 Підсумок
-
8. Налаштування Git
-
9. Git and Other Systems
- 9.1 Git як клієнт
- 9.2 Міграція на Git
- 9.3 Підсумок
-
10. Git зсередини
- 10.1 Кухонні та парадні команди
- 10.2 Об’єкти Git
- 10.3 Посилання Git
- 10.4 Файли пакунки
- 10.5 Специфікація посилань (refspec)
- 10.6 Протоколи передачі
- 10.7 Супроводження та відновлення даних
- 10.8 Змінні середовища
- 10.9 Підсумок
-
A1. Додаток A: Git в інших середовищах
- A1.1 Графічні інтерфейси
- A1.2 Git у Visual Studio
- A1.3 Git в Eclipse
- A1.4 Git у Bash
- A1.5 Git у Zsh
- A1.6 Git у Powershell
- A1.7 Підсумок
-
A2. Додаток B: Вбудовування Git у ваші застосунки
- A2.1 Git з командного рядка
- A2.2 Libgit2
- A2.3 JGit
- A2.4 go-git
-
A3. Додаток C: Команди Git
- A3.1 Налаштування та конфігурація
- A3.2 Отримання та створення проектів
- A3.3 Базове збереження відбитків
- A3.4 Галуження та зливання
- A3.5 Поширення й оновлення проектів
- A3.6 Огляд та порівняння
- A3.7 Зневаджування
- A3.8 Латання (patching)
- A3.9 Електронна пошта
- A3.10 Зовнішні системи
- A3.11 Адміністрування
- A3.12 Кухонні команди
7.7 Інструменти Git - Усвідомлення скидання (reset)
Усвідомлення скидання (reset)
До того, як переходити до більш спеціалізованих інструментів, поговорімо про команди Git reset
(скинути) та checkout
(отримати).
Ці дві команди найбільше збивають з пантелику, особливо, коли ви вперше ними користуєтесь.
Вони роблять так багато всього, що спроба дійсно зрозуміти їх та використовувати правильно здається безнадійною.
Щоб усе ж таки це зробити, ми пропонуємо просту метафору.
Три дерева
Набагато легше зрозуміти reset
та checkout
, якщо уявити, що Git керує вмістом трьох різних дерев.
“Деревом” ми тут називатимемо “колекцію файлів”, а не саме структуру даних.
(Є декілька випадків, в яких індекс насправді не поводиться як дерево, проте легше поки що уявляти його так.)
Git як система керує та маніпулює трьома деревами під час нормальної роботи:
Дерево | Роль |
---|---|
HEAD |
Знімок останнього коміту, наступний батько |
Індекс |
Пропонований знімок наступного коміту |
Робоча Директорія |
Пісочниця |
HEAD
HEAD є вказівником на посилання поточної гілки, яка у свою чергу вказує на останній коміт, що був зроблений у цій гілці. Це означає, що HEAD буде батьком наступного створеного коміту. Зазвичай найпростіше думати про HEAD як про знімок останнього коміту в цій гілці.
Насправді, доволі легко побачити, як цей знімок виглядає. Ось приклад отримання списку файлів директорії та SHA-1 суми кожного файла в знімку HEAD:
$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon 1301511835 -0700
committer Scott Chacon 1301511835 -0700
initial commit
$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152... README
100644 blob 8f94139338f9404f2... Rakefile
040000 tree 99f1a6d12cb4b6f19... lib
Команди Git cat-file
та ls-tree
є командами низького рівня, якими не дуже користуються в повсякденній роботі, проте вони корисні при зʼясуванні того, що насправді коїться.
Індекс
Індекс — це пропозиція наступного коміту.
Ця концепція Git також має назву “Область Додавання”, адже саме сюди дивиться Git при виконанні git commit
.
Git заповнює цей індекс списком усього вмісту файлів, що був отриманий зі сховища до вашої робочої теки востаннє та яким він тоді був.
Потім ви замінюєте деякі з цих файлів новими версіями, та git commit
перетворює це на дерево для нового коміту.
$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README
100644 8f94139338f9404f26296befa88755fc2598c289 0 Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0 lib/simplegit.rb
Ми знову скористалися git ls-files
, яка зазвичай виконується за лаштунками та показує як наразі виглядає індекс.
Технічно, індекс не є деревом — насправді його реалізовано як сплощений маніфест, проте для нас це достатньо близько.
Робоча директорія
Нарешті, є ваша робоча директорія.
Інші два дерева зберігають свій вміст у ефективний проте незручний спосіб: усередині теки .git
.
Робоча Директорія розпаковує їх до реальних файлів, що дозволяє їх редагувати набагато легше.
Вважайте Робочу Директорію пісочницею, де ви можете спробувати зміни до того, як додати їх до індексу, а потім до історії.
$ tree
.
├── README
├── Rakefile
└── lib
└── simplegit.rb
1 directory, 3 files
Робочий процес
Головне призначення Git - записувати знімки вашого проекту в послідовно кращих станах за допомогою маніпулювання цими трьома деревами.
![reset workflow](png/reset-workflow.png)
Уявімо собі цей процес: припустімо, ви переходите до нової директорії з єдиним файлом у ній.
Ми назвемо цю версію файла v1, та будемо позначати її синім.
Тепер виконаємо git init
, що створить сховище Git з посиланням HEAD, що вказує на ненароджену гілку (master
ще не існує).
![reset ex1](png/reset-ex1.png)
Наразі тільки в дереві Робочої Директорії є якийсь вміст.
Тепер ми бажаємо зробити коміт з цим файлом, отже ми використовуємо git add
щоб взяти вміст з Робочої Директорії та скопіювати його до Індексу.
![reset ex2](png/reset-ex2.png)
Потім виконуємо git commit
, що бере вміст Індексу та зберігає його в незмінному знімку, створює обʼєкт коміту, що вказує на цей знімок, та оновлює master
, щоб той вказував на цей коміт.
![reset ex3](png/reset-ex3.png)
Якщо ми виконаємо git status
, то не побачимо ніяких змін, адже всі три дерева однакові.
Тепер ми хочемо зробити зміну в цьому файлі та зберегти їх у коміті. Ми пройдемо той самий процес. Спочатку змінимо файл у робочій директорії. Назвемо цю версію файла v2, та позначимо її червоним.
![reset ex4](png/reset-ex4.png)
Якщо ми зараз виконаємо git status
, то побачимо файл червоним у “Changes not staged for commit”, адже в цього елемента є різниця між Індексом та Робочою Директорією.
Далі виконуємо git add
на ньому, щоб додати його до Індекса.
![reset ex5](png/reset-ex5.png)
Тепер, якщо ми виконаємо git status
, то побачимо файл зеленим під “Changes to be commited”, адже Індекс та HEAD різняться — тобто, наш пропонований наступний коміт зараз відрізняється від останнього коміту.
Нарешті, виконуємо git commit
щоб завершити коміт.
![reset ex6](png/reset-ex6.png)
Тепер git status
нічого не виведе, адже всі три дерева знову однакові.
Переключення гілок та клонування проходить за схожим процесом. Коли ви отримуєте (checkout) гілку, Git перенаправляє HEAD до нового посилання гілки, заповнює Індекс знімком того коміту, далі копіює вміст Індексу до Робочої Директорії.
Роль скидання (reset)
Команду reset
легше зрозуміти в цьому контексті.
Задля наступних прикладів, скажімо ми змінили файл file.txt
знову та зробили третій коміт.
Отже тепер наша історія виглядає так:
![reset start](png/reset-start.png)
Розгляньмо ґрунтовно що саме робить команда reset
при виклику.
Вона безпосередньо змінює ці три дерева в простий та передбачуваний спосіб.
Вона здійснює три базові операції.
Крок 1: перемістити HEAD
Спершу reset
перемістить те, на що вказує HEAD.
Це не те саме, що змінити сам HEAD (це робить checkout
). reset
пересуває гілку, на яку вказує HEAD.
Це означає, що якщо HEAD вказує на гілку master
(тобто гілка master
є поточною), то виконання git reset 9e5e6a4
почнеться зі зміни вказівника master
так, що він буде вказувати на 9e5e6a4
.
![reset soft](png/reset-soft.png)
Байдуже яку форму reset
викликано, все одно це перше, що команда спробує зробити.
Виклик reset --soft
просто зупиниться на цьому.
Тепер подивіться хвильку на зображення та усвідомте, що сталося: суттєво остання команда git commit
була скасована.
При виконанні git commit
, Git створює новий коміт та пересуває до нього гілку, на яку вказує HEAD.
Коли ви робите reset
назад до HEAD~
(батько HEAD), ви повертаєтесь туди, де були, без зміни Індексу чи Робочої Директорії.
Тепер можна оновити Індекс та знову виконати git commit
, щоб здійснити те, що можна зробити за допомогою git commit --amend
(дивіться Зміна останнього коміту).
Крок 2: оновлення індексу (--mixed)
Зауважте, що при виконанні git status
ви побачити зеленим різницю між Індексом та новим HEAD.
Наступне, що зробить reset
— оновить Index вмістом знімку, на який тепер вказує HEAD.
![reset mixed](png/reset-mixed.png)
Якщо ви використаєте опцію --mixed
, то reset
на цьому зупиниться.
Те саме буде і без опції, отже якщо ви не будете надавати ніяких опцій (просто git reset HEAD~
у даному випадку), тут команда і зупиниться.
Тепер подивіться ще хвильку на це зображення, щоб зрозуміти що сталося: ви все одно скасували останню commit
, проте також деіндексували все.
Ви відкотили до стану до виконання git add
та git commit
.
Крок 3: оновлення робочої теки (--hard)
Третє, що робить reset
— змушує Робочу Директорію виглядати як Індекс.
Якщо ви використаєте опцію --hard
, Git виконає і цей крок.
![reset hard](png/reset-hard.png)
Поміркуймо що щойно сталося.
Ви скасували останній коміт, команди git add
і git commit
, і всю працю, яку ви робили у вашій робочій директорії.
Важливо зазначити, що ця опція (--hard
) єдиний шлях зробити reset
небезпечним, і один з дуже нечисленних випадків, в яких Git може дійсно знищити дані.
Будь-який інший виклик reset
можна доволі легко скасувати, проте опцію --hard
не можна, адже вона насилу переписує файли в Робочій Директорії.
У даному окремому випадку, ми досі маємо версію v3 нашого файлу в коміті в нашій базі даних Git, і могли б відновити її за допомогою reflog
, проте якби ми не зробили були коміт, Git все одно переписав би файли та ми ніяк не змогли б відновити нашу працю.
Короткий підсумок
Команда reset
переписує ці три дерева у заданому порядку, та зупиняється де ви їй скажете:
-
Пересунути гілку, на яку вказує HEAD (зупинитися тут, якщо
--soft
) -
Зробити так, щоб Індекс виглядав як HEAD (зупинитися тут, якщо не
--hard
) -
Зробити так, щоб Робоча Директорія виглядала як Індекс
Скидання зі шляхом
Ми розглянули поведінку reset
у базовому випадку, проте їй також можна передати шлях, що його треба обробити.
Якщо задати шлях, reset
пропустить крок 1, та обмежить решту своїх дій файлом або декількома файлами.
Це доволі розумно — HEAD є просто вказівником та не може вказувати на частину одного коміту та частину іншого.
Проте Індекс та Робоча директорія можуть бути частково оновленими, отже скидання йде далі до кроків 2 та 3.
Отже, припустіть, що ми виконали git reset file.txt
.
Цей формат (адже ви не вказали SHA-1 коміту або гілку, та не задали --soft
або --hard
) є скороченням git reset --mixed HEAD file.txt
, що:
-
Пересунути гілку, на яку вказує HEAD (пропущено)
-
Зробити так, щоб Індекс виглядав як HEAD (зупинитися тут)
Отже суттєво просто копіює file.txt
з HEAD до Індексу.
![reset path1](png/reset-path1.png)
Це має ефект деіндексації файла.
Якщо подивитися на зображення, та згадати, що робить git add
, то зрозуміло що вони роблять цілковито протилежні речі.
![reset path2](png/reset-path2.png)
Ось чому вивід команди git status
пропонує виконувати цю команду для деіндексації файла.
(Докладніше в Вилучання файла з індексу.)
Ми могли б так же легко не давати Git припускати, що ми маємо на увазі “візьми дані з HEAD”, якби б задали окремий коміт, з якого треба брати версію.
Ми могли просто виконати щось на кшталт git reset eb43bf file.txt
.
![reset path3](png/reset-path3.png)
Ця досягає такого ж ефекту, ніби ми повернули вміст файлу до v1 у Робочій Директорії, виконали на ньому git add
, а потім повернули його вміст до v3 знов (тільки без проходження всіх цих кроків).
Якщо тепер виконати git commit
, буде записано зміну, що повертає файл до стану v1, хоча ми ніколи не мали його в такому стані в Робочій Дирикторії.
Також цікаво знати, що як і команда git add
, reset
приймає опцію --patch
щоб деіндексувати вміст файлу по шматкам.
Отже ви можете вибірково деіндексувати або скасовувати вміст.
Зварювання (squashing)
Подивімося на те, як можна зробити щось цікаве цією винайденою можливістю — зварювання комітів.
Припустімо у вас є послідовність комітів з повідомленнями “oops.”, “WIP” та “forgot this file” (забув цей файл).
Ви можете використати reset
щоб швидко та легко зварити їх в один коміт, що дозволяє вам виглядати дійсно витонченим.
(У Зварювання комітів йдеться про інший спосіб це зробити, проте в даному випадку легше скористатися reset
.)
Скажімо у вас є проект, в якому перший коміт містить один файл, другий додав новий файл та змінив перший, а третій знову змінив перший файл. Другий коміт був незавершеною працею та ви бажаєте його зварити з останнім.
![reset squash r1](png/reset-squash-r1.png)
Ви можете виконати git reset --soft HEAD~2
щоб перемістити гілку HEAD назад до старшого коміту (найновішого коміту, який ви бажаєте залишити):
![reset squash r2](png/reset-squash-r2.png)
Та просто знову виконати git commit
:
![reset squash r3](png/reset-squash-r3.png)
Тепер, як бачите, ваша досяжна історія, історія яку ви будете викладати, тепер виглядає ніби ви зробили були лише коміт з file-a.txt
першої версії, потім другий коміт та коміт, що змінив file-a.txt
до третьої версії та додав file-b.txt
.
Коміт з другою версією файла більше не існує в історії.
Отримання (checkout)
Нарешті, вам може бути цікаво в чому різниця між checkout
та reset
.
Як і reset
, checkout
маніпулює трьома деревами, проте трохи по-іншому в залежності від того, даєте ви їй окремі файли чи ні.
Без окремих файлів
Виконання git checkout [гілка]
дуже схоже на виконання git reset --hard [гілка]
в тому, що оновлює всі три дерева до [гілки]
, проте є дві важливих відмінності.
По-перше, на відміну від reset --hard
, checkout
безпечна команда щодо робочої директорії. Вона спершу перевіряє, що не зіпсує ніяких файлів, в яких є зміни.
Насправді, вона навіть трохи кмітливіша — вона намагається зробити просте злиття в Робочій Директорії, отже всі файли, які ви не змінювали, будуть оновлені.
reset --hard
, з іншого боку, просто замінить все не розбираючи та не перевіряючи.
Друга важлива відмінність у тому, як checkout
оновлює HEAD.
reset
переміщує гілку, на яку вказує HEAD, а checkout
натомість переміщує сам HEAD, щоб той вказував на іншу гілку.
Наприклад, скажімо в нас є гілки master
та develop
, які вказують на різні коміти, та поточною гілкою є develop
(отже HEAD на неї вказує).
Якщо виконати git reset master
, то сам develop
почне вказувати на той самий коміт, що й master
.
Якщо ж виконати git checkout master
, то пересувається не develop
, а HEAD.
HEAD почне вказувати на master
.
Отже, в обох випадках ми переміщуємо HEAD до коміту A, проте як ми це робимо дуже різниться.
reset
переміщує гілку, на яку вказує HEAD, а checkout
переміщує сам HEAD.
![reset checkout](png/reset-checkout.png)
З файлами
Інший спосіб виконати команду checkout
— це надати йому шляхи файлів, що, як і з reset
, призведе до збереження HEAD.
Вона поводиться так само як git reset [гілка] файл
в тому, що оновлює індекс файлом з того коміту, проте також переписує файл у робочій директорії.
Вона була б повністю рівнозначна git reset --hard [гілка] файл
(якби б reset
це дозволяв) — вона небезпечна для робочою директорії, та не переміщує HEAD.
Також, як і git reset
та git add
, checkout
розуміє опцію --patch
, що дозволяє вибірково повертати вміст файла по шматкам.
Підсумок
Сподіваємось, що тепер ви розумієте і почуваєтесь комфортніше з командою reset
, проте ймовірно досі трохи спантеличені, чим саме вона відрізняється від checkout
та навряд чи запам’ятали всі правила різноманітних форм викликів.
Ось шпаргалка щодо того, як команди впливають на які дерева. Колонка “HEAD” має значення “ПОС” (посилання), якщо переміщує посилання (гілку), на яке вказує HEAD, або “HEAD”, якщо переміщує сам HEAD. Приділить особливу увагу колонці РД у Безпеці? (РД - робоча директорія) — якщо в ній НІ, двічі поміркуйте перш ніж виконати команду.
HEAD | Індекс | Робоча Директорія | РД у Безпеці? | |
---|---|---|---|---|
Рівень Комітів |
||||
|
ПОС |
НІ |
НІ |
ТАК |
|
ПОС |
ТАК |
НІ |
ТАК |
|
ПОС |
ТАК |
ТАК |
НІ |
|
HEAD |
ТАК |
ТАК |
ТАК |
Рівень Файлів |
||||
|
НІ |
ТАК |
НІ |
ТАК |
|
НІ |
ТАК |
ТАК |
НІ |