Дослідження історії

Останнє оновлення 2024-06-04 | Редагувати цю сторінку

Приблизний час: 25 хвилин

Огляд

Питання

  • Як визначити старі версії файлів?
  • Як переглянути свої зміни?
  • Як відновити старі версії файлів?

Цілі

  • Пояснення, що таке HEAD репозиторію та як його використовувати.
  • Визначення та використання ідентифікаторів комітів Git.
  • Порівняння різних версій відстежуваних файлів.
  • Відновлення старих версій файлів.

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

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

BASH

$ nano mars.txt
$ cat mars.txt

ВИХІД

Cold and dry, but everything is my favorite color
The two moons may be a problem for Wolfman
But the Mummy will appreciate the lack of humidity
An ill-considered change

Тепер подивимося на наш результат.

BASH

$ git diff HEAD mars.txt

ВИХІД

diff --git a/mars.txt b/mars.txt
index b36abfd..0848c8d 100644
--- a/mars.txt
+++ b/mars.txt
@@ -1,3 +1,4 @@
 Cold and dry, but everything is my favorite color
 The two moons may be a problem for Wolfman
 But the Mummy will appreciate the lack of humidity
+An ill-considered change.

Це те саме, що ви отримаєте, якщо пропустите HEAD (спробуйте це).
Але справжня користь полягає у тому, що ви таким чином можете посилатися на попередні коміти. Наприклад, додаючи ~1 (де “~” - це тільда), ми посилаємось на коміт зроблений безпосередньо перед HEAD.

BASH

$ git diff HEAD~1 mars.txt

Якщо ми хочемо побачити різницю між старішими комітами, ми можемо знову використовувати git diff, використовуючи для послання на них HEAD~1, HEAD~2 тощо:

BASH

$ git diff HEAD~3 mars.txt

ВИХІД

diff --git a/mars.txt b/mars.txt
index df0654a..b36abfd 100644
--- a/mars.txt
+++ b/mars.txt
@@ -1 +1,4 @@
 Cold and dry, but everything is my favorite color
+The two moons may be a problem for Wolfman
+But the Mummy will appreciate the lack of humidity
+An ill-considered change

Ми також можемо використати команду git show, яка показує які зміни ми внесли у будь-якому попередньо зробленому коміті, а також і повідомлення коміту (на відміну від команди git diff яка покаже різницю між комітом та нашим робочим каталогом).

BASH

$ git show HEAD~3 mars.txt

ВИХІД

commit f22b25e3233b4645dabd0d81e651fe074bd8e73b
Author: Vlad Dracula <vlad@tran.sylvan.ia>
Date:   Thu Aug 22 09:51:46 2013 -0400

    Start notes on Mars as a base

diff --git a/mars.txt b/mars.txt
new file mode 100644
index 0000000..df0654a
--- /dev/null
+++ b/mars.txt
@@ -0,0 +1 @@
+Cold and dry, but everything is my favorite color

Таким чином, ми можемо побудувати ланцюжок комітів. Останній кінець ланцюжка називається HEAD; ми можемо посилатися на попередні коміти, використовуючи нотацію ~, тому HEAD~1 означає “попередній коміт”, тоді як HEAD~123 повертається на 123 коміти назад від того місця, де ми зараз знаходимось.

Ми також можемо вказувати на коміти, використовуючи ті довгі рядки цифр і букв, які показує git log. Це унікальні ідентифікатори змін, де “унікальний” насправді означає унікальний: кожна зміна будь-якого набору файлів на будь-якому комп’ютері буде мати унікальний 40-символьний ідентифікатор. Наш перший коміт отримав ідентифікатор f22b25e3233b4645dabd0d81e651fe074bd8e73b, тож спробуймо наступне:

BASH

$ git diff f22b25e3233b4645dabd0d81e651fe074bd8e73b mars.txt

ВИХІД

