Repository navigation
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 #595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -147,6 +147,74 @@ final class HubLogicTests: XCTestCase { | |
| } | ||
|
|
||
| /// `commit=<from>..<to>`: 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", | ||
| "-c", "core.hooksPath=/dev/null", "-c", "commit.gpgsign=false"] + 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 ..< 6 { | ||
| _ = 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")) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧪 Tests | 🔵 Low · confidence 93/100 Test application of a resolved local range to the review The new fixture verifies HubLocalCommits.resolve, and the other new test verifies request-token invalidation, but neither exercises showLocalCommits. Consequently the user-visible half of this fix—the PR-base versus first-parent scope choice, PRLinkedRange pushed-sha marking, and informational notice—is not asserted. Existing rangeScope tests exercise the separate forge-listed path, not the new implementation. Add model-level coverage applying local results for a range starting at the PR's first commit, a later pushed commit, and a local-only commit, asserting scope, linkedRange.shas, and notice.isError/count. These are explicit promises in the PR description and can regress while the resolver fixture continues passing. 🧩 AnalysisGrep evidence:
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accepted. The follow-up will test application of a resolved local range to the review. The pull request was merged after its fix rounds, so this finding waits for a follow-up, and this thread stays open until then. |
||
| 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") | ||
| } | ||
| 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")) | ||
|
|
@@ -482,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 = """ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚡ Performance | 🟡 Medium · confidence 92/100
Bound the local git subprocess waits with a deadline
The new local lookup blocks on both readDataToEndOfFile() and waitUntilExit() without any deadline or cancellation. Moving this to a detached task protects the UI thread, but a stalled git process (for example on an unavailable filesystem) leaves the link unresolved indefinitely, and subsequent links can accumulate more blocked tasks and children. Invalidating rangeLookup only discards the eventual answer; it does not stop the process. This violates the project review-memory rule that every wait needs a deadline and an explicit expiry outcome. Bound the entire subprocess operation, terminate/reap it on expiry, and surface a lookup failure rather than leaving it pending.
🧩 Analysis
Grep evidence:
readDataToEndOfFile\(|waitUntilExit\(|HubLocalCommits.resolveThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accepted. The follow-up will bound the local git subprocess waits with a deadline. The pull request was merged after its fix rounds, so this finding waits for a follow-up, and this thread stays open until then.