diff --git a/CodeApp/Containers/SourceControlContainer.swift b/CodeApp/Containers/SourceControlContainer.swift index cf2704ca4..061cafd50 100644 --- a/CodeApp/Containers/SourceControlContainer.swift +++ b/CodeApp/Containers/SourceControlContainer.swift @@ -146,7 +146,8 @@ struct SourceControlContainer: View { guard let serviceProvider = App.workSpaceStorage.gitServiceProvider else { throw SourceControlError.gitServiceProviderUnavailable } - guard let gitURL = URL(string: urlString) else { + let normalizedURLString = LocalGitCredentialsHelper.normalizeRemoteURL(urlString) + guard let gitURL = URL(string: normalizedURLString) else { App.notificationManager.showErrorMessage("errors.source_control.invalid_url") throw SourceControlError.invalidURL } diff --git a/CodeApp/Managers/FileSystem/Local/LocalGitCredentialsHelper.swift b/CodeApp/Managers/FileSystem/Local/LocalGitCredentialsHelper.swift index 23f636de7..108cafa57 100644 --- a/CodeApp/Managers/FileSystem/Local/LocalGitCredentialsHelper.swift +++ b/CodeApp/Managers/FileSystem/Local/LocalGitCredentialsHelper.swift @@ -204,12 +204,43 @@ final class LocalGitCredentialsHelper { } func credentialsForRemote(remote: Remote) throws -> Credentials { - guard let url = URL(string: remote.URL) else { + let normalizedURL = LocalGitCredentialsHelper.normalizeRemoteURL(remote.URL) + guard let url = URL(string: normalizedURL) else { throw HelperError.UnsupportedRemoteURL } return try credentialsForRemoteURL(url: url) } + /// Normalizes a Git remote URL string to ensure proper parsing + /// Handles bare IP:port and hostname:port formats by adding http:// scheme + /// Preserves scp-like syntax (git@host:path) and fully-qualified URLs + static func normalizeRemoteURL(_ urlString: String) -> String { + // If URL already has a valid scheme and parses correctly, return as-is + if let url = URL(string: urlString), + let scheme = url.scheme, + ["http", "https", "ssh", "git", "file", "ftp", "ftps"].contains(scheme), + url.host != nil { + return urlString + } + + // Check if it's an scp-like URL (has @ but no ://) + // e.g., git@github.com:user/repo.git + if urlString.contains("@") && !urlString.contains("://") { + return urlString // Let parseRemoteURL handle it + } + + // Check if it looks like bare IP:port or hostname:port + // Pattern matches: 192.1.1.1:3000/path or forgejo.local:3000/path + let pattern = #"^([a-zA-Z0-9\.\-]+):(\d+)(/.*)?$"# + if let regex = try? NSRegularExpression(pattern: pattern), + regex.firstMatch(in: urlString, range: NSRange(urlString.startIndex..., in: urlString)) != nil { + // Prepend http:// as default scheme + return "http://\(urlString)" + } + + return urlString + } + static func parseRemoteURL(url: URL) -> URL? { if url.scheme == nil { // Handle scp-like syntax urls. diff --git a/CodeUITests/CodeUITests.swift b/CodeUITests/CodeUITests.swift index b2beab417..0d881e906 100644 --- a/CodeUITests/CodeUITests.swift +++ b/CodeUITests/CodeUITests.swift @@ -80,4 +80,48 @@ final class CodeUITests: XCTestCase { XCTAssertEqual(parsed!.scheme, "ssh") XCTAssertEqual(parsed!.path, "/codeapp.git") } + + func testNormalizeRemoteURL_bareIPWithPort() throws { + let urlString = "192.1.1.1:3000/repo.git" + let normalized = LocalGitCredentialsHelper.normalizeRemoteURL(urlString) + + XCTAssertEqual(normalized, "http://192.1.1.1:3000/repo.git") + XCTAssertNotNil(URL(string: normalized)) + XCTAssertEqual(URL(string: normalized)!.host, "192.1.1.1") + XCTAssertEqual(URL(string: normalized)!.port, 3000) + } + + func testNormalizeRemoteURL_hostnameWithPort() throws { + let urlString = "forgejo.local:3000/user/repo.git" + let normalized = LocalGitCredentialsHelper.normalizeRemoteURL(urlString) + + XCTAssertEqual(normalized, "http://forgejo.local:3000/user/repo.git") + XCTAssertNotNil(URL(string: normalized)) + XCTAssertEqual(URL(string: normalized)!.host, "forgejo.local") + XCTAssertEqual(URL(string: normalized)!.port, 3000) + } + + func testNormalizeRemoteURL_scpLikeSyntax() throws { + let urlString = "git@github.com:user/repo.git" + let normalized = LocalGitCredentialsHelper.normalizeRemoteURL(urlString) + + // Should not be modified - let parseRemoteURL handle it + XCTAssertEqual(normalized, urlString) + } + + func testNormalizeRemoteURL_fullyQualifiedHTTPS() throws { + let urlString = "https://github.com/user/repo.git" + let normalized = LocalGitCredentialsHelper.normalizeRemoteURL(urlString) + + // Should not be modified - already valid + XCTAssertEqual(normalized, urlString) + } + + func testNormalizeRemoteURL_fullyQualifiedHTTP() throws { + let urlString = "http://192.1.1.1:3000/repo.git" + let normalized = LocalGitCredentialsHelper.normalizeRemoteURL(urlString) + + // Should not be modified - already valid + XCTAssertEqual(normalized, urlString) + } }