Version control with Git for network automation¶
Once your network's intended state lives in code — YAML data, Jinja templates, Python scripts, Terraform files — Git becomes the system of record for that code. This article covers the mental model and the handful of operations that matter most in practice: branching and merging, resolving conflicts, cherry-picking a fix, and the three different ways to undo a change without breaking your teammates.
Git is the distributed version-control system that underpins virtually all modern automation work. It records the history of your files as a series of commits, lets multiple engineers work in parallel on branches, and lets you merge, undo, and reorganize that history precisely.
The mental model: three areas and a pointer¶
Every change flows through three areas:
| Area | What it holds | How you move things in |
|---|---|---|
| Working directory | Your files on disk | Edit files |
| Staging area (index) | Changes marked for the next commit | git add |
| Repository | Committed history | git commit |
Two ideas explain almost everything else. A branch is just a movable pointer to a
commit — it is not a copy of your files. HEAD points to the branch you are
currently on. Once you internalize that branches are pointers, the behavior of reset,
checkout, and revert stops being mysterious: they are all, in different ways, about
where that pointer sits.
The everyday cycle:
git status # what changed, what is staged
git add config.yml # stage a file
git commit -m "Add VLAN 10 to access layer"
git switch -c feature/ospf # create + switch to a new branch
git push -u origin feature/ospf # publish it
Modern Git (since 2.23)
splits the old, overloaded git checkout into two focused commands:
git switch for branches and
git restore for files. They are clearer and
harder to misuse, so prefer them for new work; checkout still works and you will see it
everywhere.
Merging: fast-forward, merge commit, or squash¶
Merging brings work from one branch into another, and Git does it in one of three ways:
- Fast-forward — if the target branch has not moved since the feature branch was created, Git just advances the pointer. No new commit.
- Merge commit — if both branches have new commits, Git performs a three-way merge
and creates a commit with two parents, preserving both histories. Force this with
--no-ff. - Squash — collapse every commit on the feature branch into a single set of changes
that you commit once. You get one tidy commit instead of a noisy
wip / fix typo / try againhistory, but the branch link is not recorded.
git switch main
git merge feature/ospf # fast-forward if possible, else a merge commit
git merge --no-ff feature/ospf # always create a merge commit
git merge --squash feature/ospf # stage the combined changes, does NOT commit
git commit -m "Add OSPF area 0 to the core"
GitLab and GitHub merge requests can be configured to use any of the three; squash-on-merge
is a common default because it keeps main reading as one logical change per feature.
Resolving a conflict¶
A conflict occurs when the same lines were changed differently on both branches and Git
cannot decide automatically. It pauses the merge and writes conflict markers into the
affected file — HEAD (your current branch) above, the incoming branch below:
<<<<<<< HEAD
ntp server 10.0.0.1
=======
ntp server 192.0.2.53
>>>>>>> feature/ospf
Edit the file to the correct final content, delete all three marker lines, stage it, and finish the merge:
# after editing config.yml to the desired final content:
git add config.yml # mark the conflict resolved
git commit # or: git merge --continue
git merge --abort # bail out and return to the pre-merge state
cherry-pick: copy one commit¶
git cherry-pick applies the changes from one specific commit onto your current branch as
a new commit. The classic use case is backporting: a fix landed on main, and you need
just that one fix on a release branch without dragging everything else along.
git switch release/24.1
git cherry-pick 9f3c1ab # apply just that commit here
git cherry-pick a1b2c3d e4f5a6b # pick several, in order
git cherry-pick --continue # after resolving a conflict (or --abort)
The three ways to undo¶
This is where discipline pays off. reset, checkout, and revert all "undo" something,
but they differ in whether they rewrite history — and rewriting shared history is the
single most disruptive thing you can do to a team.
git reset moves the branch pointer. It has three modes that differ in how far the
change reaches:
git reset --soft HEAD~1 # undo last commit, keep changes staged
git reset HEAD~1 # (--mixed, default) undo commit, keep changes unstaged
git reset --hard HEAD~1 # undo commit AND discard the changes — permanent
git reset HEAD config.yml # unstage a single file
Because reset moves the pointer, it rewrites history. That is fine on a local,
unshared branch, but dangerous on a branch others have already pulled — their history
diverges from yours and only a force-push (and apologies) reconciles it. --hard also
permanently discards uncommitted work, so treat it with care.
git revert creates a new commit that applies the inverse of a previous one. Nothing
is rewritten — the original commit stays in the log, followed by the revert. This is the
safe way to undo a change that has already been pushed and shared.
git revert 9f3c1ab # new commit that undoes 9f3c1ab
git revert HEAD # safely undo the most recent commit
git revert --no-commit 9f3c1ab a1b2c3d # stage multiple reverts, commit once
The rule Atlassian and most teams follow: reset for local, unpublished commits; revert for anything already shared. If a teammate has pulled the commit, revert it — do not reset.
git checkout (or git switch) can put you on a specific commit, which lands you in
detached HEAD — HEAD points at a commit rather than a branch, and any new commits you
make belong to no branch and are easily lost. If you want to keep work started there,
create a branch first:
git switch -c fixup 9f3c1ab # start a real branch at that commit
Rebase vs. merge¶
Both merge and rebase integrate one branch into another, but they produce different
histories. Merge preserves exactly what happened, adding a merge commit that ties the two
histories together. Rebase replays your branch's commits on top of the latest target,
producing a single straight line with no merge commit — cleaner to read, at the cost of
new commit hashes.
The practical rule: rebase local, private work to tidy it before sharing; never rebase commits others have already pulled. Rewriting shared history forces everyone else into painful conflicts. Rebase before you push; merge after.
Keep secrets out with .gitignore¶
A .gitignore file lists paths Git should never
track — build artifacts, virtual environments, and, critically, anything containing
secrets. It is the first line of defense against the most common Git accident in
automation: committing a credentials file or a .env. Once a path is ignored it will not
show up in git status, so it cannot be staged by mistake.
# secrets — never commit
.env
*.pem
group_vars/*/vault.yml
# tooling clutter
__pycache__/
.venv/
*.retry
Ignoring a file only prevents future tracking. If a secret was already committed, it
lives in history — rotate the credential and scrub it with a tool like
git filter-repo.
Key takeaways¶
- A branch is just a pointer to a commit;
HEADis your current position. This one fact explains reset, checkout, and revert. - Merging is fast-forward (pointer moves), a two-parent merge commit (
--no-ffforces it), or a squash (many commits → one, branch link lost). - Resolve conflicts by editing out the
<<<<<<</=======/>>>>>>>markers, thengit addand commit. cherry-pickbackports a single commit;reset --hardpermanently discards uncommitted work.resetrewrites history (use it locally);revertpreserves history (use it on shared commits). Same for rebase — never rewrite what others have pulled.- A
.gitignorekeeps secrets and clutter out of the repo, but only for files not already committed.