Git
Chapters ▾ 2nd Edition

7.7 Mga Git na Kasangkapan - Ang Reset Demystified

Ang Reset Demystified

Bago lumipat sa mas natatanging mga kasangkapan, pag-usapan natin ang tungkol sa Git ang reset at checkout na mga utos. Ang mga utos na ito ay dalawang pinakanakakalito na mga parte sa Git na kapag una mong nakasalubong sila. Marami silang ginawa sa mga bagay na tila walang pag-asa upang aktwal na maunawaan ang mga ito at gumamit nila ng maayos. Para dito, inirerekumenda natin ang isang simpleng metapora.

Ang Tatlong mga Tree

Isang mas madaling paraan para isipin ang tungkol sa reset at checkout ay sa pamamagitan ng mental frame sa Git sa pagiging tagapamahala ng nilalaman sa tatlong magkaibang mga tree. Sa pamamagitan ng “tree” dito, talagang ibig sabihin nito ay “collection of files”, hindi particular na kaayusan ng datos. (Mayroong ilang mga kaso na kung saan ang index ay hindi eksaktong kumikilos tulad ng isang tree, para sa aming mga gamit ito ay mas madali para isipin ang tungkol nito sa ganitong paraan para sa ngayon.)

Ang Git bilang isang sistemang namamahala at humahawak sa tatlong mga tree sa kanyang normal na operasyon:

Tree Role

HEAD

Huling commit ng snapshot, susunod na magulang

Index

Inimungkahing susunod na commit ng snapshot

Tinatrabahong Direktoryo

Sandbox

Ang HEAD

Ang HEAD ay ang puntero sa kasalukuyang branch na reperensiya, na kung saan ay inikot ang puntero sa huling commit na nagawa sa branch na iyon. Ibig sabihin na ang HEAD ay maging magulang sa susunod na commit na nilikha. Sa pangkalahatan ang pinakasimple na maisip sa HEAD bilang snapshot sa iyong huling commit sa branch na iyon.

Sa katunayan, ito ay medyo madali para tingnan kung ano ang mukha ng snapshot. Narito ang isang halimbawa sa aktwal na direktoryo na listahan at SHA-1 na mga checksum para sa bawat file sa HEAD na snapshot:

$ 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

Ang Git cat-file at ls-tree na mga utos ay “plumbing” na mga utos na ginagamit para sa mas mababang antas ng mga bagay at hindi talagang ginagamit sa araw-araw na trabaho, ngunit sila ay nakakatulong sa atin upang makita kung ano ang nangyayari dito.

Ang Index

Ang Index ay ang iyong iminungkahing susunod na commit. Nag-uulat din kami sa konseptong ito bilang Git na “Staging na Lawak” dahil ito ay kung ano ang tingin ng Git ay makikita kapag ikaw ay nagpatakbo ng git commit.

Ang Git ay nagpaparami ng index na ito na may isang listahan sa lahat ng mga nilalaman ng file na huling na tingnan sa iyong tinatrabahong direktoryo at kung ano ang hitsura nila kapag sila ay orihinal na naka-checkout. Matapos mong palitan ang ilan sa mga file na iyon na may bagong mga bersyon sa kanila, at ang git commit ay nagpapalit na nasa tree para sa isang bagong commit.

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

Muli, dito kami gumagamit ng git ls-files, na kung saan ay higit pa sa isang likod ng mga eksekna na utos na nagpapakita sa iyo kung ano ang mukha ng iyong index.

Ang index ay hindi teknikal na isang istraktura ng tree — ito ay talagang ipinatutupad bilang isang silsilflattened na manipesto — ngunit para sa aming mga layunin ito ay malapit na.

Ang Tinatrabahong Direktoryo

Sa wakas, mayroon kang tinatrabahong direktoryo. Ang ibang dalawang tree ay nag-iimbak ng kanilang nilalaman sa isang mahusay ngunit hindi maginhawang paraan, sa loob ng .git na folder. Ang Tinatrabahong Direktoryo ay nag-unpack sa kanila sa aktwal na mga file, na kung saan ay gumagawa ito nang mas madali para sa iyo para i-edit sila. Isipin ang Tinatrabahong Direktoryo bilang isang sandbox, na kung saan ikaw ay maaaring sumubok ng mga pagbabago bago i-commit sila sa iyong staging na lugar (index) at pagkatapos sa kasaysayan.

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

