Git for Ops

Why This Matters

Your colleague made a change to the Nginx configuration last Thursday. It broke something subtle -- response headers are missing, and a downstream service started failing. Nobody remembers exactly what changed, when, or why. The config file has no comments about the modification. You are left diffing mental models and guessing.

Now imagine the same scenario, but the config is tracked in Git. You run git log and see:

commit a3f9e2b  Thu Feb 19 14:32:00 2026
Author: Priya <priya@ops.team>
    Remove X-Request-ID header to fix proxy buffering issue

There it is. You see exactly what changed, when, who did it, and why. You run git revert a3f9e2b, and the header is back. Problem solved in under two minutes.

Git is not just for software developers. For operations engineers, Git is the backbone of infrastructure as code, configuration management, change tracking, collaboration, and disaster recovery. If you manage servers and you are not using Git, you are flying blind.


Try This Right Now

# Install Git
# Debian/Ubuntu
sudo apt install git

# Fedora/RHEL
sudo dnf install git

# Arch
sudo pacman -S git

# Set your identity (Git needs to know who makes each commit)
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"

# Create a practice repository
mkdir ~/git-practice && cd ~/git-practice
git init
echo "Hello, Git!" > README.txt
git add README.txt
git commit -m "Initial commit"

# See what you just did
git log

You have just created a repository, tracked a file, and saved a snapshot of it. Every concept in this chapter builds on this foundation.


How Git Works: The Mental Model

Git is a distributed version control system. But what does that actually mean?

Think of Git as a series of snapshots. Every time you commit, Git takes a picture of all your tracked files at that moment and stores a reference to that snapshot. If a file has not changed, Git does not store it again -- it just links to the previous identical version.

+-----------------------------------------------------------+
|                    GIT MENTAL MODEL                       |
+-----------------------------------------------------------+
|                                                           |
|  Working       Staging        Repository                  |
|  Directory     Area (Index)   (.git/)                     |
|                                                           |
|  +----------+  +----------+  +------------------------+  |
|  |          |  |          |  |  commit 3 (HEAD)       |  |
|  | files    |  | files    |  |    |                    |  |
|  | you see  |--| ready to |--+  commit 2              |  |
|  | and edit |  | commit   |  |    |                    |  |
|  |          |  |          |  |  commit 1               |  |
|  +----------+  +----------+  +------------------------+  |
|                                                           |
|    git add ->    git commit ->                            |
|                                                           |
+-----------------------------------------------------------+

Working Directory -- The actual files on disk. This is what you see and edit.

Staging Area (Index) -- A holding area where you prepare changes for the next commit. You choose exactly which changes to include.

Repository (.git/) -- The database of all commits, branches, and history. This lives in the hidden .git/ directory.

Think About It: Why does Git have a staging area instead of just committing all changes? Consider a scenario where you fixed a bug AND reformatted a config file. The staging area lets you commit the bug fix separately from the formatting change, keeping the history clean and meaningful.


The Basic Git Workflow

Step 1: Check Status

Always start by understanding the current state:

git status

Output:

On branch main
nothing to commit, working tree clean

This tells you: you are on the main branch, and there are no uncommitted changes.

Step 2: Make Changes

# Create or edit files
echo "server_name=webserver01" > server.conf
echo "port=8080" >> server.conf
echo "log_level=info" >> server.conf

Check status again:

git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        server.conf

nothing added to commit but untracked files present

Git sees the new file but is not tracking it yet.

Step 3: Stage Changes

# Stage a specific file
git add server.conf

# Stage all changes (new, modified, deleted files)
git add .

# Stage only modified and deleted files (not new untracked files)
git add -u
git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   server.conf

The file is now staged (in the index), ready to be committed.

Step 4: Commit

git commit -m "Add initial server configuration"
[main a1b2c3d] Add initial server configuration
 1 file changed, 3 insertions(+)
 create mode 100644 server.conf

Commit messages matter. Write them as if you are explaining the change to a colleague who will read it at 3 AM during an outage. Good: "Fix log rotation to prevent disk full on /var/log". Bad: "update stuff".

Step 5: View History

git log
commit a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
Author: Your Name <your.email@example.com>
Date:   Sat Feb 21 10:00:00 2026 +0000

    Add initial server configuration

commit 1234567890abcdef1234567890abcdef12345678
Author: Your Name <your.email@example.com>
Date:   Sat Feb 21 09:50:00 2026 +0000

    Initial commit

Useful log variations:

# Compact one-line format
git log --oneline

# Show what changed in each commit
git log -p

# Show stats (files changed, insertions, deletions)
git log --stat