diff --git a/mars.txt b/mars.txt
index df0654a..93a3e13 100644
--- a/mars.txt
+++ b/mars.txt
@@ -1 +1,4 @@
 Cold and dry, but everything is my favorite color
+The two moons may be a problem for Wolfman
+But the Mummy will appreciate the lack of humidity
+An ill-considered change

Це правильна відповідь, проте, введення випадкових рядків з 40 символів дуже незручно. Тож Git дозволяє нам використовувати тільки перші кілька символів (як правило, сім для проєктів нормального розміру):

BASH

$ git diff f22b25e mars.txt

ВИХІД

diff --git a/mars.txt b/mars.txt
index df0654a..93a3e13 100644
--- a/mars.txt
+++ b/mars.txt
@@ -1 +1,4 @@
 Cold and dry, but everything is my favorite color
+The two moons may be a problem for Wolfman
+But the Mummy will appreciate the lack of humidity
+An ill-considered change

Це чудово! Отже, ми можемо зберегти зміни у файлах і побачити, що ми змінили. Наступне питання - а як ми можемо відновити їх старіші версії? Припустимо, що ми передумали щодо останнього оновлення файлу mars.txt (рядок “ill-considered change”).

git status зараз каже нам, що файл був змінений, але ці зміни не були додані до зони стейджингу:

BASH

$ git status

ВИХІД

On branch main
Changes not staged for commit:
   (use "git add/rm <file>..." to update what will be committed)
   (use "git checkout -- <file>..." to discard changes in working directory)

    deleted:    nibiru.txt

no changes added to commit (use "git add" and/or "git commit -a")

Отже, ми можемо повернути цей файл до його попереднього стану за допомогою git checkout:

BASH

$ git checkout HEAD mars.txt
$ cat mars.txt

ВИХІД

Cold and dry, but everything is my favorite color
The two moons may be a problem for Wolfman
But the Mummy will appreciate the lack of humidity

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

BASH

$ git checkout f22b25e mars.txt

BASH

$ cat mars.txt

ВИХІД

Cold and dry, but everything is my favorite color

BASH

$ git status

ВИХІД

On branch main
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   mars.txt

Звернуть увагу, що зміни зараз знаходяться у зоні стейджінгу. Знову ж таки, ми можемо повернути усе як було за допомогою git checkout:

BASH

$ git checkout HEAD mars.txt

Не втрачайте голову (тобто, HEAD)

Вище ми використовували

BASH

$ git checkout f22b25e mars.txt

щоб повернути mars.txt до його стану після коміту f22b25e. Проте, будьте обережні! Команда checkout має інші важливі варіанти використання, та Git не зрозуміє ваших намірів, якщо ви будете неточно вводити команди. Наприклад, якщо ви забудете mars.txt у попередній команді.

BASH

$ git checkout f22b25e

ПОМИЛКА

Note: checking out 'f22b25e'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

 git checkout -b <new-branch-name>

HEAD is now at f22b25e Start notes on Mars as a base

Таким чином, стан “detached HEAD” схожий на “дивися, але не чіпай”, тому ви не повинні робити жодних змін у цьому стані. Після дослідження стану вашого репозиторію у минулому, знову “приєднайте” свій HEAD за допомогою команди git checkout main.

Важливо пам’ятати, що ми повинні використовувати номер коміту, який ідентифікує стан репозиторію до зміни, яку ми намагаємося скасувати. Поширеною помилкою є використання номеру коміту, в якому ми зробили зміни, які намагаємося скасувати. У наведеному нижче прикладі ми хочемо отримати стан перед самим останнім комітом (HEAD~1), тобто коміт f22b25e:

Використання git checkout

Отже, якщо скласти це все разом, то Git працює як зображено у цьому коміксі:

https://figshare.com/articles/How\_Git\_works\_a\_cartoon/1328266

Спрощення загального випадку

Якщо уважно прочитати результат команди git status, ви побачите, що він містить цю підказку:

ВИХІД

(use "git checkout -- <file>..." to discard changes in working directory)

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

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