1 directory, 3 files

Ang Workflow

Ang pangunahing layunin sa Git ay ang pagtatala ng mga snapshot sa iyong proyekto sa sunod-sunod na mas mabuting estado, sa pamamagitan ng pagmamanipula ng mga tatlong mga tree.

reset workflow

Isalarawan natin ang prosesong ito: sabihin mong pumasok ka sa isang bagong direktoryo na may isang solong file sa loob. Tatawagan natin itong v1 sa file, at ipapakita natin ito sa asul. Ngayon nagpatakbo kami ng git init, na kung saan ikaw ay lumikha ng isang Git na repositoryo na may isang HEAD na reperensiya na kung saan ang mga puntos sa isang hindi pa isinisilang na branch (ang master ay hindi umiiral pa).

reset ex1

Sa puntong ito, Tanging ang Tinatrabahong Direktoryo na tree ay merong anumang nilalaman.

Ngayon gusto nating i-commit ang file na ito, kaya gumamit kami ng git add upang kunin ang nilalaman ng Tinatrabahong Direktoryo at kopyahin ito sa Index.

reset ex2

Pagkatapos ay pinatakbo natin ang git commit, na kumukuha ng mga nilalaman sa Index at sine-save ito bilang isang permanenteng snapshot, lumilikha ng isang commit na bagay na kung saan ay nagtuturo sa snapshot na iyon, at i-update ang master upang ituro ang commit na iyon.

reset ex3

Kung magpatakbo ng git status, hindi kami makakita ng mga pagbabago, dahil lahat ng tatlong mga tree ay pareho lang.

Ngayon gusto nating gumawa ng isang pagbabago sa file na iyon at i-commit ito. Kami ay dumadaan sa parehong proseso; una ay binago natin ang file sa aming tinatrabahong direktoryo. Tawagin natin itong v2 sa file, at ipahiwatig ito sa pula.

reset ex4

Kung patakbuhin natin ang git status ngayon na, makikita natin ang file sa pula bilang “Pagbabago ay hindi naka-stage para commit,” dahil ang entry na ito ay naiiba ang Index at ang Tinatrabahong Direktoryo. Susunod pinatakbo natin ang git add dito upang i-stage ito sa ating Index.

reset ex5

Sa puntong ito, kung ipatakbo natin ang git status, makikita natin ang file sa berde sa ilalim ng “Pagbabago na i-commit” dahil ang Index at HEAD na pagkaiba — yan ay, ang aming iminungkahi sa susunod na commit na ngayon ay naiiba mula sa ating huling commit. Sa wakas, kami ay nagpatakbo ng git commit upang makumpleto ang commit.

reset ex6

Ngayon ang git status ay magbibigay sa amin nang walang output, dahil lahat ng tatlong mga tree ay pareho muli.

Paglipat ng mga branch o pag-clone ay dumadaan sa isang katulad na proseso. Kapag ikaw ay nag-checkout sa isang branch, ito ay nagbabago ng HEAD upang ituro sa bagong branch na ref, nagpapalaganap na iyong Index na gamit ang snapshot sa commit na iyon, pagkatapos ay kumukopya sa mga nilalaman sa Index sa iyong Tinatrabahong Direktoryo.

Ang Tungkilin ng Reset

Ang reset na utos ay mas makakatulong kapag tiningnan sa kontekstong ito.

Para sa mga layunin sa mga halimbawang ito, sabihin natin na binago natin ang file.txt muli at naka-commit ito sa ikatlong pagkakataon. Kaya ngayon ang aming kasaysayan ay katulad nito:

reset start

Maglakad na tayo ngayon sa pamamagitan ng eksaktong kung ano ang reset ay kapag tinawag mo ito. Ito ay direktang nagmanipula ang tatlong mga tree sa isang simple at mahuhulaan na paraan. Ginagawa ito sa tatlong pangunahing mga operasyon.

