diff --git a/README.md b/README.md index 3f19766..5545309 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,12 @@ Project heavily inspired by the amazing - [X] Embed templates mechanism. - [ ] Embed style.css +- [ ] Correctly manage relative links. +- [ ] Add a base url to avoid relative links if provided. - [X] Generate the index html file for the `index` subcommand. - [ ] Generate the log html file for a repository. - [ ] Detect and link README, LICENSE and CONTRIBUTING files. -- [ ] Generate the files html file and file structure. +- [X] Generate the files html file and file structure. - [ ] Generate the refs html file. - [X] Add a proper CLI parsing and subcommands. - [ ] Add a sample CSS file for the default templates. @@ -20,9 +22,10 @@ Project heavily inspired by the amazing modified. - [ ] Take binary files into account. - [ ] Limit the output for large diffs. -- [ ] Allow to anchor lines. +- [X] Allow to anchor lines. - [ ] Check if the templates exist on a location and use them if so. Allow to change that location through CLI flags or env vars. +- [ ] Optimize tree generation, it is currently very time consuming. - [ ] Optimize repository generation through a cache. - [ ] Add a flag to regenerate in case a `push -f` comes in. - [ ] Optimize output generation through the use of smaller templates diff --git a/cmd.go b/cmd.go index a04f4b8..7008b1c 100644 --- a/cmd.go +++ b/cmd.go @@ -4,10 +4,18 @@ type indexCmd struct { Paths []string `arg:"" help:"The paths to the repositories to include in the index."` } +func (i *indexCmd) Run() error { + return generateIndex(i.Paths) +} + type repoCmd struct { Path string `arg:"" help:"The path to the repository."` } +func (r *repoCmd) Run() error { + return generateRepo(r.Path) +} + var cli struct { Debug bool `help:"Print debug information."` diff --git a/gitssg.go b/gitssg.go index 69d14a8..8d1c000 100644 --- a/gitssg.go +++ b/gitssg.go @@ -3,7 +3,6 @@ package main import ( "bytes" "embed" - "flag" "fmt" "html/template" "log/slog" @@ -14,6 +13,7 @@ import ( "github.com/alecthomas/kong" "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" ) type RepoDir struct { @@ -23,56 +23,46 @@ type RepoDir struct { 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 }} -func executeTemplate(name string, data any) (string, error) { +// 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 "", fmt.Errorf("cannot read embedded file %q: %w", path, err) + return nil, fmt.Errorf("cannot read embedded file %q: %w", path, err) } - tmpl, err := template.New("").Parse(string(b)) + tmpl, err := template.New("").Funcs(funcMap).Parse(string(b)) if err != nil { - return "", fmt.Errorf("cannot parse template %q: %w", path, err) + return nil, fmt.Errorf("cannot parse template %q: %w", path, err) } var strb bytes.Buffer if err := tmpl.Execute(&strb, data); err != nil { - return "", fmt.Errorf("cannot execute template %q: %w", path, err) + return nil, fmt.Errorf("cannot execute template %q: %w", path, err) } - return strb.String(), nil -} - -func errAndExit(msg string, args ...any) { - fmt.Fprintf(os.Stderr, msg, args...) - os.Exit(1) -} - -func (i *indexCmd) Run() error { - slog.Debug("Generating index", "args", flag.Args()) - - return generateIndex(i.Paths) -} - -func (r *repoCmd) Run() error { - slog.Debug("Generating repository", "path", r.Path) - - return generateRepo(r.Path) -} - -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) - } + return strb.Bytes(), nil } func readFile(path string) (string, error) { @@ -101,18 +91,22 @@ func readRepoFile(dirname, name string) (contents string, err error) { } 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) @@ -139,15 +133,13 @@ func generateIndex(args []string) error { repoDirs = append(repoDirs, repoDir) } - data := map[string]any{ - "repoDirs": repoDirs, - } + 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", []byte(contents), 0755); err != nil { + if err := os.WriteFile("index.html", contents, 0755); err != nil { return fmt.Errorf("cannot write index contents to \"index.html\": %w", err) } @@ -155,5 +147,141 @@ func generateIndex(args []string) error { } 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) + } +} diff --git a/templates/file.html.tmpl b/templates/file.html.tmpl new file mode 100644 index 0000000..64d4b7b --- /dev/null +++ b/templates/file.html.tmpl @@ -0,0 +1,53 @@ + + + + + + {{.filename}} - {{.repoInfo.Name}} - {{.repoInfo.Description}} + + + + + + + + + + + + + + + + + + + {{end}} + {{if .repoInfo.HasContributing}} + | CONTRIBUTING + {{end}} + +

{{.repoInfo.Name}}

{{.repoInfo.Description}}
+ {{if ne .repoInfo.Url ""}} + git clone {{.repoInfo.Url}} + {{end}} +
+ Log + | Files + | Refs + {{if .repoInfo.HasReadme}} + | README + {{end}} + {{if .repoInfo.HasLicense}} + | LICENSE
+
+
+

{{.filename}} ({{.filesize}}B)


+{{- range $i, $line := .filelines}}
+{{- $index := inc $i}}
+{{printf "%7d" $index}} {{$line}}
+{{- end}}
+      
+
+ + diff --git a/templates/files.html.tmpl b/templates/files.html.tmpl new file mode 100644 index 0000000..123bfe0 --- /dev/null +++ b/templates/files.html.tmpl @@ -0,0 +1,55 @@ + + + + + + Files - {{.repoInfo.Name}} - {{.repoInfo.Description}} + + + + + + + + + + + + + + + + + + + + +

stagit

static git page generator
git clone git://git.codemadness.org/stagit
+ Log + | Files + | Refs + | README + | LICENSE +
+
+
+ + + + + + + + + {{range .files}} + + + + + + {{end}} + +
ModeNameSize
{{.Mode}}{{.Name}}{{.Lines}}L
+
+ +