Merging and conflicts

Last Section:

(warning slightly technical section)

👉 You can skip the top section of this page if you are not an engineer or you are not familiar with git!

To understand merging, we have to explain the order in which the commit trees in floro grow and how they are merged together. Floro has one giant difference from git in this respect -- floro commits cannot have more than one parent. There are many advantages to this approach, the biggest one is that you get simple linear commit history. This also greatly reduces the complexity of resolving merge conflicts. The drawback is that it can be a little bit harder to make sense of how a commit history came to be.

You do not need to understand the details of how commit trees are merged but it can be a bit confusing why a branch ends up where it does after a merge if you don't understand how floro thinks about commits, branches and merges.

To explain this, we are going to do our best to show a fairly common scenario and illustrate how floro handles the merge history.

Initially we start off with a simple commit tree. Our first user has made a branch (F1) that branches off of the main branch at commit A. Our second user has made a branch (F2) that branches off of the main branch at commit B. Since F2 was originally created, an additional commit C has been appended to the main branch.

Since commit C has been appended to the main branch, our first user's branch is out of date. To fix this, they need to merge the main branch into their branch (F1). We could describe this operation as the following:

main (merging into) -> F1 (F1 is altered)

So what happened? Because the most recent common commit ancestor between F1 and main was the commit A, F1 knew it needed to include the changes it was missing from main, in this case that was commit B and commit C. It had to rebase the commits B and C on top of commit Y. The SHA values of B and C did change. We rewrote the rebased commits as B' and C'. We then made a merge commit called M0 that is the commit of the merge between commit C and commit Y.

Now let's imagine our second user does the same thing with their branch (F2).

main (merging into) -> F2 (F2 is altered)

This time the most recent common commit ancestor between main and F2 was commit B. Therefore the only change we had to rebase was commit C. As we've already rewritten the rebase of commit C as C' when its parent is commit Y, we rewrote the second rebased version of commit C as C", whose parent is commit J. Finally, commit M1 is the merge commit between J and C.

As user one's branch F1 no longer has any conflicts with main, when they merge F1 into main, the only thing that happens is the branch head of main updates to the branch head of F1, which is M0.

F1 (merging into) -> main (main is altered)

While F2 was previously un-conflicted with main, after F1 was merged into main. conflicts now exist between F2 and main again.

Again we will merge main into F2.

main (merging into) -> F2 (F2 is altered)

It's not as complicated as it looks, we promise! When we merged main into F2, the most recent common commit ancestor was C' and C", remember, they're really the same commit. For this reason, when F2 rebased its history on top of M0, it excluded C" from its rebase list. M1 also needed to be rebased, which is why it is denoted as M1' in the new commit lineage. Finally, M2 was created, which is the reconciled merge of M0 and M1.

Now, F2 is able to merge with main again. So we merge F2 into main again.

F2 (merging into) -> main (main is altered)

As main is a direct descendent of F2, we simply move the head of main up to the head of F2. In the vast majority of cases this is the end. F1 would have likely been deleted at this point. If we did merge main into F1 again, we would finally converge at the same point.

main (merging into) -> F1 (F1 is altered)

And that's really all there is to merging! In practice, you do not need to remember any of this. It is helpful for reading commit history to have some idea of what is going on though.

It should be apparent that the order in which merges occur affects the order of the commit history, however, once all branches are merged, they should eventually converge to the same point.

Merging rule of thumb

Merging can get tricky when we think about it conceptually. In practice, it's really simple. You almost always merge the main branch into your feature branches locally. You merge your feature branch into the main branch remotely.

Merging locally

We've setup a local repository with a deliberate merge conflict. Our conflict exists with the current head of main. Here is a picture from our sha graph, which shows the current state of our repository.

We are now going to attempt to merge the main branch into our branch, conflict branch. To do this we need to enter compare mode from the version control panel, we need to change the comparing selector from uncommitted changes to branch. Make sure the branch we are comparing is main. We can now see above the merge button that there is a warning informing us that we cannot automatically merge. We will have to reconcile the merge conflict manually.

Managing merge conflicts

By default when we have a merge conflict, our changes will be prioritized over the conflicting changes. To review the conflicting changes, you can alter the merge direction.

If there's a clear resolution by simply toggling between yours and theirs then you can resolve the merge and that is the end of the conflict. A merge commit will be created that includes your selection for resolving the conflict.

If we are not happy with either of the changes. We can abort the merge, which will restore us back to our last commit. Alternatively, we can pick between yours and theirs and then add our own manual resolutions to the conflict. To do this we need to exit compare mode and enter edit mode. We will now see a button to revisit the conflict after we are done making our edits.

Once you've resolved your merge, you're done. Just click the resolve merge button. Merge conflicts do occur in floro, but due to the nature of structural content, they are far less common than in plain text systems. However, it's always a good idea to have small merge requests and to commit and merge frequently in order to avoid painful conflicts.

Auto merge

The majority of the time your merges will automatically merge. All remote merges must be conflict-free and automatically mergeable in order to be merged. If you have a merge request that cannot be merged due to a conflict. It's easy to fix.

In your local repository, pull the base branch of the merge request. Next switch back to the branch of the merge request (still locally). Finally, merge the base branch into your branch. After resolving the conflict, push your new merge commit up to the remote repository. This will allow your merge request to be merged.

To branch or not to (feature) branch

Believe it or not, we actually believe most repositories do not need to make extensive use of branching. While, it is a very handy feature at times, it can lead to a lot of confusion and unnecessary process overhead. In the context of plain-text, branching makes a lot of sense, especially as a team scales. However, in the case of structured data, branching may hinder your workflow.

When you pull a branch, like in git, the remote version of a branch is merged with your local version of the branch. In our opinion, it is easiest for most teams to work off of a single main branch and push directly to main. In doing so, you sacrifice the review process you gain with feature branches, but you gain considerable efficiencies in development workflow and remove some of the more technical aspects of version control making your process more accessible to contributors who may not have a software engineering background.

Next section

We're now ready to cover some of the more advanced commands in floro, we can now learn about reverting, sha graphs, cherry-picking, stashing and copying and pasting across repositories

In the next section we will be covering using advanced floro commands.