Step 1: Ilipat ang HEAD

Ang unang bagay ang reset ay gagawin ay maglipat sa kung ano ang itinuro ng HEAD. Ito ay hindi pareho bilang pagbabago sa HEAD mismo (kung saan ay anong checkout ang ginagawa); ang reset ay naglilipat sa branch na ang HEAD ay tinuturo. Nangunguhulugan ito na kung ang HEAD ay nagtakda sa master na branch (i.e. ikaw ay kasalukuyang nasa master na branch), tumatakbong git reset 9e5e6a4 ay magsisimula sa paggawa ng master na tumuturo sa 9e5e6a4.

reset soft

Hindi mahalaga kung anong porma ng reset na may isang commit na iyong tinawag, ito ang unang bagay na laging sinusubukang gawin. Sa reset --soft, ito ay hihinto lamang doon.

Ngayon ay tumagal ng isang segundo para tumingin sa diagram at magpatanto kung ano ang nangyari: ito ay talagang hindi mabubuhay ang huling git commit na utos. Kapag pinatakbo mo ang git commit, ang Git ay lumilikha ng isang bagong commit at naglilipat ng branch na ang HEAD ay tinuturo dito. Kapag ikaw ay nag reset pabalik sa HEAD~ (ang magulang sa HEAD), ikaw ay naglilipat sa branch pabalik kung saan ito nagmula, nang walang pagbabago ang Index o Tinatrabahong Direktoryo. Maaari mo na ngayong i-update ang Index at patakbuhin ang git commit ay muling tuparin ang kung ano ang git commit --amend na sana tapos na (see Pagbabago sa Huling Commit).

Step 2: Updating the Index (--mixed)

Note that if you run git status now you’ll see in green the difference between the Index and what the new HEAD is.

The next thing reset will do is to update the Index with the contents of whatever snapshot HEAD now points to.

reset mixed

If you specify the --mixed option, reset will stop at this point. This is also the default, so if you specify no option at all (just git reset HEAD~ in this case), this is where the command will stop.

Now take another second to look at that diagram and realize what happened: it still undid your last commit, but also unstaged everything. You rolled back to before you ran all your git add and git commit commands.

Step 3: Updating the Working Directory (--hard)

The third thing that reset will do is to make the Working Directory look like the Index. If you use the --hard option, it will continue to this stage.

reset hard

So let’s think about what just happened. You undid your last commit, the git add and git commit commands, and all the work you did in your working directory.

It’s important to note that this flag (--hard) is the only way to make the reset command dangerous, and one of the very few cases where Git will actually destroy data. Any other invocation of reset can be pretty easily undone, but the --hard option cannot, since it forcibly overwrites files in the Working Directory. In this particular case, we still have the v3 version of our file in a commit in our Git DB, and we could get it back by looking at our reflog, but if we had not committed it, Git still would have overwritten the file and it would be unrecoverable.

Recap

The reset command overwrites these three trees in a specific order, stopping when you tell it to:

  1. Move the branch HEAD points to (stop here if --soft)

  2. Make the Index look like HEAD (stop here unless --hard)

  3. Make the Working Directory look like the Index

Reset With a Path

That covers the behavior of reset in its basic form, but you can also provide it with a path to act upon. If you specify a path, reset will skip step 1, and limit the remainder of its actions to a specific file or set of files. This actually sort of makes sense — HEAD is just a pointer, and you can’t point to part of one commit and part of another. But the Index and Working directory can be partially updated, so reset proceeds with steps 2 and 3.

So, assume we run git reset file.txt. This form (since you did not specify a commit SHA-1 or branch, and you didn’t specify --soft or --hard) is shorthand for git reset --mixed HEAD file.txt, which will:

  1. Move the branch HEAD points to (skipped)

  2. Make the Index look like HEAD (stop here)

So it essentially just copies file.txt from HEAD to the Index.

reset path1

This has the practical effect of unstaging the file. If we look at the diagram for that command and think about what git add does, they are exact opposites.

reset path2

