A Small Team Git Workflow
Motivation
In my university courses, it was often the case that team members assigned to group projects had widely varying levels of experience with version control tools. For one project in particular, I was the most experienced with Git and offered to set out our team's collaborative workflow. I wrote this guide to (a) lay out the workflow, and (b) help my team-members learn to use Git collaboratively.
My goal was to make it detailed enough that team-members with little Git experience could follow the guide in order to meaningfully contribute to the project. It does assume some prior knowledge of Git (e.g., what a "commit" is), but overall I've tried to:
- Limit the use of jargon, and clearly introduce Git terminology before using it
- Introduce concepts by showing the motivation for different steps
- Show how to do each step with both a GUI and the command line
Environment
The guide assumes the following development tools are being used:
- Version control: Git
- Project management: Jira
- Git hosting: Bitbucket
- Git GUI: GitKraken
Our team was constrained to Jira for project management and Bitbucket for Git hosting. All team members were running versions of Microsoft Windows. However, the workflow steps are fairly general and you can substitute references to Bitbucket and Jira with your Git host and task management tool of choice. The only thing unbendingly specific is the choice of GUI tools - descriptions of how to do things in GitKraken do not necessarily transfer well to other GUIs.
Preconditions
The guide starts assuming a sprint has been planned:
- We decided what stories to work on
- Stories have been subdivided into tasks
- We've attempted to estimate the difficulty of the different stories
- At least some of the tasks have been assigned to individual people
From there, the next 3 top-level sections cover how to use Git when completing a task. The final section contains additional explanation of how rebasing works and why we use it.
Starting a Task
1) Choose a task to work on
Go to the current sprint in Jira, and look for items in the "to-do" list. Pick higher priority tasks that either (a) have been assigned to you or (b) have been assigned to no one. It's also easier to work on tasks that have no blockers - the task doesn't depend on another task in order to be completed.
2) Make sure your local copy of the project is up to date
If other people have uploaded work to Bitbucket since the last time you "fetched" in your copy of the project, you will want to download those changes, so that your new task is based on the most recent version of the app.
a) fetch
The Git repo on your computer keeps two sets of branches: your local branches, and branches that match the branches on Bitbucket. The branches that match the ones on Bitbucket are called “remote tracking branches” and are prefixed with the name of the remote (usually “origin”). The first step is to make sure your remote tracking branches are updated so that you can see other people’s work.
In GitKraken:
By default GitKraken should automatically download new changes every few minutes. You can click on the arrow by the “Pull” button and select “Fetch” in the dropdown menu:
In Git Bash:
$ git fetch --all
b) fast-forward major branches
After fetching, if there were any changes on Bitbucket, any branch that exists on your computer and on Bitbucket might now be in two different places: where your computer thinks it is, and where Bitbucket thinks it is. In GitKraken, this situation looks like this:
You can see master and develop are in two different places: one is marked with the little computer icon – these are my local branches. The others are marked with a little Bitbucket icon – these are the remote tracking branches. The version of the develop and master branches on my computer are behind the versions on Bitbucket. You can also see in the branch list in the left sidebar that Local/develop and Local/master are marked with a number and down-pointing arrow, showing that these branches are behind their remote counterparts by some number of commits.
To fix this, we do a “fast-forward merge” of origin/master into master, and likewise for origin/develop and develop. “Fast-forwarding” is a special kind of merge that does not create a separate merge commit. It is only possible when the branch that you are merging into (destination) does not have any commits that aren’t already in the source branch.
In GitKraken:
Checkout the branch you want to fast-forward, then right click the remote tracking branch and select “Fast-forward <branch> to origin/<branch>”:
In Git Bash:
$ git checkout <branch-to-fast-forward>
$ git merge --ff-only origin/<branch-to-fast-forward-to>
When you’re done, master and develop should match on your computer and on Bitbucket, like so:
3) Create a branch for your task
Now you need to make a branch for the changes related to the task you are going to work on. The branch needs to be based on the develop branch and must include the Jira task/story identifier in the branch name. For example, let’s say the task ID on Jira is “DES-12” and the task is to implement a file upload form. Then at minimum, your branch name should be “des-12”, but you can also include some descriptive words after the identifier by using hyphens, like “des-12-upload-form”. This will help everyone tell which branches are for which tasks.
In GitKraken:
Make sure you have checked out develop, then right-click on develop and select “Create branch here”. Enter your branch name, then hit Enter.
In Git Bash:
$ git checkout -b <new-branch-name> develop
4) Mark the task on Jira as “in progress”
Find your task on Jira, and make sure the status is set to “In progress”.
Doing work on a task
5) Make some changes to the project
This is the part where you actually make changes to the code.
6) Make a commit of your work
Once you’ve made some changes, stage those changes and create a commit with a commit message that describes what the change does. How often to commit and how much to include in a commit is a bit of an art that you just have to practice; but here are some rules of thumb to think about:
-
Are the changes in this commit logically related? It’s best to try to make your commits logical units of work; if your commit does two different things, it may be better to have two different commits.
-
Will someone else be able to look at this commit and quickly understand what changes were made? Someone will have to review your code before it is merged; it is helpful to have commits that are small enough to easily review.
You can Do a little bit of work, then Commit, then Do a little bit of work, then Commit, etc. – repeating this cycle as necessary until you’re finished. Or, you can Do a Lot of Work, and divide the changes into smaller commits using staging: Stage a few related changes, commit, stage a few related changes, commit, etc. until all your changes have been committed.
Things you should not commit…
Generally, anything that is specific to your personal development preferences or the development environment on one specific computer. For example, some PyCharm files are okay to share, because they will be the same no matter who opens the project, while others are specific to your user and computer. If in doubt, don’t commit changes to anything other than code files that you edited yourself!
7) Push commits to Bitbucket
At some point, you will want to push your feature branch to Bitbucket so that other people (and Jira!) can see what you’ve finished. You can do this at any point – you don’t have to have finished the whole task yet! Pushing a branch makes a copy of the branch on Bitbucket that mirrors the branch on your computer.
a) the first time
Remember the remote tracking branches from earlier? The first time you push your feature branch, we actually create one of those. Then whenever you push your feature branch again, it will just upload any commits that are in your version of the branch but not on Bitbucket yet.
In GitKraken:
Make sure your feature branch is checked-out, then click the “Push” button on the toolbar. It will ask you for a branch name to use on the remote: in this case, leave the default and click “Submit”.
In Git Bash:
$ git push –set-upstream origin <your-branch>
b) subsequent pushes
You can continue to make commits on your feature branch. As you do, the version on Bitbucket will become behind the branch on your computer. This looks like this:
Notice how there are two versions of “PRC-2-just-demo”: one on Bitbucket, and one on my computer. The version on Bitbucket has one of my commits, but not the most recent two. Also notice that branch is marked with a number and upward-facing arrow in the branch list; this indicates that the local branch is 2 commits ahead of the remote one. Updating the version on Bitbucket is easy:
In GitKraken:
Click the “Push” button again.
In Git Bash:
$ git push
The circle of life
While you’re working on a task, you can cycle through the process of
change → commit → push (optional) → repeat
As many times as needed until you’re done. As long as you’re working on a feature branch that is specific to you personally, it is very difficult to do any damage to the repository.
Finishing a task
8) rebase your feature branch
It is very common that while you are working on a feature branch, someone else will complete a task before you. Another feature branch will be merged into develop, so you get a graph that looks like this:
The develop branch on Bitbucket is now ahead again! And not only that, but your feature branch is now based on the old version of develop. We want to somehow get the most recent changes into your feature branch so that you aren’t basing your work off an old version of the app. We also want to try to keep the Git history relatively clean, so it’s easier for us to understand later. To do this, we will use rebasing[1].
a) Update local copy of develop
Checkout develop and fast-forward it, just like you did before starting your new branch.
b) Rebase your branch onto develop
Rebase takes the commits from your branch and copies them onto the new base you specify, effectively moving your branch so that it starts at the new version of develop.
In GitKraken:
In Git Bash:
git rebase develop <your-branch>
Conflicts
If two different branches have made changes to the same parts of the same files, it is possible for there to be a “merge conflict” at this point: rebase will try to create the copies of your changes but won’t know what to do, because one of your changes conflicts with one of the new changes in the develop branch. In that case, Git will pause in the middle of the rebase and ask you to “resolve merge conflicts” before continuing. To do this, you will have to edit the files containing conflicts so that the appropriate changes are preserved. Usually this is pretty obvious, but you may need to communicate with the author of the conflicting changes to make sure no important changes by them are deleted. Once you’ve edited and saved the files, you mark each conflicting file as resolved and can tell Git to continue the rebase.
[To-do: examples]
c) force-push your branch to Bitbucket
At this point, here is what we’ve got:
If we try to push our branch, we get the following error:
Do not Pull! That is the correct thing to do in some cases; but in this case, what we actually want to do is to force the branch on Bitbucket to match the branch on our computer. That said, only Force-Push a feature branch that you are responsible for and that does not have any child branches. Git considers force-pushing to be a dangerous operation because it can cause history to be overwritten or child branches to be orphaned (which leaves duplicate commits in the history). However, if your branch is not the parent of any other branches, then it is safe to force-push.
In GitKraken:
Push, then select the red “Force-Push” option, then select the option again when it asks if you’re sure.
In Git Bash:
git push -f
This should leave you with a graph that looks like this:
The local and remote copies of our feature branch match, and are based on the most recent version of the develop branch.
9) Open a pull request
Go to the repository page on Bitbucket, then select “Pull Requests” in the sidebar. On the Pull Requests page, click the “Create Pull Request” button near the top-right of the page. The form looks like this:
-
The branch on the left should be your feature branch; the branch on the right should be “develop”.
-
Bitbucket automatically fills the title with the branch name. Change the title to make it more readable or descriptive if needed.
-
Bitbucket automatically puts a list of commits messages in the Description box. This is a good place to describe the task, the changes you made/strategy you used to complete the task, and any problems you had. It’s also nice to insert a hyperlink to the task on Jira.
-
If you need a specific person to review your pull request, put their username here; otherwise this is fine to leave blank.
-
Checking this box deletes the source branch when the pull request is merged. Safe to use on feature branches, but never use this when merging major branches (e.g. merging develop into master).
10) Make changes based on feedback
Someone will be responsible for reviewing your pull request. They may leave comments on the pull request asking for specific changes to be made before the pull request is merged. If needed, go checkout the branch on your computer, and continue with the “change → commit → push” cycle. Any commit you push to the feature branch will automatically be added to any open pull requests for that branch; then just leave a comment saying you have fixed the problems.
11) Pull request is closed
Once the pull request has been approved, it will be merged into the develop branch. The version of develop on Bitbucket will move forward, and everyone will need to fast-forward develop on their computers when they want to try out the new version.
12) Mark the task as finished on Jira
Pretty obvious – one you are done working on your task and the corresponding feature branch has been merged into develop, go to Jira and make sure the task is marked as “Done”. This helps us all track the progress we’re making on the sprint.
And that’s everything I can think of right now! Congratulations, you have finished a task and used Git to collaborate with the team. You can start back at the beginning for future tasks!
More on Rebasing
Updating a feature branch with new changes from its upstream branch can be handled in one of two ways – merging or rebasing. Although there are pros and cons to both approaches, I prefer rebasing for reasons including:
-
It places the responsibility for resolving merge conflicts on the owner of the feature branch, rather than the owner of the development/master branch
-
It results in a cleaner commit history that is easier to understand
-
It results in a cleaner commit history that makes it easier to see when, where, and how bugs were introduced, for example using tools like git-bisect.
The premise
You and someone else start working on different features at the same time.
The other person finishes their feature and opens a pull request, which is merged into the develop branch.
The problem
At this point, if you were to merge your feature directly into the develop branch, the history graph would look like this:
With only two branches in this simple example, that doesn’t look so bad. But with many people working on lots of features over an extended period of time, simply merging will end up looking something like this:
(This is a screenshot of an actual repository history with uncontrolled merges and many long-running branches.)
The solution
If instead of merging immediately, you rebase your branch on develop, you get a history that looks like this:
Rebasing has copied the commits F and I so that they are rooted on the most recent commit in develop, and moved the Your Feature branch to point at the new copies. This has the effect of taking your branch, clipping it off of its parent, and re-inserting it at the point you rebase to.
Now if you merge your feature, the history will look like this:
Over time, using this pattern arranges the history so that it looks hierarchical, for example:
In practice it will often look more complex, but a history like this is a lot easier to make sense of.
For more explanation of how rebase works and why we use this method instead of something else, see "More on Rebasing" ↩︎