Fri, 31 Jan 2025 22:11:04 +0100
finish MVP
configure | file | annotate | diff | comparison | revisions | |
make/project.xml | file | annotate | diff | comparison | revisions | |
src/Makefile | file | annotate | diff | comparison | revisions | |
src/heatmap.cpp | file | annotate | diff | comparison | revisions | |
src/heatmap.h | file | annotate | diff | comparison | revisions | |
src/html.cpp | file | annotate | diff | comparison | revisions | |
src/html.h | file | annotate | diff | comparison | revisions | |
src/main.cpp | file | annotate | diff | comparison | revisions | |
src/settings.h | file | annotate | diff | comparison | revisions |
--- a/configure Tue Jan 21 21:01:54 2025 +0100 +++ b/configure Fri Jan 31 22:11:04 2025 +0100 @@ -293,6 +293,7 @@ break fi + TEMP_CXXFLAGS="$TEMP_CXXFLAGS -std=c++20" break done break
--- a/make/project.xml Tue Jan 21 21:01:54 2025 +0100 +++ b/make/project.xml Fri Jan 31 22:11:04 2025 +0100 @@ -2,6 +2,7 @@ <project version="0.3" xmlns="http://unixwork.de/uwproj"> <dependency> <lang>cpp</lang> + <cxxflags>-std=c++20</cxxflags> </dependency> </project>
--- a/src/Makefile Tue Jan 21 21:01:54 2025 +0100 +++ b/src/Makefile Fri Jan 31 22:11:04 2025 +0100 @@ -23,7 +23,7 @@ include ../config.mk -SRC=main.cpp process.cpp repositories.cpp +SRC=main.cpp process.cpp repositories.cpp heatmap.cpp html.cpp OBJ=$(SRC:%.cpp=../build/%.o) OUTPUT=../build/fallusmeter @@ -40,7 +40,15 @@ FORCE: -../build/main.o: main.cpp settings.h repositories.h +../build/heatmap.o: heatmap.cpp heatmap.h + @echo "Compiling $<" + $(CXX) -o $@ $(CXXFLAGS) -c $< + +../build/html.o: html.cpp html.h + @echo "Compiling $<" + $(CXX) -o $@ $(CXXFLAGS) -c $< + +../build/main.o: main.cpp settings.h repositories.h process.h heatmap.h @echo "Compiling $<" $(CXX) -o $@ $(CXXFLAGS) -c $<
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/heatmap.cpp Fri Jan 31 22:11:04 2025 +0100 @@ -0,0 +1,58 @@ +/* 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 "heatmap.h" + +#include <charconv> +#include <ranges> +#include <chrono> + +namespace chrono = std::chrono; + +void fm::heatmap::add(const std::string &log) { + using std::string_view_literals::operator ""sv; + + for (auto &&line: std::views::split(log, "\n"sv)) { + if (line.empty()) continue; + auto parts = std::views::split(line, "#"sv).begin(); + auto author_range = *parts; + std::string author{author_range.begin(), author_range.end()}; + + int year = 0; + unsigned int month = 0, day = 0; + unsigned part = 0; + for (auto &&date_part: std::views::split(*++parts, "-"sv)) { + std::string_view dp{date_part.begin(), date_part.end()}; + int val; + // no error handling, we trust hg and git + std::from_chars(dp.begin(), dp.end(), val); + switch (part++) { + case 0: year = val; break; + case 1: month = val; break; + default: day = val; break; + } + } + m_heatmap[m_current_repo][author][chrono::year_month_day{chrono::year{year}, chrono::month{month}, chrono::day{day}}]++; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/heatmap.h Fri Jan 31 22:11:04 2025 +0100 @@ -0,0 +1,58 @@ +/* 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. + */ + +#ifndef HEATMAP_H +#define HEATMAP_H + +#include <map> +#include <string> +#include <chrono> + +namespace fm { + +class heatmap { + // to have nice sorted output later, we use ordered maps here + std::map< + std::string, // repository + std::map< + std::string, // author + std::map< + std::chrono::year_month_day, // date + unsigned int // commits + >>> m_heatmap; + std::string m_current_repo = "All Repositories"; +public: + void set_repo(const std::string& repo) { + m_current_repo.assign(repo); + } + void add(const std::string& log); + + [[nodiscard]] const auto& data() const { + return m_heatmap; + } +}; + +} + +#endif //HEATMAP_H
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/html.cpp Fri Jan 31 22:11:04 2025 +0100 @@ -0,0 +1,154 @@ +/* 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 "html.h" + +#include <cstdio> + +namespace html { + static unsigned indentation; + static const char *tabs = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"; + static void indent(int change = 0) { + indentation += change; + fwrite(tabs, 1, indentation, stdout); + } + + static std::string encode(const std::string &data) { + std::string buffer; + buffer.reserve(data.size()+16); + for (const char &pos: data) { + switch (pos) { + case '&': + buffer.append("&"); + break; + case '\"': + buffer.append("""); + break; + case '\'': + buffer.append("'"); + break; + case '<': + buffer.append("<"); + break; + case '>': + buffer.append(">"); + break; + default: + buffer.append(&pos, 1); + break; + } + } + return buffer; + } +} + +void html::open() { + puts( +R"(<html> + <head> + <style> + table.heatmap { + table-layout: fixed; + border-collapse: collapse; + font-family: sans-serif; + } + table.heatmap td, table.heatmap th { + text-align: center; + border: solid 1px lightgray; + height: 1.5em; + } + table.heatmap td { + border: solid 1px lightgray; + width: 1.5em; + height: 1.5em; + } + table.heatmap td.out-of-range { + background-color: gray; + } + </style> + </head> + <body>)"); + indentation = 2; +} + +void html::close() { + puts("\t</body>\n</html>"); +} + +void html::h1(const std::string& heading) { + indent(); + printf("<h1>%s</h1>\n", encode(heading).c_str()); +} + +void html::h2(const std::string& heading) { + indent(); + printf("<h2>%s</h2>\n", encode(heading).c_str()); +} + +void html::table_begin() { + indent(); + puts("<table class=\"heatmap\">"); + indent(1); + puts("<tr>"); + indent(1); + puts("<th></th>"); + static constexpr const char* months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + static constexpr int colspans[] = {5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4, 5}; + for (unsigned i = 0 ; i < 12 ; i++) { + indent(); + printf("<th scope=\"col\" colspan=\"%d\">%s</th>\n", colspans[i], months[i]); + } + indent(-1); + puts("</tr>"); +} + +void html::table_end() { + indentation--; + indent(); + puts("</table>"); +} + +void html::row_begin(unsigned int row) { + static constexpr const char* weekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; + indent(); + puts("<tr>"); + indent(1); + printf("<th scope=\"row\">%s</th>\n", weekdays[row]); +} + +void html::row_end() { + indent(-1); + puts("</tr>"); +} + +void html::cell_out_of_range() { + indent(); + puts("<td class=\"out-of-range\"></td>"); +} + +void html::cell(unsigned commits) { + indent(); + // TODO: use nice coloring and tooltips instead of printing the commits + printf("<td>%u</td>\n", commits); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/html.h Fri Jan 31 22:11:04 2025 +0100 @@ -0,0 +1,48 @@ +/* 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. + */ + +#ifndef HTML_H +#define HTML_H + +#include <string> + +namespace html { + + static constexpr unsigned columns = 53; + + void open(); + void close(); + + void h1(const std::string& heading); + void h2(const std::string& heading); + void table_begin(); + void table_end(); + void row_begin(unsigned int weekday); + void row_end(); + void cell_out_of_range(); + void cell(unsigned commits); + +} + +#endif //HTML_H
--- a/src/main.cpp Tue Jan 21 21:01:54 2025 +0100 +++ b/src/main.cpp Fri Jan 31 22:11:04 2025 +0100 @@ -25,12 +25,17 @@ #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" @@ -123,14 +128,6 @@ return 0; } -static void print_html_header() { - puts("<html>\n\t<body>"); -} - -static void print_html_footer() { - puts("\t</body>\n</html>"); -} - int main(int argc, char *argv[]) { // parse settings fm::settings settings; @@ -176,11 +173,107 @@ } } } - // TODO: calculate the heat maps + + // 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", + "--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(); + + // initialize counters + unsigned column = 0, row = 0; - print_html_header(); - // TODO: output the heat maps here - print_html_footer(); + // 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(0); + } else { + html::cell(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; }
--- a/src/settings.h Tue Jan 21 21:01:54 2025 +0100 +++ b/src/settings.h Fri Jan 31 22:11:04 2025 +0100 @@ -31,7 +31,7 @@ namespace fm { -constexpr static short SETTINGS_CURRENT_YEAR = 0; +constexpr static short settings_current_year = 0; struct settings { std::string hg{"/usr/bin/hg"}; @@ -40,7 +40,7 @@ unsigned char depth = 1; bool update_repos = true; bool separate = false; - unsigned short year = SETTINGS_CURRENT_YEAR; + unsigned short year = settings_current_year; }; }