3 режима команды git reset: —soft, —mixed(по умолчанию), —hard
К моему удивлению на целом хабрахабре нет ни одного поста где бы было понятно написано про 3 вида git reset . Например, во второй по релевантности статье по запросу «git reset» автор пишет что «данное действие может быть двух видов: мягкого(soft reset) и жесткого(hard reset)». Режим —mixed , используемый по умолчанию, почему-то не удостоился упоминания.
Ничего удивительного, что часто видишь непонимание работы этой команды. Под катом коротко и ясно расскажу о всех трёх режимах git reset , после прочтения топика неясностей остаться не должно.
Сделанные изменения в репозитории по умолчанию имеют статус unstaged. Для того чтобы их закоммитить сначала вы должны добавить изменения в индекс, выполнив git add . Когда вы делаете git commit , в репозиторий будет закоммичено только то, что было в индексе.
git reset —soft
Возьмем для примера ветку:
— A — B — C (master)
HEAD указывает на C и индекс совпадает с C.
git reset --soft B
HEAD будет указывать на B и изменения из коммита C будут в индексе, как будто вы их добавили командой git add . Если вы сейчас выполните git commit вы получите коммит полностью идентичный C.
git reset —mixed (по умолчанию)
Режим —mixed используется по умолчанию, т.е. git reset —mixed = git reset
Вернемся к тем же начальным условиям:
— A — B — C (master)
git reset --mixed B
git reset B
HEAD опять же будет указывать на B, но на этот раз изменения из С не будут в индексе и если вы запустите здесь git commit ничего не произойдет т.к. ничего нет в индексе. У нас есть все изменения из С, но если запустить git status то вы увидите, что все изменения not staged. Чтобы их закоммитить нужно сначала добавить их в индекс командой git add и только после этого git commit .
git reset —hard
Те же самые начальные условия:
— A — B — C (master)
Последний режим —hard также как и —mixed переместит HEAD на В и очистит индекс, но в отличие от —mixed жесткий reset изменит файлы в вашей рабочей директории. Если выполнить
git reset --hard B
то изменения из С, равно как и незакоммиченные изменения, будут удалены и файлы в репозитории будут совпадать с B. Учитывая то, что этот режим подразумевает потерю изменений, вы всегда должны проверять git status перед тем как выполнить жесткий reset чтобы убедиться что нет незакоммиченных изменений(или они не нужны).
Сравнительную таблицу режимов git reset :
| меняет индекс | меняет файлы в рабочей директории |
нужно быть внимательным |
|
|---|---|---|---|
| reset —soft | нет | нет | нет |
| reset [—mixed] | да | нет | нет |
| reset —hard | да | да | да |
Ну и напоследок картинкой: (thx to VBauer)
What’s the difference between git reset —mixed, —soft, and —hard?
I’m looking to split a commit up and not sure which reset option to use. I was looking at the page In plain English, what does «git reset» do?, but I realized I don’t really understand what the git index or staging area is and thus the explanations didn’t help. Also, the use cases for —mixed and —soft look the same to me in that answer (when you want to fix and recommit). Can someone break it down even more? I realize —mixed is probably the option to go with, but I want to know why. Lastly, what about —hard ? Can someone give me a workflow example of how selecting the 3 options would happen?
24.4k 21 21 gold badges 161 161 silver badges 241 241 bronze badges
asked Aug 20, 2010 at 4:41
Michael Chinen Michael Chinen
17.9k 5 5 gold badges 33 33 silver badges 45 45 bronze badges
@mkarasek answer is pretty good but one may be interested in taking a look at this question, too.
Apr 5, 2013 at 14:56
Note to self: In general, soft: stage everything , mixed: unstage everything , hard: ignore everything up to the commit I’m resetting from.
Oct 22, 2016 at 10:37
Sep 21, 2018 at 20:02
Mar 6, 2020 at 16:42
For those who’d like to see a concrete example with animated visuals, I’ve put together an explanation of git reset here.
Sep 27, 2022 at 2:24
18 Answers 18
When you modify a file in your repository, the change is initially unstaged. In order to commit it, you must stage it—that is, add it to the index—using git add . When you make a commit, the changes that are committed are those that have been added to the index.
git reset changes, at minimum, where the current branch ( HEAD ) is pointing. The difference between —mixed and —soft is whether or not your index is also modified. So, if we’re on branch master with this series of commits:
- A - B - C (master)
HEAD points to C and the index matches C .
When we run git reset —soft B , master (and thus HEAD ) now points to B , but the index still has the changes from C ; git status will show them as staged. So if we run git commit at this point, we’ll get a new commit with the same changes as C .
Okay, so starting from here again:
- A - B - C (master)
Now let’s do git reset —mixed B . (Note: —mixed is the default option). Once again, master and HEAD point to B, but this time the index is also modified to match B . If we run git commit at this point, nothing will happen since the index matches HEAD . We still have the changes in the working directory, but since they’re not in the index, git status shows them as unstaged. To commit them, you would git add and then commit as usual.
And finally, —hard is the same as —mixed (it changes your HEAD and index), except that —hard also modifies your working directory. If we’re at C and run git reset —hard B , then the changes added in C , as well as any uncommitted changes you have, will be removed, and the files in your working copy will match commit B . Since you can permanently lose changes this way, you should always run git status before doing a hard reset to make sure your working directory is clean or that you’re okay with losing your uncommitted changes.

