I used to just memorize git commands without understanding what was going on behind the scenes. Add, commit, push, and hope it works. Then one day I actually opened the .git folder and everything clicked.
This post covers the basics of how Git works internally, how to configure it properly, and how branches and merging actually function. If you are tired of blindly typing commands and want to understand what Git is actually doing, this is for you.
Setting Up Git Properly
When you first install Git, it needs to know who you are. Every commit gets tagged with a name and email, so Git stores these in a file called .gitconfig.
You can set these globally, which means every repository on your machine uses the same identity. Or you can set them locally per project. Most people go global for their name and email since those do not change.
You can also change your default editor. Git loves opening Vim for commit messages, which is fine if you know Vim, but most beginners would rather just use VSCode. Run this to fix that:
git config --global core.editor "code --wait"
The --wait flag is important. It tells Git to pause and wait until you close the editor window before it continues.
Then set your name and email:
git config --global user.name "ufraan"
git config --global user.email "ufraan1@gmail.com"
You can check what you set by running the same commands without the values. These settings live in your .gitconfig file. On Linux or macOS it is at ~/.gitconfig. On Windows it is at C:\Users\<YourUsername>\.gitconfig. Open it up and you will see your settings, plus any SSH keys you have configured for talking to GitHub or GitLab.
The .gitignore File
Git tracks everything by default. That means build artifacts, environment files with your API keys, node_modules, and random OS metadata all get picked up. You do not want any of that in your repository.
Create a .gitignore file in your project root to tell Git what to ignore:
node_modules/
*.log
.DS_Store
*.pyc
.env
Quick breakdown of what these cover:
-
node_modules/gets massive and you can always reinstall it withnpm install -
*.logfiles are just program output -
.DS_Storeis macOS metadata that has nothing to do with your code -
*.pycare compiled Python files generated automatically -
.envusually has secrets and should never be committed
If you are starting a new project and do not know what to put in there, search for "gitignore generator" online. Pick your tech stack and it will give you a ready-made template.
Once your .gitignore is set up:
git add .
git commit -m "Initial commit"
git log
The git add . command automatically respects your .gitignore file, so it will not stage anything you told it to ignore.
What Happens Behind the Scenes
Run git log --oneline and you will see short commit hashes and your messages. You might also see something like HEAD -> master. But what is actually happening?
Every commit stores a reference to the previous commit. Except the very first one, which has no parent. Each commit also gets a unique hash that acts like a fingerprint for that snapshot. This creates a chain of commits going all the way back to the beginning.
You can see this for yourself by peeking into the .git folder:
ls -la
cd .git
ls
You will find a few important things inside:
-
HEADpoints to the branch you are currently on. Open it and you will see something likeref: refs/heads/master -
objects/stores all your actual data, including commits and file contents -
refs/stores branch pointers -
logs/keeps a history of changes to HEAD and other references
So when you run git commit, Git creates a new commit object in the objects directory, updates the branch pointer in refs, and HEAD follows the current branch. That is the whole process.
Understanding Git Branches
Branches are basically parallel timelines for your project. You can work on a feature in isolation without touching the main codebase. Git creates a default branch called master (or main on newer setups), which is usually where the stable version lives.
Let us walk through how this actually works. Create a new folder and initialize a repo:
mkdir gittwo
cd gittwo
git init
git status
After git init, you will see "On branch master". Create a file and track it:
# Create an index.html file with some basic HTML
git status
git add index.html
git commit -m "add index file"
git branch
The output shows * master. The asterisk means that is where HEAD is pointing.
Now make some changes and commit again:
git add index.html
git commit -m "update code for index file"
Let us say you need to work on a navigation bar without affecting master. Create a branch:
git branch nav-bar
git branch
You will see nav-bar listed below master, and the asterisk is still on master. You created the branch but you are not on it yet. Switch over:
git checkout nav-bar
# or the newer command:
git switch nav-bar
git branch
Now the asterisk moved to nav-bar. Create a file on this branch:
# Create nav-bar.html
git add nav-bar.html
git commit -m "add navbar to codebase"
Now switch back to master:
git checkout master
Notice that nav-bar.html disappeared from your editor. It is not deleted. It just lives on the nav-bar branch now. This is the key thing about branches. Each branch has its own working directory state. Switch back to nav-bar and the file comes right back.
You can always check where HEAD points:
git branch
git log --oneline
# Shows HEAD -> master or HEAD -> nav-bar
A few useful branch commands to keep around:
git branch # list all branches
git branch bugfix # create a new branch without switching
git switch bugfix # switch to a branch
git switch -c dark-mode # create and switch in one step
git checkout -b pink-mode # same thing, older syntax
git branch -d branch-name # delete a branch
One thing to remember. Always commit your changes before switching branches. If you leave work uncommitted and switch, it can either follow you to the new branch or cause problems. Just commit first.
Merging Branches
Once you are done on a branch, you need to bring those changes back into master. That is what merging does.
Fast-Forward Merge
This is the simple case. If master has not changed since you created your branch, Git just moves the master pointer forward to your latest commit. No extra merge commit needed.
git checkout master
git merge nav-bar
Non-Fast-Forward Merge
If both master and your branch have new commits since you split off, Git cannot just fast-forward. It has to create a merge commit that combines both histories. You can also force this behavior even when a fast-forward is possible:
git checkout master
git merge --no-ff nav-bar
The --no-ff flag creates an explicit merge commit. This is useful if you want to keep a clear record of when a feature branch was integrated, even if a fast-forward was possible.
Let us try merging in practice:
git checkout master
git merge nav-bar -m "merge navbar"
git log --oneline
The nav-bar commits are now part of master. You can see nav-bar.html in your editor alongside the other files. Once merged, you can clean up the branch:
git branch -d nav-bar
Do it again with another branch:
git checkout -b footer
# Create footer.html
git add footer.html
git commit -m "add footer section to codebase"
git checkout master
git merge footer
Now footer.html appears in master as well.
Resolving Merge Conflicts
Conflicts happen when two branches modify the same part of the same file. Git does not know which version to keep, so it stops and asks you to decide.
Here is how to trigger and fix one:
# On master, modify index.html
# Add "footer added" inside the body tag
git add index.html
git commit -m "add footer in index file"
# Switch to footer branch
git checkout footer
# Modify the same file differently
# Add "footer was added successfully" inside the body tag
git add index.html
git commit -m "update index file with footer code"
# Switch back to master and try to merge
git checkout master
git merge footer
Git will stop and tell you there is a conflict. Open index.html and you will see something like:
<<<<<<< HEAD
footer added
=======
footer was added successfully
>>>>>>> footer
The section between <<<<<<< HEAD and ======= is your current branch. The section between ======= and >>>>>>> footer is the incoming branch. You need to pick one, combine them, or write something entirely different. Then delete the conflict markers completely.
Once the file looks right:
git add index.html
git commit -m "resolve conflict in index file"
That is it. The conflict is resolved and both branches are merged into master.
My Git Aliases
I use a few aliases to save myself from typing the same commands over and over. Here is what I have in my .zshrc:
alias gs='git status'
alias ga='git add'
alias gcm='git commit -m'
alias gp='git push'
alias gpl='git pull'
alias gc='git clone'
alias gb='git branch'
They are nothing fancy, but gcm and gs alone save a ton of keystrokes over the course of a day.
Wrapping Up
If you want to go deeper into how Git actually works, the Pro Git book is the best resource out there. It is free to read online at https://git-scm.com/book/en/v2 and covers everything from basics to advanced internals. I highly recommend it if you really want to master Git.
Git stops being scary once you understand what is happening under the hood. Commits are just snapshots linked in a chain. Branches are pointers to those snapshots. Merging is moving pointers around. The .git folder is not some black box. It is just files and references that you can look at whenever you want.
Adios! :)
Top comments (0)