Fri, 31 Jan 2025 23:00:00 +0100
fix log not being captured from git
/* Copyright 2025 Mike Becker. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "settings.h" #include "repositories.h" #include "process.h" #include "heatmap.h" #include "html.h" #include <chrono> #include <cstdlib> #include <cstdio> #include <cstring> #include <cerrno> namespace chrono = std::chrono; static void print_help() { fputs( "Usage: fallusmeter [OPTION]... [PATH]...\n\n" "Options:\n" " -h, --help Print this help message\n" " -d, --depth <num> The search depth (default: 1, max: 255)\n" " -n, --no-pull Do not pull the repositories\n" " -s, --separate Output a separate heat map for each repository\n" " -y, --year <year> The year for which to create the heat map\n" " --hg <path> Path to hg binary (default: /usr/bin/hg)\n" " --git <path> Path to git binary (default: /usr/bin/git)\n\n" "Scans all specified paths recursively for Mercurial and Git repositories and\n" "creates a commit heat map for the specified \033[1myear\033[22m or the current year.\n" "By default, the recursion \033[1mdepth\033[22m is one, meaning that this tool assumes that\n" "each \033[1mpath\033[22m is either a repository or contains repositories as subdirectories.\n" "You can change the \033[1mdepth\033[22m to support other directory structures.\n\n" "When you do not specify \033[1m--no-pull\033[22m, this tool will execute the pull command\n" "(and for hg the update command) before retrieving the commit log, assuming\n" "to be on the default branch with \033[4mno uncommitted changes\033[24m.\n\n" "Afterwards, this tool prints an HTML page to stdout. A separate heap map is\n" "generated for each author showing commits across all repositories, unless the\n" "\033[1m--separate\033[22m option is specified in which case each repository is displayed with\n" "its own heat map.\n" , stderr); } static bool chk_arg(const char *arg, const char *opt1, const char *opt2) { return strcmp(arg, opt1) == 0 || (opt2 != nullptr && strcmp(arg, opt2) == 0); } template<typename T> static bool parse_unsigned(const char *str, T *result, unsigned long max) { char *endptr; errno = 0; unsigned long d = strtoul(str, &endptr, 10); if (*endptr != '\0' || errno == ERANGE) return true; if (d < max) { *result = d; return false; } else { return true; } } static int parse_args(fm::settings &settings, int argc, char *argv[]) { for (int i = 1; i < argc; i++) { if (chk_arg(argv[i], "-h", "--help")) { print_help(); return -1; } else if (chk_arg(argv[i], "-d", "--depth")) { if (i + 1 >= argc || parse_unsigned(argv[++i], &settings.depth, 256)) { fputs("missing or invalid depth\n", stderr); return -1; } } else if (chk_arg(argv[i], "-y", "--year")) { if (i + 1 >= argc || parse_unsigned(argv[++i], &settings.year, 9999)) { fputs("missing or invalid year\n", stderr); return -1; } } else if (chk_arg(argv[i], "-n", "--no-pull")) { settings.update_repos = false; } else if (chk_arg(argv[i], "-s", "--separate")) { settings.separate = true; } else if (chk_arg(argv[i], "--hg", nullptr)) { if (i + 1 < argc) { settings.hg.assign(argv[++i]); } else { fputs("--hg is expecting a path\n", stderr); return -1; } } else if (chk_arg(argv[i], "--git", nullptr)) { if (i + 1 < argc) { settings.git.assign(argv[++i]); } else { fputs("--git is expecting a path\n", stderr); return -1; } } else if (argv[i][0] == '-') { fprintf(stderr, "Unknown option: %s\n", argv[i]); return -1; } else { settings.paths.emplace_back(argv[i]); } } if (settings.paths.empty()) { settings.paths.emplace_back("./"); } return 0; } int main(int argc, char *argv[]) { // parse settings fm::settings settings; if (parse_args(settings, argc, argv)) { return EXIT_FAILURE; } // check hg and git fm::process proc; proc.setbin(settings.hg); if (proc.exec({"--version"})) { fprintf(stderr, "Testing hg binary '%s' failed!\n", settings.hg.c_str()); return EXIT_FAILURE; } proc.setbin(settings.git); if (proc.exec({"--version"})) { fprintf(stderr, "Testing git binary '%s' failed!\n", settings.git.c_str()); return EXIT_FAILURE; } // scan for repos fm::repositories repos; for (auto &&path: settings.paths) { repos.scan(path, settings.depth); } // update repos, if not disabled if (settings.update_repos) { for (auto &&repo : repos.list()) { proc.chdir(repo.path); if (repo.type == fm::HG) { proc.setbin(settings.hg); if (proc.exec({"pull"})) { fprintf(stderr, "Pulling repo '%s' failed!\nMaybe there is no remote?\n", repo.path.c_str()); } else if (proc.exec({"update"})) { fprintf(stderr, "Updating repo '%s' failed!\nMaybe there are local changes?\n", repo.path.c_str()); } } else { proc.setbin(settings.git); if (proc.exec({"pull"})) { fprintf(stderr, "Pulling repo '%s' failed!\nMaybe there is no remote or there are local changes?\n", repo.path.c_str()); } } } } // determine our reporting range int year; if (settings.year == fm::settings_current_year) { year = static_cast<int>(chrono::year_month_day{chrono::floor<chrono::days>(chrono::system_clock::now())}.year()); } else { year = settings.year; } chrono::year_month_day report_begin{chrono::year{year}, chrono::month{1}, chrono::day{1}}; chrono::year_month_day report_end{chrono::year{year}, chrono::month{12}, chrono::day{31}}; // read the commit logs fm::heatmap heatmap; for (auto &&repo : repos.list()) { if (settings.separate) { heatmap.set_repo(repo.path); } proc.chdir(repo.path); if (repo.type == fm::HG) { proc.setbin(settings.hg); if (proc.exec_log({"log", "--date", std::format("{0}-01-01 to {0}-12-31", year), "--template", "{author}#{date|shortdate}\n"})) { fprintf(stderr, "Reading commit log for repo '%s' failed!\n", repo.path.c_str()); return EXIT_FAILURE; } heatmap.add(proc.output()); } else { proc.setbin(settings.git); if (proc.exec_log({"log", "--since", std::format("{0}-01-01", year), "--until", std::format("{0}-12-31", year), "--format=tformat:%an <%ae>#%cs"})) { fprintf(stderr, "Reading commit log for repo '%s' failed!\n", repo.path.c_str()); return EXIT_FAILURE; } heatmap.add(proc.output()); } } html::open(); for (const auto &[repo, authors] : heatmap.data()) { html::h1(repo); for (const auto &[author, entries] : authors) { html::h2(author); html::table_begin(year); // initialize counters unsigned column = 0, row = 0; // initialize first day (which must be a Monday, possibly the year before) chrono::sys_days day_to_check{report_begin}; day_to_check -= chrono::days{chrono::weekday{day_to_check}.iso_encoding() - 1}; // remember the starting point auto start = day_to_check; // now add all entries for Monday, Tuesdays, etc. always starting back in january while (true) { html::row_begin(row); // check if we need to add blank cells while (day_to_check < report_begin) { html::cell_out_of_range(); day_to_check += chrono::days{7}; column++; } while (day_to_check <= report_end) { // get the entry from the heatmap auto find_result = entries.find(day_to_check); if (find_result == entries.end()) { html::cell(day_to_check, 0); } else { html::cell(day_to_check, find_result->second); } // advance seven days and one column day_to_check += chrono::days{7}; column++; } // fill remaining columns with blank cells for (unsigned i = column ; i < html::columns ; i++) { html::cell_out_of_range(); } // terminate the row html::row_end(); // if we have seen all seven weekdays, that's it if (++row == 7) break; // otherwise, advance the starting point by one day, reset, and begin a new row start += chrono::days{1}; day_to_check = start; column =0; } html::table_end(); } } html::close(); return EXIT_SUCCESS; }