And finally, a visualization:
8,610 5 5 gold badges 34 34 silver badges 54 54 bronze badges
answered Aug 20, 2010 at 5:53
22.5k 2 2 gold badges 21 21 silver badges 10 10 bronze badges
In other words, —soft is discarding last commit, —mix is discarding last commit and add, —hard is discarding last commit,add and any changes you made on the codes which is the same with git checkout HEAD
Mar 28, 2014 at 22:25
@eventualEntropy You can recover any committed changes with the reflog; uncommitted changes that are removed with reset —hard are gone forever.
Apr 14, 2014 at 16:36
What happens if I have local modifications in my working tree before the reset —mixed ? Will it overwrite my local changes or will it merge them?
Apr 17, 2014 at 16:20
@Robert Neither; —mixed changes your index but not your working directory, so any local modifications are unaffected.
Apr 18, 2014 at 15:18
May be helpful for visual people who use git on terminal with colour: 1.’git reset —soft A’ and you will see B and C’s stuff in green (staged) 2.’git reset —mixed A’ and you will see B and C’s stuff in red (unstaged) 3.’git reset —hard A’ and you will no longer see B and C’s changes anywhere (will be as if they never existed)
Nov 20, 2014 at 15:32
In the simplest terms:
- —soft : uncommit changes, changes are left staged (index).
- —mixed(default): uncommit + unstage changes, changes are left in working tree.
- —hard : uncommit + unstage + delete changes, nothing left.
12.1k 6 6 gold badges 51 51 silver badges 57 57 bronze badges
answered Apr 25, 2018 at 12:30
6,841 1 1 gold badge 9 9 silver badges 11 11 bronze badges
best answer because the answer uses technical terms to provide a complete answer that is also the most concise
Jul 9, 2018 at 14:52
@Nikhil Perhaps what you mean is that the original commit still exists, which is true. But the branch has been changed so that the commit is no longer part of the branch. Do we agree on that?
Oct 9, 2018 at 8:35
For beginnners: The answer is neither wrong nor true. It implies that resets are only made to previous commits. However, you can reset to any commit in the tree anywhere. You have to understand that reset moves the HEAD and the associated branch pointer and does not actually modify the tree of commits (as implied by «uncommit»). However, git will, for reasons of efficiency, remove commits after a while (default 90 days) when they are unreachable, i.e. not in the history of any branch. Think about what this implies if you want to go exploring. (also git prune and gc may delete commits)
Dec 30, 2018 at 21:51
Does «uncommit» mean «move the HEAD»? This answer makes it sounds as if the previous commit was deleted, which I don’t believe is the case at all. Plus, you can use RESET to pull changes from the current HEAD, which doesn’t uncommit anything.
May 30, 2019 at 6:50
This is the only readable answer. It’s accurate: you can’t improve it in any way that helps my everyday work. I don’t care about implementation trivia.
Jun 3, 2019 at 23:39
Three types of regret
A lot of the existing answers don’t seem to answer the actual question. They are about what the commands do, not about what you (the user) want — the use case. But that is what the OP asked about!
It might be more helpful to couch the description in terms of what it is precisely that you regret at the time you give a git reset command. Let’s say we have this:
A - B - C - D
Here are some possible regrets and what to do about them:
1. I regret that B, C, and D are not one commit.
git reset --soft A . I can now immediately commit and presto, all the changes since A are one commit.
2. I regret that B, C, and D are not two commits (or ten commits, or whatever).
git reset --mixed A . The commits are gone and the index is back at A, but the work area still looks as it did after D. So now I can add-and-commit in a whole different grouping.
3. I regret that B, C, and D happened on this branch; I wish I had branched after A and they had happened on that other branch.
Make a new branch otherbranch , and then git reset --hard A . The current branch now ends at A, with otherbranch stemming from it and containing B, C, and D.
(Of course you could also use a hard reset because you wish B, C, and D had never happened at all.)
answered Jan 10, 2020 at 3:33
518k 87 87 gold badges 880 880 silver badges 1152 1152 bronze badges
In regret 3, you could have used a soft reset instead of a hard one, right? When checking out the new branch, both the index and the working directory would match commit D. Correct me if I’m wrong. By the way, if we do a mixed reset, then after checking out the new branch we would have to add the working directory to the index and then both the index and working directory would match commit D. Right?
Nov 2, 2021 at 0:37
@PedroMachado I don't see it that way at all, sorry.
Nov 2, 2021 at 1:53
You honestly should consider a career in technical teaching. This was an excellent way to frame/teach the "I f*cked up, now what do" scenario most people are in when searching for this answer. Props friend.
Sep 15, 2022 at 0:19
@elbowlobstercowstand That's in fact the career I did have.
Sep 15, 2022 at 10:45
This is great. As said, the OP wanted use cases, and here they are.
Oct 11, 2022 at 20:59
Please be aware, this is a simplified explanation intended as a first step in seeking to understand this complex functionality.
May be helpful for visual learners who want to visualise what their project state looks like after each of these commands:
Given: - A - B - C (master)
For those who use Terminal with colour turned on (git config --global color.ui auto):
git reset --soft A and you will see B and C's stuff in green (staged and ready to commit)
git reset --mixed A (or git reset A ) and you will see B and C's stuff in red (unstaged and ready to be staged (green) and then committed)
git reset --hard A and you will no longer see B and C's changes anywhere (will be as if they never existed)
Or for those who use a GUI program like 'Tower' or 'SourceTree'
git reset --soft A and you will see B and C's stuff in the 'staged files' area ready to commit
git reset --mixed A (or git reset A ) and you will see B and C's stuff in the 'unstaged files' area ready to be moved to staged and then committed
git reset --hard A and you will no longer see B and C's changes anywhere (will be as if they never existed)
63.4k 36 36 gold badges 271 271 silver badges 389 389 bronze badges
answered Nov 21, 2014 at 12:06
7,233 8 8 gold badges 48 48 silver badges 67 67 bronze badges
This is misleading, at best: your answer reads as if git reset only changes the look of git status 's output.
Nov 21, 2014 at 12:18
I see your point, but disagree because as a visual learner, seeing how my project 'looked' after using the 3 commands finally helped me understand what they were doing!
Nov 21, 2014 at 12:20
I saw it more of a 'git for dummies' kind of idea to help people ease in to what is actually happening. Can you think of how it could be improved so as not to be misleading
Jan 14, 2015 at 17:22
No, we don't need to change this answer. It provides a handy "cheat sheet". Think about it: soft=green, mixed=red, hard=nothing(means gone)! How easy to remember! For those newbies who don't even understand what those color really mean, they know too little about git, and they are going to take hard lessons down the road anyway, and that is NOT @unegma 's fault! BTW, I just upvote this answer to counteract that previous downvote. Good job, @unegma!
Feb 11, 2015 at 2:34
This served as a great supplemental summary to better understand the inner workings as I read them elsewhere. Thank you!
Mar 2, 2015 at 15:51
All the other answers are great, but I find it best to understand them by breaking down files into three categories: unstaged , staged , commit :
- --hard should be easy to understand, it restores everything
- --mixed (default) :
- unstaged files: don't change
- staged files: move to unstaged
- commit files: move to unstaged
- --soft :
- unstaged files: don't change
- staged files: dont' change
- commit files: move to staged
- --soft option will move everything (except unstaged files) into staging area
- --mixed option will move everything into unstaged area
answered Jun 14, 2019 at 23:57
1,028 1 1 gold badge 10 10 silver badges 17 17 bronze badges
In these cases I like a visual that can hopefully explain this:
git reset --[hard/mixed/soft] :

