Enforce Rust and Swift ownership boundary

This commit is contained in:
Georg Bauer
2026-07-31 14:10:00 +02:00
parent 6eb27537e8
commit a2615d5d83
10 changed files with 576 additions and 190 deletions

View File

@@ -135,13 +135,10 @@ final class IssuesViewController: RefreshingTableViewController {
private func milestoneMenu(_ options: IssueFilterOptions) -> UIMenu {
let selected = options.selectedMilestone
var milestones = options.milestones
if !selected.isEmpty, !milestones.contains(selected) { milestones.append(selected) }
let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) {
[weak self] _ in self?.selectMilestone("")
}
let actions = milestones.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
.map { milestone in
let actions = options.milestones.map { milestone in
UIAction(
title: milestone,
state: selected == milestone ? .on : .off
@@ -156,13 +153,10 @@ final class IssuesViewController: RefreshingTableViewController {
}
private func labelMenu(_ options: IssueFilterOptions) -> UIMenu {
let available = Set(options.labels)
let selected = Set(options.selectedLabels)
let labels = available.union(selected)
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
let actions = labels.map { label in
let actions = options.labels.map { label in
UIAction(
title: available.contains(label) ? label : "\(label) (Unavailable)",
title: options.unavailableLabels.contains(label) ? "\(label) (Unavailable)" : label,
attributes: .keepsMenuPresented,
state: selected.contains(label) ? .on : .off
) { [weak self] action in
@@ -172,7 +166,7 @@ final class IssuesViewController: RefreshingTableViewController {
if labels.remove(label) == nil { labels.insert(label) }
do {
try self.saveFilters(milestone: options.selectedMilestone, labels: labels)
self.filterOptions?.selectedLabels = labels.sorted()
self.filterOptions?.selectedLabels = Array(labels)
action.state = labels.contains(label) ? .on : .off
self.updateFilterMenu()
self.loadIssues(refreshing: false)
@@ -208,14 +202,12 @@ final class IssuesViewController: RefreshingTableViewController {
owner: owner,
repository: repository,
milestone: milestone,
labels: labels.sorted()
labels: Array(labels)
)
}
private var filtersActive: Bool {
context.core.settings().issueStatus != "open"
|| filterOptions?.selectedMilestone.isEmpty == false
|| filterOptions?.selectedLabels.isEmpty == false
(try? context.core.issueFiltersActive(owner: owner, repository: repository)) ?? false
}
private func updateFilterTint() {
@@ -519,11 +511,10 @@ final class MilestoneCell: UITableViewCell {
titleLabel.text = row.title
descriptionLabel.text = row.description
metaLabel.text = row.meta
let total = row.openIssues + row.closedIssues
progress.progress = total == 0 ? 0 : Float(row.closedIssues) / Float(total)
progress.trackTintColor = total == 0 ? .systemGray5 : .systemOrange
progress.progress = Float(row.progress)
progress.trackTintColor = row.hasIssues ? .systemOrange : .systemGray5
progress.accessibilityLabel = "Milestone progress"
progress.accessibilityValue = "\(row.closedIssues) closed, \(row.openIssues) open"
progress.accessibilityValue = row.progressAccessibility
}
}
@@ -599,7 +590,7 @@ final class PullsViewController: RefreshingTableViewController {
let row = rows[indexPath.row]
configureTextCell(
cell,
title: "\(row.repository) #\(row.number)\n\(row.title)",
title: row.title,
detail: "\(row.summary)\n\(row.meta)"
)
cell.accessoryType = .disclosureIndicator
@@ -819,9 +810,7 @@ final class CommitCell: UITableViewCell {
var content = defaultContentConfiguration()
content.directionalLayoutMargins.leading = laneCount == 0 ? 0 : min(72, CGFloat(laneCount) * 10 + 8)
content.text = row.title
content.secondaryText = row.refs.isEmpty
? "\(row.meta)\n\(row.sha.prefix(8))"
: "\(row.refs)\n\(row.meta) · \(row.sha.prefix(8))"
content.secondaryText = row.detail
content.secondaryTextProperties.numberOfLines = 2
contentConfiguration = content
setNeedsDisplay()

View File

@@ -3,7 +3,6 @@ import MarkdownUI
import QuickLook
import SwiftUI
import UIKit
import UniformTypeIdentifiers
@MainActor
class MarkdownPageViewController: UIViewController {
@@ -273,13 +272,13 @@ final class RepositoryDirectoryViewController: RefreshingTableViewController {
private let path: String
private var rows: [RepositoryContentRow] = []
init(context: AppContext, owner: String, repository: String, path: String) {
init(context: AppContext, owner: String, repository: String, path: String, name: String) {
self.context = context
self.owner = owner
self.repository = repository
self.path = path
super.init()
title = path.split(separator: "/").last.map(String.init) ?? repository
title = name
}
@available(*, unavailable)
@@ -349,18 +348,19 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
private var loadingTask: Task<Void, Never>?
private var previewURL: URL?
private var markdownSource: String?
private var fileLanguage: String?
private var displayedController: UIViewController?
private var displayedView: UIView?
private weak var sourceScrollView: UIScrollView?
private weak var sourceTextView: UITextView?
init(context: AppContext, owner: String, repository: String, path: String) {
init(context: AppContext, owner: String, repository: String, path: String, name: String) {
self.context = context
self.owner = owner
self.repository = repository
self.path = path
super.init(nibName: nil, bundle: nil)
title = path.split(separator: "/").last.map(String.init)
title = name
}
@available(*, unavailable)
@@ -372,19 +372,17 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
beginNavigationLoading(spinner)
loadingTask = Task {
do {
let data = try await context.core.repositoryFile(
let page = try await context.core.repositoryFile(
owner: owner,
repository: repository,
path: path
)
if !isMediaFile(path), let source = String(data: data, encoding: .utf8) {
if isMarkdownFile {
showMarkdown(source)
} else {
showSource(source)
}
} else {
try showQuickLook(data)
title = page.name
fileLanguage = page.language.isEmpty ? nil : page.language
switch page.kind {
case "markdown": showMarkdown(page.text)
case "source": showSource(page.text)
default: try showQuickLook(page.data, name: page.name)
}
} catch {
if !Task.isCancelled { show(error: error) }
@@ -398,20 +396,6 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
updateSourceLayout()
}
private func isMediaFile(_ path: String) -> Bool {
guard let type = UTType(filenameExtension: (path as NSString).pathExtension) else {
return false
}
return type.conforms(to: .image)
|| type.conforms(to: .audio)
|| type.conforms(to: .audiovisualContent)
|| type.conforms(to: .pdf)
}
private var isMarkdownFile: Bool {
["md", "markdown", "mdown", "mkd"].contains((path as NSString).pathExtension.lowercased())
}
deinit {
loadingTask?.cancel()
if let previewURL { try? FileManager.default.removeItem(at: previewURL) }
@@ -464,7 +448,7 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
traitCollection.userInterfaceStyle == .dark ? "atom-one-dark" : "atom-one-light"
)
highlighter?.theme.setCodeFont(.monospacedSystemFont(ofSize: 13, weight: .regular))
let highlighted = highlighter?.highlight(source, as: sourceLanguage(path))
let highlighted = highlighter?.highlight(source, as: fileLanguage)
?? NSAttributedString(string: source, attributes: [
.font: UIFont.monospacedSystemFont(ofSize: 13, weight: .regular),
.foregroundColor: UIColor.label,
@@ -532,8 +516,7 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
scrollView.contentSize = contentSize
}
private func showQuickLook(_ data: Data) throws {
let name = path.split(separator: "/").last.map(String.init) ?? "Preview"
private func showQuickLook(_ data: Data, name: String) throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("\(UUID().uuidString)-\(name)")
try data.write(to: url, options: .atomic)

View File

@@ -303,17 +303,6 @@ final class HomeViewController: RefreshingTableViewController {
case all
case issues
case pulls
func includes(_ row: ActivityRow) -> Bool {
switch self {
case .all:
return true
case .issues:
return row.icon.contains("issue")
case .pulls:
return row.icon.contains("pull")
}
}
}
private let context: AppContext
@@ -321,7 +310,12 @@ final class HomeViewController: RefreshingTableViewController {
private var filter = ActivityFilter.all
private var activities: [ActivityRow] {
page?.activities.filter(filter.includes) ?? []
guard let page else { return [] }
switch filter {
case .all: return page.activities
case .issues: return page.issueActivities
case .pulls: return page.pullActivities
}
}
init(context: AppContext) {
@@ -443,20 +437,7 @@ final class HeatmapView: UIView {
private let calendar = Calendar(identifier: .gregorian)
init(page: HomePage, selectedFilter: Int, onFilter: @escaping (Int) -> Void) {
let latest = page.heatCells.last.map {
Date(timeIntervalSince1970: TimeInterval($0.timestamp))
} ?? Date()
let latestMonth = Calendar(identifier: .gregorian).date(
from: Calendar(identifier: .gregorian).dateComponents([.year, .month], from: latest)
) ?? latest
let firstMonth = Calendar(identifier: .gregorian).date(
byAdding: .month,
value: -8,
to: latestMonth
) ?? latestMonth
cells = page.heatCells.filter {
Date(timeIntervalSince1970: TimeInterval($0.timestamp)) >= firstMonth
}
cells = page.heatCells
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 180))
backgroundColor = .systemBackground
let title = UILabel(frame: CGRect(x: 16, y: 12, width: 300, height: 22))
@@ -465,7 +446,7 @@ final class HeatmapView: UIView {
title.textColor = .secondaryLabel
addSubview(title)
let total = UILabel(frame: CGRect(x: 16, y: 108, width: 300, height: 18))
total.text = "\(cells.reduce(0) { $0 + $1.contributions }) contributions"
total.text = "\(page.contributionCount) contributions"
total.font = .preferredFont(forTextStyle: .caption1)
total.textColor = .tertiaryLabel
addSubview(total)

View File

@@ -151,39 +151,19 @@ func showRepositoryContent(
context: context,
owner: owner,
repository: repository,
path: row.path
path: row.path,
name: row.name
)
: RepositoryFileViewController(
context: context,
owner: owner,
repository: repository,
path: row.path
path: row.path,
name: row.name
)
navigationController?.pushViewController(destination, animated: true)
}
func sourceLanguage(_ path: String) -> String? {
let filename = (path as NSString).lastPathComponent.lowercased()
let filenames = [
"cmakelists.txt": "cmake", "dockerfile": "dockerfile", "gemfile": "ruby",
"makefile": "makefile", "podfile": "ruby",
]
if let language = filenames[filename] { return language }
let languages = [
"asm": "x86asm", "c": "c", "cc": "cpp", "clj": "clojure", "cpp": "cpp",
"cs": "csharp", "css": "css", "dart": "dart", "ex": "elixir", "exs": "elixir",
"fs": "fsharp", "go": "go", "groovy": "groovy", "h": "cpp", "hpp": "cpp",
"hs": "haskell", "html": "xml", "java": "java", "jl": "julia", "js": "javascript",
"json": "json", "jsx": "javascript", "kt": "kotlin", "kts": "kotlin", "lua": "lua",
"m": "objectivec", "md": "markdown", "mm": "objectivec", "php": "php", "pl": "perl",
"plist": "xml", "ps1": "powershell", "py": "python", "r": "r", "rb": "ruby",
"rs": "rust", "scala": "scala", "scss": "scss", "sh": "bash", "sql": "sql",
"swift": "swift", "toml": "ini", "ts": "typescript", "tsx": "typescript",
"txt": "plaintext", "xml": "xml", "yaml": "yaml", "yml": "yaml",
]
return languages[(path as NSString).pathExtension.lowercased()]
}
func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) -> NSAttributedString {
let output = NSMutableAttributedString()
if let image = UIImage(systemName: symbol)?.withTintColor(color, renderingMode: .alwaysOriginal) {