@@ -16,6 +16,8 @@ import (
16
16
"strings"
17
17
)
18
18
19
+ const defaultFilename = ".lima.yaml"
20
+
19
21
// transformGitHubURL transforms a github: URL to a raw.githubusercontent.com URL.
20
22
// Input format: ORG/REPO[/PATH][@BRANCH]
21
23
//
@@ -25,12 +27,7 @@ import (
25
27
// If PATH is just a directory (trailing slash), it will be set to .lima.yaml
26
28
// IF FILE is .lima.yaml and contents looks like a symlink, it will be replaced by the symlink target.
27
29
func 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 , "@" )
34
31
35
32
parts := strings .Split (input , "/" )
36
33
for len (parts ) < 2 {
@@ -44,24 +41,25 @@ func transformGitHubURL(ctx context.Context, input string) (string, error) {
44
41
45
42
// If REPO is omitted (github:ORG or github:ORG//PATH), default it to ORG
46
43
repo := cmp .Or (parts [1 ], org )
47
- pathPart := strings .Join (parts [2 :], "/" )
44
+ filePath := strings .Join (parts [2 :], "/" )
48
45
49
- if pathPart == "" {
50
- pathPart = ".lima.yaml"
46
+ if filePath == "" {
47
+ filePath = defaultFilename
51
48
} else {
52
49
// 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
55
52
}
56
53
57
54
// If the filename (excluding first char for hidden files) has no extension, add .yaml
58
- filename := path .Base (pathPart )
55
+ filename := path .Base (filePath )
59
56
if ! strings .Contains (filename [1 :], "." ) {
60
- pathPart += ".yaml"
57
+ filePath += ".yaml"
61
58
}
62
59
}
63
60
64
61
// Query default branch if no branch was specified
62
+ branch := origBranch
65
63
if branch == "" {
66
64
var err error
67
65
branch , err = getGitHubDefaultBranch (ctx , org , repo )
@@ -71,13 +69,24 @@ func transformGitHubURL(ctx context.Context, input string) (string, error) {
71
69
}
72
70
73
71
// 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 )
78
74
}
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
+ }
79
81
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 )
81
90
}
82
91
83
92
// 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
129
138
}
130
139
131
140
// 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 )
145
145
if err != nil {
146
146
return "" , fmt .Errorf ("failed to fetch file: %w" , err )
147
147
}
148
148
defer resp .Body .Close ()
149
149
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
+ }
150
157
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 )
152
159
}
153
160
154
161
// Read first 1KB to check the file content
155
162
buf := make ([]byte , 1024 )
156
163
n , err := resp .Body .Read (buf )
157
164
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 )
159
166
}
160
167
content := string (buf [:n ])
161
168
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
+
162
174
// A symlink must be a single line (without trailing newline), no spaces, no colons
163
175
if ! (content == "" || strings .ContainsAny (content , "\n :" )) {
164
176
// 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
166
214
}
167
- return filePath , nil
215
+ return redirect , nil
168
216
}
0 commit comments