# Last 5 commits
git log -5

# Commits by a specific author
git log --author="Priya"

# Commits in the last week
git log --since="1 week ago"

# Search commit messages
git log --grep="nginx"

Viewing Changes with git diff

git diff is your X-ray vision into what changed.

# Edit the config file
echo "log_level=debug" >> server.conf

# See unstaged changes (working directory vs staging area)
git diff

# Stage the change
git add server.conf

# See staged changes (staging area vs last commit)
git diff --staged

# See changes between any two commits
git diff abc123..def456

# See changes in a specific file
git diff server.conf

# See just the names of changed files
git diff --name-only

Example output:

diff --git a/server.conf b/server.conf
index 1234567..abcdefg 100644
--- a/server.conf
+++ b/server.conf
@@ -1,3 +1,4 @@
 server_name=webserver01
 port=8080
 log_level=info
+log_level=debug

Lines starting with + are additions, - are deletions. The @@ line shows the location in the file.


Undoing Things

Everyone makes mistakes. Git gives you several levels of undo.

Unstage a File

# You staged something you did not mean to
git restore --staged server.conf

# Older syntax (still works)
git reset HEAD server.conf

Discard Working Directory Changes

# Discard changes to a specific file (revert to last committed version)
git restore server.conf

# Older syntax
git checkout -- server.conf

WARNING: git restore <file> permanently discards your uncommitted changes to that file. There is no undo for this operation. The changes are gone.

Amend the Last Commit

# Forgot to include a file, or want to fix the commit message
git add forgotten-file.conf
git commit --amend -m "Add initial server configuration with all files"

Revert a Commit

# Create a new commit that undoes a previous commit
# This is safe -- it preserves history
git revert a1b2c3d

This does not delete the original commit. It creates a new commit that applies the inverse of the original changes. This is the safe way to undo changes, especially on shared branches.


Branches: Parallel Timelines

Branches let you work on changes in isolation without affecting the main line. This is essential for ops work -- you can test a config change on a branch before applying it.

          main:    A---B---C
                            \
          feature:           D---E

Branch Commands

# List all branches
git branch

# Create a new branch
git branch feature/new-nginx-config

# Switch to a branch
git checkout feature/new-nginx-config

# Create and switch in one command
git checkout -b feature/new-nginx-config

# Modern syntax (Git 2.23+)
git switch -c feature/new-nginx-config

# Delete a branch (after merging)
git branch -d feature/new-nginx-config

# Force delete an unmerged branch
git branch -D feature/abandoned-experiment

Merging Branches

When your changes on a branch are ready, merge them back:

# Switch to the branch you want to merge INTO
git checkout main

# Merge the feature branch
git merge feature/new-nginx-config

If Git can cleanly combine the changes, it creates a merge commit automatically. If the same lines were changed in both branches, you get a merge conflict.

Resolving Merge Conflicts

When a conflict occurs, Git marks the conflicting sections in the file:

<<<<<<< HEAD
log_level=info
=======
log_level=debug
>>>>>>> feature/new-nginx-config

To resolve:

  1. Open the file and decide which version to keep (or combine them)
  2. Remove the conflict markers (<<<<<<<, =======, >>>>>>>)
  3. Stage the resolved file: git add server.conf
  4. Complete the merge: git commit

Think About It: Why would you use branches for ops work instead of just editing files directly on main? Consider code review, testing changes before applying them, and the ability to quickly revert if something goes wrong.


Working with Remote Repositories

So far, everything has been local. Remote repositories let you collaborate, back up your work, and deploy from a central location.

Cloning a Repository

# Clone an existing repository
git clone https://github.com/example/server-configs.git

# Clone into a specific directory
git clone https://github.com/example/server-configs.git my-configs

# Clone via SSH (requires SSH key setup)
git clone git@github.com:example/server-configs.git

Adding a Remote

If you started with git init locally:

# Add a remote (conventionally named "origin")
git remote add origin https://github.com/yourteam/server-configs.git

# List remotes
git remote -v

# Output:
# origin  https://github.com/yourteam/server-configs.git (fetch)
# origin  https://github.com/yourteam/server-configs.git (push)

Push, Pull, and Fetch

# Push your commits to the remote
git push origin main

# Push and set the upstream tracking (do this the first time)
git push -u origin main

# After -u, you can just use:
git push

# Fetch changes from the remote (does not modify your working directory)
git fetch origin

# Pull changes (fetch + merge)
git pull origin main

