@@ -16,6 +16,8 @@ import (
1616	"strings" 
1717)
1818
19+ const  defaultFilename  =  ".lima.yaml" 
20+ 
1921// transformGitHubURL transforms a github: URL to a raw.githubusercontent.com URL. 
2022// Input format: ORG/REPO[/PATH][@BRANCH] 
2123// 
@@ -25,12 +27,7 @@ import (
2527// If PATH is just a directory (trailing slash), it will be set to .lima.yaml 
2628// IF FILE is .lima.yaml and contents looks like a symlink, it will be replaced by the symlink target. 
2729func  transformGitHubURL (ctx  context.Context , input  string ) (string , error ) {
28- 	// Check for explicit branch specification with @ at the end 
29- 	var  branch  string 
30- 	if  idx  :=  strings .LastIndex (input , "@" ); idx  !=  - 1  {
31- 		branch  =  input [idx + 1 :]
32- 		input  =  input [:idx ]
33- 	}
30+ 	input , origBranch , _  :=  strings .Cut (input , "@" )
3431
3532	parts  :=  strings .Split (input , "/" )
3633	for  len (parts ) <  2  {
@@ -44,24 +41,25 @@ func transformGitHubURL(ctx context.Context, input string) (string, error) {
4441
4542	// If REPO is omitted (github:ORG or github:ORG//PATH), default it to ORG 
4643	repo  :=  cmp .Or (parts [1 ], org )
47- 	pathPart  :=  strings .Join (parts [2 :], "/" )
44+ 	filePath  :=  strings .Join (parts [2 :], "/" )
4845
49- 	if  pathPart  ==  ""  {
50- 		pathPart  =  ".lima.yaml" 
46+ 	if  filePath  ==  ""  {
47+ 		filePath  =  defaultFilename 
5148	} else  {
5249		// If path ends with /, it's a directory, so append .lima 
53- 		if  strings .HasSuffix (pathPart , "/" ) {
54- 			pathPart  +=  ".lima" 
50+ 		if  strings .HasSuffix (filePath , "/" ) {
51+ 			filePath  +=  defaultFilename 
5552		}
5653
5754		// If the filename (excluding first char for hidden files) has no extension, add .yaml 
58- 		filename  :=  path .Base (pathPart )
55+ 		filename  :=  path .Base (filePath )
5956		if  ! strings .Contains (filename [1 :], "." ) {
60- 			pathPart  +=  ".yaml" 
57+ 			filePath  +=  ".yaml" 
6158		}
6259	}
6360
6461	// Query default branch if no branch was specified 
62+ 	branch  :=  origBranch 
6563	if  branch  ==  ""  {
6664		var  err  error 
6765		branch , err  =  getGitHubDefaultBranch (ctx , org , repo )
@@ -71,13 +69,24 @@ func transformGitHubURL(ctx context.Context, input string) (string, error) {
7169	}
7270
7371	// If filename is .lima.yaml, check if it's a symlink/redirect to another file 
74- 	if  path .Base (pathPart ) ==  ".lima.yaml"  {
75- 		if  redirectPath , err  :=  resolveGitHubSymlink (ctx , org , repo , branch , pathPart ); err  ==  nil  {
76- 			pathPart  =  redirectPath 
77- 		}
72+ 	if  path .Base (filePath ) ==  defaultFilename  {
73+ 		return  resolveGitHubSymlink (ctx , org , repo , branch , filePath , origBranch )
7874	}
75+ 	return  githubUserContentURL (org , repo , branch , filePath ), nil 
76+ }
77+ 
78+ func  githubUserContentURL (org , repo , branch , filePath  string ) string  {
79+ 	return  fmt .Sprintf ("https://raw.githubusercontent.com/%s/%s/%s/%s" , org , repo , branch , filePath )
80+ }
7981
80- 	return  fmt .Sprintf ("https://raw.githubusercontent.com/%s/%s/%s/%s" , org , repo , branch , pathPart ), nil 
82+ func  getGitHubUserContent (ctx  context.Context , org , repo , branch , filePath  string ) (* http.Response , error ) {
83+ 	url  :=  githubUserContentURL (org , repo , branch , filePath )
84+ 	req , err  :=  http .NewRequestWithContext (ctx , http .MethodGet , url , http .NoBody )
85+ 	if  err  !=  nil  {
86+ 		return  nil , fmt .Errorf ("failed to create request: %w" , err )
87+ 	}
88+ 	req .Header .Set ("User-Agent" , "lima" )
89+ 	return  http .DefaultClient .Do (req )
8190}
8291
8392// getGitHubDefaultBranch queries the GitHub API to get the default branch for a repository. 
@@ -129,40 +138,79 @@ func getGitHubDefaultBranch(ctx context.Context, org, repo string) (string, erro
129138}
130139
131140// resolveGitHubSymlink checks if a file at the given path is a symlink/redirect to another file. 
132- // If the file contains a single line without YAML content, it's treated as a path to the actual file. 
133- // Returns the redirect path if found, or the original path otherwise. 
134- func  resolveGitHubSymlink (ctx  context.Context , org , repo , branch , filePath  string ) (string , error ) {
135- 	url  :=  fmt .Sprintf ("https://raw.githubusercontent.com/%s/%s/%s/%s" , org , repo , branch , filePath )
136- 
137- 	req , err  :=  http .NewRequestWithContext (ctx , http .MethodGet , url , http .NoBody )
138- 	if  err  !=  nil  {
139- 		return  "" , fmt .Errorf ("failed to create request: %w" , err )
140- 	}
141- 
142- 	req .Header .Set ("User-Agent" , "lima" )
143- 
144- 	resp , err  :=  http .DefaultClient .Do (req )
141+ // If the file contains a single line without newline, space, or colon then it's treated as a path to the actual file. 
142+ // Returns a URL to the redirect path if found, or a URL for original path otherwise. 
143+ func  resolveGitHubSymlink (ctx  context.Context , org , repo , branch , filePath , origBranch  string ) (string , error ) {
144+ 	resp , err  :=  getGitHubUserContent (ctx , org , repo , branch , filePath )
145145	if  err  !=  nil  {
146146		return  "" , fmt .Errorf ("failed to fetch file: %w" , err )
147147	}
148148	defer  resp .Body .Close ()
149149
150+ 	// Special rule for branch/tag propagation for github:ORG// requests. 
151+ 	if  resp .StatusCode  ==  http .StatusNotFound  &&  repo  ==  org  {
152+ 		defaultBranch , err  :=  getGitHubDefaultBranch (ctx , org , repo )
153+ 		if  err  ==  nil  {
154+ 			return  resolveGitHubRedirect (ctx , org , repo , defaultBranch , filePath , branch )
155+ 		}
156+ 	}
150157	if  resp .StatusCode  !=  http .StatusOK  {
151- 		return  "" , fmt .Errorf ("file not found or inaccessible: status %d" , resp .StatusCode )
158+ 		return  "" , fmt .Errorf ("file %q  not found or inaccessible: status %d" ,  resp . Request . URL , resp .StatusCode )
152159	}
153160
154161	// Read first 1KB to check the file content 
155162	buf  :=  make ([]byte , 1024 )
156163	n , err  :=  resp .Body .Read (buf )
157164	if  err  !=  nil  &&  ! errors .Is (err , io .EOF ) {
158- 		return  "" , fmt .Errorf ("failed to read file  content: %w" , err )
165+ 		return  "" , fmt .Errorf ("failed to read %q  content: %w" ,  resp . Request . URL , err )
159166	}
160167	content  :=  string (buf [:n ])
161168
169+ 	// Symlink can also be a github: redirect if we are in a github:ORG// repo. 
170+ 	if  repo  ==  org  &&  strings .HasPrefix (content , "github:" ) {
171+ 		return  validateGitHubRedirect (content , org , origBranch , resp .Request .URL .String ())
172+ 	}
173+ 
162174	// A symlink must be a single line (without trailing newline), no spaces, no colons 
163175	if  ! (content  ==  ""  ||  strings .ContainsAny (content , "\n  :" )) {
164176		// symlink is relative to the directory of filePath 
165- 		return  path .Join (path .Dir (filePath ), content ), nil 
177+ 		filePath  =  path .Join (path .Dir (filePath ), content )
178+ 	}
179+ 	return  githubUserContentURL (org , repo , branch , filePath ), nil 
180+ }
181+ 
182+ // resolveGitHubRedirect checks if a file at the given path is a github: URL to another file within the same repo. 
183+ // Returns the URL, or an error if the file doesn't exist, or doesn't start with github:ORG. 
184+ func  resolveGitHubRedirect (ctx  context.Context , org , repo , defaultBranch , filePath , origBranch  string ) (string , error ) {
185+ 	// Refetch the filepath from the defaultBranch 
186+ 	resp , err  :=  getGitHubUserContent (ctx , org , repo , defaultBranch , filePath )
187+ 	if  err  !=  nil  {
188+ 		return  "" , fmt .Errorf ("failed to fetch file: %w" , err )
189+ 	}
190+ 	defer  resp .Body .Close ()
191+ 	if  resp .StatusCode  !=  http .StatusOK  {
192+ 		return  "" , fmt .Errorf ("file %q not found or inaccessible: status %d" , resp .Request .URL , resp .StatusCode )
193+ 	}
194+ 	body , err  :=  io .ReadAll (resp .Body )
195+ 	if  err  !=  nil  {
196+ 		return  "" , fmt .Errorf ("failed to read %q content: %w" , resp .Request .URL , err )
197+ 	}
198+ 	return  validateGitHubRedirect (string (body ), org , origBranch , resp .Request .URL .String ())
199+ }
200+ 
201+ func  validateGitHubRedirect (body , org , origBranch , url  string ) (string , error ) {
202+ 	redirect , _ , _  :=  strings .Cut (body , "\n " )
203+ 	redirect  =  strings .TrimSpace (redirect )
204+ 
205+ 	if  ! strings .HasPrefix (redirect , "github:" + org + "/" ) {
206+ 		return  "" , fmt .Errorf (`redirect %q is not a "github:%s" URL (from %q)` , redirect , org , url )
207+ 	}
208+ 	if  strings .ContainsRune (redirect , '@' ) {
209+ 		return  "" , fmt .Errorf ("redirect %q must not include a branch/tag/sha (from %q)" , redirect , url )
210+ 	}
211+ 	// If the origBranch is empty, then we need to look up the default branch in the redirect 
212+ 	if  origBranch  !=  ""  {
213+ 		redirect  +=  "@"  +  origBranch 
166214	}
167- 	return  filePath , nil 
215+ 	return  redirect , nil 
168216}
0 commit comments