Version Control and Developer Tooling Questions
The everyday toolchain of software work: version control with Git (branching, merging, rebasing, conflict resolution, using git bisect to find a regression), command-line and shell proficiency for day-to-day navigation, log inspection, and troubleshooting, IDE and editor workflows, build systems and package/dependency management (npm, Maven, pip, Gradle, CocoaPods, and embedded/cross-compilation toolchains), and the growing practice of AI-assisted coding: using, reviewing, and verifying AI-generated code and tests. Deliberately generic across languages and stacks; language- and domain-specific frameworks live in their own categories. This topic covers a developer's individual command of these tools, not: writing durable shell automation and glue scripts (Shell Scripting and Automation owns that), producing, versioning, and publishing build artifacts or container images (Build Automation and Artifact Management owns that), release cadence and change governance (Release Management and Change Control owns that), or diagnosing a live production incident end to end (Performance Troubleshooting and Incident Response and the Observability topics own that).
Explain the difference between df and du and provide the exact commands you would run to: (a) show overall filesystem free space in human-readable form, and (b) find the top 5 largest directories immediately under /var, excluding mounted virtual filesystems. Include any flags and filters you would use.
Sample Answer
Direct answer
df reports space at the filesystem level, asking the kernel directly how full each mounted volume is, which is near-instant. du walks a directory tree and sums up the actual size of the files it finds, which can be slow on a large tree. They can legitimately disagree: a large file that was deleted while a process still has it open inflates df's used-space number, but du never sees it since it is no longer in any directory listing.
Structured elaboration
# (a) overall filesystem free space, human-readable
df -h
# (b) top 5 largest directories immediately under /var, staying on one filesystem
du -h -d 1 -x /var 2>/dev/null | sort -rh | head -n 5
Key points:
df -h:-his human-readable (K/M/G). Typical GNU/Linux columns are Filesystem, Size, Used, Avail, Use%, Mounted on (macOS/BSDdfadds inode columns but reports the same underlying data).du -d 1: limits depth to 1, immediate subdirectories only, not a full recursive per-file listing.-d Nis the portable spelling across both GNU coreutils and BSDdu(GNU also accepts--max-depth=N, but plain BSDduonly understands-d).-x: stay on one filesystem, do not cross into a separately mounted filesystem. This is exactly how you exclude a mounted virtual filesystem, such as a tmpfs mounted at/var/run, without hardcoding its path.2>/dev/null: suppress "Permission denied" noise on directories you cannot read.
Worked example
$ du -h -d 1 mockvar 2>/dev/null | sort -rh | head -5
5.4M mockvar
1.9M mockvar/lib
1.8M mockvar/weird dir
880K mockvar/run
492K mockvar/log
This was run against a small mock directory tree, not the real /var (whose sizes are host-specific). Note the root of the tree you point du at (here mockvar itself, in production /var itself) shows up as its own total in the output; if you only want the children, pipe through tail -n +2 after sorting, or filter it out explicitly.
Trade-offs & pitfalls
dfanswers "how full is the disk,"duanswers "how big is this specific tree." They measure different things, and disagreeing by design is normal, not a bug to chase before checking the classic cause: a held-open, deleted file.lsof +L1finds those.dustats every file it counts. On a directory with millions of small files (a real risk under something like a build cache ornode_modules), that can be slow and I/O heavy;-d 1bounds the scan to one level to keep a quick "what's big" survey fast.- Both tools report allocated (block-rounded) usage by default, not the logical byte count of the file's content. A sparse file can show a smaller size via
duthan its logical length would suggest; that is expected behavior, not a bug.
Dev secrets: propose an approach to secure secrets for local development that minimizes friction, and compare the realistic options for storing and distributing them. Discuss developer ergonomics, auditability, and offline development.
Sample Answer
Direct answer
There is no single right answer, only a trade-off across developer ergonomics, auditability, and offline development, and the right choice scales with team size. For a small team, an encrypted-in-repo file or a lightweight secrets-manager CLI beats a shared plaintext .env. For a team large enough that access needs to be revocable per person, a real secrets manager earns its setup cost, because auditability (knowing exactly who has fetched what, and being able to cut off one person's access without rotating the underlying secret) is a property nothing else on this list gives you.
Structured elaboration
| Approach | Developer ergonomics | Auditability | Offline development |
|---|---|---|---|
Plain .env, values shared manually (chat, a doc) | Fast to start, but propagating an updated value is manual and easy to forget | None: no record of who has which secret or when it was shared | Works fully offline once the file exists locally |
| Secrets manager (Vault, a cloud secrets manager, or a developer-focused tool like Doppler) with a CLI or agent | One-time login, secrets refresh automatically after that; needs the tool installed and reachable over the network at least once | Strong: every fetch is logged, access is per-identity and revocable without rotating the secret itself | Breaks, or falls back to a stale cache, the moment the network is unavailable, unless the tool explicitly ships an offline mode |
Encrypted-in-repo file (tools like sops or git-crypt) | Clone-and-decrypt is simple once keys are distributed; rotating a secret means committing a new encrypted blob | Git history shows who committed a change to the encrypted file, but not who actually decrypted and read the value locally | Fully offline once the repo is cloned and the decryption key is present |
| OS keychain (macOS Keychain, Windows Credential Manager) | Native, no extra file on disk, but strictly per machine, a new laptop means re-entering everything | Effectively none across a team; it is a single-machine, single-user store | Fully offline, it is entirely local |
Keep a documented, deliberate offline fallback (a clearly-labeled stub or a short-lived cached credential) for anyone stuck without network access. A workflow that hard-fails without connectivity gets worked around with a hand-copied plaintext file the first time someone is stuck at an airport, quietly recreating the exact risk the tool was meant to remove.
Worked example
A team of six starts with a shared .env.example and real values pasted into a team chat thread. When a contractor's engagement ends, the team realizes it would need to rotate every secret in that thread, because there is no way to know what the contractor still has a copy of; this is exactly the failure mode auditability buys back. They migrate to a lightweight secrets-manager CLI: each developer authenticates once, and a local wrapper script pulls current secrets into environment variables at the start of a dev session, fetched fresh each time, never written to disk in plaintext. Revoking the departed contractor's access afterward is a single action in the secrets manager, no rotation needed, because the underlying secret's value never left the manager's control in a form the contractor could independently retain.
Trade-offs and pitfalls
- A secrets-manager workflow is only as strong as its offline story; if the dev server hard-fails without network because it can't reach the vault, developers will improvise a local plaintext copy the moment connectivity is bad, quietly recreating the exact risk the tool was meant to remove.
- Encrypted-in-repo tools solve distribution but not real-time revocation: decrypting a secret onto a developer's laptop means it exists in plaintext there from that point on, so rotating the underlying value is still required if that laptop is compromised or that person leaves.
- Auditability is a property of the system, not of policy alone. Asking people not to share secrets over chat, without a technical alternative that is actually easier than chat, will not hold up under deadline pressure; the ergonomics and the security property have to point the same direction or the security property loses.
Provide a devcontainer.json and brief Dockerfile snippet for VS Code that sets up a Python Flask app with Postgres and Redis for local development. Include postCreateCommand to install pip dependencies, forwarded ports, and recommended VS Code extensions. Explain the benefits for onboarding and reproducibility.
Sample Answer
Direct answer
A devcontainer for this stack is a devcontainer.json that points VS Code at a docker-compose.yml (Flask app plus Postgres plus Redis, each its own service), a small Dockerfile for the app service, a postCreateCommand that installs Python dependencies after the workspace is mounted, and forwardPorts for the app, database, and cache. The benefit isn't the individual files, it's that the exact runtime versions and system dependencies are declared once, in files versioned alongside the code, so a new hire's environment stops depending on what happened to already be installed on their machine.
Structured elaboration
.devcontainer/devcontainer.json:
{
"name": "flask-postgres-redis",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"forwardPorts": [5000, 5432, 6379],
"postCreateCommand": "pip install -r requirements.txt",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-azuretools.vscode-docker",
"mtxr.sqltools"
]
}
}
}
.devcontainer/docker-compose.yml:
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- ..:/workspace:cached
command: sleep infinity
depends_on:
- db
- redis
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/appdb
REDIS_URL: redis://redis:6379/0
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: appdb
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:7
restart: unless-stopped
volumes:
postgres-data:
.devcontainer/Dockerfile:
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends git curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
This is a schema-accurate illustration of the devcontainer and compose format, I don't have the VS Code Dev Containers extension available in this environment to actually build and open it, so I'm not claiming an executed run here, only that the file shapes above match the documented spec.
forwardPorts exposes the app (5000), Postgres (5432), and Redis (6379) to the host so a browser or a local psql/redis-cli can reach them. postCreateCommand runs once, after the container is built and the workspace is mounted, which matters because requirements.txt lives in the mounted workspace, not baked into the image, so pip install has to happen after the mount exists, not during the Dockerfile build. The customizations.vscode.extensions list installs automatically inside the container so a new hire gets a working Python and SQL setup with no manual extension hunting.
Worked example
A new hire clones the repo and opens it in VS Code. The Dev Containers extension detects .devcontainer/devcontainer.json, builds the app image from the Dockerfile, starts db and redis alongside it, waits for all three, then runs pip install -r requirements.txt as the postCreateCommand. A few minutes later the editor reopens attached to the running container, with ms-python.python and the SQL extension already installed, localhost:5432 and localhost:6379 reachable from a local client, and localhost:5000 ready once the developer runs the Flask app. No manual "install Python 3.12, install Postgres, install Redis, hope the versions match" steps happen at all.
The docker-compose-without-devcontainer angle. The same docker-compose.yml also works standalone for a developer who isn't using VS Code's Dev Containers extension at all: docker compose -f .devcontainer/docker-compose.yml up -d db redis starts just the dependencies, and that developer runs the Flask app directly on their host against those two containers. This is the more common path for a team supporting multiple editors, the devcontainer.json layers a fully containerized, one-click environment on top of the same compose file, it doesn't replace the compose-only workflow, it wraps it.
The macOS volume-mount caveat. The ..:/workspace:cached line bind-mounts the whole repository into the container. On macOS, this kind of bind mount has historically been slow, filesystem events crossing the VM boundary went through gRPC-FUSE/osxfs (the older mechanism Docker Desktop used to relay file changes between the Mac host and the Linux VM it runs containers in, over a remote-procedure-call bridge), which made large, write-heavy directories (a Python virtual environment being one) noticeably sluggish. The :cached consistency flag helps, since it lets the host be the source of truth for reads without every write round-tripping through the VM, and modern Docker Desktop's VirtioFS backend (a faster successor that shares files through a more direct virtual-filesystem protocol instead of relaying individual events), the default on recent versions, has closed most of this gap. If you're still on an older setup or seeing slow installs, moving a heavy write-churn directory like the virtual environment into a named Docker volume instead of the bind mount, so that traffic never crosses the slow path at all, is the more durable fix.
Trade-offs and pitfalls
- Onboarding: a new hire needs Docker and the Dev Containers extension, nothing else pinned to their machine; the Python version, Postgres version, Redis version, and system packages are all declared in files checked into the repo, so "works on my machine" stops depending on what was already installed.
- Reproducibility is bounded by what's actually pinned:
postgres:16andredis:7still float across minor versions, a team that wants a stronger guarantee should pin to a specific minor version or an image digest. - Running two entry points to the same environment, the devcontainer path and the standalone compose path, means both have to be kept in sync deliberately; if only one gets updated when a new environment variable is added, the other quietly drifts.
- The first build is genuinely slow (pulling three base images, installing system packages, installing Python dependencies); that cost is paid once per machine, not once per session, but it's worth setting expectations for a new hire's first hour.
A teammate asks: 'What is the staging area (index) and how does git add differ from git commit?' Explain what each command does and give an example where using git add -p or git add -A would matter.
Sample Answer
Direct answer
The staging area, also called the index, is git's holding area for exactly the changes you intend to put into your next commit. git add copies changes from your working directory into that staging area; git commit takes whatever is currently staged and permanently records it as a new commit in history. They're separate steps specifically so you can build a commit out of a chosen subset of your changes, rather than being forced to commit everything you've touched.
Structured elaboration
# stage one specific file
git add config/nginx.conf
# create a commit from whatever is currently staged
git commit -m "Update nginx healthcheck path"
git status shows the distinction directly: files listed under "Changes to be committed" are staged; files under "Changes not staged for commit" are modified in your working directory but not yet added.
Worked example
Say you've edited one file, infra/nginx.conf, and it now contains both a real configuration change and a stray debug line you added while troubleshooting. Staging the whole file would commit the debug line too.
git add -p infra/nginx.conf (patch mode: it walks through the file's changes hunk by hunk and asks yes or no for each one) lets you stage only the real configuration change and leave the debug line unstaged, so it never enters history:
git add -p infra/nginx.conf
# git shows each hunk and asks: Stage this hunk [y,n,q,a,d,...]?
git add -A (stages every change across the whole repository: tracked file edits, brand-new untracked files, and deletions) is the opposite instinct: reach for it when you deliberately want one commit to capture everything that changed, for example right before cutting a release where several files were intentionally updated together:
git add -A
git commit -m "Release 2.4.0: bump image tags, remove deprecated manifests"
Trade-offs and pitfalls
git add -A is easy to reach for out of habit, and that habit is exactly how unrelated or debug changes end up in a commit unnoticed. git add -p takes more time per commit, but produces commits that are easier to review and, if one specific change turns out to be wrong, easier to revert individually rather than untangling it from everything else that landed alongside it.
Your organization uses forks extensively. Describe a workflow for contributing to an upstream repository from a fork that minimizes merge conflicts and supports CI validation. Include how to keep forks up-to-date, how to create PRs against upstream, and how automation (CI) should be configured to run tests for forked PRs without leaking secrets.
Sample Answer
Direct answer
Add the original project as a second remote, conventionally named upstream, alongside your fork's own remote (origin). Regularly sync your fork's default branch from upstream before branching, keep feature branches short-lived and rebased on the latest upstream branch before opening a pull request, and configure CI so pull requests from forks run tests safely without ever exposing real secrets to code you don't control.
Structured elaboration and worked example
- Set up the remotes once, per clone:
git remote add upstream git@github.com:org/repo.git
git remote -v # confirms origin (your fork) and upstream (the real project)
A common mechanical snag: if you already have a remote called upstream pointing at something else, git remote add upstream ... fails outright. Use a different name (git remote add project-upstream ...) or git remote remove upstream first if the existing one is stale. A second common snag is a differing default branch name: your fork might default to main while upstream still uses master, or the reverse, so always check upstream's actual default branch rather than assuming it matches your fork's.
- Keep your fork's default branch current before branching off it:
git fetch upstream
git checkout main
git merge upstream/main
git push origin main
- Rebase your feature branch onto the latest upstream before opening or updating a pull request, so conflicts surface early and in small pieces instead of all at once at review time:
git fetch upstream
git checkout feature/my-change
git rebase upstream/main
git push --force-with-lease origin feature/my-change
- Open the pull request from your fork's branch against
upstream's default branch, keeping the diff small and focused.
CI for forked pull requests without leaking secrets
Most CI platforms, GitHub Actions among them, already mask or withhold repository secrets from workflows triggered by a pull request originating from a fork, specifically because a forked PR's workflow file is attacker-controlled content that hasn't been reviewed yet. Build on that default rather than fighting it:
- Run a public tier automatically on every forked pull request: unit tests, linting, build, with no privileged secrets available to that job at all.
- Gate anything that genuinely needs credentials, integration tests against a real staging environment or a deploy preview, behind a maintainer action, for example a second workflow that only runs after a maintainer reviews the diff and manually triggers it, so untrusted code never executes with privileged access.
Trade-offs and pitfalls
Forgetting to sync and rebase before opening a pull request is the single biggest source of unnecessary conflicts in a fork-heavy workflow, since a fork's branch quietly drifts from upstream the entire time it sits open. Granting forked-PR workflows broad secret access "just to make CI green" is exactly the mistake that lets an attacker-controlled pull request exfiltrate credentials; the fix is to keep the untrusted tier genuinely unprivileged rather than loosening it under pressure to unblock a contributor.
Unlock Full Question Bank
Get access to all Version Control and Developer Tooling interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.