summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Pointner <equinox@spreadspace.org>2016-01-12 23:40:57 +0100
committerChristian Pointner <equinox@spreadspace.org>2016-01-12 23:40:57 +0100
commit6dd37766912de3ac9dd51b96c10ace60662de458 (patch)
tree5fbccb05de7393f6910fdc7d9216985464ffdfa7
parentalso create special user for vanity to run as (diff)
imported vantiy 0.1.1 and updated debian package actually build from source
-rw-r--r--LICENSE25
-rw-r--r--Makefile43
-rw-r--r--README.md203
-rw-r--r--debian/control6
-rwxr-xr-xdebian/rules8
-rw-r--r--debian/source/format2
-rw-r--r--debian/vanity.docs1
-rw-r--r--debian/vanity.lintian-overrides2
-rw-r--r--main.go474
-rw-r--r--refs_test.go194
-rw-r--r--version.go143
-rw-r--r--version_test.go127
12 files changed, 1211 insertions, 17 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8cdef0e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+gopkg.in - versioned URLs for Go packages
+
+Copyright (c) 2014 - Gustavo Niemeyer <gustavo@niemeyer.net>
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Makefile b/Makefile
index 7d6197d..9cf51a6 100644
--- a/Makefile
+++ b/Makefile
@@ -1,15 +1,38 @@
-VERSION := 0.1.1
-DEBARCHS := amd64 i386 armhf
-EXECUTEABLES := $(DEBARCHS:%=vanity.%)
+GOX_OSARCH ?= "darwin/amd64 linux/amd64 linux/arm freebsd/386 freebsd/amd64 linux/386 windows/386"
+GOX_OUTPUT_DIR ?= bin
+GH_ACCESS_TOKEN ?= Missing access token.
+MESSAGE ?= Latest release.
-all: $(EXECUTEABLES)
+all: clean
+ @mkdir -p $(GOX_OUTPUT_DIR) && \
+ gox -osarch=$(GOX_OSARCH) -output "$(GOX_OUTPUT_DIR)/{{.Dir}}_{{.OS}}_{{.Arch}}" && \
+ gzip bin/vanity_darwin_* && \
+ gzip bin/vanity_freebsd_* && \
+ gzip bin/vanity_linux_* && \
+ zip -r bin/vanity_windows_386.zip bin/vanity_windows_386.exe
-vanity.%: vanity_linux_%.gz
- cat $< | gunzip > $@
+require-version:
+ @if [[ -z "$$VERSION" ]]; then echo "Missing \$$VERSION"; exit 1; fi
-vanity_linux_%.gz:
- wget -nc "https://github.com/xiam/vanity/releases/download/v$(VERSION)/$(shell echo $@ | sed s/i386/386/ | sed s/armhf/arm/)" -O $@
+release: require-version
+ @RESP=$$(curl --silent --data '{ \
+ "tag_name": "v$(VERSION)", \
+ "name": "v$(VERSION)", \
+ "body": "$(MESSAGE)", \
+ "target_commitish": "$(git rev-parse --abbrev-ref HEAD)", \
+ "draft": false, \
+ "prerelease": false \
+ }' "https://api.github.com/repos/xiam/vanity/releases?access_token=$(GH_ACCESS_TOKEN)") && \
+ \
+ UPLOAD_URL_TEMPLATE=$$(echo $$RESP | python -mjson.tool | grep upload_url | awk '{print $$2}' | sed s/,$$//g | sed s/'"'//g) && \
+ if [[ -z "$$UPLOAD_URL_TEMPLATE" ]]; then echo $$RESP; exit 1; fi && \
+ \
+ for ASSET in $$(ls -1 bin/); do \
+ UPLOAD_URL=$$(echo $$UPLOAD_URL_TEMPLATE | sed s/"{?name,label}"/"?access_token=$(GH_ACCESS_TOKEN)\&name=$$ASSET"/g) && \
+ MIME_TYPE=$$(file --mime-type bin/$$ASSET | awk '{print $$2}') && \
+ curl --silent -H "Content-Type: $$MIME_TYPE" --data-binary @bin/$$ASSET $$UPLOAD_URL > /dev/null && \
+ echo "-> $$ASSET OK." \
+ ; done
clean:
- rm -f vanity.*
- rm -f vanity_linux_*.gz
+ @rm -rf $(GOX_OUTPUT_DIR)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6f2f3a2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,203 @@
+# vanity
+
+Use `vanity` to provide pretty package names using your own custom domain:
+
+```go
+import (
+ "example.org/coolpkg"
+)
+```
+
+## A simple example
+
+Let's see the available parameters:
+
+```
+vanity -h
+Usage of ./gopkg:
+ -addr string
+ Serve HTTP at given address (default ":8080")
+ -repo-root string
+ Git repository root URL (e.g.: https://github.com/upper).
+ -vanity-root string
+ Vanity root URL (e.g.: https://upper.io).
+```
+
+Now run `vanity` on localhost:
+
+```
+vanity -addr localhost:8082 -repo-root https://github.com/golang \
+-vanity-root http://localhost:8082
+```
+
+Using a different terminal session, try to `go get` a package from
+`localhost:8082`:
+
+```
+go get -insecure -v localhost:8082/example/hello
+```
+
+`vanity` will tell `go get` to keep the import path and pull the source from a
+different place.
+
+Output should be similar to:
+
+```
+Fetching https://localhost:8082/example/hello?go-get=1
+https fetch failed.
+Fetching http://localhost:8082/example/hello?go-get=1
+Parsing meta tags from http://localhost:8082/example/hello?go-get=1 (status code 200)
+get "localhost:8082/example/hello": found meta tag main.metaImport{Prefix:"localhost:8082/example", VCS:"git", RepoRoot:"http://localhost:8082/example"} at http://localhost:8082/example/hello?go-get=1
+get "localhost:8082/example/hello": verifying non-authoritative meta tag
+Fetching https://localhost:8082/example?go-get=1
+https fetch failed.
+Fetching http://localhost:8082/example?go-get=1
+Parsing meta tags from http://localhost:8082/example?go-get=1 (status code 200)
+localhost:8082/example (download)
+localhost:8082/example/hello
+```
+
+At the end of the `go get` program, you should have the example `hello` package
+on `$GOPATH/src/localhost:8082/example/hello` and in your `$GOPATH/bin`
+directory as well:
+
+```
+hello
+Hello, Go examples!
+```
+
+### Versioning support
+
+`vanity` also comes with versioning support from the original
+[http://gopkg.in](http://gopkg.in) with no extra cost. For instance, the
+following import
+
+```
+go get -v example.org/coolpkg.v1
+```
+
+automatically redirects to the `v1` branch on the github.com/username/coolpkg
+repo, and it can also be imported that way:
+
+```go
+import (
+ "example.org/coolpkg.v1"
+)
+```
+
+Oh, and `vanity` is not tied to GitHub at all, you can use any public git
+repository with https support:
+
+```
+vanity -addr :80 -repo-root https://othergitsite.com/username -vanity-root https://example.org
+```
+
+## Deploy
+
+It is not recommended to run `vanity` directly, as `vanity` does not have a
+nice welcome page nor instructions for humans, `vanity` only expects only
+communication from `go get`, if any other kind of request is received it will
+return a `404` error.
+
+But fear not, you can run `vanity` with `nginx` easily by using that fact, plus
+you can have pretty documentation for humans at your custom domain and also be
+able to use the same URL as an import path for `go get` with no effort.
+
+Let's see an example:
+
+```
+vanity -addr 127.0.0.1:9192 -repo-root https://github.com/upper \
+-vanity-root https://upper.io
+```
+
+This is the site configuration for nginx:
+
+```nginx
+location / {
+ # First try with vanity.
+ try_files =404 @vanity;
+}
+
+location @vanity {
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+ proxy_pass http://127.0.0.1:9192;
+ proxy_intercept_errors on;
+ recursive_error_pages on;
+
+ set $pass 0;
+ if ($arg_go-get = 1) {
+ set $pass 1;
+ }
+ if ($request_uri ~ git-upload-pack) {
+ set $pass 1;
+ }
+ if ($pass = 0) {
+ return 404;
+ }
+
+ error_page 404 = @real_location;
+}
+
+location @real_location {
+ # Fallback location for when vanity returns 404.
+ ...
+}
+```
+
+Let's see it live, use cURL to request `upper.io/db`:
+
+```
+curl "upper.io/db" -L
+...
+```
+
+You'll see some HTML gibberish made for humans, now request it again but this
+time using the `go-get=1` parameter (this is what `go get` does):
+
+```
+curl "upper.io/db?go-get=1" -L
+...
+<meta name="go-import" content="upper.io/db git https://upper.io/db">
+<meta name="go-source" content="upper.io/db _ https://github.com/upper/db/tree/master{/dir} https://github.com/upper/db/blob/master{/dir}/{file}#L{line}">
+...
+```
+
+And you'll see a reduced HTML page with special tags for `go get`.
+
+## License
+
+### gopkg.in
+
+This project was based on [gopkg.in](http://labix.org/gopkg.in) by [Gustavo
+Niemeyer](http://labix.org/):
+
+```
+gopkg.in - versioned URLs for Go packages
+
+Copyright (c) 2014 - Gustavo Niemeyer <gustavo@niemeyer.net>
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
diff --git a/debian/control b/debian/control
index 7e20985..4ace6d9 100644
--- a/debian/control
+++ b/debian/control
@@ -3,11 +3,11 @@ Maintainer: Christian Pointner <equinox@spreadspace.org>
Section: utils
Priority: optional
Standards-Version: 3.9.2
-Build-Depends: debhelper (>= 9), wget, ca-certificates
+Build-Depends: debhelper (>= 9), golang-go
Package: vanity
-Architecture: amd64 i386 armhf
-Depends: ${misc:Depends}
+Architecture: any
+Depends: ${misc:Depends}, ${shlibs:Depends}, adduser
Description: makes golang packages go-getable
Vanity can be used to make golang packages hosted on custom domains
getable. It also has built-in support for versioning of golang packages
diff --git a/debian/rules b/debian/rules
index b985f0b..33d2893 100755
--- a/debian/rules
+++ b/debian/rules
@@ -2,11 +2,15 @@
%:
dh $@
+override_dh_clean:
+ dh_clean
+ rm -f vanity
+
override_dh_auto_configure:
override_dh_auto_build:
- make vanity.$(DEB_BUILD_ARCH)
+ go build -o vanity
override_dh_auto_install:
install -d $$(pwd)/debian/tmp/usr/bin/
- install -m 755 $$(pwd)/vanity.$(DEB_BUILD_ARCH) $$(pwd)/debian/tmp/usr/bin/vanity
+ install -m 755 $$(pwd)/vanity $$(pwd)/debian/tmp/usr/bin/vanity
diff --git a/debian/source/format b/debian/source/format
index d3827e7..163aaf8 100644
--- a/debian/source/format
+++ b/debian/source/format
@@ -1 +1 @@
-1.0
+3.0 (quilt)
diff --git a/debian/vanity.docs b/debian/vanity.docs
new file mode 100644
index 0000000..42061c0
--- /dev/null
+++ b/debian/vanity.docs
@@ -0,0 +1 @@
+README.md \ No newline at end of file
diff --git a/debian/vanity.lintian-overrides b/debian/vanity.lintian-overrides
index 72e91b4..0408589 100644
--- a/debian/vanity.lintian-overrides
+++ b/debian/vanity.lintian-overrides
@@ -1,2 +1,2 @@
-vanity: statically-linked-binary usr/bin/vanity
vanity: binary-without-manpage usr/bin/vanity
+vanity: hardening-no-relro usr/bin/vanity \ No newline at end of file
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..c935f13
--- /dev/null
+++ b/main.go
@@ -0,0 +1,474 @@
+package main
+
+import (
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+ "text/template"
+ "time"
+)
+
+var (
+ addrFlag = flag.String("addr", ":8080", "Serve HTTP at given address")
+ socketFlag = flag.String("socket", "", "Serve HTTP at given UNIX socket")
+ vanityRootFlag = flag.String("vanity-root", "", "Vanity root URL (e.g.: https://upper.io).")
+ repoRootFlag = flag.String("repo-root", "", "Git repository root URL (e.g.: https://github.com/upper).")
+)
+
+var packagePattern = regexp.MustCompile(`^/([a-zA-Z0-9]+)\.?(v[1-9][0-9]*)?(.*)$`)
+
+var httpClient = &http.Client{Timeout: 10 * time.Second}
+
+const refsSuffix = ".git/info/refs?service=git-upload-pack"
+
+// Error messages.
+var (
+ ErrNoRepo = errors.New("repository not found")
+ ErrNoVersion = errors.New("version reference not found")
+)
+
+func main() {
+ if err := run(); err != nil {
+ log.Fatalf("Could not start server: %q", err)
+ }
+}
+
+func run() error {
+ flag.Parse()
+
+ if *addrFlag == "" && *socketFlag == "" {
+ return fmt.Errorf("must provide -addr")
+ }
+
+ if *repoRootFlag == "" {
+ return fmt.Errorf("must provide -repo-root")
+ }
+
+ if *vanityRootFlag == "" {
+ return fmt.Errorf("must provide -vanity-root")
+ }
+
+ repoRoot, err := NewRepoRoot(*repoRootFlag, *vanityRootFlag)
+ if err != nil {
+ return fmt.Errorf("could not parse -repo-root: %q", err)
+ }
+
+ var listenAddr, listenNet string
+
+ if *socketFlag != "" {
+ listenNet, listenAddr = "unix", *socketFlag
+ } else {
+ listenNet, listenAddr = "tcp", *addrFlag
+ }
+
+ li, err := net.Listen(listenNet, listenAddr)
+ if err != nil {
+ return fmt.Errorf("Failed to bind to %s %s: %v", listenNet, listenAddr, err)
+ }
+
+ http.HandleFunc("/", newHandler(repoRoot))
+
+ log.Printf("Listening at %s. %s -> %s", listenAddr, *vanityRootFlag, *repoRootFlag)
+
+ return http.Serve(li, nil)
+}
+
+var gogetTemplate = template.Must(template.New("").Parse(`
+<html>
+<head>
+<meta name="go-import" content="{{.VanityPath}} git {{.VanityURL}}">
+<meta name="go-source" content="{{.VanityPath}} _ {{.RepoRootURL}}/tree/{{.GitTree}}{/dir} {{.RepoRootURL}}/blob/{{.GitTree}}{/dir}/{file}#L{line}">
+</head>
+<body>
+go get {{.VanityPath}}
+</body>
+</html>
+`))
+
+// RepoRoot represents a real repository and vanity name pair.
+type RepoRoot struct {
+ vanityURL *url.URL
+ repoURL *url.URL
+ RepoHostPath string
+ VanityHostPath string
+}
+
+func parseRepoURL(in string) (*url.URL, error) {
+ u, err := url.Parse(in)
+ if err != nil {
+ return nil, err
+ }
+ if u.Scheme == "" {
+ u.Scheme = "https"
+ }
+ return u, err
+}
+
+// NewRepoRoot creates a RepoRoot
+func NewRepoRoot(repoURL string, vanityURL string) (*RepoRoot, error) {
+ u, err := parseRepoURL(repoURL)
+ if err != nil {
+ return nil, err
+ }
+
+ v, err := parseRepoURL(vanityURL)
+ if err != nil {
+ return nil, err
+ }
+
+ return &RepoRoot{
+ repoURL: u,
+ vanityURL: v,
+ RepoHostPath: u.Host + u.Path,
+ VanityHostPath: v.Host + v.Path,
+ }, nil
+}
+
+// NewRepo creates a new repository.
+func (root *RepoRoot) NewRepo(name string) *Repo {
+ return &Repo{
+ Root: root,
+ Name: name,
+ FullVersion: InvalidVersion,
+ }
+}
+
+// Repo represents a source code repository on GitHub.
+type Repo struct {
+ Root *RepoRoot
+
+ Name string
+ MajorVersion Version
+
+ // FullVersion is the best version in AllVersions that matches MajorVersion.
+ // It defaults to InvalidVersion if there are no matches.
+ FullVersion Version
+
+ // AllVersions holds all versions currently available in the repository,
+ // either coming from branch names or from tag names. Version zero (v0)
+ // is only present in the list if it really exists in the repository.
+ AllVersions VersionList
+}
+
+// SetVersions records in the relevant fields the details about which
+// package versions are available in the repository.
+func (repo *Repo) SetVersions(all []Version) {
+ repo.AllVersions = all
+ for _, v := range repo.AllVersions {
+ if v.Major == repo.MajorVersion.Major && v.Unstable == repo.MajorVersion.Unstable && repo.FullVersion.Less(v) {
+ repo.FullVersion = v
+ }
+ }
+}
+
+// RepoRoot returns the repository root, without a schema.
+func (repo *Repo) RepoRoot() string {
+ return repo.Root.RepoHostPath + "/" + repo.Name
+}
+
+// VanityRoot returns the vanity repository root, without a schema.
+func (repo *Repo) VanityRoot() string {
+ return repo.Root.VanityHostPath + "/" + repo.Name
+}
+
+// GitTree returns the repository tree name for the selected version.
+func (repo *Repo) GitTree() string {
+ if repo.FullVersion == InvalidVersion {
+ return "master"
+ }
+ return repo.FullVersion.String()
+}
+
+// VanityPath returns the real package path, without a schema.
+func (repo *Repo) VanityPath() string {
+ return repo.VanityVersionRoot(repo.MajorVersion)
+}
+
+// VanityURL returns the vanity package's URL.
+func (repo *Repo) VanityURL() string {
+ return repo.Root.vanityURL.Scheme + "://" + repo.VanityPath()
+}
+
+// RepoRootURL returns the real package's URL.
+func (repo *Repo) RepoRootURL() string {
+ return repo.Root.repoURL.Scheme + "://" + repo.RepoRoot()
+}
+
+// VanityVersionRoot returns the package's vanity root for the provided
+// version, without a schema.
+func (repo *Repo) VanityVersionRoot(version Version) string {
+ version.Minor = -1
+ version.Patch = -1
+ v := version.String()
+ if v == "v0" {
+ return repo.VanityRoot()
+ }
+ return repo.VanityRoot() + "." + v
+}
+
+func newHandler(repoRoot *RepoRoot) func(http.ResponseWriter, *http.Request) {
+ return func(resp http.ResponseWriter, req *http.Request) {
+ if req.URL.Path == "/health-check" {
+ resp.Write([]byte("ok"))
+ return
+ }
+
+ log.Printf("%s requested %s", req.RemoteAddr, req.URL)
+
+ if req.URL.Path == "/" {
+ sendNotFound(resp, "Missing package name.")
+ return
+ }
+
+ u, err := url.Parse(req.URL.Path)
+ if err != nil {
+ sendError(resp, "Failed to parse request path")
+ return
+ }
+
+ p := packagePattern.FindStringSubmatch(u.Path)
+
+ pkgName := p[1]
+ version := p[2]
+ extra := p[3]
+
+ repo := repoRoot.NewRepo(pkgName)
+
+ if version == "" {
+ version = "v0"
+ }
+
+ var ok bool
+ repo.MajorVersion, ok = parseVersion(version)
+ if !ok {
+ sendNotFound(resp, "Version %q improperly considered invalid; please warn the service maintainers.", version)
+ return
+ }
+
+ var changed []byte
+ var versions VersionList
+ original, err := fetchRefs(repo)
+ if err == nil {
+ changed, versions, err = changeRefs(original, repo.MajorVersion)
+ repo.SetVersions(versions)
+ }
+
+ switch err {
+ case nil:
+ // all ok
+ case ErrNoRepo:
+ sendNotFound(resp, "Git repository not found at https://%s", repo.RepoRoot())
+ return
+ case ErrNoVersion:
+ major := repo.MajorVersion
+ suffix := ""
+ if major.Unstable {
+ major.Unstable = false
+ suffix = unstableSuffix
+ }
+ v := major.String()
+ sendNotFound(resp, `Git repository at https://%s has no branch or tag "%s%s", "%s.N%s" or "%s.N.M%s"`, repo.RepoRoot(), v, suffix, v, suffix, v, suffix)
+ return
+ default:
+ resp.WriteHeader(http.StatusBadGateway)
+ resp.Write([]byte(fmt.Sprintf("Cannot obtain refs from Git: %v", err)))
+ return
+ }
+
+ switch extra {
+ case `/git-upload-pack`:
+ resp.Header().Set("Location", "https://"+repo.RepoRoot()+"/git-upload-pack")
+ resp.WriteHeader(http.StatusMovedPermanently)
+ return
+ case `/info/refs`:
+ resp.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement")
+ resp.Write(changed)
+ return
+ }
+
+ resp.Header().Set("Content-Type", "text/html")
+ if req.FormValue("go-get") == "1" {
+ // execute simple template when this is a go-get request
+ err = gogetTemplate.Execute(resp, repo)
+ if err != nil {
+ log.Printf("error executing go get template: %s\n", err)
+ }
+ return
+ }
+
+ sendNotFound(resp, "Missing ?go-get=1 parameter.")
+ }
+}
+
+func sendError(resp http.ResponseWriter, msg string, args ...interface{}) {
+ if len(args) > 0 {
+ msg = fmt.Sprintf(msg, args...)
+ }
+ resp.WriteHeader(http.StatusInternalServerError)
+ resp.Write([]byte(msg))
+}
+
+func sendNotFound(resp http.ResponseWriter, msg string, args ...interface{}) {
+ if len(args) > 0 {
+ msg = fmt.Sprintf(msg, args...)
+ }
+ resp.WriteHeader(http.StatusNotFound)
+ resp.Write([]byte(msg))
+}
+
+func fetchRefs(repo *Repo) (data []byte, err error) {
+ repoURL := repo.RepoRootURL() + refsSuffix
+ resp, err := httpClient.Get(repoURL)
+ if err != nil {
+ return nil, fmt.Errorf("cannot talk to git repository: %v", err)
+ }
+ defer resp.Body.Close()
+
+ switch resp.StatusCode {
+ case 200:
+ // ok
+ case 401, 404:
+ return nil, ErrNoRepo
+ default:
+ return nil, fmt.Errorf("error from git repository: %v", resp.Status)
+ }
+
+ data, err = ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("error reading from git: %v", err)
+ }
+ return data, err
+}
+
+func changeRefs(data []byte, major Version) (changed []byte, versions VersionList, err error) {
+ var hlinei, hlinej int // HEAD reference line start/end
+ var mlinei, mlinej int // master reference line start/end
+ var vrefhash string
+ var vrefname string
+ var vrefv = InvalidVersion
+
+ // Record all available versions, the locations of the master and HEAD lines,
+ // and details of the best reference satisfying the requested major version.
+ versions = make([]Version, 0)
+ sdata := string(data)
+ for i, j := 0, 0; i < len(data); i = j {
+ size, err := strconv.ParseInt(sdata[i:i+4], 16, 32)
+ if err != nil {
+ return nil, nil, fmt.Errorf("cannot parse refs line size: %s", string(data[i:i+4]))
+ }
+ if size == 0 {
+ size = 4
+ }
+ j = i + int(size)
+ if j > len(sdata) {
+ return nil, nil, fmt.Errorf("incomplete refs data received from GitHub")
+ }
+ if sdata[0] == '#' {
+ continue
+ }
+
+ hashi := i + 4
+ hashj := strings.IndexByte(sdata[hashi:j], ' ')
+ if hashj < 0 || hashj != 40 {
+ continue
+ }
+ hashj += hashi
+
+ namei := hashj + 1
+ namej := strings.IndexAny(sdata[namei:j], "\n\x00")
+ if namej < 0 {
+ namej = j
+ } else {
+ namej += namei
+ }
+
+ name := sdata[namei:namej]
+
+ if name == "HEAD" {
+ hlinei = i
+ hlinej = j
+ }
+ if name == "refs/heads/master" {
+ mlinei = i
+ mlinej = j
+ }
+
+ if strings.HasPrefix(name, "refs/heads/v") || strings.HasPrefix(name, "refs/tags/v") {
+ if strings.HasSuffix(name, "^{}") {
+ // Annotated tag is peeled off and overrides the same version just parsed.
+ name = name[:len(name)-3]
+ }
+ v, ok := parseVersion(name[strings.IndexByte(name, 'v'):])
+ if ok && major.Contains(v) && (v == vrefv || !vrefv.IsValid() || vrefv.Less(v)) {
+ vrefv = v
+ vrefhash = sdata[hashi:hashj]
+ vrefname = name
+ }
+ if ok {
+ versions = append(versions, v)
+ }
+ }
+ }
+
+ // If v0 was requested, accept the master as-is.
+ if major == (Version{0, -1, -1, false}) {
+ return data, nil, nil
+ }
+
+ // If the file has no HEAD line or the version was not found, report as unavailable.
+ if hlinei == 0 || vrefhash == "" {
+ return nil, nil, ErrNoVersion
+ }
+
+ var buf bytes.Buffer
+ buf.Grow(len(data) + 256)
+
+ // Copy the header as-is.
+ buf.Write(data[:hlinei])
+
+ // Extract the original capabilities.
+ caps := ""
+ if i := strings.Index(sdata[hlinei:hlinej], "\x00"); i > 0 {
+ caps = strings.Replace(sdata[hlinei+i+1:hlinej-1], "symref=", "oldref=", -1)
+ }
+
+ // Insert the HEAD reference line with the right hash and a proper symref capability.
+ var line string
+ if strings.HasPrefix(vrefname, "refs/heads/") {
+ if caps == "" {
+ line = fmt.Sprintf("%s HEAD\x00symref=HEAD:%s\n", vrefhash, vrefname)
+ } else {
+ line = fmt.Sprintf("%s HEAD\x00symref=HEAD:%s %s\n", vrefhash, vrefname, caps)
+ }
+ } else {
+ if caps == "" {
+ line = fmt.Sprintf("%s HEAD\n", vrefhash)
+ } else {
+ line = fmt.Sprintf("%s HEAD\x00%s\n", vrefhash, caps)
+ }
+ }
+ fmt.Fprintf(&buf, "%04x%s", 4+len(line), line)
+
+ // Insert the master reference line.
+ line = fmt.Sprintf("%s refs/heads/master\n", vrefhash)
+ fmt.Fprintf(&buf, "%04x%s", 4+len(line), line)
+
+ // Append the rest, dropping the original master line if necessary.
+ if mlinei > 0 {
+ buf.Write(data[hlinej:mlinei])
+ buf.Write(data[mlinej:])
+ } else {
+ buf.Write(data[hlinej:])
+ }
+
+ return buf.Bytes(), versions, nil
+}
diff --git a/refs_test.go b/refs_test.go
new file mode 100644
index 0000000..55580c5
--- /dev/null
+++ b/refs_test.go
@@ -0,0 +1,194 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ . "gopkg.in/check.v1"
+ "sort"
+)
+
+var _ = Suite(&RefsSuite{})
+
+type RefsSuite struct{}
+
+type refsTest struct {
+ summary string
+ original string
+ version string
+ changed string
+ versions []string
+}
+
+var refsTests = []refsTest{{
+ "Version v0 works even without any references",
+ reflines(
+ "hash1 HEAD",
+ ),
+ "v0",
+ reflines(
+ "hash1 HEAD",
+ ),
+ nil,
+}, {
+ "Preserve original capabilities",
+ reflines(
+ "hash1 HEAD\x00caps",
+ ),
+ "v0",
+ reflines(
+ "hash1 HEAD\x00caps",
+ ),
+ nil,
+}, {
+ "Matching major version branch",
+ reflines(
+ "00000000000000000000000000000000000hash1 HEAD",
+ "00000000000000000000000000000000000hash2 refs/heads/v0",
+ "00000000000000000000000000000000000hash3 refs/heads/v1",
+ "00000000000000000000000000000000000hash4 refs/heads/v2",
+ ),
+ "v1",
+ reflines(
+ "00000000000000000000000000000000000hash3 HEAD\x00symref=HEAD:refs/heads/v1",
+ "00000000000000000000000000000000000hash3 refs/heads/master",
+ "00000000000000000000000000000000000hash2 refs/heads/v0",
+ "00000000000000000000000000000000000hash3 refs/heads/v1",
+ "00000000000000000000000000000000000hash4 refs/heads/v2",
+ ),
+ []string{"v0", "v1", "v2"},
+}, {
+ "Matching minor version branch",
+ reflines(
+ "00000000000000000000000000000000000hash1 HEAD",
+ "00000000000000000000000000000000000hash2 refs/heads/v1.1",
+ "00000000000000000000000000000000000hash3 refs/heads/v1.3",
+ "00000000000000000000000000000000000hash4 refs/heads/v1.2",
+ ),
+ "v1",
+ reflines(
+ "00000000000000000000000000000000000hash3 HEAD\x00symref=HEAD:refs/heads/v1.3",
+ "00000000000000000000000000000000000hash3 refs/heads/master",
+ "00000000000000000000000000000000000hash2 refs/heads/v1.1",
+ "00000000000000000000000000000000000hash3 refs/heads/v1.3",
+ "00000000000000000000000000000000000hash4 refs/heads/v1.2",
+ ),
+ []string{"v1.1", "v1.2", "v1.3"},
+}, {
+ "Disable original symref capability",
+ reflines(
+ "00000000000000000000000000000000000hash1 HEAD\x00foo symref=bar baz",
+ "00000000000000000000000000000000000hash2 refs/heads/v1",
+ ),
+ "v1",
+ reflines(
+ "00000000000000000000000000000000000hash2 HEAD\x00symref=HEAD:refs/heads/v1 foo oldref=bar baz",
+ "00000000000000000000000000000000000hash2 refs/heads/master",
+ "00000000000000000000000000000000000hash2 refs/heads/v1",
+ ),
+ []string{"v1"},
+}, {
+ "Replace original master branch",
+ reflines(
+ "00000000000000000000000000000000000hash1 HEAD",
+ "00000000000000000000000000000000000hash1 refs/heads/master",
+ "00000000000000000000000000000000000hash2 refs/heads/v1",
+ ),
+ "v1",
+ reflines(
+ "00000000000000000000000000000000000hash2 HEAD\x00symref=HEAD:refs/heads/v1",
+ "00000000000000000000000000000000000hash2 refs/heads/master",
+ "00000000000000000000000000000000000hash2 refs/heads/v1",
+ ),
+ []string{"v1"},
+}, {
+ "Matching tag",
+ reflines(
+ "00000000000000000000000000000000000hash1 HEAD",
+ "00000000000000000000000000000000000hash2 refs/tags/v0",
+ "00000000000000000000000000000000000hash3 refs/tags/v1",
+ "00000000000000000000000000000000000hash4 refs/tags/v2",
+ ),
+ "v1",
+ reflines(
+ "00000000000000000000000000000000000hash3 HEAD",
+ "00000000000000000000000000000000000hash3 refs/heads/master",
+ "00000000000000000000000000000000000hash2 refs/tags/v0",
+ "00000000000000000000000000000000000hash3 refs/tags/v1",
+ "00000000000000000000000000000000000hash4 refs/tags/v2",
+ ),
+ []string{"v0", "v1", "v2"},
+}, {
+ "Tag peeling",
+ reflines(
+ "00000000000000000000000000000000000hash1 HEAD",
+ "00000000000000000000000000000000000hash2 refs/heads/master",
+ "00000000000000000000000000000000000hash3 refs/tags/v1",
+ "00000000000000000000000000000000000hash4 refs/tags/v1^{}",
+ "00000000000000000000000000000000000hash5 refs/tags/v2",
+ ),
+ "v1",
+ reflines(
+ "00000000000000000000000000000000000hash4 HEAD",
+ "00000000000000000000000000000000000hash4 refs/heads/master",
+ "00000000000000000000000000000000000hash3 refs/tags/v1",
+ "00000000000000000000000000000000000hash4 refs/tags/v1^{}",
+ "00000000000000000000000000000000000hash5 refs/tags/v2",
+ ),
+ []string{"v1", "v1", "v2"},
+}, {
+ "Matching unstable versions",
+ reflines(
+ "00000000000000000000000000000000000hash1 HEAD",
+ "00000000000000000000000000000000000hash2 refs/heads/master",
+ "00000000000000000000000000000000000hash3 refs/heads/v1",
+ "00000000000000000000000000000000000hash4 refs/heads/v1.1-unstable",
+ "00000000000000000000000000000000000hash5 refs/heads/v1.3-unstable",
+ "00000000000000000000000000000000000hash6 refs/heads/v1.2-unstable",
+ "00000000000000000000000000000000000hash7 refs/heads/v2",
+ ),
+ "v1-unstable",
+ reflines(
+ "00000000000000000000000000000000000hash5 HEAD\x00symref=HEAD:refs/heads/v1.3-unstable",
+ "00000000000000000000000000000000000hash5 refs/heads/master",
+ "00000000000000000000000000000000000hash3 refs/heads/v1",
+ "00000000000000000000000000000000000hash4 refs/heads/v1.1-unstable",
+ "00000000000000000000000000000000000hash5 refs/heads/v1.3-unstable",
+ "00000000000000000000000000000000000hash6 refs/heads/v1.2-unstable",
+ "00000000000000000000000000000000000hash7 refs/heads/v2",
+ ),
+ []string{"v1", "v1.1-unstable", "v1.2-unstable", "v1.3-unstable", "v2"},
+}}
+
+func reflines(lines ...string) string {
+ var buf bytes.Buffer
+ buf.WriteString("001e# service=git-upload-pack\n0000")
+ for _, l := range lines {
+ buf.WriteString(fmt.Sprintf("%04x%s\n", len(l)+5, l))
+ }
+ buf.WriteString("0000")
+ return buf.String()
+}
+
+func (s *RefsSuite) TestChangeRefs(c *C) {
+ for _, test := range refsTests {
+ c.Logf(test.summary)
+
+ v, ok := parseVersion(test.version)
+ if !ok {
+ c.Fatalf("Test has an invalid version: %q", test.version)
+ }
+
+ changed, versions, err := changeRefs([]byte(test.original), v)
+ c.Assert(err, IsNil)
+
+ c.Assert(string(changed), Equals, test.changed)
+
+ sort.Sort(versions)
+
+ var vs []string
+ for _, v := range versions {
+ vs = append(vs, v.String())
+ }
+ c.Assert(vs, DeepEquals, test.versions)
+ }
+}
diff --git a/version.go b/version.go
new file mode 100644
index 0000000..6fcb9c1
--- /dev/null
+++ b/version.go
@@ -0,0 +1,143 @@
+package main
+
+import (
+ "fmt"
+)
+
+// Version represents a version number.
+// An element that is not present is represented as -1.
+type Version struct {
+ 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%s", v.Major, suffix)
+ }
+ if v.Patch < 0 {
+ return fmt.Sprintf("v%d.%d%s", v.Major, v.Minor, suffix)
+ }
+ return fmt.Sprintf("v%d.%d.%d%s", v.Major, v.Minor, v.Patch, suffix)
+}
+
+// Less returns whether v is less than other.
+func (v Version) Less(other Version) bool {
+ if v.Major != other.Major {
+ return v.Major < other.Major
+ }
+ if v.Minor != other.Minor {
+ return v.Minor < other.Minor
+ }
+ if v.Patch != other.Patch {
+ return v.Patch < other.Patch
+ }
+ return v.Unstable && !other.Unstable
+}
+
+// Contains returns whether version v contains version other.
+// Version v is defined to contain version other when they both have the same Major
+// version and v.Minor and v.Patch are either undefined or are equal to other's.
+//
+// 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
+ }
+ if v.Minor != -1 {
+ return v.Major == other.Major && v.Minor == other.Minor
+ }
+ return v.Major == other.Major
+}
+
+// IsValid returns trus if the version is valid.
+func (v Version) IsValid() bool {
+ return v != InvalidVersion
+}
+
+// InvalidVersion represents a version that can't be parsed.
+var InvalidVersion = Version{-1, -1, -1, false}
+
+func parseVersion(s string) (v Version, ok bool) {
+ v = InvalidVersion
+ if len(s) < 2 {
+ return
+ }
+ if s[0] != 'v' {
+ return
+ }
+ vout := InvalidVersion
+ unstable := false
+ i := 1
+ 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
+}
+
+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(c - '0')
+ if part < 0 {
+ return -1, false, -1
+ }
+ i++
+ 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)
+ }
+ }
+ }
+ panic("unreachable")
+}
+
+// VersionList implements sort.Interface
+type VersionList []Version
+
+func (vl VersionList) Len() int { return len(vl) }
+func (vl VersionList) Less(i, j int) bool { return vl[i].Less(vl[j]) }
+func (vl VersionList) Swap(i, j int) { vl[i], vl[j] = vl[j], vl[i] }
diff --git a/version_test.go b/version_test.go
new file mode 100644
index 0000000..22774fd
--- /dev/null
+++ b/version_test.go
@@ -0,0 +1,127 @@
+package main
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+var _ = Suite(&VersionSuite{})
+
+type VersionSuite struct{}
+
+var versionParseTests = []struct {
+ major int
+ minor int
+ patch int
+ dev bool
+ s string
+}{
+ {-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, 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) {
+ for _, t := range versionParseTests {
+ got, ok := parseVersion(t.s)
+ if t.major == -1 {
+ if ok || got != InvalidVersion {
+ c.Fatalf("version %q is invalid but parsed as %#v", t.s, got)
+ }
+ } else {
+ 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)
+ }
+ if got.String() != t.s {
+ c.Fatalf("version %q got parsed as %#v and stringified as %q", t.s, got, got.String())
+ }
+ }
+ }
+}
+
+var versionLessTests = []struct {
+ oneMajor, oneMinor, onePatch int
+ oneUnstable bool
+ twoMajor, twoMinor, twoPatch int
+ twoUnstable, less bool
+}{
+ {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, 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)
+ }
+ }
+}
+
+var versionContainsTests = []struct {
+ oneMajor, oneMinor, onePatch int
+ oneUnstable bool
+ twoMajor, twoMinor, twoPatch int
+ twoUnstable, contains bool
+}{
+ {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, 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)
+ }
+ }
+}
+
+func (s *VersionSuite) TestIsValid(c *C) {
+ c.Assert(InvalidVersion.IsValid(), Equals, false)
+ c.Assert(Version{0, 0, 0, false}.IsValid(), Equals, true)
+}