diff options
-rw-r--r-- | commit.go | 8 | ||||
-rw-r--r-- | gemini.go | 31 | ||||
-rw-r--r-- | gopher.go | 41 | ||||
-rw-r--r-- | refs.go | 2 | ||||
-rw-r--r-- | repo.go | 30 |
5 files changed, 111 insertions, 1 deletions
@@ -5,6 +5,7 @@ import ( "time" ) +// Commit represents a git commit. type Commit struct { Repo *Repository @@ -22,15 +23,22 @@ type Commit struct { Message string } +// ParentHash returns a ref name usable to reach the commit's parent. func (c *Commit) ParentHash() string { return c.Hash + "^" } +// ShortMessage returns the first line of the commit message. func (c *Commit) ShortMessage() string { short, _, _ := strings.Cut(c.Message, "\n") return short } +// RestOfMessage returns all but the first line of the commit message. +// +// It will trim any newline prefixes however, so be aware that +// c.ShortMessage + "\n" + c.RestOfMessage may not produce the original +// commit message. For that use c.Message. func (c *Commit) RestOfMessage() string { _, rest, _ := strings.Cut(c.Message, "\n") return strings.TrimPrefix(rest, "\n") @@ -19,6 +19,37 @@ const ( reponamekey = "syw_reponame" ) +// GeminiRouter builds a router that will handle requests into a directory of git repositories. +// +// The routes it defines are: +// / gemtext listing of the repositories in the directory +// /:syw_reponame[/] gemtext overview of the repository +// /:syw_reponame/branches gemtext list of branches/heads +// /:syw_reponame/tags gemtext listing of tags +// /:syw_reponame/refs/:ref/ gemtext overview of a ref +// /:syw_reponame/refs/:ref/tree/*path gemtext listing of directories, raw files +// /:syw_reponame/diffstat/:fromref/:toref text/plain diffstat between two refs +// /:syw_reponame/diff/:fromref/:toref text/x-diff between two refs +// +// The overrides argument can provide templates to define the behavior of nearly all of the above +// routes. All of them have default implementations so the argument can even be nil, but otherwise +// the template names used are: +// repo_root.gmi gemtext at / +// repo_home.gmi gemtext at /:syw_reponame/ +// branch_list.gmi gemtext at /:syw_reponame/branches +// tag_list.gmi gemtext at /:syw_reponame/tags +// ref.gmi gemtext at /:syw_reponame/refs/:ref/ +// tree.gmi gemtext for directories requested under /:syw_reponame/refs/:ref/tree/*path +// (file paths return the raw files without any template involved) +// diffstat.gmi the plaintext diffstat at /:syw_reponame/diffstat/:fromref/:toref +// diff.gmi the text/x-diff at /:syw_reponame/diff/:fromref/:toref +// +// Most of the templates above are rendered with an object with 3 fields: +// Ctx: the context.Context from the request +// Repo: a *syw.Repository object corresponding to <repodir>/:syw_reponame +// Params: a map[string]string of the route parameters +// +// The only exception is repo_root.gmi, which is rendered with a slice of the repo names instead. func GeminiRouter(repodir string, overrides *template.Template) *sliderule.Router { tmpl, err := addTemplates(geminiTemplate, overrides) if err != nil { @@ -14,6 +14,47 @@ import ( "tildegit.org/tjp/sliderule/gopher" ) +// GopherRouter builds a router that will handle gopher requests in a directory of git repositories. +// +// The routes it defines are: +// / gophermap listing of the repositories in the directory +// /:syw_reponame gophermap overview of the repository +// /:syw_reponame/branches gophermap list of branches/head +// /:syw_reponame/tags gophermap listing of tags +// /:syw_reponame/refs/:ref gophermap overview of a ref +// /:syw_reponame/refs/:ref/tree gophermap listing of a ref's root directory +// /:syw_reponame/refs/:ref/tree/*path for directories: gophermap list of contents +// for files: raw files (guessed item type text/binary/image/etc) +// /:syw_reponame/diffstat/:fromref/:toref text diffstat between two refs +// /:syw_reponame/diff/:fromref/:toref text diff between two refs +// +// The overrides argument can provide templates to define the behavior of nearly all of the above routes. +// All of them have default implementations, so the argument can be nil, but otherwise the template names +// used are: +// repo_root.gophermap gophermap at / +// repo_home.gophermap gophermap at /:syw_reponame +// branch_list.gophermap gophermap at /:syw_reponame/branches +// tag_list.gophermap gophermap at /:syw_reponame/tags +// ref.gophermap gophermap at /:syw_reponame/refs/:ref +// tree.gophermap gophermap at direcotry paths under /:syw_reponame/refs/:ref/tree/*path +// (file paths return the raw files without any template involved) +// diffstat.gophertext plain text diffstat at /:syw_reponame/diffstat/:fromref/:toref +// diff.gophertext plain text diff at /:syw_reponame/diff/:fromref/:toref +// +// Most of the templates above are rendered with an object with 6 fields: +// Ctx: the context.Context from the request +// Repo: a *syw.Repository corresponding to <repodir>/:syw_reponame +// Params: the map[string]string of the route parameters +// Host: the hostname of the running server +// Port: the port number of the running server +// Selector: the selector in the current request +// +// The only exception is repo_root.gophermap, which is instead rendered with a slice of the repo names. +// +// All templates have 3 additional functions made available to them: +// combine: func(string, ...string) string - successively combines paths using url.URL.ResolveReference +// join: func(string, ...string) string - successively joins path segments +// rawtext: func(selector, host, port, text string) string renders text lines as gopher info-message lines. func GopherRouter(repodir string, overrides *template.Template) *sliderule.Router { tmpl, err := addTemplates(gopherTemplate, overrides) if err != nil { @@ -2,6 +2,7 @@ package syw import "strings" +// Ref is an object representing a commit in a repository. type Ref struct { Repo *Repository Name string @@ -11,6 +12,7 @@ type Ref struct { func (r Ref) IsBranch() bool { return strings.HasPrefix(r.Name, "refs/heads/") } func (r Ref) IsTag() bool { return strings.HasPrefix(r.Name, "refs/tags/") } +// ShortName returns the branch or tag name with "refs/[heads|tags]/" trimmed off. func (r Ref) ShortName() string { if r.IsBranch() { return r.Name[11:] @@ -14,8 +14,16 @@ import ( "tildegit.org/tjp/sliderule/logging" ) +// Repository represents a git repository. type Repository string +// Open produces a git repository from a directory path. +// +// It will also try a few variations (dirpath.git, dirpath/.git) and use the first +// path found to be a git repository. +// +// It returns nil if neither dirpath nor any of its variations are a valid git +// repository. func Open(dirpath string) *Repository { check := []string{dirpath} if !strings.HasSuffix(dirpath, ".git") { @@ -38,6 +46,7 @@ func Open(dirpath string) *Repository { return nil } +// Name is the repository name, defined by the directory path. func (r *Repository) Name() string { name := filepath.Base(string(*r)) if name == ".git" { @@ -46,6 +55,7 @@ func (r *Repository) Name() string { return strings.TrimSuffix(name, ".git") } +// NameBytes returns a byte slice of the repository name. func (r *Repository) NameBytes() []byte { return []byte(r.Name()) } @@ -64,6 +74,7 @@ func (r *Repository) cmd(ctx context.Context, cmdname string, args ...string) (* return result, err } +// Type returns the result of "git cat-file -t <hash>". func (r *Repository) Type(ctx context.Context, hash string) (string, error) { res, err := r.cmd(ctx, "cat-file", "-t", hash) if err != nil { @@ -76,6 +87,7 @@ func (r *Repository) Type(ctx context.Context, hash string) (string, error) { return strings.Trim(res.out.String(), "\n"), nil } +// Refs returns a list of branch and tag references. func (r *Repository) Refs(ctx context.Context) ([]Ref, error) { res, err := r.cmd(ctx, "show-ref", "--head", "--heads", "--tags") if err != nil { @@ -101,6 +113,7 @@ func (r *Repository) Refs(ctx context.Context) ([]Ref, error) { var badRevListOutput = errors.New("unexpected 'git rev-list' output") +// Commits lists commits backwards from a given head. 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$$", @@ -176,6 +189,7 @@ func (r *Repository) Commits(ctx context.Context, head string, count int) ([]Com return commits, nil } +// Commit gathers a single commit by a reference string. func (r *Repository) Commit(ctx context.Context, ref string) (*Commit, error) { commits, err := r.Commits(ctx, ref, 1) if err != nil { @@ -184,6 +198,7 @@ func (r *Repository) Commit(ctx context.Context, ref string) (*Commit, error) { return &commits[0], nil } +// Diffstat produces a diffstat of two trees by their references. 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 { @@ -195,6 +210,7 @@ func (r *Repository) Diffstat(ctx context.Context, fromref, toref string) (strin return res.out.String(), nil } +// Diff produces a diff of two trees by their references. 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 { @@ -206,19 +222,27 @@ func (r *Repository) Diff(ctx context.Context, fromref, toref string) (string, e return res.out.String(), nil } +// Readme represents a README file. type Readme struct { Filename string RawContents string } +// GeminiEscapedContent produces the file contents with any ```-leading lines prefixed with a space. func (r Readme) GeminiEscapedContents() string { - return strings.ReplaceAll(r.RawContents, "\n```", "\n ```") + body := r.RawContents + if strings.HasPrefix(body, "```") { + body = " " + body + } + return strings.ReplaceAll(body, "\n```", "\n ```") } +// GopherEscapedContent produces the file formatted as gophermap with every line an info-message line. func (r Readme) GopherEscapedContents(selector, host, port string) string { return gopherRawtext(selector, host, port, r.RawContents) } +// Readme finds a README blob in the root path under a ref string. func (r *Repository) Readme(ctx context.Context, ref string) (*Readme, error) { dir, err := r.Tree(ctx, ref, "") if err != nil { @@ -248,6 +272,7 @@ func (r *Repository) Readme(ctx context.Context, ref string) (*Readme, error) { return nil, nil } +// Description reads the "description" file from in the git repository. func (r *Repository) Description() string { f, err := os.Open(filepath.Join(string(*r), "description")) if err != nil { @@ -263,6 +288,7 @@ func (r *Repository) Description() string { return strings.TrimRight(string(b), "\n") } +// Blob returns the contents of a blob at a given ref (commit) and path. func (r *Repository) Blob(ctx context.Context, ref, path string) ([]byte, error) { res, err := r.cmd(ctx, "cat-file", "blob", ref+":"+path) switch { @@ -277,6 +303,7 @@ func (r *Repository) Blob(ctx context.Context, ref, path string) ([]byte, error) } } +// ObjectDescription represents an object within a git tree (directory). type ObjectDescription struct { Mode int Type string @@ -285,6 +312,7 @@ type ObjectDescription struct { Path string } +// Tree lists the contents of a given directory (path) in a commit (ref). func (r *Repository) Tree(ctx context.Context, ref, path string) ([]ObjectDescription, error) { pattern := ref if path != "" && path != "." { |