Mastering Git: Complete Guide from Init to Push with Branch Management

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-authentication
  • bugfix/login-error
  • hotfix/security-patch
  • refactor/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 diff first

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.

Avatar photo

Asif Khan

I have spent over 10 years working across IT systems, open source software, DevOps, Linux administration and cloud operations. Three things drive most of what I do: automation, security and resilience. Much of that work involves planning and building the platforms that sit behind services people rely on daily, which means designing for failure just as carefully as for load. Cloud computing held my attention early on, largely for its flexibility. Being able to scale up and then back down again means far less guessing about how much capacity you will need. Across projects I work with the full DevOps toolchain, from provisioning, orchestration and configuration management through to release management and microservices architecture.