krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

0bef9acc515cfcb4ac6e2530cd34cbdbc47c2c1d

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-02T04:37:12Z

feat: show commit dates in Refs tab
 Hutch.xcodeproj/project.pbxproj                    | 16 ++--
 Hutch/Models/Git.swift                             |  7 ++
 Hutch/Views/Repositories/ReferencesListView.swift  | 13 +++-
 .../Repositories/RepositoryDetailViewModel.swift   | 42 ++++++++--
 .../Repositories/RepositorySettingsView.swift      |  2 +-
 .../Repositories/RepositorySettingsViewModel.swift |  4 +-
 codex-fix-refs-type-conflict.md                    | 89 ++++++++++++++++++++++
 7 files changed, 153 insertions(+), 20 deletions(-)

diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index 71bbc39..cdab899 100644
--- a/Hutch.xcodeproj/project.pbxproj
+++ b/Hutch.xcodeproj/project.pbxproj
@@ -539,7 +539,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 19;
+				CURRENT_PROJECT_VERSION = 20;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -556,7 +556,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 2.8.0;
+				MARKETING_VERSION = 2.8.1;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -576,7 +576,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 19;
+				CURRENT_PROJECT_VERSION = 20;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -593,7 +593,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 2.8.0;
+				MARKETING_VERSION = 2.8.1;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -656,7 +656,7 @@
 				APPLICATION_EXTENSION_API_ONLY = YES;
 				CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 19;
+				CURRENT_PROJECT_VERSION = 20;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				GENERATE_INFOPLIST_FILE = NO;
 				INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -666,7 +666,7 @@
 					"@executable_path/Frameworks",
 					"@executable_path/../../Frameworks",
 				);
-				MARKETING_VERSION = 2.8.0;
+				MARKETING_VERSION = 2.8.1;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
@@ -685,7 +685,7 @@
 				APPLICATION_EXTENSION_API_ONLY = YES;
 				CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 19;
+				CURRENT_PROJECT_VERSION = 20;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				GENERATE_INFOPLIST_FILE = NO;
 				INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -695,7 +695,7 @@
 					"@executable_path/Frameworks",
 					"@executable_path/../../Frameworks",
 				);
-				MARKETING_VERSION = 2.8.0;
+				MARKETING_VERSION = 2.8.1;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
diff --git a/Hutch/Models/Git.swift b/Hutch/Models/Git.swift
index 905822e..a45086b 100644
--- a/Hutch/Models/Git.swift
+++ b/Hutch/Models/Git.swift
@@ -82,6 +82,13 @@ struct Reference: Codable, Sendable, Hashable {
     let target: String?
 }
 
+/// A Git reference enriched with the date of its tip commit or tag, for display in the Refs tab.
+struct ReferenceDetail: Sendable, Hashable {
+    let name: String
+    let target: String?
+    let date: Date?
+}
+
 // MARK: - TreeEntry
 
 /// An entry in a Git tree (file or directory).
