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