For my very first post I wanted to tell a story about how I learned to use and love Git, but I'd like to start much earlier on, when I was just learning about VCS tools.
While studying I was exposed to SVN as my default VCS, we struggled with a couple of friends trying to understand its mysteries while developing the prototype of some forgotten web application.
Our young and unworthy minds could barely grasp the very concept of trunks, branches, commits, and everything else in between. We would finally set on a single trunk and issue related branches to separate our work.
But our poor understanding led us to make several mistakes when merging our work, with a mixed set of problems, varying from methods getting scrambled, to complete classes being overwritten. Many "accidents" later we gave up on the idea of a VCS, we weren't capable of both handling its components and understanding how did that tool fit into our workflow. Thus I gave up entirely, that early on, on even using such tools.
So I kept to my studies and from time to time developing, somehow, without any form of VCS, until I landed my first serious job (and to the date of this article, my current one). It was December 2013, and I was to join a small team of developers that had been using Git for just over a year. I was blown away when they explained to me what it was, how they used it and how simple it felt over my previous tragic experience with SVN.
Back then the development workflow was centered around two main Git branches: master that hosted all of the verified and stable code deployed to our production environment and test, a branch that contained all the code that had to be reviewed by our QA team, this branch was deployed to a special "testing" environment. Also, each developer would create a feature/bugfix branch to solve particular issues, these branches were to be deployed on local instances of the system for every developer. Once each developer finished their work they would immediately merge it into the test branch.
At certain times during the day, our head developer would deploy the contents of the test branch into the testing environment. Once the testing was done and all of the content of the test branch was considered as "approved", then our head developer would merge the test branch into our master branch and proceed to deploy all of its contents into the production environment.
Please consider that at that time I thought that was the most awesome thing I had ever seen ...
This workflow had many problems, among them:
- No feature or bugfix branch was updated with the changes added to the master branch in between deploys. This caused massive conflicts for older features that sometimes implied making the whole thing again.
- All merging was done with fast-forward (at that time we had no idea what that meant) making the logs barely readable for tracking groups of changes made to the system.
- To accomplish the production deploy our head developer would have all of us stop merging our features and fixtures into the test branch until he was done. Occasionally a developer would forget this and merge something into it causing what in my country slang we would call a "midfield goal" (mandatory soccer reference from a Chilean ... check) effectively deploying code straight into production without anyone from the QA team giving it a simple AOK.
As a graph, it looked something like this:
Somehow we managed to grind through with this workflow up to mid 2014 when we finally hit critical mass, the poorly solved conflicts and the many "midfield goals" placed us on a really bad place, missing deadlines and not being totally capable of guaranteeing stable-ish code on our production environment, we needed a change and we needed it fast.
Enter Gitflow, descending upon us from the hands of our fearless head developer, a "new-to-us" way to organize our work and reclaim control of the mess we had on the master branch. We were to add the development branch to separate the lifecycles of our feature and bugfix branches and use the release branches to gradually add features to the system in a controlled and, hopefully, planned manner. Tags were to be used not only as flags to indicate the given status of the application in the git history but also as specific points to perform deployments to the production environment.
We had the added challenge that our test branch had to stay, because it was still the only way we had to deploy code so it could be tested by our QA team, so in practice, we ended up with two Gitflows, one for test branch and one for the master branch. Bugfixes and releases would get merged into either branch given its current status.
As a graph this new workflow looked like this:
We were also told that we would no longer have to wait to deploy merged code to the test branch for bugfixes. If a developer finished, in addition to from immediately merging the branch into the test branch they would also deploy that code into the test environment.
And for a while this was ok.
You see, we thought that just by changing the branching strategy would solve all our problems, when we should have also reviewed our handling of the branches, specifically:
- We still weren't properly updating old branches.
- We still weren't sure what this "fast-forward" strategy was.
- Conflicts were resolved without really paying much attention to what was really happening. At that time we weren't using diff tools to resolve merges, and a popular strategy in the office was just accepting the changes currently being merged into the target branch as the "correct" ones, instead of having to read and manually accepting either set of changes.
- The test branch started to "clutter" with all the issue related branches we were constantly merging, causing many unnecessary conflicts and false positives on issues that weren't really solved.
17 releases ... that was all it took us to crash and burn again. A year and a half had almost passed since we adopted this modified Gitflow and that last release took at least a week and a half to fully solve every single conflict it had.
We had to fix this mess and we weren´t sure what to do, we started by dropping GitFlow altogether, going back to our "2 main branch workflow", and slowly trying to identify what went wrong. From there we started addressing each problem detected, one at a time:
- We learned about the different strategies to keep our development up to date.
- We established a proper channel to discuss the resolution of conflicts.
- We agreed to use non fast-forward merges so our code would be easily grouped an read.
With that in mind, we designed a workflow model, based on both our past experiences and things that we found from articles online, that could be adapted to our learning process and to the reality of our development process lifecycle.
We ended up setting on a workflow that required:
- That each branch that hasn't been merged into master must be updated relative to the last pushed tag.
- The branch update strategy was through rebasing.
- Any merge done into the master or test branches is done in "non-fast-forward" format, leaving an additional commit, properly identifying when an issue related branch was merged.
- The test branch must be as close as possible in content to the latest pushed tag on master.
We also dropped the immediate test merge and deploy of any issue related branches and opted for a "wait until we really need this on the test environment" approach.
Also, we adopted a procedure where whenever we push a new tag to master, we would delete the test branch and started a new one by branching from the newly pushed tag.
With these changes we managed to:
Almost completely eliminate big conflicts on either main branch. And reduce small conflicts from a "daily frequency on the majority of the issue related branches" to "a once a month on a couple issue related branches".
Create a simple and readable git history, where all the work performed for each issue could be easily identifiable.
Almost completely eliminate false positives during the testing phase of the issues.
We've continued to tweak this model up to this very day, the last thing we've done is introduce Gitlab to the mix hoping to use Merge Requests and CI/CD in the near future. We hope to continue to grow and establish a more robust workflow with each passing day.
As I look back on this story, I almost can't believe how much has passed and how much we had to work and learn to get to where we are. I'll try not to forget this as I continue to learn.
Thanks for taking the time to read this :)