Education › DevOps › Stage 1: Foundations

Git & collaborative workflow

Branching, rebasing, pull requests, and trunk-based development.

Beginner ~30 min read Module 2 of 17

Git is where every change to your application and your infrastructure begins. Pipelines trigger on it, GitOps controllers deploy from it, and during an incident the first question is usually "what changed?", which Git answers. This module moves you past memorised commands to a working model of commits, branches and remotes, so that rebasing, resolving conflicts and recovering from mistakes stop being frightening.

After this module you can
  • Explain commits, branches and HEAD as a graph of snapshots with movable labels
  • Use a short-lived feature branch and pull request workflow from start to merge
  • Choose between merge and rebase, and resolve a conflict in either one
  • Undo mistakes safely with restore, revert, reset and reflog
  • Describe trunk-based development and why delivery pipelines favour it

The model: snapshots and labels

A commit is a snapshot of your whole project plus a message, an author, and a pointer to its parent commit. Commits are identified by a hash and never change; any command that appears to edit a commit actually creates a new one. Follow the parent pointers and you get the history: a graph.

A branch is nothing more than a label pointing at one commit. Creating a branch copies no files; it writes one tiny reference file. When you commit, the label of the branch you are on moves forward to the new commit. HEAD is the label that says "you are here", and it normally points at a branch.

Between your files and the history sits the staging area (the index). git add copies a change into the staging area, and git commit turns whatever is staged into a snapshot. That extra step is what lets you split a messy afternoon of edits into several clean, reviewable commits.

bash
git status                      # what is modified, staged, untracked
git diff                        # unstaged changes
git diff --staged               # what the next commit will contain
git add -p                      # stage selected hunks interactively
git commit -m "Add health endpoint"
git log --oneline --graph --all # the commit graph, one line per commit
Tip

When Git confuses you, run git log --oneline --graph --all. Nearly every command is an operation on that picture: adding a commit, moving a label, or copying commits from one place to another.

Remotes and the feature-branch workflow

A remote is another copy of the repository, conventionally named origin. Your clone keeps read-only bookmarks such as origin/main that record where the remote's branches were the last time you talked to it. git fetch updates those bookmarks and changes nothing else. git pull is a fetch followed by a merge (or a rebase, if configured) into your current branch.

YOUR MACHINEgit addgit commitgit pushgit fetchWorking diryour editsStaging areathe next commitLocal repo.git, all historyoriginshared remote
Where a change lives at each step: edits sit in the working directory, `git add` copies them to the staging area, `git commit` snapshots them into your local repository, and only `git push` sends them to the remote.

The everyday team workflow is: branch off the latest main, commit, push the branch, open a pull request (PR), let CI and a reviewer check it, merge, delete the branch.

bash
git switch main
git pull                                  # start from the latest main
git switch -c feature/health-endpoint     # create the branch and move onto it
# ...edit, git add, git commit...
git push -u origin feature/health-endpoint   # -u remembers the upstream
# open the pull request in your Git host, get review, merge
git switch main
git pull
git branch -d feature/health-endpoint     # delete the merged local branch

A good PR is small, does one thing, and has a description that says why. Small PRs are reviewed faster and more carefully, conflict less, and are trivial to revert. That is not politeness; it is the biggest lever you have on delivery speed, as you will see in the DORA metrics module.

Merge or rebase

While you worked, main moved on. There are two ways to combine the histories. A merge creates a new commit with two parents that joins the lines of work. Nothing existing is altered, so it is always safe, but the history shows every join. If main has not moved at all, Git simply slides the label forward, which is called a fast-forward.

A rebase takes your commits and replays them, one by one, on top of the new main. The result is a straight line that reads as if you had started today. Because commits are immutable, the replayed commits are new commits with new hashes; the originals are abandoned.

bash
# bring your feature branch up to date by rebasing
git fetch origin
git rebase origin/main

# tidy your own commits before review: squash, reorder, reword
git rebase -i origin/main

# you already pushed this branch, so the remote must accept rewritten history
git push --force-with-lease
Watch out

The golden rule: never rebase commits that other people have based work on, which in practice means never rebase a shared branch such as main. Rebasing your own feature branch is fine. When you must force-push, use --force-with-lease, which refuses if someone else pushed to the branch since your last fetch. Plain --force silently destroys their work.

Most teams rebase or squash locally and let the Git host perform the final merge. "Squash and merge" collapses a PR into one commit on main, which keeps main readable and makes each change a single revertable unit.

Resolving conflicts

A conflict happens when both sides changed the same lines and Git will not guess. It pauses, and marks the file:

text
<<<<<<< HEAD
replicas: 3
=======
replicas: 5
>>>>>>> feature/scale-up

The part above ======= is the branch you are on; the part below is what is coming in. Edit the file to the correct final content, which may be one side, the other, or a combination, and delete all three marker lines. Then tell Git you are finished.

bash
git status                  # lists the files still in conflict
# ...edit each file, remove the markers...
git add deploy/values.yaml
git merge --continue        # or: git rebase --continue

git merge --abort           # changed your mind? go back to before the merge
git rebase --abort
Note

During a rebase the meaning of the two sides is swapped: HEAD is the branch you are replaying onto, and the lower part is your own commit. Read the content, not the labels. A rebase can also stop several times, once for each of your commits that conflicts.

Undoing things safely

Which undo you need depends on how far the mistake travelled.

SituationCommandEffect
Bad edit, not stagedgit restore FILEDiscards the working-copy change. Not recoverable.
Staged by accidentgit restore --staged FILEUnstages it; your edit is kept.
Typo in the last commit, not pushedgit commit --amendReplaces the last commit with a corrected one.
Bad commits, not pushedgit reset --soft HEAD~1Moves the branch back one commit; changes stay staged.
Bad commit already on maingit revert HASHAdds a new commit that applies the inverse. History is preserved.