Відновлення старих версій файлу

Дженніфер зробила цього ранку деякі зміни у скрипті Python, над яким вона до цього працювала тижнями, та “зламала” його - він більше не запускається. Вона витратила майже годину, намагаючись виправити його, але безрезультатно…

На щастя, вона використовує Git для відстеження змін у свому проєкті! Які з наведених нижче команд допоможуть їй відновити останню збережену у Git версію її скрипту, який називається data_cruncher.py?

  1. $ git checkout HEAD

  2. $ git checkout HEAD data_cruncher.py

  3. $ git checkout HEAD~1 data_cruncher.py

  4. $ git checkout <unique ID of last commit> data_cruncher.py

  5. Як 2, так і 4

Відповідь (5) - як 2, так і 4.

Команда checkout відновлює файли з репозиторію, перезаписуючи файли у ваш робочий каталог. Обидві відповіді 2 та 4 відновлюють останню збережену в репозиторії версію файлу data_cruncher.py. Відповідь 2 використовує HEAD, щоб вказати останній коміт, тоді як відповідь 4 використовує унікальний ідентифікатор останнього коміту, що саме й означає HEAD.

Відповідь 3 замінить data_cruncher.py його версією з коміту перед HEAD, що НЕ є тим, що ми хотіли.

Відповідь 1 може бути небезпечною! Без назви файлу, git checkout відновить всі файли у поточному каталозі (і усіх підкаталогах нижче нього) до їх стану згідно із вказаним комітом. Ця команда відновить data_cruncher.py до його останньої збереженої версії, але вона також відновить всі інші файли, які було змінено на ту ж саму версію, стираючи будь-які зміни, які ви могли внести до цих файлів! Як обговорювалося вище, ви перейдете у стан “detached HEAD”, але ж ви не хочете там бути.

Скасування коміту

Дженніфер співпрацює з колегами над її скриптом Python. Вона зрозуміла, що її останній коміт до репозиторію проєкту містив помилку, і хоче його скасувати. Дженніфер хоче скасувати його правильним чином, щоб всі користувачі репозиторію цього проєкту отримали правильні зміни. Команда git revert [erroneous commit ID] створить новий коміт, який скасує помилковий коміт.

Команда git revert відрізняється від git checkout [commit ID], оскільки git checkout повертає файли, зміни у яких ще не ввійшли до нового коміту у локальному репозиторії, до їх попереднього стану, тоді як git revert скасовує зміни, які вже внесені до локального та віддаленого репозиторіїв.

Нижче наведені правильні кроки та пояснення для Дженніфер щодо користування git revert. Яка команда відсутня?

  1. ________ # Подивіться на історію змін, щоб знайти ідентифікатор коміту

  2. Скопіюйте цей ID (перші кілька символів ID, наприклад 0b1d055).

  3. git revert [commit ID]

  4. Введіть повідомлення для нового коміту.

  5. Збережіть його та закрийте редактор.

Команда git log зображує історію проєкту з ідентифікаторами комітів.

Команда git show HEAD покаже зміни, зроблені в останньому коміті, і покаже його ID; однак Дженніфер повинна перевірити, що це правильний коміт, а не нові зміни, які зробив у спільному репозиторії хтось інший.

Розуміння послідовності дій та історії

Який результат останньої команди в цій послідовності?

BASH

$ cd planets
$ echo "Venus is beautiful and full of love" > venus.txt
$ git add venus.txt
$ echo "Venus is too hot to be suitable as a base" >> venus.txt
$ git commit -m "Comment on Venus as an unsuitable base"
$ git checkout HEAD venus.txt
$ cat venus.txt #this will print the contents of venus.txt to the screen
  1. ВИХІД

Venus is too hot to be suitable as a base

2. ```output
Venus is beautiful and full of love
  1. ВИХІД

Venus is beautiful and full of love Venus is too hot to be suitable as a base

4. ```output
Error because you have changed venus.txt without committing the changes

