Avoiding Merge Conflicts For Fork Projects

In my previous team, I’ve worked on a project which was actually a fork of another internal project at the parent company. A git fork project is very powerful in that the project maintains its connection to the original project. When there are new commits added to the original project, a fork project can simply rebase onto the latest commit and enjoy all the new features/fixes.

One of the challenges of maintaining and developing such a project is having to resolve merge conflicts caused by the differences introduced between the original and fork projects by independent changes made to the codebase. And as the time goes on, the number of conflicts just grows on and on. Every time you do a rebase, a bunch of files and commits have to be manually fixed. At some point it almost becomes impossible to do a rebase anymore.

I’ll share some of the ways to remedy for this and reduce the number of merge conflicts, such that rebasing becomes a easy and even tedious task!

What is Merge Conflict?

If you’ve ever worked on a team using Git, you’ve probably seen the dreaded “merge conflict” message. It’s one of those moments where Git throws up its hands and says, “I have no idea what you want me to do here — you figure it out.” So, what exactly causes a merge conflict?

A merge conflict happens when Git can’t automatically combine changes from different branches. This usually occurs during:

  • git merge
  • git rebase
  • git pull (which does a fetch + merge)
  • git cherry-pick

Git is smart, but it’s not a mind reader. When changes collide in a way that Git can’t resolve automatically, it flags them as conflicts for you to review. Let’s say two developers are working on the same file. Here are common scenarios that cause a conflict:

Same File, Same Line

This is the most classic case. Two people edit the same line in a file:

// Branch A
console.log("Hello from branch A");

// Branch B
console.log("Hello from branch B");

When you try to merge one branch into the other, Git can’t decide which version to keep.

File Deleted in One Branch, Edited in Another

  • Branch A deletes a file.
  • Branch B edits the same file.

Git gets confused — should the file be deleted or updated?

Conflicting Renames

  • Branch A renames utils.js to helpers.js.
  • Branch B renames it to common.js.

Now Git’s stuck. Which rename should it keep?

Why Does Git Conflict?

Git relies on a common ancestor to determine how to combine changes. But when both branches make competing changes to the same part of a file, it needs human intervention. For fork projects, conflict hell ensues as rebasing is basically merging all of the commits onto the new base every single time I do a rebase.


Why Rebase? Not Cherry-pick?

If rebasing causes more conflicts, why not cherry-pick instead? Cherry-pick is a Git command to copy and create a commit onto the current HEAD. Unlike rebase, this means that a merge conflict only happens at the cherry-picking time. However, cherry-pick is not suitable for maintaining a fork project.

Here is a example of a fork project keeping up with original project using cherry-pick.

# Initial state
A-B-C-D-E-F          (Original)
             \
               -α            (Fork)   # branch off D for Paldogam work

# Cherry-pick F
A-B-C-D-E-F          (Original)
             \
               -α-F'         (Fork)   # pulled in F, skipped E

# Both sides keep committing
A-B-C-D-E-F-G        (Original)
         \
           -α-F'-β   (Fork)

# Need G (which depends on E) → cherry-pick again
A-B-C-D-E-F-G        (Original)
         \
           -α-F'-β-E'-G'   (Fork)

As you can see, cherry-picking results in non-linear git graph. This becomes a problem when trying to compare the original project to the fork project; it’s impossible to figure out where and which state the fork project is in currently. Where do I even begin to continue cherry-pick? Which commits are made by the original and which are by the fork?

So rebasing a fork project is the common pattern in maintaining the project up to date with the original. Okay then, how can we reduce the number of merge conflicts?


Strategies to Minimize Merge Conflicts

TLDR; Split files to avoid merge conflict altogether, or at least try not to change the same line.

The key to minimizing merge conflict is to not make conflicts (duh, right?). I’ll give you some examples to make this more clear. The example is based on Spring Framework Application as that was what the project was build on.

Adding New Method to a Bean

  • Create a new bean in a new file that extends the existing one
  • Add method to the new bean.
// 🚫 causes conflict
@Service
class SomeService {
	// original code
	fun oldMethod() { ... }
 
	// fork code
	fun newMethod() { ... }
}

// ✅ no conflicts
@Service
class SomeService {
	// original code
	fun oldMethod() { ... }
}

@Service
@Primary
class PatchedSomeService : SomeService() 
{
	// fork code
	fun newMethod() { ... }
}

Modifying an Existing Method of a Bean

  • Create a new bean in a new file that extends the existing one, just like the previous example
  • Override the target method to implement new behavior
// 🚫 causes conflict
@Service
class SomeService {
	// original code
	fun oldMethod() { 
		... 
		newCode() // fork code
	}
}

// ✅ no conflicts
@Service
class SomeService {
	// original code
	fun oldMethod() { ... }
}

@Service
@Primary
class PatchedSomeService : SomeService() 
{
	override fun oldMethod() { 
		super.oldMethod()
		newCode() // fork code
	}
}

Adding an Enum Value

  • Unfortunately, enum classes cannot be split into a new file without changing the client code
  • Best we can do is to avoid conflicting lines
  • Usually, new codes are added to the end part of the file
  • So let’s make changes to the top part of the file to minimize the chance of conflict
// 🚫 causes conflict
enum class Type {
	OLD_TYPE, // original code
	NEW_TYPE, // fork code
}

// ✅ reduces chance of conflict
enum class Type {
	NEW_TYPE, // fork code
	OLD_TYPE, // original code
}

Leave a Reply

Your email address will not be published. Required fields are marked *