46 "Options:\n" |
46 "Options:\n" |
47 " -a, --author <name> Only report this author\n" |
47 " -a, --author <name> Only report this author\n" |
48 " (repeat option to report multiple authors)\n" |
48 " (repeat option to report multiple authors)\n" |
49 " -A, --authormap <file> Apply an author mapping file\n" |
49 " -A, --authormap <file> Apply an author mapping file\n" |
50 " -d, --depth <num> The search depth (default: 1, max: 255)\n" |
50 " -d, --depth <num> The search depth (default: 1, max: 255)\n" |
51 " -f, --fragment Output as fragment\n" |
51 " -f, --fragment [indent] Output as fragment\n" |
52 " -h, --help Print this help message\n" |
52 " -h, --help Print this help message\n" |
53 " -p, --pull Try to pull the repositories\n" |
53 " -p, --pull Try to pull the repositories\n" |
54 " -s, --separate Output a separate heat map for each repository\n" |
54 " -s, --separate Output a separate heat map for each repository\n" |
55 " -V, --version Output the version of this program and exit\n" |
55 " -V, --version Output the version of this program and exit\n" |
56 " -y, --year <year> The year for which to create the heat map\n" |
56 " -y, --year <year> The year for which to create the heat map\n" |
84 "Finally, this tool prints an HTML page to stdout. A separate heap map is\n" |
84 "Finally, this tool prints an HTML page to stdout. A separate heap map is\n" |
85 "generated for each author showing commits across all repositories, unless the\n" |
85 "generated for each author showing commits across all repositories, unless the\n" |
86 "\033[1m--separate\033[22m option is specified in which case each repository is displayed with\n" |
86 "\033[1m--separate\033[22m option is specified in which case each repository is displayed with\n" |
87 "its own heat map. By using the \033[1m--fragment\033[22m option, the tool only outputs a\n" |
87 "its own heat map. By using the \033[1m--fragment\033[22m option, the tool only outputs a\n" |
88 "single HTML div container without any header or footer that can be embedded in\n" |
88 "single HTML div container without any header or footer that can be embedded in\n" |
89 "your custom web page.\n" |
89 "your custom web page. You can optionally specify an indentation for that\n" |
|
90 "container (default is 0 and maximum is 12).\n" |
90 , stderr); |
91 , stderr); |
91 } |
92 } |
92 |
93 |
93 static bool chk_arg(const char *arg, const char *opt1, const char *opt2) { |
94 static bool chk_arg(const char *arg, const char *opt1, const char *opt2) { |
94 return strcmp(arg, opt1) == 0 || (opt2 != nullptr && strcmp(arg, opt2) == 0); |
95 return strcmp(arg, opt1) == 0 || (opt2 != nullptr && strcmp(arg, opt2) == 0); |
98 static bool parse_unsigned(const char *str, T *result, unsigned long max) { |
99 static bool parse_unsigned(const char *str, T *result, unsigned long max) { |
99 char *endptr; |
100 char *endptr; |
100 errno = 0; |
101 errno = 0; |
101 unsigned long d = strtoul(str, &endptr, 10); |
102 unsigned long d = strtoul(str, &endptr, 10); |
102 if (*endptr != '\0' || errno == ERANGE) return true; |
103 if (*endptr != '\0' || errno == ERANGE) return true; |
103 if (d < max) { |
104 if (d <= max) { |
104 *result = d; |
105 *result = d; |
105 return false; |
106 return false; |
106 } else { |
107 } else { |
|
108 errno = ERANGE; |
107 return true; |
109 return true; |
108 } |
110 } |
109 } |
111 } |
110 |
112 |
111 static int parse_args(fm::settings &settings, int argc, char *argv[]) { |
113 static int parse_args(fm::settings &settings, int argc, char *argv[]) { |
112 for (int i = 1; i < argc; i++) { |
114 for (int i = 1; i < argc; i++) { |
113 if (chk_arg(argv[i], "-h", "--help")) { |
115 if (chk_arg(argv[i], "-h", "--help")) { |
114 print_help(); |
116 print_help(); |
115 return 1; |
117 return 1; |
116 } else if (chk_arg(argv[i], "-d", "--depth")) { |
118 } else if (chk_arg(argv[i], "-d", "--depth")) { |
117 if (i + 1 >= argc || parse_unsigned(argv[++i], &settings.depth, 256)) { |
119 if (i + 1 >= argc || parse_unsigned(argv[++i], &settings.depth, 255)) { |
118 fputs("missing or invalid depth\n", stderr); |
120 fputs("missing or invalid depth\n", stderr); |
119 return -1; |
121 return -1; |
120 } |
122 } |
121 } else if (chk_arg(argv[i], "-y", "--year")) { |
123 } else if (chk_arg(argv[i], "-y", "--year")) { |
122 if (i + 1 >= argc || parse_unsigned(argv[++i], &settings.year, 9999)) { |
124 if (i + 1 >= argc || parse_unsigned(argv[++i], &settings.year, 9999)) { |
132 } |
134 } |
133 } else if (chk_arg(argv[i], "-p", "--pull")) { |
135 } else if (chk_arg(argv[i], "-p", "--pull")) { |
134 settings.update_repos = true; |
136 settings.update_repos = true; |
135 } else if (chk_arg(argv[i], "-f", "--fragment")) { |
137 } else if (chk_arg(argv[i], "-f", "--fragment")) { |
136 settings.fragment = true; |
138 settings.fragment = true; |
|
139 if (i + 1 < argc && !parse_unsigned(argv[i+1], &settings.fragment_indent, html::max_external_indentation)) { |
|
140 i++; |
|
141 } else if (errno == ERANGE) { |
|
142 fputs("invalid fragment indentation\n", stderr); |
|
143 return -1; |
|
144 } |
137 } else if (chk_arg(argv[i], "-s", "--separate")) { |
145 } else if (chk_arg(argv[i], "-s", "--separate")) { |
138 settings.separate = true; |
146 settings.separate = true; |
139 } else if (chk_arg(argv[i], "-A", "--authormap")) { |
147 } else if (chk_arg(argv[i], "-A", "--authormap")) { |
140 if (i + 1 < argc) { |
148 if (i + 1 < argc) { |
141 if (settings.parse_authormap(argv[++i])) { |
149 if (settings.parse_authormap(argv[++i])) { |
199 } |
207 } |
200 |
208 |
201 // scan for repos |
209 // scan for repos |
202 fm::repositories repos; |
210 fm::repositories repos; |
203 for (auto &&path: settings.paths) { |
211 for (auto &&path: settings.paths) { |
|
212 if (!fm::repositories::exists(path)) { |
|
213 fprintf(stderr, "Path '%s' does not exist!\n", path.c_str()); |
|
214 return EXIT_FAILURE; |
|
215 } |
204 repos.scan(path, settings.depth); |
216 repos.scan(path, settings.depth); |
|
217 } |
|
218 |
|
219 // check if we found something |
|
220 if (repos.count() == 0) { |
|
221 fprintf(stderr, "No repositories found!\n"); |
|
222 return EXIT_FAILURE; |
205 } |
223 } |
206 |
224 |
207 // update repos, if not disabled |
225 // update repos, if not disabled |
208 if (settings.update_repos) { |
226 if (settings.update_repos) { |
209 for (auto &&repo : repos.list()) { |
227 for (auto &&repo : repos.list()) { |
260 } |
278 } |
261 heatmap.add(settings, proc.output()); |
279 heatmap.add(settings, proc.output()); |
262 } |
280 } |
263 } |
281 } |
264 |
282 |
265 html::open(settings.fragment); |
283 html::open(settings.fragment, settings.fragment_indent); |
266 for (const auto &[repo, authors] : heatmap.data()) { |
284 for (const auto &[repo, authors] : heatmap.data()) { |
267 bool h1_rendered = false; |
285 bool h1_rendered = false; |
268 for (const auto &[author, entries] : authors) { |
286 for (const auto &[author, entries] : authors) { |
269 if (settings.exclude_author(author)) continue; |
287 if (settings.exclude_author(author)) continue; |
270 if (!h1_rendered) { |
288 if (!h1_rendered) { |