feat: log page

Signed-off-by: jolheiser <john.olheiser@gmail.com>
ffdhall
jolheiser 2024-01-15 22:54:43 -06:00
parent 80d0d9b1de
commit ead3a998de
Signed by: jolheiser
GPG Key ID: B853ADA5DA7BBF7A
11 changed files with 369 additions and 19 deletions

View File

@ -89,19 +89,67 @@ func (r Repo) Git() (*git.Repository, error) {
return git.PlainOpen(r.path)
}
// LastCommit returns the last commit of the repo
func (r Repo) LastCommit() (*object.Commit, error) {
// Commit is a git commit
type Commit struct {
SHA string
Message string
Signature string
Author string
Email string
When time.Time
}
func (c Commit) Short() string {
return c.SHA[:8]
}
func (c Commit) Summary() string {
return strings.Split(c.Message, "\n")[0]
}
func (c Commit) Details() string {
return strings.Join(strings.Split(c.Message, "\n")[1:], "\n")
}
// Commit gets a specific commit by SHA
func (r Repo) Commit(sha string) (Commit, error) {
repo, err := r.Git()
if err != nil {
return nil, err
return Commit{}, err
}
return commit(repo, sha)
}
// LastCommit returns the last commit of the repo
func (r Repo) LastCommit() (Commit, error) {
repo, err := r.Git()
if err != nil {
return Commit{}, err
}
head, err := repo.Head()
if err != nil {
return nil, err
return Commit{}, err
}
return repo.CommitObject(head.Hash())
return commit(repo, head.Hash().String())
}
func commit(repo *git.Repository, sha string) (Commit, error) {
obj, err := repo.CommitObject(plumbing.NewHash(sha))
if err != nil {
return Commit{}, err
}
return Commit{
SHA: obj.Hash.String(),
Message: obj.Message,
Signature: obj.PGPSignature,
Author: obj.Author.Name,
Email: obj.Author.Email,
When: obj.Author.When,
}, nil
}
// Branches is all repo branches, default first and sorted alphabetically after that
@ -192,3 +240,40 @@ func (r Repo) Tags() ([]Tag, error) {
return tags, nil
}
// Commits returns commits from a specific hash in descending order
func (r Repo) Commits(ref string) ([]Commit, error) {
repo, err := r.Git()
if err != nil {
return nil, err
}
hash, err := repo.ResolveRevision(plumbing.Revision(ref))
if err != nil {
return nil, err
}
cmts, err := repo.Log(&git.LogOptions{
From: *hash,
})
if err != nil {
return nil, err
}
var commits []Commit
if err := cmts.ForEach(func(commit *object.Commit) error {
commits = append(commits, Commit{
SHA: commit.Hash.String(),
Message: commit.Message,
Signature: commit.PGPSignature,
Author: commit.Author.Name,
Email: commit.Author.Email,
When: commit.Author.When,
})
return nil
}); err != nil {
return nil, err
}
return commits, nil
}

View File

@ -28,9 +28,9 @@ func lastCommit(repo *git.Repo, human bool) string {
return ""
}
if human {
return humanize.Time(c.Author.When)
return humanize.Time(c.When)
}
return c.Author.When.Format("01/02/2006 03:04:05 PM")
return c.When.Format("01/02/2006 03:04:05 PM")
}
templ Index(ic IndexContext) {

View File

@ -38,9 +38,9 @@ func lastCommit(repo *git.Repo, human bool) string {
return ""
}
if human {
return humanize.Time(c.Author.When)
return humanize.Time(c.When)
}
return c.Author.When.Format("01/02/2006 03:04:05 PM")
return c.When.Format("01/02/2006 03:04:05 PM")
}
func Index(ic IndexContext) templ.Component {

View File

@ -0,0 +1,36 @@
package html
import "fmt"
import "github.com/dustin/go-humanize"
import "go.jolheiser.com/ugit/internal/git"
type RepoLogContext struct {
BaseContext
RepoHeaderComponentContext
Commits []git.Commit
}
templ RepoLog(rlc RepoLogContext) {
@base(rlc.BaseContext) {
@repoHeaderComponent(rlc.RepoHeaderComponentContext)
<div class="grid grid-cols-8 gap-5 text-text mt-5">
for _, commit := range rlc.Commits {
<div class="col-span-4">
<div>{ commit.Short() }</div>
<div class="whitespace-pre">
if commit.Details() != "" {
<details><summary class="cursor-pointer">{ commit.Summary() }</summary>{ commit.Details() }</details>
} else {
{ commit.Message }
}
</div>
</div>
<div class="col-span-4">
<div>{ commit.Author }{ " " }<a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("mailto:%s", commit.Email)) }>{ fmt.Sprintf("<%s>", commit.Email) }</a></div>
<div title={ commit.When.Format("01/02/2006 03:04:05 PM") }>{ humanize.Time(commit.When) }</div>
</div>
}
</div>
}
}

View File

