In this article I will show you a basic GIT workflow that you can use in your projects. This workflow is for newbies, if you are an experienced GIT user you won’t need this. Also this isn’t the most scalable or efficient way but it’s easy to follow and I think it’s good enough for small teams. If you just moved to GIT from (hopefully) another versioning system, you’ll most probably want to embrace a push workflow. In this workflow every team member push their commits to a common (“central”) repository on one of the primary branches. It is similar with a centralized system and is suited for medium-small teams.
Creating the repository
You need a central GIT repository on a shared server, with some access rights configured. You can create a plain GIT repository using the init command but I strongly suggest to use GitLab, GitHub or any other repository manager. Installing GitLab or other GIT repository manager is relatively easy and recommended because these are bundled with lots of cool features. Once you created the repository, every team member can clone it using the clone command:
git clone <repository url>
The primary branches are the long lived branches, shared by multiple developers. By default we have two primary branches: master and release. You can define more primary branches for long running tasks if you need to. The master branch is considered the development branch and all new (and possibly untested) features goes here. I chose the master branch for development, because it is selected by default when you clone the repository. The release branch will be used for the release builds and bug fixes only. Obviously by bugs I mean bugs from the release code. The bugs introduced by the new features on master must be fixed on master.
As you can see in the image above, the new features from the master branch won’t affect the release branch. You can safely do the build with the bugfix commit. Other primary branches will be treated as development branches similar with the master branch. The goal is to maintain a clean project history. Merge commits should be relevant and between primary branches only. By looking at the commit tree, you should see only information relevant to the project itself. Your project history should look something like this:
A secondary branch is a short lived, local only branch. Ideally all your tasks should be done on secondary branches.
No matter what task you are working on, you should follow these basic rules:
- The primary branches should be kept clean. You shouldn’t commit directly to the primary branch, use a secondary (local only) branch instead.
- Secondary branches must be based on commits from primary branches only. Do not base your work on other people’s crap!.
- You should not merge from secondary to primary branches, use cherry-pick instead.
- Merge operation is only permitted between two distinct primary branches and should always have a custom merge commit message.
- Only fast forward pulls are allowed. When push is rejected because your primary branch is not in fast forward state you should not use the pull-push strategy. It will pollute your primary branch with a lot of meaningless merge commits.
- Changing the history of the primary branches is strictly forbidden!
- Once you’ve finished a task, the commits for that task must be pushed back to the original primary branch. For example, if you do a new feature starting from the master branch, you should push back to the master branch.
Create a primary branch
# Create and go to the local primary branch git checkout -b primary # Push primary branch to origin to make it publicly available git push origin primary
Create a secondary branch
# Go to the primary branch. git checkout primary # Make sure the primary branch is up to date. # Make sure pull is fast forward only. git pull origin primary --ff-only # Create a secondary branch starting from the primary branch git checkout -b secondary
You are on a secondary branch, so you are free to make some changes to the project. You can make as many commits as you want. To make a commit you can use the commit command as follows:
# Stage all modified files. git add --all # Commit your changes. git commit -m “Commit message”
You’ve done some work, now let’s see how you can publish the commits back to the primary branch.
# Go back to the primary branch. git checkout primary # Synchronize local primary branch with the remote one. git pull origin primary --ff-only # Grab the commit from the secondary branch and copy it on top # of primary. git cherry-pick <commit sha> # In case of conflicts you will need to fix them and then # run git cherry-pick --continue # Push back changes to primary branch. git push origin primary # Delete the secondary branch. You don’t need it anymore. # If you need to extend the task, you will start another secondary branch # from the commit you’ve just created. git branch -D secondary
If you have multiple commits, you will need to run cherry-pick for each of them.
To squash commits from the secondary branch into a single commit you can use the -n argument for the cherry pick command. This will copy the changes of the commits but it won’t actually create the commit. You can copy changes from multiple commits and then create a single commit with those changes.
# Go back to the primary branch. git checkout primary # Synchronize local primary branch with the remote one. git pull origin primary --ff-only # Copy changes from the commits with sha 1 .. sha n. git cherry-pick -n <commit sha 1> ... git cherry-pick -n <commit sha n> # In case of conflicts, GIT will complain and show you the # conflicts, fix them and then: # Stage all changes. git add --all # Create a single commit with all the changes. git commit -m “Message” # Push back changes to primary branch. git push origin primary # Delete the secondary branch. We don’t need it anymore. # If we need to extend the task, we will start another secondary # branch from the commit we just created. git branch -D secondary
Note that in the example above you don’t need to include all your commits into the final commit. You can skip commits you don’t want, or squash specific commits, and then cherry pick the others.
What to do if push is rejected?
Push operation can be rejected if your local primary branch is not in fast forward state with the origin branch. This can happen if somebody else already pushed to the remote primary branch before you. To get out of this situation you should:
- Go to the primary branch and create a temporary branch starting from it.
- Delete the primary branch and recreate it from origin using the checkout command.
- Now your changes are on the temporary branch and the primary branch is in synch with the remote branch. Simply, cherry pick the changes on top of your updated primary branch. Just follow the guide from the Publish changes section.
# You are on a desynchronised primary branch. git checkout primary # Create a temporary branch from the current head. git checkout -b temporary # Delete the local primary branch because by adding commits # to it we made it divergent from the origin. git branch -D primary # Recreate the primary branch from the origin. git checkout primary # Now you are on a fresh primary branch, your work is on # the temporary branch, you are good to go.
What to do if you accidentally pulled and created a merge commit?
In this case what you really want to do is to delete that merge commit, and restore your primary branch to be exactly the same as the origin primary branch. In other words you want to do exactly the same steps as described in the What to do if push is rejected? section, plus an extra deletion step.
# You are on a desynchronised primary branch that contains the # merge commit git checkout primary # Go to the your last commit, and start your temporary branch # from your last commit. git checkout <sha of your last commit> # Create a temporary branch from your last commit. git checkout -b temporary # Delete the local primary branch because by adding commits # to it we made it divergent from the origin. This operation # will delete the merge commit too. git branch -D primary # Recreate the primary branch from the origin. git checkout primary # Now you are on a fresh primary branch, your work is on # the temporary branch, you are good to go.
What to do if you’ve started a task from a primary branch?
If you didn’t make any commit yet, you are lucky. You can easily switch to the secondary branch right away with the checkout command:
# Create and go to a secondary branch. git checkout -b secondary
If the task is ready and we want to push it back to origin, we don’t even need to create the secondary branch. We can do the commit right away as follows:
# Put your changes into the stash. git stash # Download all commits from the primary branch git pull origin primary --ff-only # Apply your changes on top of the primary branch git stash apply # Stage all changes. git add --all # Squash commits into a single commit. git commit -m “Message” # Push back changes to primary branch. git push origin primary # Drop changes from stash. git stash drop
If you already made some commits on the primary branch directly, you can use the workflow described at the Publish changes section. It is the exact same scenario.
What to do if you based your changes on a secondary branch?
This scenario is not catastrophic either. In this case there is a big chance you will need to fix some merge conflict and adjust your code, because you based your work on unstable commits. Anyway, you can use the exact same workflow as described above (Publish changes section). The secondary branch is also a branch, it works the same way.
What to do when you are interrupted?
Let’s suppose you are interrupted and you need to work on another task right away. To start working on another task you should first stash the uncommitted changes, and then, start working on a task as described above. Let’s see the command sequence:
# Stash uncommitted changes. git stash # Go back to the primary branch. git checkout primary # Synchronize local primary branch with the remote one. git pull origin primary # Create and go to a secondary branch. We use this branch # to implement our second task. git checkout -b secondary_2
Now you simply work on our secondary_2 branch, when finished you publish the commits, go back to our original secondary branch and continue to work on the original task.
Merge two primary branches
If you want to join two primary branches together you must use a merge operation with a custom merge message. For example, you want to do a merge between the master and release branches, when you do a new release build. The merge message should describe why was the merge operation needed. For example: “Merged devel code into release because we wanted to include it into the release version 1.0.0”. The merge sequence is simple:
# We want to merge primary2 branch into primary1. # The primary1 branch will contain all the commits, # primary2 branch remains the same. # Go to primary2 branch and update it git checkout primary2 git pull origin primary2 --ff-only # Go to primary1 branch and update it git checkout primary1 git pull origin primary1 --ff-only # Merge primary2 branch into primary1. git merge primary2 -m “My custom message” # If we want primary2 branch to be exactly the same as primary1 # we can now do the merge in reverse order. git checkout primary2 git merge primary1
What to do in case of merge conflicts?
In case of merge conflicts, GIT will complain, will show you the conflicts and you will need to fix them. After you fix the conflicts you will need to create the merge commit manually:
git add --all git commit -m “My custom merge message”
How to abort a merge operation in case of conflicts?
git reset --merge