@@ -298,16 +298,13 @@ func (r *codeRepo) Latest() (*RevInfo, error) {
298
298
// If statVers is a valid module version, it is used for the Version field.
299
299
// Otherwise, the Version is derived from the passed-in info and recent tags.
300
300
func (r * codeRepo ) convert (info * codehost.RevInfo , statVers string ) (* RevInfo , error ) {
301
- info2 := & RevInfo {
302
- Name : info .Name ,
303
- Short : info .Short ,
304
- Time : info .Time ,
305
- }
306
-
307
301
// If this is a plain tag (no dir/ prefix)
308
302
// and the module path is unversioned,
309
303
// and if the underlying file tree has no go.mod,
310
304
// then allow using the tag with a +incompatible suffix.
305
+ //
306
+ // (If the version is +incompatible, then the go.mod file must not exist:
307
+ // +incompatible is not an ongoing opt-out from semantic import versioning.)
311
308
var canUseIncompatible func () bool
312
309
canUseIncompatible = func () bool {
313
310
var ok bool
@@ -321,19 +318,12 @@ func (r *codeRepo) convert(info *codehost.RevInfo, statVers string) (*RevInfo, e
321
318
return ok
322
319
}
323
320
324
- invalidf := func (format string , args ... any ) error {
325
- return & module.ModuleError {
326
- Path : r .modPath ,
327
- Err : & module.InvalidVersionError {
328
- Version : info2 .Version ,
329
- Err : fmt .Errorf (format , args ... ),
330
- },
331
- }
332
- }
333
-
334
- // checkGoMod verifies that the go.mod file for the module exists or does not
335
- // exist as required by info2.Version and the module path represented by r.
336
- checkGoMod := func () (* RevInfo , error ) {
321
+ // checkCanonical verifies that the canonical version v is compatible with the
322
+ // module path represented by r, adding a "+incompatible" suffix if needed.
323
+ //
324
+ // If statVers is also canonical, checkCanonical also verifies that v is
325
+ // either statVers or statVers with the added "+incompatible" suffix.
326
+ checkCanonical := func (v string ) (* RevInfo , error ) {
337
327
// If r.codeDir is non-empty, then the go.mod file must exist: the module
338
328
// author — not the module consumer, — gets to decide how to carve up the repo
339
329
// into modules.
@@ -344,73 +334,91 @@ func (r *codeRepo) convert(info *codehost.RevInfo, statVers string) (*RevInfo, e
344
334
// r.findDir verifies both of these conditions. Execute it now so that
345
335
// r.Stat will correctly return a notExistError if the go.mod location or
346
336
// declared module path doesn't match.
347
- _ , _ , _ , err := r .findDir (info2 . Version )
337
+ _ , _ , _ , err := r .findDir (v )
348
338
if err != nil {
349
339
// TODO: It would be nice to return an error like "not a module".
350
340
// Right now we return "missing go.mod", which is a little confusing.
351
341
return nil , & module.ModuleError {
352
342
Path : r .modPath ,
353
343
Err : & module.InvalidVersionError {
354
- Version : info2 . Version ,
344
+ Version : v ,
355
345
Err : notExistError {err : err },
356
346
},
357
347
}
358
348
}
359
349
360
- // If the version is +incompatible, then the go.mod file must not exist:
361
- // +incompatible is not an ongoing opt-out from semantic import versioning.
362
- if strings .HasSuffix (info2 .Version , "+incompatible" ) {
363
- if ! canUseIncompatible () {
350
+ invalidf := func (format string , args ... any ) error {
351
+ return & module.ModuleError {
352
+ Path : r .modPath ,
353
+ Err : & module.InvalidVersionError {
354
+ Version : v ,
355
+ Err : fmt .Errorf (format , args ... ),
356
+ },
357
+ }
358
+ }
359
+
360
+ // Add the +incompatible suffix if needed or requested explicitly, and
361
+ // verify that its presence or absence is appropriate for this version
362
+ // (which depends on whether it has an explicit go.mod file).
363
+
364
+ if v == strings .TrimSuffix (statVers , "+incompatible" ) {
365
+ v = statVers
366
+ }
367
+ base := strings .TrimSuffix (v , "+incompatible" )
368
+ var errIncompatible error
369
+ if ! module .MatchPathMajor (base , r .pathMajor ) {
370
+ if canUseIncompatible () {
371
+ v = base + "+incompatible"
372
+ } else {
364
373
if r .pathMajor != "" {
365
- return nil , invalidf ("+incompatible suffix not allowed: module path includes a major version suffix, so major version must match" )
374
+ errIncompatible = invalidf ("module path includes a major version suffix, so major version must match" )
366
375
} else {
367
- return nil , invalidf ("+incompatible suffix not allowed: module contains a go.mod file, so semantic import versioning is required" )
376
+ errIncompatible = invalidf ("module contains a go.mod file, so module path must match major version (%q)" , path . Join ( r . pathPrefix , semver . Major ( v )) )
368
377
}
369
378
}
379
+ } else if strings .HasSuffix (v , "+incompatible" ) {
380
+ errIncompatible = invalidf ("+incompatible suffix not allowed: major version %s is compatible" , semver .Major (v ))
381
+ }
370
382
371
- if err := module .CheckPathMajor (strings .TrimSuffix (info2 .Version , "+incompatible" ), r .pathMajor ); err == nil {
372
- return nil , invalidf ("+incompatible suffix not allowed: major version %s is compatible" , semver .Major (info2 .Version ))
383
+ if statVers != "" && statVers == module .CanonicalVersion (statVers ) {
384
+ // Since the caller-requested version is canonical, it would be very
385
+ // confusing to resolve it to anything but itself, possibly with a
386
+ // "+incompatible" suffix. Error out explicitly.
387
+ if statBase := strings .TrimSuffix (statVers , "+incompatible" ); statBase != base {
388
+ return nil , & module.ModuleError {
389
+ Path : r .modPath ,
390
+ Err : & module.InvalidVersionError {
391
+ Version : statVers ,
392
+ Err : fmt .Errorf ("resolves to version %v (%s is not a tag)" , v , statBase ),
393
+ },
394
+ }
373
395
}
374
396
}
375
397
376
- return info2 , nil
398
+ if errIncompatible != nil {
399
+ return nil , errIncompatible
400
+ }
401
+
402
+ return & RevInfo {
403
+ Name : info .Name ,
404
+ Short : info .Short ,
405
+ Time : info .Time ,
406
+ Version : v ,
407
+ }, nil
377
408
}
378
409
379
410
// Determine version.
380
- //
381
- // If statVers is canonical, then the original call was repo.Stat(statVers).
382
- // Since the version is canonical, we must not resolve it to anything but
383
- // itself, possibly with a '+incompatible' annotation: we do not need to do
384
- // the work required to look for an arbitrary pseudo-version.
385
- if statVers != "" && statVers == module .CanonicalVersion (statVers ) {
386
- info2 .Version = statVers
387
-
388
- if module .IsPseudoVersion (info2 .Version ) {
389
- if err := r .validatePseudoVersion (info , info2 .Version ); err != nil {
390
- return nil , err
391
- }
392
- return checkGoMod ()
393
- }
394
411
395
- if err := module .CheckPathMajor (info2 .Version , r .pathMajor ); err != nil {
396
- if canUseIncompatible () {
397
- info2 .Version += "+incompatible"
398
- return checkGoMod ()
399
- } else {
400
- if vErr , ok := err .(* module.InvalidVersionError ); ok {
401
- // We're going to describe why the version is invalid in more detail,
402
- // so strip out the existing “invalid version” wrapper.
403
- err = vErr .Err
404
- }
405
- return nil , invalidf ("module contains a go.mod file, so major version must be compatible: %v" , err )
406
- }
412
+ if module .IsPseudoVersion (statVers ) {
413
+ if err := r .validatePseudoVersion (info , statVers ); err != nil {
414
+ return nil , err
407
415
}
408
-
409
- return checkGoMod ()
416
+ return checkCanonical (statVers )
410
417
}
411
418
412
- // statVers is empty or non-canonical, so we need to resolve it to a canonical
413
- // version or pseudo-version.
419
+ // statVers is not a pseudo-version, so we need to either resolve it to a
420
+ // canonical version or verify that it is already a canonical tag
421
+ // (not a branch).
414
422
415
423
// Derive or verify a version from a code repo tag.
416
424
// Tag must have a prefix matching codeDir.
@@ -441,71 +449,62 @@ func (r *codeRepo) convert(info *codehost.RevInfo, statVers string) (*RevInfo, e
441
449
if v == "" || ! strings .HasPrefix (trimmed , v ) {
442
450
return "" , false // Invalid or incomplete version (just vX or vX.Y).
443
451
}
444
- if isRetracted (v ) {
445
- return "" , false
446
- }
447
452
if v == trimmed {
448
453
tagIsCanonical = true
449
454
}
450
-
451
- if err := module .CheckPathMajor (v , r .pathMajor ); err != nil {
452
- if canUseIncompatible () {
453
- return v + "+incompatible" , tagIsCanonical
454
- }
455
- return "" , false
456
- }
457
-
458
455
return v , tagIsCanonical
459
456
}
460
457
461
458
// If the VCS gave us a valid version, use that.
462
459
if v , tagIsCanonical := tagToVersion (info .Version ); tagIsCanonical {
463
- info2 .Version = v
464
- return checkGoMod ()
460
+ if info , err := checkCanonical (v ); err == nil {
461
+ return info , err
462
+ }
465
463
}
466
464
467
465
// Look through the tags on the revision for either a usable canonical version
468
466
// or an appropriate base for a pseudo-version.
469
- var pseudoBase string
467
+ var (
468
+ highestCanonical string
469
+ pseudoBase string
470
+ )
470
471
for _ , pathTag := range info .Tags {
471
472
v , tagIsCanonical := tagToVersion (pathTag )
472
- if tagIsCanonical {
473
- if statVers != "" && semver .Compare (v , statVers ) == 0 {
474
- // The user requested a non-canonical version, but the tag for the
475
- // canonical equivalent refers to the same revision. Use it.
476
- info2 .Version = v
477
- return checkGoMod ()
473
+ if statVers != "" && semver .Compare (v , statVers ) == 0 {
474
+ // The tag is equivalent to the version requested by the user.
475
+ if tagIsCanonical {
476
+ // This tag is the canonical form of the requested version,
477
+ // not some other form with extra build metadata.
478
+ // Use this tag so that the resolved version will match exactly.
479
+ // (If it isn't actually allowed, we'll error out in checkCanonical.)
480
+ return checkCanonical (v )
478
481
} else {
479
- // Save the highest canonical tag for the revision. If we don't find a
480
- // better match, we'll use it as the canonical version.
482
+ // The user explicitly requested something equivalent to this tag. We
483
+ // can't use the version from the tag directly: since the tag is not
484
+ // canonical, it could be ambiguous. For example, tags v0.0.1+a and
485
+ // v0.0.1+b might both exist and refer to different revisions.
481
486
//
482
- // NOTE: Do not replace this with semver.Max. Despite the name,
483
- // semver.Max *also* canonicalizes its arguments, which uses
484
- // semver.Canonical instead of module.CanonicalVersion and thereby
485
- // strips our "+incompatible" suffix.
486
- if semver .Compare (info2 .Version , v ) < 0 {
487
- info2 .Version = v
488
- }
487
+ // The tag is otherwise valid for the module, so we can at least use it as
488
+ // the base of an unambiguous pseudo-version.
489
+ //
490
+ // If multiple tags match, tagToVersion will canonicalize them to the same
491
+ // base version.
492
+ pseudoBase = v
493
+ }
494
+ }
495
+ // Save the highest non-retracted canonical tag for the revision.
496
+ // If we don't find a better match, we'll use it as the canonical version.
497
+ if tagIsCanonical && semver .Compare (highestCanonical , v ) < 0 && ! isRetracted (v ) {
498
+ if module .MatchPathMajor (v , r .pathMajor ) || canUseIncompatible () {
499
+ highestCanonical = v
489
500
}
490
- } else if v != "" && semver .Compare (v , statVers ) == 0 {
491
- // The user explicitly requested something equivalent to this tag. We
492
- // can't use the version from the tag directly: since the tag is not
493
- // canonical, it could be ambiguous. For example, tags v0.0.1+a and
494
- // v0.0.1+b might both exist and refer to different revisions.
495
- //
496
- // The tag is otherwise valid for the module, so we can at least use it as
497
- // the base of an unambiguous pseudo-version.
498
- //
499
- // If multiple tags match, tagToVersion will canonicalize them to the same
500
- // base version.
501
- pseudoBase = v
502
501
}
503
502
}
504
503
505
- // If we found any canonical tag for the revision, return it.
504
+ // If we found a valid canonical tag for the revision, return it.
506
505
// Even if we found a good pseudo-version base, a canonical version is better.
507
- if info2 . Version != "" {
508
- return checkGoMod ( )
506
+ if highestCanonical != "" {
507
+ return checkCanonical ( highestCanonical )
509
508
}
510
509
511
510
// Find the highest tagged version in the revision's history, subject to
@@ -528,11 +527,10 @@ func (r *codeRepo) convert(info *codehost.RevInfo, statVers string) (*RevInfo, e
528
527
tag , _ = r .code .RecentTag (info .Name , tagPrefix , allowedMajor ("v0" ))
529
528
}
530
529
}
531
- pseudoBase , _ = tagToVersion (tag ) // empty if the tag is invalid
530
+ pseudoBase , _ = tagToVersion (tag )
532
531
}
533
532
534
- info2 .Version = module .PseudoVersion (r .pseudoMajor , pseudoBase , info .Time , info .Short )
535
- return checkGoMod ()
533
+ return checkCanonical (module .PseudoVersion (r .pseudoMajor , pseudoBase , info .Time , info .Short ))
536
534
}
537
535
538
536
// validatePseudoVersion checks that version has a major version compatible with
@@ -556,10 +554,6 @@ func (r *codeRepo) validatePseudoVersion(info *codehost.RevInfo, version string)
556
554
}
557
555
}()
558
556
559
- if err := module .CheckPathMajor (version , r .pathMajor ); err != nil {
560
- return err
561
- }
562
-
563
557
rev , err := module .PseudoVersionRev (version )
564
558
if err != nil {
565
559
return err
0 commit comments