dbt Source Control: Managing a dbt Project With Git the Right Way
A dbt project is, at its core, a folder of SQL and YAML files โ which means every discipline that applies to managing application code with Git applies here too, but with a few dbt-specific wrinkles worth knowing before they cost you a broken production deploy.
Basic Repository Structure
A dbt projectโs Git repository typically tracks the source, deliberately excluding generated artifacts:
.gitignore: target/ dbt_packages/ logs/ .user.ymltarget/ and dbt_packages/ are generated/downloaded on every run โ committing them bloats the repository with regenerable content and, worse, can cause confusing merge conflicts on files that should never be manually edited or reviewed in the first place.
Branching Strategy
Most dbt teams use a conventional feature-branch workflow, no different from application code:
git checkout -b feature/add-customer-ltv-model# make changes to models, tests, docsgit add models/marts/customer_ltv.sql models/marts/_customer_ltv.ymlgit commit -m "Add customer lifetime value model with tests"git push origin feature/add-customer-ltv-modelA pull request against main (or whatever the production-tracking branch is called) is where CI validation, covered in dbt CI/CD Integration, runs automatically before merge.
What Actually Belongs in a dbt Pull Request
A well-scoped dbt PR usually bundles four things together for a single logical change:
models/marts/customer_ltv.sql โ the model itselfmodels/marts/_customer_ltv.yml โ description + teststests/assert_ltv_never_negative.sql โ any custom test, if neededdocs/customer_ltv_description.md โ doc block, if the description is longReviewing a model without its accompanying tests and documentation is reviewing half the change โ a good team convention is treating โmodel with no testsโ as an automatic PR review flag, not an acceptable default.
Reviewing dbt Pull Requests Effectively
Reviewing dbt SQL differs from reviewing application code in a few specific ways worth calling out to reviewers:
- Check the compiled SQL, not just the Jinja. A
{% for %}loop can look correct in Jinja and still produce malformed SQL โ reviewers should checktarget/compiled/output for anything with non-trivial templating, not just read the raw file. - Verify
ref()andsource()usage, not hardcoded table names. A hardcoded schema reference is a correctness bug waiting to surface in a different environment, not a style preference. - Confirm tests were added or updated alongside logic changes. A model whose logic changed but whose tests didnโt is a signal the tests may not actually be exercising the changed behavior.
- Check for downstream impact using the lineage graph, covered in Lineage Graph, before approving a change to a heavily-depended-upon model.
Handling Merge Conflicts in YAML Schema Files
Multiple engineers frequently touch the same _models.yml file to add tests or descriptions to different models within it, and merge conflicts here are common. Splitting large schema YAML files by model (one YAML file per model or small group, rather than one giant file per folder) significantly reduces conflict frequency:
models/staging/ _stg_orders.yml โ instead of one giant _staging.yml _stg_customers.yml stg_orders.sql stg_customers.sqlProtecting the Production Branch
A production-tracking branch (commonly main) should require CI checks to pass before merge โ at minimum, dbt build succeeding against a CI environment, ideally with the source freshness and testing gates covered in dbt CI/CD Integration. Direct pushes to this branch, bypassing review and CI, are the most common way a broken model reaches production undetected.
Branch protection rule (GitHub example): - Require pull request before merging - Require status checks to pass (dbt build, dbt test) - Require at least 1 approving reviewTagging Releases
For teams that deploy on a schedule rather than continuously, tagging specific commits as releases gives a clean rollback point and a clear record of what was actually running in production at any given time.
git tag -a v2026.07.01 -m "Weekly production release"git push origin v2026.07.01Commit Message Conventions Worth Adopting
Because a dbt commit often touches a model, its tests, and its documentation together, a commit message describing the business intent โ not just the mechanical change โ makes future history far more useful.
# Less usefulgit commit -m "update customer_ltv.sql"
# More usefulgit commit -m "Add customer_ltv model with churn-adjusted revenue calculation
Adds not_null and range tests on the ltv_value column.Depends on the new subscription_tenure staging model."Six months later, git log --follow models/marts/customer_ltv.sql becomes a genuinely readable history of why the model evolved the way it did, rather than a list of โupdate modelโ commits that require opening every diff individually to understand what actually changed and why.
Common Mistakes
Committing target/ or dbt_packages/. This bloats the repository and can cause spurious merge conflicts on files nobody should be hand-editing or reviewing.
Reviewing only the Jinja source, never the compiled output. Templating bugs are frequently invisible in the raw file and only obvious in the fully-rendered SQL.
Allowing direct pushes to the production branch. This is the single most common way a broken or untested change reaches production without ever going through CI validation.
One giant schema YAML file per folder. This significantly increases merge conflict frequency as a team grows โ splitting by model avoids most of it.
Summary
| Practice | Why it matters |
|---|---|
.gitignore for target/, dbt_packages/ | Avoids bloat and spurious conflicts on generated content |
| Reviewing compiled SQL, not just Jinja | Catches templating bugs invisible in raw source |
| Branch protection with required CI checks | Prevents unreviewed or failing changes from reaching production |
| Per-model YAML schema files | Reduces merge conflict frequency as team size grows |
Git discipline for a dbt project isnโt fundamentally different from any other codebase โ the main adjustments are reviewing compiled output for Jinja correctness and treating tests as a mandatory, not optional, part of every model change.