A macOS utility to track home-manager JJ repo status
at main 140 lines 4.6 kB view raw
1import Foundation 2 3/// Represents the current status of the jj repository relative to trunk(). 4public struct RepoStatus: Equatable { 5 /// Number of commits on the current working copy ancestry that are not on trunk(). 6 public let ahead: Int 7 /// Number of commits on trunk() that are not on the current working copy ancestry. 8 public let behind: Int 9 /// Whether the working copy has uncommitted changes (non-empty). 10 public let dirty: Bool 11 /// If non-nil, an error occurred while checking status. 12 public let error: String? 13 /// First line of each commit description for commits ahead of trunk (newest first). 14 public let aheadCommits: [String] 15 /// First line of each commit description for commits behind trunk (newest first). 16 public let behindCommits: [String] 17 18 /// The last time this status was successfully checked. 19 public let checkedAt: Date 20 21 /// Maximum number of commit descriptions to display in the dropdown. 22 public static let maxDisplayedCommits = 5 23 /// Maximum character width for a single commit description line. 24 public static let maxDescriptionLength = 72 25 26 public init( 27 ahead: Int, behind: Int, dirty: Bool, error: String?, 28 aheadCommits: [String] = [], behindCommits: [String] = [], 29 checkedAt: Date 30 ) { 31 self.ahead = ahead 32 self.behind = behind 33 self.dirty = dirty 34 self.error = error 35 self.aheadCommits = aheadCommits 36 self.behindCommits = behindCommits 37 self.checkedAt = checkedAt 38 } 39 40 // MARK: - Convenience initializers 41 42 public static func synced(dirty: Bool = false, at date: Date = Date()) -> RepoStatus { 43 RepoStatus(ahead: 0, behind: 0, dirty: dirty, error: nil, checkedAt: date) 44 } 45 46 public static func error(_ message: String, at date: Date = Date()) -> RepoStatus { 47 RepoStatus(ahead: 0, behind: 0, dirty: false, error: message, checkedAt: date) 48 } 49 50 // MARK: - Computed properties 51 52 public var isSynced: Bool { ahead == 0 && behind == 0 && error == nil } 53 public var isAhead: Bool { ahead > 0 && error == nil } 54 public var isBehind: Bool { behind > 0 && error == nil } 55 public var isDiverged: Bool { ahead > 0 && behind > 0 && error == nil } 56 public var hasError: Bool { error != nil } 57 58 /// Compact text shown in the menu bar. 59 public var menuBarText: String { 60 if hasError { return "!" } 61 if isDiverged { return "\u{2191}\(ahead)\u{2193}\(behind)" } 62 if isAhead { return "\u{2191}\(ahead)" } 63 if isBehind { return "\u{2193}\(behind)" } 64 return "" 65 } 66 67 /// Full text for the menu bar label. 68 public var menuBarLabel: String { 69 var label = menuBarText 70 if dirty && !hasError { 71 label += "\u{25CF}" // dot 72 } 73 if label.isEmpty { 74 return "\u{2713}" // checkmark 75 } 76 return label 77 } 78 79 /// Human-readable status description for the dropdown. 80 public var statusDescription: String { 81 if hasError { 82 return "Error: \(error!)" 83 } 84 85 var parts: [String] = [] 86 87 if isSynced { 88 parts.append("In sync with trunk") 89 } else { 90 if ahead > 0 { 91 parts.append("\(ahead) commit\(ahead == 1 ? "" : "s") ahead of trunk") 92 parts.append( 93 contentsOf: Self.formatCommitList(aheadCommits, total: ahead)) 94 } 95 if behind > 0 { 96 parts.append("\(behind) commit\(behind == 1 ? "" : "s") behind trunk") 97 parts.append( 98 contentsOf: Self.formatCommitList(behindCommits, total: behind)) 99 } 100 } 101 102 if dirty { 103 parts.append("Working copy has changes") 104 } else if !hasError { 105 parts.append("Working copy is clean") 106 } 107 108 return parts.joined(separator: "\n") 109 } 110 111 // MARK: - Commit list formatting 112 113 /// Format a list of commit descriptions for display, with truncation. 114 static func formatCommitList(_ commits: [String], total: Int) -> [String] { 115 let displayCount = min(commits.count, maxDisplayedCommits) 116 var lines: [String] = [] 117 118 for i in 0..<displayCount { 119 let desc = commits[i] 120 let label = desc.isEmpty ? "(no description)" : truncate(desc, to: maxDescriptionLength) 121 lines.append(" \u{00B7} \(label)") 122 } 123 124 let remaining = total - displayCount 125 if remaining > 0 { 126 lines.append(" \u{00B7} ... and \(remaining) more") 127 } 128 129 return lines 130 } 131 132 /// Truncate a string to a maximum length, appending "" if needed. 133 static func truncate(_ string: String, to maxLength: Int) -> String { 134 if string.count <= maxLength { 135 return string 136 } 137 let endIndex = string.index(string.startIndex, offsetBy: maxLength - 1) 138 return string[string.startIndex..<endIndex] + "\u{2026}" 139 } 140}