diff --git a/Hutch/Views/Repositories/ReferencesListView.swift b/Hutch/Views/Repositories/ReferencesListView.swift
index 615ba25..6470a42 100644
--- a/Hutch/Views/Repositories/ReferencesListView.swift
+++ b/Hutch/Views/Repositories/ReferencesListView.swift
@@ -51,14 +51,21 @@ struct ReferencesListView: View {
 }
 
 private struct ReferenceRow: View {
-    let reference: Reference
+    let reference: ReferenceDetail
     let prefix: String
 
     var body: some View {
         HStack {
             Label {
-                Text(shortName)
-                    .font(.body.monospaced())
+                VStack(alignment: .leading, spacing: 2) {
+                    Text(shortName)
+                        .font(.body.monospaced())
+                    if let date = reference.date {
+                        Text(date.relativeDescription)
+                            .font(.caption)
+                            .foregroundStyle(.secondary)
+                    }
+                }
             } icon: {
                 Image(systemName: icon)
                     .foregroundStyle(iconColor)
diff --git a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
index e2eaf4d..72c4d1f 100644
--- a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
+++ b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
@@ -24,10 +24,33 @@ private struct RefsRepository: Decodable, Sendable {
 }
 
 private struct RefsPage: Decodable, Sendable {
-    let results: [Reference]
+    let results: [ReferencePayload]
     let cursor: String?
 }
 
+/// Decodes a single reference including the optional follow object for date extraction.
+private struct ReferencePayload: Decodable, Sendable {
+    let name: String
+    let target: String?
+    let follow: ReferenceFollow?
+
+    func toDetail() -> ReferenceDetail {
+        // Branches and lightweight tags follow to a Commit (author.time).
+        // Annotated tags follow to a Tag object (tagger.time).
+        let date = follow?.author?.time ?? follow?.tagger?.time
+        return ReferenceDetail(name: name, target: target, date: date)
+    }
+}
+
+private struct ReferenceFollow: Decodable, Sendable {
+    let author: ReferenceSignature?
+    let tagger: ReferenceSignature?
+}
+
+private struct ReferenceSignature: Decodable, Sendable {
+    let time: Date
+}
+
 private struct ReadmeResponse: Decodable, Sendable {
     let repository: ReadmeRepository?
 }
@@ -98,8 +121,8 @@ final class RepositoryDetailViewModel {
 
     // MARK: - References state
 
-    private(set) var branches: [Reference] = []
-    private(set) var tags: [Reference] = []
+    private(set) var branches: [ReferenceDetail] = []
+    private(set) var tags: [ReferenceDetail] = []
     private(set) var isLoadingRefs = false
 
     // MARK: - README state
@@ -220,7 +243,14 @@ final class RepositoryDetailViewModel {
     query refs($rid: ID!) {
         repository(rid: $rid) {
             references {
-                results { name target }
+                results {
+                    name
+                    target
+                    follow {
+                        ... on Commit { author { time } }
+                        ... on Tag { tagger { time } }
+                    }
+                }
                 cursor
             }
         }
@@ -240,8 +270,8 @@ final class RepositoryDetailViewModel {
                 responseType: RefsResponse.self
             )
             let allRefs = result.repository?.references.results ?? []
-            branches = allRefs.filter { $0.name.hasPrefix("refs/heads/") }
-            tags = allRefs.filter { $0.name.hasPrefix("refs/tags/") }
+            branches = allRefs.filter { $0.name.hasPrefix("refs/heads/") }.map { $0.toDetail() }
+            tags = allRefs.filter { $0.name.hasPrefix("refs/tags/") }.map { $0.toDetail() }
         } catch {
             self.error = error.userFacingMessage
         }
diff --git a/Hutch/Views/Repositories/RepositorySettingsView.swift b/Hutch/Views/Repositories/RepositorySettingsView.swift
index aca2ff9..98d3d8a 100644
--- a/Hutch/Views/Repositories/RepositorySettingsView.swift
+++ b/Hutch/Views/Repositories/RepositorySettingsView.swift
@@ -2,7 +2,7 @@ import SwiftUI
 
 struct RepositorySettingsView: View {
     let repository: RepositorySummary
-    let branches: [Reference]
+    let branches: [ReferenceDetail]
     let client: SRHTClient
     let onRenamed: (String) -> Void
     let onDeleted: () -> Void
diff --git a/Hutch/Views/Repositories/RepositorySettingsViewModel.swift b/Hutch/Views/Repositories/RepositorySettingsViewModel.swift
index 2847cb7..571ae85 100644
--- a/Hutch/Views/Repositories/RepositorySettingsViewModel.swift
+++ b/Hutch/Views/Repositories/RepositorySettingsViewModel.swift
@@ -102,7 +102,7 @@ final class RepositorySettingsViewModel {
 
     // MARK: - Branches (for HEAD picker)
 
-    var branches: [Reference]
+    var branches: [ReferenceDetail]
 
     // MARK: - Results
 
@@ -112,7 +112,7 @@ final class RepositorySettingsViewModel {
 
     init(
         repository: RepositorySummary,
-        branches: [Reference],
+        branches: [ReferenceDetail],
         client: SRHTClient
     ) {
         self.repositoryId = repository.id
diff --git a/codex-fix-refs-type-conflict.md b/codex-fix-refs-type-conflict.md
new file mode 100644
index 0000000..4ff2eec
--- /dev/null
+++ b/codex-fix-refs-type-conflict.md
@@ -0,0 +1,89 @@
+# Fix: Conflicting `[Reference]` vs `[ReferenceDetail]` in RepositorySettingsView
+
+## Background
+
+`RepositoryDetailViewModel.branches` was recently changed from `[Reference]` to
+`[ReferenceDetail]` to support displaying commit dates in the Refs tab.
+`ReferenceDetail` has the same `name: String` and `target: String?` fields as
+`Reference`, plus an additional `date: Date?`.
+
+`RepositorySettingsView` and `RepositorySettingsViewModel` still declare their
+`branches` parameter as `[Reference]`, causing this compiler error:
+
+```
+RepositoryDetailView.swift:56:51
+Conflicting arguments to generic parameter 'T' ('[ReferenceDetail]' vs. '[Reference]')
+```
+
+The settings view model only uses `branches` for the HEAD branch picker
+(`selectedHeadReferenceForSave()` accesses `$0.name`). No mapping back to
+`Reference` is needed — just update the type throughout the settings layer.
+
+---
+
+## Changes Required
+
+### 1. `Hutch/Views/Repositories/RepositorySettingsViewModel.swift`
+
+**Change the stored property type:**
+
+Find:
+```swift
+var branches: [Reference]
+```
+
+Replace with:
+```swift
+var branches: [ReferenceDetail]
+```
+
+**Change the `init` parameter type:**
+
+Find:
+```swift
+init(
+    repository: RepositorySummary,
+    branches: [Reference],
+    client: SRHTClient
+) {
+```
+
+Replace with:
+```swift
+init(
+    repository: RepositorySummary,
+    branches: [ReferenceDetail],
+    client: SRHTClient
+) {
+```
+
+### 2. `Hutch/Views/Repositories/RepositorySettingsView.swift`
+
+**Change the stored property type:**
+
+Find:
+```swift
+let branches: [Reference]
+```
+
+Replace with:
+```swift
+let branches: [ReferenceDetail]
+```
+
+---
+
+## No Other Changes
+
+- Do not modify `Reference` or `ReferenceDetail` in `Git.swift`.
+- Do not modify `RepositoryDetailView.swift` — the call site is already correct.
+- Do not modify `ReferencesListView.swift` or `RepositoryDetailViewModel.swift`.
+- `selectedHeadReferenceForSave()` in the settings view model accesses only
+  `$0.name` on each branch, which exists on `ReferenceDetail` — no logic
+  changes are needed.
+
+## Verification
+
+Build the project. The compiler error at `RepositoryDetailView.swift:56` should
+be gone. Confirm the repository settings sheet still opens and the HEAD branch
+picker populates correctly.