Sat, 01 Feb 2025 14:11:46 +0100
use chrono API to determine the first monday in the year instead of calculating it
0 | 1 | /* Copyright 2025 Mike Becker. All rights reserved. |
2 | * | |
3 | * Redistribution and use in source and binary forms, with or without | |
4 | * modification, are permitted provided that the following conditions are met: | |
5 | * | |
6 | * 1. Redistributions of source code must retain the above copyright | |
7 | * notice, this list of conditions and the following disclaimer. | |
8 | * | |
9 | * 2. Redistributions in binary form must reproduce the above copyright | |
10 | * notice, this list of conditions and the following disclaimer in the | |
11 | * documentation and/or other materials provided with the distribution. | |
12 | * | |
13 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
14 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
15 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
16 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
17 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
18 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
19 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
20 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
21 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
22 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
23 | */ | |
24 | ||
1 | 25 | #include "settings.h" |
3 | 26 | #include "repositories.h" |
4
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
27 | #include "process.h" |
5 | 28 | #include "heatmap.h" |
29 | #include "html.h" | |
1 | 30 | |
5 | 31 | #include <chrono> |
1 | 32 | #include <cstdlib> |
33 | #include <cstdio> | |
34 | #include <cstring> | |
35 | #include <cerrno> | |
36 | ||
5 | 37 | namespace chrono = std::chrono; |
11
127fb6a8f706
simplify day and month literals
Mike Becker <universe@uap-core.de>
parents:
9
diff
changeset
|
38 | using chrono::operator ""d; |
5 | 39 | |
1 | 40 | static void print_help() { |
41 | fputs( | |
42 | "Usage: fallusmeter [OPTION]... [PATH]...\n\n" | |
43 | "Options:\n" | |
44 | " -h, --help Print this help message\n" | |
45 | " -d, --depth <num> The search depth (default: 1, max: 255)\n" | |
46 | " -n, --no-pull Do not pull the repositories\n" | |
47 | " -s, --separate Output a separate heat map for each repository\n" | |
48 | " -y, --year <year> The year for which to create the heat map\n" | |
49 | " --hg <path> Path to hg binary (default: /usr/bin/hg)\n" | |
50 | " --git <path> Path to git binary (default: /usr/bin/git)\n\n" | |
51 | "Scans all specified paths recursively for Mercurial and Git repositories and\n" | |
52 | "creates a commit heat map for the specified \033[1myear\033[22m or the current year.\n" | |
53 | "By default, the recursion \033[1mdepth\033[22m is one, meaning that this tool assumes that\n" | |
54 | "each \033[1mpath\033[22m is either a repository or contains repositories as subdirectories.\n" | |
55 | "You can change the \033[1mdepth\033[22m to support other directory structures.\n\n" | |
56 | "When you do not specify \033[1m--no-pull\033[22m, this tool will execute the pull command\n" | |
57 | "(and for hg the update command) before retrieving the commit log, assuming\n" | |
9
98312f94dbdd
skip authorization requests when pulling
Mike Becker <universe@uap-core.de>
parents:
8
diff
changeset
|
58 | "to be on the default branch with \033[4mno uncommitted changes\033[24m. If pulling leads to\n" |
98312f94dbdd
skip authorization requests when pulling
Mike Becker <universe@uap-core.de>
parents:
8
diff
changeset
|
59 | "an error, an error message is written to stderr and the process continues\n" |
98312f94dbdd
skip authorization requests when pulling
Mike Becker <universe@uap-core.de>
parents:
8
diff
changeset
|
60 | "with the repository in its current state. This is also the case when pulling\n" |
98312f94dbdd
skip authorization requests when pulling
Mike Becker <universe@uap-core.de>
parents:
8
diff
changeset
|
61 | "requires authorization.\n\n" |
3 | 62 | "Afterwards, this tool prints an HTML page to stdout. A separate heap map is\n" |
63 | "generated for each author showing commits across all repositories, unless the\n" | |
64 | "\033[1m--separate\033[22m option is specified in which case each repository is displayed with\n" | |
65 | "its own heat map.\n" | |
1 | 66 | , stderr); |
67 | } | |
68 | ||
3 | 69 | static bool chk_arg(const char *arg, const char *opt1, const char *opt2) { |
1 | 70 | return strcmp(arg, opt1) == 0 || (opt2 != nullptr && strcmp(arg, opt2) == 0); |
71 | } | |
72 | ||
73 | template<typename T> | |
74 | static bool parse_unsigned(const char *str, T *result, unsigned long max) { | |
75 | char *endptr; | |
76 | errno = 0; | |
77 | unsigned long d = strtoul(str, &endptr, 10); | |
78 | if (*endptr != '\0' || errno == ERANGE) return true; | |
79 | if (d < max) { | |
80 | *result = d; | |
81 | return false; | |
82 | } else { | |
83 | return true; | |
84 | } | |
85 | } | |
86 | ||
87 | static int parse_args(fm::settings &settings, int argc, char *argv[]) { | |
88 | for (int i = 1; i < argc; i++) { | |
89 | if (chk_arg(argv[i], "-h", "--help")) { | |
90 | print_help(); | |
91 | return -1; | |
92 | } else if (chk_arg(argv[i], "-d", "--depth")) { | |
93 | if (i + 1 >= argc || parse_unsigned(argv[++i], &settings.depth, 256)) { | |
94 | fputs("missing or invalid depth\n", stderr); | |
95 | return -1; | |
96 | } | |
97 | } else if (chk_arg(argv[i], "-y", "--year")) { | |
98 | if (i + 1 >= argc || parse_unsigned(argv[++i], &settings.year, 9999)) { | |
99 | fputs("missing or invalid year\n", stderr); | |
100 | return -1; | |
101 | } | |
102 | } else if (chk_arg(argv[i], "-n", "--no-pull")) { | |
3 | 103 | settings.update_repos = false; |
1 | 104 | } else if (chk_arg(argv[i], "-s", "--separate")) { |
105 | settings.separate = true; | |
106 | } else if (chk_arg(argv[i], "--hg", nullptr)) { | |
107 | if (i + 1 < argc) { | |
108 | settings.hg.assign(argv[++i]); | |
109 | } else { | |
110 | fputs("--hg is expecting a path\n", stderr); | |
111 | return -1; | |
112 | } | |
113 | } else if (chk_arg(argv[i], "--git", nullptr)) { | |
114 | if (i + 1 < argc) { | |
115 | settings.git.assign(argv[++i]); | |
116 | } else { | |
117 | fputs("--git is expecting a path\n", stderr); | |
118 | return -1; | |
119 | } | |
120 | } else if (argv[i][0] == '-') { | |
121 | fprintf(stderr, "Unknown option: %s\n", argv[i]); | |
122 | return -1; | |
123 | } else { | |
124 | settings.paths.emplace_back(argv[i]); | |
125 | } | |
126 | } | |
127 | ||
128 | if (settings.paths.empty()) { | |
129 | settings.paths.emplace_back("./"); | |
130 | } | |
131 | ||
0 | 132 | return 0; |
133 | } | |
134 | ||
1 | 135 | int main(int argc, char *argv[]) { |
4
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
136 | // parse settings |
1 | 137 | fm::settings settings; |
138 | if (parse_args(settings, argc, argv)) { | |
139 | return EXIT_FAILURE; | |
140 | } | |
141 | ||
4
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
142 | // check hg and git |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
143 | fm::process proc; |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
144 | proc.setbin(settings.hg); |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
145 | if (proc.exec({"--version"})) { |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
146 | fprintf(stderr, "Testing hg binary '%s' failed!\n", settings.hg.c_str()); |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
147 | return EXIT_FAILURE; |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
148 | } |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
149 | proc.setbin(settings.git); |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
150 | if (proc.exec({"--version"})) { |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
151 | fprintf(stderr, "Testing git binary '%s' failed!\n", settings.git.c_str()); |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
152 | return EXIT_FAILURE; |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
153 | } |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
154 | |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
155 | // scan for repos |
3 | 156 | fm::repositories repos; |
157 | for (auto &&path: settings.paths) { | |
158 | repos.scan(path, settings.depth); | |
159 | } | |
4
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
160 | |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
161 | // update repos, if not disabled |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
162 | if (settings.update_repos) { |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
163 | for (auto &&repo : repos.list()) { |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
164 | proc.chdir(repo.path); |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
165 | if (repo.type == fm::HG) { |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
166 | proc.setbin(settings.hg); |
9
98312f94dbdd
skip authorization requests when pulling
Mike Becker <universe@uap-core.de>
parents:
8
diff
changeset
|
167 | if (proc.exec({"pull", "-y"})) { |
4
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
168 | fprintf(stderr, "Pulling repo '%s' failed!\nMaybe there is no remote?\n", repo.path.c_str()); |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
169 | } else if (proc.exec({"update"})) { |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
170 | fprintf(stderr, "Updating repo '%s' failed!\nMaybe there are local changes?\n", repo.path.c_str()); |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
171 | } |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
172 | } else { |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
173 | proc.setbin(settings.git); |
9
98312f94dbdd
skip authorization requests when pulling
Mike Becker <universe@uap-core.de>
parents:
8
diff
changeset
|
174 | if (proc.exec({"pull", "-q"})) { |
4
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
175 | fprintf(stderr, "Pulling repo '%s' failed!\nMaybe there is no remote or there are local changes?\n", repo.path.c_str()); |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
176 | } |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
177 | } |
82680ce258d6
add automatic pull/udate of repositories
Mike Becker <universe@uap-core.de>
parents:
3
diff
changeset
|
178 | } |
3 | 179 | } |
5 | 180 | |
181 | // determine our reporting range | |
182 | int year; | |
183 | if (settings.year == fm::settings_current_year) { | |
184 | year = static_cast<int>(chrono::year_month_day{chrono::floor<chrono::days>(chrono::system_clock::now())}.year()); | |
185 | } else { | |
186 | year = settings.year; | |
187 | } | |
11
127fb6a8f706
simplify day and month literals
Mike Becker <universe@uap-core.de>
parents:
9
diff
changeset
|
188 | chrono::year_month_day report_begin{chrono::year{year}, chrono::January, 1d}; |
127fb6a8f706
simplify day and month literals
Mike Becker <universe@uap-core.de>
parents:
9
diff
changeset
|
189 | chrono::year_month_day report_end{chrono::year{year}, chrono::December, 31d}; |
5 | 190 | |
191 | // read the commit logs | |
192 | fm::heatmap heatmap; | |
193 | for (auto &&repo : repos.list()) { | |
194 | if (settings.separate) { | |
195 | heatmap.set_repo(repo.path); | |
196 | } | |
197 | proc.chdir(repo.path); | |
198 | if (repo.type == fm::HG) { | |
199 | proc.setbin(settings.hg); | |
200 | if (proc.exec_log({"log", | |
201 | "--date", std::format("{0}-01-01 to {0}-12-31", year), | |
202 | "--template", "{author}#{date|shortdate}\n"})) { | |
203 | fprintf(stderr, "Reading commit log for repo '%s' failed!\n", repo.path.c_str()); | |
204 | return EXIT_FAILURE; | |
205 | } | |
206 | heatmap.add(proc.output()); | |
207 | } else { | |
208 | proc.setbin(settings.git); | |
8
6a2e20a4a8ff
fix log not being captured from git
Mike Becker <universe@uap-core.de>
parents:
7
diff
changeset
|
209 | if (proc.exec_log({"log", |
5 | 210 | "--since", std::format("{0}-01-01", year), |
211 | "--until", std::format("{0}-12-31", year), | |
212 | "--format=tformat:%an <%ae>#%cs"})) { | |
213 | fprintf(stderr, "Reading commit log for repo '%s' failed!\n", repo.path.c_str()); | |
214 | return EXIT_FAILURE; | |
215 | } | |
216 | heatmap.add(proc.output()); | |
217 | } | |
218 | } | |
219 | ||
220 | html::open(); | |
221 | for (const auto &[repo, authors] : heatmap.data()) { | |
222 | html::h1(repo); | |
223 | for (const auto &[author, entries] : authors) { | |
224 | html::h2(author); | |
6
1040ba37d4c9
improve alignment of month headers
Mike Becker <universe@uap-core.de>
parents:
5
diff
changeset
|
225 | html::table_begin(year); |
5 | 226 | |
227 | // initialize counters | |
228 | unsigned column = 0, row = 0; | |
3 | 229 | |
5 | 230 | // initialize first day (which must be a Monday, possibly the year before) |
12
d869ff924c19
use chrono API to determine the first monday in the year instead of calculating it
Mike Becker <universe@uap-core.de>
parents:
11
diff
changeset
|
231 | chrono::sys_days day_to_check = chrono::January / chrono::Monday[1] / chrono::year{year}; |
5 | 232 | |
233 | // remember the starting point | |
234 | auto start = day_to_check; | |
235 | ||
236 | // now add all entries for Monday, Tuesdays, etc. always starting back in january | |
237 | while (true) { | |
238 | html::row_begin(row); | |
239 | ||
240 | // check if we need to add blank cells | |
241 | while (day_to_check < report_begin) { | |
242 | html::cell_out_of_range(); | |
243 | day_to_check += chrono::days{7}; | |
244 | column++; | |
245 | } | |
246 | ||
247 | while (day_to_check <= report_end) { | |
248 | // get the entry from the heatmap | |
249 | auto find_result = entries.find(day_to_check); | |
250 | if (find_result == entries.end()) { | |
7 | 251 | html::cell(day_to_check, 0); |
5 | 252 | } else { |
7 | 253 | html::cell(day_to_check, find_result->second); |
5 | 254 | } |
255 | // advance seven days and one column | |
256 | day_to_check += chrono::days{7}; | |
257 | column++; | |
258 | } | |
259 | // fill remaining columns with blank cells | |
260 | for (unsigned i = column ; i < html::columns ; i++) { | |
261 | html::cell_out_of_range(); | |
262 | } | |
263 | ||
264 | // terminate the row | |
265 | html::row_end(); | |
266 | ||
267 | // if we have seen all seven weekdays, that's it | |
268 | if (++row == 7) break; | |
269 | ||
270 | // otherwise, advance the starting point by one day, reset, and begin a new row | |
271 | start += chrono::days{1}; | |
272 | day_to_check = start; | |
273 | column =0; | |
274 | } | |
275 | ||
276 | html::table_end(); | |
277 | } | |
278 | } | |
279 | html::close(); | |
3 | 280 | |
1 | 281 | return EXIT_SUCCESS; |
282 | } |