Git

Git - Hard

Page 1 of 2

Branch Protection and Team Workflows

Once more than a couple of people share a repository, "everyone commits straight to main" stops being sustainable, and the choice of workflow becomes an operational decision, not a stylistic one.

: one long-lived branch (main), every change goes through a short-lived feature branch and a pull request, and main is always deployable. Simple, works well for continuous deployment.

: separate long-lived develop and main branches, plus release/* and hotfix/* branches. More structure, more ceremony - it fits teams shipping versioned releases on a schedule, and is often overkill for teams deploying continuously.

: everyone commits small, frequent changes directly to main (often behind feature flags), avoiding long-lived branches almost entirely. Requires strong test automation and discipline, but minimizes merge pain.

Whichever workflow, branch protection rules on the hosting platform are what actually enforce it: requiring pull request review before merge, requiring status checks (CI) to pass, disallowing force-pushes to main, requiring linear history. Without protection, the workflow is just a suggestion that erodes the first time someone's in a hurry.


Git Hooks

are scripts Git runs automatically at specific points - committing, pushing, receiving a push on the server. They're how you enforce standards without relying on people remembering to run a linter manually.

# .git/hooks/pre-commit (must be executable)
#!/bin/sh
npm run lint || exit 1     # non-zero exit blocks the commit

Client-side hooks (pre-commit, pre-push) live in .git/hooks/ and aren't version-controlled by default, which is exactly why teams use tools like husky or pre-commit - they install the hooks as part of npm install or a setup script, so every contributor actually has them, instead of relying on each person configuring their own machine correctly.

(pre-receive, update) run on the Git host when someone pushes, and they're the only hooks that can't be bypassed locally - a developer can skip a client-side hook with --no-verify, but they can't skip a server-side rejection. Anything genuinely mandatory (blocking secrets, enforcing commit message format) belongs server-side or in CI, not just client-side.


Recovering From Disasters

Git rarely loses data even when it looks like it did, because of the - a local log of every place HEAD has pointed, including commits that no branch currently references.

git reflog                     # every recent HEAD movement, even "lost" commits
git reset --hard a3f9c2d       # jump back to a commit found in the reflog

Recovering a deleted branch: if you git branch -D something by mistake, the commits aren't gone - find the last commit hash in the reflog and recreate the branch:

git branch recovered-feature a3f9c2d

Fixing a bad force-push: if someone force-pushes over shared history on main, anyone who already has the old history can recover it from their own reflog and push it back - which is exactly why "protect main from force-pushes" is one of the first branch protection rules teams enable. The reflog is local, though - it doesn't help you recover something that was never fetched to any machine, and it eventually expires (default 90 days for reachable, 30 for unreachable commits), so it's a safety net, not a permanent backup.


Large Files and Git LFS

Git tracks every version of every file forever, which works well for text and terribly for large binaries - a 200MB video file changed ten times means ten copies of ~200MB permanently in history, bloating every clone.

(Large File Storage) fixes this by storing large files outside the normal repository, replacing them in Git with small text pointers.

git lfs install
git lfs track "*.psd"          # writes tracking rule to .gitattributes
git add .gitattributes design.psd
git commit -m "Add design file"

The tradeoff: LFS adds an external dependency (an LFS server, usually provided by the same host) and a subtly different mental model - clones only pull LFS content on demand, and if the LFS server is unavailable, files silently show up as tiny pointer text instead of the real content, which confuses people who don't know LFS is in play.


Submodules and Subtrees

Sometimes a repository needs to include another repository - a shared library, a vendored dependency. Two approaches, with real tradeoffs.

embed a reference (repo URL + exact commit) to another repository as a subdirectory:

git submodule add https://github.com/org/lib.git vendor/lib
git submodule update --init --recursive   # required after cloning, easy to forget

Submodules keep the sub-repository fully independent, but that independence is also the pain point: cloning doesn't pull submodule content automatically, and forgetting --init is an extremely common source of "why is this folder empty" confusion in CI pipelines especially.

instead merge the other repository's history directly into a folder of your own repo - no separate .git reference, no extra clone step, but also no clean way to keep tracking upstream changes as a distinct commit history. For most teams, subtree's simplicity wins unless you specifically need to keep contributing changes back upstream.


    Welcome to OpsQuiz!

    Real scenario-based DevOps questions, hands-on practice, and clear explanations for every answer.