Phase 1 · FOUNDATIONS
Git for DevOps — branching, merge vs rebase
By the end of today
- Turn a folder into a repo and commit with confidence
- Create, switch and merge branches without fear
- Choose merge or rebase and resolve a conflict cleanly
Git: commits, branches, and merge vs rebase
Every DevOps task eventually touches Git. It is how code, configuration, pipeline definitions and infrastructure files are versioned, reviewed and shipped — “if it isn’t in Git, it doesn’t exist” is close to a law on modern teams. Git is a distributed version control system: instead of one central copy, every clone carries the full history, so you can commit, branch and inspect the past entirely offline.
The mental model is three areas. Your working directory is the files as they sit on disk right now. The staging area (the “index”) is a holding pen where you assemble exactly the changes that belong in the next snapshot with git add. A commit is that snapshot made permanent with git commit — a full picture of the tracked files at that moment, stamped with an author, a time and a message, and chained to its parent. History is simply commits pointing back at their parents.
A branch is the cheap, central idea. It is nothing more than a movable pointer to a commit — creating one writes a tiny file, so branching is instant and free. main is just the branch that happens to be the default. You make a feature branch, commit on it, and main stays untouched until you decide to combine the work. That is what lets a team work on ten things at once without stepping on each other.
Real world: Think of a shared cookbook. Each commit is a dated, signed page you paste in — you can always flip back to any past version of a recipe. A branch is photocopying the current recipe to try more chilli on your own copy, while the master book stays clean. When the new version is good, you decide how to fold your copy back into the master book — and how you fold it is the merge-versus-rebase question.
Merge vs rebase — two ways to combine work
When your branch and main have both moved on, you have two ways to bring them together.
git merge ties the two histories with a new merge commit that has two parents. Nothing is rewritten — every original commit stays exactly where it happened, so history honestly shows two lines of work that ran in parallel and joined. If main has not moved since you branched, Git skips the merge commit and just slides the pointer forward: a fast-forward.
git rebase instead lifts your commits off and replays them one by one on top of the latest main, producing a straight, linear history as though you had started from today’s code. It rewrites commit IDs as it goes.
before: merge: rebase:
A---B---C feature D---E---F-------M main D---E---F---A'--B'--C' feature
/ \ / (A,B,C replayed on F)
D---E---F main A---B---C
The rule every team learns: rebase to tidy your own local branch before sharing; never rebase commits you have already pushed, because rewriting shared history forces everyone else to untangle it. Git itself was built by Linus Torvalds in 2005 to manage the Linux kernel — thousands of contributors, offline commits, and merges landing constantly — which is exactly the workload branches and merges were designed for.
Hands-On Lab
Budget about 25 minutes. Open your WSL2 Ubuntu 24.04 terminal (if you have not set up WSL2 yet, do Day 0 first — macOS and Linux users can follow along in their built-in terminal). You will build a tiny repo from scratch, branch it, merge cleanly, then force and resolve a real conflict. Type every command yourself and read every line of output.
# 1. Tell Git who you are (stamped on every commit) and default new repos to a 'main' branch.
git config --global user.name "Pushkar"
git config --global user.email "pushkar@example.com"
git config --global init.defaultBranch main
git config --global --list
# Output:
# user.name=Pushkar
# user.email=pushkar@example.com
# init.defaultBranch=main
# 2. Turn a new, empty folder into a Git repository — this creates the hidden .git/ store.
mkdir ~/site && cd ~/site
git init
# Output:
# Initialized empty Git repository in /home/pushkar/site/.git/
# 3. Add a file, then ask Git what it sees. A brand-new file is "untracked".
echo "# My Site" > README.md
git status
# Output:
# On branch main
#
# No commits yet
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
# README.md
#
# nothing added to commit but untracked files present (use "git add" to track)
# 4. Stage the file (add) then snapshot it (commit) with a message.
git add README.md
git commit -m "Add README"
# Output:
# [main (root-commit) a1b2c3d] Add README
# 1 file changed, 1 insertion(+)
# create mode 100644 README.md
# 5. Read the history. --oneline gives one commit per line: short hash + message.
git log --oneline
# Output:
# a1b2c3d (HEAD -> main) Add README
# 6. Create a branch and switch to it in one step. (git switch -c is the modern form.)
git switch -c feature/nav
# Output:
# Switched to a new branch 'feature/nav'
# 7. Do work on the branch and commit it. main is untouched.
echo "<nav>Home</nav>" > nav.html
git add nav.html
git commit -m "Add nav bar"
# Output:
# [feature/nav e4f5a6b] Add nav bar
# 1 file changed, 1 insertion(+)
# create mode 100644 nav.html
# 8. Switch back to main and merge the branch. main had NOT moved, so it's a fast-forward.
git switch main
git merge feature/nav
# Output:
# Switched to branch 'main'
# Updating a1b2c3d..e4f5a6b
# Fast-forward
# nav.html | 1 +
# 1 file changed, 1 insertion(+)
# create mode 100644 nav.html
# 9. Now force a real conflict: change the SAME line on two branches.
git switch -c feature/title
echo "# Awesome Site" > README.md
git commit -am "Retitle README on branch"
git switch main
echo "# Production Site" > README.md
git commit -am "Retitle README on main"
# Output:
# Switched to a new branch 'feature/title'
# [feature/title 7c8d9e0] Retitle README on branch
# 1 file changed, 1 insertion(+), 1 deletion(-)
# Switched to branch 'main'
# [main b2c3d4e] Retitle README on main
# 1 file changed, 1 insertion(+), 1 deletion(-)
# 10. Merge the branch into main. Both changed the same line, so Git stops and asks.
git merge feature/title
# Output:
# Auto-merging README.md
# CONFLICT (content): Merge conflict in README.md
# Automatic merge failed; fix conflicts and then commit the result.
# 11. See the conflict markers Git wrote, edit to the final version, then commit.
cat README.md
# Output:
# <<<<<<< HEAD
# # Production Site
# =======
# # Awesome Site
# >>>>>>> feature/title
echo "# Awesome Production Site" > README.md # keep the resolved line, delete ALL markers
git add README.md
git commit -m "Merge feature/title: resolve README title"
# Output:
# [main 9f0a1b2] Merge feature/title: resolve README title
# 12. Keep junk and secrets OUT of history: list patterns in .gitignore, then read the graph.
printf "node_modules/\n*.log\n.env\n" > .gitignore
git add .gitignore && git commit -m "Add .gitignore"
git log --oneline --graph
# Output:
# * c3d4e5f (HEAD -> main) Add .gitignore
# * 9f0a1b2 Merge feature/title: resolve README title
# |\
# | * 7c8d9e0 (feature/title) Retitle README on branch
# * | b2c3d4e Retitle README on main
# |/
# * e4f5a6b (feature/nav) Add nav bar
# * a1b2c3d Add README
Read the graph back once: a straight line of commits, one branch that merged fast-forward (nav), and one that diverged and needed a merge commit to resolve (title) — that shape is the whole day in a single picture.
Common Errors & Fixes
These three trip up almost everyone in their first week with Git. Read the error text slowly — learning to parse it is the actual skill.
Common error: Committing on a fresh box before setting an identity —
git commit -m "first"— aborts:Author identity unknown *** Please tell me who you are. Run git config --global user.email "you@example.com" git config --global user.name "Your Name" to set your account's default identity. fatal: unable to auto-detect email address (got 'pushkar@host.(none)')Why: Every commit is stamped with an author name and email. With neither configured, Git tries to guess one from your username and hostname, can’t build a valid address, and refuses to record an anonymous commit.
Fix: Set them once, globally:
git config --global user.name "Pushkar"andgit config --global user.email "you@example.com"(step 1 of the lab). For a work laptop, set a per-repo email with the same command minus--globalinside the repo.How you’d spot it in prod: A CI job that commits (a bot bumping a version, say) fails at the commit step with this exact message because the runner is a clean container with no global config. The fix belongs in the pipeline — a
git configstep before the commit — not on anyone’s laptop.
Common error: Running
git commitwhile a merge conflict is still unresolved — beforegit add-ing the fixed files:error: Committing is not possible because you have unmerged files. hint: Fix them up in the work tree, and then use 'git add/rm <file>' hint: as appropriate to mark resolution and make a commit. fatal: Exiting because of an unresolved conflict.Why: After a conflict, Git holds the file in an “unmerged” state until you tell it the clash is settled. Editing the file is not enough — Git can’t know you are done until you
git addit, so it blocks the commit to stop you sealing a half-resolved merge.Fix: Edit each conflicted file, delete the
<<<<<<<,=======and>>>>>>>markers, thengit add <file>on each andgit commit.git statuslists exactly which files are still unmerged.How you’d spot it in prod: A deploy fails to build with a syntax error and the diff shows literal
<<<<<<< HEADlines in a committed file — someone left the markers in and committed anyway. Grep the repo for<<<<<<<before shipping; some teams add a pre-commit hook that rejects it.
Common error: Pushing a branch whose history you rebased after it was already shared —
git push— is rejected:! [rejected] main -> main (non-fast-forward) error: failed to push some refs to 'github.com:pushkar/site.git' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Integrate the remote changes (e.g. 'git pull ...') hint: before pushing again.Why: Rebasing rewrote your commits into new ones with new IDs, so your local history no longer descends from what the remote has. Git refuses the push because it is not a clean fast-forward — accepting it would silently discard the remote’s commits.
Fix: For your own unshared branch,
git pull --rebaseto replay onto the latest remote, then push. Never--forceonto a shared branch likemain; the safe rule is the one from the concept — rebase only local commits nobody has pulled yet.How you’d spot it in prod: A teammate reports their commits “vanished” after someone force-pushed a rebased branch. The tell is a
git push --forcein someone’s shell history against a shared branch — which is exactly why teams protectmainso force-pushes to it are blocked outright.
Git Interview Questions
Cover the answers below and say your own version out loud first — explain merge versus rebase, and how you resolve a conflict, before you reveal each answer. Recalling before revealing is what makes these stick when an interviewer asks them cold. The four questions and answers render right after this note.
Go Deeper
Optional extras if you have ~30 more minutes today:
- 5 min — Run
git help everyday— Git’s own “everyday commands” guide, grouped by task. Skim the “Individual Developer (Standalone)” set to see the core loop you just practised. - 10 min — Read chapter 3, “Git Branching”, of the free Pro Git book — the clearest explanation anywhere of branches as movable pointers.
- 15 min — On your lab repo, try the other path: make a new branch off
main, commit twice, thengit rebase maininstead of merging, and comparegit log --oneline --graphbefore and after to see the linear history rebase produces.
What is the difference between git merge and git rebase? Both
Both combine work from two branches, but they write different history. git merge takes the two branch tips and creates a new merge commit that ties them together — the original commits stay exactly where they were, so history honestly shows two lines of work that ran in parallel and joined. git rebase instead replays your commits one by one on top of the target branch, giving a straight, linear history as if you had branched from the latest code. The rule I follow: rebase your own local branch to tidy it before sharing, but never rebase commits you have already pushed and others have pulled, because rewriting shared history forces everyone else to untangle it.
What is a fast-forward merge? Both
A fast-forward merge happens when the branch you are merging into has not moved since you branched off it. Because there is no divergent work, Git does not need a merge commit — it simply slides the branch pointer forward to the tip of your branch, and the history stays perfectly linear, as if the commits were made directly on the target. If the target branch has moved on, a fast-forward is impossible and Git creates a real merge commit instead. In team workflows people often pass --no-ff to force a merge commit even when a fast-forward is possible, so the history records that a feature branch existed and when it landed.
How do you resolve a merge conflict? Both
A conflict happens when two branches change the same lines and Git can't decide which wins, so it stops and asks me. I run git status to see which files are conflicted, then open each one — Git marks the clash with <<<<<<<, ======= and >>>>>>> around the two versions. I edit the file to the correct final result, delete every marker, then git add the file to mark it resolved. When all files are staged I finish with git commit for a merge, or git rebase --continue if I was rebasing. The classic mistake is leaving a marker behind and shipping broken code, so I always read the file back before committing.
What belongs in a .gitignore, and why does it matter in DevOps? Product
A .gitignore lists path patterns Git should never track — build output, dependency folders like node_modules, log files, and anything secret such as .env files or private keys. It matters for two reasons. First, noise: committing generated files bloats the repo and fills every diff with churn nobody reviews. Second, and far more serious, security — a committed .env or cloud key is leaked the moment it is pushed, and scrubbing it from history afterward is painful and unreliable, so the credential has to be rotated anyway. My habit is to add .gitignore before the first commit, so secrets and build junk never enter history in the first place.
Mark Day 17 complete
You can branch and merge solo now — tomorrow you take it to a team with pull requests, reviews and tagged releases.
Stuck on today’s lab? Ask in Mission 90 Q&A