Git
Chapters ▾ 2nd Edition

7.7 Гит алати - Демистификовани ресет

Демистификовани ресет

Пре него што пређемо на више специјализоване алате, хајде да представимо reset и checkout. Ове команде су два дела програма Гит који уводе највише забуне када се први пут сретнете са њима. Оне обављају доста ствари, тако да њихово потпуно разумевање и правилна употреба изгледа безнадежно. Зато предлажемо једноставну метафору.

Три стабла

Једноставнији начин размишљања о командама reset и checkout је да себи у мислима представите програм Гит као менаџера садржаја три различита стабла. Овде под „стабло” уствари мислимо на „колекцију фајлова”, а не на одређену структуру података. Постоји неколико случајева у којима се индекс не понаша баш као стабло, али за наше потребе је једноставније да се за сада овако посматрају ствари.

Програм Гит у свом уобичајеном раду као систем управља и манипулише са три стабла:

Стабло Улога

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

Команде cat-file и ls-tree су „водоводне” команде које се користе за ствари ниског нивоа и обично се не користе у свакодневном раду, али нам помажу да видимо шта се овде дешава.

Индекс

Индекс је ваш предложени наредни комит. Овај концепт смо такође звали и „стејџ” (позорницу) програма Гит, јер је то оно шта програм Гит посматра када извршите git commit.

Програм Git попуњава овај индекс листом садржаја свих фајлова који су последњи пут били одјављени у ваш радни директоријум и како су изгледали у тренутку одјављивања. Затим неке од тих фајлови замените новим верзијама, и git commit то конвертује у стабло за нови комит.

$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0	README
100644 8f94139338f9404f26296befa88755fc2598c289 0	Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0	lib/simplegit.rb

Овде поново користимо ls-files, што је више позадинска команда која приказује како тренутно изгледа ваш индекс.

Технички, индекс није структура стабла – уствари је имплементиран као спљоштени манифест – али је за наше потребе довољно близу томе.

Радни директоријум

Коначно, имате свој радни директоријум. Друга два стабла чувају свој садржај на ефикасан али незгодан начин, унутар .git директоријума. Радни директоријум их распакује у стварне фајлове, тако да вам је много лакше да их уређујете. Посматрајте радни директоријум као изоловано окружење (sandbox), у којем измене можете да испробате пре него што их комитујете у стејџ (индекс), па затим у историју.

$ tree
.
├── README
├── Rakefile
└── lib
    └── simplegit.rb

1 directory, 3 files

Процес рада

Главна сврха програма Гит је да бележи снимке вашег пројекта у сукцесивно бољим стањима, манипулацијом ова три стабла.

reset workflow

Хајде да визуелизујемо овај процес: рецимо да одете у директоријум који садржи само један фајл. То ћемо звати v1 фајла, и означаваћемо га плавом бојом. Сада извршимо git init, што креира Гит репозиторијум са HEAD референцом која показује на још увек нерођену master грану.

reset ex1

У овом тренутку, само радни директоријум има неки садржај.

Сада желимо да комитујемо овај фајл, па употребимо git add да узме садржај из радног директоријума и да га копира у индекс.

reset ex2

Затим извршимо git commit, која узима садржај индекса и чува га као трајни снимак, креира комит објекат који показује на тај снимак и ажурира master тако да показује на тај комит.

reset ex3

Ако сада извршимо git status, нећемо видети никакве промене јер су сва три стабла потпуно иста.

Сада желимо да изменимо тај фајл и да га комитујемо. Проћи ћемо кроз исти процес; најпре променимо фајл у свом радном директоријуму. Хајде да то назовемо v2 фајла и да га означавамо црвеном бојом.

reset ex4

Ако сада извршимо git status видећемо фајл у црвеној боји ка „Changes not staged for commit” (измене које нису стејџоване за комит), јер се та ставка разликује у односу на индекс и радни директоријум. Затим извршавамо git add над њим да га стејџујемо у индекс.

reset ex5

Ако у овом тренутку извршимо git status видећемо фајл у зеленој боји под „Changes to be committed” (измене које ће се комитовати) јер се индекс и HEAD разликују – то јест, наш предложени наредни комит се разликује од последњег комита. Коначно извршимо git commit да довршимо комит.

reset ex6

Сада нам git status не приказује никакав излаз јер су поново сва три стабла иста.

Прелаз на друге гране или клонирање пролазе кроз сличан процес. Када одјавите грану, то мења HEAD тако да показује на референцу нове гране, попуњава ваш индекс снимком тог комита, па затим копира садржај индекса у ваш радни директоријум.

Улога команде ресет

Команда reset има много више смисла када се посматра у овом контексту.

