What's the difference between git reset --mixed, --soft, and --hard?
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.
总结:
假设有A<--B<--C 三个commit,当前分支master指向C【先确保本地是clean的状态】
执行git reset --soft B,本地文件变成修改状态,并且是B到C的变化。
执行 git reset --mixed B,master指向B,并且文件work directory的文件内容会切换到B所在的状态
想到了关于soft的使用,如果在回滚之后,进行commit。其实就相当于一次squash
Practical uses of git reset --soft?
git reset
is all about moving HEAD
, and generally the branch ref.
Question: what about the working tree and index?
When employed with --soft
, moves HEAD
, most often updating the branch ref, and only the HEAD
.
This differ from commit --amend
as:
- it doesn't create a new commit.
- it can actually move HEAD to any commit (as
commit --amend
is only about not moving HEAD, while allowing to redo the current commit)
Just found this example of combining:
- a classic merge
- a subtree merge
all into one (octopus, since there is more than two branches merged) commit merge.
Tomas "wereHamster" Carnecky explains in his "Subtree Octopus merge" article:
- The subtree merge strategy can be used if you want to merge one project into a subdirectory of another project, and the subsequently keep the subproject up to date. It is an alternative to git submodules.
- The octopus merge strategy can be used to merge three or more branches. The normal strategy can merge only two branches and if you try to merge more than that, git automatically falls back to the octopus strategy.
The problem is that you can choose only one strategy. But I wanted to combine the two in order to get a clean history in which the whole repository is atomically updated to a new version.
I have a superproject, let's call it
projectA
, and a subproject,projectB
, that I merged into a subdirectory ofprojectA
.
(that's the subtree merge part)
I'm also maintaining a few local commits.
ProjectA
is regularly updated,projectB
has a new version every couple days or weeks and usually depends on a particular version ofprojectA
.When I decide to update both projects, I don't simply pull from
projectA
andprojectB
as that would create two commits for what should be an atomic update of the whole project.
Instead, I create a single merge commit which combinesprojectA
,projectB
and my local commits.
The tricky part here is that this is an octopus merge (three heads), butprojectB
needs to be merged with the subtree strategy. So this is what I do:
# Merge projectA with the default strategy:
git merge projectA/master
# Merge projectB with the subtree strategy:
git merge -s subtree projectB/master
Here the author used a reset --hard
, and then read-tree
to restore what the first two merges had done to the working tree and index, but that is where reset --soft
can help:
How to I redo those two merges, which have worked, i.e. my working tree and index are fine, but without having to record those two commits?
# Move the HEAD, and just the HEAD, two commits back!
git reset --soft HEAD@{2}
Now, we can resume Tomas's solution:
# Pretend that we just did an octopus merge with three heads:
echo $(git rev-parse projectA/master) > .git/MERGE_HEAD
echo $(git rev-parse projectB/master) >> .git/MERGE_HEAD
# And finally do the commit:
git commit
So, each time:
- you are satisfied with what you end up with (in term of working tree and index)
- you are not satisfied with all the commits that took you to get there:
git reset --soft
is the answer.