# After setting upstream:
git pull
+---------------------------------------------------------------+
|              PUSH AND PULL FLOW                               |
+---------------------------------------------------------------+
|                                                               |
|  Your Local Repo          Remote Repo (GitHub, GitLab, etc.)  |
|  +---------------+        +---------------+                   |
|  | Working Dir   |        |               |                   |
|  | Staging Area  | --push--> | main branch |                  |
|  | Local Commits | <-pull--- |             |                  |
|  +---------------+        +---------------+                   |
|                                                               |
+---------------------------------------------------------------+

Distro Note: Git is available on all major distributions. For SSH-based remote access, ensure openssh-client is installed. On minimal server images, you may need to install it separately: sudo apt install openssh-client (Debian/Ubuntu) or sudo dnf install openssh-clients (Fedora/RHEL).


.gitignore: Keeping Secrets Out

The .gitignore file tells Git which files to ignore. This is critical for ops -- you must never commit passwords, API keys, or sensitive data.

vim .gitignore
# Ignore editor backup files
*.swp
*.swo
*~

# Ignore OS files
.DS_Store
Thumbs.db

# Ignore secrets and credentials
*.key
*.pem
*.p12
.env
secrets.yml
credentials.conf

# Ignore logs
*.log
logs/

# Ignore compiled files
*.pyc
__pycache__/

# Ignore but track the template
!credentials.conf.example

The ! prefix negates a pattern -- useful for tracking a template file while ignoring the actual secrets file.

git add .gitignore
git commit -m "Add .gitignore to exclude secrets and temp files"

WARNING: .gitignore only prevents NEW files from being tracked. If you already committed a file with secrets and then add it to .gitignore, the secret is still in the Git history. To remove it, you need git rm --cached secrets.conf followed by a commit, and ideally a history rewrite with git filter-branch or git filter-repo. Better yet, rotate the compromised secret immediately.


Practical Ops Workflows

Workflow 1: Tracking Configuration Changes

Use Git to track changes to system configuration files:

# Initialize a repo for your configs
sudo mkdir /etc/myconfigs
sudo git -C /etc/myconfigs init

# Or track specific config files by copying them
mkdir ~/config-tracking && cd ~/config-tracking
git init

# Copy configs you want to track
cp /etc/nginx/nginx.conf .
cp /etc/ssh/sshd_config .
git add .
git commit -m "Baseline: current production configs"

# When you make changes, commit them with context
vim nginx.conf  # make your changes
git diff        # review what changed
git add nginx.conf
git commit -m "Increase worker_connections to 4096 for traffic spike

Ticket: OPS-1234
Approved by: Team Lead
Rollback: revert this commit"

Workflow 2: Managing Dotfiles

Track your personal configuration files across machines:

# Initialize a dotfiles repo
mkdir ~/dotfiles && cd ~/dotfiles
git init

# Copy your dotfiles
cp ~/.bashrc .
cp ~/.vimrc .
cp ~/.tmux.conf .
cp ~/.gitconfig .

git add .
git commit -m "Add dotfiles from workstation"

# Push to a remote
git remote add origin git@github.com:yourusername/dotfiles.git
git push -u origin main

# On a new machine, clone and create symlinks
git clone git@github.com:yourusername/dotfiles.git ~/dotfiles
ln -sf ~/dotfiles/.bashrc ~/.bashrc
ln -sf ~/dotfiles/.vimrc ~/.vimrc
ln -sf ~/dotfiles/.tmux.conf ~/.tmux.conf

Workflow 3: Collaborative Script Management

# Clone the team's scripts repo
git clone git@github.com:yourteam/ops-scripts.git
cd ops-scripts

# Create a branch for your new script
git checkout -b feature/disk-cleanup-script

# Write and test your script
vim disk-cleanup.sh
chmod +x disk-cleanup.sh
./disk-cleanup.sh --dry-run

# Commit and push
git add disk-cleanup.sh
git commit -m "Add disk cleanup script for /var/log rotation

- Removes logs older than 30 days
- Compresses logs older than 7 days
- Sends summary to syslog
- Supports --dry-run flag"

git push -u origin feature/disk-cleanup-script

# Open a pull request for team review (using GitHub CLI)
# gh pr create --title "Add disk cleanup script" --body "..."

Workflow 4: Emergency Rollback

# Something broke after a config change
# Find the commit that caused the issue
git log --oneline -10

# Output:
# a3f9e2b Fix proxy timeout settings
# b7c1d4e Update SSL cipher suite
# 9e2f3a1 Add rate limiting rules
# ...

# The proxy timeout change broke things -- revert it
git revert a3f9e2b

# Or, check out a specific file from a previous commit
git checkout 9e2f3a1 -- nginx.conf

