Commit b770865

HPCesia <me@hpcesia.com>
2026-07-01 19:02:14
Wire up multi-repo orchestration in main
1 parent e0c777c
blob.go
@@ -141,7 +141,7 @@ func generateBlobs(files []git.Blob, params Params) error {
 								CSSMarkdown:   cssMarkdown(params.Dark),
 								Name:          params.Name,
 								SiteName:      params.SiteName,
-								RootHref:      rootHref,
+								RootHref:      rootHref + params.RootPrefix,
 								RepoHref:      rootHref,
 								CurrentRefDir: params.Ref.DirName(),
 								Selected:      "code",
@@ -202,7 +202,7 @@ func generateBlobs(files []git.Blob, params Params) error {
 								Dark:          params.Dark,
 								Name:          params.Name,
 								SiteName:      params.SiteName,
-								RootHref:      rootHref,
+								RootHref:      rootHref + params.RootPrefix,
 								RepoHref:      rootHref,
 								CurrentRefDir: params.Ref.DirName(),
 								Selected:      "code",
branches.go
@@ -52,7 +52,7 @@ func generateBranches(branches []git.Ref, defaultBranch string, params Params) e
 			Name:          params.Name,
 			SiteName:      params.SiteName,
 			Dark:          params.Dark,
-			RootHref:      rootHref,
+			RootHref:      rootHref + params.RootPrefix,
 			RepoHref:      rootHref,
 			CurrentRefDir: params.DefaultRef.DirName(),
 			Selected:      "branches",
commit.go
@@ -240,7 +240,7 @@ func generateCommitPage(commit git.Commit, params Params) error {
 			Name:          params.Name,
 			SiteName:      params.SiteName,
 			Dark:          params.Dark,
-			RootHref:      rootHref,
+			RootHref:      rootHref + params.RootPrefix,
 			RepoHref:      rootHref,
 			CurrentRefDir: currentRef.DirName(),
 			Selected:      "commits",
commits_list.go
@@ -66,7 +66,7 @@ func generateLogForBranch(allCommits []git.Commit, params Params) error {
 				Name:          params.Name,
 				SiteName:      params.SiteName,
 				Dark:          params.Dark,
-				RootHref:      rootHref,
+				RootHref:      rootHref + params.RootPrefix,
 				RepoHref:      rootHref,
 				CurrentRefDir: params.Ref.DirName(),
 				Selected:      "commits",
index.go
@@ -105,7 +105,7 @@ func generateIndex(files []git.Blob, params Params) error {
 			SiteName:      params.SiteName,
 			Dark:          params.Dark,
 			CSSMarkdown:   cssMarkdown(params.Dark),
-			RootHref:      rootHref,
+			RootHref:      rootHref + params.RootPrefix,
 			RepoHref:      rootHref,
 			CurrentRefDir: params.Ref.DirName(),
 			Selected:      "code",
list.go
@@ -188,7 +188,7 @@ func generateLists(files []git.Blob, params Params) error {
 							SiteName:      params.SiteName,
 							Dark:          params.Dark,
 							CSSMarkdown:   CSSMarkdown,
-							RootHref:      rootHref,
+							RootHref:      rootHref + params.RootPrefix,
 							RepoHref:      rootHref,
 							CurrentRefDir: params.Ref.DirName(),
 							Selected:      "code",
main.go
@@ -86,15 +86,6 @@ func main() {
 	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 {
 		previewThemes()
 		os.Exit(0)
@@ -105,79 +96,177 @@ func main() {
 		panic(err)
 	}
 
-	absInput, err := filepath.Abs(input)
+	styleLight, styleDark, err := resolveTheme(flagTheme, flagThemeLight, flagThemeDark)
 	if err != nil {
 		panic(err)
 	}
-	input = absInput
 
-	if flagName == "" {
-		flagName = filepath.Base(input)
-		flagName = strings.TrimSuffix(flagName, ".git")
+	baseParams := Params{
+		Owner:        flagOwner,
+		Style:        flagTheme,
+		StyleLight:   styleLight,
+		StyleDark:    styleDark,
+		Dark:         themeStyles[flagTheme] == "dark",
+		InlineStyles: flagInlineStyles,
+		OutputDir:    outputDir,
 	}
 
-	styleLight, styleDark, err := resolveTheme(flagTheme, flagThemeLight, flagThemeDark)
-	if err != nil {
-		panic(err)
+	var siteName string
+	isMulti := false
+	var generatables []repoGeneration
+
+	if flagConfig != "" {
+		cfg, err := parseConfig(flagConfig)
+		if err != nil {
+			panic(err)
+		}
+		siteName = cfg.SiteName
+
+		if len(cfg.Repos) > 0 {
+			isMulti = true
+			for _, repo := range cfg.Repos {
+				if err := validateRepoPath(repo.Path); err != nil {
+					panic(fmt.Errorf("repos %q: %w", repo.Slug, err))
+				}
+				generatables = append(generatables, repoGeneration{
+					RepoDir:       repo.Path,
+					Name:          repo.Name,
+					Slug:          repo.Slug,
+					Description:   repo.Description,
+					DefaultBranch: repo.DefaultBranch,
+				})
+			}
+		} else if cfg.Repo != nil {
+			inputPath, err := repoInputPath()
+			if err != nil {
+				panic(err)
+			}
+			repoPath := inputPath
+			if repoPath == "." {
+				repoPath = cfg.Repo.Path
+			}
+			repoName := flagName
+			if repoName == "" {
+				repoName = cfg.Repo.Name
+			}
+			defaultBranch := flagDefaultBranch
+			if defaultBranch == "" {
+				defaultBranch = cfg.Repo.DefaultBranch
+			}
+			if err := validateRepoPath(repoPath); err != nil {
+				panic(err)
+			}
+			generatables = append(generatables, repoGeneration{
+				RepoDir:       repoPath,
+				Name:          repoName,
+				DefaultBranch: defaultBranch,
+			})
+		}
+	}
+
+	if len(generatables) == 0 {
+		inputPath, err := repoInputPath()
+		if err != nil {
+			panic(err)
+		}
+		repoName := flagName
+		if repoName == "" {
+			repoName = filepath.Base(inputPath)
+			repoName = strings.TrimSuffix(repoName, ".git")
+		}
+		if err := validateRepoPath(inputPath); err != nil {
+			panic(err)
+		}
+		generatables = append(generatables, repoGeneration{
+			RepoDir:       inputPath,
+			Name:          repoName,
+			DefaultBranch: flagDefaultBranch,
+		})
+	}
+
+	baseParams.SiteName = siteName
+
+	if !baseParams.InlineStyles {
+		if err := generateCSSFiles(baseParams); err != nil {
+			panic(err)
+		}
+	}
+
+	for _, g := range generatables {
+		params := baseParams
+		params.RepoDir = g.RepoDir
+		params.Name = g.Name
+		params.SiteName = siteName
+		params.RootPrefix = ""
+
+		if isMulti {
+			params.OutputDir = filepath.Join(outputDir, g.Slug)
+			params.RootPrefix = "../"
+		}
+
+		if err := generateRepo(g, params, noFiles, noCommitsList); err != nil {
+			panic(err)
+		}
 	}
 
+	if isMulti {
+		if err := generateMultiRepoIndex(cfgReposToEntries(generatables), siteName, baseParams); err != nil {
+			panic(err)
+		}
+	}
+
+	if flagMinify || flagGzip {
+		echo("> post-processing HTML...")
+		if err := postProcessHTML(outputDir, flagMinify, flagGzip); err != nil {
+			panic(err)
+		}
+	}
+}
+
+type repoGeneration struct {
+	RepoDir       string
+	Name          string
+	Slug          string
+	Description   string
+	DefaultBranch string
+}
+
+func generateRepo(g repoGeneration, params Params, noFiles, noCommitsList bool) error {
 	branchesFilter, err := regexp.Compile(flagBranches)
 	if err != nil {
-		panic(err)
+		return err
 	}
 
-	branches, err := git.Branches(input, branchesFilter, flagDefaultBranch)
+	branches, err := git.Branches(params.RepoDir, branchesFilter, g.DefaultBranch)
 	if err != nil {
-		panic(err)
+		return err
 	}
 
-	tags, err := git.Tags(input)
+	tags, err := git.Tags(params.RepoDir)
 	if err != nil {
-		panic(err)
+		return err
 	}
 
-	if flagDefaultBranch == "" {
+	defaultBranch := g.DefaultBranch
+	if defaultBranch == "" {
 		if containsBranch(branches, "master") {
-			flagDefaultBranch = "master"
+			defaultBranch = "master"
 		} else if containsBranch(branches, "main") {
-			flagDefaultBranch = "main"
+			defaultBranch = "main"
 		} else {
-			echo("No default branch found. Specify one using --default-branch flag.")
-			os.Exit(1)
+			return fmt.Errorf("no default branch found in %s", params.RepoDir)
 		}
 	}
 
-	if !containsBranch(branches, flagDefaultBranch) {
-		echo(fmt.Sprintf("Default branch %q not found.", flagDefaultBranch))
-		echo("Specify a valid branch using --default-branch flag.")
-		os.Exit(1)
+	if !containsBranch(branches, defaultBranch) {
+		return fmt.Errorf("default branch %q not found in %s", defaultBranch, params.RepoDir)
 	}
 
 	if yes, a, b := hasConflictingBranchNames(branches); yes {
-		echo(fmt.Sprintf("Conflicting branchs %q and %q, both want to use %q dir name.", a, b, a.DirName()))
-		os.Exit(1)
-	}
-
-	// Start generating pages
-
-	params := Params{
-		Owner:        flagOwner,
-		Name:         flagName,
-		RepoDir:      input,
-		OutputDir:    outputDir,
-		Style:        flagTheme,
-		StyleLight:   styleLight,
-		StyleDark:    styleDark,
-		Dark:         themeStyles[flagTheme] == "dark",
-		DefaultRef:   git.NewRef(flagDefaultBranch),
-		InlineStyles: flagInlineStyles,
+		return fmt.Errorf("conflicting branch names %q and %q, both want to use %q dir name", a, b, a.DirName())
 	}
 
-	if !params.InlineStyles {
-		if err := generateCSSFiles(params); err != nil {
-			panic(err)
-		}
-	}
+	params.DefaultRef = git.NewRef(defaultBranch)
 
 	commits := make(map[string]git.Commit)
 	commitsFor := make(map[git.Ref][]git.Commit, len(branches))
@@ -185,7 +274,7 @@ func main() {
 	for _, branch := range branches {
 		commitsFor[branch], err = git.Commits(branch, params.RepoDir)
 		if err != nil {
-			panic(err)
+			return err
 		}
 
 		for _, commit := range commitsFor[branch] {
@@ -197,14 +286,12 @@ func main() {
 		}
 	}
 
-	// Add commits from tags
 	for _, tag := range tags {
 		commitsForTag, err := git.Commits(git.NewRef(tag.Name), params.RepoDir)
 		if err != nil {
-			panic(err)
+			return err
 		}
 		for _, commit := range commitsForTag {
-			// Only add new commits
 			if alreadyExisting, ok := commits[commit.Hash]; ok && !alreadyExisting.Branch.IsEmpty() {
 				continue
 			}
@@ -214,8 +301,8 @@ func main() {
 
 	echo(fmt.Sprintf("> %s: %d branches, %d tags, %d commits", params.Name, len(branches), len(tags), len(commits)))
 
-	if err := generateBranches(branches, flagDefaultBranch, params); err != nil {
-		panic(err)
+	if err := generateBranches(branches, defaultBranch, params); err != nil {
+		return err
 	}
 
 	var defaultBranchFiles []git.Blob
@@ -227,72 +314,110 @@ func main() {
 		if !noFiles {
 			files, err := git.Files(params.Ref, params.RepoDir)
 			if err != nil {
-				panic(err)
+				return err
 			}
 
-			if branch.String() == flagDefaultBranch {
+			if branch.String() == defaultBranch {
 				defaultBranchFiles = files
 			}
 
 			err = generateBlobs(files, params)
 			if err != nil {
-				panic(err)
+				return err
 			}
 
 			err = generateLists(files, params)
 			if err != nil {
-				panic(err)
+				return err
 			}
 		}
 
 		if !noCommitsList {
 			err = generateLogForBranch(commitsFor[branch], params)
 			if err != nil {
-				panic(err)
+				return err
 			}
 		}
 	}
 
-	// Back to the default branch
-	params.Ref = git.NewRef(flagDefaultBranch)
+	params.Ref = git.NewRef(defaultBranch)
 
-	// Commits pages generation
 	echo("> generating commits...")
 	err = generateCommits(commits, params)
 	if err != nil {
-		panic(err)
+		return err
 	}
 
-	// Tags page generation
 	if err := generateTags(tags, params); err != nil {
-		panic(err)
+		return err
 	}
 
-	// Git dumb protocol files generation
 	if flagGit {
 		echo("> generating dumb protocol files...")
-		if err := generateDumbProtocol(params, branches, flagDefaultBranch, tags, commitsFor, commits); err != nil {
-			panic(err)
+		if err := generateDumbProtocol(params, branches, defaultBranch, tags, commitsFor, commits); err != nil {
+			return err
 		}
 	}
 
-	// Index page generation
 	if !noFiles {
 		if len(defaultBranchFiles) == 0 {
-			panic("No files found for default branch")
+			return fmt.Errorf("no files found for default branch in %s", params.RepoDir)
 		}
 		err = generateIndex(defaultBranchFiles, params)
 		if err != nil {
-			panic(err)
+			return err
 		}
 	}
 
-	if flagMinify || flagGzip {
-		echo("> post-processing HTML...")
-		if err := postProcessHTML(params.OutputDir, flagMinify, flagGzip); err != nil {
-			panic(err)
+	return nil
+}
+
+func repoInputPath() (string, error) {
+	args := flag.Args()
+	if len(args) == 0 {
+		abs, err := filepath.Abs(".")
+		if err != nil {
+			return "", err
+		}
+		return abs, nil
+	}
+	if len(args) > 1 {
+		return "", fmt.Errorf("multiple positional args not supported with --config; use [[repos]] in config instead")
+	}
+	abs, err := filepath.Abs(args[0])
+	if err != nil {
+		return "", err
+	}
+	return abs, nil
+}
+
+func validateRepoPath(path string) error {
+	info, err := os.Stat(path)
+	if err != nil {
+		return fmt.Errorf("repo path %q: %w", path, err)
+	}
+	if !info.IsDir() {
+		return fmt.Errorf("repo path %q is not a directory", path)
+	}
+	gitDir := filepath.Join(path, ".git")
+	if info, err := os.Stat(gitDir); err != nil || !info.IsDir() {
+		return fmt.Errorf("repo path %q does not contain a .git directory", path)
+	}
+	return nil
+}
+
+func cfgReposToEntries(gs []repoGeneration) []RepoEntry {
+	entries := make([]RepoEntry, len(gs))
+	for i, g := range gs {
+		entries[i] = RepoEntry{
+			Name:          g.Name,
+			Slug:          g.Slug,
+			Path:          g.RepoDir,
+			Description:   g.Description,
+			DefaultBranch: g.DefaultBranch,
 		}
 	}
+	return entries
 }
 
 func usage() {
tags.go
@@ -29,7 +29,7 @@ func generateTags(entries []git.Tag, params Params) error {
 			Name:          params.Name,
 			SiteName:      params.SiteName,
 			Dark:          params.Dark,
-			RootHref:      rootHref,
+			RootHref:      rootHref + params.RootPrefix,
 			RepoHref:      rootHref,
 			CurrentRefDir: params.DefaultRef.DirName(),
 			Selected:      "tags",