The dividing line is whether the commit has been shared. amend, reset and rebase rewrite history, so keep them for commits only you have. For anything on a shared branch use git revert, which is also how you roll back a bad production change without disturbing anyone else.

git reset --hard also throws away your working-copy changes, so use it deliberately. If you do lose commits, git reflog is the safety net: it lists every position HEAD has been at recently, including commits no branch points to any more. Find the hash, then git switch -c rescue HASH.

Watch out

A secret that was committed and pushed is compromised, even if you delete it in the next commit, because it is still in the history and in every clone. Rotate the credential first. Cleaning the history comes second.

Trunk-based development

Branching strategies differ mainly in how long work stays apart. In long-lived-branch models, changes sit on develop or release branches for weeks; merges become large, conflicts are painful, and nobody is sure what is deployable.

In trunk-based development everyone integrates into one branch (main, the trunk) at least daily, through branches that live for hours or a day or two. main is always releasable because every merge passed CI. Work that is not finished is merged anyway, hidden behind a feature flag, so integrating code is decoupled from releasing a feature.

  • Small, frequent merges make conflicts small and rare.
  • CI runs against what will really ship, not against a branch that drifted for a month.
  • Releases are cut from main with a tag such as v1.4.0, not from a long-lived branch.
  • Protect main: require a PR, passing checks, and at least one review.
bash
git tag -a v1.4.0 -m "Release 1.4.0"   # annotated tag on the current commit
git push origin v1.4.0                  # tags are not pushed by default
git describe --tags                     # nearest tag, useful for build versions
Hands-on practice

Collide with yourself, then recover

  1. Create a repository on your Git host, clone it, and commit a small config.yaml with a line replicas: 3 on main.
  2. Create feature/a and change the line to replicas: 5. Switch back to main and change the same line to replicas: 4. Commit both.
  3. On feature/a, run git rebase main, resolve the conflict by hand, and continue. Inspect the result with git log --oneline --graph --all.
  4. Push the branch, open a pull request, and merge it with "squash and merge". Pull main and confirm it gained exactly one commit.
  5. Make a bad commit on main, push it, and undo it properly with git revert. Confirm the history still contains both commits.
  6. Create a commit on a scratch branch, delete the branch with git branch -D, then recover the commit using git reflog.
  7. Turn on branch protection for main in your Git host so that direct pushes are rejected, and verify that a direct push now fails.
Cheat sheet

Git & collaborative workflow — at a glance

Main things to focus on

  • A commit is an immutable snapshot with a parent; a branch is a movable label; HEAD is where you are.
  • fetch only updates your view of the remote. pull is fetch plus merge or rebase.
  • Merge preserves history and is always safe. Rebase rewrites your commits into a straight line.
  • Never rewrite shared history. On shared branches undo with git revert, not reset.
  • --force-with-lease, never plain --force.
  • Small, short-lived branches merged to an always-releasable main beat long-lived branches.
  • git reflog can recover almost anything that was ever committed.

Everyday

git statusModified, staged and untracked files
git add -pStage changes hunk by hunk
git diff --stagedExactly what the next commit will contain
git commit -m "MESSAGE"Snapshot the staged changes
git log --oneline --graph --allThe commit graph with every branch
git stash / git stash popShelve uncommitted work, then bring it back

Branches and remotes

git switch -c NAMECreate a branch and move onto it
git switch NAMEMove to an existing branch
git fetch originUpdate origin/* bookmarks; touches nothing else
git pull --rebaseFetch, then replay your local commits on top
git push -u origin NAMEPublish a branch and remember its upstream
git branch -d NAMEDelete a merged branch (-D forces)

Combine histories

git merge NAMEJoin NAME into the current branch
git rebase origin/mainReplay your commits on top of the latest main
git rebase -i origin/mainSquash, reorder or reword your own commits
git merge --abort / git rebase --abortBack out of a conflicted operation
git cherry-pick HASHCopy one commit onto the current branch
git push --force-with-leaseOverwrite your remote branch only if nobody else pushed

Undo

git restore FILEThrow away an unstaged edit (permanent)
git restore --staged FILEUnstage, keep the edit
git commit --amendReplace the last, unpushed commit
git reset --soft HEAD~1Undo the last commit, keep its changes staged
git revert HASHNew commit that reverses HASH; safe on shared branches
git reflogEvery recent position of HEAD, for recovering lost commits

Investigate what changed

git log -p -- PATHHistory of one file, with diffs
git blame FILEWhich commit last touched each line
git diff v1.3.0..v1.4.0Everything that changed between two releases
git bisect start / good / badBinary-search history for the commit that broke something
git tag -a v1.4.0 -m "MSG"Mark a release; push it with git push origin v1.4.0

Common pitfalls

  • Rebasing or force-pushing a branch other people are working on, which orphans their commits.
  • Using git reset to undo a commit that is already on main instead of git revert.
  • Believing that deleting a committed secret in a later commit makes it safe. Rotate it.
  • Letting a feature branch live for weeks, then losing a day to one enormous merge conflict.
  • Running git pull with uncommitted work and a diverged branch, then fighting a surprise merge; fetch and look first.
  • Committing build output, .env files or dependencies because .gitignore was never set up.
Quiz

Check your understanding

5 questions · 4 to pass · answers are explained as you go. Your best score is saved on this device only.

Progress and quiz scores are saved in this browser only. Back up or restore on the hub.

Was this lesson useful? Tell me what to improve →