@@ -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,37 @@ 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+ 	// 1. SCP-like SSH: git@host:owner[/subgroups]/repo(.git) 
191+ 	sshScpRegex  =  regexp .MustCompile (`^(git@[^:]+:)([^/]+(?:/[^/]+)*)/([^/]+?)(\.git)?$` )
192+ 
193+ 	// 2. SSH URL style: ssh://user@host[:port]/owner[/subgroups]/repo(.git) 
194+ 	sshUrlRegex  =  regexp .MustCompile (`^(ssh://[^/]+/)([^/]+(?:/[^/]+)*)/([^/]+?)(\.git)?$` )
195+ 
196+ 	// 3. HTTPS: https://host/owner[/subgroups]/repo(.git) 
197+ 	httpRegex  =  regexp .MustCompile (`^(https?://[^/]+/)([^/]+(?:/[^/]+)*)/([^/]+?)(\.git)?$` )
198+ )
199+ 
200+ // replaceForkUsername rewrites a Git remote URL to use the given fork username, 
201+ // keeping the repo name and host intact. Supports SCP-like SSH, SSH URL style, and HTTPS. 
198202func  replaceForkUsername (remoteUrl , forkUsername  string ) (string , error ) {
199203	if  forkUsername  ==  ""  {
200- 		return  "" , fmt .Errorf ("Fork  username cannot be empty" )
204+ 		return  "" , fmt .Errorf ("fork  username cannot be empty" )
201205	}
202206	if  remoteUrl  ==  ""  {
203- 		return  "" , fmt .Errorf ("Remote url cannot be empty" )
204- 	}
205- 
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 
220- 	}
221- 
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 )
207+ 		return  "" , fmt .Errorf ("remote URL cannot be empty" )
229208	}
230209
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 )
210+ 	switch  {
211+ 	case  sshScpRegex .MatchString (remoteUrl ):
212+ 		return  sshScpRegex .ReplaceAllString (remoteUrl , "${1}" + forkUsername + "/$3$4" ), nil 
213+ 	case  sshUrlRegex .MatchString (remoteUrl ):
214+ 		return  sshUrlRegex .ReplaceAllString (remoteUrl , "${1}" + forkUsername + "/$3$4" ), nil 
215+ 	case  httpRegex .MatchString (remoteUrl ):
216+ 		return  httpRegex .ReplaceAllString (remoteUrl , "${1}" + forkUsername + "/$3$4" ), nil 
217+ 	default :
218+ 		return  "" , fmt .Errorf ("unsupported or invalid remote URL: %s" , remoteUrl )
236219	}
237- 
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 
262220}
263221
264222func  (self  * RemotesController ) addFork (baseRemote  * models.Remote ) error  {
@@ -269,19 +227,13 @@ func (self *RemotesController) addFork(baseRemote *models.Remote) error {
269227				Title :          self .c .Tr .NewRemoteName ,
270228				InitialContent : forkUsername ,
271229				HandleConfirm : func (remoteName  string ) error  {
272- 					if  forkUsername  ==  ""  {
273- 						return  fmt .Errorf ("Fork username cannot be empty" )
274- 					}
275230					if  len (baseRemote .Urls ) ==  0  {
276- 						return  fmt .Errorf ("Base  remote must have url" )
231+ 						return  fmt .Errorf ("base  remote must have url" )
277232					}
278- 					url  :=  baseRemote .Urls [0 ]
279- 					if  url  ==  ""  {
280- 						return  fmt .Errorf ("Base remote url cannot be empty" )
281- 					}
282- 					remoteUrl , err  :=  replaceForkUsername (url , forkUsername )
233+ 					baseUrl  :=  baseRemote .Urls [0 ]
234+ 					remoteUrl , err  :=  replaceForkUsername (baseUrl , forkUsername )
283235					if  err  !=  nil  {
284- 						return  fmt . Errorf ( "Failed to replace fork username in remote URL: `%w`, make sure it's a valid url" ,  err ) 
236+ 						return  err 
285237					}
286238
287239					return  self .addRemoteHelper (remoteName , remoteUrl )
0 commit comments