У сврху ових примера, рецимо да смо поново изменили file.txt и комитовали га по трећи пут. Тако да наша историја сада изгледа овако:

reset start

Хајде да сада прођемо кроз оно што reset ради када је позовете. Она на једноставан и предвидив начин директно манипулише ова три стабла. Обавља до три основне операције.

Корак 1: померање HEAD

Прва ствар коју ће команда reset урадити је да помери оно на шта показује HEAD. Ово није исто као измена самог HEAD (што је оно што ради команда checkout); reset помера грану на коју показује HEAD. Ово значи да се HEAD поставља на master грану (тј. тада сте тренутно на master грани), извршавајући git reset 9e5e6a4 први корак ће бити да master показује на 9e5e6a4.

reset soft

Без обзира на то коју форму команде reset са комитом позовете, ово је прва ствар коју ће она увек урадити. У случају reset --soft, једноставно ће се ту зауставити.

Застаните на тренутак и погледајте дијаграм да схватите шта се догодило: у суштини је поништена последњу git commit команда. Када извршите git commit, програм Гит креира нови комит и помера грану на коју показује HEAD на тај комит. Када извршите reset назад на HEAD~ (родитељ од HEAD), грану враћате назад где је била, без измене индекса или радног директоријума. Сада бисте могли да ажурирате индекс и поново извршите git commit да постигнете оно што би урадила команда git commit --amend (погледајте Измена последњег комита).

Корак 2: ажурирање индекса (--mixed)

Приметите да ако сада извршите git status видећете у зеленој боји разлику између индекса и онога што је сада нови HEAD.

Следећа ствар коју ће урадити reset је да ажурира индекс садржајем снимка на који HEAD сада показује.

reset mixed

Ако сте задали опцију --mixed, команда reset ће се зауставити овде. Такође, ово је и подразумевано понашање, па ако уопште не наведете ни једну опцију (само git reset HEAD~ у овом случају), ово је место на којем ће се команда зауставити.

Погледајте сада још једном тај дијаграм и уочите шта се догодило: поништила је ваш последњи commit, али је такође и све уклонила са стејџа. Премотали сте уназад до места пре покретања свих ваших git add и git commit команди.

Корак 3: ажурирање радног директоријум (--hard)

Трећа ствар коју ће команда reset урадити је да ваш радни директоријум учини да изгледа као индекс. Ако употребите опцију --hard, наставиће са извршавањем до ове етапе.

reset hard

Па хајде да размислимо о ономе што се управо догодило. Поништили сте свој последњи комит, git add и git commit команде и сав рад који сте урадили у радном директоријуму.

Важно је приметити да је ова заставица (--hard) једини начин да команда reset буде опасна и један је од врло малог броја случајева у којима програм Гит заиста може да уништи податке. Сваки други начин позива команде reset може једноставно да се поништи, али опција --hard не може јер насилно преписује фајлове у радном директоријуму. У овом одређеном случају, још увек имамо v3 верзију нашег фајла у комиту базе података програма Гит, и могли бисмо да је вратимо ако погледамо у reflog, али да је нисмо комитовали, програм Git би преписао фајл и не би било шансе да се он опорави.

Рекапитулација

Команда reset преписује ова три стабла у одређеном редоследу, заустављајући се када јој кажете:

  1. Помера грану на коју показује HEAD (овде стаје ако задате --soft)

  2. Чини да индекс изгледа као HEAD (овде стаје нисте задали --hard)

  3. Чини да радни директоријум изгледа као индекс

Ресет са путањом

Ово до сада покрива понашање команде reset у њеном основном облику, али такође можете да јој задате и путању над којом ће да оперише. Ако наведете путању, команда reset ће прескочити корак 1 и ограничити остатак својих акција на одређени скуп фајлова. Ово донекле има смисла – HEAD је само показивач и не може да показује на део једног комита и део неког другог. Али индекс и радни директоријум можете делимично да ажурирате, тако да ресет наставља са корацима 2 и 3.

Дакле, претпоставимо да извршимо git reset file.txt. Овај облик (пошто нисте навели SHA-1 комита или грану, а нисте навели ни --soft ни --hard) је скраћеница за git reset --mixed HEAD file.txt, што ће:

  1. Померити грану на коју показује HEAD (прескочено)

  2. Учинити да индекс изгледа као HEAD (овде се зауставља)

Тако да у суштини само копира file.txt из HEAD у индекс.

reset path1

Практични ефекат овога је да се фајл уклања са стејџа. Ако погледамо дијаграм за ту команду и размислимо о томе шта ради git add, оне су потпуно супротне.

reset path2

Због овога излаз команде git status сугерише да ово извршите ако желите да фајл уклоните са стејџа (За више о овоме, погледајте Уклањање фајла са стејџа).

