From 8ee66776b6ef2c2ff142f26e2e5ea1a976c0cf1f Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 23 Oct 2026 18:23:18 +0200 Subject: [PATCH 1/2] fix(GenesisHub): a link's commit range that ends in commits the PR's checkout has not pushed yet opens that range from the checkout, with a note, instead of the whole MR --- .../Sources/Parts/Hub/Hub/HubCommitLink.swift | 74 +++++++++++++++++++ .../Sources/Parts/Hub/Hub/HubPRs.swift | 61 ++++++++++++++- .../GenesisTools/Tests/HubLogicTests.swift | 49 ++++++++++++ 3 files changed, 181 insertions(+), 3 deletions(-) diff --git a/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubCommitLink.swift b/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubCommitLink.swift index 21239bb9c8..5856440463 100644 --- a/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubCommitLink.swift +++ b/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubCommitLink.swift @@ -68,6 +68,80 @@ struct PRCommitPick: Equatable { struct PRNotice: Equatable { let ref: HubPRRef let text: String + /// False for a note that the link worked another way (its commits are local only). + var isError = true +} + +/// A link's commits that the forge does not list yet: an agent links its own commits before it pushes them, so +/// they exist only in the PR's checkout (2026-10-23: a range ending in four local commits +/// opened the whole MR). Read from that checkout's git, off the main thread. +struct HubLocalCommits: Equatable { + /// Full shas, oldest first; `last` is nil for one commit. + let first: String + let last: String? + /// The commits the range holds, both ends included, and how many of them the PR's pushed head lacks. + let count: Int + let unpushed: Int + let title: String + + enum Miss: Error, Equatable { + /// The checkout does not have this end either. + case unknown(String) + /// Both ends exist, but neither descends from the other. + case unrelated + /// The commits do not continue the PR: the last one does not descend from its pushed head, or the first one is + /// neither one of its commits nor after that head (a commit from before the PR would show the base's changes). + case offBranch + } + + /// `ends` are the link's normalized shas, `head` the PR's pushed head and `pushed` the shas the forge lists for it. + /// Runs git, so never on the main thread. + nonisolated static func resolve(ends: [String], repo: String, head: String, pushed: Set = []) -> Result { + var full: [String] = [] + for end in ends { + guard let sha = git(repo, ["rev-parse", "--verify", "--quiet", end + "^{commit}"])? + .trimmingCharacters(in: .whitespacesAndNewlines), !sha.isEmpty else { return .failure(.unknown(end)) } + full.append(sha) + } + var first = full[0] + var last = full.count > 1 ? full[1] : full[0] + if first != last, !isAncestor(first, of: last, repo: repo) { + guard isAncestor(last, of: first, repo: repo) else { return .failure(.unrelated) } + swap(&first, &last) + } + guard isAncestor(head, of: last, repo: repo), + pushed.contains(first) || isAncestor(head, of: first, repo: repo) else { + return .failure(.offBranch) + } + let span = Int(git(repo, ["rev-list", "--count", "--ancestry-path", first + ".." + last])? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "") ?? 0 + let unpushed = Int(git(repo, ["rev-list", "--count", head + ".." + last])? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "") ?? 0 + let title = git(repo, ["log", "-1", "--format=%s", last])?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return .success(HubLocalCommits(first: first, last: full.count > 1 ? last : nil, count: span + 1, + unpushed: unpushed, title: title)) + } + + nonisolated private static func isAncestor(_ older: String, of newer: String, repo: String) -> Bool { + git(repo, ["merge-base", "--is-ancestor", older, newer]) != nil + } + + nonisolated private static func git(_ repo: String, _ args: [String]) -> String? { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/git") + process.arguments = ["-C", repo, "--no-optional-locks"] + args + let out = Pipe() + process.standardOutput = out + process.standardError = FileHandle.nullDevice + do { + try process.run() + } catch { + return nil + } + let data = out.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + return process.terminationStatus == 0 ? String(decoding: data, as: UTF8.self) : nil + } } /// The commit row a PR overview scrolls to once (`PRsModel.commitScroll`). diff --git a/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubPRs.swift b/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubPRs.swift index 2c07d71114..971c2a85be 100644 --- a/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubPRs.swift +++ b/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubPRs.swift @@ -750,7 +750,7 @@ final class PRsModel: ObservableObject { let settled = (detailFetched[id].map { $0 >= pending.asked } ?? false) && reviewHeadSha == shown.headSha guard let commit = HubCommitSha.find(pending.sha, in: commits) else { if settled { - showMissingCommit(pending, missing: pending.sha, in: shown, review: review) + pickLocalCommits(pending, in: shown, commits: commits, review: review) } return } @@ -758,7 +758,7 @@ final class PRsModel: ObservableObject { if let to = pending.to { guard let found = HubCommitSha.find(to, in: commits) else { if settled { - showMissingCommit(pending, missing: to, in: shown, review: review) + pickLocalCommits(pending, in: shown, commits: commits, review: review) } return } @@ -775,6 +775,61 @@ final class PRsModel: ObservableObject { } } + /// A link's commit that the forge does not list may be one the PR's checkout has not pushed yet: look there before + /// saying the PR does not hold it. The diff then comes from the checkout, which the review reads anyway. + private func pickLocalCommits(_ pending: PRCommitPick, in shown: HubPR, commits: [HubPRDetail.Commit], review: ReviewModel) { + pendingCommit = nil + let repo = review.repo.path + let ends = [pending.sha] + (pending.to.map { [$0] } ?? []) + // The newest commit the forge lists stands in for an unknown pushed head; with neither, nothing can be checked. + guard let head = shown.headSha ?? commits.last?.sha else { + showMissingCommit(pending, missing: pending.to ?? pending.sha, in: shown, review: review) + return + } + let pushed = Set(commits.map(\.sha)) + Task.detached(priority: .userInitiated) { [weak self] in + let found = HubLocalCommits.resolve(ends: ends, repo: repo, head: head, pushed: pushed) + await MainActor.run { + guard let self, self.reviewPRID == shown.id, self.review === review else { return } + switch found { + case .success(let local): + self.showLocalCommits(local, pending: pending, in: commits, pr: shown, review: review) + case .failure(.unknown(let sha)): + self.showMissingCommit(pending, missing: sha, in: shown, review: review) + case .failure(.unrelated), .failure(.offBranch): + self.showMissingCommit(pending, missing: pending.to ?? pending.sha, in: shown, review: review) + } + } + } + } + + /// The link's commits from the checkout: the range from the commit before its first one (the PR's own base + /// when that first commit is the PR's first), and a note that some of them are not pushed. + func showLocalCommits(_ local: HubLocalCommits, pending: PRCommitPick, in commits: [HubPRDetail.Commit], pr: HubPR, review: ReviewModel) { + HubDefaults.store.set(true, forKey: "hub.prs.showDiff") + HubDefaults.store.set(false, forKey: "hub.prs.fold.commits") + HubPerf.log("prs.commit \(pr.label) \(local.first.prefix(10))\(local.last.map { "..\($0.prefix(10))" } ?? "") local unpushed=\(local.unpushed)") + let scope: DiffScope + if let last = local.last { + let label = "\(pr.label) \(local.first.prefix(7))..\(last.prefix(7)) (\(Plural.count(local.count, "commit")))" + let index = commits.firstIndex(where: { $0.sha == local.first }) + // The PR's first commit starts at the PR's own base; any other at its first parent from git (`rangeScope`). + if index == 0, case .range(let base, _, _, let fallback) = Self.scope(pr, detail: details[pr.id], fetch: fetchedHead(pr)) { + scope = .range(base: base, head: last, label: label, fallbackBase: fallback) + } else { + scope = .range(base: local.first + "^", head: last, label: label) + } + // The listed commits it covers: the pushed ones from its first on; none when it starts after the pushed head. + linkedRange = PRLinkedRange(scope: scope, shas: index.map { Set(commits[$0...].map(\.sha)) } ?? []) + } else { + scope = .commit(sha: local.first, title: local.title) + } + if review.scope != scope { review.setScope(scope) } + if local.unpushed > 0 { + notice = PRNotice(ref: pending.ref, text: "\(Plural.count(local.unpushed, "commit")) of this link \(local.unpushed == 1 ? "is" : "are") not pushed yet; the diff comes from \((review.repo.path as NSString).abbreviatingWithTildeInPath)", isError: false) + } + } + /// A link's `commit=..`: those commits of the PR, both ends included, in either order. func showLinkedRange(_ one: HubPRDetail.Commit, _ other: HubPRDetail.Commit, in commits: [HubPRDetail.Commit], pr: HubPR, ref: HubPRRef, review: ReviewModel) { @@ -2087,7 +2142,7 @@ struct PRDetailView: View { .foregroundColor(ReviewPalette.dim) // A row of its own: in the title row beside the title it had no width and wrapped a word per line. if let notice = prs.notice, notice.ref.matches(pr) { - NoticePill(text: notice.text, isError: true) { prs.notice = nil } + NoticePill(text: notice.text, isError: notice.isError) { prs.notice = nil } .padding(.top, 6) } } diff --git a/src/macos/GenesisTools/Tests/HubLogicTests.swift b/src/macos/GenesisTools/Tests/HubLogicTests.swift index bc30eb6e13..a1d881a630 100644 --- a/src/macos/GenesisTools/Tests/HubLogicTests.swift +++ b/src/macos/GenesisTools/Tests/HubLogicTests.swift @@ -147,6 +147,55 @@ final class HubLogicTests: XCTestCase { } /// `commit=..`: a run of a PR's commits, both ends included, in either order. + /// A link may name commits an agent made but did not push yet (2026-10-23): the forge does not list + /// them, the PR's checkout has them, so the range comes from the checkout instead of the whole MR. + func testALinkRangeOfUnpushedCommitsResolvesFromTheCheckout() throws { + let repo = FileManager.default.temporaryDirectory.appendingPathComponent("hub-local-range-\(UUID().uuidString)").path + try FileManager.default.createDirectory(atPath: repo, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(atPath: repo) } + func git(_ args: String...) throws -> String { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/git") + process.arguments = ["-C", repo, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.com"] + args + let out = Pipe() + process.standardOutput = out + try process.run() + let data = out.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + XCTAssertEqual(process.terminationStatus, 0, "git \(args.joined(separator: " "))") + return String(decoding: data, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) + } + _ = try git("init", "-q", "-b", "main") + var shas: [String] = [] + for index in 0 ..< 5 { + _ = try git("commit", "-q", "--allow-empty", "-m", "commit \(index)") + shas.append(try git("rev-parse", "HEAD")) + } + let pushedHead = shas[2] + let pushed = Set(shas[1 ... 2]) + let range = try XCTUnwrap(try? HubLocalCommits.resolve( + ends: [String(shas[2].prefix(10)), String(shas[4].prefix(10))], repo: repo, head: pushedHead, pushed: pushed).get()) + XCTAssertEqual(range, HubLocalCommits(first: shas[2], last: shas[4], count: 3, unpushed: 2, title: "commit 4")) + let reversed = try? HubLocalCommits.resolve(ends: [shas[4], shas[2]], repo: repo, head: pushedHead, pushed: pushed).get() + XCTAssertEqual(reversed?.first, shas[2], "either order") + XCTAssertEqual(try? HubLocalCommits.resolve(ends: [shas[3]], repo: repo, head: pushedHead).get().unpushed, 1) + guard case .failure(.offBranch) = HubLocalCommits.resolve(ends: [shas[0], shas[4]], repo: repo, head: pushedHead, pushed: pushed) else { + return XCTFail("a first commit from before the PR would show the base's changes") + } + guard case .failure(.unknown("0123456789")) = HubLocalCommits.resolve(ends: ["0123456789"], repo: repo, head: pushedHead) else { + return XCTFail("an end the checkout lacks is named") + } + _ = try git("checkout", "-q", "-b", "side", shas[0]) + _ = try git("commit", "-q", "--allow-empty", "-m", "side") + let side = try git("rev-parse", "HEAD") + guard case .failure(.unrelated) = HubLocalCommits.resolve(ends: [side, shas[4]], repo: repo, head: pushedHead) else { + return XCTFail("ends on two lines are refused") + } + guard case .failure(.offBranch) = HubLocalCommits.resolve(ends: [side], repo: repo, head: pushedHead) else { + return XCTFail("a commit off the PR's line is refused") + } + } + func testACommitRangeLinkShowsItsCommitsFromTheParentOfTheFirst() throws { XCTAssertEqual(HubCommitSpan("abcdef0"), HubCommitSpan(from: "abcdef0")) XCTAssertEqual(HubCommitSpan("67AD51671E..3cac936e2f"), HubCommitSpan(from: "67ad51671e", to: "3cac936e2f")) From 4c75a85f83e07e0e1863a548e8aa59bc8ec83de2 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 26 Oct 2026 01:34:14 +0100 Subject: [PATCH 2/2] fix(GenesisHub): #595 followup - bound local ranges and discard stale lookups --- .../Sources/Parts/Hub/Hub/HubCommitLink.swift | 5 +- .../Sources/Parts/Hub/Hub/HubPRs.swift | 48 ++++++++++++------- .../GenesisTools/Tests/HubLogicTests.swift | 44 ++++++++++++++++- 3 files changed, 78 insertions(+), 19 deletions(-) diff --git a/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubCommitLink.swift b/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubCommitLink.swift index 5856440463..e495abe2b9 100644 --- a/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubCommitLink.swift +++ b/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubCommitLink.swift @@ -115,8 +115,11 @@ struct HubLocalCommits: Equatable { } let span = Int(git(repo, ["rev-list", "--count", "--ancestry-path", first + ".." + last])? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "") ?? 0 - let unpushed = Int(git(repo, ["rev-list", "--count", head + ".." + last])? + // Count only descendants of the first endpoint within this link, excluding the pushed head's history. + // The exclusive range omits the first endpoint itself, so include it when it is also unpushed. + let unpushedAfterFirst = Int(git(repo, ["rev-list", "--count", "--ancestry-path=" + first, first + ".." + last, "--not", head])? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "") ?? 0 + let unpushed = unpushedAfterFirst + (isAncestor(first, of: head, repo: repo) ? 0 : 1) let title = git(repo, ["log", "-1", "--format=%s", last])?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return .success(HubLocalCommits(first: first, last: full.count > 1 ? last : nil, count: span + 1, unpushed: unpushed, title: title)) diff --git a/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubPRs.swift b/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubPRs.swift index 971c2a85be..de6e687229 100644 --- a/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubPRs.swift +++ b/src/macos/GenesisTools/Sources/Parts/Hub/Hub/HubPRs.swift @@ -350,7 +350,7 @@ final class PRsModel: ObservableObject { /// The range a link's `commit=..` set, so the commits list marks its commits even when it spans /// the whole PR; any other scope (the whole PR, one commit) is not it. @Published private(set) var linkedRange: PRLinkedRange? - /// The parent lookup of the last range link (`showLinkedRange`): an older link's answer sets no scope. + /// The parent or local-commit lookup of the last link: an older link's answer sets no scope or notice. private var rangeLookup: UUID? /// What a link asked of a PR and could not get (its commit is not in the PR), in that PR's header until /// dismissed or another PR opens. Not in `errors`: every list load replaces those. @@ -787,17 +787,20 @@ final class PRsModel: ObservableObject { return } let pushed = Set(commits.map(\.sha)) + let lookup = beginRangeLookup() Task.detached(priority: .userInitiated) { [weak self] in let found = HubLocalCommits.resolve(ends: ends, repo: repo, head: head, pushed: pushed) await MainActor.run { guard let self, self.reviewPRID == shown.id, self.review === review else { return } - switch found { - case .success(let local): - self.showLocalCommits(local, pending: pending, in: commits, pr: shown, review: review) - case .failure(.unknown(let sha)): - self.showMissingCommit(pending, missing: sha, in: shown, review: review) - case .failure(.unrelated), .failure(.offBranch): - self.showMissingCommit(pending, missing: pending.to ?? pending.sha, in: shown, review: review) + self.finishRangeLookup(lookup) { + switch found { + case .success(let local): + self.showLocalCommits(local, pending: pending, in: commits, pr: shown, review: review) + case .failure(.unknown(let sha)): + self.showMissingCommit(pending, missing: sha, in: shown, review: review) + case .failure(.unrelated), .failure(.offBranch): + self.showMissingCommit(pending, missing: pending.to ?? pending.sha, in: shown, review: review) + } } } } @@ -830,6 +833,19 @@ final class PRsModel: ObservableObject { } } + /// Both detached lookups share request invalidation, including repeated links to the same PR and review. + func beginRangeLookup() -> UUID { + let lookup = UUID() + rangeLookup = lookup + return lookup + } + + func finishRangeLookup(_ lookup: UUID, apply: () -> Void) { + guard rangeLookup == lookup else { return } + rangeLookup = nil + apply() + } + /// A link's `commit=..`: those commits of the PR, both ends included, in either order. func showLinkedRange(_ one: HubPRDetail.Commit, _ other: HubPRDetail.Commit, in commits: [HubPRDetail.Commit], pr: HubPR, ref: HubPRRef, review: ReviewModel) { @@ -849,17 +865,17 @@ final class PRsModel: ObservableObject { // The first parent from git, not the commit listed before: a branched PR's list is no line of ancestry. let sha = commits[first].sha let root = review.repo.path - let lookup = UUID() - rangeLookup = lookup + let lookup = beginRangeLookup() Task { let parent = await Task.detached(priority: .userInitiated) { HubCommitLookup.parent(of: sha, in: root) }.value - guard rangeLookup == lookup, self.review === review else { return } - rangeLookup = nil - guard let parent else { - notice = PRNotice(ref: ref, text: HubCommitLookup.missingParent(sha, in: root)) - return + guard self.review === review else { return } + finishRangeLookup(lookup) { + guard let parent else { + notice = PRNotice(ref: ref, text: HubCommitLookup.missingParent(sha, in: root)) + return + } + show(from: parent) } - show(from: parent) } } diff --git a/src/macos/GenesisTools/Tests/HubLogicTests.swift b/src/macos/GenesisTools/Tests/HubLogicTests.swift index a1d881a630..c500bfd43d 100644 --- a/src/macos/GenesisTools/Tests/HubLogicTests.swift +++ b/src/macos/GenesisTools/Tests/HubLogicTests.swift @@ -156,7 +156,8 @@ final class HubLogicTests: XCTestCase { func git(_ args: String...) throws -> String { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/git") - process.arguments = ["-C", repo, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.com"] + args + process.arguments = ["-C", repo, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.com", + "-c", "core.hooksPath=/dev/null", "-c", "commit.gpgsign=false"] + args let out = Pipe() process.standardOutput = out try process.run() @@ -167,7 +168,7 @@ final class HubLogicTests: XCTestCase { } _ = try git("init", "-q", "-b", "main") var shas: [String] = [] - for index in 0 ..< 5 { + for index in 0 ..< 6 { _ = try git("commit", "-q", "--allow-empty", "-m", "commit \(index)") shas.append(try git("rev-parse", "HEAD")) } @@ -179,6 +180,24 @@ final class HubLogicTests: XCTestCase { let reversed = try? HubLocalCommits.resolve(ends: [shas[4], shas[2]], repo: repo, head: pushedHead, pushed: pushed).get() XCTAssertEqual(reversed?.first, shas[2], "either order") XCTAssertEqual(try? HubLocalCommits.resolve(ends: [shas[3]], repo: repo, head: pushedHead).get().unpushed, 1) + XCTAssertEqual(try? HubLocalCommits.resolve(ends: [shas[4]], repo: repo, head: pushedHead).get().unpushed, 1, + "a later single commit does not include earlier unpushed commits") + let localOnly = try HubLocalCommits.resolve(ends: [shas[3], shas[4]], repo: repo, head: pushedHead).get() + XCTAssertEqual(localOnly.count, 2) + XCTAssertEqual(localOnly.unpushed, 2, "both unpushed endpoints count") + let laterRange = try HubLocalCommits.resolve(ends: [shas[4], shas[4]], repo: repo, head: pushedHead).get() + XCTAssertEqual(laterRange.unpushed, 1, "equal endpoints include that commit once") + let pushedOnly = try HubLocalCommits.resolve(ends: [shas[2]], repo: repo, head: pushedHead, pushed: pushed).get() + XCTAssertEqual(pushedOnly.unpushed, 0) + let laterPair = try HubLocalCommits.resolve(ends: [shas[4], shas[5]], repo: repo, head: pushedHead).get() + XCTAssertEqual(laterPair.unpushed, 2, "earlier unpushed commits before this range do not count") + XCTAssertEqual(try HubLocalCommits.resolve(ends: [shas[5], shas[4]], repo: repo, head: pushedHead).get().unpushed, 2) + let tree = try git("rev-parse", pushedHead + "^{tree}") + let parallel = try git("commit-tree", tree, "-p", pushedHead, "-m", "parallel") + let merged = try git("commit-tree", tree, "-p", shas[5], "-p", parallel, "-m", "merged") + let mergedRange = try HubLocalCommits.resolve(ends: [shas[4], merged], repo: repo, head: pushedHead).get() + XCTAssertEqual(mergedRange.count, 3) + XCTAssertEqual(mergedRange.unpushed, 3, "exclude a merged side commit outside the first endpoint's ancestry path") guard case .failure(.offBranch) = HubLocalCommits.resolve(ends: [shas[0], shas[4]], repo: repo, head: pushedHead, pushed: pushed) else { return XCTFail("a first commit from before the PR would show the base's changes") } @@ -531,6 +550,27 @@ final class HubLogicTests: XCTestCase { XCTAssertNil(model.notice, "a whole-PR request clears the previous validation error too") } + @MainActor + func testANewPRLinkDiscardsBothOutcomesOfAnOlderLocalLookup() throws { + let model = PRsModel() + let ref = try XCTUnwrap(HubPRRef("7")) + // Bare refs and no rows keep these requests off the CLI. + for commit in ["abcdef0", "abcdef0..1234567", "invalid", ""] { + let old = model.beginRangeLookup() + model.request(ref, commit: commit.isEmpty ? nil : commit) + var applied: [String] = [] + model.finishRangeLookup(old) { applied.append("stale success") } + model.finishRangeLookup(old) { applied.append("stale failure") } + XCTAssertTrue(applied.isEmpty, "old results cannot change scope or reset it to the whole PR") + let current = model.beginRangeLookup() + model.finishRangeLookup(old) { applied.append("stale while newer lookup runs") } + model.finishRangeLookup(current) { applied.append("current") } + XCTAssertEqual(applied, ["current"], "discarding old results must preserve the newer lookup") + model.finishRangeLookup(current) { applied.append("duplicate") } + XCTAssertEqual(applied, ["current"]) + } + } + @MainActor func testAMissingPRCommitReplacesThePreviousCommitScopeWithTheWholePR() throws { let json = """