So each one affects different scopes:
- Hard => WorkingDir + Index + HEAD
- Mixed => Index + HEAD
- Soft => HEAD only (index and working dir unchanged).
89 1 1 silver badge 9 9 bronze badges
answered Sep 13, 2018 at 16:39
Tomer Ben David Tomer Ben David
8,356 1 1 gold badge 44 44 silver badges 24 24 bronze badges
You don't have to force yourself to remember differences between them. Think of how you actually made a commit.
- Make some changes.
- git add .
- git commit -m "I did Something"
Soft, Mixed and Hard is the way enabling you to give up the operations you did from 3 to 1.
- Soft "pretended" to never see you have did git commit .
- Mixed "pretended" to never see you have did git add .
- Hard "pretended" to never see you have made file changes.
5,061 3 3 gold badges 34 34 silver badges 50 50 bronze badges
answered Dec 14, 2018 at 13:10
magentaqin magentaqin
1,949 14 14 silver badges 10 10 bronze badges
Here is a basic explanation for TortoiseGit users:
git reset --soft and --mixed leave your files untouched.
git reset --hard actually change your files to match the commit you reset to.
In TortoiseGit, The concept of the index is very hidden by the GUI. When you modify a file, you don't have to run git add to add the change to the staging area/index. When simply dealing with modifications to existing files that are not changing file names, git reset --soft and --mixed are the same! You will only notice a difference if you added new files or renamed files. In this case, if you run git reset --mixed, you will have to re-add your file(s) from the Not Versioned Files list.
answered Oct 28, 2014 at 20:38
James Lawruk James Lawruk
30.3k 19 19 gold badges 130 130 silver badges 137 137 bronze badges
This answer is very unclear re the difference between soft and mixed. and is even dismissive in stating it. This following answer is more clear on that. stackoverflow.com/questions/2530060/…
Sep 4, 2016 at 2:44
As a user of Github Desktop which also has the same behaviour, this answer gives me some clarity of why I keep confused about --mixed and --soft .
May 9, 2017 at 4:09
mkarasek's Answer is great, in simple terms we can say.
- git reset --soft : set the HEAD to the intended commit but keep your changes staged from last commits
- git reset --mixed : it's same as git reset --soft but the only difference is it un stage your changes from last commits
- git reset --hard : set your HEAD on the commit you specify and reset all your changes from last commits including un committed changes.
--soft and --mixed are a bit similar, the only difference is, if you want to keep your changes in staging area use --soft , and if you don't want your changes in staging area use --mixed instead.
answered Jun 22, 2018 at 11:16
Vivek Maru Vivek Maru
8,387 1 1 gold badge 24 24 silver badges 34 34 bronze badges
Before going into these three option one must understand 3 things.
3) Working directory
reset --soft : History changed, HEAD changed, Working directory is not changed.
reset --mixed : History changed, HEAD changed, Working directory changed with unstaged data.
reset --hard : History changed, HEAD changed, Working directory is changed with lost data.
It is always safe to go with Git --soft. One should use other option in complex requirement.
answered Nov 14, 2017 at 11:32
Suresh Sharma Suresh Sharma
1,836 22 22 silver badges 41 41 bronze badges
There are a number of answers here with a misconception about git reset --soft . While there is a specific condition in which git reset --soft will only change HEAD (starting from a detached head state), typically (and for the intended use), it moves the branch reference you currently have checked out. Of course it can't do this if you don't have a branch checked out (hence the specific condition where git reset --soft will only change HEAD ).
I've found this to be the best way to think about git reset . You're not just moving HEAD (everything does that), you're also moving the branch ref, e.g., master . This is similar to what happens when you run git commit (the current branch moves along with HEAD ), except instead of creating (and moving to) a new commit, you move to a prior commit.
This is the point of reset , changing a branch to something other than a new commit, not changing HEAD . You can see this in the documentation example:
Undo a commit, making it a topic branch
$ git branch topic/wip (1) $ git reset --hard HEAD~3 (2) $ git checkout topic/wip (3)
- You have made some commits, but realize they were premature to be in the "master" branch. You want to continue polishing them in a topic branch, so create "topic/wip" branch off of the current HEAD.
- Rewind the master branch to get rid of those three commits.
- Switch to "topic/wip" branch and keep working.
What's the point of this series of commands? You want to move a branch, here master , so while you have master checked out, you run git reset .
The top voted answer here is generally good, but I thought I'd add this to correct the several answers with misconceptions.
Change your branch
git reset --soft : resets the branch pointer for the currently checked out branch to the commit at the specified reference, . Files in your working directory and index are not changed. Committing from this stage will take you right back to where you were before the git reset command.
Change your index too
git reset --mixed
Does what --soft does AND also resets the index to the match the commit at the specified reference. While git reset --soft HEAD does nothing (because it says move the checked out branch to the checked out branch), git reset --mixed HEAD , or equivalently git reset HEAD , is a common and useful command because it resets the index to the state of your last commit.
Change your working directory too
git reset --hard : does what --mixed does AND also overwrites your working directory. This command is similar to git checkout , except that (and this is the crucial point about reset ) all forms of git reset move the branch ref HEAD is pointing to.
A note about "such and such command moves the HEAD":
It is not useful to say a command moves the HEAD . Any command that changes where you are in your commit history moves the HEAD . That's what the HEAD is, a pointer to wherever you are. HEAD is you, and so will move whenever you do.
answered Feb 28, 2019 at 21:55
7,240 1 1 gold badge 23 23 silver badges 39 39 bronze badges
"moving the branch ref": good point. I had to update stackoverflow.com/a/5203843/6309.
Mar 1, 2019 at 5:28
Perhaps change “move the branch ref HEAD is pointing to” to “move the branch ref (away?) from where HEAD is currently pointing to”? Am I understanding it correctly?
Nov 2, 2021 at 0:57
@PedroMachado nope. You move the branch ref that HEAD is pointing to, and you go along with it, so HEAD goes along with it. See stackoverflow.com/a/54935492/7936744
Jan 19, 2022 at 20:12
This should have more votes: it gives a use case with the master and WIP branches which, I'm guessing, really gets at the heart of things for the OP and others just coming to learn about Git.
Oct 11, 2022 at 20:56
--mixed vs --soft vs --hard:
--mixed: Delete changes from the local repository and staging area. It won't touch the working directory. Possible to revert back changes by using the following commands. - git add - git commit Working tree won't be clean. --soft: Deleted changes only from the local repository. It won't touch the staging area and working directory. Possible to revert back changes by using the following command. - git commit. Working tree won't be clean --hard: Deleted changes from everywhere. Not possible to revert changes. The working tree will be clean.
NOTE: If the commits are confirmed to the local repository and to discard those commits we can use:
`git reset command`.
But if the commits are confirmed to the remote repository then not recommended to use the reset command and we have to use the revert command to discard the remote commits.
answered Nov 26, 2020 at 16:25
258 3 3 silver badges 11 11 bronze badges
you can revert git reset --hard by a git reflog
Aug 21, 2021 at 0:15
A short answer in what context the 3 options are used:
To keep the current changes in the code but to rewrite the commit history:
- soft : You can commit everything at once and create a new commit with a new description (if you use torotise git or any most other GUIs, this is the one to use, as you can still tick which files you want in the commit and make multiple commits that way with different files. In Sourcetree all files would be staged for commit.)
- mixed : You will have to add the individual files again to the index before you make commits (in Sourcetree all the changed files would be unstaged)
To actually lose your changes in the code as well:
- hard : you don't just rewrite history but also lose all your changes up to the point you reset
answered Jul 14, 2017 at 9:20
6,233 16 16 gold badges 66 66 silver badges 117 117 bronze badges
I dont get soft and mixed in this case. If you have to commit, then what was reverted? are you commiting the revert, or recommiting the changes (so getting back to the original state?)
Oct 23, 2017 at 19:42
Recommitting the changes. There will be no reverse commit.
Oct 23, 2017 at 19:48
@JohnLittle: The only difference between soft and mixed resets is that soft doesn’t change the index but mixed makes the index match the target commit (the one referred to in the reset command). Both soft and mixed (and hard) change the branch and HEAD pointers to point to the target commit. A hard reset, besides changing the two pointers and changing the index to match the target commit (like mixed does), also changes the working directory to match the target commit.
Oct 16, 2022 at 14:05
Basic difference between various options of git reset command are as below.
- --soft: Only resets the HEAD to the commit you select. Works basically the same as git checkout but does not create a detached head state.
- --mixed (default option): Resets the HEAD to the commit you select in both the history and undoes the changes in the index.
- --hard: Resets the HEAD to the commit you select in both the history, undoes the changes in the index, and undoes the changes in your working directory.
answered May 21, 2018 at 4:27
Vishwas Abhyankar Vishwas Abhyankar
396 3 3 silver badges 7 7 bronze badges
--soft : Tells Git to reset HEAD to another commit, so index and the working directory will not be altered in any way. All of the files changed between the original HEAD and the commit will be staged.
--mixed : Just like the soft, this will reset HEAD to another commit. It will also reset the index to match it while working directory will not be touched. All the changes will stay in the working directory and appear as modified, but not staged.
--hard : This resets everything - it resets HEAD back to another commit, resets the index to match it, and resets the working directory to match it as well.
The main difference between --mixed and --soft is whether or not your index is also modified. Check more about this here.
answered May 29, 2018 at 10:18
Nesha Zoric Nesha Zoric
6,296 43 43 silver badges 34 34 bronze badges
Good answer. It’s only missing to mention the change in the branch pointer.
Oct 16, 2022 at 14:09
- All types of reset change the HEAD in the repo. Additionally.
- git reset --soft moves the changes from commits removed from repo into the index, merging in any that were there already.
- git reset --hard loses the changes in the working tree and the index.
It is the only one to change the working tree.

