2

Since I'm better with visual representations, I basically want to get this done:

what I want

I did some searching and could only find this Git: Branch or Fork? And this one A decent git branching model for products that should accompany the version of another, third-party product (and pros and cons of one proposal)

But while a similar situation, those questions neither got any concrete answers.

Initial, it seemed as easy as creating a fork, but it seems you can't fork your own repos.

I then thought "Fancy House" could simply be an eternal branch, but on multiple occasions people have advised against that, saying branches are intended for temporary development and should always be used to eventually merge back into the master branch. Also, releases are awkward to handle and having a "true master" and "fancy house master" branch would cause further awkwardness and possible confusion (and only one of them being allowed to be truly the default branch).

I ran a quick test of the so called "submodules", but that seems to create a folder within the repo (so "Fancy_House/House_Project/House/Lights.txt"), which doesn't really work and messes with the folder structure (making "New & Fancy Lights.txt" not possible), and it also adds the main repos readmes and such, further clogging up the spin-off repo with unneeded stuff.

Similarly, a pull request from one repo to a different repo would also do the trick; but that's not possible to do either.

Of course, it is not absolutely impossible to simply manually copy over the changes and commits from the main project over to the spin-off; but that does mean all commits and history are being lost and requires manually doing so, which can be especially annoying if lots of small fixes are being added to the development branch. Possibly using the submodule function just so it's right there in your face and you don't need to change repos...still really annoying workaround.

We tend to exclusively use either GitHub desktop or GitKraken, if this helps at all.

I might be completely oblivious to something obvious though; which is why I'm asking here.

2

I'm going to explain some Git background to understand why long-lived forks are so difficult.

In Git, a version/commit represents a snapshot/state of the source code. It does not represent a set of changes. Therefore, no elegant way exists to track different “editions” or “variants” of your software.

A branch and a fork/separate repository are more or less the same thing as far as Git is concerned. Within the history of a Git repository, neither of these concepts exist: there are only commits. In particular, a branch is just a floating label that points to some commit. So while branches are often used to organize different threads of development, they do not represent those changes. Instead, the HEAD commit of a branch just represents the current state of development, and the commit contains any history that led up to that state.

Merging tries to unify the state of two (or more) commits/source code snapshots. First, Git looks for the latest common ancestor of all parent commits, then calculates the diffs between each parent's head and the common ancestor. Then it tries to merge both diffs. If there is no conflict, Git applies the changes and creates a new commit with this state.

The more changes there are, the more merge conflicts you get and the more difficult merges get. Therefore, merging early is imperative to keep the workload manageable. But this also means you should merge two branches in both directions. Otherwise, unmerged changes keep accumulating on one branch and you will need to resolve the same conflicts again and again upon each merge:

     1---2---3---4---5  fork
    /       /       /
---a---b---c---d---e  master

In the above example, there's a master branch and a long lived fork.

  • Commit 3 is a merge with merge-base a. This merges the changes a..c with a..2 and records a new commit 3. Relative to the fork, this merge represents how the changes a..c were applied. Because merge conflicts may have been resolved, this may not actually be equal to the changes a..c.

  • Commit 5 is another merge, this time with the merge-base c. This merges the changes c..e with c..4. Note that the commit 3 (which represents how previous conflicts between the branches were resolved) is part of only one of these branches. Where the two branches differed during merge 3, they probably still differ during merge 5 so you will have to resolve those differences/conflicts again.

Effectively, each merge is carried over to the next merge and effort only increases. Unless you merge in both directions. But that runs against your goal of having Fancy stuff only in the fork.

(There are alternative approaches like rebasing and cherry picking that apply a set of changes on top of a different commit. Because this only looks at the changes introduced by those commits and not at the source code snapshot they represent, this suffers less from conflicts. However, this doesn't merge Git histories but records entirely new, unrelated commits to represent the new source code state (history rewriting). And if the fork diverges too far, the diff may no longer “fit” the source code, requiring you to apply every change manually.)

So Git is unable to help here. There are two approaches:

One is to view one variant of your software as a set of patches on top of the other. The fork does not maintain the final result of those changes, but only the diffs that represent these changes. The original repo might then be a submodule. Whenever the original changes, you edit the patches so that they keep fitting. Within Git, these could also be a few commits that you continuously rebase on top of the original master, but that loses the history of your changes. This is only appropriate for very small, surgical changes that cannot be merged into the original repository. Maintaining patches would be sensible for small extensions or bugfixes that you cannot contribute back.

The better approach is to solve your problem on a software architecture level, not on a Git level. Instead of making direct changes to the software, make the software so configurable that the changes can be implemented as extensions. Relevant keywords: Open/Closed Principle, Feature Toggles, Dependency Injection, Design Patterns, Plugins. So either the main project exposes extension points so that you can insert the fancy changes, or you literally have a single code base where fancy/non-fancy mode can be selected at build time or at run time.

3

I then thought "Fancy House" could simply be an eternal branch, but on multiple occasions people have advised against that

Your other solutions are essentially just maintaining an eternal branch in a separate repo, and are no less advisable. Did you read what people usually recommend doing instead of eternal branches?

You want to modify your software so at construction time or configuration time you can create either a regular house or fancy house, creating well-defined modules that share as much code as possible.

A branch might be an okay short-term solution if your initial version was poorly architected and you need to get something out the door, but maintaining long-term branches is a lot of error-prone work.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.