diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 14106a264..6034fa8af 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "3f6921a5ec30d1ecb6d6b205cf27a816c318246bb00f0ea367b997cc66527d32", "pins" : [ { "identity" : "anycodable", diff --git a/CodeEdit/Features/Contributors/ContributorRowView.swift b/CodeEdit/Features/Contributors/ContributorRowView.swift index 85c989439..10385dde5 100644 --- a/CodeEdit/Features/Contributors/ContributorRowView.swift +++ b/CodeEdit/Features/Contributors/ContributorRowView.swift @@ -44,10 +44,12 @@ struct ContributorRowView: View { .resizable() .frame(width: 32, height: 32) .clipShape(Circle()) + .help(contributor.name) } placeholder: { Image(systemName: "person.circle.fill") .resizable() .frame(width: 32, height: 32) + .help(contributor.name) } } diff --git a/CodeEdit/Features/Git/Client/GitClient+CommitHistory.swift b/CodeEdit/Features/Git/Client/GitClient+CommitHistory.swift index 3d8681b58..a9c5c60bd 100644 --- a/CodeEdit/Features/Git/Client/GitClient+CommitHistory.swift +++ b/CodeEdit/Features/Git/Client/GitClient+CommitHistory.swift @@ -31,21 +31,34 @@ extension GitClient { dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z" let output = try await run( - "log --pretty=%h¦%H¦%s¦%aN¦%ae¦%cn¦%ce¦%aD¦ \(maxCountString) \(branchNameString) \(fileLocalPath)" + "log --pretty=%h¦%H¦%s¦%aN¦%ae¦%cn¦%ce¦%aD¦%b¦%D¦ \(maxCountString) \(branchNameString) \(fileLocalPath)" .trimmingCharacters(in: .whitespacesAndNewlines) ) - - let remote = try? await run("ls-remote --get-url") - let remoteURL: URL? = if let remote { - URL(string: remote.trimmingCharacters(in: .whitespacesAndNewlines)) - } else { - nil - } + let remote = try await run("ls-remote --get-url") + let remoteURL = URL(string: remote.trimmingCharacters(in: .whitespacesAndNewlines)) return output .split(separator: "\n") .map { line -> GitCommit in - let parameters = line.components(separatedBy: "¦") + let parameters = String(line).components(separatedBy: "¦") + let infoRef = parameters[safe: 9] + var refs: [String] = [] + var tag = "" + if let infoRef = infoRef { + if infoRef.contains("tag:") { + tag = infoRef.components(separatedBy: "tag:")[1].trimmingCharacters(in: .whitespaces) + } else { + refs = infoRef.split(separator: ",").compactMap { + var element = String($0) + if element.contains("origin/HEAD") { return nil } + if element.contains("HEAD -> ") { + element = element.replacingOccurrences(of: "HEAD -> ", with: "") + } + return element.trimmingCharacters(in: .whitespaces) + } + } + } + return GitCommit( hash: parameters[safe: 0] ?? "", commitHash: parameters[safe: 1] ?? "", @@ -54,6 +67,9 @@ extension GitClient { authorEmail: parameters[safe: 4] ?? "", committer: parameters[safe: 5] ?? "", committerEmail: parameters[safe: 6] ?? "", + body: parameters[safe: 8] ?? "", + refs: refs, + tag: tag, remoteURL: remoteURL, date: dateFormatter.date(from: parameters[safe: 7] ?? "") ?? Date() ) diff --git a/CodeEdit/Features/Git/Client/Models/GitCommit.swift b/CodeEdit/Features/Git/Client/Models/GitCommit.swift index 65b376fe6..b0195b9f3 100644 --- a/CodeEdit/Features/Git/Client/Models/GitCommit.swift +++ b/CodeEdit/Features/Git/Client/Models/GitCommit.swift @@ -17,6 +17,9 @@ struct GitCommit: Equatable, Hashable, Identifiable { let authorEmail: String let committer: String let committerEmail: String + let body: String + let refs: [String] + let tag: String let remoteURL: URL? let date: Date diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift index fa5b99c33..faa073143 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift @@ -25,7 +25,7 @@ struct HistoryInspectorItemView: View { } var body: some View { - CommitListItemView(commit: commit) + CommitListItemView(commit: commit, showRef: false) .popover(isPresented: showPopup, arrowEdge: .leading) { HistoryPopoverView(commit: commit) } diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift index 17a271277..c5993fa51 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift @@ -70,10 +70,13 @@ struct CommitDetailsHeaderView: View { .resizable() .clipShape(Circle()) .frame(width: 32, height: 32) + .help(commit.author) } else if phase.error != nil { defaultAvatar + .help(commit.author) } else { defaultAvatar + .help(commit.author) } } @@ -98,8 +101,20 @@ struct CommitDetailsHeaderView: View { ) .padding(.horizontal, 2.5) } + .padding(.horizontal, 16) + + Divider() + Text(commitDetails()) + .fontWeight(.bold) + .padding(.horizontal, 16) .frame(alignment: .leading) + + if !commit.body.isEmpty { + Text(commit.body) + .padding(.horizontal, 16) + .frame(alignment: .leading) + } } } } diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift index 6043efe06..b64b81003 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift @@ -41,7 +41,6 @@ struct CommitDetailsView: View { if let commit = commit { CommitDetailsHeaderView(commit: commit) - .padding(.horizontal, 16) .padding(.vertical, 16) Divider() diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift index 8a19f67e2..3c6810ba6 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift @@ -10,21 +10,126 @@ import SwiftUI struct CommitListItemView: View { var commit: GitCommit + var showRef: Bool + var width: CGFloat + + private var defaultAvatar: some View { + Image(systemName: "person.crop.circle.fill") + .symbolRenderingMode(.hierarchical) + .resizable() + .foregroundColor(avatarColor) + .frame(width: 32, height: 32) + } + + private func generateAvatarHash() -> String { + let hash = commit.authorEmail.md5(trim: true, caseSensitive: false) + return "\(hash)?d=404&s=64" // send 404 if no image available, image size 64x64 (32x32 @2x) + } + + private var avatarColor: Color { + let hash = generateAvatarHash().hash + switch hash % 12 { + case 0: return .red + case 1: return .orange + case 2: return .yellow + case 3: return .green + case 4: return .mint + case 5: return .teal + case 6: return .cyan + case 7: return .blue + case 8: return .indigo + case 9: return .purple + case 10: return .brown + case 11: return .pink + default: return .teal + } + } @Environment(\.openURL) private var openCommit - init(commit: GitCommit) { + init(commit: GitCommit, showRef: Bool) { + self.commit = commit + self.showRef = showRef + self.width = 0 + } + + init(commit: GitCommit, showRef: Bool, width: CGFloat) { self.commit = commit + self.showRef = showRef + self.width = width } var body: some View { HStack(alignment: .top) { + if width > 360 { + AsyncImage(url: URL(string: "https://www.gravatar.com/avatar/\(generateAvatarHash())")) { phase in + if let image = phase.image { + image + .resizable() + .clipShape(Circle()) + .frame(width: 32, height: 32) + .help(commit.author) + } else if phase.error != nil { + defaultAvatar + .help(commit.author) + } else { + defaultAvatar + .help(commit.author) + } + } + } VStack(alignment: .leading, spacing: 0) { - Text(commit.author) - .fontWeight(.bold) - .font(.system(size: 11)) - Text(commit.message) + HStack { + Text(commit.author) + .fontWeight(.bold) + .font(.system(size: 11)) + if showRef { + if !commit.refs.isEmpty { + HStack { + ForEach(commit.refs, id: \.self) { ref in + HStack { + Image.branch + .imageScale(.small) + .foregroundColor(.primary) + .help(ref) + Text(ref) + .font(.system(size: 10, design: .monospaced)) + } + .frame(height: 13) + .background( + RoundedRectangle(cornerRadius: 3) + .padding(.vertical, -1) + .padding(.horizontal, -2.5) + .foregroundColor(Color(nsColor: .quaternaryLabelColor)) + ) + .padding(.trailing, 2.5) + } + } + } + + if !commit.tag.isEmpty { + HStack { + Image.breakpoint + .imageScale(.small) + .foregroundColor(.primary) + .help(commit.tag) + Text(commit.tag) + .font(.system(size: 10, design: .monospaced)) + } + .frame(height: 13) + .background( + RoundedRectangle(cornerRadius: 3) + .padding(.vertical, -1) + .padding(.horizontal, -2.5) + .foregroundColor(Color(nsColor: .selectedContentBackgroundColor)) + ) + .padding(.trailing, 2.5) + } + } + } + + Text("\(commit.message) \(commit.body)") .font(.system(size: 11)) .lineLimit(2) } diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift index 2b54d39ce..c0fac857a 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift @@ -21,6 +21,7 @@ struct SourceControlNavigatorHistoryView: View { @State var commitHistory: [GitCommit] = [] @State var selection: GitCommit? + @State private var width: CGFloat = CGFloat.zero func updateCommitHistory() async { do { @@ -55,17 +56,25 @@ struct SourceControlNavigatorHistoryView: View { if commitHistory.isEmpty { CEContentUnavailableView("No History") } else { - ZStack { - List(selection: $selection) { - ForEach(commitHistory) { commit in - CommitListItemView(commit: commit) - .tag(commit) - .listRowSeparator(.hidden) + GeometryReader { geometry in + ZStack { + List(selection: $selection) { + ForEach(commitHistory) { commit in + CommitListItemView(commit: commit, showRef: true, width: width) + .tag(commit) + .listRowSeparator(.hidden) + } } + .opacity(selection == nil ? 1 : 0) + if selection != nil { + CommitDetailsView(commit: $selection) + } + } + .onAppear { + self.width = geometry.size.width } - .opacity(selection == nil ? 1 : 0) - if selection != nil { - CommitDetailsView(commit: $selection) + .onChange(of: geometry.size.width) { newWidth in + self.width = newWidth } } } @@ -89,11 +98,6 @@ struct SourceControlNavigatorHistoryView: View { } } } - .onReceive(sourceControlManager.$currentBranch) { _ in - Task { - await updateCommitHistory() - } - } .task { await updateCommitHistory() }