Lesson 14 of 15

Git Best Practices

Habits That Keep a History Useful

Everything in this lesson is a habit rather than a command, and habits are what separate a repository someone can work with from one they cannot. The test is simple: could a stranger — or you, in six months — open this history and understand what happened and why?

The most valuable habit is committing often. Small commits are easier to write messages for, easier to review, easier to revert individually, and they make git bisect useful when you are hunting a bug. They also make your work recoverable: as the previous lesson showed, committed work can almost always be rescued through the reflog, while uncommitted work can vanish for good. Committing is not just record-keeping — it is your safety net.

The second is reading git diff --staged before every commit. It takes seconds and it is where you catch the debug print statement, the commented-out block, the accidentally reformatted file, and the credentials you were about to publish. Almost every embarrassing commit in the world would have been prevented by someone reading their own diff.

The third is keeping generated files out. node_modules, build output, .class files and IDE folders do not belong in history: they bloat the repository, produce meaningless diffs on every commit, and cause conflicts that mean nothing. Anything reproducible from a command belongs in .gitignore, not in your commits.

  • Commit small and often — one logical change per commit
  • Write messages in the imperative mood that explain why, not what
  • Read git diff --staged before every single commit
  • Never commit generated or downloaded files — that is what .gitignore is for
  • Never commit secrets: API keys, passwords, tokens, private keys, database URLs
  • Push at the end of every session; a commit only on your laptop is not backed up
  • Do not commit code you know is broken to a shared branch
  • Keep large binary files out of the repository, or use Git LFS if they genuinely belong there
Notes
  • A repository is not a backup service. It is a good one if you push, because that puts a full copy on a different machine. A hundred local commits that were never pushed offer no protection at all against a dead hard disk.

Secrets: Why Deleting Them Later Does Not Work

This is the most consequential misunderstanding in this entire course, so read it slowly. Suppose you commit a file containing your database password and your payment gateway key. You notice an hour later, delete the file, commit the deletion, and push. The file is gone from the current version of the project. It is still in the history, in full, forever.

That has to be true, because history is the point of Git. Anyone who clones the repository gets every commit, including the one where the key was added. git log -p will print it. Checking out the older commit will recreate the file. If the repository is public, automated scanners find keys within minutes of them being pushed — this is not a theoretical risk, it is an industry in itself.

So the first response to a leaked secret is not a Git command at all. Assume the secret is compromised and change it. Rotate the API key, change the password, revoke the token, regenerate the credential. Do that immediately, before doing anything to the repository. A key that has been rotated is harmless no matter who has the old value; a key that is still valid remains dangerous even after a perfect history cleanup, because you cannot know who cloned the repository in the meantime.

Only after rotating is it worth cleaning history, and it is genuinely disruptive: rewriting history changes every commit hash from the leak onwards, so everyone with a clone must reset to the rewritten version. The tools that do this properly are git filter-repo and BFG Repo-Cleaner, both maintained outside Git itself. If the repository is a small personal project and there is nothing else of value in it, deleting it and starting a fresh one with a correct .gitignore is often the faster and safer choice.

Prevention is trivial by comparison. Put configuration in a .env file, add .env to .gitignore before your first commit, and commit a .env.example listing the variable names with dummy values so teammates know what to create.

Example
# The right structure, from the very first commit

# .gitignore
.env
.env.local
*.pem
config/secrets.json

# .env  (never committed — real values live here)
DB_PASSWORD=actual_secret_value
RAZORPAY_KEY_SECRET=actual_secret_value

# .env.example  (committed — names only, no real values)
DB_PASSWORD=your_db_password_here
RAZORPAY_KEY_SECRET=your_key_here

# If a secret was already committed:
#   1. ROTATE THE SECRET. This is the step that actually protects you.
#   2. Stop tracking the file and ignore it going forward
git rm --cached .env
git commit -m "Stop tracking .env"
#   3. Only then consider rewriting history with git filter-repo or BFG,
#      remembering it changes every later hash and disrupts every clone.

