Commit 5fdc5d8

mo khan <mo@mokhan.ca>
2026-01-31 01:24:30
feat: add cmd
1 parent feee7d4
Changed files (1)
cmd
gitmal
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()
+}