Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ Frameworks/
GHOSTTY_*.md
TERMINAL_INTEGRATION_STEPS.md
build/

# License configuration (contains device-specific Pro activation)
GitMac/Services/LicenseValidator.swift
45 changes: 36 additions & 9 deletions GitMac.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

1,584 changes: 0 additions & 1,584 deletions GitMac.xcodeproj/project.pbxproj.backup

This file was deleted.

3,191 changes: 2,323 additions & 868 deletions GitMac.xcodeproj/project.pbxproj.backup2

Large diffs are not rendered by default.

66 changes: 62 additions & 4 deletions GitMac/App/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ struct ContentView: View {
@State private var showOpenPanel = false
@State private var showNewBranchSheet = false
@State private var showMergeSheet = false
@State private var leftPanelWidth: CGFloat = 260
@State private var rightPanelWidth: CGFloat = 380
@State private var leftPanelWidth: CGFloat = DesignTokens.Layout.Sidebar.defaultWidth
@State private var rightPanelWidth: CGFloat = DesignTokens.Layout.StagingPanel.defaultWidth
@State private var showRevertSheet = false
@State private var revertCommits: [Commit] = []
@State private var showCherryPickSheet = false
@State private var cherryPickCommits: [Commit] = []
@State private var showDetachedHeadAlert = false
@State private var themeRefreshTrigger = UUID()
@State private var createBranchFromCommitSHA: String?
@AppStorage("toolbarDisplayMode") private var toolbarDisplayMode: ToolbarDisplayMode = .iconAndText

enum ToolbarDisplayMode: String, CaseIterable {
Expand Down Expand Up @@ -58,17 +61,32 @@ struct ContentView: View {
}
}
.sheet(isPresented: $showNewBranchSheet) {
CreateBranchSheet(isPresented: $showNewBranchSheet)
.environmentObject(appState)
CreateBranchSheet(
isPresented: $showNewBranchSheet,
fromCommitSHA: createBranchFromCommitSHA
)
.environmentObject(appState)
.onDisappear {
// Clear the commit SHA when sheet is dismissed
createBranchFromCommitSHA = nil
}
}
.sheet(isPresented: $showRevertSheet) {
RevertView(targetCommits: revertCommits)
.environmentObject(appState)
}
.sheet(isPresented: $showCherryPickSheet) {
CherryPickView(commits: cherryPickCommits)
.environmentObject(appState)
}
.sheet(isPresented: $showMergeSheet) {
MergeBranchSheet(isPresented: $showMergeSheet)
.environmentObject(appState)
}
.overlay(alignment: .bottomTrailing) {
// Git operation progress overlay
GitOperationProgressView()
}
.overlay {
if gitOperationHandler.isOperationInProgress {
OperationProgressOverlay(message: gitOperationHandler.operationMessage)
Expand Down Expand Up @@ -124,10 +142,50 @@ struct ContentView: View {
.onReceive(NotificationCenter.default.publisher(for: .openRepository)) { _ in showOpenPanel = true }
.onReceive(NotificationCenter.default.publisher(for: .cloneRepository)) { _ in showCloneSheet = true }
.onReceive(NotificationCenter.default.publisher(for: .newBranch)) { _ in showNewBranchSheet = true }
.onReceive(NotificationCenter.default.publisher(for: .createBranchFromCommit)) { notification in
handleCreateBranchFromCommit(notification)
}
.onReceive(NotificationCenter.default.publisher(for: .cherryPickCommit)) { notification in
handleCherryPickNotification(notification)
}
.onReceive(NotificationCenter.default.publisher(for: .revertCommit)) { notification in
handleRevertNotification(notification)
}
.modifier(GitOperationListeners())
.modifier(NavigationListeners(columnVisibility: $columnVisibility, selectedFileDiff: $selectedFileDiff))
}

private func handleCherryPickNotification(_ notification: Notification) {
// Handle single commit SHA
if let sha = notification.object as? String {
// Find the commit in the repository
if let repo = appState.currentRepository,
let commit = repo.commits.first(where: { $0.sha == sha }) {
cherryPickCommits = [commit]
showCherryPickSheet = true
}
}
// Handle multiple commits
else if let commits = notification.object as? [Commit] {
cherryPickCommits = commits
showCherryPickSheet = true
}
}

private func handleRevertNotification(_ notification: Notification) {
if let commits = notification.object as? [Commit] {
revertCommits = commits
showRevertSheet = true
}
}

private func handleCreateBranchFromCommit(_ notification: Notification) {
if let commitSHA = notification.object as? String {
createBranchFromCommitSHA = commitSHA
showNewBranchSheet = true
}
}

struct GitOperationListeners: ViewModifier {
@EnvironmentObject var appState: AppState
func body(content: Content) -> some View {
Expand Down
71 changes: 57 additions & 14 deletions GitMac/App/Sheets/CreateBranchSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ struct CreateBranchSheet: View {
@EnvironmentObject var appState: AppState
@Binding var isPresented: Bool

// Optional commit SHA to create branch from
var fromCommitSHA: String? = nil

@State private var branchName = ""
@State private var baseBranch = "HEAD"
@State private var checkoutAfterCreate = true
Expand Down Expand Up @@ -61,14 +64,28 @@ struct CreateBranchSheet: View {
.foregroundColor(AppTheme.textPrimary)
}

// Current branch
HStack(spacing: 4) {
Image(systemName: "arrow.triangle.branch")
.font(.system(size: 10))
.foregroundColor(AppTheme.success)
Text(currentBranchName)
.font(.system(size: 11, weight: .medium))
.foregroundColor(AppTheme.textPrimary)
// Current branch or commit SHA
if let commitSHA = fromCommitSHA {
HStack(spacing: 4) {
Image(systemName: "arrow.turn.down.right")
.font(.system(size: 10))
.foregroundColor(AppTheme.warning)
Text("from commit")
.font(.system(size: 11))
.foregroundColor(AppTheme.textSecondary)
Text(String(commitSHA.prefix(7)))
.font(.system(size: 11, design: .monospaced))
.foregroundColor(AppTheme.textPrimary)
}
} else {
HStack(spacing: 4) {
Image(systemName: "arrow.triangle.branch")
.font(.system(size: 10))
.foregroundColor(AppTheme.success)
Text(currentBranchName)
.font(.system(size: 11, weight: .medium))
.foregroundColor(AppTheme.textPrimary)
}
}

Spacer()
Expand Down Expand Up @@ -104,14 +121,34 @@ struct CreateBranchSheet: View {
.font(.system(size: 11, weight: .medium))
.foregroundColor(AppTheme.textSecondary)

Picker("", selection: $baseBranch) {
Text("Current HEAD").tag("HEAD")
ForEach(localBranches) { branch in
Text(branch.name).tag(branch.name)
if fromCommitSHA != nil {
// Show locked display when creating from specific commit
HStack {
Image(systemName: "lock.fill")
.font(.system(size: 10))
.foregroundColor(AppTheme.textMuted)
Text(baseBranch)
.font(.system(size: 13))
.foregroundColor(AppTheme.textSecondary)
Spacer()
}
.padding(8)
.background(AppTheme.backgroundTertiary.opacity(0.5))
.cornerRadius(6)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(AppTheme.border, lineWidth: 1)
)
} else {
Picker("", selection: $baseBranch) {
Text("Current HEAD").tag("HEAD")
ForEach(localBranches) { branch in
Text(branch.name).tag(branch.name)
}
}
.pickerStyle(.menu)
.labelsHidden()
}
.pickerStyle(.menu)
.labelsHidden()
}

// Checkout toggle
Expand Down Expand Up @@ -163,6 +200,12 @@ struct CreateBranchSheet: View {
}
.frame(width: 380, height: 350)
.background(AppTheme.backgroundSecondary)
.onAppear {
// If creating from a specific commit, set the base branch to that commit SHA
if let commitSHA = fromCommitSHA {
baseBranch = commitSHA
}
}
}

private func createBranch() {
Expand Down
23 changes: 18 additions & 5 deletions GitMac/Core/Git/Branch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import Foundation

/// Represents a Git branch
struct Branch: Identifiable, Equatable, Hashable {
let id: UUID
/// Stable ID includes isHead so SwiftUI detects checkout changes
var id: String { "\(fullName)-\(isHead)" }

let name: String
let fullName: String
let isRemote: Bool
Expand All @@ -22,7 +24,6 @@ struct Branch: Identifiable, Equatable, Hashable {
targetSHA: String,
upstream: UpstreamInfo? = nil
) {
self.id = UUID()
self.name = name
self.fullName = fullName
self.isRemote = isRemote
Expand All @@ -44,9 +45,21 @@ struct Branch: Identifiable, Equatable, Hashable {
return name
}

/// Check if this is the main/default branch
/// Note: This requires the repository context to be set
/// Use isMainBranch(in:) to check against a specific repository
var isMainBranch: Bool {
let mainNames = ["main", "master", "develop", "development"]
return mainNames.contains(name.lowercased())
// Fallback when no repository context is available
let commonMainNames = ["main", "master"]
return commonMainNames.contains(name.lowercased())
}

/// Check if this is the main/default branch for a specific repository
func isMainBranch(in repository: Repository?) -> Bool {
guard let repo = repository, let defaultBranch = repo.defaultBranch else {
return isMainBranch
}
return name.lowercased() == defaultBranch.lowercased()
}

/// Alias for isHead - whether this is the current checked out branch
Expand All @@ -61,7 +74,7 @@ struct Branch: Identifiable, Equatable, Hashable {
}

static func == (lhs: Branch, rhs: Branch) -> Bool {
lhs.fullName == rhs.fullName
lhs.fullName == rhs.fullName && lhs.isHead == rhs.isHead
}

func hash(into hasher: inout Hasher) {
Expand Down
23 changes: 22 additions & 1 deletion GitMac/Core/Git/Commit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,32 @@ struct Commit: Identifiable, Equatable, Hashable {
return formatter.string(from: authorDate)
}

/// Summary cleaned of markdown code block syntax (backticks)
var cleanSummary: String {
var cleaned = summary.trimmingCharacters(in: .whitespaces)
// Remove leading/trailing triple backticks
while cleaned.hasPrefix("```") {
cleaned = String(cleaned.dropFirst(3)).trimmingCharacters(in: .whitespaces)
}
while cleaned.hasSuffix("```") {
cleaned = String(cleaned.dropLast(3)).trimmingCharacters(in: .whitespaces)
}
// Remove single backticks at start/end
while cleaned.hasPrefix("`") && !cleaned.hasPrefix("``") {
cleaned = String(cleaned.dropFirst(1)).trimmingCharacters(in: .whitespaces)
}
while cleaned.hasSuffix("`") && !cleaned.hasSuffix("``") {
cleaned = String(cleaned.dropLast(1)).trimmingCharacters(in: .whitespaces)
}
return cleaned
}

var gravatarURL: URL? {
let email = authorEmail.lowercased().trimmingCharacters(in: .whitespaces)
guard let data = email.data(using: .utf8) else { return nil }
let hash = data.md5Hash
return URL(string: "https://www.gravatar.com/avatar/\(hash)?d=identicon&s=80")
// Use identicon fallback to always return an image
return URL(string: "https://www.gravatar.com/avatar/\(hash)?s=80&d=identicon")
}

static func == (lhs: Commit, rhs: Commit) -> Bool {
Expand Down
Loading