Пошук уроків, статей та іншого контенту
Оберіть безпечний спосіб скасування змін, відновлення commit і підтримання зрозумілої історії в командній роботі.
Git зберігає історію як граф комітів. Коміт містить:
знімок файлів;
посилання на батьківський коміт;
автора й комітера;
повідомлення;
ідентифікатор, обчислений на основі вмісту та метаданих.
Гілка не містить окремої копії комітів. Це лише рухоме посилання на певний коміт. Наприклад:
A---B---C main
^
HEADHEAD вказує на поточну позицію, а main — на останній коміт гілки. Коли створюється новий коміт, посилання гілки переміщується вперед.
A---B---C---D main
^
HEADЦе пояснює різницю між основними способами скасування змін:
git revert створює новий коміт, який скасовує попередній;
git reset переміщує посилання гілки на інший коміт;
git restore змінює файли в робочому дереві або індексі, але не переміщує гілку.
Перед будь-якою операцією перевірте стан репозиторію:
git status
git log --oneline --decorate --graph --all -n 20Важливо відповісти на три запитання:
Чи було проблемний коміт уже опубліковано?
Чи потрібно скасувати весь коміт або лише окремі файли?
Чи потрібно зберегти сам коміт в історії?
Зазвичай діє таке правило:
опублікована історія — revert;
локальна, ще не опублікована історія — reset або rebase;
відновлення окремого файлу — restore;
випадково втрачене посилання на коміт — reflog.
git revertgit revert не видаляє старий коміт. Він створює новий коміт із протилежною зміною.
Нехай історія має вигляд:
A---B---C mainЩоб скасувати C:
git switch main
git pull --ff-only
git revert C
git push origin mainGit відкриє редактор для повідомлення нового коміту. За потреби повідомлення можна вказати одразу:
git revert --no-edit CПісля цього історія буде такою:
A---B---C---R mainR скасовує зміни C, але сам C залишається частиною історії. Це безпечно для команди, оскільки інші учасники вже мають ті самі коміти й не повинні перебазовувати свої гілки.
Щоб скасувати діапазон комітів від B до D включно:
git revert B^..DЗапис B^..D означає всі коміти від батьківського коміту B до D.
Git може створити окремий revert-коміт для кожного коміту. Якщо потрібно спочатку перевірити результат, а один підсумковий коміт створити пізніше:
git revert --no-commit B^..D
git diff --cached
git commit -m "Revert changes from B through D"Якщо під час revert виник конфлікт:
git status
# Виправте конфлікт у файлах
git add path/to/file
git revert --continueЩоб повністю скасувати поточну операцію:
git revert --abortMerge-коміт має кількох батьків, тому Git повинен знати, відносно якого батька визначати основну лінію.
Для скасування merge-коміту зазвичай використовують:
git revert -m 1 <merge-commit>-m 1 означає, що перший батьківський коміт вважається основною гілкою, а зміни іншого батька потрібно скасувати.
Перед виконанням перевірте батьківські коміти:
git show --summary <merge-commit>Revert merge-коміту може вплинути на майбутні злиття. Git вважатиме зміни цього merge вже скасованими. Якщо пізніше потрібно повторно внести їх, часто спочатку доводиться скасувати сам revert, а вже потім виконувати нове злиття.
git resetreset переміщує поточну гілку на інший коміт. Саме тому його потрібно обережно використовувати для гілок, які вже бачать інші учасники команди.
reset--softПереміщує покажчик гілки, але залишає зміни в індексі:
git reset --soft HEAD~1Це зручно, якщо потрібно переробити останній коміт і створити його знову з тим самим набором змін.
Стан:
коміт прибрано з поточної гілки;
зміни залишилися підготовленими до коміту;
робочі файли не змінено.
--mixedЦе режим за замовчуванням:
git reset HEAD~1Він переміщує гілку та прибирає зміни з індексу, але залишає їх у робочому дереві.
Стан:
коміт прибрано з поточної гілки;
зміни не підготовлені;
файли все ще містять ці зміни.
--hardgit reset --hard HEAD~1Переміщує гілку, індекс і робоче дерево. Зміни, яких немає в іншому коміті або резервній копії, можуть стати недоступними через звичайні команди Git.
Не використовуйте --hard для скасування роботи, якщо ви не перевірили, що саме буде видалено:
git diff HEAD~1
git diff --stat HEAD~1Перед ризикованим reset можна створити тимчасове посилання:
git branch backup-before-reset
git reset --hard HEAD~1Тоді попередній стан залишиться доступним через backup-before-reset.
git restoreЯкщо потрібно скасувати зміни лише в одному файлі, не змінюючи історію гілки:
git restore -- path/to/fileКоманда відновлює файл із HEAD і видаляє його незбережені зміни в робочому дереві.
Щоб прибрати файл з індексу, але залишити зміни у робочому дереві:
git restore --staged -- path/to/fileЩоб відновити файл із конкретного коміту:
git restore --source=<commit> -- path/to/fileНаприклад:
git restore --source=HEAD~2 -- src/config.jsПісля цього файл потрібно перевірити й, якщо результат правильний, створити звичайний коміт:
git diff -- src/config.js
git add src/config.js
git commit -m "Restore configuration from previous version"git restore не переписує історію. Він лише змінює поточний стан файлів.
reflogЗвичайний git log показує коміти, до яких можна дістатися з поточних гілок і тегів. Якщо після reset коміт більше не належить жодній гілці, його все ще можна знайти в локальному журналі переміщень HEAD:
git reflogПриклад:
7a1c2d3 HEAD@{0}: reset: moving to HEAD~1
9f8e7d6 HEAD@{1}: commit: Add payment validationЩоб перевірити втрачений коміт:
git show 9f8e7d6Безпечний спосіб повернути його — створити нову гілку:
git switch -c recover-payment 9f8e7d6Якщо коміт потрібно повернути в поточну гілку:
git switch feature/payments
git cherry-pick 9f8e7d6reflog зберігається локально. Він не є спільною історією репозиторію і не допоможе іншому учаснику знайти коміт у своєму локальному клону.
cherry-pickcherry-pick створює новий коміт із такими самими змінами, як у вказаного коміту:
git switch release/2.4
git cherry-pick <commit>Це корисно, коли виправлення вже є в одній гілці, але його потрібно перенести в іншу, наприклад у стабільну release-гілку.
Ідентифікатор нового коміту буде іншим, навіть якщо зміни збігаються. Причина — новий коміт має іншого батька та інші метадані.
Для кількох комітів:
git cherry-pick <oldest>^..<newest>Якщо виник конфлікт:
git status
# Виправте конфлікт у файлах
git add path/to/file
git cherry-pick --continueСкасування операції:
git cherry-pick --abortНе використовуйте cherry-pick для механічного копіювання великої гілки, якщо звичайне злиття або rebase краще передає зв’язок між комітами.
rebase -iІнтерактивний rebase дає змогу впорядкувати коміти до їх публікації:
git rebase -i HEAD~4У редакторі можна змінити команди:
pick — залишити коміт;
reword — змінити повідомлення;
edit — зупинитися для редагування;
squash — об’єднати з попереднім комітом і відредагувати повідомлення;
fixup — об’єднати з попереднім комітом без збереження власного повідомлення;
drop — видалити коміт.
Наприклад, така послідовність:
pick 1a2b3c Add validation
pick 4d5e6f Fix typo
pick 7a8b9c Add tests
pick 0d1e2f Fix validation againможе перетворитися на:
pick 1a2b3c Add validation
fixup 4d5e6f Fix typo
pick 7a8b9c Add tests
fixup 0d1e2f Fix validation againПісля завершення варто перевірити граф:
git log --oneline --decorate --graph -n 10
git diff origin/feature..HEADЯкщо rebase потрібно зупинити:
git rebase --abortЯкщо конфлікт виправлено:
git add path/to/file
git rebase --continueRebase змінює ідентифікатори комітів. Тому не слід перебазовувати гілку, над якою паралельно працюють інші люди, без узгодження з ними.
Після rebase локальна гілка зазвичай більше не є звичайним fast-forward продовженням віддаленої:
git push origin feature/loginможе завершитися помилкою. Якщо гілку дозволено переписувати, використовуйте:
git push --force-with-lease origin feature/login--force-with-lease перевіряє, що віддалена гілка все ще має очікуваний стан. Це безпечніше за:
git push --forceАле навіть --force-with-lease не робить переписування безпечним автоматично. Перед ним потрібно:
повідомити команду;
переконатися, що ніхто не опублікував нові коміти;
переконатися, що це не захищена спільна гілка;
мати спосіб відновити попередню вершину гілки.
Для main, master та інших спільних гілок перевага зазвичай надається revert, а не force push.
Зрозуміла історія допомагає відповісти на запитання: що змінилося, чому це змінилося і як безпечно повернути зміни.
Поки гілка локальна, можна:
виправляти повідомлення;
об’єднувати дрібні коміти;
видаляти випадкові коміти;
перебудовувати порядок комітів.
Після публікації краще не змінювати вже використані іншими коміти. Для помилок у спільній історії створюйте компенсувальні коміти через revert.
Коміт має містити завершену логічну зміну. Наприклад, краще розділити:
Add password validation
Add tests for password validation
Update validation error messageніж створити один коміт із не пов’язаними змінами в API, форматуванні та документації.
Водночас не потрібно розділяти одну нерозривну зміну на коміти, які не можна зібрати або перевірити окремо.
Повідомлення має описувати результат:
Add timeout for payment requests
Fix null handling in user profile
Remove deprecated report endpointУникайте повідомлень на кшталт:
Fix
Changes
Update
WIPТимчасові коміти можна об’єднати перед публікацією за допомогою інтерактивного rebase.
Корисний набір команд:
git status
git log --oneline --decorate --graph --all -n 30
git show --stat <commit>
git diff <commit>^ <commit>
git branch --contains <commit>git branch --contains <commit> допомагає визначити, які локальні гілки вже містять певний коміт. Для віддалених посилань спочатку оновіть інформацію:
git fetch --all --pruneНехай у feature-гілці є невдалий коміт, який ще не опубліковано:
A---B---C---D featureКоміт D містить помилку, але його зміни потрібно зберегти для редагування. Безпечна послідовність:
git switch feature
git status
# Створюємо резервне посилання на поточний стан
git branch backup-feature-before-reset
# Повертаємо гілку на попередній коміт, залишаючи зміни підготовленими
git reset --soft HEAD~1
# Перевіряємо зміни
git diff --cached
# Створюємо виправлений коміт
git commit -m "Add validated payment request"
# Перевіряємо результат
git log --oneline --decorate --graph -n 6Якщо результат виявився неправильним, резервна гілка дозволяє повернутися до попереднього стану:
git reset --hard backup-feature-before-resetЯкщо ж backup-feature-before-reset більше не потрібна:
git branch -d backup-feature-before-resetІнший сценарій: помилковий коміт уже потрапив у main. У такому разі не потрібно робити reset:
git switch main
git pull --ff-only
git revert --no-edit <bad-commit>
git push origin mainТак команда зберігає спільну послідовність комітів і отримує явне пояснення, чому зміни були скасовані.
Незалежно від того, чи використовується revert, cherry-pick або rebase, загальний підхід однаковий:
git status
git diffGit позначить конфліктні ділянки у файлах:
<<<<<<< HEAD
поточний варіант
=======
варіант із застосовуваного коміту
>>>>>>> commitПотрібно:
вибрати правильний результат;
видалити маркери конфлікту;
перевірити файл;
додати виправлений файл в індекс;
продовжити операцію.
Для revert:
git add path/to/file
git revert --continueДля cherry-pick:
git add path/to/file
git cherry-pick --continueДля rebase:
git add path/to/file
git rebase --continueНе позначайте файл як вирішений через git add, доки справді не перевірите його вміст.
reset --hard на спільній гілціЦе видаляє коміти з вершини гілки та змушує інших учасників узгоджувати локальні клони з новою історією.
Для вже опублікованої помилки використовуйте git revert.
push --force--force може перезаписати чужі коміти, опубліковані після вашого останнього fetch.
Надавайте перевагу:
git push --force-with-leaseі переконайтеся, що переписування гілки погоджене з командою.
restore, reset і revertrestore працює з файлами та індексом;
reset переміщує поточну гілку;
revert створює новий коміт.
Перед командою перевірте, яку частину стану репозиторію вона змінює.
Короткий ідентифікатор може бути неоднозначним або просто помилковим. Перед revert чи cherry-pick перевірте:
git show <commit>reflog спільною резервною копієюreflog локальний. Він допомагає відновити ваші локальні переміщення HEAD, але не замінює віддалений репозиторій, резервні копії або теги.
Після rebase ідентифікатори комітів змінюються. Якщо інші гілки вже базуються на старій історії, це створить зайві конфлікти та дублювання.
Для merge потрібно визначити основного батька. Без правильного -m можна отримати помилковий результат або взагалі не скасувати потрібний набір змін.
Для опублікованих змін використовуйте git revert.
Для локальної непублікованої історії використовуйте git reset або git rebase -i.
Для окремих файлів використовуйте git restore.
Для відновлення комітів після reset перевіряйте git reflog.
Для перенесення конкретного виправлення між гілками використовуйте git cherry-pick.
Merge-коміти скасовуйте з явним вибором основного батька через git revert -m.
Перед небезпечними операціями перевіряйте граф і за потреби створюйте резервну гілку.
Не переписуйте спільну історію без узгодження з командою.
Якщо переписування дозволене, використовуйте --force-with-lease, а не безумовний --force.
Зрозуміла історія складається з логічних комітів, точних повідомлень і передбачуваної політики скасування.