feat/multi-repo
  1package main
  2
  3import (
  4	"context"
  5	"fmt"
  6	"html/template"
  7	"os"
  8	"path/filepath"
  9	"runtime"
 10	"sort"
 11	"strings"
 12	"sync"
 13
 14	"github.com/antonmedv/gitmal/pkg/git"
 15	"github.com/antonmedv/gitmal/pkg/links"
 16	"github.com/antonmedv/gitmal/pkg/progress_bar"
 17	"github.com/antonmedv/gitmal/pkg/templates"
 18)
 19
 20func generateLists(files []git.Blob, params Params) error {
 21	// Build directory indexes
 22	type dirInfo struct {
 23		subdirs map[string]struct{}
 24		files   []git.Blob
 25	}
 26	dirs := map[string]*dirInfo{}
 27
 28	ensureDir := func(p string) *dirInfo {
 29		if di, ok := dirs[p]; ok {
 30			return di
 31		}
 32		di := &dirInfo{subdirs: map[string]struct{}{}, files: []git.Blob{}}
 33		dirs[p] = di
 34		return di
 35	}
 36
 37	dirsSet := links.BuildDirSet(files)
 38	filesSet := links.BuildFileSet(files)
 39
 40	for _, b := range files {
 41		// Normalize to forward slash paths for URL construction
 42		p := b.Path
 43		parts := strings.Split(p, "/")
 44		// walk directories
 45		cur := ""
 46		for i := 0; i < len(parts)-1; i++ {
 47			child := parts[i]
 48			ensureDir(cur).subdirs[child] = struct{}{}
 49			if cur == "" {
 50				cur = child
 51			} else {
 52				cur = cur + "/" + child
 53			}
 54			ensureDir(cur) // ensure it exists
 55		}
 56		ensureDir(cur).files = append(ensureDir(cur).files, b)
 57	}
 58
 59	// Prepare jobs slice to have stable iteration order (optional)
 60	type job struct {
 61		dirPath string
 62		di      *dirInfo
 63	}
 64	jobsSlice := make([]job, 0, len(dirs))
 65	for dp, di := range dirs {
 66		jobsSlice = append(jobsSlice, job{dirPath: dp, di: di})
 67	}
 68	// Sort by dirPath for determinism
 69	sort.Slice(jobsSlice, func(i, j int) bool { return jobsSlice[i].dirPath < jobsSlice[j].dirPath })
 70
 71	// Worker pool similar to generateBlobs
 72	workers := runtime.NumCPU()
 73	if workers < 1 {
 74		workers = 1
 75	}
 76
 77	ctx, cancel := context.WithCancel(context.Background())
 78	defer cancel()
 79
 80	jobCh := make(chan job)
 81	errCh := make(chan error, 1)
 82	var wg sync.WaitGroup
 83
 84	p := progress_bar.NewProgressBar("lists for "+params.Ref.String(), len(jobsSlice))
 85
 86	check := func(err error) bool {
 87		if err != nil {
 88			select {
 89			case errCh <- err:
 90				cancel()
 91			default:
 92			}
 93			return true
 94		}
 95		return false
 96	}
 97
 98	workerFn := func() {
 99		defer wg.Done()
100		for {
101			select {
102			case <-ctx.Done():
103				return
104			case jb, ok := <-jobCh:
105				if !ok {
106					return
107				}
108				func() {
109					dirPath := jb.dirPath
110					di := jb.di
111
112					outDir := filepath.Join(params.OutputDir, "blob", params.Ref.DirName())
113					if dirPath != "" {
114						// convert forward slash path into OS path
115						outDir = filepath.Join(outDir, filepath.FromSlash(dirPath))
116					}
117					if err := os.MkdirAll(outDir, 0o755); check(err) {
118						return
119					}
120
121					// Build entries
122					dirNames := make([]string, 0, len(di.subdirs))
123					for name := range di.subdirs {
124						dirNames = append(dirNames, name)
125					}
126
127					// Sort for stable output
128					sort.Strings(dirNames)
129					sort.Slice(di.files, func(i, j int) bool {
130						return di.files[i].FileName < di.files[j].FileName
131					})
132
133					subdirEntries := make([]templates.ListEntry, 0, len(dirNames))
134					for _, name := range dirNames {
135						subdirEntries = append(subdirEntries, templates.ListEntry{
136							Name:  name + "/",
137							Href:  name + "/index.html",
138							IsDir: true,
139						})
140					}
141
142					fileEntries := make([]templates.ListEntry, 0, len(di.files))
143					for _, b := range di.files {
144						fileEntries = append(fileEntries, templates.ListEntry{
145							Name: b.FileName + "",
146							Href: b.FileName + ".html",
147							Mode: b.Mode,
148							Size: humanizeSize(b.Size),
149						})
150					}
151
152					// Title and current path label
153					title := fmt.Sprintf("%s/%s at %s", params.Name, dirPath, params.Ref)
154					if dirPath == "" {
155						title = fmt.Sprintf("%s at %s", params.Name, params.Ref)
156					}
157
158					f, err := os.Create(filepath.Join(outDir, "index.html"))
159					if check(err) {
160						return
161					}
162					defer func() {
163						_ = f.Close()
164					}()
165
166					// parent link is not shown for root
167					parent := "../index.html"
168					if dirPath == "" {
169						parent = ""
170					}
171
172					depth := 0
173					if dirPath != "" {
174						depth = len(strings.Split(dirPath, "/"))
175					}
176					rootHref := strings.Repeat("../", depth+2)
177
178					readmeHTML := readme(di.files, dirsSet, filesSet, params, rootHref)
179					var CSSMarkdown template.CSS
180					if readmeHTML != "" {
181						CSSMarkdown = cssMarkdown(params.Dark)
182					}
183
184					err = templates.ListTemplate.ExecuteTemplate(f, "layout.gohtml", templates.ListParams{
185						LayoutParams: templates.LayoutParams{
186							Title:         title,
187							Name:          params.Name,
188							SiteName:      params.SiteName,
189							Dark:          params.Dark,
190							CSSMarkdown:   CSSMarkdown,
191							RootHref:      rootHref + params.RootPrefix,
192							RepoHref:      rootHref,
193							CurrentRefDir: params.Ref.DirName(),
194							Selected:      "code",
195							InlineStyles:  params.InlineStyles,
196						},
197						HeaderParams: templates.HeaderParams{
198							Ref:         params.Ref,
199							Breadcrumbs: breadcrumbs(params.Name, dirPath, false),
200						},
201						Ref:        params.Ref,
202						ParentHref: parent,
203						Dirs:       subdirEntries,
204						Files:      fileEntries,
205						Readme:     readmeHTML,
206					})
207					if check(err) {
208						return
209					}
210				}()
211
212				p.Inc()
213			}
214		}
215	}
216
217	// Start workers
218	wg.Add(workers)
219	for i := 0; i < workers; i++ {
220		go workerFn()
221	}
222
223	// Feed jobs
224	go func() {
225		defer close(jobCh)
226		for _, jb := range jobsSlice {
227			select {
228			case <-ctx.Done():
229				return
230			case jobCh <- jb:
231			}
232		}
233	}()
234
235	// Wait for workers or first error
236	doneCh := make(chan struct{})
237	go func() {
238		wg.Wait()
239		close(doneCh)
240	}()
241
242	var runErr error
243	select {
244	case runErr = <-errCh:
245		<-doneCh
246	case <-doneCh:
247	}
248
249	p.Done()
250
251	return runErr
252}