1package main23import (4 "embed"5 "flag"6 "fmt"7 "html/template"8 "io/fs"9 "log"10 "os"11 "path/filepath"12 "sort"13 "strings"14 "time"1516 "git.8pit.net/depp/css"17 "git.8pit.net/depp/gitweb"18)1920type Repo struct {21 Name string22 Title string23 Desc string24 Modified time.Time25}2627func (r *Repo) StripedName() string {28 ext := strings.LastIndex(r.Name, ".git")29 if ext > 0 {30 return r.Name[0:ext]31 }32 return r.Name33}3435type Page struct {36 CurPage int37 NumPages int3839 Title string40 Desc string41 Repos []Repo42}4344//go:embed tmpl45var templates embed.FS4647var (48 desc = flag.String("s", "", "short description of git host")49 title = flag.String("t", "depp-index", "page title")50 dest = flag.String("d", "./www", "output directory for HTML files")51 strip = flag.Bool("x", false, "strip .git extension from repository name in link")52 items = flag.Int("p", 20, "amount of repos per HTML page, a zero value disables pagination")53 recursive = flag.Bool("r", false, "recursively search repositories containing git-daemon-export-ok file")54)5556func usage() {57 fmt.Fprintf(flag.CommandLine.Output(),58 "USAGE: %s [FLAGS] REPOSITORY...\n\n"+59 "The following flags are supported:\n\n", os.Args[0])6061 flag.PrintDefaults()62 os.Exit(2)63}6465func repoLink(repo *Repo) string {66 if *strip {67 // Return a post-processed repository name without .git68 return repo.StripedName()69 } else {70 // Return the raw file name, potentially including .git71 return repo.Name72 }73}7475func pageName(page int) string {76 if page == 0 {77 return "index.html"78 } else {79 return fmt.Sprintf("page-%d.html", page)80 }81}8283func pageRefs(page Page) []int {84 pages := make([]int, page.NumPages)85 for i := 0; i < page.NumPages; i++ {86 pages[i] = i87 }88 return pages89}9091func createHTML(page Page, path string) error {92 const name = "base.tmpl"9394 html := template.New(name)95 html.Funcs(template.FuncMap{96 "repoLink": repoLink,97 "pageName": pageName,98 "pageRefs": pageRefs,99 })100101 tmpl, err := html.ParseFS(templates, "tmpl/*.tmpl")102 if err != nil {103 return err104 }105106 file, err := os.Create(path)107 if err != nil {108 return err109 }110 defer file.Close()111112 err = tmpl.Execute(file, page)113 if err != nil {114 return err115 }116117 return nil118}119120func getReposRecursive(fps []string) ([]Repo, error) {121 repos := []Repo{}122 for _, fp := range fps {123 if err := filepath.Walk(fp, func(path string, info fs.FileInfo, err error) error {124 r, err := gitweb.NewRepo(path, nil, 0)125 if err != nil {126 return nil127 }128129 if _, err := os.Stat(filepath.Join(path, "git-daemon-export-ok")); err == nil {130 reponame, err := filepath.Rel(fp, path)131 if err != nil {132 return err133 }134 commit, err := r.Tip()135 if err != nil {136 return err137 }138 desc, err := r.Description()139 if err != nil {140 return err141 }142143 sig := commit.Committer144145 repo := Repo{146 Name: reponame,147 Desc: desc,148 Modified: sig.When,149 }150 repo.Title = repo.StripedName()151 repos = append(repos, repo)152153 }154 return filepath.SkipDir155 }); err != nil {156 return []Repo{}, err157 }158 }159160 sort.Sort(byModified(repos))161 return repos, nil162}163164func getRepos(fps []string) ([]Repo, error) {165 repos := make([]Repo, len(fps))166 for i, fp := range fps {167 r, err := gitweb.NewRepo(fp, nil, 0)168 if err != nil {169 return []Repo{}, err170 }171172 commit, err := r.Tip()173 if err != nil {174 return []Repo{}, err175 }176 desc, err := r.Description()177 if err != nil {178 return []Repo{}, err179 }180181 sig := commit.Committer182 repos[i] = Repo{183 Name: filepath.Base(fp),184 Title: r.Title,185 Desc: desc,186 Modified: sig.When,187 }188 }189190 sort.Sort(byModified(repos))191 return repos, nil192}193194func getPages(repos []Repo) []Page {195 var numPages int196 if *items == 0 {197 numPages = 1198 } else {199 // division is not truncated towards zero200 numPages = 1 + (len(repos)-1) / *items201 }202203 pages := make([]Page, numPages)204 for i := 0; i < numPages; i++ {205 var maxrepos int206 if *items == 0 {207 maxrepos = len(repos)208 } else {209 maxrepos = min(*items, len(repos))210 }211212 pages[i] = Page{213 CurPage: i,214 NumPages: numPages,215 Title: *title,216 Desc: *desc,217 Repos: repos[0:maxrepos],218 }219220 repos = repos[maxrepos:]221 }222223 return pages224}225226func main() {227 flag.Usage = usage228 flag.Parse()229230 log.SetFlags(log.Lshortfile)231 if flag.NArg() == 0 {232 usage()233 }234235 var (236 repos []Repo237 err error238 )239 if *recursive {240 repos, err = getReposRecursive(flag.Args())241 } else {242 repos, err = getRepos(flag.Args())243 }244 if err != nil {245 log.Fatal(err)246 }247 pages := getPages(repos)248249 err = os.MkdirAll(*dest, 0755)250 if err != nil {251 log.Fatal(err)252 }253254 err = css.Create(filepath.Join(*dest, "style.css"))255 if err != nil {256 log.Fatal(err)257 }258259 for _, page := range pages {260 fp := filepath.Join(*dest, pageName(page.CurPage))261 err = createHTML(page, fp)262 if err != nil {263 log.Fatal(err)264 }265 }266}