# Review the reverted state
git diff HEAD~1
git status

# Commit if needed and deploy

Hands-On: Complete Git Exercise

Let us walk through a realistic scenario from start to finish:

# 1. Set up the repository
mkdir ~/server-configs && cd ~/server-configs
git init

# 2. Create initial configuration files
cat > nginx.conf << 'EOF'
worker_processes auto;
events {
    worker_connections 1024;
}
http {
    server {
        listen 80;
        server_name example.com;
        root /var/www/html;
    }
}
EOF

cat > .gitignore << 'EOF'
*.key
*.pem
.env
EOF

# 3. Initial commit
git add .
git commit -m "Initial server configuration"

# 4. Create a branch for SSL configuration
git checkout -b feature/add-ssl

# 5. Modify the config
cat > nginx.conf << 'EOF'
worker_processes auto;
events {
    worker_connections 1024;
}
http {
    server {
        listen 80;
        server_name example.com;
        return 301 https://$server_name$request_uri;
    }
    server {
        listen 443 ssl;
        server_name example.com;
        ssl_certificate /etc/ssl/certs/example.crt;
        ssl_certificate_key /etc/ssl/private/example.key;
        root /var/www/html;
    }
}
EOF

# 6. Review and commit
git diff
git add nginx.conf
git commit -m "Add SSL configuration with HTTP-to-HTTPS redirect"

# 7. Switch back to main and merge
git checkout main
git merge feature/add-ssl

# 8. View the complete history
git log --oneline --graph

# 9. Clean up the merged branch
git branch -d feature/add-ssl

# 10. Check the final state
git log --oneline
git status

Debug This

You are trying to push your changes and get this error:

! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'origin'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally.

What is happening? Someone else pushed commits to the remote main branch since your last pull. Your local branch has diverged from the remote.

Solution:

# Fetch the latest changes
git fetch origin

# Merge them into your branch
git merge origin/main

# Or, combine fetch and merge:
git pull origin main

# Resolve any conflicts if needed, then push
git push origin main

Alternatively, use rebase to keep a linear history:

git pull --rebase origin main
git push origin main

Essential Git Configuration

Here are some settings that improve the Git experience:

# Set your identity
git config --global user.name "Your Name"
git config --global user.email "you@example.com"

# Set default branch name to 'main'
git config --global init.defaultBranch main

# Use colors in output
git config --global color.ui auto

# Set default editor
git config --global core.editor vim

# Useful aliases
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.lg "log --oneline --graph --all --decorate"
git config --global alias.last "log -1 HEAD"
git config --global alias.unstage "restore --staged"

# View your config
git config --list

What Just Happened?

+------------------------------------------------------------------+
|                        CHAPTER RECAP                              |
+------------------------------------------------------------------+
|                                                                   |
|  Git tracks SNAPSHOTS of your files, not just differences.       |
|                                                                   |
|  Three areas: Working Dir -> Staging Area -> Repository          |
|    git add    = stage changes                                    |
|    git commit = save snapshot                                    |
|    git push   = share with remote                                |
|                                                                   |
|  git status  = see current state                                 |
|  git log     = see history                                       |
|  git diff    = see changes                                       |
|                                                                   |
|  Branches let you work on changes in ISOLATION.                  |
|    git checkout -b feature -> work -> git merge                  |
|                                                                   |
|  For ops: track configs, manage dotfiles, collaborate on         |
|  scripts, and maintain an audit trail of every change.           |
|                                                                   |
|  NEVER commit secrets. Use .gitignore proactively.               |
|                                                                   |
+------------------------------------------------------------------+

Try This

  1. Config Tracking: Initialize a Git repository and track copies of three system config files (e.g., /etc/hostname, /etc/hosts, /etc/resolv.conf). Make changes, commit them with meaningful messages, and use git log -p to review the history.

  2. Branch Workflow: Create a branch called feature/new-firewall-rules. Add a file with sample iptables rules. Commit it. Switch back to main, make a different change, commit. Then merge the feature branch. Resolve any conflicts.

  3. Dotfiles Repository: Set up a dotfiles repo with your .bashrc, .vimrc, and .tmux.conf. Push it to a remote (GitHub, GitLab, or a self-hosted Git server). Clone it on a different machine (or in a different directory) and create symlinks.

  4. Revert Practice: Make three commits in a row. Then use git revert to undo the middle commit. Verify that the first and third commit's changes are preserved.

  5. Bonus Challenge: Write a shell script that automates daily backups of critical config files into a Git repository. It should: copy the files, commit with a timestamp message, and push to a remote. Set it up as a cron job (see Chapter 24).