package routes import ( "compress/gzip" "fmt" "log" "path" "path/filepath" "strconv" "strings" "alin.ovh/elgit/config" "alin.ovh/elgit/data" "alin.ovh/elgit/git" "alin.ovh/elgit/templates" "github.com/microcosm-cc/bluemonday" "github.com/russross/blackfriday/v2" "github.com/savsgio/atreugo/v11" "github.com/valyala/fasthttp" ) type deps struct { c *config.Config projects []string repos *data.Entries } func (d *deps) Index(rc *atreugo.RequestCtx) error { pageData := templates.PageData{ Meta: d.c.Meta, } rc.SetContentType("text/html; charset=utf-8") return templates.IndexPage(pageData, d.repos).Render(rc) } func (d *deps) RepoIndex(rc *atreugo.RequestCtx) error { category, _ := rc.UserValue("category").(string) name, _ := rc.UserValue("name").(string) repoName := path.Join(category, name) if d.isNotAllowed(repoName) { return d.NotFound(rc) } path, err := d.GetCleanPath(repoName) if err != nil { log.Printf("getcleanpath error: %v", err) return d.NotFound(rc) } gr, err := git.Open(path, "") if err != nil { return d.NotFound(rc) } commits, err := gr.Commits() if err != nil { return err } var readmeContent string for _, readme := range d.c.Repo.Readme { ext := filepath.Ext(readme) content, _ := gr.FileContent(readme) if len(content) > 0 { switch ext { case ".md", ".mkd", ".markdown": unsafe := blackfriday.Run( []byte(content), blackfriday.WithExtensions(blackfriday.CommonExtensions), ) html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) readmeContent = string(html) default: safe := bluemonday.UGCPolicy().SanitizeBytes([]byte(content)) readmeContent = fmt.Sprintf(`
%s
`, safe) } break } } mainBranch, err := gr.FindMainBranch(d.c.Repo.MainBranch) if err != nil { return err } if len(commits) >= 3 { commits = commits[:3] } pageData := templates.PageData{ Meta: d.c.Meta, Name: repoName, DisplayName: getDisplayName(repoName), Ref: mainBranch, Description: getDescription(path), Servername: d.c.Server.Name, Gomod: isGoModule(gr), } rc.SetContentType("text/html; charset=utf-8") return templates.RepoPage(pageData, commits, readmeContent).Render(rc) } func (d *deps) RepoTree(rc *atreugo.RequestCtx) error { category, _ := rc.UserValue("category").(string) name, _ := rc.UserValue("name").(string) repoName := path.Join(category, name) if d.isNotAllowed(repoName) { return d.NotFound(rc) } rest, _ := rc.UserValue("rest").(string) treePath := strings.TrimSuffix(rest, "/") ref := rc.UserValue("ref").(string) path, err := d.GetCleanPath(repoName) if err != nil { log.Printf("getcleanpath error: %v", err) return d.NotFound(rc) } gr, err := git.Open(path, ref) if err != nil { return err } files, err := gr.FileTree(treePath) if err != nil { return err } data := make(map[string]any) data["name"] = repoName data["displayname"] = getDisplayName(repoName) data["ref"] = ref data["parent"] = treePath data["desc"] = getDescription(path) data["dotdot"] = filepath.Dir(treePath) rc.SetContentType("text/html; charset=utf-8") return d.listFiles(files, data, rc) } func (d *deps) FileContent(rc *atreugo.RequestCtx) error { var raw bool if rawParam, err := strconv.ParseBool(string(rc.Request.URI().QueryArgs().Peek("raw"))); err == nil { raw = rawParam } category, _ := rc.UserValue("category").(string) name, _ := rc.UserValue("name").(string) repoName := path.Join(category, name) if d.isNotAllowed(repoName) { return d.NotFound(rc) } treePath := rc.UserValue("rest").(string) ref := rc.UserValue("ref").(string) path, err := d.GetCleanPath(repoName) if err != nil { log.Printf("getcleanpath error: %v", err) return d.NotFound(rc) } gr, err := git.Open(path, ref) if err != nil { return d.NotFound(rc) } contents, err := gr.FileContent(treePath) if err != nil { return err } data := make(map[string]any) data["name"] = repoName data["displayname"] = getDisplayName(repoName) data["ref"] = ref data["desc"] = getDescription(path) data["path"] = treePath if raw { return rc.TextResponse(contents) } rc.SetContentType("text/html; charset=utf-8") return d.showFile(contents, data, rc) } func (d *deps) Archive(rc *atreugo.RequestCtx) error { category, _ := rc.UserValue("category").(string) name, _ := rc.UserValue("name").(string) repoName := path.Join(category, name) if d.isNotAllowed(repoName) { return d.NotFound(rc) } file := rc.UserValue("file").(string) if !strings.HasSuffix(file, ".tar.gz") { return d.NotFound(rc) } ref := strings.TrimSuffix(file, ".tar.gz") // This allows the browser to use a proper name for the file when downloading filename := fmt.Sprintf("%s-%s.tar.gz", repoName, ref) setContentDisposition(rc, filename) setGZipMIME(rc) path, err := d.GetCleanPath(repoName) if err != nil { log.Printf("getcleanpath error: %v", err) return d.NotFound(rc) } gr, err := git.Open(path, ref) if err != nil { return d.NotFound(rc) } gw := gzip.NewWriter(rc) defer func() { err := gw.Close() if err != nil { log.Printf("failed to close gzip writer: %s", err) } }() prefix := fmt.Sprintf("%s-%s", repoName, ref) err = gr.WriteTar(gw, prefix) if err != nil { // once we start writing to the body we can't report error anymore // so we are only left with printing the error. log.Println(err) return nil } err = gw.Flush() if err != nil { // once we start writing to the body we can't report error anymore // so we are only left with printing the error. log.Println(err) } return nil } func (d *deps) Log(rc *atreugo.RequestCtx) error { category, _ := rc.UserValue("category").(string) name, _ := rc.UserValue("name").(string) repoName := path.Join(category, name) if d.isNotAllowed(repoName) { return d.NotFound(rc) } ref := rc.UserValue("ref").(string) path, err := d.GetCleanPath(repoName) if err != nil { log.Printf("getcleanpath error: %v", err) return d.NotFound(rc) } gr, err := git.Open(path, ref) if err != nil { return d.NotFound(rc) } commits, err := gr.Commits() if err != nil { return err } pageData := templates.PageData{ Meta: d.c.Meta, Name: repoName, DisplayName: getDisplayName(repoName), Ref: ref, Description: getDescription(path), Log: true, } rc.SetContentType("text/html; charset=utf-8") return templates.LogPage(pageData, commits).Render(rc) } func (d *deps) Diff(rc *atreugo.RequestCtx) error { category, _ := rc.UserValue("category").(string) name, _ := rc.UserValue("name").(string) repoName := path.Join(category, name) if d.isNotAllowed(repoName) { return d.NotFound(rc) } ref := rc.UserValue("ref").(string) path, err := d.GetCleanPath(repoName) if err != nil { log.Printf("getcleanpath error: %v", err) return d.NotFound(rc) } gr, err := git.Open(path, ref) if err != nil { return d.NotFound(rc) } diff, err := gr.Diff() if err != nil { return err } pageData := templates.PageData{ Meta: d.c.Meta, Name: repoName, Stat: diff.Stat, Diff: diff.Diff, DisplayName: getDisplayName(repoName), Ref: ref, Description: getDescription(path), } rc.SetContentType("text/html; charset=utf-8") return templates.CommitPage(pageData, diff).Render(rc) } // FileDiff shows the changes to a specific file in a commit func (d *deps) FileDiff(rc *atreugo.RequestCtx) error { category, _ := rc.UserValue("category").(string) name, _ := rc.UserValue("name").(string) repoName := path.Join(category, name) if d.isNotAllowed(repoName) { return d.NotFound(rc) } ref := rc.UserValue("ref").(string) filePath := strings.TrimSuffix(rc.UserValue("file").(string), "/") path, err := d.GetCleanPath(repoName) if err != nil { log.Printf("getcleanpath error: %v", err) return d.NotFound(rc) } g, err := git.Open(path, ref) if err != nil { return err } diff, err := g.DiffFile(filePath) if err != nil { return err } pageData := templates.PageData{ Meta: d.c.Meta, DisplayName: getDisplayName(repoName), Name: repoName, Ref: ref, Description: getDescription(path), Path: filePath, Diff: diff.Diff, } rc.SetContentType("text/html; charset=utf-8") return templates.CommitPage(pageData, diff).Render(rc) } func (d *deps) Refs(rc *atreugo.RequestCtx) error { category, _ := rc.UserValue("category").(string) name, _ := rc.UserValue("name").(string) repoName := path.Join(category, name) if d.isNotAllowed(repoName) { return d.NotFound(rc) } path, err := d.GetCleanPath(repoName) if err != nil { return err } gr, err := git.Open(path, "") if err != nil { return d.NotFound(rc) } tags, err := gr.Tags() if err != nil { // Non-fatal, we *should* have at least one branch to show. log.Println(err) } branches, err := gr.Branches() if err != nil { return err } pageData := templates.PageData{ Meta: d.c.Meta, Name: repoName, DisplayName: getDisplayName(repoName), Description: getDescription(path), } rc.SetContentType("text/html; charset=utf-8") return templates.RefsPage(pageData, branches, tags).Render(rc) } func (d *deps) NotFound(rc *atreugo.RequestCtx) error { log.Printf("Not found: %s", rc.Request.RequestURI()) return ErrNotFound } func (d *deps) Error(rc *atreugo.RequestCtx, cause error, statusCode int) { log.Printf("Error: %v", cause) var err error if rc.Request.Header.HasAcceptEncoding("text/html") { rc.SetContentType("text/html; charset=utf-8") err = templates.ErrorPage(templates.PageData{ Error: &templates.Error{ Code: statusCode, Message: cause.Error(), }, }).Render(rc) } else { err = rc.TextResponse(cause.Error(), statusCode) } if err != nil { log.Printf("error response error: %v", err) } } func (d *deps) Panic(rc *atreugo.RequestCtx, data any) { switch data := data.(type) { case string, []byte: d.Error(rc, fmt.Errorf("panic: %s", data), fasthttp.StatusInternalServerError) case error: d.Error(rc, fmt.Errorf("panic: %w", data), fasthttp.StatusInternalServerError) } }