@@ -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