Skip to content

Commit da31956

Browse files
authored
Add functionality for cloning from git in welcome view (#232)
* 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
1 parent 70c431e commit da31956

File tree

6 files changed

+208
-3
lines changed

6 files changed

+208
-3
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
5CAD1B972806B57D0059A74E /* Breadcrumbs in Frameworks */ = {isa = PBXBuildFile; productRef = 5CAD1B962806B57D0059A74E /* Breadcrumbs */; };
4848
5CF38A5E27E48E6C0096A0F7 /* CodeFile in Frameworks */ = {isa = PBXBuildFile; productRef = 5CF38A5D27E48E6C0096A0F7 /* CodeFile */; };
4949
5CFA753B27E896B60002F01B /* GitClient in Frameworks */ = {isa = PBXBuildFile; productRef = 5CFA753A27E896B60002F01B /* GitClient */; };
50+
B34B213227ECDC3A006033A9 /* GitClone in Frameworks */ = {isa = PBXBuildFile; productRef = B34B213127ECDC3A006033A9 /* GitClone */; };
5051
64B64EDE27F7B79400C400F1 /* About in Frameworks */ = {isa = PBXBuildFile; productRef = 64B64EDD27F7B79400C400F1 /* About */; };
5152
B658FB3427DA9E1000EA4DBD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B658FB3327DA9E1000EA4DBD /* Assets.xcassets */; };
5253
B658FB3727DA9E1000EA4DBD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B658FB3627DA9E1000EA4DBD /* Preview Assets.xcassets */; };
@@ -158,6 +159,7 @@
158159
D70F5E2C27E4E8CF004EE4B9 /* WelcomeModule in Frameworks */,
159160
2859B93F27EB50050069BE88 /* FontPicker in Frameworks */,
160161
D7F72DEB27EA3574000C3064 /* Search in Frameworks */,
162+
B34B213227ECDC3A006033A9 /* GitClone in Frameworks */,
161163
5CFA753B27E896B60002F01B /* GitClient in Frameworks */,
162164
28CE5EA027E6493D0065D29C /* StatusBar in Frameworks */,
163165
20D2A95527F72E6800E7ECF6 /* Accounts in Frameworks */,
@@ -382,6 +384,7 @@
382384
0485EB2427E7B9C800138301 /* Overlays */,
383385
5CFA753A27E896B60002F01B /* GitClient */,
384386
D7F72DEA27EA3574000C3064 /* Search */,
387+
B34B213127ECDC3A006033A9 /* GitClone */,
385388
2859B93E27EB50050069BE88 /* FontPicker */,
386389
2803257027F3CF1F009C7DC2 /* AppPreferences */,
387390
20D2A95427F72E6800E7ECF6 /* Accounts */,
@@ -1049,6 +1052,10 @@
10491052
isa = XCSwiftPackageProductDependency;
10501053
productName = GitClient;
10511054
};
1055+
B34B213127ECDC3A006033A9 /* GitClone */ = {
1056+
isa = XCSwiftPackageProductDependency;
1057+
productName = GitClone;
1058+
};
10521059
64B64EDD27F7B79400C400F1 /* About */ = {
10531060
isa = XCSwiftPackageProductDependency;
10541061
productName = About;

CodeEditModules/Modules/GitClient/src/Interface.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public struct GitClient {
1212
public var getBranches: () throws -> [String]
1313
public var checkoutBranch: (String) throws -> Void
1414
public var pull: () throws -> Void
15+
public var cloneRepository: (String) throws -> Void
1516
/// Get commit history
1617
/// - Parameters:
1718
/// - entries: number of commits we want to fetch. Will use max if nil
@@ -24,12 +25,14 @@ public struct GitClient {
2425
getBranches: @escaping () throws -> [String],
2526
checkoutBranch: @escaping (String) throws -> Void,
2627
pull: @escaping () throws -> Void,
28+
cloneRepository: @escaping (String) throws -> Void,
2729
getCommitHistory: @escaping (_ entries: Int?, _ fileLocalPath: String?) throws -> [Commit]
2830
) {
2931
self.getCurrentBranchName = getCurrentBranchName
3032
self.getBranches = getBranches
3133
self.checkoutBranch = checkoutBranch
3234
self.pull = pull
35+
self.cloneRepository = cloneRepository
3336
self.getCommitHistory = getCommitHistory
3437
}
3538

CodeEditModules/Modules/GitClient/src/Live.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ public extension GitClient {
4444
throw GitClientError.outputError(output)
4545
}
4646
}
47+
func cloneRepository(url: String) throws {
48+
let output = try shellClient.run("cd \(directoryURL.relativePath);git clone \(url) .")
49+
if output.contains("fatal") {
50+
throw GitClientError.outputError(output)
51+
}
52+
}
4753

4854
func getCommitHistory(entries: Int?, fileLocalPath: String?) throws -> [Commit] {
4955
var entriesString = ""
@@ -79,6 +85,7 @@ public extension GitClient {
7985
throw GitClientError.notGitRepository
8086
}
8187
},
88+
cloneRepository: cloneRepository(url:),
8289
getCommitHistory: getCommitHistory(entries:fileLocalPath:)
8390
)
8491
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
//
2+
// GitCloneView.swift
3+
// CodeEdit
4+
//
5+
// Created by Aleksi Puttonen on 23.3.2022.
6+
//
7+
8+
import SwiftUI
9+
import GitClient
10+
import Foundation
11+
import ShellClient
12+
13+
public struct GitCloneView: View {
14+
private let shellClient: ShellClient
15+
@Binding private var isPresented: Bool
16+
@State private var repoUrlStr = ""
17+
@State private var repoPath = "~/"
18+
public init(shellClient: ShellClient, isPresented: Binding<Bool>) {
19+
self.shellClient = shellClient
20+
self._isPresented = isPresented
21+
}
22+
public var body: some View {
23+
VStack(spacing: 8) {
24+
HStack {
25+
Image(nsImage: NSApp.applicationIconImage)
26+
.resizable()
27+
.frame(width: 64, height: 64)
28+
.padding(.bottom, 50)
29+
VStack(alignment: .leading) {
30+
Text("Clone an existing repository")
31+
.bold()
32+
.padding(.bottom, 2)
33+
Text("Enter a git repository URL:")
34+
.font(.system(size: 11))
35+
.foregroundColor(.secondary)
36+
.alignmentGuide(.trailing) { context in
37+
context[.trailing]
38+
}
39+
TextField("Git Repository URL", text: $repoUrlStr)
40+
.lineLimit(1)
41+
.padding(.bottom, 15)
42+
.frame(width: 300)
43+
HStack {
44+
Button("Cancel") {
45+
isPresented = false
46+
}
47+
Button("Clone") {
48+
cloneRepository()
49+
}
50+
.keyboardShortcut(.defaultAction)
51+
.disabled(!isValid(url: repoUrlStr))
52+
}
53+
.offset(x: 185)
54+
.alignmentGuide(.leading) { context in
55+
context[.leading]
56+
}
57+
}
58+
}
59+
.padding(.top, 20)
60+
.padding(.horizontal, 20)
61+
.padding(.bottom, 16)
62+
.onAppear {
63+
self.checkClipboard(textFieldText: &repoUrlStr)
64+
}
65+
}
66+
}
67+
}
68+
69+
extension GitCloneView {
70+
func getPath(modifiable: inout String, saveName: String) -> String? {
71+
let dialog = NSSavePanel()
72+
dialog.showsResizeIndicator = true
73+
dialog.showsHiddenFiles = false
74+
dialog.showsTagField = false
75+
dialog.prompt = "Clone"
76+
dialog.nameFieldStringValue = saveName
77+
dialog.nameFieldLabel = "Clone as"
78+
dialog.title = "Clone"
79+
80+
if dialog.runModal() == NSApplication.ModalResponse.OK {
81+
let result = dialog.url
82+
83+
if result != nil {
84+
let path: String = result!.path
85+
// path contains the directory path e.g
86+
// /Users/ourcodeworld/Desktop/folder
87+
modifiable = path
88+
return path
89+
}
90+
} else {
91+
// User clicked on "Cancel"
92+
return nil
93+
}
94+
return nil
95+
}
96+
func showAlert(alertMsg: String, infoText: String) {
97+
let alert = NSAlert()
98+
alert.messageText = alertMsg
99+
alert.informativeText = infoText
100+
alert.addButton(withTitle: "OK")
101+
alert.alertStyle = .warning
102+
alert.runModal()
103+
}
104+
func isValid(url: String) -> Bool {
105+
// Doing the same kind of check that Xcode does when cloning
106+
let url = url.lowercased()
107+
if url.starts(with: "http://") && url.count > 7 {
108+
return true
109+
} else if url.starts(with: "https://") && url.count > 8 {
110+
return true
111+
} else if url.starts(with: "git@") && url.count > 4 {
112+
return true
113+
}
114+
return false
115+
}
116+
func checkClipboard(textFieldText: inout String) {
117+
if let url = NSPasteboard.general.pasteboardItems?.first?.string(forType: .string) {
118+
if isValid(url: url) {
119+
textFieldText = url
120+
}
121+
}
122+
}
123+
private func cloneRepository() {
124+
do {
125+
if repoUrlStr == "" {
126+
showAlert(alertMsg: "Url cannot be empty",
127+
infoText: "You must specify a repository to clone")
128+
return
129+
}
130+
// Parsing repo name
131+
let repoURL = URL(string: repoUrlStr)
132+
if var repoName = repoURL?.lastPathComponent {
133+
// Strip .git from name if it has it.
134+
// Cloning repository without .git also works
135+
if repoName.contains(".git") {
136+
repoName.removeLast(4)
137+
}
138+
guard getPath(modifiable: &repoPath, saveName: repoName) != nil else {
139+
return
140+
}
141+
} else {
142+
return
143+
}
144+
guard let dirUrl = URL(string: repoPath) else {
145+
return
146+
}
147+
var isDir: ObjCBool = true
148+
if FileManager.default.fileExists(atPath: repoPath, isDirectory: &isDir) {
149+
showAlert(alertMsg: "Error", infoText: "Directory already exists")
150+
return
151+
}
152+
try FileManager.default.createDirectory(atPath: repoPath,
153+
withIntermediateDirectories: true,
154+
attributes: nil)
155+
try GitClient.default(directoryURL: dirUrl,
156+
shellClient: shellClient).cloneRepository(repoUrlStr)
157+
// TODO: Maybe add possibility to checkout to certain branch straight after cloning
158+
isPresented = false
159+
} catch {
160+
guard let error = error as? GitClient.GitClientError else {
161+
return showAlert(alertMsg: "Error", infoText: error.localizedDescription)
162+
}
163+
switch error {
164+
case let .outputError(message):
165+
showAlert(alertMsg: "Error", infoText: message)
166+
case .notGitRepository:
167+
showAlert(alertMsg: "Error", infoText: "Not git repository")
168+
}
169+
}
170+
}
171+
}