@ -0,0 +1,198 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.501
package html
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "fmt"
import "github.com/dustin/go-humanize"
import "go.jolheiser.com/ugit/internal/git"
type RepoLogContext struct {
BaseContext
RepoHeaderComponentContext
Commits []git.Commit
}
func RepoLog(rlc RepoLogContext) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
templ_7745c5c3_Err = repoHeaderComponent(rlc.RepoHeaderComponentContext).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <div class=\"grid grid-cols-8 gap-5 text-text mt-5\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, commit := range rlc.Commits {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-span-4\"><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(commit.Short())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_log.templ`, Line: 18, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"whitespace-pre\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if commit.Details() != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<details><summary class=\"cursor-pointer\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(commit.Summary())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_log.templ`, Line: 21, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</summary>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(commit.Details())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_log.templ`, Line: 21, Col: 96}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</details>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(commit.Message)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_log.templ`, Line: 23, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div><div class=\"col-span-4\"><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(commit.Author)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_log.templ`, Line: 28, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_log.templ`, Line: 28, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a class=\"underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 templ.SafeURL = templ.SafeURL(fmt.Sprintf("mailto:%s", commit.Email))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var9)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("<%s>", commit.Email))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_log.templ`, Line: 28, Col: 213}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></div><div title=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(commit.When.Format("01/02/2006 03:04:05 PM")))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(humanize.Time(commit.When))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_log.templ`, Line: 29, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base(rlc.BaseContext).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -18,7 +18,7 @@ templ RepoRefs(rrc RepoRefsContext) {
<div class="text-text grid grid-cols-8">
for _, branch := range rrc.Branches {
<div class="col-span-1 font-bold">{ branch }</div>
<div class="col-span-7"><a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/", rrc.RepoHeaderComponentContext.Name, branch)) }>tree</a>{ " " }<a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/log/%s/", rrc.RepoHeaderComponentContext.Name, branch)) }>log</a></div>
<div class="col-span-7"><a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/", rrc.RepoHeaderComponentContext.Name, branch)) }>tree</a>{ " " }<a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/log/%s", rrc.RepoHeaderComponentContext.Name, branch)) }>log</a></div>
}
</div>
}
@ -27,12 +27,12 @@ templ RepoRefs(rrc RepoRefsContext) {
<div class="text-text grid grid-cols-8">
for _, tag := range rrc.Tags {
<div class="col-span-1 font-bold">{ tag.Name }</div>
<div class="col-span-7"><a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/", rrc.RepoHeaderComponentContext.Name, tag.Name)) }>tree</a>{ " " }<a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/log/%s/", rrc.RepoHeaderComponentContext.Name, tag.Name)) }>log</a></div>
<div class="col-span-7"><a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/", rrc.RepoHeaderComponentContext.Name, tag.Name)) }>tree</a>{ " " }<a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/log/%s", rrc.RepoHeaderComponentContext.Name, tag.Name)) }>log</a></div>
if tag.Signature != "" {
<details class="col-span-8 whitespace-pre"><summary class="cursor-pointer">Signature</summary><code>{ tag.Signature }</code></details>
}
if tag.Annotation != "" {
<div class="col-span-8">{ tag.Annotation }</div>
<div class="col-span-8 mb-3">{ tag.Annotation }</div>
}
}
</div>

View File

@ -110,7 +110,7 @@ func RepoRefs(rrc RepoRefsContext) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/log/%s/", rrc.RepoHeaderComponentContext.Name, branch))
var templ_7745c5c3_Var8 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/log/%s", rrc.RepoHeaderComponentContext.Name, branch))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var8)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
@ -201,7 +201,7 @@ func RepoRefs(rrc RepoRefsContext) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/log/%s/", rrc.RepoHeaderComponentContext.Name, tag.Name))
var templ_7745c5c3_Var15 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/log/%s", rrc.RepoHeaderComponentContext.Name, tag.Name))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var15)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
@ -252,14 +252,14 @@ func RepoRefs(rrc RepoRefsContext) templ.Component {
return templ_7745c5c3_Err
}
if tag.Annotation != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-span-8\">")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-span-8 mb-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(tag.Annotation)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_refs.templ`, Line: 34, Col: 46}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_refs.templ`, Line: 34, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {

File diff suppressed because one or more lines are too long

View File

@ -81,6 +81,7 @@ func New(settings Settings) Server {
rh.repoTree(chi.URLParam(r, "ref"), chi.URLParam(r, "*")).ServeHTTP(w, r)
})
r.Get("/refs", httperr.Handler(rh.repoRefs))
r.Get("/log/{ref}", httperr.Handler(rh.repoLog))
})
})

View File

@ -30,10 +30,10 @@ func (rh repoHandler) index(w http.ResponseWriter, r *http.Request) error {
sort.Slice(repos, func(i, j int) bool {
var when1, when2 time.Time
if c, err := repos[i].LastCommit(); err == nil {
when1 = c.Author.When
when1 = c.When
}
if c, err := repos[j].LastCommit(); err == nil {
when2 = c.Author.When
when2 = c.When
}
return when1.After(when2)
})

View File

@ -143,3 +143,33 @@ func (rh repoHandler) repoRefs(w http.ResponseWriter, r *http.Request) error {
return nil
}
func (rh repoHandler) repoLog(w http.ResponseWriter, r *http.Request) error {
repoName := chi.URLParam(r, "repo")
repo, err := git.NewRepo(rh.s.RepoDir, repoName)
if err != nil {
httpErr := http.StatusInternalServerError
if errors.Is(err, fs.ErrNotExist) {
httpErr = http.StatusNotFound
}
return httperr.Status(err, httpErr)
}
if repo.Meta.Private {
return httperr.Status(errors.New("could not get git repo"), http.StatusNotFound)
}
commits, err := repo.Commits(chi.URLParam(r, "ref"))
if err != nil {
return httperr.Error(err)
}
if err := html.RepoLog(html.RepoLogContext{
BaseContext: rh.baseContext(),
RepoHeaderComponentContext: rh.repoHeaderContext(repo, r),
Commits: commits,
}).Render(r.Context(), w); err != nil {
return httperr.Error(err)
}
return nil
}