Git Tutorial: Learn Version Control from Scratch (2026)
The first time I lost code was during my second year as a developer. I had been working on a feature for three days, made a huge refactor, and accidentally deleted the wrong directory. No backups, no undo. That weekend I learned Git, and I have never lost a line of code since. Git is more than a version control tool — it is a time machine, a collaboration platform, and a safety net that lets you experiment without fear. This tutorial covers everything from your first commit to advanced branching strategies used by teams shipping software daily in 2026.
The Git Object Model and Initial Setup
Git stores everything as objects: blobs, trees, and commits. A blob holds file contents, a tree maps filenames to blobs, and a commit points to a tree with metadata — author, message, timestamp, and parent commits. Understanding this model demystifies Git's behavior. When you commit, Git creates a SHA-1 hash of the entire state. This hash is content-addressable: the same content always produces the same hash, so Git never duplicates data. Before using Git, configure your identity: user.name and user.email are baked into every commit. I also recommend setting your default branch name to main and choosing an editor you are comfortable with for commit messages.
git config --global user.name "Jane Doe"
git config --global user.email "jane@example.com"
git config --global init.defaultBranch main
git config --global core.editor "code --wait"
Working Directory, Staging Area, and Commits
Git has three areas: the working directory where you edit files, the staging area (index) where you collect changes for the next commit, and the repository where commits are stored. This separation is what makes Git powerful. You can stage parts of a file, review what you are about to commit, and amend if you forgot something. I rarely use git add . blindly. Instead, I use interactive staging to craft clean, atomic commits: git add -p lets me review each hunk of changes before staging. A commit should represent one logical change — fixing a bug, adding a feature, or refactoring code. This discipline pays off when you need to bisect through history to find where a bug was introduced.
echo "# my project" > README.md
git init
git add README.md
git commit -m "chore: initial project setup with readme"
Branching and Merging Strategies
Branches are lightweight pointers to commits. Creating a branch is almost instantaneous because Git just writes a 40-byte file. I use branches for every feature, bug fix, and experiment. The default branch (usually main) should remain deployable at all times. When a feature is complete, you merge it back. The default merge strategy is the recursive strategy, which creates a merge commit with two parents. Fast-forward merges happen when the branch has not diverged — Git simply moves the pointer. For teams, three branching strategies dominate: GitHub Flow (branch from main, merge back), Git Flow (develop, feature, release, hotfix branches), and trunk-based development (short-lived branches merged frequently). Choose the one that matches your release cadence.
git checkout -b feature/user-auth
# make changes, stage, commit
git checkout main
git merge feature/user-auth --no-ff
git branch -d feature/user-auth
Rebasing and Interactive Rebase
Rebasing rewrites commit history by applying commits from one branch onto the tip of another. It produces a linear history that is easier to follow than a web of merge commits. The golden rule: never rebase commits that have been pushed to a shared branch. I use git rebase -i to squash, reword, reorder, and drop commits before pushing a feature branch. Squashing multiple WIP commits into a single clean commit before merging keeps the main branch history readable. When conflicts occur during a rebase, Git pauses at each conflicting commit, letting you resolve incrementally. Use git rebase --abort to cancel if the process becomes too messy.
git rebase -i HEAD~5
# pick, squash, reword, or edit each commit
git rebase --continue after resolving conflicts
git push origin feature-branch --force-with-lease
Remotes and Collaboration Workflows
Git is distributed — every clone is a full repository with complete history. Remotes are simply aliases for other repositories. The standard remote name is origin, pointing to the upstream repository. When collaborating, you fetch changes from the remote without merging them, inspect the differences, then merge or rebase locally. Pull requests (or merge requests) are not a Git feature — they are a platform feature on GitHub, GitLab, or Bitbucket. The underlying Git operation is a merge or rebase of a feature branch. I always fetch and rebase before pushing to avoid stale merge commits. If you work on open source, the fork-and-pull model lets you contribute without write access to the original repository.
git remote add origin https://github.com/user/repo.git
git push -u origin main
git fetch origin
git log --oneline HEAD..origin/main
Stashing, Tagging, and Debugging with Git
Git provides utilities beyond commits and branches. Stashing temporarily shelves changes so you can switch contexts: git stash push -m "wip login form" saves your working directory, and git stash pop restores it later. Tags mark specific points in history, typically releases. Annotated tags store the tagger name, date, and message — use these for release versions. For debugging, git bisect performs a binary search through history to find the commit that introduced a bug. I automate it with a script that returns 0 for good and 1 for bad. git blame annotates each line with the commit and author that last modified it — invaluable for understanding why a line exists.
git tag -a v2.0.0 -m "Release version 2.0.0"
git bisect start HEAD v1.0.0
git bisect run npm test
git stash list
Frequently Asked Questions
What is the difference between git pull and git fetch?
git fetch downloads objects and refs from the remote without updating your working directory. git pull runs git fetch followed by git merge (or git rebase with --rebase). I recommend using git fetch and then inspecting the changes before merging, especially in shared branches, to avoid unexpected merge commits.
How do I undo a commit that has already been pushed?
Use git revert HEAD --no-edit to create a new commit that undoes the previous commit. This is safe for shared branches because it does not rewrite history. For unpushed commits, you can use git reset --soft HEAD~1 to uncommit while keeping changes staged.
What is a detached HEAD state and how do I fix it?
Detached HEAD means you are checked out at a specific commit rather than a branch. You can view the code and make experimental commits, but they will be lost unless you create a branch. Run git checkout -b new-branch-name to create a branch from the current commit and save your work.
Why does Git ask me to set user.name and user.email even after I set them globally?
This happens when you are in a repository that has local config values overriding the global ones. Check with git config --local --list. You may also be running Git in a directory that is not a repository. Set the config either globally or per-repository.
Originally published on Ayodhyyya. Last updated June 1, 2026.