CodeEditModules/Modules/WelcomeModule/src/WelcomeView.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import SwiftUI
99
import AppKit
1010
import Foundation
1111
import AppPreferences
12+
import GitClone
1213

1314
public struct WelcomeView: View {
1415
@Environment(\.colorScheme) var colorScheme
16+
@State var showGitClone = false
1517
@State var isHovering: Bool = false
1618
@State var isHoveringClose: Bool = false
1719
@StateObject private var prefs: AppPreferencesModel = .shared
@@ -157,9 +159,9 @@ public struct WelcomeView: View {
157159
comment: ""
158160
)
159161
)
160-
.onTapGesture {
161-
// TODO: clone a Git repository
162-
}
162+
.onTapGesture {
163+
showGitClone = true
164+
}
163165
}
164166
}
165167
Spacer()
@@ -198,6 +200,9 @@ public struct WelcomeView: View {
198200
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.25)))
199201
}
200202
}
203+
.sheet(isPresented: $showGitClone) {
204+
GitCloneView(shellClient: .live, isPresented: $showGitClone)
205+
}
201206
}
202207

203208
private var dismissButton: some View {

CodeEditModules/Package.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ let package = Package(
4141
name: "Search",
4242
targets: ["Search"]
4343
),
44+
.library(
45+
name: "GitClone",
46+
targets: ["GitClone"]
47+
),
4448
.library(
4549
name: "FontPicker",
4650
targets: ["FontPicker"]
@@ -204,6 +208,14 @@ let package = Package(
204208
],
205209
path: "Modules/Search/src"
206210
),
211+
.target(
212+
name: "GitClone",
213+
dependencies: [
214+
"GitClient",
215+
"ShellClient"
216+
],
217+
path: "Modules/GitClone/src"
218+
),
207219
.target(
208220
name: "FontPicker",
209221
path: "Modules/FontPicker/src"

0 commit comments

Comments
 (0)