Пошук уроків, статей та іншого контенту
Безпечно зміните локальні коміти за допомогою reset, rebase та reflog і відновите випадково втрачені посилання.
Коміт у Git є незмінним об’єктом. Якщо змінити його повідомлення, батьківський коміт або вміст, Git створить новий коміт з іншим хешем.
Переписування історії змінює не самі старі об’єкти одразу, а посилання на них:
HEAD показує поточний коміт;
гілка, наприклад main, показує останній коміт цієї гілки;
reflog зберігає попередні значення локальних посилань.
Тому багато помилок можна виправити, навіть якщо коміт тимчасово «зник» із поточної історії.
Переписувати історію безпечно, якщо:
коміти ще не опубліковані у спільній віддаленій гілці;
перед ризикованою операцією створено резервну гілку;
перед reset --hard перевірено стан робочого каталогу.
У наступній послідовності створюється невеликий репозиторій із трьома комітами:
mkdir history-demo
cd history-demo
git init
git config user.name "Demo Developer"
git config user.email "demo@example.com"
printf "# Demo\n" > README.md
git add README.md
git commit -m "Додати README"
printf "Початкова версія\n" > app.txt
git add app.txt
git commit -m "Додати app.txt"
printf "Змінена версія\n" >> app.txt
git add app.txt
git commit -m "Оновити app.txt"
git log --oneline --decorate --graphОстанній коміт можна позначити як C, попередній — як B, а найперший — як A:
A -- B -- C (HEAD -> main)Перед зміною історії корисно переглянути стан репозиторію:
git status
git log --oneline --decorate --graph --allgit reset: переміщення посилання на гілкуgit reset переміщує поточну гілку на інший коміт. Додатково він може змінити індекс і робочий каталог.
reset --softgit reset --soft HEAD~1Після цієї команди гілка переміститься з C на B, але зміни з коміту C залишаться підготовленими до коміту:
A -- B (HEAD -> main)
Зміни коміту C перебувають в індексіЦей варіант зручно використовувати, коли потрібно:
змінити повідомлення останнього коміту;
об’єднати останній коміт із новими змінами;
розкласти зміни коміту перед повторним створенням коміту.
Наприклад:
git reset --soft HEAD~1
git commit -m "Оновити app.txt зрозуміліше"У результаті створиться новий коміт замість C.
reset без параметра або reset --mixedgit reset HEAD~1Це скорочення для:
git reset --mixed HEAD~1Гілка переміститься на B, а зміни з C залишаться у робочому каталозі, але будуть прибрані з індексу:
A -- B (HEAD -> main)
Зміни коміту C є незастежованимиТакий режим корисний, коли потрібно відредагувати набір файлів і підготувати новий коміт вибірково:
git reset HEAD~1
git status
# Додати до нового коміту лише потрібний файл
git add app.txt
git commit -m "Змінити app.txt"Якщо режим не вказано, Git використовує --mixed.
reset --hardgit reset --hard HEAD~1Гілка переміститься на B, індекс буде оновлено, а файли робочого каталогу приведено до стану B.
Незакомічені зміни, які суперечать цій операції, буде видалено:
A -- B (HEAD -> main)
Зміни коміту C більше не видно у робочому каталозі--hard варто використовувати лише після перевірки:
git status
git diff
git diff --stagedЯкщо коміт був локальним і потрібно просто скасувати останній коміт, але залишити його зміни для подальшого редагування, зазвичай безпечнішим буде:
git reset --soft HEAD~1Перед переписуванням історії можна створити тимчасове посилання на поточний стан:
git branch backup-before-rewriteТепер навіть після reset --hard до початкового коміту можна повернутися через цю гілку:
git show backup-before-rewrite
git switch backup-before-rewriteЩоб повернутися до основної гілки:
git switch mainРезервна гілка не змінює історію і не створює нового коміту. Вона лише зберігає назву, яка вказує на поточний коміт.
git rebase: перенесення комітів на нову основуrebase перебудовує послідовність комітів поверх іншого коміту. Під час операції Git створює нові коміти, тому їхні хеші змінюються.
Уявімо таку історію:
A -- B -- C (main)
\
D -- E (feature)Якщо виконати на гілці feature:
git switch feature
git rebase mainGit перенесе зміни комітів D та E поверх C:
A -- B -- C (main) -- D' -- E' (feature)D' та E' мають інші хеші, навіть якщо їхній вміст може бути таким самим.
Щоб відредагувати останні три локальні коміти:
git rebase -i HEAD~3Git відкриє список на кшталт:
pick 1111111 Додати README
pick 2222222 Додати app.txt
pick 3333333 Оновити app.txtУ цьому списку можна замінити команду pick:
reword — змінити повідомлення коміту;
edit — зупинитися на коміті для редагування;
squash — об’єднати коміт із попереднім і відредагувати повідомлення;
fixup — об’єднати коміт із попереднім та використати повідомлення попереднього;
drop — вилучити коміт.
Наприклад, щоб об’єднати останній коміт із попереднім:
pick 2222222 Додати app.txt
squash 3333333 Оновити app.txtПісля збереження Git запропонує відредагувати повідомлення об’єднаного коміту.
Щоб змінити повідомлення останнього коміту, достатньо:
git rebase -i HEAD~1і замінити:
pick 3333333 Оновити app.txtна:
reword 3333333 Оновити app.txteditКоманда edit дозволяє змінити вміст коміту:
edit 3333333 Оновити app.txtПісля зупинки rebase можна змінити файл і створити оновлений коміт:
printf "Ще один рядок\n" >> app.txt
git add app.txt
git commit --amend --no-edit
git rebase --continueЯкщо потрібно змінити лише повідомлення:
git commit --amend -m "Оновити app.txt повністю"
git rebase --continueЯкщо під час rebase виник конфлікт або результат не влаштовує, операцію можна скасувати:
git rebase --abortGit спробує повернути гілку до стану, у якому вона була до початку rebase.
Якщо конфлікт потрібно вирішити:
git statusПісля редагування конфліктних файлів:
git add path/to/file
git rebase --continueПоточну операцію rebase не слід завершувати звичайним git commit, якщо Git очікує саме git rebase --continue.
Нехай у локальній гілці є три коміти:
A -- B -- C -- D (HEAD -> feature)Коміти C і D є дрібними проміжними змінами, які перед публікацією потрібно об’єднати з B.
Спочатку створимо резервну гілку:
git branch backup-featureПотім запустимо інтерактивний rebase:
git rebase -i HEAD~3Редагуємо список, наприклад, так:
pick bbbbbbb Основна зміна
fixup ccccccc Виправити дрібницю
fixup ddddddd Ще одне уточненняПісля завершення залишиться один новий коміт замість трьох. Історія стане коротшою, а робочий результат збережеться.
Перевірити результат можна так:
git log --oneline --decorate --graph
git diff backup-featuregit reflog: пошук попередніх станівreflog — це локальний журнал переміщень посилань. Він записує, зокрема:
переходи між комітами;
виконання reset;
завершення rebase;
перемикання гілок;
створення або переміщення локальних гілок.
Переглянути журнал поточної гілки можна так:
git reflogТиповий результат може мати такий вигляд:
8f31abc HEAD@{0}: reset: moving to HEAD~1
4a72def HEAD@{1}: commit: Оновити app.txt
c18b902 HEAD@{2}: commit: Додати app.txtHEAD@{0} — поточний стан, а HEAD@{1} — попередній стан HEAD.
Для детальнішого перегляду:
git reflog show --date=localПід час відновлення краще орієнтуватися не лише на номер запису, а й на хеш та опис операції. Номери HEAD@{n} змінюються після нових команд Git.
Припустімо, виконано:
git reset --hard HEAD~1Останній коміт зник із журналу git log, але його можна знайти в reflog:
git reflogЯкщо потрібний коміт має хеш 4a72def, створимо з нього окрему гілку:
git switch -c recovered-work 4a72defТепер зміни знову доступні в гілці recovered-work.
Якщо потрібно повернути основну гілку до цього коміту:
git switch main
git reset --hard 4a72defБезпечніший варіант — спочатку перевірити знайдений коміт:
git show 4a72def
git switch -c recovered-work 4a72defЛише після перевірки можна вирішувати, чи потрібно переміщувати main.
Якщо rebase завершився небажано, у reflog зазвичай залишаються записи про стан до операції:
git reflogЗнайдіть запис на кшталт:
abc1234 HEAD@{5}: rebase (start): checkout main
def5678 HEAD@{6}: checkout: moving from main to featureПотрібний стан часто розташований безпосередньо перед записом rebase (start). Спочатку створіть резервну гілку:
git branch before-rebase def5678Після цього можна повернути поточну гілку:
git reset --hard before-rebaseТакий порядок дій зберігає знайдений стан і зменшує ризик повторної втрати посилання.
Після reset або rebase старий коміт може більше не бути доступним через назву гілки. Проте Git не видаляє його негайно, якщо на нього ще вказує запис reflog або інше посилання.
Поки коміт можна знайти за хешем:
git show <commit>його можна зберегти, створивши посилання:
git branch saved-commit <commit>Локальний reflog не є постійним архівом. Старі записи з часом очищуються під час обслуговування репозиторію. Тому знайдений важливий коміт слід одразу закріпити гілкою або іншим постійним посиланням.
reset і rebase змінюють хеші комітів та положення гілки. Якщо ці коміти вже використовують інші розробники, звичайний push може бути відхилений.
Примусове оновлення віддаленої гілки:
git push --force-with-lease origin feature--force-with-lease безпечніший за --force: він перевіряє, що віддалена гілка не змінилася після останнього отримання даних.
Однак навіть --force-with-lease може переписати історію, яку хтось уже завантажив. Тому переписуйте опубліковану гілку лише після узгодження з командою. Для локальних комітів, які ще не надсилалися на сервер, така проблема зазвичай не виникає.
reset --hard без перевірки стануreset --hard видаляє незбережені зміни з робочого каталогу.
Перед командою перевірте:
git status
git diff
git diff --stagedЯкщо зміни потрібно зберегти, спочатку створіть коміт або скористайтеся іншим безпечним способом тимчасового збереження.
reset і revertreset переписує поточну локальну історію, переміщуючи гілку.
revert створює новий коміт, який скасовує зміни попереднього. Для вже опублікованої історії зазвичай потрібен саме підхід із новим комітом, а не переписування гілки.
Після rebase старі коміти отримують нові хеші. Інші учасники команди можуть мати стару версію цієї гілки, що спричинить конфлікти та необхідність ручної синхронізації.
Номер HEAD@{n} залежить від нових дій. Після перегляду reflog або перемикання гілки нумерація може змінитися.
Перевіряйте знайдений стан:
git show <commit>
git log --oneline <commit> -n 5Після цього створюйте резервну гілку:
git branch recovered <commit>Якщо виконано:
git rebase --abortпоточну операцію завершено. Не потрібно виконувати git rebase --continue; замість цього слід перевірити стан і, за потреби, почати нову операцію.
git reset --soft переміщує гілку, залишаючи зміни в індексі.
git reset --mixed переміщує гілку та залишає зміни незастежованими.
git reset --hard також змінює робочий каталог і може видалити незакомічені зміни.
git rebase -i дає змогу змінювати порядок локальних комітів, об’єднувати їх, перейменовувати або вилучати.
Під час rebase створюються нові коміти з іншими хешами.
git reflog допомагає знайти попередні стани HEAD після невдалого reset або rebase.
Знайдений коміт потрібно закріпити гілкою, наприклад git branch recovered <commit>.
Перед ризикованою операцією створюйте резервну гілку й особливо обережно використовуйте reset --hard.
Опубліковану історію не слід переписувати без узгодження з іншими учасниками команди.