Skip to main content
01

Git from Zero to Pull Request: A Practical Guide

·6 min read
GitGitHubDeveloper ToolsVersion ControlBest Practices

You will use Git every day as a developer. Most tutorials either hand you a cheat sheet with no context or walk you through theory you'll never need. This is neither. It's the practical knowledge I wish someone had given me on day one — what Git does, how to use it, and why the commands work the way they do.

What Git Is and Why It Matters

Git is a version control system that tracks changes to your files over time. That's it. Every change you save becomes part of a history you can browse, search, and undo.

Teams use Git because it solves three problems at once: it keeps a complete history of every change, it lets multiple people work on the same codebase without overwriting each other, and it makes rollbacks trivial when something breaks.

One distinction worth making early: Git is the tool that runs on your machine. GitHub is a hosting platform where you store Git repositories online. You can use Git without GitHub — but most teams use both.

Installation and Setup

On macOS with Homebrew:

bash
brew install git

On Arch Linux:

bash
sudo pacman -S git

On Ubuntu or Debian:

bash
sudo apt install git

On Windows, download the installer from git-scm.com and follow the prompts. The defaults are fine.

After installing, configure your identity. Git attaches this to every commit you make:

bash
git config --global user.name "Your Name"
git config --global user.email "your@email.com"

Verify everything works:

bash
git --version

Repositories

A repository is a project tracked by Git. You create one in two ways.

git init creates a new repository in the current directory:

bash
mkdir my-project
cd my-project
git init

git clone copies an existing repository from a remote URL:

bash
git clone https://github.com/user/repo.git

Both create a hidden .git/ directory inside your project. That directory is the repository — it contains the entire history of your project. Delete it and you lose the history. The rest of your project files are just the working directory.

Git tracks files through three states. The working directory is where you edit files. The staging area is where you prepare changes for a commit. The repository is where committed snapshots are stored permanently. Understanding this flow — edit, stage, commit — is the mental model that makes every other Git command make sense.

Tracking Files and Making Commits

Check what Git sees:

bash
git status

This shows which files are modified, which are staged, and which are untracked. Run it often — it's your orientation command.

Stage files for a commit:

bash
git add index.html           # stage one file
git add src/                 # stage an entire directory
git add .                    # stage everything

Create a commit — a snapshot of your staged changes:

bash
git commit -m "Add homepage layout"

A commit is a snapshot, not a diff. Git stores the complete state of your staged files at that point in time. Diffs are computed later by comparing snapshots.

Good commit messages use imperative mood and a short subject line: "Add login form," "Fix null check in user service," "Remove deprecated API endpoint." Describe what the commit does, not what you did. Keep the subject under 50 characters. If you need more detail, leave a blank line and write a body.

View your commit history:

bash
git log
git log --oneline           # compact view

Some files should never be tracked — build artifacts, environment variables, dependency directories. Create a .gitignore file in your project root:

gitignore
node_modules/
.env
dist/
*.log

Git will ignore anything matching these patterns. Add .gitignore early. Removing a file from Git after it's been committed is more work than preventing it from being tracked in the first place.

Using GitHub

Create a repository on GitHub through the web interface. Don't initialize it with a README if you already have local commits — that creates a conflict.

Link your local repository to the remote:

bash
git remote add origin https://github.com/your-username/your-repo.git
git push -u origin main

The -u flag sets origin main as the default upstream, so future pushes only need git push.

GitHub supports two authentication methods: HTTPS and SSH. HTTPS prompts for credentials (use a personal access token, not your password). SSH uses a key pair and never prompts after setup. SSH is less friction day-to-day — set it up once with ssh-keygen and add the public key to your GitHub settings.

Clone someone else's repository to get a local copy:

bash
git clone https://github.com/other-user/their-repo.git

Beyond code hosting, GitHub adds a social layer: README files describe the project, Issues track bugs and feature requests, and Stars bookmark repositories you find useful. These aren't Git features — they're GitHub features.

Branching

A branch is an independent line of development. The default branch is main. Every other branch diverges from it and can be merged back later.

Create and list branches:

bash
git branch                  # list branches
git branch feature/login    # create a branch

Switch to a branch:

bash
git switch feature/login

Create and switch in one step:

bash
git switch -c feature/login

I use git switch instead of git checkout for branch operations. switch was introduced specifically for this purpose and is less ambiguous — checkout does too many things.

Branch because it gives you isolation. You can work on a feature without affecting main, experiment without risk, and throw away a branch if it doesn't work out. Your teammates do the same, and nobody steps on anyone else.

Name branches descriptively: feature/user-profile, fix/login-redirect, chore/update-deps. The prefix tells reviewers what kind of change to expect.

Pull Requests

A pull request is a request to merge your branch into another branch — usually main. It's where code review happens.

Push your branch to GitHub first:

bash
git push -u origin feature/login

Then create the PR on GitHub. Write a clear description: what changed, why it changed, and how to test it. Assign reviewers. Wait for CI checks to pass.

Code review basics: keep PRs small and focused. A PR that changes 50 lines gets reviewed carefully. A PR that changes 500 lines gets skimmed. If you're working on a large feature, break it into smaller PRs that build on each other.

Respond to review feedback by pushing additional commits to the same branch. The PR updates automatically.

Merging and Resolving Conflicts

When a PR is approved, you merge it. GitHub offers several merge strategies, but the two you'll encounter most:

Fast-forward merge moves the branch pointer forward when there's no divergence. The history stays linear. This happens when main hasn't changed since you branched off.

Merge commit creates a new commit that combines two branches. This happens when both branches have new commits. The merge commit has two parents — one from each branch.

Conflicts happen when two branches change the same lines in the same file. Git can't decide which version to keep, so it marks the file:

diff
<<<<<<< HEAD (Current Change)
const greeting = "Hello";
=======
const greeting = "Hi there";
>>>>>>> feature/login (Incoming Change)

Everything between <<<<<<< HEAD and ======= is your current branch. Everything between ======= and >>>>>>> is the incoming branch. To resolve it, delete the markers, keep the code you want, stage the file, and commit:

bash
git add src/greeting.ts
git commit -m "Resolve greeting conflict"

If things get messy and you want to start over:

bash
git merge --abort

This resets to the state before the merge attempt. No harm done.

Advanced Git

git stash shelves your uncommitted changes so you can switch branches without committing half-finished work. git stash saves them, git stash pop restores them. I use this multiple times a day — someone asks me to review their PR, I stash my work, switch branches, then come back.

git rebase replays your branch's commits on top of another branch, producing a linear history. Use rebase for local cleanup before pushing. Use merge for integrating shared branches. The rule: don't rebase commits that other people have based work on.

git cherry-pick applies a specific commit from one branch to another without merging the entire branch. Useful when a bugfix on a feature branch needs to land on main immediately — git cherry-pick abc1234 copies just that commit.

git bisect performs a binary search through your commit history to find which commit introduced a bug. You mark a known good commit and a known bad commit, and Git walks you through the middle points until it isolates the culprit. On large repositories with hundreds of commits between releases, this saves real time.

Interactive rebase (git rebase -i HEAD~5) lets you edit, squash, reorder, or drop recent commits before pushing. I use this to clean up a messy series of "WIP" commits into a coherent history. Squash the fixups, reword the messages, then push a clean branch.

These commands aren't daily drivers for most junior developers, but knowing they exist means you'll reach for them when the situation calls for it instead of working around problems manually.