From 90fa2795ad177b9add2fe46382576993e96ece4b Mon Sep 17 00:00:00 2001 From: tjpcc Date: Sat, 16 Sep 2023 20:43:19 -0600 Subject: Initial commit Gemini repository browsing --- cmd.go | 46 +++++++ commit.go | 33 +++++ gemini.go | 118 ++++++++++++++++ go.mod | 10 ++ go.sum | 16 +++ refs.go | 22 +++ repo.go | 335 ++++++++++++++++++++++++++++++++++++++++++++++ templates.go | 10 ++ templates/branch_list.gmi | 7 + templates/diff.gmi | 1 + templates/diffstat.gmi | 1 + templates/ref.gmi | 49 +++++++ templates/repo_home.gmi | 28 ++++ templates/repo_root.gmi | 5 + templates/tag_list.gmi | 7 + templates/tree.gmi | 6 + 16 files changed, 694 insertions(+) create mode 100644 cmd.go create mode 100644 commit.go create mode 100644 gemini.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 refs.go create mode 100644 repo.go create mode 100644 templates.go create mode 100644 templates/branch_list.gmi create mode 100644 templates/diff.gmi create mode 100644 templates/diffstat.gmi create mode 100644 templates/ref.gmi create mode 100644 templates/repo_home.gmi create mode 100644 templates/repo_root.gmi create mode 100644 templates/tag_list.gmi create mode 100644 templates/tree.gmi diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..fca36cb --- /dev/null +++ b/cmd.go @@ -0,0 +1,46 @@ +package syw + +import ( + "bytes" + "context" + "errors" + "os/exec" +) + +var gitbinpath string + +func findbin() string { + if gitbinpath == "" { + gitbinpath, _ = exec.LookPath("git") + if gitbinpath == "" { + panic("failed to find 'git' executable") + } + } + return gitbinpath +} + +func runCmd(ctx context.Context, args []string) (*cmdResult, error) { + cmd := exec.CommandContext(ctx, findbin(), args...) + + outbuf := &bytes.Buffer{} + cmd.Stdout = outbuf + errbuf := &bytes.Buffer{} + cmd.Stderr = errbuf + + var eerr *exec.ExitError + if err := cmd.Run(); err != nil && !errors.As(err, &eerr) { + return nil, err + } + + return &cmdResult{ + status: cmd.ProcessState.ExitCode(), + out: outbuf, + err: errbuf, + }, nil +} + +type cmdResult struct { + status int + out *bytes.Buffer + err *bytes.Buffer +} diff --git a/commit.go b/commit.go new file mode 100644 index 0000000..d10dc6d --- /dev/null +++ b/commit.go @@ -0,0 +1,33 @@ +package syw + +import ( + "strings" + "time" +) + +type Commit struct { + Repo *Repository + + Hash string + Parents []string + + CommitterName string + CommitterEmail string + CommitDate time.Time + + AuthorName string + AuthorEmail string + AuthorDate time.Time + + Message string +} + +func (c *Commit) ShortMessage() string { + short, _, _ := strings.Cut(c.Message, "\n") + return short +} + +func (c *Commit) RestOfMessage() string { + _, rest, _ := strings.Cut(c.Message, "\n") + return strings.TrimPrefix(rest, "\n") +} diff --git a/gemini.go b/gemini.go new file mode 100644 index 0000000..6222684 --- /dev/null +++ b/gemini.go @@ -0,0 +1,118 @@ +package syw + +import ( + "bytes" + "context" + "mime" + "os" + "path" + "path/filepath" + "strings" + "text/template" + + "tildegit.org/tjp/sliderule" + "tildegit.org/tjp/sliderule/gemini" +) + +const ( + repokey = "syw_repo" + reponamekey = "syw_reponame" +) + +func GeminiRouter(repodir string) *sliderule.Router { + repoRouter := &sliderule.Router{} + repoRouter.Use(assignRepo(repodir)) + repoRouter.Route("/", gmiTemplate(geminiTemplate, "repo_home.gmi")) + repoRouter.Route("/branches", gmiTemplate(geminiTemplate, "branch_list.gmi")) + repoRouter.Route("/tags", gmiTemplate(geminiTemplate, "tag_list.gmi")) + repoRouter.Route("/refs/:ref/", gmiTemplate(geminiTemplate, "ref.gmi")) + repoRouter.Route("/refs/:ref/tree/*path", sliderule.HandlerFunc(geminiTreePath)) + repoRouter.Route("/diffstat/:fromref/:toref", runTemplate(geminiTemplate, "diffstat.gmi", "text/plain")) + repoRouter.Route("/diff/:fromref/:toref", runTemplate(geminiTemplate, "diff.gmi", "text/x-diff")) + + router := &sliderule.Router{} + router.Route("/", geminiRoot(repodir)) + router.Mount("/:"+reponamekey, repoRouter) + + return router +} + +func assignRepo(repodir string) sliderule.Middleware { + return func(h sliderule.Handler) sliderule.Handler { + return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response { + repo := Open(filepath.Join(repodir, sliderule.RouteParams(ctx)[reponamekey])) + return h.Handle(context.WithValue(ctx, repokey, repo), request) + }) + } +} + +func geminiRoot(repodir string) sliderule.Handler { + return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response { + entries, err := os.ReadDir(repodir) + if err != nil { + return gemini.Failure(err) + } + + names := []string{} + for _, item := range entries { + if Open(filepath.Join(repodir, item.Name())) != nil { + names = append(names, item.Name()) + } + } + + buf := &bytes.Buffer{} + if err := geminiTemplate.ExecuteTemplate(buf, "repo_root.gmi", names); err != nil { + return gemini.Failure(err) + } + + return gemini.Success("text/gemini; charset=utf-8", buf) + }) +} + +func geminiTreePath(ctx context.Context, request *sliderule.Request) *sliderule.Response { + params := sliderule.RouteParams(ctx) + if params["path"] == "" || strings.HasSuffix(params["path"], "/") { + return gmiTemplate(geminiTemplate, "tree.gmi").Handle(ctx, request) + } + + repo := ctx.Value(repokey).(*Repository) + + body, err := repo.Blob(ctx, params["ref"], params["path"]) + if err != nil { + return gemini.Failure(err) + } + + mediaType := "" + ext := path.Ext(params["path"]) + if ext == ".gmi" { + mediaType = "text/gemini; charset=utf-8" + } else { + mediaType = mime.TypeByExtension(ext) + } + if mediaType == "" { + mediaType = "application/octet-stream" + } + + return gemini.Success(mediaType, bytes.NewBuffer(body)) +} + +func gmiTemplate(tmpl *template.Template, name string) sliderule.Handler { + return runTemplate(tmpl, name, "text/gemini; charset=utf-8") +} + +func runTemplate(tmpl *template.Template, name, mimetype string) sliderule.Handler { + return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response { + obj := map[string]any{ + "Ctx": ctx, + "Repo": ctx.Value(repokey), + "Params": sliderule.RouteParams(ctx), + } + buf := &bytes.Buffer{} + + if err := tmpl.ExecuteTemplate(buf, name, obj); err != nil { + return gemini.Failure(err) + } + + return gemini.Success(mimetype, buf) + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d0d3dd8 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module tildegit.org/tjp/syw + +go 1.21.0 + +require tildegit.org/tjp/sliderule v1.3.3-0.20230915230008-0ab036d34c55 + +require ( + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6a751d0 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +tildegit.org/tjp/sliderule v1.3.3-0.20230915222003-73f18bcd3a75 h1:jNGIxEz08OgfDBVk2QuUocwMVs5m5kyxUmq/9OSEwU0= +tildegit.org/tjp/sliderule v1.3.3-0.20230915222003-73f18bcd3a75/go.mod h1:opdo8E25iS9X9pNismM8U7pCH8XO0PdRIIhdADn8Uik= +tildegit.org/tjp/sliderule v1.3.3-0.20230915230008-0ab036d34c55 h1:zeJi8W/ouUShu+rnvr3wYpZYDaSP/G/7YXyFM3nSylg= +tildegit.org/tjp/sliderule v1.3.3-0.20230915230008-0ab036d34c55/go.mod h1:opdo8E25iS9X9pNismM8U7pCH8XO0PdRIIhdADn8Uik= diff --git a/refs.go b/refs.go new file mode 100644 index 0000000..aec9c91 --- /dev/null +++ b/refs.go @@ -0,0 +1,22 @@ +package syw + +import "strings" + +type Ref struct { + Repo *Repository + Name string + Hash string +} + +func (r Ref) IsBranch() bool { return strings.HasPrefix(r.Name, "refs/heads/") } +func (r Ref) IsTag() bool { return strings.HasPrefix(r.Name, "refs/tags/") } + +func (r Ref) ShortName() string { + if r.IsBranch() { + return r.Name[11:] + } else if r.IsTag() { + return r.Name[10:] + } else { + return r.Name + } +} diff --git a/repo.go b/repo.go new file mode 100644 index 0000000..820e8b2 --- /dev/null +++ b/repo.go @@ -0,0 +1,335 @@ +package syw + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "tildegit.org/tjp/sliderule/logging" +) + +type Repository string + +func Open(dirpath string) *Repository { + check := []string{dirpath} + if !strings.HasSuffix(dirpath, ".git") { + check = append(check, dirpath+".git") + } + check = append(check, filepath.Join(dirpath, ".git")) + + for _, p := range check { + if st, err := os.Stat(filepath.Join(p, "objects")); err != nil || !st.IsDir() { + continue + } + if st, err := os.Stat(filepath.Join(p, "refs")); err != nil || !st.IsDir() { + continue + } + + r := Repository(p) + return &r + } + + return nil +} + +func (r *Repository) Name() string { + name := filepath.Base(string(*r)) + if name == ".git" { + name = filepath.Base(filepath.Dir(string(*r))) + } + return strings.TrimSuffix(name, ".git") +} + +func (r *Repository) cmd(ctx context.Context, cmdname string, args ...string) (*cmdResult, error) { + args = append([]string{"--git-dir=" + string(*r), cmdname}, args...) + start := time.Now() + + result, err := runCmd(ctx, args) + + log, ok := ctx.Value("debuglog").(logging.Logger) + if ok { + _ = log.Log("msg", "ran git command", "args", fmt.Sprintf("%+v", args), "dur", time.Since(start)) + } + + return result, err +} + +func (r *Repository) Type(ctx context.Context, hash string) (string, error) { + res, err := r.cmd(ctx, "cat-file", "-t", hash) + if err != nil { + return "", err + } + if res.status != 0 { + return "", errors.New(res.err.String()) + } + + return strings.Trim(res.out.String(), "\n"), nil +} + +func (r *Repository) Refs(ctx context.Context) ([]Ref, error) { + res, err := r.cmd(ctx, "show-ref", "--head", "--heads", "--tags") + if err != nil { + return nil, err + } + + lines := strings.Split(res.out.String(), "\n") + + branches := make([]Ref, 0, len(lines)) + for _, line := range lines { + hash, name, found := strings.Cut(line, " ") + if !found { + continue + } + branches = append(branches, Ref{ + Repo: r, + Name: name, + Hash: hash, + }) + } + return branches, nil +} + +var badRevListOutput = errors.New("unexpected 'git rev-list' output") + +func (r *Repository) Commits(ctx context.Context, head string, count int) ([]Commit, error) { + res, err := r.cmd(ctx, "rev-list", + "--format=%an%n%ae%n%aI%n%cn%n%ce%n%cI%n%P%n%B$$END$$", + "-n", strconv.Itoa(count), + head, + ) + if err != nil { + return nil, err + } + + commits := make([]Commit, 0, count) + for _, revstr := range strings.Split(res.out.String(), "\n$$END$$\n") { + if revstr == "" { + continue + } + commits = append(commits, Commit{Repo: r}) + commit := &commits[len(commits)-1] + + commitHash, rest, found := strings.Cut(revstr, "\n") + if !found { + return nil, badRevListOutput + } + commit.Hash = commitHash[7:] + + commit.AuthorName, rest, found = strings.Cut(rest, "\n") + if !found { + return nil, badRevListOutput + } + commit.AuthorEmail, rest, found = strings.Cut(rest, "\n") + if !found { + return nil, badRevListOutput + } + + adate, rest, found := strings.Cut(rest, "\n") + if !found { + return nil, badRevListOutput + } + commit.AuthorDate, err = time.Parse(time.RFC3339, adate) + if err != nil { + return nil, err + } + + commit.CommitterName, rest, found = strings.Cut(rest, "\n") + if !found { + return nil, badRevListOutput + } + commit.CommitterEmail, rest, found = strings.Cut(rest, "\n") + if !found { + return nil, badRevListOutput + } + + cdate, rest, found := strings.Cut(rest, "\n") + if !found { + return nil, badRevListOutput + } + commit.CommitDate, err = time.Parse(time.RFC3339, cdate) + if err != nil { + return nil, err + } + + parents, rest, found := strings.Cut(rest, "\n") + if !found { + return nil, badRevListOutput + } + commit.Parents = strings.Split(parents, " ") + if len(commit.Parents) == 1 && commit.Parents[0] == "" { + commit.Parents = nil + } + + commit.Message = rest + } + + return commits, nil +} + +func (r *Repository) Commit(ctx context.Context, ref string) (*Commit, error) { + commits, err := r.Commits(ctx, ref, 1) + if err != nil { + return nil, err + } + return &commits[0], nil +} + +func (r *Repository) Diffstat(ctx context.Context, fromref, toref string) (string, error) { + res, err := r.cmd(ctx, "diff-tree", "-r", "--stat", fromref, toref) + if err != nil { + return "", err + } + if res.status != 0 { + return "", errors.New(res.err.String()) + } + return res.out.String(), nil +} + +func (r *Repository) Diff(ctx context.Context, fromref, toref string) (string, error) { + res, err := r.cmd(ctx, "diff-tree", "-r", "-p", "-u", fromref, toref) + if err != nil { + return "", err + } + if res.status != 0 { + return "", errors.New(res.err.String()) + } + return res.out.String(), nil +} + +type Readme struct { + Filename string + RawContents string +} + +func (r Readme) EscapedContents() string { + return strings.ReplaceAll(r.RawContents, "\n```", "\n ```") +} + +func (r *Repository) Readme(ctx context.Context, ref string) (*Readme, error) { + dir, err := r.Tree(ctx, ref, "") + if err != nil { + return nil, err + } + + filename := "" + for i := range dir { + if dir[i].Type == "blob" && strings.HasPrefix(strings.ToLower(dir[i].Path), "readme") { + filename = dir[i].Path + break + } + } + + if filename != "" { + body, err := r.Blob(ctx, ref, filename) + if err != nil { + return nil, err + } + + return &Readme{ + Filename: filename, + RawContents: string(body), + }, nil + } + + return nil, nil +} + +func (r *Repository) Description() string { + f, err := os.Open(filepath.Join(string(*r), "description")) + if err != nil { + return "" + } + defer func() { _ = f.Close() }() + + b, err := io.ReadAll(f) + if err != nil { + return "" + } + + return strings.TrimRight(string(b), "\n") +} + +func (r *Repository) Blob(ctx context.Context, ref, path string) ([]byte, error) { + res, err := r.cmd(ctx, "cat-file", "blob", ref+":"+path) + switch { + case res == nil: + return nil, err + case res.status == 0: + return res.out.Bytes(), nil + case res.status == 128: + return nil, objectDoesNotExist + default: + return nil, errors.New(res.err.String()) + } +} + +type ObjectDescription struct { + Mode int + Type string + Hash string + Size int + Path string +} + +func (r *Repository) Tree(ctx context.Context, ref, path string) ([]ObjectDescription, error) { + pattern := ref + if path != "" && path != "." { + pattern += ":" + path + } + + res, err := r.cmd(ctx, "ls-tree", "-l", pattern) + if err != nil { + return nil, err + } + + out := []ObjectDescription{} + for _, line := range strings.Split(res.out.String(), "\n") { + if line == "" { + continue + } + + spl := dropEmpty(strings.Split(strings.ReplaceAll(line, "\t", " "), " ")) + + mode, err := strconv.ParseInt(spl[0], 8, 64) + if err != nil { + return nil, err + } + + var size int + if spl[3] != "-" { + size, err = strconv.Atoi(spl[3]) + if err != nil { + return nil, err + } + } + + out = append(out, ObjectDescription{ + Mode: int(mode), + Type: spl[1], + Hash: spl[2], + Size: size, + Path: spl[4], + }) + } + return out, nil +} + +func dropEmpty(sl []string) []string { + n := 0 + for i := 0; i < len(sl); i += 1 { + if sl[i] == "" { + copy(sl[i:], sl[i+1:]) + i -= 1 + n += 1 + } + } + return sl[:len(sl)-n] +} + +var objectDoesNotExist = errors.New("object does not exist") diff --git a/templates.go b/templates.go new file mode 100644 index 0000000..ddce7ea --- /dev/null +++ b/templates.go @@ -0,0 +1,10 @@ +package syw + +import ( + "embed" + "text/template" +) + +//go:embed templates/*.gmi +var geminiTemplateFS embed.FS +var geminiTemplate = template.Must(template.ParseFS(geminiTemplateFS, "templates/*.gmi")) diff --git a/templates/branch_list.gmi b/templates/branch_list.gmi new file mode 100644 index 0000000..2a2e99e --- /dev/null +++ b/templates/branch_list.gmi @@ -0,0 +1,7 @@ +# {{.Repo.Name}} Branches βŒ₯ + +{{ range .Repo.Refs .Ctx -}} +{{ if .IsBranch -}} +=> ./refs/{{.Hash}}/ {{.ShortName}} +{{ end -}} +{{ end -}} diff --git a/templates/diff.gmi b/templates/diff.gmi new file mode 100644 index 0000000..f2b795b --- /dev/null +++ b/templates/diff.gmi @@ -0,0 +1 @@ +{{.Repo.Diff .Ctx .Params.fromref .Params.toref}} diff --git a/templates/diffstat.gmi b/templates/diffstat.gmi new file mode 100644 index 0000000..a51e06b --- /dev/null +++ b/templates/diffstat.gmi @@ -0,0 +1 @@ +{{.Repo.Diffstat .Ctx .Params.fromref .Params.toref}} diff --git a/templates/ref.gmi b/templates/ref.gmi new file mode 100644 index 0000000..e185ef1 --- /dev/null +++ b/templates/ref.gmi @@ -0,0 +1,49 @@ +# {{.Repo.Name}} {{slice .Params.ref 0 8}} + +{{ with .Repo.Commit .Ctx .Params.ref -}} +## {{.ShortMessage}} + +{{ with .RestOfMessage -}} +{{ if ne . "" -}} +{{.}} + +{{ end -}} +{{ end -}} +=> ../../ πŸ—‚οΈ Repository +=> ./tree/ πŸ“„ Files +{{ if ne .Parents nil -}} +=> ../../diff/{{.Hash}}^/{{.Hash}} πŸ”© Full Diff +{{ else -}} +=> ../../diff/4b825dc642cb6eb9a060e54bf8d69288fbee4904/{{.Hash}} πŸ”© Full Diff +{{ end -}} +{{ range .Parents -}} +=> ../{{.}}/ πŸ‘€ Parent {{slice . 0 8}} +{{ end -}} +{{ range .Repo.Refs $.Ctx -}} +{{ if .IsTag -}} +{{ if eq $.Params.ref .Hash -}} +=> ../{{.Hash}}/ 🏷️ {{.ShortName}} +{{ end -}} +{{ end -}} +{{ end }} + +### Authored +=> mailto:{{.AuthorEmail}} {{.AuthorName}} +{{.AuthorDate.Format "Mon Jan _2 15:04:05 MST 2006"}} + +### Committed +=> mailto:{{.CommitterEmail}} {{.CommitterName}} +{{.CommitDate.Format "Mon Jan _2 15:04:05 MST 2006"}} + +{{ if ne .Parents nil -}} +{{ with index .Parents 0 -}} +```diffstat +{{$.Repo.Diffstat $.Ctx . $.Params.ref}} +``` +{{ end -}} +{{ else -}} +```diffstat +{{$.Repo.Diffstat $.Ctx "4b825dc642cb6eb9a060e54bf8d69288fbee4904" $.Params.ref}} +``` +{{ end -}} +{{ end -}} diff --git a/templates/repo_home.gmi b/templates/repo_home.gmi new file mode 100644 index 0000000..a2af514 --- /dev/null +++ b/templates/repo_home.gmi @@ -0,0 +1,28 @@ +# {{.Repo.Name}} + +{{.Repo.Description}} + +=> ./branches βŒ₯ Branches +=> ./tags 🏷️Tags +=> ./refs/HEAD/tree/ πŸ“„ Files + +## Latest Commits + +{{ with .Repo.Commits .Ctx "HEAD" 5 -}} +{{ range . -}} +=> ./refs/{{.Hash}}/ {{.ShortMessage}} +{{ end -}} +{{ if len . | eq 0 -}} +(no commits to show) +{{ end -}} +{{ end }} + +{{ with .Repo.Readme .Ctx "HEAD" -}} +{{ if len .RawContents | ne 0 -}} +## {{.Filename}} + +```{{.Filename}} contents +{{.EscapedContents}} +``` +{{ end -}} +{{ end }} diff --git a/templates/repo_root.gmi b/templates/repo_root.gmi new file mode 100644 index 0000000..68b230b --- /dev/null +++ b/templates/repo_root.gmi @@ -0,0 +1,5 @@ +# Repositories + +{{ range . -}} +=> ./{{.}}/ {{.}} +{{ end }} diff --git a/templates/tag_list.gmi b/templates/tag_list.gmi new file mode 100644 index 0000000..2fc0fe2 --- /dev/null +++ b/templates/tag_list.gmi @@ -0,0 +1,7 @@ +# {{.Repo.Name}} Tags 🏷️ + +{{ range .Repo.Refs .Ctx -}} +{{ if .IsTag -}} +=> ./refs/{{.Hash}}/ {{.ShortName}} +{{ end -}} +{{ end -}} diff --git a/templates/tree.gmi b/templates/tree.gmi new file mode 100644 index 0000000..35d0d02 --- /dev/null +++ b/templates/tree.gmi @@ -0,0 +1,6 @@ +# {{.Params.ref}}:{{ if ne .Params.path "" }}{{.Params.path}}{{ else }}/{{ end }} + +=> ../ {{ if ne .Params.path "" }}../{{ else }}Commit{{ end }} +{{ range .Repo.Tree .Ctx .Params.ref .Params.path -}} +=> ./{{.Path}}{{if eq .Type "tree"}}/{{end}} {{if eq .Type "blob"}}πŸ“„{{else if eq .Type "tree"}}πŸ“‚{{end}} {{.Path}} +{{ end -}} -- cgit v1.2.3