summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRagnis Armus <ragnis@armus.ee>2018-07-12 22:54:41 +0300
committerRagnis Armus <ragnis@armus.ee>2018-07-12 22:54:41 +0300
commit8389d48005858c4e6af2e90373e0c18d84330456 (patch)
treef3e57be70b0c24c091caf17ecde25fced974767c
first commit
-rw-r--r--.gitignore1
-rw-r--r--cgitrc/config.go139
-rw-r--r--cgitrc/parser.go118
-rw-r--r--main.go63
4 files changed, 321 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1f23a4c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/cgit-go-import
diff --git a/cgitrc/config.go b/cgitrc/config.go
new file mode 100644
index 0000000..82b5f69
--- /dev/null
+++ b/cgitrc/config.go
@@ -0,0 +1,139 @@
+package cgitrc
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "strings"
+)
+
+// Config is a cgit configuration file.
+type Config struct {
+ CloneURL string
+ Repos map[string]*Repo
+
+ files map[string]bool
+}
+
+// Repo describes a cgit repository configuration.
+type Repo struct {
+ URL string
+ Desc string
+ CloneURL string
+}
+
+// Open reads a config file.
+func Open(filename string) (*Config, error) {
+ cfg := &Config{
+ Repos: make(map[string]*Repo),
+ files: make(map[string]bool, 1),
+ }
+ if err := cfg.fromFile(filename); err != nil {
+ return nil, err
+ }
+ return cfg, nil
+}
+
+// ResolveRepoCloneURL returns the effective clone URL for a repository.
+// Returns an empty string if no clone URL can be determined.
+func (cfg *Config) ResolveRepoCloneURL(r *Repo) string {
+ if r.CloneURL != "" {
+ return cfg.CloneURL
+ }
+ if cfg.CloneURL != "" {
+ return strings.Replace(cfg.CloneURL, "$CGIT_REPO_URL", r.URL, 1)
+ }
+ return ""
+}
+
+func (cfg *Config) fromFile(filename string) error {
+ filename = path.Clean(filename)
+ if _, ok := cfg.files[filename]; ok {
+ return errors.New("recursive include")
+ }
+ file, err := os.Open(filename)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ parser := &Parser{}
+ if _, err := io.Copy(parser, file); err != nil {
+ return err
+ }
+ if err := parser.Close(); err != nil {
+ return err
+ }
+ cfg.files[filename] = true
+ return cfg.fromFields(parser.Fields)
+}
+
+func (cfg *Config) fromFields(fields []*ParserField) error {
+ var (
+ repo *Repo
+ newRepo bool
+ )
+ for _, f := range fields {
+ switch f.Keys[0] {
+ case "include":
+ if len(f.Keys) != 1 {
+ return errors.New("invalid key")
+ }
+ if err := cfg.fromFile(f.Value); err != nil {
+ return wrapFieldErr(f, fmt.Errorf("include %s: %v", f.Value, err))
+ }
+ case "repo":
+ if len(f.Keys) == 2 && f.Keys[1] == "url" {
+ repo = &Repo{}
+ newRepo = true
+ }
+ if repo == nil {
+ return wrapFieldErr(f, errors.New("unexpected key"))
+ }
+ if err := repo.setField(f.Keys[1:], f.Value); err != nil {
+ return wrapFieldErr(f, err)
+ }
+ if newRepo {
+ cfg.Repos[repo.URL] = repo
+ newRepo = false
+ }
+ default:
+ if err := cfg.setField(f.Keys, f.Value); err != nil {
+ return wrapFieldErr(f, err)
+ }
+ }
+ }
+ return nil
+}
+
+func (cfg *Config) setField(keys []string, v string) error {
+ if len(keys) != 1 {
+ return errors.New("invalid key")
+ }
+ switch keys[0] {
+ case "clone-url":
+ cfg.CloneURL = v
+ }
+ return nil
+}
+
+func (r *Repo) setField(keys []string, v string) error {
+ if len(keys) != 1 {
+ return errors.New("invalid key")
+ }
+ switch keys[0] {
+ case "url":
+ r.URL = v
+ case "desc":
+ r.Desc = v
+ case "clone-url":
+ r.CloneURL = v
+ }
+ return nil
+}
+
+func wrapFieldErr(f *ParserField, err error) error {
+ return fmt.Errorf("on line %d: %v", f.LineNo, err)
+}
diff --git a/cgitrc/parser.go b/cgitrc/parser.go
new file mode 100644
index 0000000..4af57e7
--- /dev/null
+++ b/cgitrc/parser.go
@@ -0,0 +1,118 @@
+package cgitrc
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "strings"
+)
+
+type parserState int
+
+const (
+ parserStateLineStart parserState = iota
+ parserStateComment
+ parserStateKey
+ parserStateValue
+)
+
+// Parser parses a cgitrc file.
+type Parser struct {
+ Fields []*ParserField
+
+ state parserState
+ keys []string
+ key bytes.Buffer
+ value bytes.Buffer
+
+ lineNo uint
+}
+
+// ParserField describes a single key-value pair.
+type ParserField struct {
+ Keys []string
+ Value string
+ LineNo uint
+}
+
+// Write pushes data to the parser.
+func (p *Parser) Write(buf []byte) (n int, err error) {
+ for _, b := range buf {
+ switch p.state {
+ case parserStateLineStart:
+ err = p.parseLineStart(b)
+ case parserStateComment:
+ if b == '\n' {
+ p.state = parserStateLineStart
+ }
+ case parserStateKey:
+ err = p.parseKey(b)
+ case parserStateValue:
+ p.parseValue(b)
+ }
+ if err != nil {
+ err = fmt.Errorf("line %d: %v", p.lineNo, err)
+ return
+ }
+ n++
+ }
+ return
+}
+
+// Close finishes parsing.
+func (p *Parser) Close() (err error) {
+ if p.state != parserStateLineStart {
+ err = errors.New("unexpected EOF")
+ }
+ return
+}
+
+func (p *Parser) parseLineStart(b byte) (err error) {
+ p.lineNo++
+ switch b {
+ case '\n':
+ case '#':
+ p.state = parserStateComment
+ default:
+ p.state = parserStateKey
+ err = p.parseKey(b)
+ }
+ return
+}
+
+func (p *Parser) parseKey(b byte) (err error) {
+ switch b {
+ case '\n':
+ err = errors.New("unexpected linefeed")
+ case '.', '=':
+ p.keys = append(p.keys, p.key.String())
+ p.key.Reset()
+ if b == '=' {
+ p.state = parserStateValue
+ }
+ default:
+ p.key.WriteByte(b)
+ }
+ return
+}
+
+func (p *Parser) parseValue(b byte) {
+ switch b {
+ case '\n':
+ p.Fields = append(p.Fields, &ParserField{
+ Keys: p.keys,
+ Value: p.value.String(),
+ LineNo: p.lineNo,
+ })
+ p.keys = nil
+ p.value.Reset()
+ p.state = parserStateLineStart
+ default:
+ p.value.WriteByte(b)
+ }
+}
+
+// Key returns the field keys joined by '.'.
+func (f ParserField) Key() string {
+ return strings.Join(f.Keys, ".")
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..f09ccce
--- /dev/null
+++ b/main.go
@@ -0,0 +1,63 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+
+ "radr.ee/cgit-go-import/cgitrc"
+)
+
+type importRepo struct {
+ prefix string
+ cloneURL string
+}
+
+var (
+ handlers = make(map[string]http.Handler)
+)
+
+func serveStatus(w http.ResponseWriter, status int) {
+ w.WriteHeader(status)
+ w.Write([]byte(http.StatusText(status)))
+}
+
+func handleRequest(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "HEAD" {
+ return
+ }
+ if r.Method != "GET" || r.URL.RawQuery != "go-import=1" {
+ serveStatus(w, 404)
+ return
+ }
+ handler, ok := handlers[r.URL.Path]
+ if ok {
+ handler.ServeHTTP(w, r)
+ } else {
+ serveStatus(w, 404)
+ }
+}
+
+func createRepoHandler(cfg *cgitrc.Config, r *cgitrc.Repo) http.Handler {
+ body := []byte(fmt.Sprintf(
+ "<head><meta name=\"go-import\" content=\"radr.ee/%s git %s\"></head>",
+ r.URL,
+ cfg.ResolveRepoCloneURL(r),
+ ))
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write(body)
+ })
+}
+
+func main() {
+ cfg, err := cgitrc.Open("/etc/cgitrc")
+ if err != nil {
+ log.Fatal(err)
+ }
+ for _, r := range cfg.Repos {
+ handlers["/"+r.URL] = createRepoHandler(cfg, r)
+ }
+
+ http.ListenAndServe(":8080", http.HandlerFunc(handleRequest))
+}