public void RemoveEmptyChanges()
        {
            int totalChanges = changes.Count;

            Logger.LogInformation("Removing empty changes from {count} changes", totalChanges);

            int number = 0;

            for (int index = 0; index < changes.Count; index++)
            {
                number++;
                if (number % 1000 == 0)
                {
                    Logger.LogInformation("Checking if change {number} of {total} is empty", number, totalChanges);
                }

                var change = changes[index];
                if (change.HasChanges())
                {
                    continue;
                }
                for (int i = index - 1; 0 <= i; i--)
                {
                    var previousChange = changes[i];
                    if (previousChange.Branch != change.Branch)
                    {
                        continue;
                    }

                    ChangeSet nextChange = null;
                    int       indexNext  = index;
                    while (true)
                    {
                        indexNext++;
                        if (changes.Count <= indexNext)
                        {
                            break;
                        }
                        var nextChangeCandidate = changes[indexNext];
                        if (nextChangeCandidate.Branch != change.Branch)
                        {
                            continue;
                        }
                        nextChange = nextChangeCandidate;
                        break;
                    }

                    ReplaceBranchedFrom(change, previousChange);
                    if (nextChange != null)
                    {
                        foreach (var br in change.GetBranchedFrom())
                        {
                            nextChange.AddBranchedFrom(br);
                        }
                        // Move over comments about dirs as otherwise they will be lost.
                        int dirIndex = 0;
                        foreach (var dirComment in change.Changes)
                        {
                            if (dirComment.Type != ChangeSet.ChangeType.DIRECTORY_COMMENT)
                            {
                                throw new Exception($"Tryng to move non-dir comment change {dirComment} for {change}");
                            }
                            nextChange.InsertDirectoryComment(dirIndex, dirComment.FileName, dirComment.Version as DirectoryElementBranchVersion);
                            dirIndex++;
                        }
                    }
                    changes.RemoveAt(index);
                    // force a check for the entry that replaced it
                    index--;

                    Logger.LogDebug("Removing {change} and replacing with {previousChange}", change, previousChange);
                    break;
                }
            }

            RebuildForwardMerges();
            RebuildIndexLookup();

            Logger.LogInformation("After removing empty change we got {count} changes", changes.Count);
        }
        // Assuming A and C belongs to different branches, B is on the same
        // branch as A and D is on the same branch as C we merge the following
        // commit graph
        //      A
        //     /|
        //    C B
        //    |/
        //    D
        // to create the graph
        //      A'
        //     /
        //    C'
        // with A'=A+B and C'=C+D
        public void SquashMergeDiamonds()
        {
            int totalChanges = changes.Count;

            Logger.LogInformation("Checking if any merge diamonds could be squashed out of {count} changes", totalChanges);
            bool changed;
            int  number = 0;

            do
            {
                changed = false;
                foreach (var changeA in changes.ToList())
                {
                    if (changeA.GetBranchedFrom().Count != 1)
                    {
                        continue;
                    }
                    int indexA = indexLookup[changeA];
                    for (int indexB = indexA - 1; 0 <= indexB; indexB--)
                    {
                        var changeB = changes[indexB];
                        if (changeB.Branch != changeA.Branch)
                        {
                            continue;
                        }
                        if (!Squashable(changeB, changeA))
                        {
                            break;
                        }
                        if (forwardMerges.ContainsKey(changeB) || changeB.GetBranchedFrom().Count != 1)
                        {
                            break;
                        }
                        ChangeSet changeC = changeA.GetBranchedFrom().First();
                        if (forwardMerges[changeC].Count != 1)
                        {
                            break;
                        }
                        ChangeSet changeD = null;
                        int       indexD  = indexLookup[changeC];
                        while (true)
                        {
                            indexD--;
                            if (indexD < 0)
                            {
                                break;
                            }
                            var changeDCandidate = changes[indexD];
                            if (changeDCandidate.Branch != changeC.Branch)
                            {
                                continue;
                            }
                            if (!Squashable(changeDCandidate, changeC))
                            {
                                break;
                            }
                            if (changeB.GetBranchedFrom().First() != changeDCandidate || forwardMerges[changeDCandidate].Count != 1)
                            {
                                break;
                            }
                            changeD = changeDCandidate;
                            break;
                        }
                        if (changeD == null)
                        {
                            break;
                        }

                        ChangeSet aPrime = changeB.Plus(changeA);
                        ChangeSet cPrime = changeD.Plus(changeC);

                        // changeA is always last.
                        changes.RemoveAt(indexA);
                        changes.Insert(indexA, aPrime);
                        int indexC = indexLookup[changeC];
                        if (indexC < indexB)
                        {
                            changes.RemoveAt(indexB);
                            changes.RemoveAt(indexC);
                            changes.Insert(indexC, cPrime);
                        }
                        else
                        {
                            changes.RemoveAt(indexC);
                            changes.Insert(indexC, cPrime);
                            changes.RemoveAt(indexB);
                        }
                        // changeD is always first.
                        changes.RemoveAt(indexD);

                        aPrime.AddBranchedFrom(cPrime);

                        ReplaceBranchedFrom(changeA, aPrime);
                        ReplaceBranchedFrom(changeC, cPrime);

                        changed = true;
                        RebuildIndexLookup();
                        RebuildForwardMerges();

                        number++;
                        if (number % 500 == 0)
                        {
                            Logger.LogInformation("Squashed {number} merge diamonds so far ({total} maximum possible", number, totalChanges);
                        }
                        Logger.LogDebug("Squashing merge diamond {changeA} + {changeB} => {aPrime}; {changeC} + {changeD} => {cPrime}", changeA, changeB, aPrime, changeC, changeD, cPrime);
                        break;
                    }
                    if (changed)
                    {
                        break;
                    }
                }
            } while (changed);
            Logger.LogInformation("After squashing merge diamonds we got {count} changes", changes.Count);
        }