master
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}