Skip to content

Commit

Permalink
Git Improvements (#1458)
Browse files Browse the repository at this point in the history
* git improvements

* fix error on sync

* clone progress labels, comments

* fixes

* fix branch selector for 1 branch repo

* update refreshStatusInFileManager
  • Loading branch information
avinizhanov authored Oct 31, 2023
1 parent 8a64bc4 commit 25bfeb2
Show file tree
Hide file tree
Showing 40 changed files with 1,624 additions and 651 deletions.
84 changes: 68 additions & 16 deletions CodeEdit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,24 @@ final class CEWorkspaceFileManager {

let folderUrl: URL
let workspaceItem: CEWorkspaceFile
weak var sourceControlManager: SourceControlManager?

/// Create a file manager object with a root and a set of files to ignore.
/// - Parameters:
/// - folderUrl: The folder to use as the root of the file manager.
/// - ignoredFilesAndFolders: A set of files to ignore. These should not be paths, but rather file names
/// like `.DS_Store`
init(folderUrl: URL, ignoredFilesAndFolders: Set<String>) {
init(
folderUrl: URL,
ignoredFilesAndFolders: Set<String>,
sourceControlManager: SourceControlManager?
) {
self.folderUrl = folderUrl
self.ignoredFilesAndFolders = ignoredFilesAndFolders

self.workspaceItem = CEWorkspaceFile(url: folderUrl)
self.flattenedFileItems = [workspaceItem.id: workspaceItem]
self.sourceControlManager = sourceControlManager

self.loadChildrenForFile(self.workspaceItem)

Expand Down Expand Up @@ -142,6 +148,9 @@ final class CEWorkspaceFileManager {
flattenedFileItems[newFileItem.id] = newFileItem
}
childrenMap[file.id] = children.map { $0.relativePath }
Task {
await sourceControlManager?.refresAllChangesFiles()
}
}

/// Creates an ordered array of all files and directories at the given file object.
Expand Down Expand Up @@ -210,6 +219,14 @@ final class CEWorkspaceFileManager {
if !files.isEmpty {
self.notifyObservers(updatedItems: files)
}

// Ignore changes to .git folder
let notGitChanges = events.filter({ !$0.path.contains(".git/") })
if !notGitChanges.isEmpty {
Task {
await self.sourceControlManager?.refresAllChangesFiles()
}
}
}
}

Expand Down
86 changes: 50 additions & 36 deletions CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,27 @@

import SwiftUI
import CodeEditSymbols
import Combine

/// A view that pops up a branch picker.
struct ToolbarBranchPicker: View {
private var workspaceFileManager: CEWorkspaceFileManager?
private var gitClient: GitClient?
private var sourceControlManager: SourceControlManager?

@Environment(\.controlActiveState)
private var controlActive

@State private var isHovering: Bool = false

@State private var displayPopover: Bool = false

@State private var currentBranch: String?
@State private var currentBranch: GitBranch?

/// Initializes the ``ToolbarBranchPicker`` with an instance of a `WorkspaceClient`
/// - Parameter shellClient: An instance of the current `ShellClient`
/// - Parameter workspace: An instance of the current `WorkspaceClient`
init(
shellClient: ShellClient,
workspaceFileManager: CEWorkspaceFileManager?
) {
self.workspaceFileManager = workspaceFileManager
if let folderURL = workspaceFileManager?.folderUrl {
self.gitClient = GitClient(directoryURL: folderURL, shellClient: shellClient)
}
self._currentBranch = State(initialValue: try? gitClient?.getCurrentBranchName())
self.sourceControlManager = workspaceFileManager?.sourceControlManager
}

var body: some View {
Expand All @@ -57,7 +51,7 @@ struct ToolbarBranchPicker: View {
.help(title)
if let currentBranch {
ZStack(alignment: .trailing) {
Text(currentBranch)
Text(currentBranch.name)
.padding(.trailing)
if isHovering {
Image(systemName: "chevron.down")
Expand All @@ -79,10 +73,23 @@ struct ToolbarBranchPicker: View {
isHovering = active
}
.popover(isPresented: $displayPopover, arrowEdge: .bottom) {
PopoverView(gitClient: gitClient, currentBranch: $currentBranch)
if let sourceControlManager = workspaceFileManager?.sourceControlManager {
PopoverView(sourceControlManager: sourceControlManager)
}
}
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { (_) in
currentBranch = try? gitClient?.getCurrentBranchName()
Task {
await sourceControlManager?.refreshCurrentBranch()
}
}
.onReceive(
self.sourceControlManager?.$currentBranch.eraseToAnyPublisher() ??
Empty().eraseToAnyPublisher()
) { branch in
self.currentBranch = branch
}
.task {
await self.sourceControlManager?.refreshCurrentBranch()
}
}

Expand All @@ -100,35 +107,34 @@ struct ToolbarBranchPicker: View {
///
/// It displays the currently checked-out branch and all other local branches.
private struct PopoverView: View {
var gitClient: GitClient?

@Binding var currentBranch: String?
@ObservedObject var sourceControlManager: SourceControlManager

var body: some View {
VStack(alignment: .leading) {
if let currentBranch {
if let currentBranch = sourceControlManager.currentBranch {
VStack(alignment: .leading, spacing: 0) {
headerLabel("Current Branch")
BranchCell(name: currentBranch, active: true) {}
BranchCell(sourceControlManager: sourceControlManager, branch: currentBranch, active: true)
}
}
if !branchNames.isEmpty {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
headerLabel("Branches")
ForEach(branchNames, id: \.self) { branch in
BranchCell(name: branch) {
try? gitClient?.checkoutBranch(branch)
currentBranch = try? gitClient?.getCurrentBranchName()
}
}

let branches = sourceControlManager.branches
.filter({ $0.isLocal && $0 != sourceControlManager.currentBranch })
if !branches.isEmpty {
VStack(alignment: .leading, spacing: 0) {
headerLabel("Branches")
ForEach(branches, id: \.self) { branch in
BranchCell(sourceControlManager: sourceControlManager, branch: branch)
}
}
}
}
.padding(.top, 10)
.padding(5)
.frame(width: 340)
.task {
await sourceControlManager.refreshBranches()
}
}

func headerLabel(_ title: String) -> some View {
Expand All @@ -143,9 +149,9 @@ struct ToolbarBranchPicker: View {

/// A Button Cell that represents a branch in the branch picker
struct BranchCell: View {
var name: String
let sourceControlManager: SourceControlManager
var branch: GitBranch
var active: Bool = false
var action: () -> Void

@Environment(\.dismiss)
private var dismiss
Expand All @@ -154,12 +160,11 @@ struct ToolbarBranchPicker: View {

var body: some View {
Button {
action()
dismiss()
switchBranch()
} label: {
HStack {
Label {
Text(name)
Text(branch.name)
.frame(maxWidth: .infinity, alignment: .leading)
} icon: {
Image.checkout
Expand All @@ -184,10 +189,19 @@ struct ToolbarBranchPicker: View {
isHovering = active
}
}
}

var branchNames: [String] {
((try? gitClient?.getBranches(false)) ?? []).filter { $0 != currentBranch }
func switchBranch() {
Task {
do {
try await sourceControlManager.checkoutBranch(branch: branch)
await MainActor.run {
dismiss()
}
} catch {
await sourceControlManager.showAlertForError(title: "Failed to checkout", error: error)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs
let toolbarItem = NSToolbarItem(itemIdentifier: .branchPicker)
let view = NSHostingView(
rootView: ToolbarBranchPicker(
shellClient: currentWorld.shellClient,
workspaceFileManager: workspace?.workspaceFileManager
)
)
Expand Down
10 changes: 9 additions & 1 deletion CodeEdit/Features/Documents/WorkspaceDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate {
var quickOpenViewModel: QuickOpenViewModel?
var commandsPaletteState: CommandPaletteViewModel?
var listenerModel: WorkspaceNotificationModel = .init()
var sourceControlManager: SourceControlManager?

private var cancellables = Set<AnyCancellable>()

Expand Down Expand Up @@ -110,10 +111,17 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate {

private func initWorkspaceState(_ url: URL) throws {
self.fileURL = url
let sourceControlManager = SourceControlManager(
workspaceURL: url,
editorManager: editorManager
)
self.workspaceFileManager = .init(
folderUrl: url,
ignoredFilesAndFolders: Set(ignoredFilesAndDirectory)
ignoredFilesAndFolders: Set(ignoredFilesAndDirectory),
sourceControlManager: sourceControlManager
)
self.sourceControlManager = sourceControlManager
sourceControlManager.fileManager = workspaceFileManager
self.searchState = .init(self)
self.quickOpenViewModel = .init(fileURL: url)
self.commandsPaletteState = .init()
Expand Down
96 changes: 96 additions & 0 deletions CodeEdit/Features/Git/Client/GitClient+Branches.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// GitClient+Branches.swift
// CodeEdit
//
// Created by Albert Vinizhanau on 10/20/23.
//

import Foundation

extension GitClient {
/// Get branches
/// - Returns: Array of branches
func getBranches() async throws -> [GitBranch] {
let command = "branch --format \"%(refname:short)|%(refname)|%(upstream:short)\" -a"

return try await run(command)
.components(separatedBy: "\n")
.filter { $0 != "" && !$0.contains("HEAD") }
.compactMap { line in
let components = line.components(separatedBy: "|")
let name = components[0]
let upstream = components[safe: 2]

return .init(
name: name,
longName: components[safe: 1] ?? name,
upstream: upstream?.isEmpty == true ? nil : upstream
)
}
}

/// Get current branch
func getCurrentBranch() async throws -> GitBranch? {
let branchName = try await run("rev-parse --abbrev-ref HEAD").trimmingCharacters(in: .whitespacesAndNewlines)
let components = try await run(
"for-each-ref --format=\"%(refname)|%(upstream:short)\" refs/heads/\(branchName)"
)
.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: "|")

let upstream = components[safe: 1]

return .init(
name: branchName,
longName: components[0],
upstream: upstream?.isEmpty == true ? nil : upstream
)
}

/// Delete branch
func deleteBranch(_ branch: GitBranch) async throws {
if !branch.isLocal {
return
}

_ = try await run("branch -d \(branch.name)")
}

/// Create new branch
func newBranch(name: String, from: GitBranch) async throws {
if !from.isLocal {
return
}

_ = try await run("checkout -b \(name) \(from.name)")
}

/// Checkout branch
/// - Parameter branch: Branch to checkout
func checkoutBranch(_ branch: GitBranch, forceLocal: Bool = false) async throws {
var command = "checkout "

// If branch is remote, try to create local branch
if branch.isRemote {
let localName = branch.name.replacingOccurrences(of: "origin/", with: "")
command += forceLocal ? localName : "-b " + localName + " " + branch.name
} else {
command += branch.name
}

do {
let output = try await run(command)
if !output.contains("Switched to branch") && !output.contains("Switched to a new branch") {
throw GitClientError.outputError(output)
}
} catch {
// If branch is remote and command failed because branch already exists
// try to switch to local branch
if let error = error as? GitClientError,
branch.isRemote,
error.localizedDescription.contains("already exists") {
try await checkoutBranch(branch, forceLocal: true)
}
}
}
}
Loading

0 comments on commit 25bfeb2

Please sign in to comment.