# Check what you are about to publish
git diff --staged
git log -p --all | grep -i "password\|secret\|api[_-]key"
Notes
  • Committing .env is the single most common serious mistake students make with Git, and it usually happens through git add . on the very first commit. Write the .gitignore before you write any code.

Working With Other People

Most Git pain in a team project comes from a small number of avoidable behaviours, and nearly all of them are about branches drifting apart. The longer two branches evolve separately, the more expensive the eventual merge becomes — conflicts grow roughly with time and with the number of files touched.

So the core collaboration habit is to integrate constantly. Pull before you start work, pull before you push, and merge main into your long-running branch every day or so rather than once at the end. A branch that is merged daily produces trivial conflicts; a branch that has been alone for three weeks produces an afternoon of them, usually the afternoon before submission.

Communicate around the mechanics. Say which file you are working on before you start, so two people do not rewrite the same page in parallel. Tell the team when you merge something significant into main. And if you ever need to force-push a branch someone else might have, say so first — this is the one operation that can destroy another person's work without them doing anything wrong.

Finally, treat code review as a real activity rather than a formality. Reading someone else's code carefully is how you learn a codebase, and it is the only step in the process that catches mistakes before they become everyone's problem.

  • Pull before you start, and again before you push
  • Use feature branches; nobody commits directly to main
  • Keep branches short-lived — days, not weeks
  • Merge main into a long-running branch regularly, so conflicts stay small
  • Turn on branch protection: require a pull request, require checks to pass, block force-pushes
  • Announce any force-push before you do it, and prefer --force-with-lease
  • Review pull requests properly, and comment on the code rather than the person
  • Agree the workflow in writing before the project starts, not during the first argument
Notes
  • When something goes wrong on a shared branch, stop and ask before running commands you found on the internet. The commands that fix a private mistake (reset --hard, rebase, push --force) are the ones that turn a shared mistake into a much larger one.

Aliases and Small Comforts

Aliases let you give a long Git command a short name. This is not laziness — a command you can type in three characters is a command you will actually run, and git lg being instant is the difference between checking the branch graph regularly and never looking at it.

Set them with git config --global alias.name "the full command", or edit ~/.gitconfig directly, where they live under an [alias] section. They are per-machine, so remember to set them again on a lab computer or a new laptop.

Keep the set small at first. Aliases for status, a good log graph, and staged-diff cover most of what you do all day. Adding twenty aliases you cannot remember is worse than none, and it makes it harder to work on someone else's machine where they do not exist.

Two more comforts worth knowing. git help <command> opens the full manual for any command, which is more reliable than a search result of unknown age. And VS Code's Source Control panel is genuinely good for reviewing a diff and resolving conflicts visually — now that you know what it is doing underneath, using it is a convenience rather than a substitute for understanding.

Example
# A small, high-value set
git config --global alias.s  "status --short --branch"
git config --global alias.lg "log --oneline --graph --all --decorate"
git config --global alias.last "log -1 HEAD --stat"
git config --global alias.dc "diff --staged"
git config --global alias.unstage "restore --staged"
git config --global alias.sw "switch"

# Then:
git s                 # short status
git lg                # the branch graph
git dc                # what am I about to commit?
git unstage app.js    # take a file out of the staging area

# See what an alias expands to
git config --get alias.lg

# The built-in manual for anything
git help commit
git switch --help
  • Aliases are stored per machine in ~/.gitconfig — copy that file to a new laptop and your setup comes with you
  • Do not alias destructive commands to short names; making reset --hard easy to type is the opposite of what you want
  • git help <command> and <command> --help open the official documentation offline
  • You can alias external commands too by prefixing the definition with !, though shell quoting gets fiddly quickly
  • Learn the real commands before the aliases — you will regularly work on machines that do not have yours
Ask AI