This is why the output of the git status command suggests that you run this to unstage a file. (See Hindi pagyuyugto ng isang Yugtong File for more on this.)

We could just as easily not let Git assume we meant “pull the data from HEAD” by specifying a specific commit to pull that file version from. We would just run something like git reset eb43bf file.txt.

reset path3

This effectively does the same thing as if we had reverted the content of the file to v1 in the Working Directory, ran git add on it, then reverted it back to v3 again (without actually going through all those steps). If we run git commit now, it will record a change that reverts that file back to v1, even though we never actually had it in our Working Directory again.

It’s also interesting to note that like git add, the reset command will accept a --patch option to unstage content on a hunk-by-hunk basis. So you can selectively unstage or revert content.

Squashing

Let’s look at how to do something interesting with this newfound power — squashing commits.

Say you have a series of commits with messages like “oops.”, “WIP” and “forgot this file”. You can use reset to quickly and easily squash them into a single commit that makes you look really smart. (Pag-squash ng mga Commit shows another way to do this, but in this example it’s simpler to use reset.)

Let’s say you have a project where the first commit has one file, the second commit added a new file and changed the first, and the third commit changed the first file again. The second commit was a work in progress and you want to squash it down.

reset squash r1

You can run git reset --soft HEAD~2 to move the HEAD branch back to an older commit (the most recent commit you want to keep):

reset squash r2

And then simply run git commit again:

reset squash r3

Now you can see that your reachable history, the history you would push, now looks like you had one commit with file-a.txt v1, then a second that both modified file-a.txt to v3 and added file-b.txt. The commit with the v2 version of the file is no longer in the history.

Check It Out

Finally, you may wonder what the difference between checkout and reset is. Like reset, checkout manipulates the three trees, and it is a bit different depending on whether you give the command a file path or not.

Without Paths

Running git checkout [branch] is pretty similar to running git reset --hard [branch] in that it updates all three trees for you to look like [branch], but there are two important differences.

First, unlike reset --hard, checkout is working-directory safe; it will check to make sure it’s not blowing away files that have changes to them. Actually, it’s a bit smarter than that — it tries to do a trivial merge in the Working Directory, so all of the files you haven’t changed will be updated. reset --hard, on the other hand, will simply replace everything across the board without checking.

The second important difference is how checkout updates HEAD. Whereas reset will move the branch that HEAD points to, checkout will move HEAD itself to point to another branch.

For instance, say we have master and develop branches which point at different commits, and we’re currently on develop (so HEAD points to it). If we run git reset master, develop itself will now point to the same commit that master does. If we instead run git checkout master, develop does not move, HEAD itself does. HEAD will now point to master.

So, in both cases we’re moving HEAD to point to commit A, but how we do so is very different. reset will move the branch HEAD points to, checkout moves HEAD itself.

reset checkout

With Paths

The other way to run checkout is with a file path, which, like reset, does not move HEAD. It is just like git reset [branch] file in that it updates the index with that file at that commit, but it also overwrites the file in the working directory. It would be exactly like git reset --hard [branch] file (if reset would let you run that) — it’s not working-directory safe, and it does not move HEAD.

Also, like git reset and git add, checkout will accept a --patch option to allow you to selectively revert file contents on a hunk-by-hunk basis.

Summary

Hopefully now you understand and feel more comfortable with the reset command, but are probably still a little confused about how exactly it differs from checkout and could not possibly remember all the rules of the different invocations.

Here’s a cheat-sheet for which commands affect which trees. The “HEAD” column reads “REF” if that command moves the reference (branch) that HEAD points to, and “HEAD” if it moves HEAD itself. Pay especial attention to the WD Safe? column — if it says NO, take a second to think before running that command.

HEAD Index Workdir WD Safe?

Commit Level

reset --soft [commit]

REF

NO

NO

YES

reset [commit]

REF

YES

NO

YES

reset --hard [commit]

REF

YES

YES

NO

checkout <commit>

HEAD

YES

YES

YES

File Level

reset [commit] <paths>

NO

YES

NO

YES

checkout [commit] <paths>

NO

YES

YES

NO