Our Git Workflow
NOTE: Brian Rosner pointed out that git merge --no-ff may accomplish exactly what we're looking for without messing with rebasing. He shared this post about a git branching model. I tried that method, but it still inserts the individual commits into the master timeline, making it unsuitable for a public changelog. The git merge --squash command, however, seems to accomplish the same thing as below without going through the rebase process and re-fixing any merge conflicts from before.
Since starting with Pegasus, I've become more and more familiar with the Git version control system. I actually use it for my own projects instead of Mercurial now, because I've gotten so much more comfortable with it. One thing that bothered me for a long time about our Git workflow, however, was how messy it was. We are all habitual committers (as I feel we should be) so when we finish a feature we often have 20 or 30 commits behind it. When we merged the feature into master, those commits were weaved into the other commits on master, making it very difficult to read the master commit log over time and making it even more difficult to roll back a change if we needed to. We also did a lot of development right on top of the master branch, which often let things slip into production before they were ready.
Thankfully, someone on Twitter shared a great link to a blog post on a Git workflow using rebase (thanks to Chris Edgemon for finding the original link for me). After reading up on the concept, I proposed the idea to our team and we decided to adopt it with some modification. We decided not to bother with a staging branch because it added more overhead than our small team needed.
Keep master clean and succinct
In summary, we're now doing almost all development on small local branches and then squashing those commits into one that is merged into master at the top of the log. That way, master's log serves as a true changelog with one commit for each ticket resolved, bug fixed, or feature added.
Larger branches are pushed to origin so that we can preserve the interim commits and collaborate. When the branch is done, it is converted to a tag and then squashed into an appropriate number of commits to represent each major feature of the branch. The stale branch is then removed from origin because the interim commits can be reviewed by checking out the tag.
Small one-commit changes are still done directly on master.
How the new workflow has helped us
I think this new workflow has done us a lot of good so far, and it has given us several advantages. First, the commit history on master is much easier to read. I've actually set up a post-deploy process that pulls a one-line summary of each change between the previously deployed revision and the new revision, and then emails that changelog to the development team and our manager. I've gotten a lot of positive feedback about the emails and how they keep everyone informed and reminded about the changes that are hitting production.
This also makes it easy to revert a change if it's breaking something. You can review git log to identify the commit for the broken feature, then use git revert to reverse it. Previously, we had to create a "before" tag to represent the clean state before the 20 or 30 feature commits were intertwined with master. Reverting the change would have been a nightmare!
Because it takes a few moments of effort to get something into the master branch, it helps prevent those snafus where something unfinished accidentally makes it onto production.
The clean changelog and automated emails also gave us increased visibility and communication. We're all aware of when the last deploy was and what was included, and the management team gets a copy of the email so they can accurately represent changes and timelines when approached by the business side.
In detail
Changes with multiple commits
Any new feature, bug fix, minor change, etc. that is more than one commit is done on a local branch. Once it is ready for production, it is be packaged into one commit with a summary message, and then merged with master.
Creating a local branch
First, create a local branch.
$ git checkout -b branch_name
Now, work as you normally do. Commit as many times as you want to, so that you have a lot of "save points" to go back to if you decide to go down a different path.
As you work, try to frequently merge in changes from master so that you don't get merge conflicts later on. To do this, first checkout master and do a pull. Then, checkout your local branch and merge with master.
$ git checkout master $ git pull $ git checkout branch_name $ git merge master
Pushing your local branch to your hub
If you need to collaborate on the change with someone else, you'll probably need to push your local branch to your version control hub (Github, another service, or your own Git hosting). You can then set up your local branch to track the new remote branch so that you can pull and push to the remote branch as usual. If you get an error message with the --set-upstream option, then you need to update your copy of git to version 1.7.0 or later.
$ git push origin branch_name $ git branch --set-upstream branch_name origin/branch_name
Then, others can check out your changes with a git fetch and a git checkout.
$ git fetch $ git checkout -t origin/branch_name
This creates a new local branch from the remote one, and sets up the local branch to track the remote.
Squashing your commits
When your change is ready for production, you'll need to squash your commits into one. First, do a pull on master to make sure you have the most recent changes and merge them into your local branch.
$ git checkout master $ git pull $ git checkout branch_name $ git merge master
Then, it's time to rebase your commits so that they use the most recent commit in master as their base, and squash them into one. Use the -i flag to make this an interactive rebase so that you can do the squashing.
$ git rebase -i master
You will see a screen like this:
pick 7d56733 Test change pick bc07784 Test change 2 # Rebase 3a08298..bc07784 onto 3a08298 # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # # If you remove a line here THAT COMMIT WILL BE LOST. # However, if you remove everything, the rebase will be aborted. #
Leave the pick label in front of the top commit, and change the word pick on the other commits to the letter s.
pick 7d56733 Test change s bc07784 Test change 2
Then, save the file and exit. Next, you will be shown a screen to edit the commit message that will be used for the squashed commit.
# This is a combination of 2 commits. # The first commit's message is: Test change # This is the 2nd commit message: Test change 2 # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # Not currently on any branch. # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: requirements.txt # modified: templates/largesite/base.html #
The lines that are commented out with # are ignored. Comment out the additional commit messages, and change the first commit message to a changelog-ready summary of the change.
# This is a combination of 2 commits. # The first commit's message is: Ticket #1234 - Change to fulfill documentation requirements # This is the 2nd commit message: # Test change 2
You can then use git log to verify that the squashed commits have been rolled up to one commit.
Merging with master
Next, checkout master again and pull to make sure there haven't been any changes while you were rebasing.
$ git checkout master $ git pull
If there were changes, you'll need to do a non-interactive rebase to move your commit to the top of master's changelog and avoid unnecessary "Merge branch 'master'" commits that will clutter the changelog.
$ git checkout branch_name $ git rebase master $ git checkout master
Now, you're ready to merge your squashed commit into master.
$ git merge branch_name
Now, use git log to make sure everything is correct. If you see the "Merge branch 'master' " message, you can rebase again by using git rebase origin/master. Once you're ready, you can send your change to master to be included in the next deploy.
$ git push origin master
Merge conflicts while rebasing
I've occasionally encountered merge conflicts while rebasing. I've tried using git mergetool to resolve these conflicts to some degree of success, but I sometimes find that I end up losing bits and pieces here and there that I didn't intend to revert. I've begun to suspect that it may be better to simply use git format-patch to create a patch against master instead of trying to rebase and handle conflicts. I plan to try this strategy next time I run into this situation. If you have insight into this, let me know in the comments.
One-commit changes
Very minor changes that don't need to be tested in dev and are sure to have only one commit can be done directly on master. Often, however, this results in an unneeded "Merge branch 'master'" commit message that shows up in the changelog. This is caused by changes that are made on origin/master after you started your change on your local master branch.
To see if your commit is going to cause one of these merge messages, look at git log right before you push. If you see the merge message at the top of the log, then you need to rebase your commit.
$ git rebase origin/master
Since this is a one-commit change, there's no need to use the -i option to squash commits like you would if you were merging a branch into master. This will change your commit to use the most recent remote commit as its base, instead of using the remote commit you started on from further back in the history.
Once that's done, you can go ahead and do a git push origin master.
Converting a one-commit change to a branch
If the micro change turns into a more significant one and you need more commits, you can convert your change to a branch. Enter git checkout -b branch_name, and it should carry your local changes over to the new branch.
If you've already committed one change, and you realize before you push to master that you need a second commit for it, you can use git reset HEAD^ to un-commit the change. Then, you can checkout a new branch to continue your changes using the workflow at the top of this page. Don't do this if you've already pushed the initial commit to the hub, however. If the commit is out there, it's possible that someone else already has it.
Your opinion
We've only been following this workflow for a few months, so I'm sure there's room for improvement. If you see something here that we could obviously be doing better, let me know in the comments below!
Comments
Brandon Konkle
I've been creating websites for over 10 years, and I've been using Django since early 2008. I focus on high quality, well-tested, maintainable code and reliable high-performance deployments. Web development is something that I am very excited about, and I love finding elegant and innovative ways to push web applications further.