summaryrefslogtreecommitdiff
path: root/cmd/gitmal
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2026-01-30 18:24:30 -0700
committermo khan <mo@mokhan.ca>2026-01-30 18:24:30 -0700
commit5fdc5d8bde0feb99ef450e23c3c9924727951bcb (patch)
tree52c80b99eb9bdeb2157919f034427bfa7c9a5218 /cmd/gitmal
parentfeee7d43ef63ae607c6fd4cca88a356a93553ebe (diff)
feat: add cmd
Diffstat (limited to 'cmd/gitmal')
-rw-r--r--cmd/gitmal/main.go266
1 files changed, 266 insertions, 0 deletions
diff --git a/cmd/gitmal/main.go b/cmd/gitmal/main.go
new file mode 100644
index 0000000..0c29a32
--- /dev/null
+++ b/cmd/gitmal/main.go
@@ -0,0 +1,266 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "runtime/pprof"
+ "strings"
+
+ "mokhan.ca/antonmedv/gitmal/internal/generator"
+ "mokhan.ca/antonmedv/gitmal/internal/git"
+
+ flag "github.com/spf13/pflag"
+)
+
+var (
+ flagOwner string
+ flagName string
+ flagOutput string
+ flagBranches string
+ flagDefaultBranch string
+ flagTheme string
+ flagPreviewThemes bool
+ flagMinify bool
+ flagGzip bool
+)
+
+func main() {
+ if _, ok := os.LookupEnv("GITMAL_PPROF"); ok {
+ f, err := os.Create("cpu.prof")
+ if err != nil {
+ panic(err)
+ }
+ err = pprof.StartCPUProfile(f)
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+ defer pprof.StopCPUProfile()
+ memProf, err := os.Create("mem.prof")
+ if err != nil {
+ panic(err)
+ }
+ defer memProf.Close()
+ defer pprof.WriteHeapProfile(memProf)
+ }
+
+ _, noFiles := os.LookupEnv("NO_FILES")
+ _, noCommitsList := os.LookupEnv("NO_COMMITS_LIST")
+
+ flag.StringVar(&flagOwner, "owner", "", "Project owner")
+ flag.StringVar(&flagName, "name", "", "Project name")
+ flag.StringVar(&flagOutput, "output", "output", "Output directory for generated HTML files")
+ flag.StringVar(&flagBranches, "branches", "", "Regex for branches to include")
+ flag.StringVar(&flagDefaultBranch, "default-branch", "", "Default branch to use (autodetect master or main)")
+ flag.StringVar(&flagTheme, "theme", "github", "Style theme")
+ flag.BoolVar(&flagPreviewThemes, "preview-themes", false, "Preview available themes")
+ flag.BoolVar(&flagMinify, "minify", false, "Minify all generated HTML files")
+ flag.BoolVar(&flagGzip, "gzip", false, "Compress all generated HTML files")
+ flag.Usage = usage
+ flag.Parse()
+
+ input := "."
+ args := flag.Args()
+ if len(args) == 1 {
+ input = args[0]
+ }
+ if len(args) > 1 {
+ panic("Multiple repos not supported yet")
+ }
+
+ if flagPreviewThemes {
+ generator.PreviewThemes()
+ os.Exit(0)
+ }
+
+ outputDir, err := filepath.Abs(flagOutput)
+ if err != nil {
+ panic(err)
+ }
+
+ absInput, err := filepath.Abs(input)
+ if err != nil {
+ panic(err)
+ }
+ input = absInput
+
+ if flagName == "" {
+ flagName = filepath.Base(input)
+ flagName = strings.TrimSuffix(flagName, ".git")
+ }
+
+ themeColor, ok := generator.ThemeStyles[flagTheme]
+ if !ok {
+ panic("Invalid theme: " + flagTheme)
+ }
+
+ branchesFilter, err := regexp.Compile(flagBranches)
+ if err != nil {
+ panic(err)
+ }
+
+ branches, err := git.Branches(input, branchesFilter, flagDefaultBranch)
+ if err != nil {
+ panic(err)
+ }
+
+ tags, err := git.Tags(input)
+ if err != nil {
+ panic(err)
+ }
+
+ if flagDefaultBranch == "" {
+ if generator.ContainsBranch(branches, "master") {
+ flagDefaultBranch = "master"
+ } else if generator.ContainsBranch(branches, "main") {
+ flagDefaultBranch = "main"
+ } else {
+ generator.Echo("No default branch found. Specify one using --default-branch flag.")
+ os.Exit(1)
+ }
+ }
+
+ if !generator.ContainsBranch(branches, flagDefaultBranch) {
+ generator.Echo(fmt.Sprintf("Default branch %q not found.", flagDefaultBranch))
+ generator.Echo("Specify a valid branch using --default-branch flag.")
+ os.Exit(1)
+ }
+
+ if yes, a, b := generator.HasConflictingBranchNames(branches); yes {
+ generator.Echo(fmt.Sprintf("Conflicting branchs %q and %q, both want to use %q dir name.", a, b, a.DirName()))
+ os.Exit(1)
+ }
+
+ params := generator.Params{
+ Owner: flagOwner,
+ Name: flagName,
+ RepoDir: input,
+ OutputDir: outputDir,
+ Style: flagTheme,
+ Dark: themeColor == "dark",
+ DefaultRef: git.NewRef(flagDefaultBranch),
+ }
+
+ commits := make(map[string]git.Commit)
+ commitsFor := make(map[git.Ref][]git.Commit, len(branches))
+
+ for _, branch := range branches {
+ commitsFor[branch], err = git.Commits(branch, params.RepoDir)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, commit := range commitsFor[branch] {
+ if alreadyExisting, ok := commits[commit.Hash]; ok && alreadyExisting.Branch == params.DefaultRef {
+ continue
+ }
+ commit.Branch = branch
+ commits[commit.Hash] = commit
+ }
+ }
+
+ for _, tag := range tags {
+ commitsForTag, err := git.Commits(git.NewRef(tag.Name), params.RepoDir)
+ if err != nil {
+ panic(err)
+ }
+ for _, commit := range commitsForTag {
+ if alreadyExisting, ok := commits[commit.Hash]; ok && !alreadyExisting.Branch.IsEmpty() {
+ continue
+ }
+ commits[commit.Hash] = commit
+ }
+ }
+
+ generator.Echo(fmt.Sprintf("> %s: %d branches, %d tags, %d commits", params.Name, len(branches), len(tags), len(commits)))
+
+ if err := generator.GenerateBranches(branches, flagDefaultBranch, params); err != nil {
+ panic(err)
+ }
+
+ if err := generator.GenerateBranchesJSON(branches, commitsFor, params); err != nil {
+ panic(err)
+ }
+
+ var defaultBranchFiles []git.Blob
+
+ for i, branch := range branches {
+ generator.Echo(fmt.Sprintf("> [%d/%d] %s@%s", i+1, len(branches), params.Name, branch))
+ params.Ref = branch
+
+ if !noFiles {
+ files, err := git.Files(params.Ref, params.RepoDir)
+ if err != nil {
+ panic(err)
+ }
+
+ if branch.String() == flagDefaultBranch {
+ defaultBranchFiles = files
+ }
+
+ if err := generator.GenerateBlobs(files, params); err != nil {
+ panic(err)
+ }
+
+ if err := generator.GenerateLists(files, params); err != nil {
+ panic(err)
+ }
+ }
+
+ if !noCommitsList {
+ if err := generator.GenerateLogForBranch(commitsFor[branch], params); err != nil {
+ panic(err)
+ }
+
+ if err := generator.GenerateCommitsJSON(commitsFor[branch], params); err != nil {
+ panic(err)
+ }
+
+ if err := generator.GenerateCommitsAtom(commitsFor[branch], params); err != nil {
+ panic(err)
+ }
+ }
+ }
+
+ params.Ref = git.NewRef(flagDefaultBranch)
+
+ generator.Echo("> generating commits...")
+ if err := generator.GenerateCommits(commits, params); err != nil {
+ panic(err)
+ }
+
+ if err := generator.GenerateTags(tags, params); err != nil {
+ panic(err)
+ }
+
+ if err := generator.GenerateTagsAtom(tags, params); err != nil {
+ panic(err)
+ }
+
+ if err := generator.GenerateReleasesAtom(tags, params); err != nil {
+ panic(err)
+ }
+
+ if !noFiles {
+ if len(defaultBranchFiles) == 0 {
+ panic("No files found for default branch")
+ }
+ if err := generator.GenerateIndex(defaultBranchFiles, params); err != nil {
+ panic(err)
+ }
+ }
+
+ if flagMinify || flagGzip {
+ generator.Echo("> post-processing HTML...")
+ if err := generator.PostProcessHTML(params.OutputDir, flagMinify, flagGzip); err != nil {
+ panic(err)
+ }
+ }
+}
+
+func usage() {
+ fmt.Fprintf(os.Stderr, "Usage: gitmal [options] [path ...]\n")
+ flag.PrintDefaults()
+}