Git and Linux Daily Workflow for Backend Engineers
A practical guide to Git branching, rebasing, conflict resolution, and the Linux CLI toolkit backend engineers use daily.
Git and Linux Daily Workflow
Every backend engineer spends hours a day inside two tools that rarely get formal training: Git and a Linux shell. You learn them by osmosis, copy-pasting commands until something works. That's fine until a rebase goes sideways in front of the team, or production is on fire and you're fumbling for the right grep flag. This guide covers both tools the way you actually need them in a job — not as academic topics, but as the daily instruments of shipping and debugging backend services.
1. Git's Mental Model
Before branching strategies make sense, you need the model underneath them. Git is not a list of file versions — it's a content-addressable graph of immutable snapshots.
Every commit is a snapshot (not a diff) that points to a tree of file contents and to its parent commit(s). Branches are just movable pointers (refs/heads/<name>) to a commit. HEAD is a pointer to "where you currently are" — usually pointing at a branch, which points at a commit.
Why this matters: Once you internalize that a branch is a 41-byte pointer, not a container of commits, operations like rebase, cherry-pick, and reset stop feeling magical. They're all just graph surgery — moving pointers and replaying commits.
The four object types
| Object | Contains | Analogy |
|---|---|---|
| Blob | Raw file content | A file's bytes |
| Tree | Filenames + modes + pointers to blobs/trees | A directory listing |
| Commit | Tree pointer + parent(s) + author + message | A snapshot + metadata |
| Tag | Pointer to a commit + message | A named release marker |
# Inspect the object graph directly
git cat-file -p HEAD # show the commit object
git cat-file -p HEAD^{tree} # show the tree it points to
git log --oneline --graph --all # visualize the commit graph2. Branching Strategies
The strategy your team picks determines how often you rebase, how painful releases are, and how much CI you burn. There is no universally "correct" strategy — it depends on release cadence and team size.
| Strategy | Model | Best for | Trade-off |
|---|---|---|---|
| Trunk-based development | Everyone commits small, frequent changes to main, behind feature flags | High-velocity teams, continuous deployment | Requires strong CI, feature flags, discipline |
| GitHub Flow | main is always deployable; feature branches → PR → merge → deploy | Most SaaS teams, weekly/daily releases | Simple, but less structure for scheduled releases |
| Git Flow | main, develop, feature/*, release/*, hotfix/* | Products with scheduled release trains, versioned software | Heavyweight; slows down continuous delivery |
| Release branches | Cut a release/2.4 branch, cherry-pick fixes onto it | Enterprise software, multiple supported versions | Backporting overhead |
Default recommendation for a typical backend service: GitHub Flow with short-lived feature branches (1-3 days max), mandatory PR review, and CI gating merges to main. Reserve Git Flow for products that genuinely ship discrete numbered releases (e.g., a mobile app with app-store review cycles).
Trunk-based development in practice
# Short-lived branch, rebased daily onto main
git checkout -b feat/rate-limit-header
# ... work, commit small increments ...
git fetch origin
git rebase origin/main # stay current, avoid a giant merge later
git push --force-with-lease # safe force-push after rebaseNever git push --force on a shared branch — it overwrites remote history unconditionally, silently discarding a teammate's commits if they pushed while you were rebasing. Always use --force-with-lease, which fails safely if the remote moved since your last fetch.
3. Rebasing vs Merging
This is the single most argued-about Git topic on every team, and both sides are right in different contexts.
| Aspect | git merge | git rebase |
|---|---|---|
| History shape | Preserves true chronology, creates merge commits | Linear, rewrites commit history |
| Commit hashes | Original commits unchanged | New commit hashes (SHAs) for every replayed commit |
| Safe on shared branches? | Always | Only on branches nobody else has pulled |
Debugging with git bisect | Noisier (merge commits can hide the breaking commit) | Cleaner — every commit builds independently |
| Conflict resolution | Resolve once, in the merge commit | May need to resolve the same conflict multiple times (once per replayed commit) |
| Team convention | Common for merging PRs into main (via "squash and merge" in practice) | Common for keeping a feature branch current with main before opening a PR |
The cardinal rule of rebasing: never rebase a branch that other people have already pulled or branched from. Rebase rewrites SHAs; anyone with the old history now has a diverged, unmergeable copy. Rebase your own local/feature branches freely. Merge (or squash-merge) into shared branches like main.
# Keep your feature branch current — rebase onto latest main
git checkout feat/rate-limit-header
git fetch origin
git rebase origin/main
# Squash messy WIP commits before opening a PR
git rebase -i HEAD~5
# In the editor: mark commits as 'squash' or 'fixup', keep one clean commit
# Merging a finished feature into main (typical PR merge)
git checkout main
git merge --no-ff feat/rate-limit-header # explicit merge commit, preserves branch context4. Resolving Merge Conflicts
Conflicts happen when Git can't automatically reconcile changes to the same lines. Resolving them well — quickly and correctly — is a core professional skill, not a scary edge case.
$ git merge origin/main
Auto-merging src/main/java/com/acme/order/OrderService.java
CONFLICT (content): Merge conflict in src/main/java/com/acme/order/OrderService.java
Automatic merge failed; fix conflicts and then commit the result.A conflicted file contains conflict markers:
public void cancelOrder(String orderId) {
<<<<<<< HEAD
Order order = repository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.markCancelled();
=======
Order order = repository.findByIdOrThrow(orderId);
order.transitionTo(OrderStatus.CANCELLED);
auditLog.record(orderId, "CANCELLED");
>>>>>>> origin/main
repository.save(order);
}Resolution workflow
- Read both sides carefully — understand why each version made its change, not just what changed.
- Decide the correct combined logic — often it's not "pick one side," it's merging intent from both.
- Remove all conflict markers (
<<<<<<<,=======,>>>>>>>) — a leftover marker is a silent, catastrophic bug. - Rebuild and run tests locally before committing the resolution.
- Stage and continue:
git add src/main/java/com/acme/order/OrderService.java
git rebase --continue # if resolving during a rebase
# or
git commit # if resolving during a mergeUse a three-way merge tool for anything non-trivial: git mergetool (configured with kdiff3, meld, or your IDE's merge view). Reading raw conflict markers for a 40-line conflict is error-prone — a visual tool shows base/ours/theirs side by side.
Aborting when it's not worth it
git merge --abort # bail out of a merge, return to pre-merge state
git rebase --abort # bail out of a rebase, return to pre-rebase stateIf you're resolving the same conflict repeatedly across many commits during a rebase, consider merging instead — you resolve it once. Or use git rerere (reuse recorded resolution), which remembers how you resolved a conflict and auto-applies the same resolution next time it recurs.
5. Undoing Things Safely
Backend engineers need a reliable mental model for "I made a mistake, get me back."
| Situation | Command | Effect |
|---|---|---|
| Unstage a file | git restore --staged <file> | Keeps changes, removes from staging |
| Discard local uncommitted changes | git restore <file> | Destructive — changes are gone |
| Undo the last commit, keep changes | git reset --soft HEAD~1 | Commit undone, changes staged |
| Undo the last commit, unstage changes | git reset --mixed HEAD~1 (default) | Commit undone, changes in working dir |
| Undo the last commit, discard changes | git reset --hard HEAD~1 | Destructive — commit and changes gone |
| Undo a commit already pushed/shared | git revert <sha> | Creates a new commit that undoes it — safe for shared history |
| Recover a "lost" commit | git reflog then git reset --hard <sha> | Reflog tracks every HEAD movement for ~90 days |
git reset --hard on a shared branch is one of the most common causes of lost production hotfixes. If the branch is pushed and others may have it, use git revert instead — it's additive and never rewrites history other people depend on.
# The escape hatch: reflog remembers everything, even after a bad reset --hard
git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~1
# f4e5d6c HEAD@{1}: commit: add idempotency key to payment endpoint ← I want this back
git reset --hard f4e5d6c6. The Linux Daily Toolkit
Backend engineers live in a shell — SSH'd into a box, inside a container, or in CI logs. These are the commands you reach for dozens of times a day.
Searching: grep and find
# grep: search file contents
grep -rn "OrderNotFoundException" src/ # recursive, show line numbers
grep -rn --include="*.java" "TODO" src/ # only .java files
grep -A 3 -B 1 "ERROR" application.log # 3 lines after, 1 before match
grep -c "GET /health" access.log # count matches
grep -v "DEBUG" app.log | grep "payment-service" # exclude, then filter
# find: search the filesystem
find . -name "*.yml" -mtime -1 # yaml files modified in last 24h
find . -type f -size +100M # files over 100MB (log bloat)
find /var/log -name "*.log" -mtime +30 -delete # cleanup old logs (careful!)
find . -name "*.class" -exec rm {} \; # delete build artifactsPrefer rg (ripgrep) over grep on developer machines — it respects .gitignore, is dramatically faster on large repos, and defaults to recursive search. grep remains the safer universal choice for production boxes where you can't install tools.
Process and resource inspection
ps aux | grep java # find the running JVM process
ps -ef --forest # process tree view
top # live CPU/memory (or htop if installed)
lsof -i :8080 # what's holding port 8080
lsof -p <pid> # open files/sockets for a process
kill -15 <pid> # SIGTERM — graceful shutdown
kill -9 <pid> # SIGKILL — force kill (last resort)
df -h # disk space by filesystem
du -sh */ | sort -rh | head -10 # largest directories
free -h # memory usagekill -9 skips cleanup. A JVM killed with SIGKILL doesn't run shutdown hooks — in-flight transactions, open file handles, and connection pool cleanup are abandoned. Always try SIGTERM (kill -15, or plain kill) first and give the process time to shut down gracefully.
Logs and live debugging
tail -f /var/log/app/service.log # follow a log file live
tail -f service.log | grep --line-buffered ERROR # live-filter for errors
journalctl -u myapp.service -f # follow systemd-managed service logs
journalctl -u myapp.service --since "10 min ago" # recent logs for a unit
less +F application.log # tail-like mode inside less (Ctrl+C, then F to resume)Networking and HTTP from the CLI
curl -s https://api.internal/health | jq . # hit an endpoint, pretty-print JSON
curl -i -X POST https://api.internal/orders \
-H "Content-Type: application/json" \
-d '{"customerId":"c1","items":[]}' # inspect headers + body
curl -w "@curl-format.txt" -o /dev/null -s https://api.internal/orders # timing breakdown
ss -tulnp # listening ports (modern replacement for netstat)
dig api.internal +short # DNS resolution
nc -zv db.internal 3306 # test TCP connectivity to a portPermissions and ownership
chmod 640 application-secrets.yml # owner rw, group r, others nothing
chmod +x deploy.sh # make a script executable
chown appuser:appgroup /var/app # change owner and group
umask 027 # default permissions for newly created files| Permission digit | Meaning | Common backend use |
|---|---|---|
755 | rwxr-xr-x | Executable scripts, directories |
644 | rw-r--r-- | Regular config/code files |
640 | rw-r----- | Secrets files (owner + group only) |
600 | rw------- | Private keys, credentials |
chmod 777 is never the right answer in production. It's a common "quick fix" for permission errors that actually opens the file/directory to write access from any user on the system. Diagnose the actual owner/group mismatch instead — usually chown to the correct service account is the real fix.
systemd and service management
Most Linux backend deployments outside Kubernetes run as systemd units.
systemctl status myapp.service # is it running? recent log tail included
systemctl start myapp.service
systemctl stop myapp.service
systemctl restart myapp.service
systemctl enable myapp.service # start automatically on boot
sudo systemctl daemon-reload # reload unit files after editing them
journalctl -u myapp.service -n 100 --no-pager # last 100 log lines# /etc/systemd/system/myapp.service — a minimal Spring Boot service unit
[Unit]
Description=Order Service
After=network.target
[Service]
User=appuser
WorkingDirectory=/opt/order-service
ExecStart=/usr/bin/java -jar /opt/order-service/app.jar
SuccessExitStatus=143
Restart=on-failure
RestartSec=5
Environment=SPRING_PROFILES_ACTIVE=production
[Install]
WantedBy=multi-user.target7. A Realistic Debugging Session
Here's how these tools compose when production is unhealthy — the actual sequence, not a curated demo.
Key takeaways
- Git branches are pointers, not containers — internalizing the object graph model demystifies every "scary" Git operation.
- Rebase your own unshared branches to stay current; merge or squash-merge into shared branches. Never rebase history others depend on.
- Always use
git push --force-with-lease, never bare--force, when you must overwrite a remote branch. git revertis the safe undo for anything already shared;git reset --hardis only safe on purely local, unpushed work.git reflogis your safety net — almost nothing in Git is truly unrecoverable within its retention window.- Prefer
SIGTERMoverSIGKILLso the JVM gets a chance to run shutdown hooks and drain connections cleanly. grep,find,tail -f,curl, andps/lsoftogether cover 90% of first-response production debugging before you need heavier tooling.chmod 777andkill -9are almost always symptoms of not finding the real root cause — treat them as red flags in a runbook.
Interview Questions
- What is the difference between
git mergeandgit rebase? When would you choose each? - Why is
git push --force-with-leasesafer thangit push --force? - Walk through how you'd resolve a merge conflict in a shared file. What steps do you take before committing the resolution?
- What is the difference between
git reset --soft,--mixed, and--hard? - Why is
git revertpreferred overgit reseton a branch that's already been pushed? - What does
git reflogdo, and when would you use it? - Explain trunk-based development vs Git Flow. What team/product characteristics favor each?
- What's the difference between
SIGTERMandSIGKILL, and why does it matter for a JVM process shutting down? - How would you find which process is holding a given port open on Linux?
- Describe how you'd track down the source of repeated
OutOfMemoryErrors in a production log using standard CLI tools. - What's the difference between
chmod 644andchmod 755, and when would you use each? - How does
systemctldiffer from directly running a Java process in a terminal? Why does that matter for production reliability? - What is
git rerereand what problem does it solve? - How would you check whether a remote host's port 3306 (MySQL) is reachable from your box without connecting a full client?