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.
- Explain commits, branches and
HEADas 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,resetandreflog - 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.
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 commitWhen 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.
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.
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 branchA 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.
# 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-leaseThe 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:
<<<<<<< HEAD
replicas: 3
=======
replicas: 5
>>>>>>> feature/scale-upThe 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.
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 --abortDuring 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.
| Situation | Command | Effect |
|---|---|---|
| Bad edit, not staged | git restore FILE | Discards the working-copy change. Not recoverable. |
| Staged by accident | git restore --staged FILE | Unstages it; your edit is kept. |
| Typo in the last commit, not pushed | git commit --amend | Replaces the last commit with a corrected one. |
| Bad commits, not pushed | git reset --soft HEAD~1 | Moves the branch back one commit; changes stay staged. |
Bad commit already on main | git revert HASH | Adds 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.
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
mainwith a tag such asv1.4.0, not from a long-lived branch. - Protect
main: require a PR, passing checks, and at least one review.
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