Nearly every codebase you will ever be handed is in Git. That isn’t much of an exaggeration. Package sources, configuration management, infrastructure definitions, somebody’s dotfiles, the deploy script nobody has opened in two years. All Git.
The awkward part is that its reputation for being confusing is completely earned. My own first week with it, I couldn’t work out why adding a file and committing a file were two separate operations. It felt like paperwork. It stopped feeling like paperwork the first time I needed to commit half of what I’d changed and leave the rest alone, which is the exact problem the staging area exists to solve.
So this goes in order: install, configure, first repository, first commit, branches, remote, and the handful of things that go wrong often enough to be worth rehearsing before they happen to you. The outputs below are copied out of a real terminal rather than written from memory, so what you read is what you should see.
What is Git and Why Does It Matter?
Linus Torvalds wrote Git in 2005, and the decision that shapes everything else is sitting in the description: it’s distributed. Older systems kept the history on a server and handed you a working copy of it. Git hands you the lot. Every clone on every machine is a complete repository with the full history inside it.
That reads like a technicality until you notice what falls out of it. You can commit on a train with no signal. Branching costs almost nothing because it happens locally. And if the central server burns down, the history is still sitting on every laptop that ever cloned it.
The parts people genuinely end up leaning on:
- Branching and Merging: somewhere isolated to work that leaves the main codebase alone
- Distributed Development: everyone on the team holds a full repository, history included
- Speed: commits, diffs and history browsing happen locally, so they come back immediately
- Data Integrity: everything is checksummed, so silent corruption has nowhere to hide
- Staging Area: a place to review and arrange changes before they become a commit
Installing Git on Linux
Git is packaged everywhere, so this is the easy bit. Only the package manager changes.
Installing on RHEL/CentOS/Fedora
On Red Hat and its relatives, dnf handles it:
sudo dnf install git -y
CentOS 7 predates dnf, so yum there instead:
sudo yum install git -y
Then check what you actually got:
git --version
Output:
git version 2.43.0
Installing on Ubuntu/Debian
Debian and Ubuntu, apt:
sudo apt update
sudo apt install git -y
Same check:
git --version
Output:
git version 2.43.0
Initial Git Configuration
Git won’t let you make a commit until it knows who you are, and it’s less annoying to sort that out now than to hit the error halfway through your first one. Two settings, and they get stamped into everything you commit from here on.
Setting Your Username and Email
Name and email:
git config --global user.name "John Mitchell"
git config --global user.email "john.mitchell@linuxpathfinder.com"
Both values are public the moment you push anywhere, so use details you’re happy to have attached to your work. The --global flag writes them once for every repository on the machine. Leave the flag off while you’re inside a repository and the setting applies only to that one, which is how people keep a work identity and a personal one from bleeding into each other.
Configuring Default Branch Name
‘main’ is the name modern Git uses for the default branch. Worth setting deliberately rather than leaving it to whatever your version decides:
git config --global init.defaultBranch main
Setting Your Default Editor
Git opens an editor any time a commit message isn’t supplied on the command line. Choose one you know how to get out of:
git config --global core.editor "vim"
Or nano:
git config --global core.editor "nano"
Verifying Your Configuration
Read the whole lot back:
git config --list
Output:
user.name=John Mitchell
user.email=john.mitchell@linuxpathfinder.com
init.defaultbranch=main
core.editor=vim
Creating Your First Git Repository
Configuration done. From here it runs as one continuous example: a directory becomes a repository, files get staged and committed, a branch gets created and worked on, and the whole thing ends up on a remote.
Step 1: Initialize a New Repository
Make a directory and initialise it:
mkdir my-project
cd my-project
git init
Output:
Initialized empty Git repository in /home/john/my-project/.git/
Everything Git knows about this project now lives in a hidden .git directory at the top of it: metadata, history, configuration. Remove that folder and you’re back to an ordinary directory of files. Have a look at where things stand:
git status
Output:
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track)
Step 2: Creating Project Files
Some files to track. They’re stand-ins here, but in a real project this is your source, your configuration, your documentation:
echo "# My Project" > README.md
echo "node_modules/" > .gitignore
mkdir src
echo 'console.log("Hello, Git!");' > src/app.js
Status again:
git status
Output:
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
.gitignore
README.md
src/
nothing added to commit but untracked files present (use "git add" to track)
Git can see the files. It just isn’t doing anything with them. That’s the untracked state, and it’s deliberate. Git never assumes a file that appears in your working directory is one you meant to put in the history.
Step 3: Staging Files
Here’s the part that trips most people up. The staging area, also called the index, sits between your working directory and the repository. You put changes into it, then you commit whatever is sitting in it.
The extra step buys you control. Fix three unrelated things in one afternoon and you can still land them as three separate commits, which is worth a great deal six months later when one of them turns out to be the change that broke something.
Stage the lot:
git add .
And look at what’s queued up:
git status
Output:
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: .gitignore
new file: README.md
new file: src/app.js
Staged and ready to go. Naming files one at a time works too, and it’s the habit worth building:
git add README.md
git add src/app.js
Step 4: Making Your First Commit
A commit freezes whatever is staged into a permanent snapshot. The message attached to it is what future-you reads at two in the morning trying to work out when something changed, so write it like it matters:
git commit -m "Initial commit: Add project structure and README"
Output:
[main (root-commit) a7f8c2d] Initial commit: Add project structure and README
3 files changed, 5 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 src/app.js
That short string, a7f8c2d, is the commit’s identifier. It’s how you point at this exact state of the project anywhere else in Git. The history so far:
git log
Output:
commit a7f8c2d3f6e9b1a2c4d5e6f7a8b9c0d1e2f3a4b5
Author: John Mitchell <john.mitchell@linuxpathfinder.com>
Date: Fri Aug 16 09:15:33 2024 -0400
Initial commit: Add project structure and README
One line per commit is usually all you want:
git log --oneline
Output:
a7f8c2d (HEAD -> main) Initial commit: Add project structure and README
Working with Branches
Branching is where Git earns the reputation. A branch is somewhere to work without putting the main line of the project at risk, which means you can start something speculative, leave it half-finished, and switch away from it without tidying up first.
Creating a New Branch
A branch for the feature work:
git branch development
List what exists:
git branch
Output:
development
* main
The asterisk (*) marks where you currently are, and it’s still on main. Creating a branch doesn’t move you onto it.
Switching to a New Branch
Move across:
git checkout development
Output:
Switched to branch 'development'
Two commands for one job, though, and there’s a shortcut that does both at once:
git checkout -b feature-authentication
Output:
Switched to a new branch 'feature-authentication'
Confirm where you ended up:
git branch
Output:
development
* feature-authentication
main
Making Changes on a Branch
Now do some actual work on it:
echo 'function authenticate(user) { return true; }' > src/auth.js
git add src/auth.js
git commit -m "Add authentication module"
Output:
[feature-authentication 3b9e4f2] Add authentication module
1 file changed, 1 insertion(+)
create mode 100644 src/auth.js
And a second change on top:
echo "## Authentication" >> README.md
git add README.md
git commit -m "Update README with authentication section"
Output:
[feature-authentication 7c1d8e9] Update README with authentication section
1 file changed, 1 insertion(+)
The history on this branch:
git log --oneline
Output:
7c1d8e9 (HEAD -> feature-authentication) Update README with authentication section
3b9e4f2 Add authentication module
a7f8c2d (main, development) Initial commit: Add project structure and README
Connecting to a Remote Repository
Everything so far has been local. A remote, whether that’s GitHub, GitLab or Bitbucket, is what makes the work shareable and gives you a copy that outlives the laptop.
Adding a Remote Repository
Create the empty repository on the hosting side first, then point your local one at it:
git remote add origin https://github.com/johnmitchell/my-project.git
Check it took:
git remote -v
Output:
origin https://github.com/johnmitchell/my-project.git (fetch)
origin https://github.com/johnmitchell/my-project.git (push)
origin is only a label. Convention says it’s the name for your primary remote and near enough everybody follows it, but nothing enforces it, and a repository can carry several remotes under different names when you need it to.
Pushing Your Branch to Remote
Send the feature branch up:
git push -u origin feature-authentication
Output:
Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
Delta compression using up to 4 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (8/8), 756 bytes | 756.00 KiB/s, done.
Total 8 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), done.
To https://github.com/johnmitchell/my-project.git
* [new branch] feature-authentication -> feature-authentication
branch 'feature-authentication' set up to track 'origin/feature-authentication'.
The -u flag, short for --set-upstream, links your local branch to the one on the remote. Do it once per branch and every push after that is a bare git push with no remote or branch name attached. Skip it and Git keeps asking you to spell out the destination every single time.
Pushing the Main Branch
Main needs pushing as well:
git checkout main
git push -u origin main
Output:
Switched to branch 'main'
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 456 bytes | 456.00 KiB/s, done.
Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/johnmitchell/my-project.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
Viewing Remote Branches
Everything, local and remote together:
git branch -a
Output:
development
feature-authentication
* main
remotes/origin/feature-authentication
remotes/origin/main
Common Git Workflows
The commands above are the vocabulary. These are the sentences you’ll actually build out of them.
Scenario 1: Making Changes and Pushing Updates
Once a branch is already tracking, an ordinary day collapses to about four commands:
echo 'function logout(user) { return true; }' >> src/auth.js
git add src/auth.js
git commit -m "Add logout functionality"
git push
Output:
[feature-authentication 9d2f5a1] Add logout functionality
1 file changed, 1 insertion(+)
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 398 bytes | 398.00 KiB/s, done.
Total 4 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/johnmitchell/my-project.git
7c1d8e9..9d2f5a1 feature-authentication -> feature-authentication
Scenario 2: Pulling Changes from Remote
When somebody else has pushed, you need their work in front of you before you add yours on top:
git pull origin feature-authentication
Output:
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), 312 bytes | 104.00 KiB/s, done.
From https://github.com/johnmitchell/my-project
* branch feature-authentication -> FETCH_HEAD
9d2f5a1..e4b7c3f feature-authentication -> origin/feature-authentication
Updating 9d2f5a1..e4b7c3f
Fast-forward
src/auth.js | 2 ++
1 file changed, 2 insertions(+)
git pull isn’t really one operation. It’s git fetch to download what changed, followed immediately by git merge to fold it into your branch. Running the two separately takes longer, and it lets you look at what arrived before you agree to take it.
Scenario 3: Merging Branches
Feature finished, and it goes back into main:
git checkout main
git merge feature-authentication
Output:
Switched to branch 'main'
Updating a7f8c2d..9d2f5a1
Fast-forward
README.md | 1 +
src/auth.js | 2 ++
2 files changed, 3 insertions(+)
create mode 100644 src/auth.js
Then push the result:
git push origin main
Output:
Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
Delta compression using up to 4 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 612 bytes | 612.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To https://github.com/johnmitchell/my-project.git
a7f8c2d..9d2f5a1 main -> main
Viewing Repository Information
Checking Status
Before every commit, no exceptions:
git status
Output:
On branch feature-authentication
Your branch is up to date with 'origin/feature-authentication'.
nothing to commit, working tree clean
Viewing Detailed Commit History
The long form, with author, date and the full message:
git log --graph --oneline --all
Output:
* 9d2f5a1 (HEAD -> main, origin/main, origin/feature-authentication, feature-authentication) Add logout functionality
* 7c1d8e9 Update README with authentication section
* 3b9e4f2 Add authentication module
* a7f8c2d (development) Initial commit: Add project structure and README
Viewing Changes Before Committing
What’s changed but isn’t staged yet:
git diff
And what’s staged but not yet committed:
git diff --staged
Practical Troubleshooting Scenarios
Scenario 1: Accidentally Committed to Wrong Branch
Everybody does this one eventually. You get absorbed in the work, commit twice, then notice you were sitting on main the entire time:
git checkout -b feature-fix
git checkout main
git reset --hard HEAD~1
git checkout feature-fix
The new branch is created pointing at where you already are, so it keeps both commits, and main is then rewound to where it stood before you started. Nothing is lost in the process.
Scenario 2: Need to Undo Last Commit
A commit you’d like back:
git reset --soft HEAD~1
That drops the commit and leaves the changes sitting staged, so you can fix the message or the contents and commit again. --hard does the same thing but throws the changes away with it. Be sure before you reach for that one, because there’s no second undo waiting behind it.
Scenario 3: Merge Conflict Resolution
Sooner or later two people edit the same lines and the merge stops dead:
git pull origin main
Output:
Auto-merging src/auth.js
CONFLICT (content): Merge conflict in src/auth.js
Automatic merge failed; fix conflicts and then commit the result.
Git doesn’t guess. It writes both versions into the file, fenced off with <<<<<<<, ======= and >>>>>>> markers. Open the file, decide what the line should actually say, delete the markers along with the version you don’t want, and then:
git add src/auth.js
git commit -m "Resolve merge conflict in auth.js"
Best Practices for Git Workflow
Commit Message Guidelines
Write messages that mean something to somebody reading them cold:
- Good: “Add user authentication with JWT tokens”
- Bad: “Fixed stuff” or “Update”
Use the imperative: “Add feature”, not “Added feature”. It reads oddly for about a week and then it doesn’t, and it matches the messages Git writes for itself.
Branch Naming Conventions
Name branches so the purpose is obvious from the listing:
feature/user-authenticationbugfix/login-errorhotfix/security-patchrefactor/database-layer
Commit Frequency
Commit in units that stand up on their own. One complete change per commit, not one day’s work per commit. It pays for itself the moment you need to:
- Track a bug down with
git bisect - Revert one change without dragging unrelated ones back with it
- Read back the project history and have it make sense
Security Considerations
Credentials in a repository are the mistake that keeps costing people money. Once it’s pushed it’s in the history, and deleting the file afterwards does nothing about the commits that already contain it:
- Keep config files holding credentials out with
.gitignore - Add
.env,config/secrets.yml, and private keys to.gitignore - Pass sensitive values in through environment variables instead
- Read what you are about to push with
git difffirst
Somewhere to start with .gitignore:
node_modules/
.env
*.log
config/secrets.yml
.DS_Store
*.pem
*.key
Keep Main Branch Stable
Do the work on branches and merge into main only once it’s been tested. The point isn’t ceremony. It’s that main stays deployable, so when something urgent lands you can cut from it without first working out what state it’s in.
Essential Git Commands Reference
All of it in one place:
# Repository Setup
git init # Initialize repository
git clone [url] # Clone remote repository
git remote add origin [url] # Add remote repository
# Basic Workflow
git status # Check repository status
git add [file] # Stage file
git add . # Stage all changes
git commit -m "message" # Commit staged changes
git push # Push to remote
git pull # Fetch and merge from remote
# Branch Management
git branch # List branches
git branch [name] # Create branch
git checkout [branch] # Switch branch
git checkout -b [branch] # Create and switch branch
git merge [branch] # Merge branch into current
git branch -d [branch] # Delete local branch
git push origin --delete [branch] # Delete remote branch
# Information
git log # View commit history
git log --oneline # Compact log view
git diff # Show unstaged changes
git diff --staged # Show staged changes
# Undoing Changes
git reset --soft HEAD~1 # Undo last commit, keep changes staged
git reset --hard HEAD~1 # Undo last commit, discard changes
git checkout -- [file] # Discard changes in file
Conclusion
None of what’s above is advanced. Init, add, commit, branch, push, pull, merge. That’s the whole set, and it covers the overwhelming majority of what you’ll do in a working week.
What takes time isn’t the commands. It’s the habits. Committing small. Writing messages you’d want to read yourself. Branching by reflex rather than as an afterthought. Those only come from use, and the quickest way to build them is to put a personal project under version control and stop treating Git as something you touch when the team makes you.
The safety net is also better than people expect. Almost anything can be undone, and work sitting on a branch can’t hurt what’s on main. That’s permission to experiment. Break a branch on purpose one afternoon and get yourself back out of it, because doing that once deliberately is worth more than reading about it three times.
Two things to carry forward regardless of how you work: main stays deployable, and secrets never go in. Nearly everything else is recoverable.
Once this feels routine, the next stops are rebasing, cherry-picking and interactive staging. All worth knowing. Also not what you’ll reach for most days, so there’s no rush. And if you haven’t yet connected Git to a hosting account, installing and using Git and GitHub on Linux covers that side of it.


