@@ -2,7 +2,7 @@ package controllers
22
33import (
44 "fmt"
5- "net/url "
5+ "regexp "
66 "strings"
77
88 "github.com/jesseduffield/gocui"
@@ -186,79 +186,44 @@ func (self *RemotesController) add() error {
186186 return nil
187187}
188188
189- // replaceForkUsername replaces the "owner" part of a git remote URL with forkUsername,
190- // preserving the repo name (last path segment) and everything else (host, scheme, port, .git suffix).
191- // Supported forms:
192- // - SSH scp-like: git@host:owner[/subgroups]/repo(.git)
193- // - HTTPS/HTTP: https://host/owner[/subgroups]/repo(.git)
194- //
195- // Rules:
196- // - If there are fewer than 2 path segments (i.e., no clear owner+repo), return an error.
197- // - For multi-segment paths (e.g., group/subgroup/repo), the entire prefix is replaced by forkUsername.
189+ var (
190+ // SSH scp-like: git@host:owner[/subgroups]/repo(.git)
191+ sshRegex = regexp .MustCompile (`^(git@[^:]+:)([^/]+(?:/[^/]+)*)/([^/]+?)(\.git)?$` )
192+
193+ // HTTPS/HTTP: https://host/owner[/subgroups]/repo(.git)
194+ httpRegex = regexp .MustCompile (`^(https?://[^/]+/)([^/]+(?:/[^/]+)*)/([^/]+?)(\.git)?$` )
195+
196+ // Shorthand: owner[/subgroups]/repo(.git)
197+ shorthandRegex = regexp .MustCompile (`^([^/]+(?:/[^/]+)*)/([^/]+?)(\.git)?$` )
198+ )
199+
200+ // replaceForkUsername rewrites a Git remote URL so that the owner/group part
201+ // is replaced with the given fork username, while keeping the repo name and
202+ // host/protocol intact. Supports SSH, HTTPS, and shorthand forms.
198203func replaceForkUsername (remoteUrl , forkUsername string ) (string , error ) {
199204 if forkUsername == "" {
200- return "" , fmt .Errorf ("Fork username cannot be empty" )
205+ return "" , fmt .Errorf ("fork username cannot be empty" )
201206 }
202207 if remoteUrl == "" {
203- return "" , fmt .Errorf ("Remote url cannot be empty" )
208+ return "" , fmt .Errorf ("remote URL cannot be empty" )
204209 }
205210
206- // SSH scp-like (most common): git@host:path
207- if isScpLikeSSH (remoteUrl ) {
208- colon := strings .IndexByte (remoteUrl , ':' )
209- if colon == - 1 {
210- return "" , fmt .Errorf ("Invalid SSH remote URL (missing ':'): %s" , remoteUrl )
211- }
212- path := remoteUrl [colon + 1 :] // e.g. owner/repo(.git) or group/sub/repo(.git)
213- segments := splitNonEmpty (path , "/" )
214- if len (segments ) < 2 {
215- return "" , fmt .Errorf ("Remote URL must include owner and repo: %s" , remoteUrl )
216- }
217- last := segments [len (segments )- 1 ] // repo(.git)
218- newPath := forkUsername + "/" + last
219- return remoteUrl [:colon + 1 ] + newPath , nil
211+ // SSH
212+ if sshRegex .MatchString (remoteUrl ) {
213+ return sshRegex .ReplaceAllString (remoteUrl , "${1}" + forkUsername + "/$3$4" ), nil
220214 }
221215
222- // Try URL parsing for http(s) (and reject anything else).
223- u , err := url .Parse (remoteUrl )
224- if err != nil {
225- return "" , fmt .Errorf ("Invalid remote URL: %w" , err )
226- }
227- if u .Scheme != "https" && u .Scheme != "http" {
228- return "" , fmt .Errorf ("Unsupported remote URL scheme: %s" , u .Scheme )
216+ // HTTPS/HTTP
217+ if httpRegex .MatchString (remoteUrl ) {
218+ return httpRegex .ReplaceAllString (remoteUrl , "${1}" + forkUsername + "/$3$4" ), nil
229219 }
230220
231- // u.Path like "/owner[/subgroups]/repo(.git)" or "" or "/"
232- path := strings .Trim (u .Path , "/" )
233- segments := splitNonEmpty (path , "/" )
234- if len (segments ) < 2 {
235- return "" , fmt .Errorf ("Remote URL must include owner and repo: %s" , remoteUrl )
221+ // Shorthand (owner/repo)
222+ if shorthandRegex .MatchString (remoteUrl ) {
223+ return shorthandRegex .ReplaceAllString (remoteUrl , forkUsername + "/$2$3" ), nil
236224 }
237225
238- last := segments [len (segments )- 1 ] // repo(.git)
239- u .Path = "/" + forkUsername + "/" + last
240-
241- // Preserve trailing slash only if it existed and wasn't empty
242- // (remotes rarely care, but we'll avoid adding one)
243- return u .String (), nil
244- }
245-
246- func isScpLikeSSH (s string ) bool {
247- // Minimal heuristic: "<user>@<host>:<path>"
248- at := strings .IndexByte (s , '@' )
249- colon := strings .IndexByte (s , ':' )
250- return at > 0 && colon > at
251- }
252-
253- func splitNonEmpty (s , sep string ) []string {
254- raw := strings .Split (s , sep )
255- out := make ([]string , 0 , len (raw ))
256- for _ , p := range raw {
257- if p != "" {
258- out = append (out , p )
259- }
260- }
261- return out
226+ return "" , fmt .Errorf ("unsupported or invalid remote URL: %s" , remoteUrl )
262227}
263228
264229func (self * RemotesController ) addFork (baseRemote * models.Remote ) error {
0 commit comments