Исто тако смо могли и да не дозволимо програму Гит да претпостави како смо мислили „повуци податке са HEAD” наводећи одређени комит из којег да повуче верзију фајла. Могли смо да извршимо нешто као што је git reset eb43bf file.txt.

reset path3

Ово у суштини ради исту ствар као да смо у радном директоријуму вратили садржај фајла назад на v1, извршили git add над њим, па га затим поново вратили назад на v3 (а да не прођемо кроз све те кораке). Ако сада извршимо git commit, она ће снимити измену која враћа тај фајл назад на v1, мада је уствари никада нисмо ни имали поново у радном директоријуму.

Такође је интересантно приметити да као и git add, команда reset прихвата опцију --patch да са стејџа уклони садржај по принципу комад-по-комад. Тако да садржај селективно можете да уклоните са стејџа или вратите на старије стање.

Гњечење

Хајде да видимо како можемо урадити нешто корисно употребом ове управо откривене моћи – гњечење комитова.

Рецимо да имате низ комитова са порукама као што су „ууупс.”, „WIP” (рад је у току) и „заборавио сам овај фајл”. Можете употребити reset да их брзо и једноставно згњечите у један једини комит због којег ћете изгледати заиста паметно. Сажимање комитова приказује други начин да се ово постигне, али у овом примеру је једноставније да се употреби reset.

Рецимо да имате пројекат у којем је први комит имао само један фајл, други комит је додао нови фајл и изменио први, и трећи комит је поново изменио први фајл. У другом комиту се налази рад у току и желите да га згњечите.

reset squash r1

Можете да извршите git reset --soft HEAD~2 да HEAD грану вратите назад на старији комит (први комит који желите да задржите):

reset squash r2

И да онда једноставно поново извршите git commit:

reset squash r3

Сада можете видети да ваша доступна историја, историја коју бисте гурнули, изгледа као да сте направили један комит са file-a.txt v1, затим други који је изменио file-a.txt на v3 и додао file-b.txt.Комит са v2 верзијом фајла се више не налази у историји.

Одјавите га

На крају, упитаћете се шта је разлика између checkout и reset. Као reset, и checkout манипулише са три стабла, и донекле се разликује у зависности од тога да ли команди задате путању до фајла или не.

Без путањи

Извршавање git checkout [грана] је прилично слично са извршавањем git reset --hard [грана] у смислу да вам ажурира сва три стабла тако да изгледају као [грана], али постоје две важне разлике.

Прво, за разлику од reset --hard, checkout је безбедна за радни директоријум; извршиће проверу којом обезбеђује да вам не одува фајлове које сте изменили. Уствари, још мало је и паметнија – покушава да у радном директоријуму изврши тривијално спајање, тако да ће се ажурирати сви фајлови је нисте изменили. С друге стране, reset --hard ће једноставно редом заменити све ствари без икакве провере.

Друга важна разлика је начин на који се ажурира 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

Са путањама

Други начин да се изврши команда checkout је са путањом до фајла, који као и reset, не помера показивач HEAD. То је исто као git reset [грана] фајл у смислу да ажурира индекс задатим фајлом у том комиту, али такође и преписује фајл у радном директоријуму. Било би потпуно исто као и git reset --hard [грана] фајл (ако би вам команда reset дозволила да то извршите) – није безбедно по радни директоријум и не помера HEAD.

Такође, као git reset и git add, команда checkout ће прихватити опцију --patch која вам омогућава да селективно враћате старо стање садржаја фајла по принципу комад-по-комад.

Резиме

Надамо се да сада разумете команду reset и да можете удобније да је користите, али сте вероватно још увек донекле збуњени у вези тога како се прецизно она разликује у односу на команду checkout и верованто не можете да запамтите сва правила различитих начина позивања.

Ево „пушкице” о томе које команде утичу на која стабла. У колони „HEAD” стоји „РЕФ” ако та команда помера референцу (грану) на коју указује HEAD, а „HEAD” ако помера сам HEAD показивач. Посебну пажњу обратите колони ’РД безбедна?’ – ако у њој пише НЕ, застаните на секунд да добро размислите пре извршавања те команде.

HEAD Индекс Раднидир РД безбедна?

Комит ниво

reset --soft [комит]

РЕФ

НЕ

НЕ

ДА

reset [комит]

РЕФ

ДА

НЕ

ДА

reset --hard [комит]

РЕФ

ДА

ДА

НЕ

checkout [комит]

HEAD

ДА

ДА

ДА

Фајл ниво

reset (комит) [фајл]

НЕ

ДА

НЕ

ДА

checkout (комит) [фајл]

НЕ

ДА

ДА

НЕ