@@ -115,8 +115,8 @@ func (repo *Repo) GopkgVersionRoot(version Version) string { | |||
} | |||
} | |||
var patternOld = regexp.MustCompile(`^/(?:([a-z0-9][-a-z0-9]+)/)?((?:v0|v[1-9][0-9]*)(?:\.0|\.[1-9][0-9]*){0,2})/([a-zA-Z][-a-zA-Z0-9]*)(?:\.git)?((?:/[a-zA-Z][-a-zA-Z0-9]*)*)$`) | |||
var patternNew = regexp.MustCompile(`^/(?:([a-zA-Z0-9][-a-zA-Z0-9]+)/)?([a-zA-Z][-.a-zA-Z0-9]*)\.((?:v0|v[1-9][0-9]*)(?:\.0|\.[1-9][0-9]*){0,2})(?:\.git)?((?:/[a-zA-Z0-9][-.a-zA-Z0-9]*)*)$`) | |||
var patternOld = regexp.MustCompile(`^/(?:([a-z0-9][-a-z0-9]+)/)?((?:v0|v[1-9][0-9]*)(?:\.0|\.[1-9][0-9]*){0,2}(-unstable)?)/([a-zA-Z][-a-zA-Z0-9]*)(?:\.git)?((?:/[a-zA-Z][-a-zA-Z0-9]*)*)$`) | |||
var patternNew = regexp.MustCompile(`^/(?:([a-zA-Z0-9][-a-zA-Z0-9]+)/)?([a-zA-Z][-.a-zA-Z0-9]*)\.((?:v0|v[1-9][0-9]*)(?:\.0|\.[1-9][0-9]*){0,2}(-unstable)?)(?:\.git)?((?:/[a-zA-Z0-9][-.a-zA-Z0-9]*)*)$`) | |||
func handler(resp http.ResponseWriter, req *http.Request) { | |||
if req.URL.Path == "/health-check" { | |||
@@ -140,6 +140,7 @@ func handler(resp http.ResponseWriter, req *http.Request) { | |||
sendNotFound(resp, "Unsupported URL pattern; see the documentation at gopkg.in for details.") | |||
return | |||
} | |||
// "/v2/name" <= "/name.v2" | |||
m[2], m[3] = m[3], m[2] | |||
oldFormat = true | |||
} | |||
@@ -153,7 +154,7 @@ func handler(resp http.ResponseWriter, req *http.Request) { | |||
repo := &Repo{ | |||
User: m[1], | |||
Name: m[2], | |||
SubPath: m[4], | |||
SubPath: m[5], | |||
OldFormat: oldFormat, | |||
} | |||
@@ -174,8 +175,14 @@ func handler(resp http.ResponseWriter, req *http.Request) { | |||
sendNotFound(resp, "GitHub repository not found at https://%s", repo.GitHubRoot()) | |||
return | |||
case ErrNoVersion: | |||
v := repo.MajorVersion.String() | |||
sendNotFound(resp, `GitHub repository at https://%s has no branch or tag "%s", "%s.N" or "%s.N.M"`, repo.GitHubRoot(), v, v, v) | |||
major := repo.MajorVersion | |||
suffix := "" | |||
if major.Unstable { | |||
major.Unstable = false | |||
suffix = unstableSuffix | |||
} | |||
v := major.String() | |||
sendNotFound(resp, `GitHub repository at https://%s has no branch or tag "%s%s", "%s.N%s" or "%s.N.M%s"`, repo.GitHubRoot(), v, suffix, v, suffix, v, suffix) | |||
return | |||
default: | |||
resp.WriteHeader(http.StatusBadGateway) | |||
@@ -306,7 +313,7 @@ func hackedRefs(repo *Repo) (data []byte, versions []Version, err error) { | |||
} | |||
// If there were absolutely no versions, and v0 was requested, accept the master as-is. | |||
if len(versions) == 0 && repo.MajorVersion == (Version{0, -1, -1}) { | |||
if len(versions) == 0 && repo.MajorVersion == (Version{0, -1, -1, false}) { | |||
return data, nil, nil | |||
} | |||
@@ -124,6 +124,11 @@ const packageTemplateString = `<!DOCTYPE html> | |||
</div> | |||
</div> | |||
</div> | |||
{{ if .Repo.MajorVersion.Unstable }} | |||
<div class="col-sm-12 alert alert-danger"> | |||
This is an <b><i>unstable</i></b> package and should <i>not</i> be used in released code. | |||
</div> | |||
{{ end }} | |||
<div class="row" > | |||
<div class="col-sm-12" > | |||
<a class="btn btn-lg btn-info" href="https://{{.Repo.GitHubRoot}}/tree/{{if .Repo.AllVersions}}{{.FullVersion}}{{else}}master{{end}}{{.Repo.SubPath}}" ><i class="fa fa-github"></i> Source Code</a> | |||
@@ -153,7 +158,7 @@ const packageTemplateString = `<!DOCTYPE html> | |||
{{ if .LatestVersions }} | |||
{{ range .LatestVersions }} | |||
<div> | |||
<a href="//{{gopkgVersionRoot $.Repo .}}{{$.Repo.SubPath}}" {{if eq .Major $.Repo.MajorVersion.Major}}class="current"{{end}} >v{{.Major}}</a> | |||
<a href="//{{gopkgVersionRoot $.Repo .}}{{$.Repo.SubPath}}" {{if eq .Major $.Repo.MajorVersion.Major}}{{if eq .Unstable $.Repo.MajorVersion.Unstable}}class="current"{{end}}{{end}}>v{{.Major}}{{if .Unstable}}-unstable{{end}}</a> | |||
→ | |||
<span class="label label-default">{{.}}</span> | |||
</div> | |||
@@ -227,21 +232,33 @@ func renderPackagePage(resp http.ResponseWriter, req *http.Request, repo *Repo) | |||
Repo: repo, | |||
} | |||
// calculate version mapping | |||
latestVersionsMap := make(map[int]Version) | |||
// Calculate the latest version for each major version, both stable and unstable. | |||
latestVersions := make(map[int]Version) | |||
latestUnstable := make(map[int]Version) | |||
for _, v := range repo.AllVersions { | |||
v2, exists := latestVersionsMap[v.Major] | |||
m := latestVersions | |||
if v.Unstable { | |||
m = latestUnstable | |||
} | |||
v2, exists := m[v.Major] | |||
if !exists || v2.Less(v) { | |||
latestVersionsMap[v.Major] = v | |||
m[v.Major] = v | |||
} | |||
} | |||
data.FullVersion = latestVersionsMap[repo.MajorVersion.Major] | |||
data.LatestVersions = make(VersionList, 0, len(latestVersionsMap)) | |||
for _, v := range latestVersionsMap { | |||
data.LatestVersions = make(VersionList, 0, len(latestVersions)) | |||
for _, v := range latestVersions { | |||
data.LatestVersions = append(data.LatestVersions, v) | |||
} | |||
sort.Sort(sort.Reverse(data.LatestVersions)) | |||
if repo.MajorVersion.Unstable { | |||
data.FullVersion = latestUnstable[repo.MajorVersion.Major] | |||
// Prepend post-sorting so it's show first. | |||
data.LatestVersions = append([]Version{data.FullVersion}, data.LatestVersions...) | |||
} else { | |||
data.FullVersion = latestVersions[repo.MajorVersion.Major] | |||
} | |||
var dataMutex sync.Mutex | |||
wantResps := 2 | |||
gotResp := make(chan bool, wantResps) | |||
@@ -7,20 +7,29 @@ import ( | |||
// Version represents a version number. | |||
// An element that is not present is represented as -1. | |||
type Version struct { | |||
Major, Minor, Patch int | |||
Major int | |||
Minor int | |||
Patch int | |||
Unstable bool | |||
} | |||
const unstableSuffix = "-unstable" | |||
func (v Version) String() string { | |||
if v.Major < 0 { | |||
panic(fmt.Sprintf("cannot stringify invalid version (major is %d)", v.Major)) | |||
} | |||
suffix := "" | |||
if v.Unstable { | |||
suffix = unstableSuffix | |||
} | |||
if v.Minor < 0 { | |||
return fmt.Sprintf("v%d", v.Major) | |||
return fmt.Sprintf("v%d%s", v.Major, suffix) | |||
} | |||
if v.Patch < 0 { | |||
return fmt.Sprintf("v%d.%d", v.Major, v.Minor) | |||
return fmt.Sprintf("v%d.%d%s", v.Major, v.Minor, suffix) | |||
} | |||
return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch) | |||
return fmt.Sprintf("v%d.%d.%d%s", v.Major, v.Minor, v.Patch, suffix) | |||
} | |||
// Less returns whether v is less than other. | |||
@@ -31,7 +40,10 @@ func (v Version) Less(other Version) bool { | |||
if v.Minor != other.Minor { | |||
return v.Minor < other.Minor | |||
} | |||
return v.Patch < other.Patch | |||
if v.Patch != other.Patch { | |||
return v.Patch < other.Patch | |||
} | |||
return v.Unstable && !other.Unstable | |||
} | |||
// Contains returns whether version v contains version other. | |||
@@ -40,7 +52,13 @@ func (v Version) Less(other Version) bool { | |||
// | |||
// For example, Version{1, 1, -1} contains both Version{1, 1, -1} and Version{1, 1, 2}, | |||
// but not Version{1, -1, -1} or Version{1, 2, -1}. | |||
// | |||
// Unstable versions (-unstable) only contain unstable versions, and stable | |||
// versions only contain stable versions. | |||
func (v Version) Contains(other Version) bool { | |||
if v.Unstable != other.Unstable { | |||
return false | |||
} | |||
if v.Patch != -1 { | |||
return v == other | |||
} | |||
@@ -55,61 +73,65 @@ func (v Version) IsValid() bool { | |||
} | |||
// InvalidVersion represents a version that can't be parsed. | |||
var InvalidVersion = Version{-1, -1, -1} | |||
var InvalidVersion = Version{-1, -1, -1, false} | |||
func parseVersion(s string) (Version, bool) { | |||
func parseVersion(s string) (v Version, ok bool) { | |||
v = InvalidVersion | |||
if len(s) < 2 { | |||
return InvalidVersion, false | |||
return | |||
} | |||
if s[0] != 'v' { | |||
return InvalidVersion, false | |||
return | |||
} | |||
v := Version{-1, -1, -1} | |||
vout := InvalidVersion | |||
unstable := false | |||
i := 1 | |||
v.Major, i = parseVersionPart(s, i) | |||
if i < 0 { | |||
return InvalidVersion, false | |||
} | |||
if i == len(s) { | |||
return v, true | |||
} | |||
v.Minor, i = parseVersionPart(s, i) | |||
if i < 0 { | |||
return InvalidVersion, false | |||
} | |||
if i == len(s) { | |||
return v, true | |||
} | |||
v.Patch, i = parseVersionPart(s, i) | |||
if i < 0 || i < len(s) { | |||
return InvalidVersion, false | |||
for _, vptr := range []*int{&vout.Major, &vout.Minor, &vout.Patch} { | |||
*vptr, unstable, i = parseVersionPart(s, i) | |||
if i < 0 { | |||
return | |||
} | |||
if i == len(s) { | |||
vout.Unstable = unstable | |||
return vout, true | |||
} | |||
} | |||
return v, true | |||
return | |||
} | |||
func parseVersionPart(s string, i int) (part int, newi int) { | |||
dot := i | |||
for dot < len(s) && s[dot] != '.' { | |||
dot++ | |||
} | |||
if dot == i || dot-i > 1 && s[i] == '0' { | |||
return -1, -1 | |||
} | |||
for i < len(s) { | |||
if s[i] < '0' || s[i] > '9' { | |||
return -1, -1 | |||
func parseVersionPart(s string, i int) (part int, unstable bool, newi int) { | |||
j := i | |||
for j < len(s) && s[j] != '.' && s[j] != '-' { | |||
j++ | |||
} | |||
if j == i || j-i > 1 && s[i] == '0' { | |||
return -1, false, -1 | |||
} | |||
c := s[i] | |||
for { | |||
if c < '0' || c > '9' { | |||
return -1, false, -1 | |||
} | |||
part *= 10 | |||
part += int(s[i] - '0') | |||
part += int(c - '0') | |||
if part < 0 { | |||
return -1, -1 | |||
return -1, false, -1 | |||
} | |||
i++ | |||
if i+1 < len(s) && s[i] == '.' { | |||
return part, i + 1 | |||
if i == len(s) { | |||
return part, false, i | |||
} | |||
c = s[i] | |||
if i+1 < len(s) { | |||
if c == '.' { | |||
return part, false, i + 1 | |||
} | |||
if c == '-' && s[i:] == unstableSuffix { | |||
return part, true, i + len(unstableSuffix) | |||
} | |||
} | |||
} | |||
return part, i | |||
panic("unreachable") | |||
} | |||
// VersionList implements sort.Interface | |||
@@ -13,31 +13,35 @@ var _ = Suite(&VersionSuite{}) | |||
type VersionSuite struct{} | |||
var versionParseTests = []struct { | |||
major, minor, patch int | |||
s string | |||
major int | |||
minor int | |||
patch int | |||
dev bool | |||
s string | |||
}{ | |||
{-1, -1, -1, "v"}, | |||
{-1, -1, -1, "v-1"}, | |||
{-1, -1, -1, "v01"}, | |||
{-1, -1, -1, "v1.01"}, | |||
{-1, -1, -1, "a1"}, | |||
{-1, -1, -1, "v1a"}, | |||
{-1, -1, -1, "v1..2"}, | |||
{-1, -1, -1, "v1.2.3.4"}, | |||
{-1, -1, -1, "v1."}, | |||
{-1, -1, -1, "v1.2."}, | |||
{-1, -1, -1, "v1.2.3."}, | |||
{-1, -1, -1, false, "v"}, | |||
{-1, -1, -1, false, "v-1"}, | |||
{-1, -1, -1, false, "v-deb"}, | |||
{-1, -1, -1, false, "v01"}, | |||
{-1, -1, -1, false, "v1.01"}, | |||
{-1, -1, -1, false, "a1"}, | |||
{-1, -1, -1, false, "v1a"}, | |||
{-1, -1, -1, false, "v1..2"}, | |||
{-1, -1, -1, false, "v1.2.3.4"}, | |||
{-1, -1, -1, false, "v1."}, | |||
{-1, -1, -1, false, "v1.2."}, | |||
{-1, -1, -1, false, "v1.2.3."}, | |||
{0, -1, -1, | |||
"v0"}, | |||
{1, -1, -1, | |||
"v1"}, | |||
{1, 2, -1, | |||
"v1.2"}, | |||
{1, 2, 3, | |||
"v1.2.3"}, | |||
{12, 34, 56, | |||
"v12.34.56"}, | |||
{0, -1, -1, false, "v0"}, | |||
{0, -1, -1, true, "v0-unstable"}, | |||
{1, -1, -1, false, "v1"}, | |||
{1, -1, -1, true, "v1-unstable"}, | |||
{1, 2, -1, false, "v1.2"}, | |||
{1, 2, -1, true, "v1.2-unstable"}, | |||
{1, 2, 3, false, "v1.2.3"}, | |||
{1, 2, 3, true, "v1.2.3-unstable"}, | |||
{12, 34, 56, false, "v12.34.56"}, | |||
{12, 34, 56, true, "v12.34.56-unstable"}, | |||
} | |||
func (s *VersionSuite) TestParse(c *C) { | |||
@@ -48,7 +52,7 @@ func (s *VersionSuite) TestParse(c *C) { | |||
c.Fatalf("version %q is invalid but parsed as %#v", t.s, got) | |||
} | |||
} else { | |||
want := Version{t.major, t.minor, t.patch} | |||
want := Version{t.major, t.minor, t.patch, t.dev} | |||
if got != want { | |||
c.Fatalf("version %q must parse as %#v, got %#v", t.s, want, got) | |||
} | |||
@@ -61,24 +65,29 @@ func (s *VersionSuite) TestParse(c *C) { | |||
var versionLessTests = []struct { | |||
oneMajor, oneMinor, onePatch int | |||
oneUnstable bool | |||
twoMajor, twoMinor, twoPatch int | |||
less bool | |||
twoUnstable, less bool | |||
}{ | |||
{0, 0, 0, 0, 0, 0, false}, | |||
{1, 0, 0, 1, 0, 0, false}, | |||
{1, 0, 0, 1, 1, 0, true}, | |||
{1, 0, 0, 2, 0, 0, true}, | |||
{0, 1, 0, 0, 1, 0, false}, | |||
{0, 1, 0, 0, 1, 1, true}, | |||
{0, 0, 0, 0, 2, 0, true}, | |||
{0, 0, 1, 0, 0, 1, false}, | |||
{0, 0, 1, 0, 0, 2, true}, | |||
{0, 0, 0, false, 0, 0, 0, false, false}, | |||
{1, 0, 0, false, 1, 0, 0, false, false}, | |||
{1, 0, 0, false, 1, 1, 0, false, true}, | |||
{1, 0, 0, false, 2, 0, 0, false, true}, | |||
{0, 1, 0, false, 0, 1, 0, false, false}, | |||
{0, 1, 0, false, 0, 1, 1, false, true}, | |||
{0, 0, 0, false, 0, 2, 0, false, true}, | |||
{0, 0, 1, false, 0, 0, 1, false, false}, | |||
{0, 0, 1, false, 0, 0, 2, false, true}, | |||
{0, 0, 0, false, 0, 0, 0, true, false}, | |||
{0, 0, 0, true, 0, 0, 0, false, true}, | |||
{0, 0, 1, true, 0, 0, 0, false, false}, | |||
} | |||
func (s *VersionSuite) TestLess(c *C) { | |||
for _, t := range versionLessTests { | |||
one := Version{t.oneMajor, t.oneMinor, t.onePatch} | |||
two := Version{t.twoMajor, t.twoMinor, t.twoPatch} | |||
one := Version{t.oneMajor, t.oneMinor, t.onePatch, t.oneUnstable} | |||
two := Version{t.twoMajor, t.twoMinor, t.twoPatch, t.twoUnstable} | |||
if one.Less(two) != t.less { | |||
c.Fatalf("version %s < %s returned %v", one, two, !t.less) | |||
} | |||
@@ -87,21 +96,25 @@ func (s *VersionSuite) TestLess(c *C) { | |||
var versionContainsTests = []struct { | |||
oneMajor, oneMinor, onePatch int | |||
oneUnstable bool | |||
twoMajor, twoMinor, twoPatch int | |||
contains bool | |||
twoUnstable, contains bool | |||
}{ | |||
{12, 34, 56, 12, 34, 56, true}, | |||
{12, 34, 56, 12, 34, 78, false}, | |||
{12, 34, -1, 12, 34, 56, true}, | |||
{12, 34, -1, 12, 78, 56, false}, | |||
{12, -1, -1, 12, 34, 56, true}, | |||
{12, -1, -1, 78, 34, 56, false}, | |||
{12, 34, 56, false, 12, 34, 56, false, true}, | |||
{12, 34, 56, false, 12, 34, 78, false, false}, | |||
{12, 34, -1, false, 12, 34, 56, false, true}, | |||
{12, 34, -1, false, 12, 78, 56, false, false}, | |||
{12, -1, -1, false, 12, 34, 56, false, true}, | |||
{12, -1, -1, false, 78, 34, 56, false, false}, | |||
{12, -1, -1, true, 12, -1, -1, false, false}, | |||
{12, -1, -1, false, 12, -1, -1, true, false}, | |||
} | |||
func (s *VersionSuite) TestContains(c *C) { | |||
for _, t := range versionContainsTests { | |||
one := Version{t.oneMajor, t.oneMinor, t.onePatch} | |||
two := Version{t.twoMajor, t.twoMinor, t.twoPatch} | |||
one := Version{t.oneMajor, t.oneMinor, t.onePatch, t.oneUnstable} | |||
two := Version{t.twoMajor, t.twoMinor, t.twoPatch, t.twoUnstable} | |||
if one.Contains(two) != t.contains { | |||
c.Fatalf("version %s.Contains(%s) returned %v", one, two, !t.contains) | |||
} | |||
@@ -110,5 +123,5 @@ func (s *VersionSuite) TestContains(c *C) { | |||
func (s *VersionSuite) TestIsValid(c *C) { | |||
c.Assert(InvalidVersion.IsValid(), Equals, false) | |||
c.Assert(Version{0, 0, 0}.IsValid(), Equals, true) | |||
c.Assert(Version{0, 0, 0, false}.IsValid(), Equals, true) | |||
} |