Branching and Merging
When it comes to versioning in software, it's important to understand how to manipulate the version timeline. Since Git is very familiar with branching timelines for files and programs, it has many tools to do it well, and we will be trying to take full advantage of them. For this page (and most other pages) chats will be used. Parenthesis are versions (which Git calls a Commit), and lines are the lineage of changes (as in, what a version is using as the previous version to represent changes from).
Branches
The first large concept is the Branch, a label for a straight line of relationships between versions. On a simple project with a single person, or in version managers outside of Git, you will have a single branch of changes. In git, this primary branch is called Main (as in, the Main Branch). It would look something like this:
main ( )--( )--( )--( )
Just a chain, in a row, of changes. However, this runs into the issues described on the previous page very quickly. This is where multiple branches come in. Instead of a single main branch which is constantly changing and breaking, developers should create new branches based on existing versions where they can make new changes and versions in parallel. In a sense they are making a parallel version timeline based on a specific version in the main timeline.
main ( )--( )--( )--( )
\
new-branch ( )--( )
From here, any changes made in the main branch will be kept out of the new branch. Any changes to the new branch will be kept out of the main branch. Multiple branches can be present at once, and they can be based on different versions. It would not be uncommon to find something like this:
third-branch ( )--( )
/
main ( )--( )--( )--( )--( )--( )
\
new-branch ( )--( )
Where multiple branches are coming off the main timeline and being worked on separately from each other until their task is done.
Merging Branches
The only thing that makes branches manageable in the end is the ability to resolve them. Since most branches are temporary, such as fixing a bug or adding a feature, they are built to resolve quickly as well. Resolving a timeline/branch is called a merge, and there are a few approaches. I'm going to cover each of those ways here. They are in the order that I recommend trying to use them when merging branches.
Fast-Forward Merge
Fast-Forward Merging is Collapsing down a branch into the main branch, which can only be done in the event that the side branch is based on the most recent version of the main branch. It would need to be something like this:
main (A)--(B)--(C)
\
side-branch (D)--(E)
In this scenario, you can collapse the branch structure straight back into main, since the changes on the side branch can be seen as otherwise linear. If there were any more versions in the main branch from after the side branch formed, you cannot collapse it like this.
main (A)--(B)--(C)----(D)--(E)
This is by far the easiest to work with, but it's also rare, given that it requires other branches to have not been active in the mean time.
Merge
The standard merge, sometimes called a 3-Point Merge in Git, entails taking all the changes made since the version that formed the side branch, and applying them back into the main branch using a special version/commit that denotes the branches have merged. If there is a change inside the main branch and side branch that conflict, the merge is paused until the change is manually resolved. This preserves the history of the branch, and is generally recommended. Given a branch structure like this:
main (A)--(B)--(C)--(F)--(G)
\
side-branch (D)--(E)
A merge like this would check 3 points, C (the original point of divergence), E (the changes made in the side timeline), and G (the changes made in the main timeline). If they are all compatible with each other, then they merge like below. If they do not, then you will get a merge conflict, where you must manually adjust the content of the merge commit itself to force C, E, and G to be compatible.
main (A)--(B)--(C)--(F)--(G)-----(M)
\ /
side-branch ---(D)---(E)---
This also does not stop the side branch from continuing on (as in, the side branch can still be used as a parallel timeline). This lets merges be used just to keep a side branch up to date with the main branch instead (since changes can be merged into any branch). From the example before, you could also have a merge like this:
main (A)--(B)--(C)--(F)--(G)
\ \
side-branch (D)--(E)--(M)
From there both branches can continue
main (A)--(B)--(C)--(F)--(G)--(I)--(J)--(K)
\ \
side-branch (D)--(E)--(M)--(H)
And be merged again
main (A)--(B)--(C)--(F)--(G)--(I)--(J)--(K)--(M)
\ \ /
side-branch (D)--(E)--(M)------(H)------
Timelines using merging like this can end up being more complex than the options below, but they are also more well suited for representing complexity that arises from collective development of large programs.
Rebase
For those who want to keep the aesthetics of a single version timeline, in spite of the actual development being much more complex, rebase might be more appealing. Rebase takes a branch like this:
main (A)--(B)--(C)--(F)--(G)
\
side-branch (D)--(E)
And proceeds to try and shift the origin of the side branch to the most recent version/commit in the main branch. This means taking the contents of version D (as in, what it changes), and attempting to make them reference G instead of C. Rebase tries to take the branch and make it look like this:
main (A)--(B)--(C)--(F)--(G)
\
side-branch (D)--(E)
However, because D cannot actually be descended from G, as all of it's changes are cryptographically based on version C, it cannot actually just shift those versions around. Instead, it takes version D and E, and creates new copies that assert the same changes but cryptographically related to version G instead. To denote the difference, 'D will be the duplicate of D, and 'E will be the duplicate of E
main (A)--(B)--(C)--(F)--(G)
\
side-branch ('D)--('E)
From here, will the changed history, a Fast Forward merge can easily be preformed, making the end result of the rebase and fast forward look like this
main (A)--(B)--(C)--(F)--(G)----('D)--('E)
Squash Commit
The most brute force option, and by far most historically destructive, is the squash commit. Instead of using any of the previous attempts to preserve useful information from the development of a branch, a squash commit erases all context to turn the entire arc of a side branch into a single version/commit change on the main branch. It turns something like this
main (A)--(B)--(C)--(F)--(G)
\
side-branch (D)--(E)
Into this, where all changes made by version D and E are squashed into version H
main (A)--(B)--(C)--(F)--(G)--(H)
Clean? yes. Nightmare? More than you could ever imagine.
No comments to display
No comments to display