answered May 27, 2022 at 9:49
artfulrobot artfulrobot
20.7k 11 11 gold badges 56 56 silver badges 81 81 bronze badges
Mo Ali has put it in simplest terms and here's another simple explanation:
--soft : reset HEAD pointer to previous commit
--mixed : --soft + delete add ed changes
--hard : --mixed + recover working tree file changes (CAREFUL!)
answered Nov 28, 2021 at 12:32
10.2k 18 18 gold badges 68 68 silver badges 97 97 bronze badges
I’m not a git expert and just arrived on this forum to understand it! Thus maybe my explanation is not perfect, sorry for that. I found all the other answer helpful and I will just try to give another perspective. I will modify a bit the question since I guess that it was maybe the intent of the author: “I’m new to git. Before using git, I was renaming my files like this: main.c, main_1.c, main_2.c when i was performing majors changes in order to be able to go back in case of trouble. Thus, if I decided to come back to main_1.c, it was easy and I also keep main_2.c and main_3.c since I could also need them later. How can I easily do the same thing using git?” For my answer, I mainly use the “regret number three” of the great answer of Matt above because I also think that the initial question is about “what do I do if I have regret when using git?”. At the beginning, the situation is like that:
- The first main point is to create a new branch: git branch mynewbranch. Then one get:
A-B-C-D (master and mynewbranch)
- Let’s suppose now that one want to come back to A (3 commits before). The second main point is to use the command git reset --hard even if one can read on the net that it is dangerous. Yes, it’s dangerous but only for uncommitted changes. Thus, the way to do is:
Git reset --hard thenumberofthecommitA
Git reset --hard master~3
Then one obtains: A (master) – B – C – D (mynewbranch)
Then, it’s possible to continue working and commit from A (master) but still can get an easy access to the other versions by checking out on the other branch: git checkout mynewbranch. Now, let’s imagine that one forgot to create a new branch before the command git reset --hard. Is the commit B, C, D are lost? No, but there are not stored in any branches. To find them again, one may use the command : git reflog that is consider as “a safety command”( “in case of trouble, keep calm and use git reflog”). This command will list all commits even those that not belong to any branches. Thus, it’s a convenient way to find the commit B, C or D.
Доходчивое объяснение Git Reset
Перевод статьи «Git Reset Explained – How to Save the Day with the Reset Command».

