forked from tangled.org/core
this repo has no description

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
··· 41 {{ range .Files }} 42 {{ if not .IsFile }} 43 <div class="{{ $containerstyle }}"> 44 - <a 45 - href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref }}/{{ .Name }}" 46 - class="{{ $linkstyle }}" 47 - > 48 - <div class="flex items-center gap-2"> 49 - <i 50 - class="w-3 h-3 fill-current" 51 - data-lucide="folder" 52 - ></i 53 - >{{ .Name }}/ 54 - </div> 55 - </a> 56 </div> 57 {{ end }} 58 {{ end }} ··· 60 {{ range .Files }} 61 {{ if .IsFile }} 62 <div class="{{ $containerstyle }}"> 63 - <a 64 - href="/{{ $.RepoInfo.FullName }}/blob/{{ $.Ref }}/{{ .Name }}" 65 - class="{{ $linkstyle }}" 66 - > 67 - <div class="flex items-center gap-2"> 68 - <i 69 - class="w-3 h-3" 70 - data-lucide="file" 71 - ></i 72 - >{{ .Name }} 73 - </div> 74 - </a> 75 </div> 76 {{ end }} 77 {{ end }}
··· 41 {{ range .Files }} 42 {{ if not .IsFile }} 43 <div class="{{ $containerstyle }}"> 44 + <div class="flex justify-between items-center"> 45 + <a 46 + href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref }}/{{ .Name }}" 47 + class="{{ $linkstyle }}" 48 + > 49 + <div class="flex items-center gap-2"> 50 + <i 51 + class="w-3 h-3 fill-current" 52 + data-lucide="folder" 53 + ></i 54 + >{{ .Name }} 55 + </div> 56 + </a> 57 + 58 + <time class="text-xs text-gray-500">{{ timeFmt .LastCommit.Author.When }}</time> 59 + </div> 60 </div> 61 {{ end }} 62 {{ end }} ··· 64 {{ range .Files }} 65 {{ if .IsFile }} 66 <div class="{{ $containerstyle }}"> 67 + <div class="flex justify-between items-center"> 68 + <a 69 + href="/{{ $.RepoInfo.FullName }}/blob/{{ $.Ref }}/{{ .Name }}" 70 + class="{{ $linkstyle }}" 71 + > 72 + <div class="flex items-center gap-2"> 73 + <i 74 + class="w-3 h-3" 75 + data-lucide="file" 76 + ></i 77 + >{{ .Name }} 78 + </div> 79 + </a> 80 + 81 + <time class="text-xs text-gray-500">{{ timeFmt .LastCommit.Author.When }}</time> 82 + </div> 83 </div> 84 {{ end }} 85 {{ end }}
+18 -10
appview/pages/templates/repo/tree.html
··· 13 {{ range .Files }} 14 {{ if not .IsFile }} 15 <div class="{{ $containerstyle }}"> 16 - <a href="/{{ $.BaseTreeLink }}/{{ .Name }}" class="{{ $linkstyle }}"> 17 - <div class="flex items-center gap-2"> 18 - <i class="w-3 h-3 fill-current" data-lucide="folder"></i>{{ .Name }}/ 19 </div> 20 - </a> 21 </div> 22 {{ end }} 23 {{ end }} 24 25 {{ range .Files }} 26 {{ if .IsFile }} 27 - <div class="{{ $containerstyle }}"> 28 - <a href="/{{ $.BaseBlobLink }}/{{ .Name }}" class="{{ $linkstyle }}"> 29 - <div class="flex items-center gap-2"> 30 - <i class="w-3 h-3" data-lucide="file"></i>{{ .Name }} 31 - </div> 32 - </a> 33 </div> 34 {{ end }} 35 {{ end }} 36 </div>
··· 13 {{ range .Files }} 14 {{ if not .IsFile }} 15 <div class="{{ $containerstyle }}"> 16 + <div class="flex justify-between items-center"> 17 + <a href="/{{ $.BaseTreeLink }}/{{ .Name }}" class="{{ $linkstyle }}"> 18 + <div class="flex items-center gap-2"> 19 + <i class="w-3 h-3 fill-current" data-lucide="folder"></i>{{ .Name }} 20 + </div> 21 + </a> 22 </div> 23 + <time class="text-xs text-gray-500">{{ timeFmt .LastCommit.Author.When }}</time> 24 </div> 25 {{ end }} 26 {{ end }} 27 28 {{ range .Files }} 29 {{ if .IsFile }} 30 + <div class="{{ $containerstyle }}"> 31 + 32 + <div class="flex justify-between items-center"> 33 + <a href="/{{ $.BaseBlobLink }}/{{ .Name }}" class="{{ $linkstyle }}"> 34 + <div class="flex items-center gap-2"> 35 + <i class="w-3 h-3" data-lucide="file"></i>{{ .Name }} 36 + </div> 37 + </a> 38 + 39 + <time class="text-xs text-gray-500">{{ timeFmt .LastCommit.Author.When }}</time> 40 </div> 41 + </div> 42 {{ end }} 43 {{ end }} 44 </div>
+8 -2
appview/state/state.go
··· 519 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 520 return 521 } 522 - if resp.StatusCode != http.StatusNoContent { 523 - s.pages.Notice(w, "repo", fmt.Sprintf("Server returned unexpected status: %d", resp.StatusCode)) 524 return 525 } 526 527 // add to local db
··· 519 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 520 return 521 } 522 + 523 + switch resp.StatusCode { 524 + case http.StatusConflict: 525 + s.pages.Notice(w, "repo", "A repository with that name already exists.") 526 return 527 + case http.StatusInternalServerError: 528 + s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 529 + case http.StatusNoContent: 530 + // continue 531 } 532 533 // add to local db
+1 -1
input.css
··· 21 @apply bg-opacity-30; 22 } 23 a { 24 - @apply underline text-black hover:text-gray-800 visited:text-gray-600; 25 } 26 27 @layer base {
··· 21 @apply bg-opacity-30; 22 } 23 a { 24 + @apply underline text-black hover:text-gray-800; 25 } 26 27 @layer base {
+26 -17
knotserver/git/git.go
··· 2 3 import ( 4 "archive/tar" 5 "fmt" 6 "io" 7 "io/fs" 8 "path" 9 "sort" 10 "sync" 11 "time" 12 ··· 35 ) 36 37 type GitRepo struct { 38 - r *git.Repository 39 - h plumbing.Hash 40 } 41 42 type TagList struct { ··· 102 103 func Open(path string, ref string) (*GitRepo, error) { 104 var err error 105 - g := GitRepo{} 106 g.r, err = git.PlainOpen(path) 107 if err != nil { 108 return nil, fmt.Errorf("opening %s: %w", path, err) ··· 295 return nil 296 } 297 298 - func (g *GitRepo) LastCommitTime(filePath string) (*object.Commit, error) { 299 cacheMu.RLock() 300 - if commit, exists := commitCache.Get(filePath); exists { 301 cacheMu.RUnlock() 302 return commit.(*object.Commit), nil 303 } 304 cacheMu.RUnlock() 305 306 - commitIter, err := g.r.Log(&git.LogOptions{ 307 - From: g.h, 308 - PathFilter: func(s string) bool { 309 - return s == filePath 310 - }, 311 - Order: git.LogOrderCommitterTime, 312 - }) 313 314 - if err != nil { 315 - return nil, fmt.Errorf("failed to get commit log for %s: %w", filePath, err) 316 } 317 318 - commit, err := commitIter.Next() 319 if err != nil { 320 - return nil, fmt.Errorf("no commit found for %s", filePath) 321 } 322 323 cacheMu.Lock() 324 - commitCache.Set(filePath, commit, 1) 325 cacheMu.Unlock() 326 327 return commit, nil
··· 2 3 import ( 4 "archive/tar" 5 + "bytes" 6 "fmt" 7 "io" 8 "io/fs" 9 + "os/exec" 10 "path" 11 "sort" 12 + "strings" 13 "sync" 14 "time" 15 ··· 38 ) 39 40 type GitRepo struct { 41 + path string 42 + r *git.Repository 43 + h plumbing.Hash 44 } 45 46 type TagList struct { ··· 106 107 func Open(path string, ref string) (*GitRepo, error) { 108 var err error 109 + g := GitRepo{path: path} 110 g.r, err = git.PlainOpen(path) 111 if err != nil { 112 return nil, fmt.Errorf("opening %s: %w", path, err) ··· 299 return nil 300 } 301 302 + func (g *GitRepo) LastCommitForPath(path string) (*object.Commit, error) { 303 cacheMu.RLock() 304 + if commit, found := commitCache.Get(path); found { 305 cacheMu.RUnlock() 306 return commit.(*object.Commit), nil 307 } 308 cacheMu.RUnlock() 309 310 + cmd := exec.Command("git", "-C", g.path, "log", "-1", "--format=%H", "--", path) 311 + 312 + var out bytes.Buffer 313 + cmd.Stdout = &out 314 + cmd.Stderr = &out 315 + 316 + if err := cmd.Run(); err != nil { 317 + return nil, fmt.Errorf("failed to get commit hash: %w", err) 318 + } 319 320 + commitHash := strings.TrimSpace(out.String()) 321 + if commitHash == "" { 322 + return nil, fmt.Errorf("no commits found for path: %s", path) 323 } 324 325 + hash := plumbing.NewHash(commitHash) 326 + 327 + commit, err := g.r.CommitObject(hash) 328 if err != nil { 329 + return nil, err 330 } 331 332 cacheMu.Lock() 333 + commitCache.Set(path, commit, 1) 334 cacheMu.Unlock() 335 336 return commit, nil
+11 -4
knotserver/git/tree.go
··· 20 } 21 22 if path == "" { 23 - files = g.makeNiceTree(tree) 24 } else { 25 o, err := tree.FindEntry(path) 26 if err != nil { ··· 33 return nil, err 34 } 35 36 - files = g.makeNiceTree(subtree) 37 } 38 } 39 40 return files, nil 41 } 42 43 - func (g *GitRepo) makeNiceTree(t *object.Tree) []types.NiceTree { 44 nts := []types.NiceTree{} 45 46 for _, e := range t.Entries { 47 mode, _ := e.Mode.ToOSFileMode() 48 sz, _ := t.Size(e.Name) 49 50 - lastCommit, err := g.LastCommitTime(e.Name) 51 if err != nil { 52 continue 53 } 54
··· 20 } 21 22 if path == "" { 23 + files = g.makeNiceTree(tree, "") 24 } else { 25 o, err := tree.FindEntry(path) 26 if err != nil { ··· 33 return nil, err 34 } 35 36 + files = g.makeNiceTree(subtree, path) 37 } 38 } 39 40 return files, nil 41 } 42 43 + func (g *GitRepo) makeNiceTree(t *object.Tree, parent string) []types.NiceTree { 44 nts := []types.NiceTree{} 45 46 for _, e := range t.Entries { 47 mode, _ := e.Mode.ToOSFileMode() 48 sz, _ := t.Size(e.Name) 49 50 + var fpath string 51 + if parent != "" { 52 + fpath = fmt.Sprintf("%s/%s", parent, e.Name) 53 + } else { 54 + fpath = e.Name 55 + } 56 + lastCommit, err := g.LastCommitForPath(fpath) 57 if err != nil { 58 + fmt.Println("error getting last commit time:", err) 59 continue 60 } 61
+8 -2
knotserver/routes.go
··· 17 securejoin "github.com/cyphar/filepath-securejoin" 18 "github.com/gliderlabs/ssh" 19 "github.com/go-chi/chi/v5" 20 "github.com/go-git/go-git/v5/plumbing" 21 "github.com/go-git/go-git/v5/plumbing/object" 22 "github.com/russross/blackfriday/v2" ··· 504 err := git.InitBare(repoPath) 505 if err != nil { 506 l.Error("initializing bare repo", "error", err.Error()) 507 - writeError(w, err.Error(), http.StatusInternalServerError) 508 - return 509 } 510 511 // add perms for this user to access the repo
··· 17 securejoin "github.com/cyphar/filepath-securejoin" 18 "github.com/gliderlabs/ssh" 19 "github.com/go-chi/chi/v5" 20 + gogit "github.com/go-git/go-git/v5" 21 "github.com/go-git/go-git/v5/plumbing" 22 "github.com/go-git/go-git/v5/plumbing/object" 23 "github.com/russross/blackfriday/v2" ··· 505 err := git.InitBare(repoPath) 506 if err != nil { 507 l.Error("initializing bare repo", "error", err.Error()) 508 + if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { 509 + writeError(w, "That repo already exists!", http.StatusConflict) 510 + return 511 + } else { 512 + writeError(w, err.Error(), http.StatusInternalServerError) 513 + return 514 + } 515 } 516 517 // add perms for this user to access the repo