diff options
Diffstat (limited to 'Hutch/Views/Repositories')
8 files changed, 157 insertions, 72 deletions
diff --git a/Hutch/Views/Repositories/HgRepositoryDetailView.swift b/Hutch/Views/Repositories/HgRepositoryDetailView.swift index ac7c04f..8362467 100644 --- a/Hutch/Views/Repositories/HgRepositoryDetailView.swift +++ b/Hutch/Views/Repositories/HgRepositoryDetailView.swift @@ -7,6 +7,7 @@ struct HgRepositoryDetailView: View { @Environment(AppState.self) private var appState @Environment(\.dismiss) private var dismiss + @Environment(\.openURL) private var openURL @Environment(\.colorScheme) private var colorScheme @AppStorage(AppStorageKeys.wrapRepositoryFileLines) private var wrapRepositoryFileLines = false @@ -99,6 +100,7 @@ struct HgRepositoryDetailView: View { async let log: () = vm.loadLog() _ = await (summary, browse, log) } + RecentActivityStore.recordRepository(repository, defaults: appState.accountDefaults) } } @@ -132,6 +134,42 @@ struct HgRepositoryDetailView: View { } } + Divider() + + if let repositoryURL = SRHTWebURL.repository(repository) { + Button { + openURL(repositoryURL) + } label: { + Label("Open in Browser", systemImage: "safari") + } + + Button { + appState.copyToPasteboard(repositoryURL.absoluteString, label: "repository URL") + } label: { + Label("Copy URL", systemImage: "doc.on.doc") + } + } + + if let httpsURL = SRHTWebURL.httpsCloneURL(repository) { + Button { + appState.copyToPasteboard(httpsURL, label: "HTTPS clone URL") + } label: { + Label("Copy HTTPS URL", systemImage: "doc.on.doc") + } + } + + Button { + appState.copyToPasteboard(SRHTWebURL.sshCloneURL(repository), label: "SSH clone URL") + } label: { + Label("Copy SSH URL", systemImage: "terminal") + } + + Button { + appState.copyToPasteboard(repository.rid, label: "repository RID") + } label: { + Label("Copy RID", systemImage: "number") + } + if canManageRepository { Divider() @@ -227,10 +265,12 @@ struct HgRepositoryDetailView: View { @ViewBuilder private func metadataSection(_ viewModel: HgRepositoryDetailViewModel) -> some View { VStack(alignment: .leading, spacing: 10) { - SummaryMetadataRow( - icon: "arrow.triangle.branch", - title: viewModel.tip?.branch ?? repository.head?.name ?? repositoryVisibilityLabel(repository.visibility) - ) + if let branchLabel = repositoryPrimaryBranchLabel(for: repository, hgTipBranch: viewModel.tip?.branch) { + SummaryMetadataRow( + icon: "arrow.triangle.branch", + title: branchLabel + ) + } if let readmePath = viewModel.readmePath { SummaryMetadataRow( @@ -244,6 +284,7 @@ struct HgRepositoryDetailView: View { private func repositoryDetailsSection(_ viewModel: HgRepositoryDetailViewModel) -> some View { DisclosureGroup(isExpanded: $isShowingRepositoryDetails) { VStack(alignment: .leading, spacing: 12) { + SummaryDetailRow(label: "Forge", value: repositoryForgeLabel(repository.service)) SummaryDetailRow(label: "Visibility", value: repositoryVisibilityLabel(repository.visibility)) SummaryDetailRow(label: "Publishing", value: viewModel.nonPublishing ? "Non-publishing" : "Publishing") SummaryDetailRow(label: "Read-only", value: repositoryCloneURLs(for: repository).readOnly, monospace: true) diff --git a/Hutch/Views/Repositories/HgRepositoryDetailViewModel.swift b/Hutch/Views/Repositories/HgRepositoryDetailViewModel.swift index 5a0298c..d8303e1 100644 --- a/Hutch/Views/Repositories/HgRepositoryDetailViewModel.swift +++ b/Hutch/Views/Repositories/HgRepositoryDetailViewModel.swift @@ -526,20 +526,9 @@ final class HgRepositoryDetailViewModel { } private func isEmptyRepositoryError(_ error: Error) -> Bool { - if let srhtError = error as? SRHTError, - case .graphQLErrors(let errors) = srhtError { - return errors.contains { - let message = $0.message.localizedLowercase - return message.contains("missing") - || message.contains("not found") - || message.contains("unknown revision") - || message.contains("unknown revision or path not in the working tree") - } - } - - let message = error.localizedDescription.localizedLowercase - return message.contains("missing") - || message.contains("not found") - || message.contains("unknown revision") + error.matchesGraphQLErrorClassification(.notFound) + || error.matchesGraphQLErrorClassification(.unknownRevision) + || error.matchesGraphQLErrorClassification(.noRows) + || error.containsGraphQLErrorMessage("missing") } } diff --git a/Hutch/Views/Repositories/HgRepositorySettingsView.swift b/Hutch/Views/Repositories/HgRepositorySettingsView.swift index 6a27465..a6aa728 100644 --- a/Hutch/Views/Repositories/HgRepositorySettingsView.swift +++ b/Hutch/Views/Repositories/HgRepositorySettingsView.swift @@ -9,7 +9,6 @@ struct HgRepositorySettingsView: View { @State private var viewModel: HgRepositorySettingsViewModel? @State private var showDeleteConfirmation = false @State private var pendingACLDeletion: HgACLEntry? - @State private var saveResultAlert: SaveResultAlert? var body: some View { NavigationStack { @@ -93,23 +92,26 @@ struct HgRepositorySettingsView: View { Text("\(entry.entity.canonicalName) will lose \(entry.mode) access to this repository.") } } - .alert(item: $saveResultAlert) { alert in - Alert( - title: Text(alert.title), - message: Text(alert.message), - dismissButton: .default(Text("OK")) - ) - } } @ViewBuilder private func infoSection(_ viewModel: HgRepositorySettingsViewModel) -> some View { - Section("Info") { - LabeledContent("Name") { - Text(repository.name) + Section("Current Configuration") { + LabeledContent("Repository") { + Text("\(repository.owner.canonicalName)/\(repository.name)") .font(.body.monospaced()) } + LabeledContent("Forge") { + Text(repositoryForgeLabel(repository.service)) + } + + LabeledContent("Visibility") { + Text(repositoryVisibilityLabel(viewModel.editedVisibility)) + } + } + + Section("Repository Details") { TextField("Description", text: Bindable(viewModel).editedDescription, axis: .vertical) .lineLimit(3...6) @@ -121,11 +123,7 @@ struct HgRepositorySettingsView: View { Button { Task { - let didSave = await viewModel.saveInfo() - saveResultAlert = SaveResultAlert( - title: didSave ? "Settings Updated" : "Couldn't Update Settings", - message: didSave ? "Repository settings were saved." : (viewModel.error ?? "Please try again.") - ) + _ = await viewModel.saveInfo() } } label: { if viewModel.isSavingInfo { @@ -136,7 +134,7 @@ struct HgRepositorySettingsView: View { .frame(maxWidth: .infinity) } } - .disabled(viewModel.isSavingInfo) + .disabled(viewModel.isSavingInfo || !viewModel.isInfoDirty) } } @@ -202,16 +200,15 @@ struct HgRepositorySettingsView: View { @ViewBuilder private func featuresSection(_ viewModel: HgRepositorySettingsViewModel) -> some View { - Section("Features") { + Section("Sensitive Settings") { Toggle("Hide this repository from public listings", isOn: Bindable(viewModel).editedNonPublishing) + Text("Changes stay pending until you save this section.") + .font(.caption) + .foregroundStyle(.secondary) Button { Task { - let didSave = await viewModel.saveInfo() - saveResultAlert = SaveResultAlert( - title: didSave ? "Settings Updated" : "Couldn't Update Settings", - message: didSave ? "Repository settings were saved." : (viewModel.error ?? "Please try again.") - ) + _ = await viewModel.saveInfo() } } label: { if viewModel.isSavingInfo { @@ -222,7 +219,7 @@ struct HgRepositorySettingsView: View { .frame(maxWidth: .infinity) } } - .disabled(viewModel.isSavingInfo) + .disabled(viewModel.isSavingInfo || !viewModel.isInfoDirty) } } @@ -261,13 +258,8 @@ struct HgRepositorySettingsView: View { } } .disabled(viewModel.isDeleting) + } header: { + Text("Danger Zone") } } - - private struct SaveResultAlert: Identifiable { - let title: String - let message: String - - var id: String { "\(title)-\(message)" } - } } diff --git a/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift b/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift index a1a11ae..7b8d7f7 100644 --- a/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift +++ b/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift @@ -64,11 +64,14 @@ final class HgRepositorySettingsViewModel { let repositoryRid: String let repositoryName: String private let client: SRHTClient + private var initialDescription: String + private var initialVisibility: Visibility var editedDescription: String var editedVisibility: Visibility var editedNonPublishing: Bool var isSavingInfo = false + private(set) var loadedNonPublishing = false private(set) var acls: [HgACLEntry] = [] private(set) var isLoadingACLs = false @@ -82,12 +85,25 @@ final class HgRepositorySettingsViewModel { var didDelete = false var error: String? + var normalizedEditedDescription: String { + editedDescription.trimmingCharacters(in: .whitespacesAndNewlines) + } + + var isInfoDirty: Bool { + normalizedEditedDescription != initialDescription || + editedVisibility != initialVisibility || + editedNonPublishing != loadedNonPublishing + } + init(repository: RepositorySummary, client: SRHTClient) { self.repositoryId = repository.id self.repositoryRid = repository.rid self.repositoryName = repository.name self.client = client - self.editedDescription = repository.description ?? "" + let description = repository.description ?? "" + self.initialDescription = description + self.initialVisibility = repository.visibility + self.editedDescription = description self.editedVisibility = repository.visibility self.editedNonPublishing = false } @@ -159,9 +175,14 @@ final class HgRepositorySettingsViewModel { ) if let repository = result.repository { - editedDescription = repository.description ?? "" + let description = repository.description ?? "" + initialDescription = description + initialVisibility = repository.visibility + editedDescription = description editedVisibility = repository.visibility - editedNonPublishing = repository.nonPublishing ?? false + let nonPublishing = repository.nonPublishing ?? false + editedNonPublishing = nonPublishing + loadedNonPublishing = nonPublishing } } catch { self.error = error.userFacingMessage @@ -175,7 +196,7 @@ final class HgRepositorySettingsViewModel { do { let input: [String: any Sendable] = [ - "description": editedDescription, + "description": normalizedEditedDescription, "visibility": editedVisibility.rawValue, "nonPublishing": editedNonPublishing ] @@ -185,6 +206,9 @@ final class HgRepositorySettingsViewModel { variables: ["id": repositoryId, "input": input], responseType: HgUpdateRepositoryResponse.self ) + initialDescription = normalizedEditedDescription + initialVisibility = editedVisibility + loadedNonPublishing = editedNonPublishing return true } catch { self.error = error.userFacingMessage @@ -237,9 +261,8 @@ final class HgRepositorySettingsViewModel { } newACLEntity = "" } catch { - let message = error.localizedDescription - if message.localizedCaseInsensitiveContains("No such repository or user found") { - self.error = "That user is not available on hg.sr.ht yet. They need to create or activate an hg.sr.ht repository first." + if error.matchesGraphQLErrorClassification(.serviceNotProvisioned) { + self.error = error.userFacingMessage } else { self.error = error.userFacingMessage } diff --git a/Hutch/Views/Repositories/ReadmeView.swift b/Hutch/Views/Repositories/ReadmeView.swift index f5c0c9f..21d7a74 100644 --- a/Hutch/Views/Repositories/ReadmeView.swift +++ b/Hutch/Views/Repositories/ReadmeView.swift @@ -54,10 +54,12 @@ struct ReadmeView: View { @ViewBuilder private var metadataSection: some View { VStack(alignment: .leading, spacing: 10) { - SummaryMetadataRow( - icon: "arrow.triangle.branch", - title: viewModel.repository.head?.name ?? repositoryVisibilityLabel(viewModel.repository.visibility) - ) + if let branchLabel = repositoryPrimaryBranchLabel(for: viewModel.repository) { + SummaryMetadataRow( + icon: "arrow.triangle.branch", + title: branchLabel + ) + } if let readmePath = viewModel.readmePath { SummaryMetadataRow( @@ -71,6 +73,7 @@ struct ReadmeView: View { private var repositoryDetailsSection: some View { DisclosureGroup(isExpanded: $isShowingRepositoryDetails) { VStack(alignment: .leading, spacing: 12) { + SummaryDetailRow(label: "Forge", value: repositoryForgeLabel(viewModel.repository.service)) SummaryDetailRow(label: "Visibility", value: repositoryVisibilityLabel(viewModel.repository.visibility)) SummaryDetailRow(label: "Read-only", value: repositoryCloneURLs(for: viewModel.repository).readOnly, monospace: true) SummaryDetailRow(label: "Read/write", value: repositoryCloneURLs(for: viewModel.repository).readWrite, monospace: true) diff --git a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift index 72c4d1f..ebd07d2 100644 --- a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift +++ b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift @@ -372,9 +372,7 @@ final class RepositoryDetailViewModel { } private func isMissingGitReferenceError(_ error: Error) -> Bool { - guard let srhtError = error as? SRHTError else { return false } - guard case .graphQLErrors(let errors) = srhtError else { return false } - return errors.contains { $0.message.localizedCaseInsensitiveContains("reference not found") } + error.matchesGraphQLErrorClassification(.missingReference) } // MARK: - Artifacts diff --git a/Hutch/Views/Repositories/RepositoryRowView.swift b/Hutch/Views/Repositories/RepositoryRowView.swift index 0da6cbc..afbd740 100644 --- a/Hutch/Views/Repositories/RepositoryRowView.swift +++ b/Hutch/Views/Repositories/RepositoryRowView.swift @@ -15,14 +15,7 @@ struct RepositoryRowView: View { Spacer() - if repository.service == .hg { - Text("HG") - .font(.caption2.weight(.medium)) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.cyan.opacity(0.15), in: Capsule()) - .foregroundStyle(.cyan) - } + RepositoryForgeBadge(service: repository.service) if buildStatus != .none { RepositoryBuildStatusIndicator(status: buildStatus) diff --git a/Hutch/Views/Repositories/RepositorySummarySupport.swift b/Hutch/Views/Repositories/RepositorySummarySupport.swift index 7861b40..741ff39 100644 --- a/Hutch/Views/Repositories/RepositorySummarySupport.swift +++ b/Hutch/Views/Repositories/RepositorySummarySupport.swift @@ -46,6 +46,28 @@ func repositoryVisibilityLabel(_ visibility: Visibility) -> String { } } +func repositoryForgeLabel(_ service: SRHTService) -> String { + switch service { + case .git: + return "GIT" + case .hg: + return "HG" + default: + return service.rawValue.uppercased() + } +} + +func repositoryPrimaryBranchLabel(for repository: RepositorySummary, hgTipBranch: String? = nil) -> String? { + switch repository.service { + case .git: + return repository.defaultBranchName + case .hg: + return hgTipBranch ?? repository.defaultBranchName ?? "tip" + default: + return repository.defaultBranchName + } +} + struct SummaryMetadataRow: View { let icon: String let title: String @@ -85,3 +107,27 @@ struct SummaryDetailRow: View { } } } + +struct RepositoryForgeBadge: View { + let service: SRHTService + + var body: some View { + Text(repositoryForgeLabel(service)) + .font(.caption2.weight(.medium)) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(color.opacity(0.15), in: Capsule()) + .foregroundStyle(color) + } + + private var color: Color { + switch service { + case .git: + .indigo + case .hg: + .cyan + default: + .secondary + } + } +} |
