262 lines
12 KiB
Swift
262 lines
12 KiB
Swift
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(Color.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 ? Color.accentColor : .secondary)
|
|
Text(branch.name).font(.system(.body, design: .monospaced))
|
|
.foregroundStyle(branch.name == gitStore.currentBranch ? Color.accentColor : .primary)
|
|
.bold(branch.name == gitStore.currentBranch)
|
|
Spacer()
|
|
if branch.name == gitStore.currentBranch { Text("current").font(.caption2).foregroundStyle(Color.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])
|
|
}
|
|
}
|