Fundamentals of Git
Git is a distributed version control system used to track changes in files and to manage the history of a software project.
In software engineering, Git allows us to:
- create repositories;
- track changes over time;
- create checkpoints of development;
- compare different states of a project;
- create independent branches of development;
- merge completed work;
- undo changes;
- inspect project history;
- recover previous states;
- temporarily store unfinished work;
- mark important versions with tags.
Git is not only a tool for collaboration.
Even when you are working completely alone, Git gives you something extremely important:
A structured history of how your project changed.
That history allows you to experiment without constantly being afraid of destroying the previous working state.
Installing Git on Linux Mint
On Linux Mint, Git can be installed directly through APT:
sudo apt update
sudo apt install git
Verify the installation:
git --version
Display general Git help:
git help
Useful built-in help commands include:
git help -a
git help -g
git help init
git help commit
git help branch
git help merge
You can also use:
git <command> --help
For example:
git commit --help
git status --help
git log --help
Do not try to memorize every Git flag.
Learn the concepts first.
Then use Git’s own documentation when you need a specific option.
Initial Git Configuration
Before creating commits, configure the identity that Git will store in commit metadata.
Set your name:
git config --global user.name "Your Name"
Set your email:
git config --global user.email "you@example.com"
Inspect global configuration:
git config --global --list
Your global Git configuration is normally stored in:
~/.gitconfig
You can inspect it with:
cat ~/.gitconfig
or:
nano ~/.gitconfig
You normally do not need to create this file manually. Git creates or updates it when configuration values are written.
Creating a Working Directory
For the examples in this guide, create a directory that will contain Git repositories:
mkdir -p ~/gitLib
cd ~/gitLib
Now create the first project:
mkdir first_repo
cd first_repo
git init - Initialize a Repository
A normal directory is not automatically a Git repository.
To initialize Git inside the current directory:
git init
Example output may look similar to:
Initialized empty Git repository in /home/developer/gitLib/first_repo/.git/
Inspect hidden files:
ls -la
You will now see:
.git/
The .git directory contains Git’s repository metadata and object database.
That directory is what turns the project directory into a Git repository.
Do not manually edit or delete files inside
.gitunless you understand Git internals and know exactly what you are doing.
The Fundamental Git Cycle
A basic Git workflow looks like this:
Initialize repository
↓
Modify files
↓
Inspect status
↓
Stage selected changes
↓
Commit staged changes
↓
Inspect history
↓
Continue development
The most important commands at the beginning are:
git init
git status
git add
git commit
git log
Everything else builds on these concepts.
Working Tree, Staging Area, and Repository
Before going further, understand the three main states involved in normal Git work.
Working Tree
↓
Staging Area
↓
Repository History
The working tree contains the files you are currently editing.
The staging area contains the exact changes selected for the next commit.
The repository history contains commits that have already been recorded.
A useful mental model is:
edit
↓
git add
↓
stage
↓
git commit
↓
history
git status
Immediately after initialization:
git status
A new repository may report:
On branch master
No commits yet
nothing to commit
Depending on your Git configuration, the default branch may be named master or main.
This guide uses:
master
because that is the branch name used throughout the original Team413 material.
The Git concepts are identical regardless of the branch name.
Creating an Untracked File
Create a file:
touch message1.txt
Now inspect status:
git status
Git will report the file as untracked.
Conceptually:
message1.txt
↓
exists in working tree
↓
Git is not tracking it yet
git add - Add Changes to the Staging Area
Stage one file:
git add message1.txt
Inspect status:
git status
The file is now prepared for the next commit.
You can stage all current changes with:
git add .
But do not use git add . automatically without looking at what changed.
A better habit is:
git status
git diff
git add <specific-files>
when you want precise control over the next commit.
Removing a File From the Staging Area
The original guide demonstrates:
git rm --cached message1.txt
That command removes the file from Git’s index while leaving the working-tree file present.
For a file that you merely staged accidentally and still want Git to track later, modern Git also provides:
git restore --staged message1.txt
The important concept is:
Staging can be changed before committing.
A commit should contain the exact change you intend to record.
git commit - Create a Checkpoint
A commit records the staged state of the project.
Think of a commit as a checkpoint.
Stage the file:
git add message1.txt
Create the first commit:
git commit -m "Our first system message"
Git creates a commit with its own unique identifier.
Afterward:
git status
should show:
nothing to commit, working tree clean
The Three Common Change States
A tracked file may conceptually move through:
modified
↓
staged
↓
committed
Modified - The file differs from the last committed version.
Staged - The change has been selected for the next commit.
Committed - The staged change has been stored in repository history.
Short Status
Use:
git status -s
or:
git status --short
Typical status letters include:
?? untracked
M modified
A added
D deleted
R renamed
The short format is useful once you understand what the symbols represent.
git log - Commit History
Display commit history:
git log
A normal entry contains:
- commit ID;
- author;
- timestamp;
- commit message.
A compact view:
git log --oneline
Example:
0ef0242 Our first system message
The value:
0ef0242
is a shortened form of the commit ID.
Git commit IDs allow us to reference exact historical states.
Limiting Log Output
Show the last three commits:
git log -n 3 --oneline
Show commits between two revisions:
git log <older>..<newer> --oneline
Example:
git log d110129..74d91c --oneline
Visualizing History
A very useful command is:
git log --oneline --decorate --graph --all
This shows:
- compact commit IDs;
- branch and tag names;
- branch relationships;
- the commit graph.
Use this frequently while learning branches and merges.
Branches
A branch represents an independent line of development.
Branches allow us to isolate:
- new features;
- bug fixes;
- experiments;
- refactoring;
- temporary work.
A common workflow is:
stable branch
↓
create feature branch
↓
develop and commit
↓
test
↓
merge
↓
remove feature branch when finished
Listing Branches
git branch
The active branch is marked with:
*
Example:
* master
Creating a Branch
git branch feature/task2
List again:
git branch
Example:
feature/task2
* master
The branch exists, but we have not switched to it yet.
Switching Branches
The original guide uses:
git checkout feature/task2
Modern Git also provides:
git switch feature/task2
Both concepts mean:
Move
HEADto another branch and update the working tree to that branch’s state.
Create and Switch in One Command
Using checkout:
git checkout -b feature/task3
Modern equivalent:
git switch -c feature/task3
Rename a Branch
git branch -m old-name new-name
Example:
git branch -m small-feature quick-feature
Commit Changes on a Feature Branch
Switch to the feature branch:
git checkout feature/task2
Modify a file:
echo "This is the first system message!" >> message1.txt
Inspect:
git status
Stage:
git add message1.txt
Commit:
git commit -m "First system message has been modified"
Inspect history:
git log --oneline --decorate --graph --all
You should now see that the feature branch contains a commit that the original branch does not yet contain.
git diff Between Branches
Before merging, inspect the difference:
git diff master feature/task2
Git displays what changes would distinguish one branch state from the other.
Merging a Branch
To merge feature/task2 into master, first switch to the branch that should receive the changes:
git checkout master
Then:
git merge feature/task2
This direction matters.
Conceptually:
current branch
+
incoming branch
↓
merged current branch
Fast-Forward Merge
If master has not changed since the feature branch was created, Git may perform a fast-forward merge.
Before:
A---B master
\
C---D feature
After fast-forward:
A---B---C---D master, feature
Git does not need a new merge commit.
It simply moves the master branch pointer forward.
Deleting a Merged Branch
After a feature has been merged:
git branch -d feature/task2
The lowercase -d performs a safe deletion and refuses when the branch contains work that Git considers unmerged.
Force deletion:
git branch -D feature/task2
Use
-Dcarefully. It can remove a branch that contains commits not reachable from another branch.
git commit -a
For already tracked files, Git can stage modifications and deletions as part of commit:
git commit -am "Fourth system message has been added"
This is effectively useful for tracked files only.
It does not automatically include brand-new untracked files.
For new files, use:
git add new-file
git commit -m "Add new file"
Creating Several Commits
Create another modification:
echo "Second system message: Welcome!" >> message1.txt
git add message1.txt
git commit -m "Second system message has been added"
Then another:
echo "Third system message: Analyze!" >> message1.txt
git add message1.txt
git commit -m "Third system message has been added"
And another:
echo "Fourth system message: Proceed!" >> message1.txt
git commit -am "Fourth system message has been added"
Inspect:
git log --oneline
Now the project history contains multiple checkpoints.
Checking Out a Specific Commit
Suppose the history contains:
e0c20e6 Fourth system message has been added
24af2fa Third system message has been added
aba3bd9 Second system message has been added
650bf2c First system message has been modified
0ef0242 Our first system message
To inspect the repository at the second-message commit:
git checkout aba3bd9
Git will enter a detached HEAD state.
Understanding HEAD
HEAD represents the currently checked-out position.
Normally:
HEAD
↓
branch
↓
commit
For example:
HEAD -> master -> e0c20e6
In detached HEAD state:
HEAD -> aba3bd9
HEAD points directly to a commit rather than to a normal branch.
Detached HEAD
Detached HEAD is useful for:
- inspecting old states;
- testing old commits;
- temporary experiments;
- comparing historical versions.
Example:
git status
may show:
HEAD detached at aba3bd9
The working tree now represents the selected historical commit.
Creating Commits in Detached HEAD
You can modify files and commit while detached.
For example:
echo "Experimental message" >> message1.txt
git commit -am "Experimental detached commit"
But that commit is not automatically attached to a normal branch.
If you later switch away, Git may warn that you are leaving the commit behind.
To preserve it, create a branch:
git branch experiment <commit-id>
or while currently detached:
git switch -c experiment
Creating a Branch From an Older Commit
Checkout the historical commit:
git checkout aba3bd9
Create a new branch from that point:
git checkout -b feature/task5
Now any new commits belong to feature/task5.
This is one of Git’s most powerful properties:
Any historical commit can become the starting point of a new line of development.
git revert - Safely Undo a Commit
Suppose the latest commit added something that should be removed.
Use:
git revert HEAD
Git creates a new commit that reverses the effect of the selected commit.
History remains intact.
Conceptually:
A---B---C
↓
incorrect change
git revert C
A---B---C---D
↓
inverse of C
Both the original commit and the revert remain visible.
This makes revert especially useful when history should remain traceable.
Revert a Specific Commit
git revert <commit-id>
Example:
git revert 140c7a6
git reset - Move Repository State
git reset is different from git revert.
Revert adds a new inverse commit.
Reset moves a branch reference and can change the staging area and working tree depending on the mode.
This makes reset powerful, but potentially destructive.
Unstage Changes With Reset
A plain:
git reset
moves staged changes out of the staging area while normally leaving the working-tree modifications present.
Conceptually:
staged
↓
git reset
↓
modified but unstaged
Modern Git also provides:
git restore --staged <file>
for explicit unstaging.
Reset to a Commit
git reset <commit-id>
By default this is a mixed reset.
The branch moves to the specified commit, while later file changes normally remain in the working tree as unstaged modifications.
git reset --hard
Example:
git reset --hard 24af2fa
This moves the branch and resets both:
- staging area;
- working tree.
Changes after the target state may disappear from the visible branch and working tree.
Treat
git reset --hardas destructive. Always inspectgit statusandgit logfirst.
Do not use it casually on work you have not protected.
Revert vs Reset
A simple comparison:
| Operation | History | Working Principle | Typical Use |
|---|---|---|---|
git revert | Preserved | Add inverse commit | Safe history-preserving undo |
git reset | Repositioned | Move branch/reference | Local history manipulation |
git reset --hard | Repositioned | Move branch and discard working/staged state | Explicit destructive reset |
A useful rule:
Use revert when history should remain intact. Use reset when you intentionally want to rewrite or reposition local history.
git clean - Remove Untracked Files
git reset primarily affects tracked repository state.
git clean deals with untracked files.
Preview first:
git clean -n
or:
git clean --dry-run
This is extremely important.
It shows what Git would delete.
Remove Untracked Files
git clean -f
Remove untracked directories too:
git clean -df
Remove ignored files as well:
git clean -xf
git clean -xfis dangerous. It can delete build output, local configuration, generated data, and other ignored files.
Always run a dry-run variant first.
Merge With --no-ff
A normal fast-forward merge may not create a dedicated merge commit.
Sometimes you intentionally want the branch integration to remain visible in history.
Use:
git merge feature/task4 --no-ff
This forces a merge commit even when Git could fast-forward.
Example history:
* Merge branch 'feature/task4'
|\
| * Feature commit 2
| * Feature commit 1
|/
* Previous master commit
This makes the existence of the feature branch explicit in the project history.
Fast-Forward vs --no-ff
Fast-forward:
A---B---C---D
Forced merge commit:
A---B-------M
\ /
C---D
Neither is universally correct.
The choice depends on how you want project history to communicate development structure.
Amend the Last Commit
Sometimes the last commit needs adjustment.
You may have:
- forgotten to include a file;
- written the wrong commit message;
- staged one more correction immediately after committing.
Use:
git commit --amend
To replace only the message directly:
git commit --amend -m "Corrected commit message"
To add forgotten changes:
git add forgotten-file
git commit --amend
Important Property of Amend
Amending does not modify the existing commit in place.
It creates a new commit object with a new commit ID.
Conceptually:
old commit: 66ee579
↓ amend
new commit: 0229304
The visible history now points to the replacement commit.
This is history rewriting.
For local work, that is often fine.
Be careful when rewriting commits that other people may already depend on.
Rebase
Rebase rewrites one line of commits so that it appears to start from another base commit.
Suppose history looks like this:
A---B---C master
\
D---E feature
After rebasing feature onto master:
A---B---C---D'---E' feature
D' and E' are new commits representing the replayed changes.
The original commit IDs change.
Why Rebase?
Rebase is often used to maintain a linear history.
Instead of creating:
A---B---C
\ \
D---E---M
you can replay feature work after the current base:
A---B---C---D'---E'
Basic Rebase
Switch to the feature branch:
git checkout feature/task5
Rebase onto master:
git rebase master
Git identifies commits that belong to the feature branch and replays them on top of the current master.
Rebase Can Produce Conflicts
If both histories changed overlapping parts of the same content, Git may stop with a conflict.
Example:
CONFLICT (content): Merge conflict in message1.txt
Inspect state:
git status
Open the conflicted file.
Git conflict markers may look like:
<<<<<<< HEAD
content from one side
=======
content from the other side
>>>>>>> commit
You must decide what the final content should be.
Resolve a Rebase Conflict
- Edit the conflicted file.
- Remove conflict markers.
- Keep the correct final content.
- Stage the resolved file.
git add message1.txt
Continue:
git rebase --continue
Abort a Rebase
If you want to return to the state before rebase started:
git rebase --abort
This is one of the most important rebase commands to remember.
Skip a Rebase Commit
Git also provides:
git rebase --skip
This skips the commit currently being replayed.
Use it only when you intentionally want to discard that commit’s change from the rebased history.
Rebase Conflict Principle
Editing the same file on different branches is completely normal.
A conflict does not happen merely because the same file changed.
Conflicts usually appear when Git cannot automatically reconcile overlapping changes.
For example:
branch A modifies line 20
branch B also modifies line 20
may require manual resolution.
While:
branch A modifies line 20
branch B modifies line 200
may merge or rebase automatically.
The important skill is not avoiding all concurrent edits.
The important skill is understanding how to resolve conflicting changes safely.
Rebase Changes Commit IDs
Because rebase creates new commit objects, commit IDs before and after rebase differ.
Before:
5ffabe2 Fifth system message
6224156 Sixth system message
After replay:
5e110cf Fifth system message
4aa3940 Sixth system message
The logical changes may be similar.
The commits are new objects.
Merge After Rebase
If a feature branch has been successfully rebased onto the current master, merging it may become a fast-forward:
git checkout master
git merge feature/task5
Then remove the completed branch:
git branch -d feature/task5
git reflog
Git’s normal log shows commits reachable through normal references.
reflog records how local references such as HEAD moved over time.
Run:
git reflog
Example entries may represent:
- commits;
- checkouts;
- resets;
- rebases;
- branch switches.
Conceptually:
HEAD@{0} current position
HEAD@{1} previous movement
HEAD@{2} movement before that
Why Reflog Matters
You may run:
git reset --hard <older-commit>
and think a newer local commit has disappeared.
But Git may still record the previous HEAD movement in reflog.
Inspect:
git reflog
Then inspect a previous state:
git show HEAD@{1}
or:
git checkout HEAD@{1}
A safer recovery approach is often to create a branch:
git branch recovery HEAD@{1}
Reflog Time Expressions
Git accepts several time expressions for reflog references.
Examples:
git show master@{1.hour.ago}
git show master@{1.day.ago}
git show master@{2.weeks.ago}
Display reflog-style history:
git log -g master
Reflog is local repository metadata.
It is an extremely useful recovery mechanism, but it should not be treated as a permanent backup system.
Tags
Tags mark important commits with meaningful names.
Typical uses include:
- releases;
- release candidates;
- milestones;
- stable checkpoints.
Example:
git tag v1.7-rc1
List tags:
git tag
or:
git tag --list
Show a Tag
git show v1.7-rc1
Git displays the commit associated with the tag and related information.
Lightweight Tags
A lightweight tag is essentially a name pointing directly to a commit.
Create one:
git tag v1.4-lw
Annotated Tags
Annotated tags create a tag object containing additional metadata.
Create:
git tag -a v1.4 -m "Version 1.4"
Annotated tags are useful when you want the tag itself to carry:
- tagger information;
- timestamp;
- message.
Inspect Tag Object Type
Use:
git cat-file -t <tag>
Example:
git cat-file -t v1.4-lw
A lightweight tag commonly resolves directly to:
commit
An annotated tag resolves to:
tag
Find Tags
List matching tags:
git tag -l "v1.8*"
or:
git tag --list "v1.8*"
Describe Current State
git describe --all
Include tags:
git describe --all --tags
This is useful for connecting the current repository state to nearby named references.
Compare Tags
Tags can be used anywhere Git accepts revisions.
Example:
git diff v1.7-rc1 v1.8-rc2
This makes tags useful for release comparisons.
Move a Tag
Force a local tag to another commit:
git tag -f v3.1.0-beta <commit-id>
Delete a local tag:
git tag -d v3.1.0-beta
Be careful when moving meaningful release tags.
A tag is often treated as a stable reference.
Create a Branch From a Tag
git checkout -b branch-v1.8 v1.8-rc1
Modern equivalent:
git switch -c branch-v1.8 v1.8-rc1
This creates a new line of development from the tagged commit.
git stash
Sometimes you are in the middle of unfinished work and need to switch context.
You do not want to create a meaningless half-finished commit.
That is where stash is useful.
git stash temporarily stores uncommitted changes and restores the working tree toward the committed state.
Conceptually:
unfinished working changes
↓
git stash
↓
clean working tree
↓
do other work
↓
restore stash
Create a Stash
Modify a tracked file:
echo "For every connection we use SSL!" >> programming-language.txt
Then:
git stash
List stashes:
git stash list
Example:
stash@{0}: WIP on master: b854bf3 ...
Named Stash
A clear modern form is:
git stash push -m "database changes"
The original guide uses:
git stash save "database changes"
git stash save exists in older workflows, but git stash push -m is clearer for current Git usage.
Stash Indexing
Stashes are indexed:
stash@{0}
stash@{1}
stash@{2}
The newest stash is normally:
stash@{0}
If one is removed, the remaining indices may shift.
Do not assume an old index remains attached to the same stash forever.
Check:
git stash list
first.
Inspect a Stash
git stash show stash@{1}
Show a patch:
git stash show -p stash@{1}
Apply a Stash
Apply a specific stash without removing it from the stash list:
git stash apply stash@{1}
Apply the newest stash:
git stash apply
After application:
git status
will show the restored changes.
Pop a Stash
Apply and remove it:
git stash pop stash@{0}
Conceptually:
apply
+
drop
Drop a Stash
git stash drop stash@{1}
Delete all stashes:
git stash clear
git stash clearremoves the entire stash stack. Checkgit stash listfirst.
Stash Untracked Files
By default, stash focuses on tracked modifications and staged changes.
Include untracked files:
git stash -u
or:
git stash push -u -m "work in progress"
Create a Branch From a Stash
Git can create a branch starting from the commit where the stash was originally created and apply the stash there:
git stash branch feature/task94 stash@{0}
This is useful when unfinished work has grown into something that deserves its own branch.
Stash Can Conflict
Applying a stash may conflict with changes already present in the current working tree.
For example:
error: Your local changes would be overwritten
At that point you may need to:
- commit current changes;
- stash current changes;
- resolve overlaps manually;
- reset only when you intentionally want to discard the current work.
Do not immediately use:
git reset --hard
unless you have confirmed that the current changes can be destroyed.
git diff
git diff compares Git data sources and displays differences.
It is commonly used together with:
git status
git log
Use git status to see what changed.
Use git diff to see how it changed.
Working-Tree Diff
Modify a file:
echo "New service for Payment!" >> programming-language.txt
Then:
git diff
Git displays the unstaged change.
List Changed Filenames
git diff --name-only
Example:
programming-language.txt
Short Statistics
git diff --shortstat
Example:
1 file changed, 1 insertion(+)
Name and Status
git diff --name-status
Possible status letters include:
| Letter | Meaning |
|---|---|
A | Added |
C | Copied |
D | Deleted |
M | Modified |
R | Renamed |
T | Type changed |
U | Unmerged |
Understanding Diff Output
A simplified Git diff may look like:
diff --git a/programming-language.txt b/programming-language.txt
--- a/programming-language.txt
+++ b/programming-language.txt
@@ -3,3 +3,4 @@
We talk to each other via gRPC!
We use only HTTP2 connection!
+New service for Payment!
Lines beginning with:
+
were added.
Lines beginning with:
-
were removed.
Compare Two Commits
Get commit IDs:
git log --oneline
Then:
git diff <commit-a> <commit-b>
Example:
git diff b854bf cff609
You usually do not need to type the entire commit hash as long as the shortened prefix is unique.
Compare a Commit With HEAD
git diff b854bf HEAD
HEAD represents the currently checked-out commit.
Compare Branches
git diff master feature/task94
For a specific file:
git diff master feature/task94 -- ./diff_test.txt
Using -- clearly separates revision arguments from file paths.
Two-Dot and Three-Dot Diff
Two branch tips can be compared with:
git diff branch1 branch2
or:
git diff branch1..branch2
A three-dot comparison:
git diff branch1...branch2
compares the common ancestor of the branches with the tip of the second branch.
This can be useful for asking:
What has branch2 changed since it diverged from branch1?
Compare Tags
git diff v1.7-rc1 v1.8.1
Because tags identify commits or tag objects resolving to commits, they are natural comparison points for releases.
Save a Diff to a File
Redirect output:
git diff b854bf HEAD > ~/diff-head-commit.txt
Inspect:
cat ~/diff-head-commit.txt
A diff can therefore be stored, reviewed, attached to documentation, or processed by other command-line tools.
A Complete Local Git Workflow
A practical local development cycle might look like:
git status
Modify files.
Then:
git diff
Stage selected changes:
git add file1 file2
Inspect staged state:
git status
Commit:
git commit -m "Implement feature X"
Create another line of development:
git checkout -b feature/y
Work and commit.
Then inspect:
git log --oneline --decorate --graph --all
Return:
git checkout master
Merge:
git merge feature/y
Delete completed branch:
git branch -d feature/y
This is already enough Git knowledge to work productively on a large amount of local software development.
Git Safety Rules
Git is extremely powerful because it allows history to be manipulated.
That is also why some commands require respect.
Before destructive operations, check:
git status
git log --oneline --decorate --graph --all
Before cleaning untracked files:
git clean -n
Before force-deleting a branch:
git log <branch>
Before:
git reset --hard
make sure the working-tree changes can actually be discarded.
Before:
git stash clear
inspect:
git stash list
The general rule is:
Inspect first. Modify second.
Commands You Should Know
At the end of this chapter, you should understand the purpose of:
git init
git config
git status
git add
git restore --staged
git commit
git log
git branch
git checkout
git switch
git merge
git diff
git revert
git reset
git clean
git commit --amend
git rebase
git reflog
git tag
git stash
You do not need to remember every flag.
You need to understand the state transitions they perform.
Git State Mental Model
A useful final model is:
Untracked File
↓ git add
Tracked / Staged
↓ git commit
Committed History
For an already tracked file:
Committed
↓ edit
Modified
↓ git add
Staged
↓ git commit
New Commit
Branches add another dimension:
feature
/
A---B---C------D---E
\
master
Merge combines development histories.
Rebase replays one history onto another base.
Revert adds a new commit that undoes an older one.
Reset moves references and can change staged or working state.
Stash temporarily stores unfinished work.
Reflog records local reference movement.
Tags give important commits meaningful names.
Diff explains the difference between states.
That is Git.
Final Perspective
Do not think of Git as a collection of commands.
Think of Git as a model of project history.
Every command changes one or more of these things:
working tree
staging area
commit history
branch references
HEAD
tags
stash
Once you understand those objects, Git stops feeling random.
You begin to understand why a command behaves the way it does.
The most important habit is not typing commands quickly.
It is being able to answer:
Where is HEAD?
Which branch am I on?
What is modified?
What is staged?
What is committed?
What history will this command change?
Can I recover if I make a mistake?
When you can answer those questions before executing a Git command, you are no longer memorizing Git.
You are using it deliberately.