Skip to content

Commit 7f879cc

Browse files
authored
Merge pull request #6 from mherrera53/feat/syncghviews
Feat/Syncghviews
2 parents d979d96 + e56ea28 commit 7f879cc

3 files changed

Lines changed: 98 additions & 30 deletions

File tree

GitMac/Core/Services/BranchPRTracker.swift

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,8 @@ class BranchPRTracker: ObservableObject {
5555
}
5656
.store(in: &cancellables)
5757

58-
// Refresh periodically (every 60 seconds)
59-
Timer.publish(every: 60, on: .main, in: .common)
60-
.autoconnect()
61-
.sink { [weak self] _ in
62-
guard let self, !self.owner.isEmpty else { return }
63-
Task { await self.refresh() }
64-
}
65-
.store(in: &cancellables)
58+
// Note: No periodic timer - PR data is refreshed after each git action
59+
// (push, pull, checkout, merge, delete) for immediate feedback
6660
}
6761

6862
// MARK: - Configuration

GitMac/Features/Branches/BranchListView.swift

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import SwiftUI
44
struct BranchListView: View {
55
@EnvironmentObject var appState: AppState
66
@StateObject private var viewModel = BranchListViewModel()
7+
@ObservedObject private var prTracker = BranchPRTracker.shared
78
@State private var searchText = ""
89
@State private var showNewBranchSheet = false
910
@State private var selectedBranch: Branch?
@@ -115,18 +116,31 @@ struct BranchListView: View {
115116
.task {
116117
if let repo = appState.currentRepository {
117118
viewModel.loadBranches(from: repo)
119+
// Configure PR tracker for this repository
120+
await prTracker.configure(forRepoAt: repo.path)
118121
}
119122
}
120-
.onChange(of: appState.currentRepository?.path) { _, _ in
123+
.onChange(of: appState.currentRepository?.path) { _, newPath in
121124
if let repo = appState.currentRepository {
122125
viewModel.loadBranches(from: repo)
126+
// Reconfigure PR tracker when repo changes
127+
Task { await prTracker.configure(forRepoAt: repo.path) }
123128
}
124129
}
125130
.onReceive(NotificationCenter.default.publisher(for: .repositoryDidRefresh)) { notification in
126131
if let repo = appState.currentRepository {
127132
viewModel.loadBranches(from: repo)
128133
}
129134
}
135+
.onReceive(NotificationCenter.default.publisher(for: .branchPRsDidUpdate)) { _ in
136+
// Force view refresh when PR data updates
137+
// Trigger cache update to force SwiftUI re-render
138+
updateFilterCache()
139+
}
140+
.onChange(of: prTracker.branchPRs.count) { _, _ in
141+
// Also react to PR count changes for immediate UI update
142+
updateFilterCache()
143+
}
130144
// Phase 0.2: Cache updates for performance
131145
.onChange(of: searchText) { _, newValue in
132146
updateFilterCache()
@@ -227,7 +241,10 @@ struct BranchListView: View {
227241

228242

229243
private func branchRowView(for branch: Branch) -> some View {
230-
Group {
244+
// Get PR for this branch (if any)
245+
let pr = prTracker.getPR(for: branch.name)
246+
247+
return Group {
231248
BranchRow(
232249
branch: branch,
233250
isSelected: selectedBranch?.id == branch.id,
@@ -254,10 +271,29 @@ struct BranchListView: View {
254271
selectedBranch = branch
255272
showRebaseSheet = true
256273
},
274+
// PR integration
275+
pullRequest: pr,
276+
onCreatePR: branch.isRemote ? nil : {
277+
selectedBranch = branch
278+
showPRSheet = true
279+
},
280+
onViewPR: { prItem in
281+
// Open PR in browser
282+
if let url = URL(string: prItem.htmlUrl) {
283+
NSWorkspace.shared.open(url)
284+
}
285+
},
286+
onMergePR: { prItem, method in
287+
Task {
288+
try? await prTracker.mergePR(prItem, method: method)
289+
}
290+
},
257291
onBranchDropped: { droppedBranch in
258292
handleBranchDrop(dragged: droppedBranch, onto: branch)
259293
}
260294
)
295+
// Force re-render when PR state changes for this branch
296+
.id("\(branch.id)-\(pr?.number ?? 0)")
261297
}
262298
}
263299

@@ -434,6 +470,9 @@ class BranchListViewModel: ObservableObject {
434470
// Post both notifications for full sync
435471
NotificationCenter.default.post(name: .branchDidCheckout, object: branchName)
436472
NotificationCenter.default.post(name: .repositoryDidRefresh, object: path)
473+
474+
// Refresh PR tracker after checkout
475+
await BranchPRTracker.shared.refresh()
437476
} catch let gitError as GitError {
438477
self.error = gitError.localizedDescription
439478
if let fix = gitError.suggestedFix {
@@ -537,6 +576,9 @@ class BranchListViewModel: ObservableObject {
537576
// Post notifications for full sync
538577
NotificationCenter.default.post(name: .branchDidCheckout, object: localName)
539578
NotificationCenter.default.post(name: .repositoryDidRefresh, object: path)
579+
580+
// Refresh PR tracker after checkout
581+
await BranchPRTracker.shared.refresh()
540582
} catch {
541583
self.error = error.localizedDescription
542584
}
@@ -568,6 +610,9 @@ class BranchListViewModel: ObservableObject {
568610

569611
// Notify UI
570612
NotificationCenter.default.post(name: .repositoryDidRefresh, object: path)
613+
614+
// Refresh PR tracker after delete (PR may have been closed)
615+
await BranchPRTracker.shared.refresh()
571616
} catch {
572617
self.error = error.localizedDescription
573618
}
@@ -584,6 +629,9 @@ class BranchListViewModel: ObservableObject {
584629
// Notify UI
585630
NotificationCenter.default.post(name: .repositoryDidRefresh, object: path)
586631

632+
// Refresh PR tracker after merge
633+
await BranchPRTracker.shared.refresh()
634+
587635
// Track successful merge
588636
RemoteOperationTracker.shared.recordMerge(
589637
success: true,
@@ -622,24 +670,27 @@ class BranchListViewModel: ObservableObject {
622670
guard let path = currentRepoPath else { return }
623671
isLoading = true
624672
do {
625-
if branch.isHead {
626-
try await engine.push(at: path)
673+
// Push the specific branch (works for both HEAD and non-HEAD branches)
674+
let options = PushOptions(branch: branch.name)
675+
try await engine.push(options: options, at: path)
627676

628-
// Notify UI
629-
NotificationCenter.default.post(name: .remoteOperationCompleted, object: "push")
630-
NotificationCenter.default.post(name: .repositoryDidRefresh, object: path)
631-
GitHubSyncManager.shared.notifyOperationCompleted(type: .push, details: branch.name)
677+
// Notify UI
678+
NotificationCenter.default.post(name: .remoteOperationCompleted, object: "push")
679+
NotificationCenter.default.post(name: .repositoryDidRefresh, object: path)
680+
GitHubSyncManager.shared.notifyOperationCompleted(type: .push, details: branch.name)
632681

633-
NotificationManager.shared.success(
634-
"Pushed '\(branch.name)'",
635-
detail: "Changes pushed to remote"
636-
)
637-
RemoteOperationTracker.shared.recordPush(
638-
success: true,
639-
branch: branch.name,
640-
remote: "origin"
641-
)
642-
}
682+
// Refresh PR tracker immediately after push
683+
await BranchPRTracker.shared.refresh()
684+
685+
NotificationManager.shared.success(
686+
"Pushed '\(branch.name)'",
687+
detail: "Changes pushed to remote"
688+
)
689+
RemoteOperationTracker.shared.recordPush(
690+
success: true,
691+
branch: branch.name,
692+
remote: "origin"
693+
)
643694
} catch let gitError as GitError {
644695
self.error = gitError.localizedDescription
645696
RemoteOperationTracker.shared.recordPush(
@@ -690,6 +741,9 @@ class BranchListViewModel: ObservableObject {
690741
NotificationCenter.default.post(name: .repositoryDidRefresh, object: path)
691742
GitHubSyncManager.shared.notifyOperationCompleted(type: .pull, details: branch.name)
692743

744+
// Refresh PR tracker immediately after pull
745+
await BranchPRTracker.shared.refresh()
746+
693747
NotificationManager.shared.success(
694748
"Pulled '\(branch.name)'",
695749
detail: "Updated from remote"
@@ -1118,6 +1172,9 @@ struct CreatePullRequestSheet: View {
11181172
detail: title
11191173
)
11201174

1175+
// Refresh PR tracker immediately so branch context menu updates
1176+
await BranchPRTracker.shared.refresh()
1177+
11211178
// Post notification to refresh PR data across the app
11221179
NotificationCenter.default.post(name: .pullRequestCreated, object: newPR)
11231180
NotificationCenter.default.post(name: .repositoryDidRefresh, object: repo.path)

GitMac/Features/PullRequests/PRListView.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,14 +244,19 @@ class PRListViewModel: ObservableObject {
244244
assignees: [String],
245245
labels: [String]
246246
) async {
247+
// Strip remote prefix from branch names (e.g., "origin/feature" -> "feature")
248+
// GitHub API expects just the branch name, not the remote prefix
249+
let cleanHead = head.replacingOccurrences(of: #"^origin/"#, with: "", options: .regularExpression)
250+
let cleanBase = base.replacingOccurrences(of: #"^origin/"#, with: "", options: .regularExpression)
251+
247252
do {
248253
let newPR = try await githubService.createPullRequest(
249254
owner: owner,
250255
repo: repo,
251256
title: title,
252257
body: body,
253-
head: head,
254-
base: base,
258+
head: cleanHead,
259+
base: cleanBase,
255260
draft: draft
256261
)
257262

@@ -289,6 +294,13 @@ class PRListViewModel: ObservableObject {
289294
"PR #\(newPR.number) created",
290295
detail: title
291296
)
297+
298+
// Refresh PR tracker immediately so branch context menu updates
299+
await BranchPRTracker.shared.refresh()
300+
301+
// Notify other views
302+
NotificationCenter.default.post(name: .pullRequestCreated, object: newPR)
303+
292304
await loadPullRequests()
293305
} catch {
294306
self.error = error.localizedDescription
@@ -1025,6 +1037,11 @@ struct CreatePRSheet: View {
10251037

10261038
private let aiService = AIService()
10271039

1040+
/// Only local branches can be used as PR head
1041+
private var localBranches: [Branch] {
1042+
appState.currentRepository?.branches.filter { !$0.isRemote } ?? []
1043+
}
1044+
10281045
var body: some View {
10291046
let theme = Color.Theme(themeManager.colors)
10301047

@@ -1059,12 +1076,12 @@ struct CreatePRSheet: View {
10591076

10601077
ScrollView {
10611078
VStack(spacing: DesignTokens.Spacing.lg) {
1062-
// Branches
1079+
// Branches - only show local branches for PR head
10631080
HStack {
10641081
VStack(alignment: .leading, spacing: DesignTokens.Spacing.xs) {
10651082
Text("From").font(.caption).foregroundColor(theme.text)
10661083
Picker("", selection: $headBranch) {
1067-
ForEach(appState.currentRepository?.branches ?? [], id: \.id) { branch in
1084+
ForEach(localBranches, id: \.id) { branch in
10681085
Text(branch.name).tag(branch.name)
10691086
}
10701087
}

0 commit comments

Comments
 (0)