Sources/OrgApp/GitBranch.swift
41 lines · 1998 bytes
1import Foundation
2
3/// The checked-out branch of the repository holding a file, read from `HEAD` without running
4/// git.
5public enum GitBranch {
6 /// The branch name, the first 7 characters of the commit when HEAD is detached, or nil
7 /// outside a repository.
8 public static func current(for path: String) -> String? {
9 var directory = URL(fileURLWithPath: path).deletingLastPathComponent()
10 while true {
11 let dotGit = directory.appendingPathComponent(".git")
12 if let head = head(dotGit) { return parse(head) }
13 let parent = directory.deletingLastPathComponent()
14 if parent.path == directory.path { return nil }
15 directory = parent
16 }
17 }
18
19 /// `HEAD` of a `.git` folder, or of the folder a `.git` file (worktree, submodule) names.
20 static func head(_ dotGit: URL) -> String? {
21 var isDirectory: ObjCBool = false
22 guard FileManager.default.fileExists(atPath: dotGit.path, isDirectory: &isDirectory) else { return nil }
23 var gitDir = dotGit
24 if !isDirectory.boolValue {
25 guard let link = try? String(contentsOf: dotGit, encoding: .utf8),
26 let line = link.split(separator: "\n").first, line.hasPrefix("gitdir: ") else { return nil }
27 let target = String(line.dropFirst("gitdir: ".count))
28 gitDir = target.hasPrefix("/") ? URL(fileURLWithPath: target) : dotGit.deletingLastPathComponent().appendingPathComponent(target)
29 }
30 return try? String(contentsOf: gitDir.appendingPathComponent("HEAD"), encoding: .utf8)
31 }
32
33 static func parse(_ head: String) -> String? {
34 let line = head.trimmingCharacters(in: .whitespacesAndNewlines)
35 if line.hasPrefix("ref: ") {
36 let ref = line.dropFirst("ref: ".count)
37 return ref.hasPrefix("refs/heads/") ? String(ref.dropFirst("refs/heads/".count)) : String(ref)
38 }
39 return line.isEmpty ? nil : String(line.prefix(7))
40 }
41}