«Помогите! Я закоммитил не в ту ветку!» «Ну вот, опять… Где мой коммит?» Знакомые ситуации, правда?
Я такое слышал неоднократно. Кто-то окликает меня по имени и просит помочь, когда у него что-то пошло не так с git. И такое происходило не только когда я учил студентов, но также и в работе с опытными разработчиками.
Со временем я стал кем-то вроде «того парня, который разбирается в Git».
Мы используем git постоянно, и обычно он помогает нам в работе. Но порой (и куда чаще, чем нам хотелось бы!) что-то идет не так.
Бывает, мы отправляем коммит не в ту ветку. Бывает, теряем часть написанного кода. А можем и добавить в коммит что-то лишнее.

По git есть много онлайн-ресурсов, и часть из них (например, вот эта статья) фокусируется на том, что делать в таких вот нежелательных ситуациях.
Но мне всегда казалось, что в этих ресурсах не хватает объяснений, почему нужно делать так, а не иначе. Когда приводится набор команд, что делает каждая из них? И вообще, как вы пришли к этим командам?
В прошлом посте я рассказывал о внутреннем устройстве Git. И хотя понимать его полезно, читая теория практически всегда недостаточна. Как применить свои знания внутреннего устройства git и использовать их для решения возникающих проблем?
В этом посте я хотел бы построить мост между теорией и практикой и рассказать о команде git reset . Мы разберем, что делает эта команда, что происходит за кулисами, а также применим эти знания в различных сценариях.
Исходные условия — рабочая директория, индекс и репозиторий
Чтобы разобраться во внутренних механизмах git reset , важно понимать процесс записи изменений внутри git. В частности, я имею в виду записи в рабочей директории, индексе и репозитории.
Если вы хорошо ориентируетесь в этой теме, переходите к следующему разделу. Если же вам нужно более глубокое пояснение, почитайте мой предыдущий пост.
Когда мы работаем над кодом своего проекта, мы делаем это в рабочей директории. Ею может быть любая директория в нашей файловой системе, имеющая привязанный к ней репозиторий. В ней хранятся папки и файлы нашего проекта, а также директория под названием .git.
После того как мы внесли какие-то изменения, мы хотим отправить их в репозиторий. Репозиторий это набор коммитов, каждый из которых представляет собой архив того, как выглядело рабочее дерево проекта на момент создания этого архива (на нашей машине или на чьей-то еще).

