Skip to content

Commit 8d6f31e

Browse files
authored
Branch checkout after cloning (#460)
* Git Clone: Add basic functionality of cloning repository Basics of cloning repository implemented. Pressing clone on welcome view now opens simple window with url input with clone and close buttons For now it clones the repositories in to your home dir. Error handling for now is nada and should be implemented in later commits * Git Clone: Select target folder and clone the repository Now you can select the folder to which you want to clone the repository. This defaults to home folder. Also added check that the repository isn't empty when trying to clone. * Git Clone: Open cloned folder Now after we press `clone` button, we firstly clone the specified repository to given folder. After cloning, we open folder picker for the user to pick folder to open the editor in. However this could be better if we straight up open the editor in the cloned folder, rather than giving user an option to choose the folder in between. * Git Clone: Add error handling Added error handling for git related errors e.g. when trying to clone non existent repository * Git Clone: Implementation as a module Changed GitClone to be module as discussed in #232 (comment) * Git Clone: Uncomment dependency from Package.swift * Git Clone: Switch to use NSSavePanel Switched the git clone view folder picking to use NSSavePanel instead of NSOpenPanel. NSSavePanel gives us kind of the same flow as Xcode has, as it's also using NSSavePanel to prompt the target folder for cloning. The view now consist of two separate modals that pop open, and it may seem cumbersome, but it now has kind of the same functionality as Xcode. * Git Clone: Remove .git only if the url has it. * Git Clone: Reposition UI Repositioning buttons, input and text. Also added app icon to the view * Git Clone: Change clone view to be sheet Changed git clone view to be sheet instead of normal window. This is better for UX as you can't click anywhere else than the clone view until you either cancel or clone the repository * Git Clone: Validate url and more error handling Naive Xcode-like check for the url implemented. For now if the url does not start with certain string, the clone button is disabled, just like in Xcode. Also added check for when user decides to press cancel on folder selection #232 (comment) * Git Clone: Add automatic pasting If you have 'valid' git url in your clipboard when opening the clone view, it now automatically pastes it to the textfield * GitClone: Fix some force unwrappings and make vars private * GitClone: Make shellClient constant * GitClone: Fix indentation and make logic private func Fixed indentation in WelcomeView and moved the logic of cloning repository into it's own private function * GitClone: Add CheckoutBranchModal Added modal to checkout to other branches after cloning the repository * GitClone: Make checkout button defaultAction * GitClone: Add possibility to checkout to remote branches Showing all branhes to make it possible to checkout to remote branches as well as local * GitClone: Make the modal wider * GitClone: Open checkout branch modal only if there's multiple branches * GitClone: Refactor extension to separate file.
1 parent 66a4516 commit 8d6f31e

File tree

9 files changed

+179
-13
lines changed

9 files changed

+179
-13
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
5CAD1B972806B57D0059A74E /* Breadcrumbs in Frameworks */ = {isa = PBXBuildFile; productRef = 5CAD1B962806B57D0059A74E /* Breadcrumbs */; };
4949
5CF38A5E27E48E6C0096A0F7 /* CodeFile in Frameworks */ = {isa = PBXBuildFile; productRef = 5CF38A5D27E48E6C0096A0F7 /* CodeFile */; };
5050
5CFA753B27E896B60002F01B /* GitClient in Frameworks */ = {isa = PBXBuildFile; productRef = 5CFA753A27E896B60002F01B /* GitClient */; };
51+
B34B213227ECDC3A006033A9 /* GitClone in Frameworks */ = {isa = PBXBuildFile; productRef = B34B213127ECDC3A006033A9 /* GitClone */; };
5152
64B64EDE27F7B79400C400F1 /* About in Frameworks */ = {isa = PBXBuildFile; productRef = 64B64EDD27F7B79400C400F1 /* About */; };
5253
B34B213227ECDC3A006033A9 /* GitClone in Frameworks */ = {isa = PBXBuildFile; productRef = B34B213127ECDC3A006033A9 /* GitClone */; };
5354
B658FB3427DA9E1000EA4DBD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B658FB3327DA9E1000EA4DBD /* Assets.xcassets */; };
@@ -1069,6 +1070,10 @@
10691070
isa = XCSwiftPackageProductDependency;
10701071
productName = GitClient;
10711072
};
1073+
B34B213127ECDC3A006033A9 /* GitClone */ = {
1074+
isa = XCSwiftPackageProductDependency;
1075+
productName = GitClone;
1076+
};
10721077
64B64EDD27F7B79400C400F1 /* About */ = {
10731078
isa = XCSwiftPackageProductDependency;
10741079
productName = About;

CodeEdit/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,6 @@
4545
</dict>
4646
</array>
4747
<key>GitHash</key>
48-
<string>67a2618bab3900fa3dd96764fb0232183a95fb88</string>
48+
<string>13ac174726e9ecffc846676d833be1cac8fd953c</string>
4949
</dict>
5050
</plist>

CodeEditModules/Modules/GitClient/src/Interface.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99

1010
public struct GitClient {
1111
public var getCurrentBranchName: () throws -> String
12-
public var getBranches: () throws -> [String]
12+
public var getBranches: (Bool) throws -> [String]
1313
public var checkoutBranch: (String) throws -> Void
1414
public var pull: () throws -> Void
1515
public var cloneRepository: (String) throws -> Void
@@ -22,7 +22,7 @@ public struct GitClient {
2222

2323
init(
2424
getCurrentBranchName: @escaping () throws -> String,
25-
getBranches: @escaping () throws -> [String],
25+
getBranches: @escaping (Bool) throws -> [String],
2626
checkoutBranch: @escaping (String) throws -> Void,
2727
pull: @escaping () throws -> Void,
2828
cloneRepository: @escaping (String) throws -> Void,

CodeEditModules/Modules/GitClient/src/Live.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@ public extension GitClient {
1414
directoryURL: URL,
1515
shellClient: ShellClient
1616
) -> GitClient {
17-
func getBranches() throws -> [String] {
18-
try shellClient.run(
17+
func getBranches(_ allBranches: Bool = false) throws -> [String] {
18+
if allBranches == true {
19+
return try shellClient.run(
20+
"cd \(directoryURL.relativePath);git branch -a --format \"%(refname:short)\""
21+
)
22+
.components(separatedBy: "\n")
23+
.filter { $0 != "" }
24+
}
25+
return try shellClient.run(
1926
"cd \(directoryURL.relativePath);git branch --format \"%(refname:short)\""
2027
)
2128
.components(separatedBy: "\n")
@@ -40,7 +47,7 @@ public extension GitClient {
4047
)
4148
if output.contains("fatal: not a git repository") {
4249
throw GitClientError.notGitRepository
43-
} else if !output.contains("Switched to branch") {
50+
} else if !output.contains("Switched to branch") && !output.contains("Switched to a new branch") {
4451
throw GitClientError.outputError(output)
4552
}
4653
}
@@ -50,7 +57,6 @@ public extension GitClient {
5057
throw GitClientError.outputError(output)
5158
}
5259
}
53-
5460
func getCommitHistory(entries: Int?, fileLocalPath: String?) throws -> [Commit] {
5561
var entriesString = ""
5662
let fileLocalPath = fileLocalPath ?? ""
@@ -75,7 +81,7 @@ public extension GitClient {
7581

7682
return GitClient(
7783
getCurrentBranchName: getCurrentBranchName,
78-
getBranches: getBranches,
84+
getBranches: getBranches(_:),
7985
checkoutBranch: checkoutBranch(name:),
8086
pull: {
8187
let output = try shellClient.run(
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// CheckoutBranch.swift
3+
//
4+
//
5+
// Created by Aleksi Puttonen on 18.4.2022.
6+
//
7+
8+
import Foundation
9+
import GitClient
10+
import SwiftUI
11+
12+
public extension CheckoutBranchView {
13+
func getBranches() -> [String] {
14+
guard let url = URL(string: repoPath) else {
15+
return [""]
16+
}
17+
do {
18+
let branches = try GitClient.default(directoryURL: url,
19+
shellClient: shellClient).getBranches(true)
20+
return branches
21+
} catch {
22+
return [""]
23+
}
24+
}
25+
func checkoutBranch() {
26+
var parsedBranch = selectedBranch
27+
if selectedBranch.contains("origin/") || selectedBranch.contains("upstream/") {
28+
parsedBranch = selectedBranch.components(separatedBy: "/")[1]
29+
}
30+
do {
31+
if let url = URL(string: repoPath) {
32+
try GitClient.default(directoryURL: url,
33+
shellClient: shellClient).checkoutBranch(parsedBranch)
34+
isPresented = false
35+
}
36+
} catch {
37+
guard let error = error as? GitClient.GitClientError else { return }
38+
let alert = NSAlert()
39+
alert.alertStyle = .critical
40+
alert.addButton(withTitle: "Ok")
41+
switch error {
42+
case .notGitRepository:
43+
alert.messageText = "Not a git repository"
44+
case let .outputError(message):
45+
alert.messageText = message
46+
}
47+
alert.runModal()
48+
}
49+
}
50+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//
2+
// CheckoutBranchView.swift
3+
//
4+
//
5+
// Created by Aleksi Puttonen on 14.4.2022.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
import GitClient
11+
import ShellClient
12+
13+
public struct CheckoutBranchView: View {
14+
internal let shellClient: ShellClient
15+
@Binding internal var isPresented: Bool
16+
@Binding internal var repoPath: String
17+
// TODO: This has to be derived from git
18+
@State internal var selectedBranch = "main"
19+
public init(isPresented: Binding<Bool>,
20+
repoPath: Binding<String>,
21+
shellClient: ShellClient) {
22+
self.shellClient = shellClient
23+
self._isPresented = isPresented
24+
self._repoPath = repoPath
25+
}
26+
public var body: some View {
27+
VStack(spacing: 8) {
28+
HStack {
29+
Image(nsImage: NSApp.applicationIconImage)
30+
.resizable()
31+
.frame(width: 64, height: 64)
32+
.padding(.bottom, 50)
33+
VStack(alignment: .leading) {
34+
Text("Checkout branch")
35+
.bold()
36+
.padding(.bottom, 2)
37+
Text("Select a branch to checkout")
38+
.font(.system(size: 11))
39+
.foregroundColor(.secondary)
40+
.alignmentGuide(.trailing) { context in
41+
context[.trailing]
42+
}
43+
Menu {
44+
ForEach(getBranches().filter {!$0.contains("HEAD")}, id: \.self) { branch in
45+
Button {
46+
guard selectedBranch != branch else { return }
47+
selectedBranch = branch
48+
} label: {
49+
Text(branch)
50+
}.disabled(selectedBranch == branch)
51+
}
52+
} label: {
53+
Text(selectedBranch)
54+
}
55+
HStack {
56+
Button("Cancel") {
57+
isPresented = false
58+
}
59+
Button("Checkout") {
60+
checkoutBranch()
61+
}
62+
.keyboardShortcut(.defaultAction)
63+
}
64+
.alignmentGuide(.trailing) { context in
65+
context[.trailing]
66+
}
67+
.offset(x: 145)
68+
}
69+
}
70+
.padding(.top, 20)
71+
.padding(.horizontal, 20)
72+
.padding(.bottom, 16)
73+
.frame(width: 400)
74+
}
75+
}
76+
}

CodeEditModules/Modules/GitClone/src/GitCloneView.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,17 @@ import ShellClient
1313
public struct GitCloneView: View {
1414
private let shellClient: ShellClient
1515
@Binding private var isPresented: Bool
16+
@Binding private var showCheckout: Bool
17+
@Binding private var repoPath: String
1618
@State private var repoUrlStr = ""
17-
@State private var repoPath = "~/"
18-
public init(shellClient: ShellClient, isPresented: Binding<Bool>) {
19+
public init(shellClient: ShellClient,
20+
isPresented: Binding<Bool>,
21+
showCheckout: Binding<Bool>,
22+
repoPath: Binding<String>) {
1923
self.shellClient = shellClient
2024
self._isPresented = isPresented
25+
self._showCheckout = showCheckout
26+
self._repoPath = repoPath
2127
}
2228
public var body: some View {
2329
VStack(spacing: 8) {
@@ -154,8 +160,8 @@ extension GitCloneView {
154160
attributes: nil)
155161
try GitClient.default(directoryURL: dirUrl,
156162
shellClient: shellClient).cloneRepository(repoUrlStr)
157-
// TODO: Maybe add possibility to checkout to certain branch straight after cloning
158163
isPresented = false
164+
checkBranches(dirUrl: dirUrl)
159165
} catch {
160166
guard let error = error as? GitClient.GitClientError else {
161167
return showAlert(alertMsg: "Error", infoText: error.localizedDescription)
@@ -168,4 +174,17 @@ extension GitCloneView {
168174
}
169175
}
170176
}
177+
private func checkBranches(dirUrl: URL) {
178+
// Check if repo has only one branch, and if so, don't show the checkout page
179+
do {
180+
let branches = try GitClient.default(directoryURL: dirUrl,
181+
shellClient: shellClient).getBranches(true)
182+
let filtered = branches.filter {!$0.contains("HEAD")}
183+
if filtered.count > 1 {
184+
showCheckout = true
185+
}
186+
} catch {
187+
return
188+
}
189+
}
171190
}

CodeEditModules/Modules/StatusBar/src/StatusBarItems/StatusBarBranchPicker.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ internal struct StatusBarBranchPicker: View {
1818

1919
internal var body: some View {
2020
Menu {
21-
ForEach((try? model.gitClient.getBranches()) ?? [], id: \.self) { branch in
21+
ForEach((try? model.gitClient.getBranches(false)) ?? [], id: \.self) { branch in
2222
Button {
2323
do {
2424
guard model.selectedBranch != branch else { return }

CodeEditModules/Modules/WelcomeModule/src/WelcomeView.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import GitClone
1414
public struct WelcomeView: View {
1515
@Environment(\.colorScheme) var colorScheme
1616
@State var showGitClone = false
17+
@State var showCheckoutBranch = false
18+
@State private var repoPath = "~/"
1719
@State var isHovering: Bool = false
1820
@State var isHoveringClose: Bool = false
1921
@StateObject private var prefs: AppPreferencesModel = .shared
@@ -201,7 +203,15 @@ public struct WelcomeView: View {
201203
}
202204
}
203205
.sheet(isPresented: $showGitClone) {
204-
GitCloneView(shellClient: .live, isPresented: $showGitClone)
206+
GitCloneView(shellClient: .live,
207+
isPresented: $showGitClone,
208+
showCheckout: $showCheckoutBranch,
209+
repoPath: $repoPath)
210+
}
211+
.sheet(isPresented: $showCheckoutBranch) {
212+
CheckoutBranchView(isPresented: $showCheckoutBranch,
213+
repoPath: $repoPath,
214+
shellClient: .live)
205215
}
206216
}
207217

0 commit comments

Comments
 (0)