@@ -3,7 +3,6 @@ package flake
3
3
4
4
import (
5
5
"cmp"
6
- "fmt"
7
6
"net/url"
8
7
"path"
9
8
"slices"
@@ -88,6 +87,47 @@ type Ref struct {
88
87
Port int32 `json:port,omitempty`
89
88
}
90
89
90
+ // ParseRef parses a raw flake reference. Nix supports a variety of flake ref
91
+ // formats, and isn't entirely consistent about how it parses them. ParseRef
92
+ // attempts to mimic how Nix parses flake refs on the command line. The raw ref
93
+ // can be one of the following:
94
+ //
95
+ // - Indirect reference such as "nixpkgs" or "nixpkgs/unstable".
96
+ // - Path-like reference such as "./flake" or "/path/to/flake". They must
97
+ // start with a '.' or '/' and not contain a '#' or '?'.
98
+ // - URL-like reference which must be a valid URL with any special characters
99
+ // encoded. The scheme can be any valid flake ref type except for mercurial,
100
+ // gitlab, and sourcehut.
101
+ //
102
+ // ParseRef does not guarantee that a parsed flake ref is valid or that an
103
+ // error indicates an invalid flake ref. Use the "nix flake metadata" command or
104
+ // the builtins.parseFlakeRef Nix function to validate a flake ref.
105
+ func ParseRef (ref string ) (Ref , error ) {
106
+ if ref == "" {
107
+ return Ref {}, redact .Errorf ("empty flake reference" )
108
+ }
109
+
110
+ // Handle path-style references first.
111
+ parsed := Ref {}
112
+ if ref [0 ] == '.' || ref [0 ] == '/' {
113
+ if strings .ContainsAny (ref , "?#" ) {
114
+ // The Nix CLI does seem to allow paths with a '?'
115
+ // (contrary to the manual) but ignores everything that
116
+ // comes after it. This is a bit surprising, so we just
117
+ // don't allow it at all.
118
+ return Ref {}, redact .Errorf ("path-style flake reference %q contains a '?' or '#'" , ref )
119
+ }
120
+ parsed .Type = TypePath
121
+ parsed .Path = ref
122
+ return parsed , nil
123
+ }
124
+ parsed , fragment , err := parseURLRef (ref )
125
+ if fragment != "" {
126
+ return Ref {}, redact .Errorf ("flake reference %q contains a URL fragment" , ref )
127
+ }
128
+ return parsed , err
129
+ }
130
+
91
131
func parseURLRef (ref string ) (parsed Ref , fragment string , err error ) {
92
132
// A good way to test how Nix parses a flake reference is to run:
93
133
//
@@ -196,6 +236,7 @@ func parseURLRef(ref string) (parsed Ref, fragment string, err error) {
196
236
refURL .Scheme = refURL .Scheme [5 :] // remove file+
197
237
parsed .URL = refURL .String ()
198
238
case "git" , "git+http" , "git+https" , "git+ssh" , "git+git" , "git+file" :
239
+ parsed .Type = TypeGit
199
240
query := refURL .Query ()
200
241
parsed .Dir = query .Get ("dir" )
201
242
parsed .Ref = query .Get ("ref" )
@@ -206,49 +247,25 @@ func parseURLRef(ref string) (parsed Ref, fragment string, err error) {
206
247
query .Del ("ref" )
207
248
query .Del ("rev" )
208
249
refURL .RawQuery = query .Encode ()
209
-
210
250
if len (refURL .Scheme ) > 3 {
211
251
refURL .Scheme = refURL .Scheme [4 :] // remove git+
212
252
}
213
-
214
- if strings .HasPrefix (refURL .Scheme , TypeSSH ) {
215
- parsed .Type = TypeSSH
216
- } else if strings .HasPrefix (refURL .Scheme , TypeFile ) {
217
- parsed .Type = TypeFile
218
- } else {
219
- parsed .Type = TypeGit
220
- }
221
-
222
253
parsed .URL = refURL .String ()
223
-
224
- if err := parseGitRef (refURL , & parsed ); err != nil {
225
- return Ref {}, "" , err
226
- }
227
- case "bitbucket" :
228
- parsed .Type = TypeBitBucket
229
- if err := parseGitRef (refURL , & parsed ); err != nil {
230
- return Ref {}, "" , err
231
- }
232
- case "gitlab" :
233
- parsed .Type = TypeGitLab
234
- if err := parseGitRef (refURL , & parsed ); err != nil {
235
- return Ref {}, "" , err
236
- }
237
254
case "github" :
238
- parsed .Type = TypeGitHub
239
- if err := parseGitRef (refURL , & parsed ); err != nil {
255
+ if err := parseGitHubRef (refURL , & parsed ); err != nil {
240
256
return Ref {}, "" , err
241
257
}
242
258
default :
243
259
return Ref {}, "" , redact .Errorf ("unsupported flake reference URL scheme: %s" , redact .Safe (refURL .Scheme ))
244
260
}
245
-
246
261
return parsed , fragment , nil
247
262
}
248
263
249
- func parseGitRef (refURL * url.URL , parsed * Ref ) error {
264
+ func parseGitHubRef (refURL * url.URL , parsed * Ref ) error {
250
265
// github:<owner>/<repo>(/<rev-or-ref>)?(\?<params>)?
251
266
267
+ parsed .Type = TypeGitHub
268
+
252
269
// Only split up to 3 times (owner, repo, ref/rev) so that we handle
253
270
// refs that have slashes in them. For example,
254
271
// "github:jetify-com/devbox/gcurtis/flakeref" parses as "gcurtis/flakeref".
@@ -268,7 +285,6 @@ func parseGitRef(refURL *url.URL, parsed *Ref) error {
268
285
269
286
parsed .Host = refURL .Query ().Get ("host" )
270
287
parsed .Dir = refURL .Query ().Get ("dir" )
271
-
272
288
if qRef := refURL .Query ().Get ("ref" ); qRef != "" {
273
289
if parsed .Rev != "" {
274
290
return redact .Errorf ("github flake reference has a ref and a rev" )
@@ -342,60 +358,60 @@ func (r Ref) Locked() bool {
342
358
// string.
343
359
func (r Ref ) String () string {
344
360
switch r .Type {
345
-
346
361
case TypeFile :
347
362
if r .URL == "" {
348
363
return ""
349
364
}
350
- return "file+" + r .URL
351
- case TypeSSH :
352
- base := fmt .Sprintf ("git+ssh://git@%s" , r .Host )
353
- if r .Port > 0 {
354
- base = fmt .Sprintf ("%s:%d" , base , r .Port )
355
- }
356
365
357
- queryParams := url.Values {}
358
-
359
- if r .Rev != "" {
360
- queryParams .Add ("rev" , r .Rev )
366
+ url , err := url .Parse ("file+" + r .URL )
367
+ if err != nil {
368
+ // This should be rare and only happen if the caller
369
+ // messed with the parsed URL.
370
+ return ""
361
371
}
362
- if r .Ref != "" {
363
- queryParams .Add ("ref" , r .Ref )
372
+ url .RawQuery = appendQueryString (url .Query (),
373
+ "lastModified" , itoaOmitZero (r .LastModified ),
374
+ "narHash" , r .NARHash ,
375
+ )
376
+ return url .String ()
377
+ case TypeGit :
378
+ if r .URL == "" {
379
+ return ""
364
380
}
365
-
366
- if r .Dir != "" {
367
- queryParams .Add ("dir" , r .Dir )
381
+ if ! strings .HasPrefix (r .URL , "git" ) {
382
+ r .URL = "git+" + r .URL
368
383
}
369
384
370
- return fmt .Sprintf ("%s/%s/%s?%s" , base , r .Owner , r .Repo , queryParams .Encode ())
371
-
372
- case TypeGitLab , TypeBitBucket , TypeGitHub :
373
- if r .Owner == "" || r .Repo == "" {
374
- return ""
385
+ // Nix removes "ref" and "rev" from the query string
386
+ // (but not other parameters) after parsing. If they're empty,
387
+ // we can skip parsing the URL. Otherwise, we need to add them
388
+ // back.
389
+ if r .Ref == "" && r .Rev == "" {
390
+ return r .URL
375
391
}
376
-
377
- scheme := "github" // using as default
378
- if r .Type == TypeGitLab {
379
- scheme = "gitlab"
392
+ url , err := url .Parse (r .URL )
393
+ if err != nil {
394
+ // This should be rare and only happen if the caller
395
+ // messed with the parsed URL.
396
+ return ""
380
397
}
381
- if r .Type == TypeBitBucket {
382
- scheme = "bitbucket"
398
+ url .RawQuery = appendQueryString (url .Query (), "ref" , r .Ref , "rev" , r .Rev , "dir" , r .Dir )
399
+ return url .String ()
400
+ case TypeGitHub :
401
+ if r .Owner == "" || r .Repo == "" {
402
+ return ""
383
403
}
384
-
385
404
url := & url.URL {
386
- Scheme : scheme ,
387
- Opaque : buildEscapedPath (r .Owner , r .Repo , r .Rev , r .Ref ),
388
- //RawQuery: buildQueryString("host", r.Host, "dir", r.Dir),
405
+ Scheme : "github" ,
406
+ Opaque : buildEscapedPath (r .Owner , r .Repo , cmp .Or (r .Rev , r .Ref )),
389
407
RawQuery : appendQueryString (nil ,
390
408
"host" , r .Host ,
391
409
"dir" , r .Dir ,
392
410
"lastModified" , itoaOmitZero (r .LastModified ),
393
411
"narHash" , r .NARHash ,
394
412
),
395
413
}
396
-
397
414
return url .String ()
398
-
399
415
case TypeIndirect :
400
416
if r .ID == "" {
401
417
return ""
@@ -660,7 +676,13 @@ func ParseInstallable(raw string) (Installable, error) {
660
676
661
677
// Interpret installables with path-style flake refs as URLs to extract
662
678
// the attribute path (fragment). This means that path-style flake refs
663
- // cannot point to files with a '#' or '?' in their name, since those
679
+ //
680
+ //
681
+ //
682
+ //
683
+ //
684
+ //
685
+ //// cannot point to files with a '#' or '?' in their name, since those
664
686
// would be parsed as the URL fragment or query string. This mimic's
665
687
// Nix's CLI behavior.
666
688
if raw [0 ] == '.' || raw [0 ] == '/' {
0 commit comments