Давайте создадим в рабочей директории какой-нибудь файл и запустим команду git status :

Да, git не записал (не закоммитил) изменения, сделанные в рабочей директории, напрямую в репозиторий.
Вместо этого изменения сначала регистрируются в индексе (или в стейджинге). Оба эти термина означают одно и то же, и оба часто используются в документации git. В этой статье мы тоже будем пользоваться обоими, так как они полностью взаимозаменяемы.
Когда мы применяем git add , мы добавляем файлы (или изменения внутри файлов) в стейджинг. Давайте попробуем использовать эту команду для только что созданного файла:

Как показывает git status , наш файл теперь в стейджинге и готов к коммиту. Да, он еще не является частью никакого коммита. Другими словами, сейчас он находится в рабочей директории, а также в индексе, но не в репозитории.

Если мы теперь выполним git commit , мы создадим коммит на основе состояния индекса. Таким образом новый коммит (в примере — commit 3) будет включать файл, который мы чуть ранее добавили в стейджинг.

Рабочая директория находится в точно таком же состоянии, как индекс и репозиторий.
При выполнении git commit текущая ветка master начинает указывать на только что созданный объект commit.

Внутренняя работа git reset
Мне нравится представлять git reset как команду, которая поворачивает вспять описанный выше процесс (внесение изменений в рабочей директории, добавление их в индекс, а затем сохранение в репозиторий).
У git reset есть три режима: --soft , --mixed и --hard . Я рассматриваю их как три стадии:
- Стадия 1. Обновление HEAD — git reset --soft
- Стадия 2. Обновление индекса — git reset --mixed
- Стадия 3. Обновление рабочей директории — git reset --hard
Стадия 1. Обновление HEAD — git reset —soft
Прежде всего, git reset меняет то, на что указывает HEAD. Если мы выполним git reset --hard HEAD~1 , HEAD будет указывать не на master, а на HEAD~1. Если использовать флаг --soft , git reset на этом и остановится.
Если вернуться к нашему примеру, HEAD будет указывать на commit 2, и таким образом new_file.txt не будет частью дерева текущего коммита. Но он будет частью индекса и рабочей директории.

