| 45 "Usage: repoheat [OPTION]... [PATH]...\n\n" |
46 "Usage: repoheat [OPTION]... [PATH]...\n\n" |
| 46 "Options:\n" |
47 "Options:\n" |
| 47 " -a, --author <name> Only report this author\n" |
48 " -a, --author <name> Only report this author\n" |
| 48 " (repeat option to report multiple authors)\n" |
49 " (repeat option to report multiple authors)\n" |
| 49 " -A, --authormap <file> Apply an author mapping file\n" |
50 " -A, --authormap <file> Apply an author mapping file\n" |
| |
51 " --csv Output the gathered data as CSV\n" |
| |
52 " --csv-path <file> Write CSV data to file instead of stdout\n" |
| 50 " -d, --depth <num> The search depth (default: 1, max: 255)\n" |
53 " -d, --depth <num> The search depth (default: 1, max: 255)\n" |
| 51 " -f, --fragment [indent] Output as fragment\n" |
54 " -f, --fragment [indent] Output as fragment (HTML only)\n" |
| 52 " -h, --help Print this help message\n" |
55 " -h, --help Print this help message\n" |
| 53 " -p, --pull Try to pull the repositories\n" |
56 " -p, --pull Try to pull the repositories\n" |
| 54 " -s, --separate Output a separate heat map for each repository\n" |
57 " -s, --separate Output a separate heat map for each repository\n" |
| 55 " --styles-and-script Output the default CSS and Javascript and quit\n" |
58 " --styles-and-script Output the default CSS and Javascript and quit\n" |
| 56 " -V, --version Output the version of this program and exit\n" |
59 " -V, --version Output the version of this program and exit\n" |
| 89 "single HTML div container without any header or footer that can be embedded in\n" |
92 "single HTML div container without any header or footer that can be embedded in\n" |
| 90 "your custom web page. You can optionally specify an indentation for that\n" |
93 "your custom web page. You can optionally specify an indentation for that\n" |
| 91 "container (default is 0 and maximum is 12).\n" |
94 "container (default is 0 and maximum is 12).\n" |
| 92 "When you want to combine this with the default style and scripts, you can use\n" |
95 "When you want to combine this with the default style and scripts, you can use\n" |
| 93 "the \033[1m--styles-and-script\033[22m option print the defaults to stdout and redirect them\n" |
96 "the \033[1m--styles-and-script\033[22m option print the defaults to stdout and redirect them\n" |
| 94 "into a file when you are composing your custom HTML page.\n" |
97 "into a file when you are composing your custom HTML page.\n\n" |
| |
98 "In case you want to work with the raw data, you can advise the tool to output\n" |
| |
99 "the data in CSV format with the \033[1m--csv\033[22m option. The CSV data will be written to\n" |
| |
100 "stdout instead of the HTML unless you specify a different file with \033[1m--csv-path\033[22m.\n" |
| 95 , stderr); |
101 , stderr); |
| 96 } |
102 } |
| 97 |
103 |
| 98 static bool chk_arg(const char *arg, const char *opt1, const char *opt2) { |
104 static bool chk_arg(const char *arg, const char *opt1, const char *opt2) { |
| 99 return strcmp(arg, opt1) == 0 || (opt2 != nullptr && strcmp(arg, opt2) == 0); |
105 return strcmp(arg, opt1) == 0 || (opt2 != nullptr && strcmp(arg, opt2) == 0); |
| 134 settings.authors.emplace_back(argv[++i]); |
140 settings.authors.emplace_back(argv[++i]); |
| 135 } else { |
141 } else { |
| 136 fputs("missing author name\n", stderr); |
142 fputs("missing author name\n", stderr); |
| 137 return -1; |
143 return -1; |
| 138 } |
144 } |
| |
145 } else if (chk_arg(argv[i], "--csv", nullptr)) { |
| |
146 settings.csv = true; |
| |
147 } else if (chk_arg(argv[i], "--csv-path", nullptr)) { |
| |
148 if (i + 1 < argc) { |
| |
149 settings.csv_path.assign(argv[++i]); |
| |
150 } else { |
| |
151 fputs("missing csv path\n", stderr); |
| |
152 return -1; |
| |
153 } |
| 139 } else if (chk_arg(argv[i], "-p", "--pull")) { |
154 } else if (chk_arg(argv[i], "-p", "--pull")) { |
| 140 settings.update_repos = true; |
155 settings.update_repos = true; |
| 141 } else if (chk_arg(argv[i], "-f", "--fragment")) { |
156 } else if (chk_arg(argv[i], "-f", "--fragment")) { |
| 142 settings.fragment = true; |
157 settings.fragment = true; |
| 143 if (i + 1 < argc && !parse_unsigned(argv[i+1], &settings.fragment_indent, html::max_external_indentation)) { |
158 if (i + 1 < argc && !parse_unsigned(argv[i+1], &settings.fragment_indent, html::max_external_indentation)) { |
| 191 |
206 |
| 192 if (settings.paths.empty()) { |
207 if (settings.paths.empty()) { |
| 193 settings.paths.emplace_back("./"); |
208 settings.paths.emplace_back("./"); |
| 194 } |
209 } |
| 195 |
210 |
| |
211 return 0; |
| |
212 } |
| |
213 |
| |
214 static void generate_html( |
| |
215 const fm::settings &settings, |
| |
216 year report_year, year_month_day report_begin, year_month_day report_end, |
| |
217 const fm::heatmap &heatmap) { |
| |
218 |
| |
219 html::open(settings.fragment, settings.fragment_indent); |
| |
220 for (const auto &[repo, authors] : heatmap.data()) { |
| |
221 bool h1_rendered = false; |
| |
222 for (const auto &[author, entries] : authors) { |
| |
223 if (settings.exclude_author(author)) continue; |
| |
224 if (!h1_rendered) { |
| |
225 html::heading_repo(repo); |
| |
226 h1_rendered = true; |
| |
227 } |
| |
228 html::chart_begin(repo, author); |
| |
229 |
| |
230 const auto commits_per_month = heatmap.commits_per_month(repo, author, report_year); |
| |
231 const auto total_commits = std::accumulate(commits_per_month.begin(), commits_per_month.end(), 0u, |
| |
232 [](unsigned sum, const auto &summary) { return sum + summary.count(); }); |
| |
233 html::heading_author(author, total_commits); |
| |
234 html::table_begin(report_year, settings.separate, commits_per_month); |
| |
235 |
| |
236 // initialize counters |
| |
237 unsigned column = 0, row = 0; |
| |
238 |
| |
239 // initialize the first day (which must be a Monday, possibly the year before) |
| |
240 sys_days day_to_check = January / Monday[1] / report_year; |
| |
241 if (year_month_day{day_to_check}.day() != 1d) { |
| |
242 day_to_check -= days{7}; |
| |
243 } |
| |
244 |
| |
245 // remember the starting point |
| |
246 auto start = day_to_check; |
| |
247 |
| |
248 // now add all entries for Monday, Tuesdays, etc. always starting back in January |
| |
249 while (true) { |
| |
250 html::row_begin(row); |
| |
251 |
| |
252 // check if we need to add blank cells |
| |
253 while (day_to_check < report_begin) { |
| |
254 html::cell_out_of_range(); |
| |
255 day_to_check += days{7}; |
| |
256 column++; |
| |
257 } |
| |
258 |
| |
259 while (day_to_check <= report_end) { |
| |
260 // get the entry from the heatmap |
| |
261 auto find_result = entries.find(day_to_check); |
| |
262 if (find_result == entries.end()) { |
| |
263 html::cell(day_to_check); |
| |
264 } else { |
| |
265 html::cell(day_to_check, settings.separate, find_result->second); |
| |
266 } |
| |
267 // advance seven days and one column |
| |
268 day_to_check += days{7}; |
| |
269 column++; |
| |
270 } |
| |
271 // fill remaining columns with blank cells |
| |
272 for (unsigned i = column ; i < html::columns ; i++) { |
| |
273 html::cell_out_of_range(); |
| |
274 } |
| |
275 |
| |
276 // terminate the row |
| |
277 html::row_end(); |
| |
278 |
| |
279 // if we have seen all seven weekdays, that's it |
| |
280 if (++row == 7) break; |
| |
281 |
| |
282 // otherwise, advance the starting point by one day, reset, and begin a new row |
| |
283 start += days{1}; |
| |
284 day_to_check = start; |
| |
285 column =0; |
| |
286 } |
| |
287 |
| |
288 html::table_end(); |
| |
289 html::chart_end(); |
| |
290 } |
| |
291 } |
| |
292 html::close(settings.fragment); |
| |
293 } |
| |
294 |
| |
295 static int generate_csv(const fm::settings &settings, const fm::heatmap &heatmap) { |
| |
296 FILE *out; |
| |
297 if (settings.csv_path.empty()) { |
| |
298 out = stdout; |
| |
299 } else { |
| |
300 out = fopen(settings.csv_path.c_str(), "w"); |
| |
301 if (out == nullptr) return 1; |
| |
302 } |
| |
303 |
| |
304 auto escape_csv = [](const std::string &str) -> std::string { |
| |
305 std::string result; |
| |
306 result.reserve(str.size()+2); |
| |
307 result += '"'; |
| |
308 for (char c : str) { |
| |
309 if (c == '"') { |
| |
310 result += '"'; |
| |
311 } |
| |
312 result += c; |
| |
313 } |
| |
314 result += '"'; |
| |
315 return result; |
| |
316 }; |
| |
317 |
| |
318 fprintf(out, "repository,author,date,hash,summary,is_tag\n"); |
| |
319 for (const auto &authors: heatmap.data() | std::views::values) { |
| |
320 for (const auto &[author, entries]: authors) { |
| |
321 for (const auto &[date, commits]: entries) { |
| |
322 for (const auto &[repo, infos] : commits.summaries) { |
| |
323 for (const auto &info : infos) { |
| |
324 fprintf(out, "%s,%s,%d-%02u-%02u,%s,%s,false\n", |
| |
325 repo.c_str(), |
| |
326 author.c_str(), |
| |
327 static_cast<int>(date.year()), |
| |
328 static_cast<unsigned>(date.month()), |
| |
329 static_cast<unsigned>(date.day()), |
| |
330 info.hash.c_str(), |
| |
331 escape_csv(info.message).c_str() |
| |
332 ); |
| |
333 } |
| |
334 } |
| |
335 for (const auto &[repo, infos] : commits.tags) { |
| |
336 for (const auto &info : infos) { |
| |
337 fprintf(out, "%s,%s,%d-%02u-%02u,%s,%s,true\n", |
| |
338 repo.c_str(), |
| |
339 author.c_str(), |
| |
340 static_cast<int>(date.year()), |
| |
341 static_cast<unsigned>(date.month()), |
| |
342 static_cast<unsigned>(date.day()), |
| |
343 info.hash.c_str(), |
| |
344 escape_csv(info.message).c_str() |
| |
345 ); |
| |
346 } |
| |
347 } |
| |
348 } |
| |
349 } |
| |
350 } |
| |
351 fclose(out); |
| 196 return 0; |
352 return 0; |
| 197 } |
353 } |
| 198 |
354 |
| 199 int main(int argc, char *argv[]) { |
355 int main(int argc, char *argv[]) { |
| 200 // parse settings |
356 // parse settings |
| 295 } |
451 } |
| 296 heatmap.add(settings, proc.output()); |
452 heatmap.add(settings, proc.output()); |
| 297 } |
453 } |
| 298 } |
454 } |
| 299 |
455 |
| 300 html::open(settings.fragment, settings.fragment_indent); |
456 bool html = true; |
| 301 for (const auto &[repo, authors] : heatmap.data()) { |
457 if (settings.csv) { |
| 302 bool h1_rendered = false; |
458 if (generate_csv(settings, heatmap)) { |
| 303 for (const auto &[author, entries] : authors) { |
459 return EXIT_FAILURE; |
| 304 if (settings.exclude_author(author)) continue; |
460 } |
| 305 if (!h1_rendered) { |
461 html = !settings.csv_path.empty(); |
| 306 html::heading_repo(repo); |
462 } |
| 307 h1_rendered = true; |
463 if (html) { |
| 308 } |
464 generate_html(settings, report_year, report_begin, report_end, heatmap); |
| 309 html::chart_begin(repo, author); |
465 } |
| 310 |
|
| 311 const auto commits_per_month = heatmap.commits_per_month(repo, author, report_year); |
|
| 312 const auto total_commits = std::accumulate(commits_per_month.begin(), commits_per_month.end(), 0u, |
|
| 313 [](unsigned sum, const auto &summary) { return sum + summary.count(); }); |
|
| 314 html::heading_author(author, total_commits); |
|
| 315 html::table_begin(report_year, settings.separate, commits_per_month); |
|
| 316 |
|
| 317 // initialize counters |
|
| 318 unsigned column = 0, row = 0; |
|
| 319 |
|
| 320 // initialize the first day (which must be a Monday, possibly the year before) |
|
| 321 sys_days day_to_check = January / Monday[1] / report_year; |
|
| 322 if (year_month_day{day_to_check}.day() != 1d) { |
|
| 323 day_to_check -= days{7}; |
|
| 324 } |
|
| 325 |
|
| 326 // remember the starting point |
|
| 327 auto start = day_to_check; |
|
| 328 |
|
| 329 // now add all entries for Monday, Tuesdays, etc. always starting back in January |
|
| 330 while (true) { |
|
| 331 html::row_begin(row); |
|
| 332 |
|
| 333 // check if we need to add blank cells |
|
| 334 while (day_to_check < report_begin) { |
|
| 335 html::cell_out_of_range(); |
|
| 336 day_to_check += days{7}; |
|
| 337 column++; |
|
| 338 } |
|
| 339 |
|
| 340 while (day_to_check <= report_end) { |
|
| 341 // get the entry from the heatmap |
|
| 342 auto find_result = entries.find(day_to_check); |
|
| 343 if (find_result == entries.end()) { |
|
| 344 html::cell(day_to_check); |
|
| 345 } else { |
|
| 346 html::cell(day_to_check, settings.separate, find_result->second); |
|
| 347 } |
|
| 348 // advance seven days and one column |
|
| 349 day_to_check += days{7}; |
|
| 350 column++; |
|
| 351 } |
|
| 352 // fill remaining columns with blank cells |
|
| 353 for (unsigned i = column ; i < html::columns ; i++) { |
|
| 354 html::cell_out_of_range(); |
|
| 355 } |
|
| 356 |
|
| 357 // terminate the row |
|
| 358 html::row_end(); |
|
| 359 |
|
| 360 // if we have seen all seven weekdays, that's it |
|
| 361 if (++row == 7) break; |
|
| 362 |
|
| 363 // otherwise, advance the starting point by one day, reset, and begin a new row |
|
| 364 start += days{1}; |
|
| 365 day_to_check = start; |
|
| 366 column =0; |
|
| 367 } |
|
| 368 |
|
| 369 html::table_end(); |
|
| 370 html::chart_end(); |
|
| 371 } |
|
| 372 } |
|
| 373 html::close(settings.fragment); |
|
| 374 |
466 |
| 375 return EXIT_SUCCESS; |
467 return EXIT_SUCCESS; |
| 376 } |
468 } |