From 80539f9b53a55c54514aa31d920ba1c4358dec83 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Mon, 24 Aug 2026 17:29:34 +0200 Subject: [PATCH 1/9] Vendor Maestro changelog tool Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/maestro-changelog.yml | 6 +- scripts/changelog/Program.cs | 115 ++++++++++++++++++++++++ scripts/changelog/README.md | 16 ++++ scripts/changelog/changelog.csproj | 10 +++ scripts/changelog/fragment.mk | 4 + 5 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 scripts/changelog/Program.cs create mode 100644 scripts/changelog/README.md create mode 100644 scripts/changelog/changelog.csproj create mode 100644 scripts/changelog/fragment.mk diff --git a/.github/workflows/maestro-changelog.yml b/.github/workflows/maestro-changelog.yml index e8a308e390c9..e801d15938d2 100644 --- a/.github/workflows/maestro-changelog.yml +++ b/.github/workflows/maestro-changelog.yml @@ -17,6 +17,7 @@ jobs: if: github.event.pull_request.user.login == 'dotnet-maestro[bot]' steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: '9' @@ -24,10 +25,7 @@ jobs: - name: 'Compute changelog' run: | set -exo pipefail - git clone https://github.com/spouliot/dotnet-tools - cd dotnet-tools/changelog - dotnet build - ./bin/Debug/net9.0/changelog https://github.com/$GITHUB_REPOSITORY/pull/${GITHUB_REF_NAME/\/*/} > /tmp/changelog.txt 2>&1 + dotnet run --project scripts/changelog/changelog.csproj -- https://github.com/$GITHUB_REPOSITORY/pull/${GITHUB_REF_NAME/\/*/} > /tmp/changelog.txt 2>&1 - name: 'Add changelog' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/scripts/changelog/Program.cs b/scripts/changelog/Program.cs new file mode 100644 index 000000000000..33d0f586786d --- /dev/null +++ b/scripts/changelog/Program.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace changelog +{ + + class Program + { + static List list = new (); + static List filters = new (); + + // current repo (1) points to dotnet/installer (2) + // getting into other repos (pointed by dotnet/installer) can show different results + static int level = 2; + + static async Task Main (string[] args) + { + var pr = args [0]; + for (int i = 1; i < args.Length; i++) + filters.Add (args [i]); + + Console.WriteLine ($"# .net ChangeLog for {pr}"); + if (pr.StartsWith ("https://github.com/", StringComparison.Ordinal)) { + pr = pr.Replace ("https://github.com/", "https://patch-diff.githubusercontent.com/raw/") + ".diff"; + } + list.Add (pr); + await Process (); + Console.WriteLine ("Generated using https://github.com/spouliot/dotnet-tools/tree/master/changelog"); + } + + static async Task Process () + { + using var client = new HttpClient (); + for (int i = 0; i < Math.Min (list.Count, level); i++) { + Console.WriteLine ($"## Level {i + 1}"); + var url = list [i]; + if (url.StartsWith ("https://", StringComparison.Ordinal)) { + var result = await client.GetAsync (list [i]); + ProcessDiff (result.Content.ReadAsStream ()); + } else { + ProcessDiff (new FileStream (url, FileMode.Open, FileAccess.Read)); + } + Console.WriteLine (); + } + } + + static bool Include (string uri) + { + if (filters.Count == 0) + return true; + + foreach (var filter in filters) { + if (uri.EndsWith (filter, StringComparison.Ordinal)) + return true; + } + return false; + } + + static void ProcessDiff (Stream s) + { + bool processing = false; + var uri = String.Empty; + var old_sha = String.Empty; + var new_sha = String.Empty; + using (var sr = new StreamReader (s)) { + while (!sr.EndOfStream) { + var line = sr.ReadLine (); + if (line is null) + break; + if (line == "diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml") { + processing = true; + continue; + } + if (processing) { + if (line.StartsWith ("diff --git ", StringComparison.Ordinal)) + return; + if (line.Length < 1) + continue; + bool removal = (line [0] == '-'); + bool addition = (line [0] == '+'); + var tl = (removal || addition) ? line [1..] : line; + tl = tl.Trim (); + if (tl.StartsWith ("", StringComparison.Ordinal)) { + uri = tl [5..^6]; + } else if (removal && tl.StartsWith ("", StringComparison.Ordinal)) { + old_sha = tl [5..^6]; + } else if (addition && tl.StartsWith ("", StringComparison.Ordinal)) { + new_sha = tl [5..^6]; + var diff_url = $"{uri}/compare/{old_sha}..{new_sha}.diff"; + if (!Include (uri)) + continue; + // skip duplicates (if same revisions are used) + if (!list.Contains (diff_url)) { + if (string.IsNullOrEmpty (old_sha)) { + Console.WriteLine ($"* {uri} [{new_sha [0..7]}]({uri}/commits/{new_sha}) (new dependency)"); + } else if (string.IsNullOrEmpty (new_sha)) { + Console.WriteLine ($"* {uri} [{old_sha [0..7]}]({uri}/commits/{old_sha}) (removed dependency)"); + } else { + Console.WriteLine ($"* {uri} [{old_sha [0..7]}...{new_sha [0..7]}]({uri}/compare/{old_sha}...{new_sha})"); + } + list.Add (diff_url); + } + } + } + } + } + } + } +} diff --git a/scripts/changelog/README.md b/scripts/changelog/README.md new file mode 100644 index 000000000000..55e0324340bc --- /dev/null +++ b/scripts/changelog/README.md @@ -0,0 +1,16 @@ + +# Changelog + +This tool reads dependency changes from a pull request and formats them as a +Markdown changelog. + +## Usage + +```sh +dotnet run --project scripts/changelog/changelog.csproj -- https://github.com/dotnet/macios/pull/11175 +``` + +Additional arguments filter the dependency repositories included in the output. diff --git a/scripts/changelog/changelog.csproj b/scripts/changelog/changelog.csproj new file mode 100644 index 000000000000..8d759480bd5f --- /dev/null +++ b/scripts/changelog/changelog.csproj @@ -0,0 +1,10 @@ + + + + Exe + net9.0 + + diff --git a/scripts/changelog/fragment.mk b/scripts/changelog/fragment.mk new file mode 100644 index 000000000000..e07f8990a9c8 --- /dev/null +++ b/scripts/changelog/fragment.mk @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +include $(TOP)/scripts/template.mk +$(eval $(call TemplateScript,CHANGELOG,changelog)) From c7fdf5483bdb41c4f1616104330ad7b672373739 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Mon, 24 Aug 2026 17:57:52 +0200 Subject: [PATCH 2/9] Use bundled framework for changelog tool Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- scripts/changelog/changelog.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/changelog/changelog.csproj b/scripts/changelog/changelog.csproj index 8d759480bd5f..b6c8e0a318e6 100644 --- a/scripts/changelog/changelog.csproj +++ b/scripts/changelog/changelog.csproj @@ -5,6 +5,6 @@ Licensed under the MIT License. Exe - net9.0 + net$(BundledNETCoreAppTargetFrameworkVersion) From ae2a79a117c9cea0e8bdbf01b38025fb7a9c2ae3 Mon Sep 17 00:00:00 2001 From: GitHub Actions Autoformatter Date: Mon, 24 Aug 2026 16:08:11 +0000 Subject: [PATCH 3/9] Auto-format source code --- scripts/changelog/Program.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/changelog/Program.cs b/scripts/changelog/Program.cs index 33d0f586786d..50b5c9833672 100644 --- a/scripts/changelog/Program.cs +++ b/scripts/changelog/Program.cs @@ -7,11 +7,9 @@ using System.Net.Http; using System.Threading.Tasks; -namespace changelog -{ +namespace changelog { - class Program - { + class Program { static List list = new (); static List filters = new (); @@ -19,7 +17,7 @@ class Program // getting into other repos (pointed by dotnet/installer) can show different results static int level = 2; - static async Task Main (string[] args) + static async Task Main (string [] args) { var pr = args [0]; for (int i = 1; i < args.Length; i++) From 17f2b1330fd5f423303cb1adfc58cd2afead9737 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Mon, 24 Aug 2026 21:48:23 +0200 Subject: [PATCH 4/9] Address changelog review comments Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/maestro-changelog.yml | 4 +- scripts/changelog/Program.cs | 52 +++++++++++++++---------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/.github/workflows/maestro-changelog.yml b/.github/workflows/maestro-changelog.yml index e801d15938d2..7be7fda6cc53 100644 --- a/.github/workflows/maestro-changelog.yml +++ b/.github/workflows/maestro-changelog.yml @@ -18,6 +18,8 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: '9' @@ -25,7 +27,7 @@ jobs: - name: 'Compute changelog' run: | set -exo pipefail - dotnet run --project scripts/changelog/changelog.csproj -- https://github.com/$GITHUB_REPOSITORY/pull/${GITHUB_REF_NAME/\/*/} > /tmp/changelog.txt 2>&1 + dotnet run --project scripts/changelog/changelog.csproj --verbosity quiet -- https://github.com/$GITHUB_REPOSITORY/pull/${GITHUB_REF_NAME/\/*/} /tmp/changelog.txt - name: 'Add changelog' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/scripts/changelog/Program.cs b/scripts/changelog/Program.cs index 50b5c9833672..7c5e0888d40d 100644 --- a/scripts/changelog/Program.cs +++ b/scripts/changelog/Program.cs @@ -19,32 +19,44 @@ class Program { static async Task Main (string [] args) { + if (args.Length < 2) { + Console.Error.WriteLine ("Usage: changelog [repo-filter ...]"); + Environment.ExitCode = 1; + return; + } + var pr = args [0]; - for (int i = 1; i < args.Length; i++) + var outputFile = args [1]; + for (int i = 2; i < args.Length; i++) filters.Add (args [i]); - Console.WriteLine ($"# .net ChangeLog for {pr}"); - if (pr.StartsWith ("https://github.com/", StringComparison.Ordinal)) { - pr = pr.Replace ("https://github.com/", "https://patch-diff.githubusercontent.com/raw/") + ".diff"; + using (var writer = new StreamWriter (outputFile)) { + writer.WriteLine ($"# .net ChangeLog for {pr}"); + if (pr.StartsWith ("https://github.com/", StringComparison.Ordinal)) { + pr = pr.Replace ("https://github.com/", "https://patch-diff.githubusercontent.com/raw/") + ".diff"; + } + list.Add (pr); + await Process (writer); + writer.WriteLine ("Generated using https://github.com/spouliot/dotnet-tools/tree/master/changelog"); } - list.Add (pr); - await Process (); - Console.WriteLine ("Generated using https://github.com/spouliot/dotnet-tools/tree/master/changelog"); } - static async Task Process () + static async Task Process (TextWriter writer) { using var client = new HttpClient (); for (int i = 0; i < Math.Min (list.Count, level); i++) { - Console.WriteLine ($"## Level {i + 1}"); + writer.WriteLine ($"## Level {i + 1}"); var url = list [i]; if (url.StartsWith ("https://", StringComparison.Ordinal)) { - var result = await client.GetAsync (list [i]); - ProcessDiff (result.Content.ReadAsStream ()); + using var result = await client.GetAsync (list [i]); + result.EnsureSuccessStatusCode (); + using var stream = await result.Content.ReadAsStreamAsync (); + ProcessDiff (stream, writer); } else { - ProcessDiff (new FileStream (url, FileMode.Open, FileAccess.Read)); + using var stream = new FileStream (url, FileMode.Open, FileAccess.Read); + ProcessDiff (stream, writer); } - Console.WriteLine (); + writer.WriteLine (); } } @@ -60,12 +72,12 @@ static bool Include (string uri) return false; } - static void ProcessDiff (Stream s) + static void ProcessDiff (Stream s, TextWriter writer) { bool processing = false; - var uri = String.Empty; - var old_sha = String.Empty; - var new_sha = String.Empty; + var uri = ""; + var old_sha = ""; + var new_sha = ""; using (var sr = new StreamReader (s)) { while (!sr.EndOfStream) { var line = sr.ReadLine (); @@ -96,11 +108,11 @@ static void ProcessDiff (Stream s) // skip duplicates (if same revisions are used) if (!list.Contains (diff_url)) { if (string.IsNullOrEmpty (old_sha)) { - Console.WriteLine ($"* {uri} [{new_sha [0..7]}]({uri}/commits/{new_sha}) (new dependency)"); + writer.WriteLine ($"* {uri} [{new_sha [0..7]}]({uri}/commits/{new_sha}) (new dependency)"); } else if (string.IsNullOrEmpty (new_sha)) { - Console.WriteLine ($"* {uri} [{old_sha [0..7]}]({uri}/commits/{old_sha}) (removed dependency)"); + writer.WriteLine ($"* {uri} [{old_sha [0..7]}]({uri}/commits/{old_sha}) (removed dependency)"); } else { - Console.WriteLine ($"* {uri} [{old_sha [0..7]}...{new_sha [0..7]}]({uri}/compare/{old_sha}...{new_sha})"); + writer.WriteLine ($"* {uri} [{old_sha [0..7]}...{new_sha [0..7]}]({uri}/compare/{old_sha}...{new_sha})"); } list.Add (diff_url); } From 80c6b5c6f235c2b30222acdfee8aa097779fde09 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Mon, 24 Aug 2026 22:01:43 +0200 Subject: [PATCH 5/9] Fix dependency diff parsing and vendored tool references Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- scripts/changelog/Program.cs | 23 +++++++++++------------ scripts/changelog/README.md | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/scripts/changelog/Program.cs b/scripts/changelog/Program.cs index 7c5e0888d40d..9fc9b760c17e 100644 --- a/scripts/changelog/Program.cs +++ b/scripts/changelog/Program.cs @@ -37,7 +37,7 @@ static async Task Main (string [] args) } list.Add (pr); await Process (writer); - writer.WriteLine ("Generated using https://github.com/spouliot/dotnet-tools/tree/master/changelog"); + writer.WriteLine ("Generated using scripts/changelog"); } } @@ -98,23 +98,22 @@ static void ProcessDiff (Stream s, TextWriter writer) tl = tl.Trim (); if (tl.StartsWith ("", StringComparison.Ordinal)) { uri = tl [5..^6]; + old_sha = ""; + new_sha = ""; } else if (removal && tl.StartsWith ("", StringComparison.Ordinal)) { old_sha = tl [5..^6]; } else if (addition && tl.StartsWith ("", StringComparison.Ordinal)) { new_sha = tl [5..^6]; - var diff_url = $"{uri}/compare/{old_sha}..{new_sha}.diff"; if (!Include (uri)) continue; - // skip duplicates (if same revisions are used) - if (!list.Contains (diff_url)) { - if (string.IsNullOrEmpty (old_sha)) { - writer.WriteLine ($"* {uri} [{new_sha [0..7]}]({uri}/commits/{new_sha}) (new dependency)"); - } else if (string.IsNullOrEmpty (new_sha)) { - writer.WriteLine ($"* {uri} [{old_sha [0..7]}]({uri}/commits/{old_sha}) (removed dependency)"); - } else { - writer.WriteLine ($"* {uri} [{old_sha [0..7]}...{new_sha [0..7]}]({uri}/compare/{old_sha}...{new_sha})"); - } - list.Add (diff_url); + if (string.IsNullOrEmpty (old_sha)) { + writer.WriteLine ($"* {uri} [{new_sha [0..7]}]({uri}/commits/{new_sha}) (new dependency)"); + } else { + var diff_url = $"{uri}/compare/{old_sha}..{new_sha}.diff"; + writer.WriteLine ($"* {uri} [{old_sha [0..7]}...{new_sha [0..7]}]({uri}/compare/{old_sha}...{new_sha})"); + // skip duplicates (if same revisions are used) + if (!list.Contains (diff_url)) + list.Add (diff_url); } } } diff --git a/scripts/changelog/README.md b/scripts/changelog/README.md index 55e0324340bc..599edc954696 100644 --- a/scripts/changelog/README.md +++ b/scripts/changelog/README.md @@ -10,7 +10,7 @@ Markdown changelog. ## Usage ```sh -dotnet run --project scripts/changelog/changelog.csproj -- https://github.com/dotnet/macios/pull/11175 +dotnet run --project scripts/changelog/changelog.csproj -- https://github.com/dotnet/macios/pull/11175 /tmp/changelog.txt ``` Additional arguments filter the dependency repositories included in the output. From 234aebffc6a0e1eee8fde14b85eb3ef096422c6b Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 25 Aug 2026 07:36:31 +0200 Subject: [PATCH 6/9] Use global.json for dotnet SDK selection in changelog workflow Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/maestro-changelog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maestro-changelog.yml b/.github/workflows/maestro-changelog.yml index 7be7fda6cc53..969bc441c3c5 100644 --- a/.github/workflows/maestro-changelog.yml +++ b/.github/workflows/maestro-changelog.yml @@ -22,7 +22,7 @@ jobs: persist-credentials: false - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: - dotnet-version: '9' + global-json-file: ./global.json - name: 'Compute changelog' run: | From c06b5d2b59316e2b1b29459a2d6e242a68d0492b Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 25 Aug 2026 08:35:28 +0200 Subject: [PATCH 7/9] Address suppressed changelog review comments Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/maestro-changelog.yml | 4 +++- scripts/changelog/Program.cs | 25 +++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/workflows/maestro-changelog.yml b/.github/workflows/maestro-changelog.yml index 969bc441c3c5..90b51961df75 100644 --- a/.github/workflows/maestro-changelog.yml +++ b/.github/workflows/maestro-changelog.yml @@ -45,7 +45,9 @@ jobs: issue_number: context.issue.number }).then ((comments) => { - const changelogComment = comments.find(comment => comment.body.includes (".net ChangeLog for") && comment.user.login == 'github-actions[bot]') + const changelogComment = comments.find(comment => + (comment.body.includes (".NET Changelog for") || comment.body.includes (".net ChangeLog for")) && + comment.user.login == 'github-actions[bot]') if (changelogComment) commentId = changelogComment.id }) diff --git a/scripts/changelog/Program.cs b/scripts/changelog/Program.cs index 9fc9b760c17e..a4a9e5d9dacd 100644 --- a/scripts/changelog/Program.cs +++ b/scripts/changelog/Program.cs @@ -31,16 +31,33 @@ static async Task Main (string [] args) filters.Add (args [i]); using (var writer = new StreamWriter (outputFile)) { - writer.WriteLine ($"# .net ChangeLog for {pr}"); - if (pr.StartsWith ("https://github.com/", StringComparison.Ordinal)) { - pr = pr.Replace ("https://github.com/", "https://patch-diff.githubusercontent.com/raw/") + ".diff"; - } + writer.WriteLine ($"# .NET Changelog for {pr}"); + if (TryGetGitHubPullRequestDiffUrl (pr, out var diffUrl)) + pr = diffUrl; list.Add (pr); await Process (writer); writer.WriteLine ("Generated using scripts/changelog"); } } + static bool TryGetGitHubPullRequestDiffUrl (string url, out string diffUrl) + { + diffUrl = url; + if (!Uri.TryCreate (url, UriKind.Absolute, out var uri)) + return false; + if (uri.Host != "github.com") + return false; + if (url.EndsWith (".diff", StringComparison.Ordinal)) + return false; + + var parts = uri.AbsolutePath.Split ('/', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 4 || parts [2] != "pull") + return false; + + diffUrl = $"https://patch-diff.githubusercontent.com/raw/{string.Join ('/', parts)}.diff"; + return true; + } + static async Task Process (TextWriter writer) { using var client = new HttpClient (); From ab7e6a6cfab763a5d6093c4da9eb9839a75e85b5 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 25 Aug 2026 17:47:54 +0200 Subject: [PATCH 8/9] Address remaining changelog review suggestions Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- scripts/changelog/Program.cs | 4 ++-- scripts/changelog/changelog.csproj | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/changelog/Program.cs b/scripts/changelog/Program.cs index a4a9e5d9dacd..de998738d6fc 100644 --- a/scripts/changelog/Program.cs +++ b/scripts/changelog/Program.cs @@ -10,8 +10,8 @@ namespace changelog { class Program { - static List list = new (); - static List filters = new (); + static readonly List list = new (); + static readonly List filters = new (); // current repo (1) points to dotnet/installer (2) // getting into other repos (pointed by dotnet/installer) can show different results diff --git a/scripts/changelog/changelog.csproj b/scripts/changelog/changelog.csproj index b6c8e0a318e6..bd08a2be8d8d 100644 --- a/scripts/changelog/changelog.csproj +++ b/scripts/changelog/changelog.csproj @@ -4,7 +4,6 @@ Licensed under the MIT License. --> - Exe net$(BundledNETCoreAppTargetFrameworkVersion) From 631aea4e497955f11587d33f64b74f1c05ea1073 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 25 Aug 2026 17:49:58 +0200 Subject: [PATCH 9/9] Fix changelog compare diff URL Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- scripts/changelog/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/changelog/Program.cs b/scripts/changelog/Program.cs index de998738d6fc..c36b3539ddbf 100644 --- a/scripts/changelog/Program.cs +++ b/scripts/changelog/Program.cs @@ -126,7 +126,7 @@ static void ProcessDiff (Stream s, TextWriter writer) if (string.IsNullOrEmpty (old_sha)) { writer.WriteLine ($"* {uri} [{new_sha [0..7]}]({uri}/commits/{new_sha}) (new dependency)"); } else { - var diff_url = $"{uri}/compare/{old_sha}..{new_sha}.diff"; + var diff_url = $"{uri}/compare/{old_sha}...{new_sha}.diff"; writer.WriteLine ($"* {uri} [{old_sha [0..7]}...{new_sha [0..7]}]({uri}/compare/{old_sha}...{new_sha})"); // skip duplicates (if same revisions are used) if (!list.Contains (diff_url))