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:

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:

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
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:

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:

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:

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.