Если посмотреть git status , мы увидим, что этот файл определенно в стейджинге, но не закоммичен.

Иными словами, мы вернули процесс на стадию, где мы уже применили git add , но еще не применяли git commit .
Стадия 2. Обновление индекса — git reset —mixed
Если мы используем git reset --mixed HEAD~1 , git не остановится на обновлении того, на что указывает HEAD. Помимо этого обновится еще и индекс (до состояния уже обновленного HEAD).
В нашем примере это значит, что индекс будет в том же виде, что и commit 2:

Таким образом мы вернули процесс на стадию до выполнения команды git add . Новосозданный файл является частью рабочей директории, но не индекса и не репозитория.

Стадия 3. Обновление рабочей директории — git reset —hard
Если использовать git reset -- hard HEAD~1 , то после перевода указателя HEAD (на что бы он ни указывал раньше) на HEAD~1, а также обновления индекса до (уже обновленного) HEAD, git пойдет еще дальше и обновит рабочую директорию до состояния индекса.
Применительно к нашему примеру это означает, что рабочая директория будет приведена к состоянию индекса, который уже приведен в состояние commit 2:

Собственно, мы вернули весь процесс на этап до создания файла my_file.txt.
Применяем наши знания в реальных сценариях
Теперь, когда мы разобрались с тем, как работает git reset , давайте применим эти знания, чтобы спасти какую-нибудь ситуацию!
1. Упс! Я закоммитил что-то по ошибке
Рассмотрим следующий сценарий. Мы создали файл со строкой «This is very importnt», отправили его в стейджинг, а после — в коммит.

