allow requests for repository sub-paths (#6)

This commit is contained in:
Ross Light 2017-07-07 13:21:37 -07:00 committed by Jaana B. Dogan
parent b514f2bb0d
commit 148d52de30
2 changed files with 143 additions and 17 deletions

View File

@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
"sort"
"strings" "strings"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
@ -26,26 +27,40 @@ import (
type handler struct { type handler struct {
host string host string
m map[string]*struct { paths pathConfigSet
}
type pathConfig struct {
path string
repo string
display string
vcs string
}
func newHandler(config []byte) (*handler, error) {
var m map[string]struct {
Repo string `yaml:"repo,omitempty"` Repo string `yaml:"repo,omitempty"`
Display string `yaml:"display,omitempty"` Display string `yaml:"display,omitempty"`
VCS string `yaml:"vcs,omitempty"` VCS string `yaml:"vcs,omitempty"`
} }
} if err := yaml.Unmarshal(config, &m); err != nil {
func newHandler(config []byte) (*handler, error) {
h := new(handler)
if err := yaml.Unmarshal(config, &h.m); err != nil {
return nil, err return nil, err
} }
for path, e := range h.m { h := new(handler)
for path, e := range m {
pc := pathConfig{
path: strings.TrimSuffix(path, "/"),
repo: e.Repo,
display: e.Display,
vcs: e.VCS,
}
switch { switch {
case e.Display != "": case e.Display != "":
// Already filled in. // Already filled in.
case strings.HasPrefix(e.Repo, "https://github.com/"): case strings.HasPrefix(e.Repo, "https://github.com/"):
e.Display = fmt.Sprintf("%v %v/tree/master{/dir} %v/blob/master{/dir}/{file}#L{line}", e.Repo, e.Repo, e.Repo) pc.display = fmt.Sprintf("%v %v/tree/master{/dir} %v/blob/master{/dir}/{file}#L{line}", e.Repo, e.Repo, e.Repo)
case strings.HasPrefix(e.Repo, "https://bitbucket.org"): case strings.HasPrefix(e.Repo, "https://bitbucket.org"):
e.Display = fmt.Sprintf("%v %v/src/default{/dir} %v/src/default{/dir}/{file}#{file}-{line}", e.Repo, e.Repo, e.Repo) pc.display = fmt.Sprintf("%v %v/src/default{/dir} %v/src/default{/dir}/{file}#{file}-{line}", e.Repo, e.Repo, e.Repo)
} }
switch { switch {
case e.VCS != "": case e.VCS != "":
@ -54,18 +69,20 @@ func newHandler(config []byte) (*handler, error) {
return nil, fmt.Errorf("configuration for %v: unknown VCS %s", path, e.VCS) return nil, fmt.Errorf("configuration for %v: unknown VCS %s", path, e.VCS)
} }
case strings.HasPrefix(e.Repo, "https://github.com/"): case strings.HasPrefix(e.Repo, "https://github.com/"):
e.VCS = "git" pc.vcs = "git"
default: default:
return nil, fmt.Errorf("configuration for %v: cannot infer VCS from %s", path, e.Repo) return nil, fmt.Errorf("configuration for %v: cannot infer VCS from %s", path, e.Repo)
} }
h.paths = append(h.paths, pc)
} }
sort.Sort(h.paths)
return h, nil return h, nil
} }
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
current := r.URL.Path current := r.URL.Path
p, ok := h.m[current] pc, _ := h.paths.find(current)
if !ok { if pc == nil {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@ -80,10 +97,10 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Display string Display string
VCS string VCS string
}{ }{
Import: host + current, Import: host + pc.path,
Repo: p.Repo, Repo: pc.repo,
Display: p.Display, Display: pc.display,
VCS: p.VCS, VCS: pc.vcs,
}); err != nil { }); err != nil {
http.Error(w, "cannot render the page", http.StatusInternalServerError) http.Error(w, "cannot render the page", http.StatusInternalServerError)
} }
@ -101,3 +118,30 @@ var vanityTmpl = template.Must(template.New("vanity").Parse(`<!DOCTYPE html>
Nothing to see here; <a href="https://godoc.org/{{.Import}}">see the package on godoc</a>. Nothing to see here; <a href="https://godoc.org/{{.Import}}">see the package on godoc</a>.
</body> </body>
</html>`)) </html>`))
type pathConfigSet []pathConfig
func (pset pathConfigSet) Len() int {
return len(pset)
}
func (pset pathConfigSet) Less(i, j int) bool {
return pset[i].path < pset[j].path
}
func (pset pathConfigSet) Swap(i, j int) {
pset[i], pset[j] = pset[j], pset[i]
}
func (pset pathConfigSet) find(path string) (pc *pathConfig, subpath string) {
i := sort.Search(len(pset), func(i int) bool {
return pset[i].path >= path
})
if i < len(pset) && pset[i].path == path {
return &pset[i], ""
}
if i > 0 && strings.HasPrefix(path, pset[i-1].path+"/") {
return &pset[i-1], path[len(pset[i-1].path)+1:]
}
return nil, ""
}

View File

@ -19,6 +19,7 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"sort"
"testing" "testing"
) )
@ -66,6 +67,24 @@ func TestHandler(t *testing.T) {
goImport: "example.com/mygit git https://bitbucket.org/zombiezen/mygit", goImport: "example.com/mygit git https://bitbucket.org/zombiezen/mygit",
goSource: "example.com/mygit https://bitbucket.org/zombiezen/mygit https://bitbucket.org/zombiezen/mygit/src/default{/dir} https://bitbucket.org/zombiezen/mygit/src/default{/dir}/{file}#{file}-{line}", goSource: "example.com/mygit https://bitbucket.org/zombiezen/mygit https://bitbucket.org/zombiezen/mygit/src/default{/dir} https://bitbucket.org/zombiezen/mygit/src/default{/dir}/{file}#{file}-{line}",
}, },
{
name: "subpath",
config: "/portmidi:\n" +
" repo: https://github.com/rakyll/portmidi\n" +
" display: https://github.com/rakyll/portmidi _ _\n",
path: "/portmidi/foo",
goImport: "example.com/portmidi git https://github.com/rakyll/portmidi",
goSource: "example.com/portmidi https://github.com/rakyll/portmidi _ _",
},
{
name: "subpath with trailing config slash",
config: "/portmidi/:\n" +
" repo: https://github.com/rakyll/portmidi\n" +
" display: https://github.com/rakyll/portmidi _ _\n",
path: "/portmidi/foo",
goImport: "example.com/portmidi git https://github.com/rakyll/portmidi",
goSource: "example.com/portmidi https://github.com/rakyll/portmidi _ _",
},
} }
for _, test := range tests { for _, test := range tests {
h, err := newHandler([]byte(test.config)) h, err := newHandler([]byte(test.config))
@ -132,3 +151,66 @@ func findMeta(data []byte, name string) string {
} }
return string(content[:j]) return string(content[:j])
} }
func TestPathConfigSetFind(t *testing.T) {
tests := []struct {
paths []string
query string
want string
subpath string
}{
{
paths: []string{"/portmidi"},
query: "/portmidi",
want: "/portmidi",
},
{
paths: []string{"/portmidi"},
query: "/portmidi/",
want: "/portmidi",
},
{
paths: []string{"/portmidi"},
query: "/foo",
want: "",
},
{
paths: []string{"/portmidi"},
query: "/zzz",
want: "",
},
{
paths: []string{"/abc", "/portmidi", "/xyz"},
query: "/portmidi",
want: "/portmidi",
},
{
paths: []string{"/abc", "/portmidi", "/xyz"},
query: "/portmidi/foo",
want: "/portmidi",
subpath: "foo",
},
}
emptyToNil := func(s string) string {
if s == "" {
return "<nil>"
}
return s
}
for _, test := range tests {
pset := make(pathConfigSet, len(test.paths))
for i := range test.paths {
pset[i].path = test.paths[i]
}
sort.Sort(pset)
pc, subpath := pset.find(test.query)
var got string
if pc != nil {
got = pc.path
}
if got != test.want || subpath != test.subpath {
t.Errorf("pathConfigSet(%v).find(%q) = %v, %v; want %v, %v",
test.paths, test.query, emptyToNil(got), subpath, emptyToNil(test.want), test.subpath)
}
}
}