diff --git a/CLI/cmux.swift b/CLI/cmux.swift index b14dad7c..c1c98599 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -4988,39 +4988,63 @@ struct CMUXCLI { private func versionSummary() -> String { let info = resolvedVersionInfo() + let commit = info["CMUXCommit"].flatMap { normalizedCommitHash($0) } + let baseSummary: String if let version = info["CFBundleShortVersionString"], let build = info["CFBundleVersion"] { - return "cmux \(version) (\(build))" + baseSummary = "cmux \(version) (\(build))" + } else if let version = info["CFBundleShortVersionString"] { + baseSummary = "cmux \(version)" + } else if let build = info["CFBundleVersion"] { + baseSummary = "cmux build \(build)" + } else { + baseSummary = "cmux version unknown" } - if let version = info["CFBundleShortVersionString"] { - return "cmux \(version)" - } - if let build = info["CFBundleVersion"] { - return "cmux build \(build)" - } - return "cmux version unknown" + guard let commit else { return baseSummary } + return "\(baseSummary) [\(commit)]" } private func resolvedVersionInfo() -> [String: String] { + var info: [String: String] = [:] if let main = versionInfo(from: Bundle.main.infoDictionary) { - return main + info.merge(main, uniquingKeysWith: { current, _ in current }) } - for plistURL in candidateInfoPlistURLs() { - guard let data = try? Data(contentsOf: plistURL), - let raw = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil), - let dictionary = raw as? [String: Any], - let parsed = versionInfo(from: dictionary) - else { - continue + let needsPlistFallback = + info["CFBundleShortVersionString"] == nil || + info["CFBundleVersion"] == nil || + info["CMUXCommit"] == nil + if needsPlistFallback { + for plistURL in candidateInfoPlistURLs() { + guard let data = try? Data(contentsOf: plistURL), + let raw = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil), + let dictionary = raw as? [String: Any], + let parsed = versionInfo(from: dictionary) + else { + continue + } + info.merge(parsed, uniquingKeysWith: { current, _ in current }) + if info["CFBundleShortVersionString"] != nil, + info["CFBundleVersion"] != nil, + info["CMUXCommit"] != nil { + break + } } - return parsed } - if let fromProject = versionInfoFromProjectFile() { - return fromProject + let needsProjectFallback = + info["CFBundleShortVersionString"] == nil || + info["CFBundleVersion"] == nil || + info["CMUXCommit"] == nil + if needsProjectFallback, let fromProject = versionInfoFromProjectFile() { + info.merge(fromProject, uniquingKeysWith: { current, _ in current }) } - return [:] + if info["CMUXCommit"] == nil, + let commit = normalizedCommitHash(ProcessInfo.processInfo.environment["CMUX_COMMIT"]) { + info["CMUXCommit"] = commit + } + + return info } private func versionInfo(from dictionary: [String: Any]?) -> [String: String]? { @@ -5039,6 +5063,10 @@ struct CMUXCLI { info["CFBundleVersion"] = trimmed } } + if let commit = dictionary["CMUXCommit"] as? String, + let normalizedCommit = normalizedCommitHash(commit) { + info["CMUXCommit"] = normalizedCommit + } return info.isEmpty ? nil : info } @@ -5064,6 +5092,9 @@ struct CMUXCLI { if let build = firstProjectSetting("CURRENT_PROJECT_VERSION", in: contents) { info["CFBundleVersion"] = build } + if let commit = gitCommitHash(at: current) { + info["CMUXCommit"] = commit + } if !info.isEmpty { return info } @@ -5100,6 +5131,45 @@ struct CMUXCLI { return value } + private func gitCommitHash(at directory: URL) -> String? { + let process = Process() + let stdout = Pipe() + process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.arguments = ["git", "-C", directory.path, "rev-parse", "--short=9", "HEAD"] + process.standardOutput = stdout + process.standardError = Pipe() + + do { + try process.run() + } catch { + return nil + } + process.waitUntilExit() + guard process.terminationStatus == 0 else { + return nil + } + + let data = stdout.fileHandleForReading.readDataToEndOfFile() + guard let output = String(data: data, encoding: .utf8) else { + return nil + } + return normalizedCommitHash(output) + } + + private func normalizedCommitHash(_ value: String?) -> String? { + guard let value else { return nil } + let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty, !trimmed.contains("$(") else { + return nil + } + let normalized = trimmed.lowercased() + let allowed = CharacterSet(charactersIn: "0123456789abcdef") + guard normalized.unicodeScalars.allSatisfy({ allowed.contains($0) }) else { + return nil + } + return String(normalized.prefix(12)) + } + private func candidateInfoPlistURLs() -> [URL] { guard let executable = currentExecutablePath(), !executable.isEmpty else { return [] diff --git a/tests/test_cli_version_commit_metadata.py b/tests/test_cli_version_commit_metadata.py new file mode 100644 index 00000000..3029fe0d --- /dev/null +++ b/tests/test_cli_version_commit_metadata.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Regression test: CLI version output wiring keeps commit metadata support.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + + +def get_repo_root() -> Path: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + return Path(result.stdout.strip()) + return Path.cwd() + + +def require(content: str, needle: str, message: str, failures: list[str]) -> None: + if needle not in content: + failures.append(message) + + +def main() -> int: + repo_root = get_repo_root() + cli_path = repo_root / "CLI" / "cmux.swift" + if not cli_path.exists(): + print(f"FAIL: missing expected file: {cli_path}") + return 1 + + content = cli_path.read_text(encoding="utf-8") + failures: list[str] = [] + + require( + content, + 'let commit = info["CMUXCommit"].flatMap { normalizedCommitHash($0) }', + "versionSummary no longer reads CMUXCommit metadata", + failures, + ) + require( + content, + 'return "\\(baseSummary) [\\(commit)]"', + "versionSummary no longer appends commit metadata", + failures, + ) + require( + content, + 'if let commit = dictionary["CMUXCommit"] as? String,', + "Info.plist parsing no longer reads CMUXCommit", + failures, + ) + require( + content, + "if let commit = gitCommitHash(at: current) {", + "Project fallback no longer probes git commit hash", + failures, + ) + require( + content, + '["git", "-C", directory.path, "rev-parse", "--short=9", "HEAD"]', + "Git commit probe command changed unexpectedly", + failures, + ) + require( + content, + 'normalizedCommitHash(ProcessInfo.processInfo.environment["CMUX_COMMIT"])', + "Environment commit fallback (CMUX_COMMIT) is missing", + failures, + ) + + if failures: + print("FAIL: CLI version commit metadata regression(s) detected") + for failure in failures: + print(f"- {failure}") + return 1 + + print("PASS: CLI version commit metadata wiring is intact") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())