А затем — ой! — обнаружили, что в предложении у нас опечатка.
Ну, теперь-то мы знаем, что это можно легко исправить. Мы можем отменить наш последний коммит и вернуть файл в рабочую директорию, используя git reset --mixed HEAD~1 . Теперь моно отредактировать содержимое файла и сделать коммит еще раз.
Совет. В данном конкретном случае мы также можем использовать git commit --amend , как описано здесь.
2. Упс! Я сделал коммит не в ту ветку, а эти изменения мне нужны в новой ветке
Со всеми нами такое случалось. Сделал что-то, закоммитил…

О нет, мы сделали коммит в ветку master, а надо было создать новую и затем сделать пул-реквест.
Я считаю, что здесь будет полезно визуализировать наше положение и то положение, в котором мы хотели бы оказаться.

Собственно, от желаемого состояния нас отделяют три изменения.
- Ветка new должна указывать на наш недавно добавленный коммит.
- Ветка master должна указывать на предыдущий коммит.
- HEAD должен указывать на new.
Мы можем достичь желаемого положения в три шага:
Во-первых, нужно сделать так, чтобы ветка new указывала на недавно добавленный коммит. Достичь этого можно при помощи команды git branch new . Таким образом мы достигаем следующего состояния:

Во-вторых, нужно сделать так, чтобы master указывала на предыдущий коммит (иными словами, на HEAD~1). Достичь этого можно при помощи команды git reset --hard HEAD~1 . Таким образом мы достигаем следующего состояния:

Наконец, мы хотели бы оказаться в ветке new, т. е. сделать так, чтобы HEAD указывал на new . Это легко достижимо путем выполнения команды git checkout new .
- git branch new
- git reset --hard HEAD~1
- git checkout new
3. Упс! Я отправил коммит не в ту ветку, а он мне нужен в другой (уже существующей) ветке
В этом случае мы проходим те же шаги, что и в предыдущем сценарии. Мы проделали какую-то работу и закоммитили изменения…

О нет, мы отправили коммит в ветку master , а нужно было отправить в совсем другую.
Давайте снова изобразим текущее и желаемое положение:

У нас опять же есть три отличия.
Нам нужно, чтобы самый последний коммит оказался в ветке existing. Поскольку в настоящее время на этот коммит указывает master , мы можем попросить git взять последний коммит из ветки master и применить его к ветке existing :
- git checkout existing — переключение на ветку existing ,
- git cherry-pick master — применение последнего коммита в ветке master к текущей ветке ( existing ).
Теперь наше положение следующее:

Все, что нам нужно, это сделать так, чтобы master указывала на предыдущий коммит, а не на самый последний. Для этого:
- git checkout master — смена активной ветки на master ,
- git reset --hard HEAD~1 — теперь мы вернулись к изначальному состоянию этой ветки.
Таким образом мы достигли желаемого положения:

Итоги
В этой статье мы изучили, как работает git reset , а также разобрали три разных режима этой команды: --soft , --mixed и --hard .
Также мы применили свои новые знания для решения жизненных задач.
Понимание работы git позволяет уверенно действовать в любых ситуациях, а также наслаждаться красотой этого инструмента.
Как отменить git reset hard
Прежде всего надо понять что такое гит и что делает git reset --hard . Гит это набор ссылок, где каждая хранит изменения файлов. Команда git reset перемещает указатель на выбранную ссылку, а флаг --hard еще и обновляет все файлы в соотвествии с ссылкой. Отсюда решение на первый взгляд парадоксальное -- чтобы отменить git reset --hard нужно сделать git reset --hard на отмененную ссылку.
echo 'foobaz' > 1.txt git add . git commit -m 'add 1.txt' # [main (root-commit) dd64acb] add 1.txt # 1 file changed, 1 insertion(+) # create mode 100644 1.txt echo 'hellowordl' > 2.txt git add . git commit -m 'add 2.txt' # [main c626c6c] add 2.txt # 1 file changed, 1 insertion(+) # create mode 100644 2.txt git log # c626c6c (HEAD -> main) add 2.txt # dd64acb add 1.txt git reset --hard HEAD^1 # HEAD is now at dd64acb add 1.txt cat 2.txt # cat: 2.txt: No such file or directory # команда git reflog позволяет посмотреть всю историю коммитов и найти хеш нужного нам коммита git reflog # dd64acb HEAD@: reset: moving to HEAD^1 # c626c6c (HEAD -> main) HEAD@: commit: add 2.txt # dd64acb HEAD@: commit (initial): add 1.txt git reset --hard c626c6c # HEAD is now at c626c6c add 2.txt cat 2.txt # hellowordl