package main import ( "bytes" "embed" "fmt" "html/template" "log/slog" "os" "path/filepath" "strings" "time" "github.com/alecthomas/kong" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" ) type RepoDir struct { Name string Description string Owner string LastCommit time.Time } type RepoInfo struct { Name string Description string Url string HasReadme bool HasLicense bool HasContributing bool } type CommitFile struct { Mode string Path string Name string Lines int } //go:embed templates var embedTmpl embed.FS var funcMap = template.FuncMap{"inc": func(i int) int { return i + 1 }} // ToDo: add a map function to generate the menu func executeTemplate(name string, data any) ([]byte, error) { path := filepath.Join("templates", fmt.Sprintf("%s.html.tmpl", name)) b, err := embedTmpl.ReadFile(path) if err != nil { return nil, fmt.Errorf("cannot read embedded file %q: %w", path, err) } tmpl, err := template.New("").Funcs(funcMap).Parse(string(b)) if err != nil { return nil, fmt.Errorf("cannot parse template %q: %w", path, err) } var strb bytes.Buffer if err := tmpl.Execute(&strb, data); err != nil { return nil, fmt.Errorf("cannot execute template %q: %w", path, err) } return strb.Bytes(), nil } func readFile(path string) (string, error) { if fi, err := os.Stat(path); err == nil && !fi.IsDir() { b, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("cannot read contents of file %q: %w", path, err) } return strings.TrimSpace(string(b)), nil } return "", nil } func readRepoFile(dirname, name string) (contents string, err error) { contents, err = readFile(filepath.Join(dirname, ".git", name)) if err != nil { return } if contents == "" { contents, err = readFile(filepath.Join(dirname, name)) return } return } func generateIndex(args []string) error { slog.Debug("Generating index", "args", args) repoDirs := []*RepoDir{} for _, dirname := range args { slog.Debug("Processing directory", "dirname", dirname) reponame := strings.TrimSuffix(filepath.Base(dirname), ".git") repoDir := &RepoDir{Name: reponame} // read description description, err := readRepoFile(dirname, "description") if err != nil { return fmt.Errorf("cannot read description for repository %q: %w", dirname, err) } repoDir.Description = description // read owner owner, err := readRepoFile(dirname, "owner") if err != nil { return fmt.Errorf("cannot read owner for repository %q: %w", dirname, err) } repoDir.Owner = owner // get last commit date repo, err := git.PlainOpen(dirname) if err != nil { return fmt.Errorf("cannot open repository %q: %w", dirname, err) } head, err := repo.Head() if err != nil { return fmt.Errorf("cannot get repository head for %q: %w", dirname, err) } c, err := repo.CommitObject(head.Hash()) if err != nil { return fmt.Errorf("cannot get commit %q for repository %q: %w", head.Hash(), dirname, err) } repoDir.LastCommit = c.Author.When repoDirs = append(repoDirs, repoDir) } data := map[string]any{"repoDirs": repoDirs} contents, err := executeTemplate("index", data) if err != nil { return fmt.Errorf("cannot execute index template: %w", err) } if err := os.WriteFile("index.html", contents, 0755); err != nil { return fmt.Errorf("cannot write index contents to \"index.html\": %w", err) } return nil } func generateRepo(path string) error { slog.Debug("Generating repository", "path", path) repoInfo := &RepoInfo{Name: strings.TrimSuffix(filepath.Base(path), ".git")} // create and clean destination directory fi, err := os.Stat(repoInfo.Name) if err == nil && !fi.IsDir() { return fmt.Errorf("path %q is not a directory", repoInfo.Name) } if err != nil && !os.IsNotExist(err) { return fmt.Errorf("cannot stat %q: %w", repoInfo.Name, err) } if err == nil && fi.IsDir() { if err := os.RemoveAll(repoInfo.Name); err != nil { return fmt.Errorf("cannot delete directory for %q: %w", repoInfo.Name, err) } } if err := os.Mkdir(repoInfo.Name, 0755); err != nil { return fmt.Errorf("cannot create directory for %q: %w", repoInfo.Name, err) } // read description description, err := readRepoFile(path, "description") if err != nil { return fmt.Errorf("cannot read description for repository %q: %w", path, err) } repoInfo.Description = description // read url url, err := readRepoFile(path, "url") if err != nil { return fmt.Errorf("cannot read url for repository %q: %w", path, err) } repoInfo.Url = url // open repository repo, err := git.PlainOpen(path) if err != nil { return fmt.Errorf("cannot open repository %q: %w", path, err) } head, err := repo.Head() if err != nil { return fmt.Errorf("cannot get repo head: %w", err) } c, err := repo.CommitObject(head.Hash()) if err != nil { return fmt.Errorf("cannot get commit for hash %s: %w", head.Hash(), err) } // ToDo: populate hasReadme, hasLicense and hasContributing tree, err := c.Tree() if err != nil { return fmt.Errorf("cannot get tree for commit %s: %w", c, err) } slog.Debug("Processing repository tree", "head", head.Hash()) files := []*CommitFile{} iErr := tree.Files().ForEach(func(o *object.File) error { slog.Debug("Processing tree file", "name", o.Name) dirpath := filepath.Join(repoInfo.Name, "file", filepath.Dir(o.Name)) filename := filepath.Base(o.Name) slog.Debug("Creating directory", "dirpath", dirpath) if err := os.MkdirAll(dirpath, 0755); err != nil { return fmt.Errorf("cannot create directory %q: %w", dirpath, err) } data := map[string]any{ "repoInfo": repoInfo, "filename": filename, "filesize": o.Size, } isBinary, err := o.IsBinary() if err != nil { return fmt.Errorf("cannot get binary state for file %q: %w", o.Name, err) } data["isBinary"] = isBinary if !isBinary { lines, err := o.Lines() if err != nil { return fmt.Errorf("cannot get lines for %q: %w", o.Name, err) } data["filelines"] = lines } contents, err := executeTemplate("file", data) if err != nil { return fmt.Errorf("cannot execute file template for file %q: %w", o.Name, err) } dstpath := filepath.Join(dirpath, fmt.Sprintf("%s.html", filename)) if err := os.WriteFile(dstpath, contents, 0755); err != nil { return fmt.Errorf("cannot write file contents to %q: %w", dstpath, err) } file := &CommitFile{ Mode: o.Mode.String(), // ToDo: correctly calculate the mode string Path: filepath.Join("file", fmt.Sprintf("%s.html", o.Name)), Name: o.Name, } files = append(files, file) return nil }) if iErr != nil { return fmt.Errorf("error while processing tree: %w", iErr) } // ToDo: bundle execute and write into a function data := map[string]any{"repoInfo": repoInfo, "files": files} contents, err := executeTemplate("files", data) if err != nil { return fmt.Errorf("cannot execute files template for repository %q: %w", repoInfo.Name, err) } if err := os.WriteFile(filepath.Join(repoInfo.Name, "files.html"), contents, 0755); err != nil { return fmt.Errorf("cannot write files contents to \"files.html\": %w", err) } return nil } func main() { ctx := kong.Parse(&cli) if cli.Debug { slog.SetLogLoggerLevel(slog.LevelDebug) } if err := ctx.Run(); err != nil { fmt.Fprintf(os.Stderr, "error: %s\n", err) os.Exit(1) } }