Skip to content

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 again history, 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; HEAD is your current position. This one fact explains reset, checkout, and revert.
  • Merging is fast-forward (pointer moves), a two-parent merge commit (--no-ff forces it), or a squash (many commits → one, branch link lost).
  • Resolve conflicts by editing out the <<<<<<< / ======= / >>>>>>> markers, then git add and commit.
  • cherry-pick backports a single commit; reset --hard permanently discards uncommitted work.
  • reset rewrites history (use it locally); revert preserves history (use it on shared commits). Same for rebase — never rewrite what others have pulled.
  • A .gitignore keeps secrets and clutter out of the repo, but only for files not already committed.