Скасування змін: reset, revert, stash
Зміст
Рано чи пізно кожен, хто працює з Git, робить одне з чотирьох: комітить не те, комітить не в ту гілку, псує повідомлення коміту або хоче тимчасово відкласти незавершене, щоб перемкнутися на термінову задачу. Для AQA це буденність: тестовий код живе в git так само, як продуктовий, і плутанина «закомітив половину рефакторингу page object у master замість фіча-гілки» трапляється частіше, ніж хотілося б. Скасування лякає, бо здається, що одним рухом можна знищити годину роботи — і саме через цей страх люди або паралізуються, або навпаки рубають git reset --hard наосліп.
Ключ до впевненості — не завчити команди, а зрозуміти, на яку з трьох зон Git (робоча директорія, індекс, репозиторій — детально в главі «Системи контролю версій і модель Git») кожна команда діє. Тоді видно, що майже все оборотне, і лише одна операція справді небезпечна. На співбесідах це тема з високою вагою: питання «чим reset відрізняється від revert» — класика, на якій відсіюють тих, хто вчив команди напам'ять, не розуміючи моделі.
Три зони — і що означає «скасувати»
Нагадаємо картину з базового циклу (глава «Базовий цикл Git»). Зміна проходить три стани:
- Робоча директорія (working directory) — файли, які ви редагуєте зараз.
- Індекс (index, staging area) — чернетка наступного коміту; те, що ви додали через
git add. - Репозиторій (repository) — послідовність зафіксованих комітів; на верхівку вашої гілки вказує
HEAD.
«Скасувати зміни» — розмите формулювання, і саме тому новачки хапають не ту команду. Питання завжди конкретне: на якій зоні лежить те, що я хочу відкотити? Не закомічена правка в файлі, помилково застейджений файл, останній коміт, коміт тижневої давнини в спільній гілці — це чотири різні ситуації з чотирма різними інструментами. Далі — по кожній.
restore: відкотити незакомічене
git restore (з'явився в Git 2.23) — сучасна команда для скасування змін, які ще не потрапили в коміт. Вона розводить два різні наміри, які раніше плутали в перевантаженому git checkout.
# Викинути незастейджені правки у файлі — повернути його до стану в індексі
git restore tests/login.spec.ts
# Прибрати файл зі staging (розстейджити), правки в робочій директорії лишаються
git restore --staged tests/login.spec.ts
Різниця принципова. git restore <файл> знищує ваші незбережені правки в робочій директорії — повертає файл до останньої застейдженої (або закомміченої) версії. git restore --staged <файл> нічого не знищує: він лише виймає файл з чернетки коміту, а самі зміни лишаються у вас під руками.
Тут перша важлива пастка, до якої ми ще повернемось: робоча директорія не версіонується. Якщо ви зробили git restore над правкою, якої ще ніде не зафіксували, її не врятує ніщо — на відміну від комітів, які Git тримає навіть після «видалення». Тому restore над робочою директорією — операція без страхувальної сітки.
reset: рухаємо вказівник гілки
git reset робить одну концептуальну річ: пересуває вказівник поточної гілки на інший коміт. А що станеться з індексом і робочою директорією — визначає режим. Саме тут живуть три прапорці, які питають на кожній другій співбесіді.
Уявіть, що ви на коміті C (історія A → B → C) і робите git reset HEAD~1, тобто відкочуєте гілку на B. Коміт C більше не в історії гілки. Питання лише в тому, куди подінуться зміни, що були в C:
| Режим | HEAD (гілка) | Індекс | Робоча директорія | Де опиняться зміни з відкоченого коміту |
| --- | --- | --- | --- |
| --soft | рухає | не чіпає | не чіпає | застейджені, готові до нового коміту |
| --mixed (дефолт) | рухає | скидає | не чіпає | у робочій директорії, незастейджені |
| --hard | рухає | скидає | скидає | знищуються |
Звідси випливає практика:
--soft— «переграти останній коміт». Ви відкотилиC, але всі його зміни лишились застейдженими. Класичний кейс: закомітили зарано або хочете зібрати кілька дрібних комітів в один.git reset --soft HEAD~1— і робіть новий коміт як слід.--mixed(те, що ви отримаєте, якщо режим не вказали) — «розібрати коміт назад до правок». Зміни повертаються в робочу директорію незастейдженими. Так самоgit resetбез аргументів (тобтоgit reset HEAD) — це найшвидший спосіб розстейджити геть усе одразу.--hard— «зробити так, ніби нічого не було». Гілка, індекс і файли на диску стають точно як у цільовому коміті. Все незакоммічене зникає. Це єдиний по-справжньому руйнівний режим — до нього ще повернемось окремим розділом.
Типовий рятувальний сценарій для AQA: ви закомітили фікс автотесту в master, хоча мали — у фіча-гілку. git reset --soft HEAD~1 знімає коміт, лишаючи зміни застейдженими; далі створюєте гілку й комітите вже туди. Історія master чиста, робота ціла.
commit --amend: полагодити останній коміт
Окремий випадок — коли коміт правильний за суттю, але зіпсований у дрібниці: одрук у повідомленні або забутий файл. git commit --amend замінює останній коміт новим.
# Виправити тільки текст повідомлення останнього коміту
git commit --amend -m "fix: стабілізувати очікування в login.spec"
# Додати забутий файл до останнього коміту, повідомлення лишити
git add tests/helpers/wait.ts
git commit --amend --no-edit
Важливо розуміти механіку: --amend не редагує коміт «на місці». Коміт у Git незмінний, тож Git створює новий коміт з новим хешем і перевішує на нього гілку, а старий стає недосяжним. Тобто це, по суті, переписування історії в мініатюрі. Наслідок практичний: --amend безпечний, лише поки коміт не запушений. Якщо ви заамендили те, що вже бачать колеги, локальна й віддалена історія розійдуться — і push пройде тільки з примусом, що ламає роботу іншим. Це підводить нас до головного правила теми.
reset vs revert: чому публічну історію тільки revert
Це — центральне питання глави і найчастіше на співбесіді. Обидві команди «скасовують коміт», але роблять це протилежно.
git resetпересуває вказівник гілки назад. Небажаний коміт зникає з історії гілки, ніби його не було. Історія переписується.git revertстворює новий коміт, який застосовує зворотні зміни до цільового. Небажаний коміт лишається в історії, а поверх з'являється ще один, що його скасовує. Історія доповнюється, а не переписується.
Чому це вирішує, який інструмент брати? Бо у Git гілка — спільний ресурс. Коли ви запушили коміти, їх уже могли витягнути (pull) колеги і CI. Якщо ви приберете ці коміти через reset і зробите push --force, ваша історія розійдеться з їхньою: у них ці коміти ще є, у вас — уже ні. Наступний їхній pull або поверне «видалене» назад, або впаде конфліктом, або (найгірше) хтось затре чиюсь роботу. Це і є «золоте правило» Git, докладніше — у главі «Rebase, cherry-pick і переписування історії»: не переписуй історію, яку вже бачать інші.
Звідси проста, майже механічна відповідь:
- Коміт локальний, ще не запушений — можна
reset(чи--amend, чи rebase). Історія ваша особиста, переписуйте вільно. - Коміт уже в спільній гілці (
main,develop, будь-що запушене) — тількиrevert. Ви не стираєте минуле, ви додаєте коректувальний коміт. Історія лишається несуперечливою для всіх.
# Скасувати конкретний коміт у спільній гілці — з'явиться новий коміт-антидот
git revert a1b2c3d
# Скасувати діапазон
git revert HEAD~3..HEAD
Нюанс, який відрізняє сильного кандидата: revert merge-коміту потребує вказати, яку з батьківських гілок вважати основною (git revert -m 1 <хеш>), бо в merge-коміту їх дві. Тема мержів — у главі «Merge-конфлікти». І ще: revert не «видаляє слід» бага — і сам баг, і його скасування лишаються в історії. Для аудиту це плюс (видно, що і коли відкотили), а не недолік.
stash: відкласти незавершене
Ситуація: ви на середині правки, нічого не готове до коміту, і тут прилітає «терміново перевір фікс у гілці release». Перемкнути гілку з брудною робочою директорією Git часто не дасть, а комітити напівфабрикат не хочеться. Для цього є git stash — «полиця» (стек), куди складають незавершене.
# Сховати незакомічені зміни (робоча директорія стає чистою, як у HEAD)
git stash push -m "недороблений тест checkout"
# Подивитися, що на полиці
git stash list # stash@{0}, stash@{1}, ...
# Повернути найсвіжіший stash і прибрати його з полиці
git stash pop
# Те саме, але лишити копію на полиці
git stash apply
Механіка: stash бере зміни з робочої директорії й індексу, зберігає їх окремо (технічно — як приховані коміти) і повертає робочу директорію до стану HEAD. Ви перемикаєтесь, робите термінове, повертаєтесь і git stash pop накладає відкладене назад.
Дві пастки. По-перше, за замовчуванням stash не бере невідстежувані (untracked) файли — новий файл, який ви ще не додавали, лишиться на місці й не сховається; щоб узяти і його, потрібен git stash -u. По-друге, stash — це стек, і pop знімає найсвіжіший елемент; якщо застешили кілька разів і забули, легко застосувати не той. Stash зручний для коротких перемикань, але не для довготривалого зберігання «на потім» — для такого краще звичайна гілка.
reflog: страхувальна сітка
Тепер найважливіше для спокою. Коли ви робите reset, --amend чи невдалий rebase, «зниклі» коміти насправді нікуди не діваються одразу. Git веде журнал переміщень HEAD — reflog (reference log). Кожен раз, коли HEAD або гілка кудись переходили, туди лягає запис.
# Показати, де побував HEAD (згори — найсвіжіше)
git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~1
# e4f5g6h HEAD@{1}: commit: додав перевірку тосту <- ось "втрачений" коміт
HEAD@{1} — це попереднє положення HEAD. Тобто щойно ви зрозуміли, що зайвого відкотили, ви бачите в reflog хеш того коміту й повертаєтесь на нього:
# Відновити гілку на "втрачений" коміт
git reset --hard e4f5g6h
Ось чому паніка після reset --hard майже завжди зайва: доки коміт існував, його хеш є в reflog, і його можна дістати. Записи reflog не вічні — за замовчуванням досяжні тримаються близько 90 днів, недосяжні близько 30, — але цього з надлишком вистачає, щоб виправити помилку того ж дня. Головне обмеження — reflog локальний: він живе лише у вашому клоні репозиторію. На чужій машині вашого reflog немає, і свіжого клону теж.
І тут — критична межа страхувальної сітки: reflog рятує коміти, але не рятує незакомічене. Правки, які жили лише в робочій директорії й ніколи не потрапляли навіть у staging, не мають хеша, тож у reflog їх немає. Саме тому наступний розділ — окремо.
Пастка reset --hard
git reset --hard — найпідступніша команда цієї глави: вона безповоротно знищує роботу і робить це тихо, без підтвердження. Незакомічене нищить і git restore над файлом, але там намір явний — ви самі назвали, що викинути; а --hard виглядає як невинний відкат коміту. Небезпечний не сам відкат (коміти в reflog), а те, що --hard заразом затирає незакомічені зміни в робочій директорії й індексі. А їх, як ми з'ясували, не версіоновано — тож жоден reflog їх не поверне.
Класичний сценарій втрати: ви півгодини правили тест, нічого не комітили, потім вирішили «почати начисто» й зробили git reset --hard HEAD. Коміт HEAD на місці — а ваші півгодини зникли назавжди. Виглядає як «безпечний відкат до останнього коміту», а насправді — безшумне видалення всього незбереженого.
Практична гігієна:
- Перед
reset --hardзавждиgit status— переконайтесь, що в робочій директорії немає нічого, що шкода втратити. - Якщо є навіть підозра, що зміни потрібні, — спершу
git stash(або звичайний коміт «чернетка»), і лише потім експериментуйте. Stash повертається,--hard— ні. --hardнад запушеним комітом — це ще й переписування спільної історії з усіма наслідками з розділу про revert.
Типові помилки
Виглядає як «прибрав один файл»: git checkout . мав скасувати правку в одному файлі, а зніс усі незбережені зміни. Насправді і старий checkout -- ., і git restore . без імені файла діють на всю робочу директорію. Скасовуйте адресно: git restore <конкретний файл>.
Виглядає як «зробив reset, і коміт пропав назавжди». Насправді reset лише пересунув вказівник гілки; коміт живий і його хеш є в git reflog (протягом десятків днів). Панікувати не треба — треба подивитись reflog і зробити reset --hard <хеш> назад.
Виглядає як «revert видалить баговий коміт з історії». Насправді revert нічого не видаляє: він додає новий коміт зі зворотними змінами. І баг, і його скасування лишаються в історії — це навмисно, щоб не переписувати спільну гілку.
Виглядає як «застешив і перемкнувся — усе збережено». Насправді git stash за замовчуванням не бере невідстежувані файли. Новий, ще не доданий файл лишиться в робочій директорії; для нього потрібен git stash -u.
Виглядає як «--amend просто підправить повідомлення». Насправді --amend створює новий коміт з новим хешем. Якщо старий коміт уже запушений, історія розійдеться, і push пройде лише з --force — на спільній гілці цього робити не можна.
Підсумок
resetрухає вказівник гілки; що станеться зі staging і файлами — вирішує режим:--soft(лишає застейдженим) →--mixed(лишає в робочій директорії) →--hard(знищує незакомічене).resetі--amendпереписують історію — це безпечно лише для локальних, ще не запушених комітів. Для скасування коміту в спільній гілці існує лишеrevert, який додає коміт-антидот, а не стирає минуле.- Незакомічене не версіонується:
git restore <файл>іgit reset --hardзнищують такі правки безповоротно — reflog їх не поверне, бо в них немає хеша. reflog— локальний журнал переміщеньHEAD; він рятує «втрачені» коміти після reset/amend/rebase протягом десятків днів. Перша реакція на «я загубив коміт» — не паніка, аgit reflog.stashтимчасово відкладає незавершене, щоб перемкнути гілку; це стек, і за замовчуванням він не бере untracked-файли.
Що питають на співбесіді
- «Чим
resetвідрізняється відrevert? Коли який?» — головне питання теми. Інтерв'юер перевіряє не знання команд, а розуміння, що гілка спільна: чи усвідомлює кандидат, щоresetпереписує історію і на запушеному ламає роботу іншим, аrevertбезпечний, бо лише додає коміт. - «Поясни
--soft,--mixed,--hard.» — перевірка моделі трьох зон. Сильна відповідь прив'язує кожен прапорець до того, які зони він чіпає, а не переказує наслідки напам'ять. - «Ти випадково зробив
git reset --hardі втратив коміт. Дії?» — перевіряють, чи кандидат знає проreflogі чи не панікує. Бонус — уточнити: якщо втрачене було незакоміченим, reflog не допоможе. - «Закомітив у
mainзамість фіча-гілки. Як виправити?» — очікуютьreset --soft HEAD~1+ новий коміт у правильній гілці (для локального стану), і згадку проrevert, якщо вже запушено. - «Треба терміново перемкнути гілку, а є незавершені зміни.» — очікують
git stash, і що кандидат згадає про-uдля untracked.
Червоний прапорець для інтерв'юера — коли кандидат пропонує push --force на спільну гілку як буденний інструмент, не згадуючи ризику для колег. Зелений — коли сам розрізняє «локальне, переписую вільно» і «спільне, тільки revert».
Джерела
- Pro Git — 2.4 Undoing Things та 7.7 Reset Demystified — канонічне пояснення reset через три зони.
- git-reset, git-revert, git-restore, git-stash — офіційна документація команд.
- git-reflog — журнал посилань і терміни зберігання записів.
Що таке три зони Git і чому «скасувати зміни» завжди починається з питання, у якій із них лежить те, що відкочуємо?
Git тримає ваш стан у трьох місцях: робоча директорія (working directory) — файли, які ви редагуєте зараз; індекс (index, staging area) — чернетка наступного коміту, куди правки потрапляють через git add; і репозиторій (repository) — ланцюг зафіксованих комітів, на верхівку гілки якого вказує HEAD. Сама фраза «скасувати зміни» нічого не означає, поки не уточнено рівень: викинути незбережену правку у файлі, розстейджити помилково доданий файл, зняти останній коміт чи відкотити давній коміт у спільній гілці — це чотири різні задачі з чотирма різними інструментами. Тому спокійний інженер спершу питає не «яку команду набрати», а «на якій зоні живе те, що я хочу прибрати». Коли зона визначена, вибір команди майже автоматичний, і одразу видно, що майже все оборотне, а по-справжньому небезпечна лише одна операція.
Чим git restore <файл> відрізняється від git restore --staged <файл>?
Це дві протилежні дії, які раніше плутали в перевантаженому git checkout. git restore <файл> викидає незастейджені правки в робочій директорії й повертає файл до тієї версії, що лежить в індексі (або в останньому коміті) — тобто знищує вашу незбережену роботу над цим файлом. git restore --staged <файл> нічого не знищує: він лише виймає файл із чернетки коміту, а самі зміни лишаються у вас під руками в робочій директорії. Простими словами: перша команда — «забудь мою правку», друга — «просто прибери цей файл зі staging, правку лиши». Плутанина між ними коштує втраченої роботи, тому корисно проговорювати вголос, знищуєте ви зміну чи лише розстейджуєте.
Чому git restore над робочою директорією — операція без страхувальної сітки?
Бо робоча директорія не версіонується. Git пам'ятає лише те, що потрапило хоча б у staging чи в коміт — у нього є хеш і його можна дістати навіть після «видалення». А правка, яка жила тільки у файлі на диску й ніколи не додавалась через git add, хеша не має; коли git restore повертає файл до попереднього стану, ця правка зникає безслідно. Її не поверне reflog, бо в reflog потрапляють переміщення посилань, а не вміст неверсіонованих файлів. Практичний висновок: над незбереженим restore так само небезпечний, як reset --hard, тому перед ним варто хоча б застейджити або застешити те, що шкода втратити.
Що концептуально робить git reset і як його змінюють режими --soft, --mixed, --hard?
По суті git reset робить одну річ — пересуває вказівник поточної гілки на інший коміт; усе інше залежить від режиму, який вирішує долю індексу й робочої директорії. --soft рухає лише вказівник: зміни відкоченого коміту лишаються застейдженими, готовими до нового коміту. --mixed (це дефолт, якщо режим не вказали) рухає вказівник і скидає індекс — зміни повертаються в робочу директорію незастейдженими. --hard рухає вказівник і затирає і індекс, і робочу директорію під цільовий коміт — усе незакомічене знищується. Сильна відповідь на співбесіді прив'язує кожен прапорець саме до зон, які він чіпає (тільки HEAD → HEAD плюс індекс → HEAD плюс індекс плюс файли), а не переказує наслідки напам'ять.
Ти випадково закомітив фікс автотесту в main замість фіча-гілки. Як це виправити, якщо ще не запушив?
Оскільки коміт локальний і його ще ніхто не бачив, історію можна переписати безпечно. Робочий рецепт: git reset --soft HEAD~1 знімає останній коміт із main, лишаючи всі його зміни застейдженими; далі створюєте потрібну гілку (git switch -c fix/login-wait) і комітите вже туди. У підсумку main повертається до чистого стану, ніби коміту й не було, а робота ціла і опиняється там, де мала бути. Ключове тут — саме --soft: він не чіпає ваші файли, тільки переставляє вказівник. Якби ви цей коміт уже запушили в спільну гілку, відповідь була б інша — revert, бо переписувати опубліковану історію не можна.
Що насправді робить git commit --amend і чому він безпечний лише до пушу?
git commit --amend замінює останній коміт новим — зручно, коли коміт правильний за суттю, але з одруком у повідомленні або із забутим файлом. Важлива механіка: коміт у Git незмінний, тож --amend не редагує його «на місці», а створює новий коміт із новим хешем і перевішує на нього гілку, а старий стає недосяжним. Тобто це переписування історії в мініатюрі. Наслідок: поки коміт не запушений, амендити можна вільно. Але якщо старий коміт уже бачили колеги, ваша локальна історія розійдеться з віддаленою, і push пройде лише з --force, що ламає роботу іншим. Тому --amend на вже опублікованому коміті — те саме табу, що й reset спільної гілки.
У чому принципова різниця між git reset і git revert і коли брати який?
Обидві команди «скасовують коміт», але роблять це протилежно. git reset пересуває вказівник гілки назад — небажаний коміт зникає з історії, ніби його не було; історія переписується. git revert створює новий коміт, який застосовує зворотні зміни до цільового — небажаний коміт лишається в історії, а поверх з'являється коміт-антидот; історія доповнюється, а не переписується. Звідси механічне правило вибору: коміт локальний і ще не запушений — можна reset; коміт уже в спільній гілці — тільки revert. Це найчастіше питання теми, і інтерв'юер перевіряє не знання синтаксису, а розуміння, що гілка спільна: чи усвідомлює кандидат, що reset на запушеному ламає роботу іншим.
Чому опубліковану історію можна скасовувати лише через revert, а не reset?
Бо у Git гілка — спільний ресурс. Щойно ви запушили коміти, їх уже могли витягнути через pull колеги і CI. Якщо ви приберете ці коміти через reset і зробите push --force, ваша історія розійдеться з їхньою: у них коміти ще є, у вас — уже ні. Наступний їхній pull або поверне «видалене» назад, або впаде конфліктом, або хтось затре чужу роботу. Це і є «золоте правило» Git: не переписуй історію, яку вже бачать інші. revert цю проблему обходить, бо нічого не стирає — він додає новий коміт зі зворотними змінами, і історія лишається несуперечливою для всіх, хто вже має ці коміти в себе.
Як скасувати merge-коміт через revert і чому тут потрібен додатковий прапорець?
Merge-коміт особливий тим, що має двох батьків, а не одного, тож Git не знає, «до якого зі станів» повертати, — і без підказки git revert над ним просто відмовиться працювати. Треба явно вказати, яку з батьківських гілок вважати основною, через прапорець -m (mainline): git revert -m 1 <хеш> каже брати за базу першого батька — зазвичай це гілка, в яку вливали. Це деталь, яка відрізняє сильного кандидата: він знає, що revert мержу — не те саме, що revert звичайного коміту, і що вибір -m 1 проти -m 2 змінює, які саме зміни відкотяться. Ще нюанс: після такого revert повторний мерж тієї ж гілки може повестися несподівано, бо Git вважатиме зміни вже застосованими.
Правда, що git revert видаляє баговий коміт з історії?
Ні, і це поширена помилка. revert нічого не видаляє — він додає новий коміт, який застосовує зворотні зміни до цільового. У підсумку і сам баговий коміт, і його скасування лишаються в історії поруч. Це зроблено навмисно: саме так вдається не переписувати спільну гілку. Для аудиту така поведінка — плюс, а не недолік: за історією видно, що баг був, коли його завезли й коли відкотили. Якщо ж мета — саме «стерти слід», щоб коміта ніби ніколи не існувало, то це вже переписування історії (reset/rebase), допустиме лише на локальних, ще не запушених комітах.
Навіщо потрібен git stash і як він працює механічно?
git stash рятує в типовій ситуації: ви на середині правки, нічого не готове до коміту, і прилітає «терміново перевір фікс у гілці release». Перемкнути гілку з брудною робочою директорією Git часто не дасть, а комітити напівфабрикат не хочеться. Механіка така: stash забирає зміни з робочої директорії й індексу, зберігає їх окремо (технічно — як приховані коміти) і повертає робочу директорію до стану HEAD, тобто робить її чистою. Ви спокійно перемикаєтесь, робите термінове, повертаєтесь і накладаєте відкладене назад. Це стек (полиця): застешити можна кілька разів, а елементи адресуються як stash@{0}, stash@{1} і так далі.
Яка різниця між git stash pop і git stash apply?
Обидві команди накладають відкладені зміни назад на робочу директорію, але по-різному поводяться з полицею. git stash pop бере найсвіжіший елемент (stash@{0}), застосовує його й одразу прибирає з полиці — зручно, коли stash разовий і більше не потрібен. git stash apply застосовує той самий елемент, але лишає його копію на полиці — корисно, якщо треба накласти ті самі зміни на кілька гілок або підстрахуватися на випадок конфлікту. Практична порада: якщо pop наткнувся на конфлікт, елемент зазвичай не видаляється, тож панікувати не варто; але звичка ставити apply, а потім свідомо git stash drop, дає більше контролю, коли зміни цінні.
Які дві типові пастки чатують на того, хто користується stash?
Перша: за замовчуванням stash не бере невідстежувані (untracked) файли — новий файл, який ви ще не додавали через git add, лишиться на місці й не сховається. Ви перемкнете гілку, думаючи, що робоча директорія чиста, а він волочитиметься за вами; щоб застешити і його, потрібен git stash -u. Друга: stash — це стек, і pop завжди знімає найсвіжіший елемент; якщо ви застешили кілька разів і забули порядок, легко застосувати не той. Тому stash добрий для коротких перемикань, а не для довготривалого зберігання «на потім» — для такого надійніша звичайна гілка з нормальним комітом: її не доведеться згодом розшукувати серед безіменних полиць у git stash list.
Що таке git reflog і чому він знімає паніку після невдалого reset?
reflog (reference log) — це локальний журнал переміщень HEAD і гілок: кожного разу, коли HEAD кудись переходив (коміт, reset, amend, rebase, перемикання гілок), туди лягає запис із хешем. Тому коли після reset вам здається, що коміт «пропав назавжди», насправді він нікуди не подівся: команда лише пересунула вказівник, а хеш коміту досі лежить у git reflog. Ви дивитесь журнал, знаходите попереднє положення (наприклад HEAD@{1}), берете його хеш і повертаєте гілку назад через git reset --hard <хеш>. Записи не вічні — досяжні коміти тримаються близько 90 днів, недосяжні близько 30, — але цього з надлишком вистачає, щоб виправити помилку того ж дня. Перша реакція на «я загубив коміт» — не паніка, а git reflog.
Де межа можливостей reflog — чого він не врятує?
Reflog має два принципові обмеження. Перше: він рятує коміти, але не рятує незакомічене. Правки, що жили лише в робочій директорії й ніколи не потрапляли навіть у staging, не мають хеша, тож у reflog їх просто немає — після git reset --hard чи git restore над ними вони зникають безповоротно. Друге: reflog локальний — він живе тільки у вашому клоні репозиторію. На чужій машині вашого reflog немає, і в свіжому клоні його теж немає, тож він не допоможе, якщо ви вже пересіли за інший комп'ютер. Саме тому reset --hard над незбереженою роботою — найтихіша втрата в Git: команда виглядає як безпечний відкат до останнього коміту, а насправді безшумно знищує все, що не встигло стати комітом.
Чому саме git reset --hard вважають єдиною по-справжньому небезпечною командою теми і як з ним поводитись?
Небезпечний не сам відкат коміту — коміти в reflog і повертаються, — а те, що --hard заразом затирає незакомічені зміни в робочій директорії й індексі, а їх, як не версіонованих, не поверне ніщо. Класичний сценарій втрати: ви півгодини правили тест, нічого не комітили, вирішили «почати начисто» й зробили git reset --hard HEAD; коміт на місці, а ваші півгодини зникли. І робить це команда тихо, без підтвердження. Гігієна проста: перед reset --hard завжди git status, щоб переконатися, що в робочій директорії немає нічого цінного; за найменшої підозри спершу git stash або коміт-чернетка, і лише потім експерименти — stash повертається, --hard ні. Окремо пам'ятайте: --hard над запушеним комітом — це ще й переписування спільної історії з усіма наслідками золотого правила.
Три ситуації з робочого дня AQA, де питання не «яка команда крутіша», а «на якій зоні лежить те, що я псую, і чи вже бачили це колеги». Скрізь — послідовність дій і чому саме так.
Кейс 1. Закомітив рефакторинг page object не в ту гілку
Класика: ви годину чистили LoginPage, закомітили — і аж тепер помітили, що сиділи в main, а не у фіча-гілці. Перше й головне питання: цей коміт уже запушений? Від відповіді залежить геть усе.
Якщо ще не запушений — історія ваша особиста, переписуйте вільно:
# 1) перевіряємо, де ми і що накоїли
git status
git log --oneline -3 # верхній коміт — той самий "не туди"
# 2) знімаємо коміт із main, зміни лишаємо застейдженими
git reset --soft HEAD~1
# 3) переносимо роботу в правильну гілку і комітимо вже там
git switch -c refactor/login-page
git commit -m "refactor: винести локатори LoginPage в page object"
Що тут працює і чому:
--softне чіпає ваші файли. Він рухає лише вказівникmainна один коміт назад; усі зміни рефакторингу лишаються в індексі, готові до нового коміту. Якби ви взяли--hard, робота б зникла.mainповертається до чистого стану. Після кроку 2 гілка виглядає так, ніби зайвого коміту й не було, — саме те, що треба для гілки, куди інші дивляться.
Якщо ж коміт уже запушений у main, reset заборонений — ви розійдетеся з колегами. Тоді робота двоетапна: скасувати ефект у спільній гілці через revert, а самі зміни перенести в фіча-гілку окремо (наприклад, cherry-pick того коміту у нову гілку до реверту).
git revert <хеш-коміту> # у main з'явиться коміт-антидот, історія несуперечлива
Кейс 2. «Хочу скасувати X» — таблиця рішень
Половина плутанини зникає, щойно перекласти розмите «відкотити зміни» на дві осі: яка зона і чи запушено. Ось як ситуація мапиться на команду.
| Що хочу скасувати | Стан | Команда | Що станеться зі змінами |
|---|---|---|---|
| Незбережену правку у файлі | не закомічена | git restore <файл> | знищується безповоротно |
| Помилково застейджений файл | у staging | git restore --staged <файл> | лишається в робочій директорії |
| Одрук у повідомленні останнього коміту | не запушено | git commit --amend | новий хеш того ж коміту |
| Забутий файл у останньому коміті | не запушено | git add ... + git commit --amend --no-edit | доклеюється до коміту |
| Останній коміт, зміни лишити | не запушено | git reset --soft HEAD~1 | застейджені |
| Останній коміт, розібрати назад до правок | не запушено | git reset HEAD~1 (mixed) | у робочій директорії |
| Коміт у спільній гілці | запушено | git revert <хеш> | додається коміт-антидот |
| Тимчасово відкласти все незавершене | не закомічено | git stash -u | на полиці, робоча директорія чиста |
Головне правило читання таблиці: якщо в колонці «стан» стоїть «запушено» — рядок із reset/--amend вам недоступний, лишається revert. І окремо тримайте в голові git reset --hard: його в таблиці немає навмисно, бо він знищує незакомічене без вороття — беріть його свідомо, а не як «відкотити все».
Кейс 3. Панічний reset --hard — і де reflog безсилий
Ви зробили кілька комітів, потім вирішили «переграти» й рубанули git reset --hard HEAD~3. За секунду згадали, що серед відкочених був потрібний коміт із перевіркою тосту. Виглядає як катастрофа — насправді ні, доки йшлося про коміти.
# дивимось журнал переміщень HEAD (згори — найсвіжіше)
git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~3
# 9f8e7d6 HEAD@{1}: commit: додав перевірку тосту <- ось "втрачений" коміт
# ...
# повертаємо гілку рівно на той коміт
git reset --hard 9f8e7d6
Що дивитися і чому:
resetне знищує коміти — він рухає вказівник. Хеш нікуди не дівається, він лежить у reflog десятки днів. Тому перша реакція на «загубив коміт» — не паніка, аgit reflog, де видно попереднє положенняHEAD(HEAD@{1}).- reflog локальний. Цей рятунок працює лише на тій машині, де ви робили reset. У свіжому клоні чи на CI-раннері вашого reflog немає.
А тепер межа, об яку розбивають надії. Якщо перед reset --hard у вас були незакомічені правки — півгодини роботи над тестом, які ви не встигли навіть застейджити, — reflog їх не поверне. У них немає хеша, тож у журналі їх просто немає. Саме тому дисципліна така: git status перед --hard, а якщо є хоч щось цінне — спершу застрахуватись.
# страховка перед будь-яким небезпечним відкатом
git status # переконатись, що в робочій директорії немає незбереженого
git stash -u # відкласти все, включно з untracked, на полицю
# ...експерименти...
git stash pop # повернути, коли вляглося
Правило одним рядком: stash повертається, reset --hard над незбереженим — ні.
Три зони і що означає «скасувати»
- Розрізняю три зони Git — робоча директорія, індекс (staging), репозиторій (
HEAD) — і знаю, що вибір команди починається з питання, на якій із них лежить те, що відкочую. - Пам'ятаю, що робоча директорія не версіонується: правку, яка ніколи не потрапляла хоча б у staging, не поверне жоден reflog.
restore: незакомічене
- Знаю різницю
git restore <файл>(знищує незастейджену правку) vsgit restore --staged <файл>(лише розстейджує, зміни лишає). - Розумію, чому
git restore .без імені файла небезпечний — діє на всю робочу директорію, а не на один файл.
reset і його режими
- Можу пояснити, що
git resetконцептуально робить одне — пересуває вказівник гілки, а долю індексу й файлів вирішує режим. - Знаю таблицю режимів:
--soft(тількиHEAD, зміни застейджені) →--mixed/дефолт (HEADплюс індекс, зміни в робочій директорії) →--hard(HEAD, індекс і файли, незакомічене знищено). - Пам'ятаю рятувальний сценарій «закомітив у
main»:git reset --soft HEAD~1плюс новий коміт у правильній гілці.
commit --amend і переписування історії
- Розумію, що
git commit --amendне редагує коміт «на місці», а створює новий коміт із новим хешем — тобто переписує історію в мініатюрі. - Знаю, що
--amendбезпечний лише до пушу; на опублікованому коміті він розводить локальну й віддалену історію і вимагає--force.
reset vs revert: головне питання
- Можу чітко протиставити:
resetприбирає коміт з історії (переписує),revertдодає коміт-антидот (доповнює). - Знаю механічне правило: локальний ще не запушений коміт — можна
reset; коміт у спільній гілці — тількиrevert. - Можу пояснити «золоте правило» через спільність гілки: після
push --forceісторія розійдеться з колегами, і чийсьpullце зламає. - Розумію, що
revertнічого не видаляє: і баг, і його скасування навмисно лишаються в історії (плюс для аудиту). - Знаю, що revert merge-коміту потребує
-m 1(вибір основного батька), бо в мержу два предки.
stash
- Знаю різницю
git stash pop(застосувати й прибрати з полиці) vsgit stash apply(застосувати, копію лишити). - Пам'ятаю пастку: за замовчуванням stash не бере untracked-файли, для них потрібен
git stash -u. - Розумію, що stash — це стек (
stash@{0},stash@{1}), тож для довгого зберігання надійніша звичайна гілка.
reflog і безпека
- Розумію, що
reflog— локальний журнал переміщеньHEAD; перша реакція на «загубив коміт» — не паніка, аgit reflogплюсgit reset --hard <хеш>назад (коміти живуть десятки днів). - Пам'ятаю дві межі reflog: він локальний (немає на чужій машині чи в свіжому клоні) і не рятує незакомічене (немає хеша).
- Розумію, чому
git reset --hard— єдина по-справжньому руйнівна команда: тихо затирає незакомічене, і роблюgit statusперед нею завжди.
Що концептуально робить `git reset`, незалежно від режиму?
Питання
Три зони Git — які й до чого тут «скасувати зміни»?