Правильною є відповідь 2.

Команда git add venus.txt розміщує поточну версію ‘venus.txt’ в зоні стейджингу. Зміни до файлу з другої команди echo будуть зроблені лише у робочій копії цього файлу, але не у його версії в зоні стейджингу.

Тож, коли виконується команда git commit -m "Comment on Venus as an unsuitable base", версія venus.txt, яка буде збережена у коміті, буде з зони стейджингу та буде мати тільки один рядок.

На цей час робоча копія файлу ще має другий рядок (і тому git status покаже, що файл змінено). Однак git checkout HEAD venus.txt замінить робочу копію останньою збереженою версією venus.txt.

Тож, cat venus.txt покаже

ВИХІД

Venus is beautiful and full of love.

Перевірка розуміння git diff

Розглянемо цю команду: git diff HEAD~9 mars.txt. Що, на вашу думку, зробить ця команда, якщо ви її виконаєте? Що станеться, коли ви її виконаєте? Чому?

Спробуйте іншу команду, git diff [ID] mars.txt, де [ID] замінено на унікальний ідентифікатор вашого останнього коміту. Як ви думаєте, що вона зробить? Виконайте її та перевірте, чи це так.

Скасування змін у зоні стейджингу

Команда git checkout може бути використана для відновлення попереднього коміту, коли зміни були зроблені, але ще не додані до зони стейджингу. Але чи спрацює вона і для змін, які були додані до зони стейджингу, але не ще збережені у коміті? Зробіть зміни у mars.txt, додайте їх до зони стейджингу за допомогою git add, та використайте git checkout, щоб побачити чи можете ви скасувати свої зміни.

Після додавання зміни за допомогою git add, команду git checkout не можна використовувати безпосередньо. Подивіться на результат git status:

ВИХІД

On branch main
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   mars.txt

Зауважте, що якщо ви не маєте такого самого результату, то можливо, ви забули змінити файл, або ви не тільки додали його до зони стейджингу, а також зробили коміт.

Використання команди git checkout -- mars.txt тепер не дає помилки, але також не відновлює файл. Git зображує корисне повідомлення - нам потрібно спочатку використати git reset, щоб видалити файл з зони стейджингу:

BASH

$ git reset HEAD mars.txt

ВИХІД

Unstaged changes after reset:
M	mars.txt

Тепер git status зображує наступне:

BASH

$ git status

ВИХІД

On branch main
Changes not staged for commit:
   (use "git add/rm <file>..." to update what will be committed)
   (use "git checkout -- <file>..." to discard changes in working directory)

    deleted:    nibiru.txt

no changes added to commit (use "git add" and/or "git commit -a")

Це означає, що тепер ми можемо використовувати git checkout, щоб відновити файл до попереднього коміту:

BASH

$ git checkout -- mars.txt
$ git status

ВИХІД

On branch main
nothing to commit, working tree clean

Перегляд історії

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

Уявіть, що проєкт planets має більш ніж 50 файлів. Ви хотіли б знайти коміт, який змінює певний текст у mars.txt. Коли ви вводите git log, з’являється дуже довгий список. Як можна звузити коло пошуку?

Нагадаємо, що команда git diff може використовуватись для одного конкретного файлу, наприклад, git diff mars.txt. Ми можемо застосувати тут подібну ідею.

BASH

$ git log mars.txt

На жаль, деякі з цих повідомлень комітів дуже неоднозначні, наприклад, update files. Як же переглянути усі ці версії файлу?

Обидві команди git diff та git log дуже корисні для отримання звітів про різні деталі історії проєкту. Але чи можна об’єднати їх результат в одну команду? Давайте спробуємо наступне:

BASH

$ git log --patch mars.txt

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

Питання: Що робить наступна команда?

BASH

$ git log --patch HEAD~9 *.txt

Ключові моменти

  • git diff відображає відмінності між комітами.
  • git checkout відновлює старі версії файлів.