src/main.cpp

Fri, 31 Jan 2025 23:00:00 +0100

author
Mike Becker <universe@uap-core.de>
date
Fri, 31 Jan 2025 23:00:00 +0100
changeset 8
6a2e20a4a8ff
parent 7
d0f77dd2da42
child 9
98312f94dbdd
permissions
-rw-r--r--

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;
}

mercurial