import SwiftUI import UIKit struct GitPanel: View { @ObservedObject var gitStore: GitStore let gitService: GitService @State private var tab: PanelTab = .changes @State private var showCommit = false @State private var showBranchPicker = false @State private var showNewBranch = false @State private var commitMessage = "" @State private var newBranchName = "" enum PanelTab: String, CaseIterable { case changes = "Changes"; case history = "History"; case branches = "Branches" var icon: String { switch self { case .changes: return "square.and.pencil"; case .history: return "clock"; case .branches: return "arrow.triangle.branch" } } } var body: some View { VStack(spacing: 0) { // Header HStack(spacing: 8) { Menu { ForEach(gitStore.repositories) { repo in Button(repo.name) { gitStore.activeRepo = repo.name gitService.fetchStatus(repo: repo.name) gitService.fetchBranches(repo: repo.name) } } Divider() Button { gitService.listRepositories() } label: { Label("Refresh Repos", systemImage: "arrow.clockwise") } } label: { HStack(spacing: 4) { Image(systemName: "externaldrive.fill").font(.caption) Text(gitStore.activeRepo.isEmpty ? "Select Repo" : gitStore.activeRepo) .font(.system(.caption, design: .monospaced)).lineLimit(1) Image(systemName: "chevron.down").font(.caption2) } .foregroundStyle(.primary) } Spacer() Button { showBranchPicker = true; gitService.fetchBranches(repo: gitStore.activeRepo) } label: { HStack(spacing: 3) { Image(systemName: "arrow.triangle.branch").font(.caption) Text(gitStore.currentBranch).font(.system(.caption, design: .monospaced)).lineLimit(1) } .padding(.horizontal, 6).padding(.vertical, 3) .background(.quaternary, in: RoundedRectangle(cornerRadius: 4)) } .disabled(gitStore.activeRepo.isEmpty) Button { gitService.pull(repo: gitStore.activeRepo) } label: { Image(systemName: "arrow.triangle.2.circlepath").font(.caption) } .disabled(gitStore.activeRepo.isEmpty) } .padding(.horizontal, 10).padding(.vertical, 8) Divider() Picker("", selection: $tab) { ForEach(PanelTab.allCases, id: \.self) { t in Label(t.rawValue, systemImage: t.icon).tag(t) } } .pickerStyle(.segmented).padding(.horizontal, 8).padding(.vertical, 6) Divider() switch tab { case .changes: changesTab case .history: historyTab case .branches: branchesTab } } .sheet(isPresented: $showCommit) { commitSheet } .sheet(isPresented: $showBranchPicker) { NavigationStack { List(gitStore.branches) { branch in Button { showBranchPicker = false gitService.checkout(repo: gitStore.activeRepo, branch: branch.isRemote ? branch.localName : branch.name) } label: { HStack { Image(systemName: branch.isRemote ? "cloud" : "arrow.triangle.branch").font(.caption).foregroundStyle(.secondary) Text(branch.name).font(.system(.body, design: .monospaced)) Spacer() if branch.name == gitStore.currentBranch { Image(systemName: "checkmark").foregroundStyle(.accentColor) } } }.buttonStyle(.plain) } .navigationTitle("Switch Branch").navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { showBranchPicker = false } } } } .presentationDetents([.medium]) } .onReceive(NotificationCenter.default.publisher(for: .gitCheckoutCompleted)) { _ in guard !gitStore.activeRepo.isEmpty else { return } gitService.fetchBranches(repo: gitStore.activeRepo) gitService.fetchStatus(repo: gitStore.activeRepo) } .onReceive(NotificationCenter.default.publisher(for: .gitCommitCompleted)) { _ in guard !gitStore.activeRepo.isEmpty else { return } gitService.fetchStatus(repo: gitStore.activeRepo) } } // MARK: - Changes Tab private var changesTab: some View { VStack(spacing: 0) { if gitStore.fileStatuses.isEmpty { ContentUnavailableView("No Changes", systemImage: "checkmark.circle", description: Text("Working tree is clean.")) } else { List(gitStore.fileStatuses) { file in HStack(spacing: 8) { Image(systemName: file.statusIcon).font(.caption).foregroundStyle(file.statusSwiftUIColor).frame(width: 16) VStack(alignment: .leading, spacing: 1) { Text(file.name).font(.system(.body, design: .monospaced)) Text(file.path).font(.caption2).foregroundStyle(.secondary).lineLimit(1) } Spacer() Text({ switch file.status { case "modified": return "M"; case "added": return "A"; case "deleted": return "D"; default: return "?" } }()) .font(.system(.caption2, design: .monospaced).bold()) .foregroundStyle(file.statusSwiftUIColor) } } .listStyle(.plain) } Divider() HStack { Button { gitService.fetchStatus(repo: gitStore.activeRepo) } label: { Image(systemName: "arrow.clockwise") }.buttonStyle(.plain) Spacer() Text("\(gitStore.fileStatuses.count) changed").font(.caption2).foregroundStyle(.secondary) Spacer() Button("Commit…") { showCommit = true } .buttonStyle(.borderedProminent).controlSize(.small) .disabled(gitStore.fileStatuses.isEmpty || gitStore.activeRepo.isEmpty) } .padding(.horizontal, 10).padding(.vertical, 8) } } // MARK: - History Tab private var historyTab: some View { Group { if gitStore.commitLog.isEmpty { ContentUnavailableView("No History", systemImage: "clock", description: Text("Tap refresh to load commits.")) .onAppear { if !gitStore.activeRepo.isEmpty { gitService.fetchLog(repo: gitStore.activeRepo) } } } else { List(gitStore.commitLog) { commit in VStack(alignment: .leading, spacing: 3) { Text(commit.summary).font(.subheadline).lineLimit(2) HStack(spacing: 6) { Text(commit.shortId).font(.system(.caption, design: .monospaced)).foregroundStyle(.secondary) .padding(.horizontal, 4).padding(.vertical, 1) .background(.quaternary, in: RoundedRectangle(cornerRadius: 3)) Text(commit.author.components(separatedBy: "<").first?.trimmingCharacters(in: .whitespaces) ?? commit.author) .font(.caption).foregroundStyle(.secondary) Spacer() Text(commit.formattedDate).font(.caption2).foregroundStyle(.tertiary) } } .padding(.vertical, 2) } .listStyle(.plain) } } } // MARK: - Branches Tab private var branchesTab: some View { List { Section("Local") { ForEach(gitStore.branches.filter { !$0.isRemote }) { b in branchRow(b) } Button { showNewBranch = true } label: { Label("New Branch…", systemImage: "plus").font(.subheadline) } } let remotes = gitStore.branches.filter { $0.isRemote } if !remotes.isEmpty { Section("Remote") { ForEach(remotes) { b in branchRow(b) } } } } .listStyle(.plain) .sheet(isPresented: $showNewBranch) { NavigationStack { Form { Section("Branch Name") { TextField("feature/my-feature", text: $newBranchName) .autocorrectionDisabled().autocapitalization(.none) } } .navigationTitle("New Branch").navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { showNewBranch = false; newBranchName = "" } } ToolbarItem(placement: .confirmationAction) { Button("Create") { gitService.createBranch(repo: gitStore.activeRepo, branch: newBranchName) showNewBranch = false; newBranchName = "" } .disabled(newBranchName.trimmingCharacters(in: .whitespaces).isEmpty) } } } .presentationDetents([.height(220)]) } } private func branchRow(_ branch: GitBranch) -> some View { Button { gitService.checkout(repo: gitStore.activeRepo, branch: branch.isRemote ? branch.localName : branch.name) } label: { HStack { Image(systemName: branch.isRemote ? "cloud" : "arrow.triangle.branch") .font(.caption).foregroundStyle(branch.name == gitStore.currentBranch ? .accentColor : .secondary) Text(branch.name).font(.system(.body, design: .monospaced)) .foregroundStyle(branch.name == gitStore.currentBranch ? .accentColor : .primary) .bold(branch.name == gitStore.currentBranch) Spacer() if branch.name == gitStore.currentBranch { Text("current").font(.caption2).foregroundStyle(.accentColor) } } } .buttonStyle(.plain) } // MARK: - Commit Sheet private var commitSheet: some View { NavigationStack { Form { Section("Changed Files (\(gitStore.fileStatuses.count))") { ForEach(gitStore.fileStatuses) { f in HStack(spacing: 6) { Image(systemName: f.statusIcon).font(.caption).foregroundStyle(f.statusSwiftUIColor).frame(width: 16) Text(f.name).font(.system(.caption, design: .monospaced)) } } } Section("Commit Message") { TextEditor(text: $commitMessage) .font(.system(.body, design: .monospaced)).frame(minHeight: 80) .autocorrectionDisabled().autocapitalization(.sentences) } Section { Button("Commit & Push") { gitService.commitAndPush(repo: gitStore.activeRepo, message: commitMessage) commitMessage = ""; showCommit = false } .disabled(commitMessage.trimmingCharacters(in: .whitespaces).isEmpty) Button("Commit Only") { gitService.commit(repo: gitStore.activeRepo, message: commitMessage) commitMessage = ""; showCommit = false } .disabled(commitMessage.trimmingCharacters(in: .whitespaces).isEmpty) } } .navigationTitle("Commit Changes").navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { showCommit = false } } } } .presentationDetents([.medium, .large]) } }