Much has been written on the theory and practice of agile (or Agile, if you prefer—the debate is real) including books, blog posts, and formal training.
What this guide aims to cover in depth is how to apply that theory to branch management in git, specifically for those working with Drupal 7, Drupal 8 or Drupal 9.
The following content is based on the work of our talented friend and former colleague Matt Corks.
Who is this guide for?
This guide is for you if:
- You're using an agile workflow, with sprints and regular deployments
- You have a team of multiple developers, a scrum master (project maintainer), and a product owner (client representative)
- You want to do regular releases of whatever tickets are ready
- You review releases (continuous integration, user acceptance testing, change approval board, etc.) before deployment
- You occasionally need to do hotfixes for urgent problems directly on the production site
- Before a deployment, you occasionally need to revert from a release branch a ticket that didn't pass review
Contents
- Prerequisites and prep
- Drupal with git: best practices
- Git branching strategies
- Managing multiple remotes
- Working with branches: code snippets
Prerequisites and Prep
To make this work, it's best to use an issue tracker with a ticket for each user story, and to make all changes to your site on a separate git branch labelled with that ticket number.
The changesets should be self-contained, and perform all necessary database changes in code via update hooks, features (Drupal 7), or the configuration management system (Drupal 8/9). Any further steps necessary to test or deploy the new version should be clearly described in the ticket description.
Once all your updates are in code, you're ready to use git to prepare for and manage your testing and deployment process. You'll need to adopt specific git practices to make this work smoothly.
Drupal with Git: Best Practices
Drupal 7 + Git best practices
In Drupal 7, the best practice is to export all possible configurations using the Features module, so that it can be tracked and versioned in git. After setting up your site's views, content types, variables, and other configuration, you would use the Features module to export these to code as a series of special modules in sites/all/modules/features/ and from there commit them to git. It's simplest to group these by functionality (for example, you could create one features module which contains all the configuration necessary for the blog section of your site).
After making changes, run drush features-update-all
to export the configuration from the database to code. To bring in changes after doing git pull
you would run the command drush features-revert-all
to import from the version in code to the database. You'll need to return to the Features module to add new components of your site (such as a new view) before they will be included in the set of configuration saved in a features module.
To learn more about exporting the configuration of your Drupal 7 site using features, see the documentation.
Drupal 8/9 + Git best practices
In Drupal 8 and higher, most configuration is stored in YAML files, making this much simpler.
By default D8 configuration is stored in the public files/ directory under a difficult-to-guess directory name. Wherever possible, add the following to settings.php or settings.local.php to define the config directory, which should be someplace outside the webserver docroot. (This means your website docroot will be a subdirectory of your git root folder, giving you a place above that to store test scripts and other files outside the docroot.)
// Define the sync configuration directory
$settings['config_sync_directory'] = '../config/sync';
Once this is done, run the following after making local changes on dev machines: drush config-export
You can now commit and push these changes to your git repository. To import these on test, staging, and production environments, do a git pull and then run the following: drush config-import -y
Like with D7 features, the suggested workflow is to avoid making changes to these files directly on the production website, but instead to deploy by importing from YAML. Changes made directly on prod will be discarded.
You can read more about the basics of configuration management in Drupal 8 here.
It's also possible to use configuration files during site install to spin up a site instance without a database, as described in this article.
You should also have a convenient way to fetch a copy of the production database from dev & staging environments (eg via drush aliases), and use the Stage File Proxy module to avoid having to sync public files from prod back to dev and stage environments until they're needed.
Use drush sql-sync --sanitize
to reset passwords and email addresses in the user tables when copying the database (note that you can also capture all mail sent from dev and stage by sending it to a utility such as MailHog for local review).
Git Branching Strategies
To explain our preferred git workflow, we'll start by explaining some other, simpler workflows that can be used by smaller teams. My reference for this is the tutorial from Atlassian.
We're assuming knowledge of basic git concepts like branching, committing, and pulling. If these are new to you, we suggest taking the time to read an introductory tutorial such as this one, again from Atlassian.
Method 1: Centralized workflow
This is the simplest possible git workflow.
- There are no branches: everyone always commits to master
- Works best when each part of project has only one developer (i.e. one front-end, one back-end)
Method 2: Feature branches, aka GitHub-style pull requests
Once multiple people are working on a codebase at the same time, it's necessary to use a more sophisticated model. Many open source projects on GitHub work this way, with one person responsible for testing and approving all proposed changes.
- Developers start a new branch to work on each feature, sending a pull request when ready for review
- Developers merge the master branch back into their dev branches on a regular basis during development to keep up with other changes
- Maintainer reviews proposed changes and merges (pulls) feature branches into master when ready
- Maintainer occasionally tags a commit as a named/numbered release
- Works best for simpler projects without a need for external reviewers from outside the dev team
Method 3: GitFlow
Once your project requires a review process for each set of new features, some of which may depend on others to be completed at the same time, it's necessary to create branches for each release so that these can be tested as a group. It follows that you might need to remove a feature from a release if it isn't accepted during testing, and that you'll occasionally need to urgently fix the production version of the code without waiting for your usual release cycle.
- Naming convention allows for dev, feature, hotfix, and release branches, with defined procedures for updating them
- Created by Vincent Driessen in 2010
- Works best for multiple developers using an agile process as described above
Git branching best practices ✅
Make sure every commit message includes the ticket number, both to create pointers from your issue tracker, and to allow you to find commits related to a given ticket at a later date. You can even set up your CI/automated testing scripts to reject commits without numbers, and create a script in .git/hooks/prepare-commit-msg.sh to insert this automatically in the commit message template. Here's one such script.
Set mergeoptions = --no-ff
for the master and dev branches in .git/config so that every merge to those branches has a merge commit for later tracking.
Managing multiple remotes
You might want to use multiple remotes, for example a local GitLab instance, GitHub for automated integration with Circle CI, and a git instance at your hosting provider (e.g. Pantheon). You can do this by creating a placeholder remote called "all"
with multiple pushurl
URLs defined.
[remote "gitlab"]
url = [email protected]:projects/clientproject.git
fetch = +refs/heads/*:refs/remotes/gitlab/*
[remote "pantheon"]
url = ssh://[email protected]_hex.drush.in:2222/~/repository.git
fetch = +refs/heads/*:refs/remotes/pantheon/*
[remote "github"]
url = [email protected]:mycompany/clientproject.git
fetch = +refs/heads/*:refs/remotes/github/*
[remote "all"]
url = [email protected]:projects/clientproject.git
fetch = +refs/heads/*:refs/remotes/all/*
pushurl = [email protected]:projects/clientproject.git
pushurl = ssh://[email protected]_hex.drush.in:2222/~/repository.git
pushurl = [email protected]:mycompany/clientproject.git
Working with Branches
Naming conventions
- master: used only on the production website
- dev/123 or feature/123: used to add a feature or fix a bug, as defined in ticket 123 in your issue tracker (branched off of dev; will be merged back into dev and deleted when resolved)
- dev: used to stage all completed feature branches before they're released (was initially branched off of master)
- release/03: used to stage a set of features (a snapshot of the dev branch) for review and deployment (branched off of dev; will be merged into master and deleted when deployed)
- hotfix/234: used to fix an urgent bug (branched off of master; will be merged back into master and deleted when resolved)
List all branches
# update local copy of all remotes, removing branches which have been deleted
$ git fetch --all --prune
Fetching origin
# list all local and remote branches
# (shows hash and commit subject line, remote tracking branch, and status)
$ git branch -vva
dev 88c87e0 [origin/dev] Merged branch 'dev/456' into dev
dev/234 88c87e2 [origin/dev/234] Fixes footer; refs #234
* hotfix/345 88c87e3 [origin/hotfix/345: ahead 3, behind 1] Urgent homepage fix; refs #345
master 88c87e4 [origin/master: behind 10] Merged branch 'release/02' into master
release/03 88c87e6 [origin/release/03] Merged branch 'dev' into release/03
remotes/origin/dev 88c87e0 Merged branch 'dev/345' into dev
remotes/origin/dev/123 88c87e1 Fixes sidebar; refs #123
remotes/origin/dev/234 88c87e2 Fixes footer; refs #234
remotes/origin/HEAD -> origin/master
remotes/origin/hotfix/345 88c87e7Urgent homepage fix; refs #345
remotes/origin/master 88c87e6 Merged branch 'release/02' into master
remotes/origin/release/03 88c87e5 Merged branch 'dev' into release/03
Start working on a feature
git checkout -b dev/123 # create new branch
# work happens here
git merge dev # update feature branch
# more work happens here
# can rebase here if desired to clean up commit history
git push -u origin dev/123 # push branch to remote for review
Note: on rebasing
Rebase when you want to rewrite history (e.g. remove commits of debugging code); merge when you want to preserve history. You should only rebase a local branch that isn't yet pushed to origin and shared with others! If you're not already familiar with rebasing, ignore this section. If you really want to learn about this, this tutorial is helpful.
If your goal for rebasing is simply to reduce clutter in your git log, consider using git merge --squash
when merging feature branches into dev to combine all changes into one new commit. This is safer than rebasing, and won't cause problems for others.
Review and accept a feature
git checkout -t origin/dev/123
git merge dev
git diff dev # show all changes with respect to the dev branch
git diff dev --stat # list of changed files, with number of lines added and removed
git diff --name-status # list of changed files, tagged as modified, added, or deleted
# sync database from prod
# testing happens here
git checkout dev
git merge --no-ff dev/123 # force creation of a merge commit (so you can revert if needed)
git branch -d dev/123 # delete local branch
git push origin --delete dev/123 # delete remote branch
Prepare release branch
Once enough tickets have been closed and their corresponding feature branches merged into the dev branch, create a release branch and deploy to your staging environment for review.
On local machine
git checkout dev
git branch -b release/04
# sync database from prod
# perform final local tests of deployment steps
git push -u origin release/04
On staging server
# sync database from prod
git checkout -t release/04
# run deployment steps
Deploy release
On local machine
git checkout master
git merge --no-ff release/04
git tag release-04
git push --tags
On production server
git pull
# run deployment steps
Hotfix production site
On local machine
git checkout master
git branch -b hotfix/345
# fix up code
# testing happens here
git checkout master
git merge --no-ff hotfix/345
git tag release-04-hotfix-01
git push --tags
On production server
git pull
# run deployment steps
Resolve merge conflicts
Your tickets should be small enough in scope that merge conflicts don't happen very often. Here are three things you can do when they come up:
- Run git status to list files with conflicts, then edit each one manually
- Configure a merge tool which can help with three-way merges (see
git mergetool --tool-help
for supported options on your platform) - If you get lost halfway through:
git merge --abort
There are lots of resources on resolving conflicts online. Here's one good intro-level tutorial.
Here's an example of working with a three-way merge using vim.
Remove a feature that didn't pass review
git checkout dev
git revert -m 1 88c87e0 # revert the merge which brought in feature branch dev/456
git push
git checkout dev/456
git merge dev # update feature branch to include the revert
git revert 88c87e1 # revert the revert itself to return to the code that needs further work
A lengthy description of this process is available here.
Find the commit that made a given change
A common complaint about gitflow is that it fills your git history with merge commits, but in fact git allows you to exclude these with git log --no-merges
(or to show only merges with git log --merges
).
git log
git log --since="2017-01-13" --until="yesterday"
git log -3 # shows last 3 commits
git log --grep="refs #123" # searches commit messages
git log -S"needle" # searches diffs for fixed string "needle"
git log -G"ne+dle" # searches diffs for regex "ne+dle"