Working in a repository
This part has a small setup stage. Run 03_setup.sh if you want to follow along.
In this part we will learn more advanced staging techniques, how to amend a commit and what the reflog is and how it can be used. Further, we will look on how to stash changed files, look at the changes a commit will introduce and we will also looking at resetting the HEAD, the index and the working directory.
First steps
Before we start to work, we will first take a look at the current status and history.
The history
This time we use git log
with the --oneline
option to make it concise.
$ git log --oneline
cf11082 add secret file
324194a inital commit
We see two commits. What happened in this commits?
git show
without arguments will show the last commit, while
git show HEAD^
will show the commit before that.
$ git show HEAD^
commit 324194a4ad5de579f7552eb8c5f509afd9616540
Author: setup <setup@maschmi.net>
Date: Thu Jul 17 00:18:29 2025 +0200
inital commit
diff --git a/partially_stage.txt b/partially_stage.txt
new file mode 100644
index 0000000..2447d81
--- /dev/null
+++ b/partially_stage.txt
@@ -0,0 +1,2 @@
+this file is already tracked
+keep this line deleted
$ git show
commit cf110821a87c5ae8592ffc5bab55e3885373f59d
Author: setup <setup@maschmi.net>
Date: Thu Jul 17 00:18:29 2025 +0200
add secret file
diff --git a/file.secret b/file.secret
new file mode 100644
index 0000000..c197c0d
--- /dev/null
+++ b/file.secret
@@ -0,0 +1 @@
+never ever track me!
We see a file named partially_stage.txt
was added in the
initial commit. Wehreas a file which should never have been added, was
added in the second commit.
The current status
Well, we had seen four files in the working directory. But only two files were committed.
$ git status --short
M partially_stage.txt
?? put_into_commit_3.txt
?? stage_me.txt
We can see the file partially_stage.txt
was modified.
The other two files are not yet tracked by git.
Untracking a file
There is a file called file.secret
, it is not marked a
untracked. And it should never have been tracked by git. How can we
untrack this file? The safest way is to use git rm
.
However, this will also delete the file from your working tree. If you
need the file again, create a backup first, delete it, then restore the
file.
$ cp file.secret file.secret.bak
$ git rm file.secret
rm 'file.secret'
$ git status --short
D file.secret
M partially_stage.txt
?? file.secret.bak
?? put_into_commit_3.txt
?? stage_me.txt
$ git commit -m 'remove secret file'
[main 683751c] remove secret file
1 file changed, 1 deletion(-)
delete mode 100644 file.secret
$ mv file.secret.bak file.secret
There is a way to set ASSUME_UNCHANGED and UNTRACKED flags with the
git update-index
command, but, according to the man page,
git may still use the file in certain operations.
Be aware, we still can re-create the file, when we check out the original commit!
Ignoring files
What a tedious way to untrack files. Wouldn't it be better if we
never had added them by mistake? Yes there is, the
.gitignore
file. If a file or directory matches the pattern
in there, it will not be added, until we supply the -f
switch.
$ echo '*.secret' > .gitignore
$ git add .gitignore
$ git status --short
A .gitignore
M partially_stage.txt
?? put_into_commit_3.txt
?? stage_me.txt
$ git commit -m 'add gitignore'
[main 58333b0] add gitignore
1 file changed, 1 insertion(+)
create mode 100644 .gitignore
As we see, the file is neither shown in the status, nor is it staged.
Diff
Let's look at what changed in the modified file. To see the
difference of the working directory to the commit the HEAD points to, we
can use git diff
. We will revisit it later with more
option. For now we run
$ git diff
diff --git a/partially_stage.txt b/partially_stage.txt
index 2447d81..88165ad 100644
--- a/partially_stage.txt
+++ b/partially_stage.txt
@@ -1,2 +1,5 @@
+this line has not to be staged, but keep the next!
this file is already tracked
-keep this line deleted
+this file line needs to be staged
+this line line does not to be staged
+stage this line!
We see a few lines were added to the file. And from the contents of it we need to stage parts of that file.
Advanced staging
We already know how to add whole file to the stage and prepare it for
the next commit. However, we can also decide on a hunk base which hunks
in a changed file shall be staged. We even can edit the hunk in the
process. Let's do that. We want to stage stage_me.txt
completely and partially_stage.txt
partly. This can be done
with git add
and git add -p
or even
interactively with git add -i
- refer to the "Working in a
repository" exercise for interactive staging.
First, do the easy part. Stage the stage_me.txt
file.
$ git add stage_me.txt
$ git status --short
M partially_stage.txt
A stage_me.txt
?? put_into_commit_3.txt
As we can see, the file is now added to the stage. Now we start the partially stage.
$ git add -p partially_stage.txt
git add -p partially_stage.txt
index 2447d81..88165ad 100644
--- a/partially_stage.txt
+++ b/partially_stage.txt
@@ -1,2 +1,5 @@
+this line has not to be staged, but keep the next!
this file is already tracked
-keep this line deleted
+this file line needs to be staged
+this line line does not to be staged
+stage this line!
(1/1) Stage this hunk [y,n,q,a,d,s,e,p,?]? ?
y - stage this hunk
n - do not stage this hunk
q - quit; do not stage this hunk or any of the remaining ones
a - stage this hunk and all later hunks in the file
d - do not stage this hunk or any of the later hunks in the file
s - split the current hunk into smaller hunks
e - manually edit the current hunk
p - print the current hunk, 'P' to use the pager
? - print help
(1/1) Stage this hunk [y,n,q,a,d,s,e,p,?]?
Git detects the change to this file as one hunk. Let's edit it by
pressing e
.
We will do a mistake and fix it right after. Follow along. Your hunk should look like
# Manual hunk edit mode -- see bottom for a quick guide.
@@ -1,2 +1,5 @@
+this line has not to be staged, but keep the next!
-keep this line deleted
this file is already tracked
+this file line needs to be staged
+stage this line!
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
# If the patch applies cleanly, the edited hunk will immediately be marked for staging.
# If it does not apply cleanly, you will be given an opportunity to
# edit again. If all lines of the hunk are removed, then the edit is
# aborted and the hunk is left unchanged.
Save and exit your editor and you will be back at the command prompt.
Diffs
It's always good to look what we commit before we create one. As long
as we are using the staging area are not using
git commit -a
to commit all changed files we can do this
with git diff --staged
$ git diff --staged
diff --git a/partially_stage.txt b/partially_stage.txt
index 2447d81..a4fae39 100644
--- a/partially_stage.txt
+++ b/partially_stage.txt
@@ -1,2 +1,4 @@
+this line has not to be staged, but keep the next!
this file is already tracked
-keep this line deleted
+this file line needs to be staged
+stage this line!
diff --git a/stage_me.txt b/stage_me.txt
new file mode 100644
index 0000000..942c233
--- /dev/null
+++ b/stage_me.txt
@@ -0,0 +1 @@
+this file needs to be staged completely!
We can see the complete stage_me.txt
file and the staged
changes to partially_stage.txt
including the first line we
were supposed to delete. Before we fix this mistake let's have a look at
what git diff
is showing.
$ git diff
diff --git a/partially_stage.txt b/partially_stage.txt
index a4fae39..88165ad 100644
--- a/partially_stage.txt
+++ b/partially_stage.txt
@@ -1,4 +1,5 @@
this line has not to be staged, but keep the next!
this file is already tracked
this file line needs to be staged
+this line line does not to be staged
stage this line!
As only diff we see the not staged line of
partially_stage.txt
. git diff
without a
argument shows the diff of your tracked working director files to the
index, also known as staging area.
Restore a staged file
Now we need to fix this mistake. We could try to just run another
git add -p partially_stage.txt
, edit the hunk by deleting
the line and being informed 'edited hunk does not apply'.
But what now? Maybe git
offers a hint. There was a lot
of text when running git status
, right?
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: partially_stage.txt
new file: stage_me.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: partially_stage.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
put_into_commit_3.txt
Apparently we have two options to restore files:
git restore --staged partially_stage.txt
to unstagegit restore partially_stage.txt
roll back the working directory changes
Looking into the manpage with man git restore
we are
informed the source of the restore operation is either HEAD or the
index. Index is used when --staged
is supplied.
$ git restore --staged partially_stage.txt
$ git status --short
M partially_stage.txt
A stage_me.txt
?? put_into_commit_3.txt
$ git diff
diff --git a/partially_stage.txt b/partially_stage.txt
index 2447d81..88165ad 100644
--- a/partially_stage.txt
+++ b/partially_stage.txt
@@ -1,2 +1,5 @@
+this line has not to be staged, but keep the next!
this file is already tracked
-keep this line deleted
+this file line needs to be staged
+this line line does not to be staged
+stage this line!
We are back where we started. Now we can use
git add -p partially_stage.txt
again and do it right this
time.
git restore
does also take the -p
switch to
work hunk based!
Stash
We are not committing yet. There is an other handy command to know.
git stash
stashes the current changes on a stack, managed
by git. The following commands are useful:
git stash
stashes the changed files of the working directory and the indexgit stash --keep-indexed
stashes the changed files of the working directory but keep the index, be careful if applying the stash when you staged and changed the same file/hunk in the working directory. This may lead to conflict.git stash list
lists the currently available stashesgit stash pop
applies the topmost stash and deletes itgit stash apply
same as pop, but no deleting
You can reference a stash to apply with stash@{X}
, see
the output of git stash list
to get the correct
reference.
$ git status --short
M partially_stage.txt
A stage_me.txt
?? put_into_commit_3.txt
$ git stash
Saved working directory and index state WIP on main: 58333b0 add gitignore
$ git status --short
$ git stash pop
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: stage_me.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: partially_stage.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
put_into_commit_3.txt
Dropped refs/stash@{0} (c83a7385d4a14cc4337deebc19fad9faf1998897)
$ git status --short
M partially_stage.txt
A stage_me.txt
?? put_into_commit_3.txt
$ git diff
diff --git a/partially_stage.txt b/partially_stage.txt
index 2447d81..88165ad 100644
--- a/partially_stage.txt
+++ b/partially_stage.txt
@@ -1,2 +1,5 @@
+this line has not to be staged, but keep the next!
this file is already tracked
-keep this line deleted
+this file line needs to be staged
+this line line does not to be staged
+stage this line!
Wait, our staged hunks of partially_stage.txt
are lost.
Now we need to do it all over again. Last time, let's run
git add -p
and edit the file.
Commit short forms
Directly supply a commit message
Again, let's check what we will commit.
$ git diff --staged
diff --git a/partially_stage.txt b/partially_stage.txt
index 2447d81..2fe5011 100644
--- a/partially_stage.txt
+++ b/partially_stage.txt
@@ -1,2 +1,3 @@
this file is already tracked
-keep this line deleted
+this file line needs to be staged
+stage this line!
diff --git a/stage_me.txt b/stage_me.txt
new file mode 100644
index 0000000..942c233
--- /dev/null
+++ b/stage_me.txt
@@ -0,0 +1 @@
+this file needs to be staged completely!
And now we commit it and set the commit message to 'second commit' in one go.
$ git commit -m 'second commit'
[main 66469da] second commit
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 stage_me.txt
git log --oneline
66469da second commit
58333b0 add gitignore
683751c remove secret file
cf11082 add secret file
324194a inital commit
We bypassed the editor and added a commit message using the
-m
switch directly.
Add all changed files
There is another useful flag -a
this adds all tracked
and changed files to the index and the commits them. We can combine it
with -m
.
$ git status --short
M partially_stage.txt
?? put_into_commit_3.txt
$ git commit -am 'third commit'
[main b44d786] third commit
1 file changed, 2 insertions(+)
git log --oneline
b44d786 third commit
66469da second commit
58333b0 add gitignore
683751c remove secret file
cf11082 add secret file
324194a inital commit
$ git status --short
?? put_into_commit_3.txt
Nice, we committed the last changed of
partially_stage.txt
directly. But we forgot to add
'put_into_commit_3.txt`. No problem, we just amend the last commit.
Amending a commit
Amending a commit changes the commit content and therefore its hash. When other people already know about this commit, talk to them first. Especially when you are using remotes to collaborate. Technically it replaces the tip of the current branch by creating a new commit.
Usually git commit --amend
will open an editor. We will
use a short form here and supply it with a commit message directly.
First we add all untracked files, then we amend.
$ git add .
$ git status --short
A put_into_commit_3.txt
$ git commit --amend -m 'fixed third commit'
[main 58cd7bb] fixed third commit
Date: Thu Jul 17 00:18:29 2025 +0200
2 files changed, 3 insertions(+)
create mode 100644 put_into_commit_3.txt
$ git status --short
$ git log --oneline
58cd7bb fixed third commit
66469da second commit
58333b0 add gitignore
683751c remove secret file
cf11082 add secret file
324194a inital commit
git show 58cd7bb5586beb702bebf8e975dccfcd3a76e36b
commit 58cd7bb5586beb702bebf8e975dccfcd3a76e36b
Author: maschmi <maschmi@maschmi.net>
Date: Thu Jul 17 00:18:29 2025 +0200
fixed third commit
diff --git a/partially_stage.txt b/partially_stage.txt
index 2fe5011..88165ad 100644
--- a/partially_stage.txt
+++ b/partially_stage.txt
@@ -1,3 +1,5 @@
+this line has not to be staged, but keep the next!
this file is already tracked
this file line needs to be staged
+this line line does not to be staged
stage this line!
diff --git a/put_into_commit_3.txt b/put_into_commit_3.txt
new file mode 100644
index 0000000..87248fd
--- /dev/null
+++ b/put_into_commit_3.txt
@@ -0,0 +1 @@
+this file will be in our third commit!
We see our fixed commit in the log and we see the file added in the commit content.
But what happened to our old commit?
Reflog
Remember the HEAD? This is how git keeps track at which revision we are working. Git not only keeps track of this information in this moment, it also logs the information in the reflog.
git reflog
will print the reflog of what happened to the headgit log -g
will print the reflog of what happened to the head in the standard log format
$ git reflog
58cd7bb HEAD@{0}: commit (amend): fixed third commit
b44d786 HEAD@{1}: commit: third commit
66469da HEAD@{2}: commit: second commit
58333b0 HEAD@{3}: reset: moving to HEAD
58333b0 HEAD@{4}: commit: add gitignore
683751c HEAD@{5}: commit: remove secret file
cf11082 HEAD@{6}: commit: add secret file
324194a HEAD@{7}: commit (initial): inital commit
We can see the amend and the initial commit easily. We also can see
the reset, it actually came from the git stash
when it
reset the HEAD. Before we go looking deeper at resets, let's have a
quick check if we can find our original commit. It is right at the
second place from the top!
git show 66469da6a6bc02028684e32771b8b4d2550b13c5
commit 66469da6a6bc02028684e32771b8b4d2550b13c5
Author: maschmi <maschmi@maschmi.net>
Date: Thu Jul 17 00:18:29 2025 +0200
second commit
diff --git a/partially_stage.txt b/partially_stage.txt
index 2447d81..2fe5011 100644
--- a/partially_stage.txt
+++ b/partially_stage.txt
@@ -1,2 +1,3 @@
this file is already tracked
-keep this line deleted
+this file line needs to be staged
+stage this line!
diff --git a/stage_me.txt b/stage_me.txt
new file mode 100644
index 0000000..942c233
--- /dev/null
+++ b/stage_me.txt
@@ -0,0 +1 @@
+this file needs to be staged completely!
Resets
Resets come in three variants:
- soft
- mixed
- hard
they differ in what they reset. All of them reset the HEAD, not all of them index and working tree.
type | HEAD | index | working tree |
---|---|---|---|
soft | reset | keep | keep |
mixed | reset | reset | keep |
hard | reset | reset | reset |
Mixed is the default. Looking at that table we can say:
- A soft reset keeps your staged files and changed files as they are. It will add the changed files from previous commits to the index.
- A mixed reset does not keep your staged files but will keep your changed files. Changes from previous commits will not be changed.
- A hard reset does keep noting. All changes will be lost.
So, when to use which one? * Use the hard reset when you will not
keep anything. One of the most ways I use it is with
git reset --hard HEAD
if I have done some experimental
changes and want just get rid of them. * A mixed reset comes in handy,
when you've made multiple commits and want to change the contents of
them. No files are staged, you can just start packing your commits
again. * A soft reset comes in hand when you want to squash some commits
together. After the reset all the changes are stages. You just need to
do a commit.
Before we perform a squash with a soft reset, one word about an
interactive rebase. An interactive rebase allows you to change the order
of commits, content of commits and the messages in one go. You even can
drop and squash or fixup commits. It is started with a
git rebase -i commitHash
. I will not go into further detail
in here. Just be aware, it will re-write the commits and changes
history.
Squash the commits 2 to 5
We want to combine commits 2 to 5 into one single commit. No one will see, we tracked a secret file! The workflow is as follows:
- perform a soft reset to the commit before commit 2
- verify all changes a staged
- commit
We will also check the reflog and see if we can find the old commits.
git log --oneline
58cd7bb fixed third commit
66469da second commit
58333b0 add gitignore
683751c remove secret file
cf11082 add secret file
324194a inital commit
We need to reset to
324194a4ad5de579f7552eb8c5f509afd9616540
. But there are
other ways to select the commit we want to reset too. We know we need to
go back two commits. We can use either of those short forms as well:
what it does | ||
---|---|---|
HEAD^ | HEAD~1 | resets to the parent of the HEAD commit |
HEAD^^ | HEAD~2 | resets to the parent of parent of the HEAD commit |
HEAD~n | resets to the n-th parent of the HEAD commit |
Let's use the one with the ~5
.
git reset --soft HEAD~5
git log --oneline
324194a inital commit
git status --short
A .gitignore
M partially_stage.txt
A put_into_commit_3.txt
A stage_me.txt
git commit -m 'squashed commits 2 to 5'
[main faca401] squashed commits 2 to 5
4 files changed, 7 insertions(+), 1 deletion(-)
create mode 100644 .gitignore
create mode 100644 put_into_commit_3.txt
create mode 100644 stage_me.txt
git log --oneline
faca401 squashed commits 2 to 5
324194a inital commit
Nice, we have squashed the commits 2 to 5 into one.
git reflog
faca401 HEAD@{0}: commit: squashed commits 2 to 5
324194a HEAD@{1}: reset: moving to HEAD~5
58cd7bb HEAD@{2}: commit (amend): fixed third commit
b44d786 HEAD@{3}: commit: third commit
66469da HEAD@{4}: commit: second commit
58333b0 HEAD@{5}: reset: moving to HEAD
58333b0 HEAD@{6}: commit: add gitignore
683751c HEAD@{7}: commit: remove secret file
cf11082 HEAD@{8}: commit: add secret file
324194a HEAD@{9}: commit (initial): inital commit
We can see the movement of the HEAD. It was reset to
324194a4ad5de579f7552eb8c5f509afd9616540
and then a new
commit was added and the HEAD was moved to
faca401823b7c46275fd52998ce7429769d11620
. We can also see
the former commits in the reflog.
Finding unreferenced objects
We can use git fsck
to perform a file system check and
find unreferenced objects.
git fsck
dangling commit c83a7385d4a14cc4337deebc19fad9faf1998897
dangling blob a4fae396699dcf19fc8383d8ca95281b0d6d71db
does show one dangling object.
But this object is a commit we cannot find in the reflog.
$ git show c83a7385d4a14cc4337deebc19fad9faf1998897
a4fae396699dcf19fc8383d8ca95281b0d6d71db
commit c83a7385d4a14cc4337deebc19fad9faf1998897
Merge: 58333b0 662de1c
Author: maschmi <maschmi@maschmi.net>
Date: Thu Jul 17 00:18:29 2025 +0200
WIP on main: 58333b0 add gitignore
diff --cc partially_stage.txt
index 2447d81,2447d81..88165ad
--- a/partially_stage.txt
+++ b/partially_stage.txt
@@@ -1,2 -1,2 +1,5 @@@
++this line has not to be staged, but keep the next!
this file is already tracked
--keep this line deleted
++this file line needs to be staged
++this line line does not to be staged
++stage this line!
this line has not to be staged, but keep the next!
this file is already tracked
this file line needs to be staged
stage this line!
Turns out, it is the stash we dropped.
Interesting, we expected some more dangling commit. Before be look into this, let's define what a dangling object is. A dangling object is one which is never directly used. We know, the reflog is still using the commits we expected to find.
Let's exclude the reflog and try again.
$ git fsck --no-reflog
dangling commit c83a7385d4a14cc4337deebc19fad9faf1998897
dangling commit 58cd7bb5586beb702bebf8e975dccfcd3a76e36b
dangling blob a4fae396699dcf19fc8383d8ca95281b0d6d71db
dangling commit b44d786e8d6c7ba2ee532c2eef5b60df8168c065
Still, we are missing some commits. This is, because the first dangling commit, is the start of a whole segment. It points to its parent and so on. So the other commits are not reachable directly, but still indirectly used.
To check for non-reachable objects we must use
git fsck --unreachable
.
$ git fsck --unreachable --no-reflog
unreachable tree 004c6d378c25bf21bb4560d4a6cce2c19617c019
unreachable blob c197c0d7a98577d3c6929804651f2eb07895aef4
unreachable commit c83a7385d4a14cc4337deebc19fad9faf1998897
unreachable commit cf110821a87c5ae8592ffc5bab55e3885373f59d
unreachable tree 114ff376b9727ce6b634f5aa31a9b07ec20f558e
unreachable commit 58333b0b4f0c97602473dd810146ccd405bda5ee
unreachable tree 9996727d1e9b40121e2e23902a5634e333e12b8a
unreachable commit 58cd7bb5586beb702bebf8e975dccfcd3a76e36b
unreachable tree 63cd3aac6d61bf0e7c05e9688b3f9b6c7eddfa7c
unreachable commit 662de1c83e39277b93a243f22bbff54f311a8fe7
unreachable commit 66469da6a6bc02028684e32771b8b4d2550b13c5
unreachable commit 683751cbe61e9ec1f071463d49cb2a9793dc5bce
unreachable blob a4fae396699dcf19fc8383d8ca95281b0d6d71db
unreachable blob 2fe501170d33b54392bcd7782270fb65e88e9ae7
unreachable tree 6f64cbb82438a07dd3e5b8bf3461f440deefaad6
unreachable commit b44d786e8d6c7ba2ee532c2eef5b60df8168c065
Thats a lot of objects. Let's just look at the commits!
$ git fsck --unreachable --no-reflog | grep commit
unreachable commit c83a7385d4a14cc4337deebc19fad9faf1998897
unreachable commit cf110821a87c5ae8592ffc5bab55e3885373f59d
unreachable commit 58333b0b4f0c97602473dd810146ccd405bda5ee
unreachable commit 58cd7bb5586beb702bebf8e975dccfcd3a76e36b
unreachable commit 662de1c83e39277b93a243f22bbff54f311a8fe7
unreachable commit 66469da6a6bc02028684e32771b8b4d2550b13c5
unreachable commit 683751cbe61e9ec1f071463d49cb2a9793dc5bce
unreachable commit b44d786e8d6c7ba2ee532c2eef5b60df8168c065
$ git reflog
faca401 HEAD@{0}: commit: squashed commits 2 to 5
324194a HEAD@{1}: reset: moving to HEAD~5
58cd7bb HEAD@{2}: commit (amend): fixed third commit
b44d786 HEAD@{3}: commit: third commit
66469da HEAD@{4}: commit: second commit
58333b0 HEAD@{5}: reset: moving to HEAD
58333b0 HEAD@{6}: commit: add gitignore
683751c HEAD@{7}: commit: remove secret file
cf11082 HEAD@{8}: commit: add secret file
324194a HEAD@{9}: commit (initial): inital commit
Here they are. Git never deleted these commits. They are still present.
If you want to know how to delete them and when they may be deleted
automatically. Run man git gc
to read the docs.
git gc
is usually is run by some porcelain commands when
thy determine it needes to be run. Then at least 2 week old unreachable
objects will be pruned and loose objects will be packed.
This is not the place to go into details of gc, nor into packfiles.