finish MVP

Fri, 31 Jan 2025 22:11:04 +0100

author
Mike Becker <universe@uap-core.de>
date
Fri, 31 Jan 2025 22:11:04 +0100
changeset 5
60c2588b4455
parent 4
82680ce258d6
child 6
1040ba37d4c9

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("&amp;");
+                    break;
+                case '\"':
+                    buffer.append("&quot;");
+                    break;
+                case '\'':
+                    buffer.append("&apos;");
+                    break;
+                case '<':
+                    buffer.append("&lt;");
+                    break;
+                case '>':
+                    buffer.append("&gt;");
+                    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;
 };
 
 }

mercurial