mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-11 04:16:55 +00:00
macos: show release notes link
This commit is contained in:
@@ -191,6 +191,28 @@ fileprivate struct UpdateAvailableView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
|
|
||||||
|
if let notes = update.releaseNotes {
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Link(destination: notes.url) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "doc.text")
|
||||||
|
.font(.system(size: 11))
|
||||||
|
Text(notes.label)
|
||||||
|
.font(.system(size: 11, weight: .medium))
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "arrow.up.right")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
}
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.padding(12)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(Color(nsColor: .controlBackgroundColor))
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -173,6 +173,76 @@ enum UpdateState: Equatable {
|
|||||||
struct UpdateAvailable {
|
struct UpdateAvailable {
|
||||||
let appcastItem: SUAppcastItem
|
let appcastItem: SUAppcastItem
|
||||||
let reply: @Sendable (SPUUserUpdateChoice) -> Void
|
let reply: @Sendable (SPUUserUpdateChoice) -> Void
|
||||||
|
|
||||||
|
var releaseNotes: ReleaseNotes? {
|
||||||
|
let currentCommit = Bundle.main.infoDictionary?["GhosttyCommit"] as? String
|
||||||
|
return ReleaseNotes(displayVersionString: appcastItem.displayVersionString, currentCommit: currentCommit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReleaseNotes {
|
||||||
|
case commit(URL)
|
||||||
|
case compareTip(URL)
|
||||||
|
case tagged(URL)
|
||||||
|
|
||||||
|
init?(displayVersionString: String, currentCommit: String?) {
|
||||||
|
let version = displayVersionString
|
||||||
|
|
||||||
|
// Check for semantic version (x.y.z)
|
||||||
|
if let semver = Self.extractSemanticVersion(from: version) {
|
||||||
|
let slug = semver.replacingOccurrences(of: ".", with: "-")
|
||||||
|
if let url = URL(string: "https://ghostty.org/docs/install/release-notes/\(slug)") {
|
||||||
|
self = .tagged(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to git hash detection
|
||||||
|
guard let newHash = Self.extractGitHash(from: version) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let currentHash = currentCommit, !currentHash.isEmpty,
|
||||||
|
let url = URL(string: "https://github.com/ghostty-org/ghostty/compare/\(currentHash)...\(newHash)") {
|
||||||
|
self = .compareTip(url)
|
||||||
|
} else if let url = URL(string: "https://github.com/ghostty-org/ghostty/commit/\(newHash)") {
|
||||||
|
self = .commit(url)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func extractSemanticVersion(from version: String) -> String? {
|
||||||
|
let pattern = #"^\d+\.\d+\.\d+$"#
|
||||||
|
if version.range(of: pattern, options: .regularExpression) != nil {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func extractGitHash(from version: String) -> String? {
|
||||||
|
let pattern = #"[0-9a-f]{7,40}"#
|
||||||
|
if let range = version.range(of: pattern, options: .regularExpression) {
|
||||||
|
return String(version[range])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var url: URL {
|
||||||
|
switch self {
|
||||||
|
case .commit(let url): return url
|
||||||
|
case .compareTip(let url): return url
|
||||||
|
case .tagged(let url): return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var label: String {
|
||||||
|
switch (self) {
|
||||||
|
case .commit: return "View GitHub Commit"
|
||||||
|
case .compareTip: return "Changes Since This Tip Release"
|
||||||
|
case .tagged: return "View Release Notes"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Error {
|
struct Error {
|
||||||
|
130
macos/Tests/Update/ReleaseNotesTests.swift
Normal file
130
macos/Tests/Update/ReleaseNotesTests.swift
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import Testing
|
||||||
|
import Foundation
|
||||||
|
@testable import Ghostty
|
||||||
|
|
||||||
|
struct ReleaseNotesTests {
|
||||||
|
/// Test tagged release (semantic version)
|
||||||
|
@Test func testTaggedRelease() async throws {
|
||||||
|
let notes = UpdateState.ReleaseNotes(
|
||||||
|
displayVersionString: "1.2.3",
|
||||||
|
currentCommit: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(notes != nil)
|
||||||
|
if case .tagged(let url) = notes {
|
||||||
|
#expect(url.absoluteString == "https://ghostty.org/docs/install/release-notes/1-2-3")
|
||||||
|
#expect(notes?.label == "View Release Notes")
|
||||||
|
} else {
|
||||||
|
Issue.record("Expected tagged case")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test tip release comparison with current commit
|
||||||
|
@Test func testTipReleaseComparison() async throws {
|
||||||
|
let notes = UpdateState.ReleaseNotes(
|
||||||
|
displayVersionString: "tip-abc1234",
|
||||||
|
currentCommit: "def5678"
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(notes != nil)
|
||||||
|
if case .compareTip(let url) = notes {
|
||||||
|
#expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/compare/def5678...abc1234")
|
||||||
|
#expect(notes?.label == "Changes Since This Tip Release")
|
||||||
|
} else {
|
||||||
|
Issue.record("Expected compareTip case")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test tip release without current commit
|
||||||
|
@Test func testTipReleaseWithoutCurrentCommit() async throws {
|
||||||
|
let notes = UpdateState.ReleaseNotes(
|
||||||
|
displayVersionString: "tip-abc1234",
|
||||||
|
currentCommit: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(notes != nil)
|
||||||
|
if case .commit(let url) = notes {
|
||||||
|
#expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/abc1234")
|
||||||
|
#expect(notes?.label == "View GitHub Commit")
|
||||||
|
} else {
|
||||||
|
Issue.record("Expected commit case")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test tip release with empty current commit
|
||||||
|
@Test func testTipReleaseWithEmptyCurrentCommit() async throws {
|
||||||
|
let notes = UpdateState.ReleaseNotes(
|
||||||
|
displayVersionString: "tip-abc1234",
|
||||||
|
currentCommit: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(notes != nil)
|
||||||
|
if case .commit(let url) = notes {
|
||||||
|
#expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/abc1234")
|
||||||
|
} else {
|
||||||
|
Issue.record("Expected commit case")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test version with full 40-character hash
|
||||||
|
@Test func testFullGitHash() async throws {
|
||||||
|
let notes = UpdateState.ReleaseNotes(
|
||||||
|
displayVersionString: "tip-1234567890abcdef1234567890abcdef12345678",
|
||||||
|
currentCommit: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(notes != nil)
|
||||||
|
if case .commit(let url) = notes {
|
||||||
|
#expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/1234567890abcdef1234567890abcdef12345678")
|
||||||
|
} else {
|
||||||
|
Issue.record("Expected commit case")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test version with no recognizable pattern
|
||||||
|
@Test func testInvalidVersion() async throws {
|
||||||
|
let notes = UpdateState.ReleaseNotes(
|
||||||
|
displayVersionString: "unknown-version",
|
||||||
|
currentCommit: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(notes == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test semantic version with prerelease suffix should not match
|
||||||
|
@Test func testSemanticVersionWithSuffix() async throws {
|
||||||
|
let notes = UpdateState.ReleaseNotes(
|
||||||
|
displayVersionString: "1.2.3-beta",
|
||||||
|
currentCommit: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
// Should not match semantic version pattern, falls back to hash detection
|
||||||
|
#expect(notes == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test semantic version with 4 components should not match
|
||||||
|
@Test func testSemanticVersionFourComponents() async throws {
|
||||||
|
let notes = UpdateState.ReleaseNotes(
|
||||||
|
displayVersionString: "1.2.3.4",
|
||||||
|
currentCommit: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
// Should not match pattern
|
||||||
|
#expect(notes == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test version string with git hash embedded
|
||||||
|
@Test func testVersionWithEmbeddedHash() async throws {
|
||||||
|
let notes = UpdateState.ReleaseNotes(
|
||||||
|
displayVersionString: "v2024.01.15-abc1234",
|
||||||
|
currentCommit: "def5678"
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(notes != nil)
|
||||||
|
if case .compareTip(let url) = notes {
|
||||||
|
#expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/compare/def5678...abc1234")
|
||||||
|
} else {
|
||||||
|
Issue.record("Expected compareTip case")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user