forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview: show last mod time for files and dirs

Changed files
+104 -60
appview
pages
templates
state
knotserver
+32 -24
appview/pages/templates/repo/index.html
···
{{ range .Files }}
{{ if not .IsFile }}
<div class="{{ $containerstyle }}">
-
<a
-
href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref }}/{{ .Name }}"
-
class="{{ $linkstyle }}"
-
>
-
<div class="flex items-center gap-2">
-
<i
-
class="w-3 h-3 fill-current"
-
data-lucide="folder"
-
></i
-
>{{ .Name }}/
-
</div>
-
</a>
</div>
{{ end }}
{{ end }}
···
{{ range .Files }}
{{ if .IsFile }}
<div class="{{ $containerstyle }}">
-
<a
-
href="/{{ $.RepoInfo.FullName }}/blob/{{ $.Ref }}/{{ .Name }}"
-
class="{{ $linkstyle }}"
-
>
-
<div class="flex items-center gap-2">
-
<i
-
class="w-3 h-3"
-
data-lucide="file"
-
></i
-
>{{ .Name }}
-
</div>
-
</a>
</div>
{{ end }}
{{ end }}
···
{{ range .Files }}
{{ if not .IsFile }}
<div class="{{ $containerstyle }}">
+
<div class="flex justify-between items-center">
+
<a
+
href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref }}/{{ .Name }}"
+
class="{{ $linkstyle }}"
+
>
+
<div class="flex items-center gap-2">
+
<i
+
class="w-3 h-3 fill-current"
+
data-lucide="folder"
+
></i
+
>{{ .Name }}
+
</div>
+
</a>
+
+
<time class="text-xs text-gray-500">{{ timeFmt .LastCommit.Author.When }}</time>
+
</div>
</div>
{{ end }}
{{ end }}
···
{{ range .Files }}
{{ if .IsFile }}
<div class="{{ $containerstyle }}">
+
<div class="flex justify-between items-center">
+
<a
+
href="/{{ $.RepoInfo.FullName }}/blob/{{ $.Ref }}/{{ .Name }}"
+
class="{{ $linkstyle }}"
+
>
+
<div class="flex items-center gap-2">
+
<i
+
class="w-3 h-3"
+
data-lucide="file"
+
></i
+
>{{ .Name }}
+
</div>
+
</a>
+
+
<time class="text-xs text-gray-500">{{ timeFmt .LastCommit.Author.When }}</time>
+
</div>
</div>
{{ end }}
{{ end }}
+18 -10
appview/pages/templates/repo/tree.html
···
{{ range .Files }}
{{ if not .IsFile }}
<div class="{{ $containerstyle }}">
-
<a href="/{{ $.BaseTreeLink }}/{{ .Name }}" class="{{ $linkstyle }}">
-
<div class="flex items-center gap-2">
-
<i class="w-3 h-3 fill-current" data-lucide="folder"></i>{{ .Name }}/
</div>
-
</a>
</div>
{{ end }}
{{ end }}
{{ range .Files }}
{{ if .IsFile }}
-
<div class="{{ $containerstyle }}">
-
<a href="/{{ $.BaseBlobLink }}/{{ .Name }}" class="{{ $linkstyle }}">
-
<div class="flex items-center gap-2">
-
<i class="w-3 h-3" data-lucide="file"></i>{{ .Name }}
-
</div>
-
</a>
</div>
{{ end }}
{{ end }}
</div>
···
{{ range .Files }}
{{ if not .IsFile }}
<div class="{{ $containerstyle }}">
+
<div class="flex justify-between items-center">
+
<a href="/{{ $.BaseTreeLink }}/{{ .Name }}" class="{{ $linkstyle }}">
+
<div class="flex items-center gap-2">
+
<i class="w-3 h-3 fill-current" data-lucide="folder"></i>{{ .Name }}
+
</div>
+
</a>
</div>
+
<time class="text-xs text-gray-500">{{ timeFmt .LastCommit.Author.When }}</time>
</div>
{{ end }}
{{ end }}
{{ range .Files }}
{{ if .IsFile }}
+
<div class="{{ $containerstyle }}">
+
+
<div class="flex justify-between items-center">
+
<a href="/{{ $.BaseBlobLink }}/{{ .Name }}" class="{{ $linkstyle }}">
+
<div class="flex items-center gap-2">
+
<i class="w-3 h-3" data-lucide="file"></i>{{ .Name }}
+
</div>
+
</a>
+
+
<time class="text-xs text-gray-500">{{ timeFmt .LastCommit.Author.When }}</time>
</div>
+
</div>
{{ end }}
{{ end }}
</div>
+8 -2
appview/state/state.go
···
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
return
}
-
if resp.StatusCode != http.StatusNoContent {
-
s.pages.Notice(w, "repo", fmt.Sprintf("Server returned unexpected status: %d", resp.StatusCode))
return
}
// add to local db
···
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
return
}
+
+
switch resp.StatusCode {
+
case http.StatusConflict:
+
s.pages.Notice(w, "repo", "A repository with that name already exists.")
return
+
case http.StatusInternalServerError:
+
s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
+
case http.StatusNoContent:
+
// continue
}
// add to local db
+1 -1
input.css
···
@apply bg-opacity-30;
}
a {
-
@apply underline text-black hover:text-gray-800 visited:text-gray-600;
}
@layer base {
···
@apply bg-opacity-30;
}
a {
+
@apply underline text-black hover:text-gray-800;
}
@layer base {
+26 -17
knotserver/git/git.go
···
import (
"archive/tar"
"fmt"
"io"
"io/fs"
"path"
"sort"
"sync"
"time"
···
)
type GitRepo struct {
-
r *git.Repository
-
h plumbing.Hash
}
type TagList struct {
···
func Open(path string, ref string) (*GitRepo, error) {
var err error
-
g := GitRepo{}
g.r, err = git.PlainOpen(path)
if err != nil {
return nil, fmt.Errorf("opening %s: %w", path, err)
···
return nil
}
-
func (g *GitRepo) LastCommitTime(filePath string) (*object.Commit, error) {
cacheMu.RLock()
-
if commit, exists := commitCache.Get(filePath); exists {
cacheMu.RUnlock()
return commit.(*object.Commit), nil
}
cacheMu.RUnlock()
-
commitIter, err := g.r.Log(&git.LogOptions{
-
From: g.h,
-
PathFilter: func(s string) bool {
-
return s == filePath
-
},
-
Order: git.LogOrderCommitterTime,
-
})
-
if err != nil {
-
return nil, fmt.Errorf("failed to get commit log for %s: %w", filePath, err)
}
-
commit, err := commitIter.Next()
if err != nil {
-
return nil, fmt.Errorf("no commit found for %s", filePath)
}
cacheMu.Lock()
-
commitCache.Set(filePath, commit, 1)
cacheMu.Unlock()
return commit, nil
···
import (
"archive/tar"
+
"bytes"
"fmt"
"io"
"io/fs"
+
"os/exec"
"path"
"sort"
+
"strings"
"sync"
"time"
···
)
type GitRepo struct {
+
path string
+
r *git.Repository
+
h plumbing.Hash
}
type TagList struct {
···
func Open(path string, ref string) (*GitRepo, error) {
var err error
+
g := GitRepo{path: path}
g.r, err = git.PlainOpen(path)
if err != nil {
return nil, fmt.Errorf("opening %s: %w", path, err)
···
return nil
}
+
func (g *GitRepo) LastCommitForPath(path string) (*object.Commit, error) {
cacheMu.RLock()
+
if commit, found := commitCache.Get(path); found {
cacheMu.RUnlock()
return commit.(*object.Commit), nil
}
cacheMu.RUnlock()
+
cmd := exec.Command("git", "-C", g.path, "log", "-1", "--format=%H", "--", path)
+
+
var out bytes.Buffer
+
cmd.Stdout = &out
+
cmd.Stderr = &out
+
+
if err := cmd.Run(); err != nil {
+
return nil, fmt.Errorf("failed to get commit hash: %w", err)
+
}
+
commitHash := strings.TrimSpace(out.String())
+
if commitHash == "" {
+
return nil, fmt.Errorf("no commits found for path: %s", path)
}
+
hash := plumbing.NewHash(commitHash)
+
+
commit, err := g.r.CommitObject(hash)
if err != nil {
+
return nil, err
}
cacheMu.Lock()
+
commitCache.Set(path, commit, 1)
cacheMu.Unlock()
return commit, nil
+11 -4
knotserver/git/tree.go
···
}
if path == "" {
-
files = g.makeNiceTree(tree)
} else {
o, err := tree.FindEntry(path)
if err != nil {
···
return nil, err
}
-
files = g.makeNiceTree(subtree)
}
}
return files, nil
}
-
func (g *GitRepo) makeNiceTree(t *object.Tree) []types.NiceTree {
nts := []types.NiceTree{}
for _, e := range t.Entries {
mode, _ := e.Mode.ToOSFileMode()
sz, _ := t.Size(e.Name)
-
lastCommit, err := g.LastCommitTime(e.Name)
if err != nil {
continue
}
···
}
if path == "" {
+
files = g.makeNiceTree(tree, "")
} else {
o, err := tree.FindEntry(path)
if err != nil {
···
return nil, err
}
+
files = g.makeNiceTree(subtree, path)
}
}
return files, nil
}
+
func (g *GitRepo) makeNiceTree(t *object.Tree, parent string) []types.NiceTree {
nts := []types.NiceTree{}
for _, e := range t.Entries {
mode, _ := e.Mode.ToOSFileMode()
sz, _ := t.Size(e.Name)
+
var fpath string
+
if parent != "" {
+
fpath = fmt.Sprintf("%s/%s", parent, e.Name)
+
} else {
+
fpath = e.Name
+
}
+
lastCommit, err := g.LastCommitForPath(fpath)
if err != nil {
+
fmt.Println("error getting last commit time:", err)
continue
}
+8 -2
knotserver/routes.go
···
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/gliderlabs/ssh"
"github.com/go-chi/chi/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/russross/blackfriday/v2"
···
err := git.InitBare(repoPath)
if err != nil {
l.Error("initializing bare repo", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
}
// add perms for this user to access the repo
···
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/gliderlabs/ssh"
"github.com/go-chi/chi/v5"
+
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/russross/blackfriday/v2"
···
err := git.InitBare(repoPath)
if err != nil {
l.Error("initializing bare repo", "error", err.Error())
+
if errors.Is(err, gogit.ErrRepositoryAlreadyExists) {
+
writeError(w, "That repo already exists!", http.StatusConflict)
+
return
+
} else {
+
writeError(w, err.Error(), http.StatusInternalServerError)
+
return
+
}
}
// add perms for this user to access the repo