Do's and Don'ts
Following a branching model is the first step. Adopting a set of best practices is what truly makes a workflow efficient and scalable. These are the rules and tips I live by to keep our history clean, our collaboration smooth, and our production environment safe.
These rules are divided into three categories:
- Repository Rules: High-level policies to configure on your Git platform.
- Daily Workflow Rules: Best practices for your day-to-day work.
- Local Environment Setup: One-time configurations to make your life easier.
Repository Rules
Set these at the project level to create a safe and clean environment for everyone.
1. Enforce a Clean History with Squash-on-Merge
This is the most important rule for maintaining a readable main branch. All pull requests must be squashed when merging. No exceptions.
Your main branch should tell a high-level story of the project's evolution, not a minute-by-minute account of every developer's "WIP" commits and merge conflicts. Squashing turns the messy history of a feature branch into a single, clean, and purposeful commit on main.
Just look at the alternative-a tangled mess of merge commits that makes the history impossible to follow:
Let's avoid distasteful noodles and unneeded commit messages, let's avoid the headaches and constant fear of having your colleagues merging something "different". Let's have a fool-proof way of completing your merge request and make sure main will look and read the same today, tomorrow and a year after.
Setup your software forge to always merge with squash
GitLab
On your project > Settings > Merge requests

At the same time, select Fast-forward as the Merge method: this will prevent a "merge commit" loop from appearing, which is now completely useless.
You might as well configure the merge commit template to ensure a good audit trail and great changelog generation:
%{title}
%{description}
Source-branch: %{source_branch}
Target-branch: %{target_branch}
Merge-request-id: %{reference}
Authored-by: %{merge_request_author}
%{co_authored_by}
%{reviewed_by}
%{approved_by}
Merged-by: %{merged_by}Azure DevOps
On your project > Repos > Branches, find your "main" branch, and click on "Branch policies"

Then, on Limit merge types, uncheck everything except "Squash merge"

2. Protect the main Branch
The main branch is your source of truth. It must always be stable and deployable. Never allow direct pushes to it. All changes must come through a reviewed Pull Request.
How to Protect a Branch in GitLab
- Go to Settings > Repository > Protected Branches.
- Add
mainand other critical branches likesupport/*. - Set "Allowed to merge" to "Maintainers" and "Allowed to push and commit" to "No one".
This prevents unreviewed code from ever hitting your main line. 
3. Handle Large Files with Git LFS
Git is designed for text, not large binary files (images, videos, compiled assets). Storing binaries directly in Git bloats the repository size exponentially, as it stores a full copy of the file for every change. Locally, your repository must also download and store all versions of each binary present in its history.
Use Git LFS (Large File Storage) instead. It replaces large files in your repository with tiny text pointers, while storing the actual file content on a separate server. When you check out a commit, LFS retrieves only one version of the binary: the one referenced by the pointer, keeping your local repository small and fast.
# Install LFS once per machine
git lfs install
# In your project, tell LFS which file types to track
git lfs track "*.psd"
git lfs track "*.mp4"
# Don't forget to commit the .gitattributes file that tracks this
git add .gitattributes
git commit -m "chore(assets): Configure Git LFS"Here's a ready-to-use .gitattributes files, shamelessly stolen from Muhammad Rehan Saeed's blog
.gitattributes
###############################
# Git Line Endings #
###############################
# Set default behaviour to automatically normalize line endings.
* text=auto
# Force batch scripts to always use CRLF line endings so that if a repo is accessed
# in Windows via a file share from Linux, the scripts will work.
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
# Force bash scripts to always use LF line endings so that if a repo is accessed
# in Unix via a file share from Windows, the scripts will work.
*.sh text eol=lf
###############################
# Git Large File System (LFS) #
###############################
# Archives
*.7z filter=lfs diff=lfs merge=lfs -text
*.br filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.tar filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
# Documents
*.pdf filter=lfs diff=lfs merge=lfs -text
# Images
*.gif filter=lfs diff=lfs merge=lfs -text
*.ico filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text
# Fonts
*.woff2 filter=lfs diff=lfs merge=lfs -text
# Other
*.exe filter=lfs diff=lfs merge=lfs -textDaily Workflow Rules
Adopt these habits to make your collaboration seamless and effective.
4. One Task, One Branch, One Commit
We've all been there. It's late, and you just want to cram three bug fixes and a new feature into one massive branch. Don't do it.
This creates a nightmare for code reviewers and makes it impossible to cherry-pick a critical fix if it's bundled with an unfinished feature. Keep it simple: one task, one branch. When squashed, this becomes one clean commit on main.
5. Keep Branches Short-Lived
A feature branch should be a temporary workspace, not a permanent home. The longer a branch lives, the further it diverges from main, leading to a higher risk of merge conflicts.
Break down your work into the smallest possible shippable units. This leads to smaller pull requests, faster reviews, and quicker delivery of value.
6. Communicate with Commits
Your commit messages are a story you tell to your future self. Make it a good one. Follow the Conventional Commits standard to ensure every commit is uniform, descriptive, and machine-readable.
7. Push with Purpose, Not with force
Rewriting history on your own private feature branch is often necessary (e.g., when rebasing). But git push --force is a sledgehammer that can delete commits others have based their work on.
NEVER Use git push --force
It blindly overwrites the remote branch and can cause permanent data loss.
Instead, always use git push --force-with-lease --force-if-includes. This safer command checks if someone else has pushed to the branch since you last pulled. If they have, the push will fail, protecting their work.
Local Environment Setup
Configure these settings once on your machine to prevent common mistakes.
8. Master the Command Line First
GUIs like GitKraken or Fork are great for visualizing history, but they can hide what Git is actually doing. Using a GUI without understanding the underlying commands is like driving a car without knowing what the pedals do.
Start with the command line to learn the fundamentals. Once you're comfortable, use a GUI for its strengths (visualization, staging individual lines), but always know the command you're actually running.
9. Customize Your git config
Make Git work for you by setting up some smart defaults and aliases.
Some git config Tricks
Run these commands to set them globally for all your projects.
Make git pull always use rebase This avoids creating messy, local merge commits on your feature branches when you update from main.
git config --global pull.rebase truePrevent Git from messing with line endings This avoids phantom changes when collaborating across Windows and macOS/Linux.
git config --global core.autocrlf falseCreate a safe "force push" alias Type git pf instead of that long, safe push command.
git config --global alias.pf 'push --force-with-lease --force-if-includes'Now you can just run git pf!
10. Tame Your GUI
If you use a GUI, configure it to work with your system's Git, not against it. Many GUIs (like GitKraken) bundle their own version of Git, which can ignore your global .gitconfig settings.
Essential GitKraken Configuration
1. Use Your System's Git Executable GitKraken bundles its own version of Git, which might ignore your global .gitconfig settings.
- Go to File > Preferences > Experimental.
- Check "Use local Git executable" and point it to your system's Git installation.
2. Set the Correct sh.exe Path This prevents issues with Git Hooks on Windows.
- Go to File > Preferences > General.
- Scroll to "Path to sh.exe" and ensure it points to the
sh.exeinside yourGit\binfolder.
I still highly recommend a minimalistic GUI. GitLens if you want something integrated to your IDE, Git Fork if you want a fuller experience.