| 43 |
43 |
| 44 static void print_help() { |
44 static void print_help() { |
| 45 fputs( |
45 fputs( |
| 46 "Usage: repoheat [OPTION]... [PATH]...\n\n" |
46 "Usage: repoheat [OPTION]... [PATH]...\n\n" |
| 47 "Options:\n" |
47 "Options:\n" |
| 48 " -a, --author <name> Only report this author\n" |
48 " -a, --author <name> Only report this author\n" |
| 49 " (repeat option to report multiple authors)\n" |
49 " (repeat option to report multiple authors)\n" |
| 50 " -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 to stdout\n" |
51 " --csv Output the gathered data as CSV to stdout\n" |
| 52 " --csv-path <file> Write CSV data to file instead of stdout\n" |
52 " --csv-output <file> Write CSV data to file instead of stdout\n" |
| 53 " -d, --depth <num> The search depth (default: 1, max: 255)\n" |
53 " -d, --depth <num> The search depth (default: 1, max: 255)\n" |
| 54 " -f, --fragment [indent] Output as fragment (HTML only)\n" |
54 " -f, --fragment [indent] Output as fragment (HTML only)\n" |
| 55 " -h, --help Print this help message\n" |
55 " -h, --help Print this help message\n" |
| 56 " -p, --pull Try to pull the repositories\n" |
56 " -o, --output <file> Write output to file instead of stdout\n" |
| 57 " -s, --separate Output a separate heat map for each repository\n" |
57 " -p, --pull Try to pull the repositories\n" |
| 58 " --styles-and-script Output the default CSS and Javascript and quit\n" |
58 " -s, --separate Output a separate heat map for each repository\n" |
| 59 " -V, --version Output the version of this program and exit\n" |
59 " -S, --separate-output <file> Write results for -s to file instead of stdout\n" |
| 60 " -y, --year <year> The year for which to create the heat map\n" |
60 " --styles-and-script Output the default CSS and Javascript and quit\n" |
| 61 " --hg <path> Path to hg binary (default: /usr/bin/hg)\n" |
61 " -V, --version Output the version of this program and exit\n" |
| 62 " --git <path> Path to git binary (default: /usr/bin/git)\n\n" |
62 " -y, --year <year> The year for which to create the heat map\n" |
| |
63 " --hg <path> Path to hg binary (default: /usr/bin/hg)\n" |
| |
64 " --git <path> Path to git binary (default: /usr/bin/git)\n\n" |
| 63 "Scans all specified paths recursively for Mercurial and Git repositories and\n" |
65 "Scans all specified paths recursively for Mercurial and Git repositories and\n" |
| 64 "creates a commit heat map for the specified \033[1myear\033[22m or the current year.\n" |
66 "creates a commit heat map for the specified \033[1myear\033[22m or the current year.\n" |
| 65 "By default, the recursion \033[1mdepth\033[22m is one, meaning that this tool assumes that\n" |
67 "By default, the recursion \033[1mdepth\033[22m is one, meaning that this tool assumes that\n" |
| 66 "each \033[1mpath\033[22m is either a repository or contains repositories as subdirectories.\n" |
68 "each \033[1mpath\033[22m is either a repository or contains repositories as subdirectories.\n" |
| 67 "You can change the \033[1mdepth\033[22m to support other directory structures.\n\n" |
69 "You can change the \033[1mdepth\033[22m to support other directory structures.\n\n" |
| 86 "should \033[4monly\033[24m be used on the left-hand side. When you use the \033[1m--author\033[22m option at\n" |
88 "should \033[4monly\033[24m be used on the left-hand side. When you use the \033[1m--author\033[22m option at\n" |
| 87 "the same time, you only need to specify the new author names.\n\n" |
89 "the same time, you only need to specify the new author names.\n\n" |
| 88 "Finally, this tool prints an HTML page to stdout. A separate heap map is\n" |
90 "Finally, this tool prints an HTML page to stdout. A separate heap map is\n" |
| 89 "generated for each author showing commits across all repositories, unless the\n" |
91 "generated for each author showing commits across all repositories, unless the\n" |
| 90 "\033[1m--separate\033[22m option is specified in which case each repository is displayed with\n" |
92 "\033[1m--separate\033[22m option is specified in which case each repository is displayed with\n" |
| 91 "its own heat map. By using the \033[1m--fragment\033[22m option, the tool only outputs a\n" |
93 "its own heat map.\n" |
| 92 "single HTML div container without any header or footer that can be embedded in\n" |
94 "If you use \033[1m--separate-output\033[22m you can specify a file instead, and both the\n" |
| 93 "your custom web page. You can optionally specify an indentation for that\n" |
95 "aggregated and separated heat maps are written. The aggregated output is still\n" |
| 94 "container (default is 0 and maximum is 12).\n" |
96 "written to stdout unless you specify a file with \033[1m--output\033[22m.\n" |
| |
97 "By using the \033[1m--fragment\033[22m option, the tool only outputs a single HTML div\n" |
| |
98 "container without any header or footer that can be embedded in your custom web\n" |
| |
99 "page. You can optionally specify an indentation for that container (default is\n" |
| |
100 "0 and maximum is 12).\n" |
| 95 "When you want to combine this with the default style and scripts, you can use\n" |
101 "When you want to combine this with the default style and scripts, you can use\n" |
| 96 "the \033[1m--styles-and-script\033[22m option print the defaults to stdout and redirect them\n" |
102 "the \033[1m--styles-and-script\033[22m option print the defaults to stdout and redirect them\n" |
| 97 "into a file when you are composing your custom HTML page.\n\n" |
103 "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" |
104 "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" |
105 "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" |
106 "stdout instead of the HTML. You can use \033[1m--csv-output\033[22m to specify a file, instead.\n" |
| 101 , stderr); |
107 , stderr); |
| 102 } |
108 } |
| 103 |
109 |
| 104 static bool chk_arg(const char *arg, const char *opt1, const char *opt2) { |
110 static bool chk_arg(const char *arg, const char *opt1, const char *opt2) { |
| 105 return strcmp(arg, opt1) == 0 || (opt2 != nullptr && strcmp(arg, opt2) == 0); |
111 return strcmp(arg, opt1) == 0 || (opt2 != nullptr && strcmp(arg, opt2) == 0); |
| 142 fputs("missing author name\n", stderr); |
148 fputs("missing author name\n", stderr); |
| 143 return -1; |
149 return -1; |
| 144 } |
150 } |
| 145 } else if (chk_arg(argv[i], "--csv", nullptr)) { |
151 } else if (chk_arg(argv[i], "--csv", nullptr)) { |
| 146 settings.csv = true; |
152 settings.csv = true; |
| 147 } else if (chk_arg(argv[i], "--csv-path", nullptr)) { |
153 } else if (chk_arg(argv[i], "--csv-output", nullptr)) { |
| 148 settings.csv = true; |
154 if (i + 1 < argc) { |
| 149 if (i + 1 < argc) { |
155 settings.csv = true; |
| 150 settings.csv_path.assign(argv[++i]); |
156 settings.csv_path.assign(argv[++i]); |
| 151 } else { |
157 } else { |
| 152 fputs("missing csv path\n", stderr); |
158 fputs("missing csv path\n", stderr); |
| |
159 return -1; |
| |
160 } |
| |
161 } else if (chk_arg(argv[i], "-o", "--output")) { |
| |
162 if (i + 1 < argc) { |
| |
163 settings.out_path.assign(argv[++i]); |
| |
164 } else { |
| |
165 fputs("missing output path\n", stderr); |
| 153 return -1; |
166 return -1; |
| 154 } |
167 } |
| 155 } else if (chk_arg(argv[i], "-p", "--pull")) { |
168 } else if (chk_arg(argv[i], "-p", "--pull")) { |
| 156 settings.update_repos = true; |
169 settings.update_repos = true; |
| 157 } else if (chk_arg(argv[i], "-f", "--fragment")) { |
170 } else if (chk_arg(argv[i], "-f", "--fragment")) { |
| 162 fputs("invalid fragment indentation\n", stderr); |
175 fputs("invalid fragment indentation\n", stderr); |
| 163 return -1; |
176 return -1; |
| 164 } |
177 } |
| 165 } else if (chk_arg(argv[i], "-s", "--separate")) { |
178 } else if (chk_arg(argv[i], "-s", "--separate")) { |
| 166 settings.separate = true; |
179 settings.separate = true; |
| |
180 } else if (chk_arg(argv[i], "-S", "--separate-output")) { |
| |
181 if (i + 1 < argc) { |
| |
182 settings.separate = true; |
| |
183 settings.separate_path.assign(argv[++i]); |
| |
184 } else { |
| |
185 fputs("missing separate output path\n", stderr); |
| |
186 return -1; |
| |
187 } |
| 167 } else if (chk_arg(argv[i], "-A", "--authormap")) { |
188 } else if (chk_arg(argv[i], "-A", "--authormap")) { |
| 168 if (i + 1 < argc) { |
189 if (i + 1 < argc) { |
| 169 if (settings.parse_authormap(argv[++i])) { |
190 if (settings.parse_authormap(argv[++i])) { |
| 170 fputs("parsing authormap failed\n", stderr); |
191 fputs("parsing authormap failed\n", stderr); |
| 171 return -1; |
192 return -1; |
| 214 |
235 |
| 215 static void generate_html( |
236 static void generate_html( |
| 216 const fm::settings &settings, |
237 const fm::settings &settings, |
| 217 year report_year, year_month_day report_begin, year_month_day report_end, |
238 year report_year, year_month_day report_begin, year_month_day report_end, |
| 218 const fm::heatmap &heatmap) { |
239 const fm::heatmap &heatmap) { |
| 219 |
240 const auto &heatmap_data = settings.separate ? heatmap.separated() : heatmap.aggregated(); |
| 220 html::open(settings.fragment, settings.fragment_indent); |
241 html::open(settings.fragment, settings.fragment_indent); |
| 221 for (const auto &[repo, authors] : heatmap.data()) { |
242 for (const auto &[repo, authors] : heatmap_data) { |
| 222 bool h1_rendered = false; |
243 bool h1_rendered = false; |
| 223 for (const auto &[author, entries] : authors) { |
244 for (const auto &[author, entries] : authors) { |
| 224 if (settings.exclude_author(author)) continue; |
245 if (settings.exclude_author(author)) continue; |
| 225 if (!h1_rendered) { |
246 if (!h1_rendered) { |
| 226 html::heading_repo(repo); |
247 html::heading_repo(repo); |
| 315 result += '"'; |
336 result += '"'; |
| 316 return result; |
337 return result; |
| 317 }; |
338 }; |
| 318 |
339 |
| 319 fprintf(out, "repository,author,date,hash,summary,is_tag\n"); |
340 fprintf(out, "repository,author,date,hash,summary,is_tag\n"); |
| 320 for (const auto &authors: heatmap.data() | std::views::values) { |
341 for (const auto &authors: heatmap.aggregated() | std::views::values) { |
| 321 for (const auto &[author, entries]: authors) { |
342 for (const auto &[author, entries]: authors) { |
| 322 for (const auto &[date, commits]: entries) { |
343 for (const auto &[date, commits]: entries) { |
| 323 for (const auto &[repo, infos] : commits.summaries) { |
344 for (const auto &[repo, infos] : commits.summaries) { |
| 324 for (const auto &info : infos) { |
345 for (const auto &info : infos) { |
| 325 fprintf(out, "%s,%s,%d-%02u-%02u,%s,%s,false\n", |
346 fprintf(out, "%s,%s,%d-%02u-%02u,%s,%s,false\n", |
| 423 }; |
445 }; |
| 424 year_month_day report_begin{report_year, January, 1d}; |
446 year_month_day report_begin{report_year, January, 1d}; |
| 425 year_month_day report_end{report_year, December, 31d}; |
447 year_month_day report_end{report_year, December, 31d}; |
| 426 |
448 |
| 427 // read the commit logs |
449 // read the commit logs |
| 428 fm::heatmap heatmap{settings.separate}; |
450 fm::heatmap heatmap; |
| 429 for (auto &&repo : repos.list()) { |
451 for (auto &&repo : repos.list()) { |
| 430 heatmap.set_repo(repo.name); |
452 heatmap.set_repo(repo.name); |
| 431 proc.chdir(repo.path); |
453 proc.chdir(repo.path); |
| 432 if (repo.type == fm::HG) { |
454 if (repo.type == fm::HG) { |
| 433 proc.setbin(settings.hg); |
455 proc.setbin(settings.hg); |
| 452 } |
474 } |
| 453 heatmap.add(settings, proc.output()); |
475 heatmap.add(settings, proc.output()); |
| 454 } |
476 } |
| 455 } |
477 } |
| 456 |
478 |
| 457 bool html = true; |
479 const bool write_csv = settings.csv; |
| 458 if (settings.csv) { |
480 const bool write_separate = settings.separate; |
| |
481 const bool write_default = !settings.out_path.empty() || ( |
| |
482 !(write_csv && settings.csv_path.empty()) |
| |
483 && !(write_separate && settings.separate_path.empty()) |
| |
484 ); |
| |
485 |
| |
486 if (write_csv) { |
| 459 if (generate_csv(settings, heatmap)) { |
487 if (generate_csv(settings, heatmap)) { |
| |
488 perror("Cannot open file for writing"); |
| 460 return EXIT_FAILURE; |
489 return EXIT_FAILURE; |
| 461 } |
490 } |
| 462 html = !settings.csv_path.empty(); |
491 } |
| 463 } |
492 if (write_separate) { |
| 464 if (html) { |
493 settings.separate = true; |
| |
494 if (html::set_output(settings.separate_path)) { |
| |
495 perror("Cannot open file for writing"); |
| |
496 return EXIT_FAILURE; |
| |
497 } |
| 465 generate_html(settings, report_year, report_begin, report_end, heatmap); |
498 generate_html(settings, report_year, report_begin, report_end, heatmap); |
| 466 } |
499 } |
| |
500 if (write_default) { |
| |
501 settings.separate = false; |
| |
502 if (html::set_output(settings.out_path)) { |
| |
503 perror("Cannot open file for writing"); |
| |
504 return EXIT_FAILURE; |
| |
505 } |
| |
506 generate_html(settings, report_year, report_begin, report_end, heatmap); |
| |
507 } |
| 467 |
508 |
| 468 return EXIT_SUCCESS; |
509 return EXIT_SUCCESS; |
| 469 } |
510 } |