public static SemanticReleaseNotes Parse(string releaseNotes)
        {
            var releases = new List<SemanticRelease>();
            var lines = releaseNotes.Replace("\r", string.Empty).Split('\n');

            var currentRelease = new SemanticRelease();
            foreach (var line in lines)
            {
                if (line.TrimStart().StartsWith("# "))
                {
                    var match = ReleaseRegex.Match(line);
                    if (line != lines.First())
                    {
                        releases.Add(currentRelease);
                    }

                    currentRelease = new SemanticRelease
                    {
                        ReleaseName = match.Groups["Title"].Value
                    };

                    if (currentRelease.ReleaseName == "vNext")
                    {
                        currentRelease.ReleaseName = null;
                    }

                    if (match.Groups["Date"].Success)
                    {
                        DateTime parsed;
                        var toParse = match.Groups["Date"].Value;
                        if (DateTime.TryParse(toParse, out parsed))
                        {
                            currentRelease.When = parsed;
                        }

                        if (DateTime.TryParseExact(toParse, "dd MMMM yyyy", CultureInfo.InvariantCulture,
                            DateTimeStyles.None, out parsed))
                        {
                            currentRelease.When = parsed;
                        }
                        else if (DateTime.TryParseExact(toParse, "MMMM dd, yyyy", CultureInfo.InvariantCulture,
                            DateTimeStyles.None, out parsed))
                        {
                            currentRelease.When = parsed;
                        }
                        else
                        {
                            // We failed to parse the date, just append to the end
                            currentRelease.ReleaseName += " (" + toParse + ")";
                        }
                    }
                }
                else if (line.StartsWith("Commits: "))
                {
                    var commitText = line.Replace("Commits: ", string.Empty);
                    var linkMatch = LinkRegex.Match(commitText);
                    if (linkMatch.Success)
                    {
                        commitText = linkMatch.Groups["Text"].Value;
                        currentRelease.DiffInfo.DiffUrlFormat = linkMatch.Groups["Link"].Value;
                    }

                    var commits = commitText.Split(new[] { "..." }, StringSplitOptions.None);
                    currentRelease.DiffInfo.BeginningSha = commits[0];
                    currentRelease.DiffInfo.EndSha = commits[1];
                }
                else if (line.StartsWith(" - "))
                {
                    // Improve this parsing to extract issue numbers etc
                    var title = line.StartsWith(" - ") ? line.Substring(3) : line;
                    var releaseNoteItem = new ReleaseNoteItem(title, null, null, null, currentRelease.When, new Contributor[0]);
                    currentRelease.ReleaseNoteLines.Add(releaseNoteItem);
                }
                else if (string.IsNullOrWhiteSpace(line))
                {
                    currentRelease.ReleaseNoteLines.Add(new BlankLine());
                }
                else
                {
                    // This picks up comments and such
                    var title = line.StartsWith(" - ") ? line.Substring(3) : line;
                    var releaseNoteItem = new ReleaseNoteLine(title);
                    currentRelease.ReleaseNoteLines.Add(releaseNoteItem);
                }
            }

            releases.Add(currentRelease);

            // Remove additional blank lines
            foreach (var semanticRelease in releases)
            {
                for (int i = 0; i < semanticRelease.ReleaseNoteLines.Count; i++)
                {
                    if (semanticRelease.ReleaseNoteLines[i] is BlankLine)
                    {
                        semanticRelease.ReleaseNoteLines.RemoveAt(i--);
                    }
                    else
                    {
                        break;
                    }
                }
                for (int i = semanticRelease.ReleaseNoteLines.Count - 1; i >= 0; i--)
                {
                    if (semanticRelease.ReleaseNoteLines[i] is BlankLine)
                    {
                        semanticRelease.ReleaseNoteLines.RemoveAt(i);
                    }
                    else
                    {
                        break;
                    }
                }
            }

            return new SemanticReleaseNotes(releases, new Categories());
        }
        public static async Task<SemanticReleaseNotes> GenerateReleaseNotesAsync(ReleaseNotesGenerationParameters generationParameters,
            IRepository gitRepo, IIssueTracker issueTracker, SemanticReleaseNotes previousReleaseNotes,
            Categories categories, TaggedCommit tagToStartFrom, ReleaseInfo currentReleaseInfo)
        {
            var releases = ReleaseFinder.FindReleases(gitRepo, tagToStartFrom, currentReleaseInfo);

            var findIssuesSince =
                IssueStartDateBasedOnPreviousReleaseNotes(gitRepo, previousReleaseNotes)
                ??
                tagToStartFrom.Commit.Author.When;

            var filter = new IssueTrackerFilter
            {
                Since = findIssuesSince,
                IncludeOpen = false
            };

            var closedIssues = (await issueTracker.GetIssuesAsync(filter)).ToArray();

            // As discussed here: https://github.com/GitTools/GitReleaseNotes/issues/85

            var semanticReleases = new Dictionary<string, SemanticRelease>();

            foreach (var issue in closedIssues)
            {
                // 1) Include all issues from the issue tracker that are assigned to this release
                foreach (var fixVersion in issue.FixVersions)
                {
                    if (!fixVersion.IsReleased)
                    {
                        continue;
                    }

                    if (!semanticReleases.ContainsKey(fixVersion.Name))
                    {
                        semanticReleases.Add(fixVersion.Name, new SemanticRelease(fixVersion.Name, fixVersion.ReleaseDate));
                    }

                    var semanticRelease = semanticReleases[fixVersion.Name];

                    var releaseNoteItem = new ReleaseNoteItem(issue.Title, issue.Id, issue.Url, issue.Labels,
                        issue.DateClosed, new Contributor[] { /*TODO: implement*/ });

                    semanticRelease.ReleaseNoteLines.Add(releaseNoteItem);
                }

                // 2) Get closed issues from the issue tracker that have no fixversion but are closed between the last release and this release
                if (issue.FixVersions.Count == 0)
                {
                    foreach (var release in releases)
                    {
                        if (issue.DateClosed.HasValue &&
                            issue.DateClosed.Value > release.PreviousReleaseDate &&
                            (release.When == null || issue.DateClosed <= release.When))
                        {
                            if (!semanticReleases.ContainsKey(release.Name))
                            {
                                var beginningSha = release.FirstCommit != null ? release.FirstCommit.Substring(0, 10) : null;
                                var endSha = release.LastCommit != null ? release.LastCommit.Substring(0, 10) : null;

                                semanticReleases.Add(release.Name, new SemanticRelease(release.Name, release.When, new ReleaseDiffInfo
                                {
                                    BeginningSha = beginningSha,
                                    EndSha = endSha,
                                    // TODO DiffUrlFormat = context.Repository.DiffUrlFormat
                                }));
                            }

                            var semanticRelease = semanticReleases[release.Name];

                            var releaseNoteItem = new ReleaseNoteItem(issue.Title, issue.Id, issue.Url, issue.Labels,
                                issue.DateClosed, issue.Contributors);

                            semanticRelease.ReleaseNoteLines.Add(releaseNoteItem);
                        }
                    }
                }
            }

            // 3) Remove any duplicates
            foreach (var semanticRelease in semanticReleases.Values)
            {
                var handledIssues = new HashSet<string>();

                for (var i = 0; i < semanticRelease.ReleaseNoteLines.Count; i++)
                {
                    var releaseNoteLine = semanticRelease.ReleaseNoteLines[i] as ReleaseNoteItem;
                    if (releaseNoteLine == null)
                    {
                        continue;
                    }

                    if (handledIssues.Contains(releaseNoteLine.IssueNumber))
                    {
                        semanticRelease.ReleaseNoteLines.RemoveAt(i--);
                        continue;
                    }

                    handledIssues.Add(releaseNoteLine.IssueNumber);
                }
            }

            var semanticReleaseNotes = new SemanticReleaseNotes(semanticReleases.Values, categories);
            var mergedReleaseNotes = semanticReleaseNotes.Merge(previousReleaseNotes);
            return mergedReleaseNotes;
        }