The 38 MB commit and the conflict resolved with 'ours'
Aisha is on a four-person team using a shared main branch on a hosted Git remote. Yesterday she committed a 38 MB customers.csv containing 5,000 email addresses and, inside a config file, a database password, then pushed and merged to main. Two hours later she noticed. Separately, her teammate Dev branched feature/reports 12 commits behind main three days ago. When he integrated, he hit a conflict in report.py during a rebase and resolved it by choosing 'ours' for the whole file, then force-pushed his branch. A week later the quarterly report is missing a column that only Dev's side had added. main has had 40 commits from four developers since Dev branched. CI runs on every push to main and caches dependencies keyed on the branch name.
1. Give the ordered recovery plan for the secret and the CSV. Justify the order and name the steps people usually get wrong.
Read the worked answer
Rotate first, rewrite second. The password has been on a remote, in CI logs and caches, and in every clone that fetched since. Treat it as compromised the moment it was pushed; deleting the commit does not un-leak it, and rewriting history first just delays the only step that actually reduces risk. Rotate the credential, check access logs for the window, then update CI secrets so builds do not break.
Then remove the data from history. git rm plus a new commit leaves the blob in every prior commit and in the remote's object store, so a fresh clone still pulls 38 MB and the addresses are still recoverable. Use git filter-repo (or BFG) to rewrite the path out of all commits, coordinate a freeze window, force-push, and tell every clone to re-clone or hard-reset rather than merge - a merge would reintroduce the old history. Open PRs need rebasing onto the rewritten branch.
Steps people get wrong: rewriting before rotating; force-pushing without a team freeze; forgetting CI caches, forks, and the remote's dangling objects (ask the host to garbage-collect); and adding .gitignore afterwards and believing it protects history. .gitignore only stops future adds. Finally, check whether the addresses trigger a notification obligation, since email data and a credential in one repo is a reportable category for many organisations.
2. Explain why 'ours' removed the column during a rebase, how Dev should have resolved it, and how to recover the change now.
Read the worked answer
In a rebase, 'ours' and 'theirs' are swapped relative to intuition. Git replays Dev's commits on top of main, so during the conflict 'ours' is the upstream branch (main as it already stands) and 'theirs' is Dev's own commit being replayed. Choosing 'ours' for the whole file therefore discards Dev's edits, and the column that only existed on his side is gone. In a merge, 'ours' would have meant his branch and the outcome would have been different - the same button means opposite things. Taking a whole-file side also discards the other side's unrelated changes in that file, which is what happened to the column.
Correct approach: resolve hunk by hunk, keep both sides where they are additions, and re-run the tests for report.py before continuing the rebase. If the file is large, git checkout --conflict=diff3 or zdiff3 shows the common ancestor and makes it obvious which side added what.
Recovery now: find the pre-rebase commit in git reflog (or the PR's old head), git show <sha>:report.py, and port the missing column as a normal commit on top of current main. Cheap and auditable; do not rewrite main to undo this. Add a test asserting the report's column set so the next conflict fails loudly.
3. Recommend process and tooling changes so this pair of incidents is unlikely to recur. Rank them by impact for a four-person team and say what each one actually prevents.
Read the worked answer
Rank by the damage avoided, not by how easy each is to install.
- Secret scanning with a pre-commit hook and a server-side push-protection rule. Prevents the credential reaching the remote at all, which is the only version of this problem with a cheap fix. A local hook alone is not enough - it can be skipped with
--no-verify- so enable the host's push protection as well. .gitignoreentries for*.csv, local config, and.env, committed before the next incident. Cheap, and prevents the routine version of the mistake, but does nothing for history, so it is a complement to item 1, not a replacement.- Branch protection on
main: required pull request, one approval, CI green, no force-push. Prevents direct pushes of large or secret-bearing files, and gives a second pair of eyes on conflicts. For four people this is not bureaucracy; it is the review that would have caught 'ours' on a whole file. - Short-lived branches and small PRs. Dev was 12 commits behind with 40 commits of drift, which is exactly when a conflict resolution is hardest to verify. Rebase daily and keep changes narrow.
- CI: keep the test suite fast and make the report tests assert column presence. A conflict resolved badly should fail a test within minutes, not surface at quarter end.
What none of these fix: history that is already published. Items 1–3 reduce future incidents; the rotation and rewrite in the previous answers are still required for this one.