src/main.cpp

changeset 78
5177d8af5536
parent 76
110a234a3260
child 79
cd8ad39dda76
equal deleted inserted replaced
77:43a1ba0e11e3 78:5177d8af5536
33 #include <cstdio> 33 #include <cstdio>
34 #include <cstring> 34 #include <cstring>
35 #include <cerrno> 35 #include <cerrno>
36 36
37 #include <numeric> 37 #include <numeric>
38 #include <ranges>
38 39
39 using namespace std::chrono; 40 using namespace std::chrono;
40 41
41 static constexpr auto program_version = "1.2.0-preview"; 42 static constexpr auto program_version = "1.2.0-preview";
42 43
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 }

mercurial