highlight days with tags - resolves #672

Sun, 10 Aug 2025 15:22:25 +0200

author
Mike Becker <universe@uap-core.de>
date
Sun, 10 Aug 2025 15:22:25 +0200
changeset 61
d77763d2fdda
parent 60
9b1cbc665851
child 62
89b12ef5e190

highlight days with tags - resolves #672

src/commit-data.h file | annotate | diff | comparison | revisions
src/heatmap.cpp file | annotate | diff | comparison | revisions
src/html.cpp file | annotate | diff | comparison | revisions
src/main.cpp file | annotate | diff | comparison | revisions
--- a/src/commit-data.h	Sun Aug 10 11:59:03 2025 +0200
+++ b/src/commit-data.h	Sun Aug 10 15:22:25 2025 +0200
@@ -25,16 +25,19 @@
 #ifndef COMMIT_DATA_H
 #define COMMIT_DATA_H
 
-#include <map>
+#include <unordered_map>
 #include <numeric>
 #include <vector>
 #include <string>
 
 namespace fm {
     struct commits final {
-        std::map<
+        std::unordered_map<
             std::string, // repository name
             std::vector<std::string> > summaries;
+        std::unordered_map<
+            std::string, // repository name
+            std::vector<std::string> > tags;
 
         [[nodiscard]] unsigned count(const std::string &repo) const {
             return summaries.at(repo).size();
--- a/src/heatmap.cpp	Sun Aug 10 11:59:03 2025 +0200
+++ b/src/heatmap.cpp	Sun Aug 10 15:22:25 2025 +0200
@@ -41,10 +41,12 @@
         const auto line_view = std::string_view{line};
         const auto pos_delim1 = line_view.find('#', 0);
         const auto pos_delim2 = line_view.find('#', pos_delim1 + 1);
+        const auto pos_delim3 = line_view.find('#', pos_delim2 + 1);
 
         std::string author{settings.map_author(line_view.substr(0, pos_delim1))};
-        std::string_view date_view{line_view.substr(pos_delim1+1, pos_delim2)};
-        std::string_view summary_view{line_view.substr(pos_delim2+1)};
+        std::string_view date_view{line_view.substr(pos_delim1+1, pos_delim2 - pos_delim1 - 1)};
+        std::string_view tags_view{line_view.substr(pos_delim2+1, pos_delim3 - pos_delim2 - 1)};
+        std::string_view summary_view{line_view.substr(pos_delim3+1)};
 
         int year = 0;
         unsigned int month = 0, day = 0;
@@ -54,10 +56,14 @@
         std::from_chars(date_parts[0].begin(), date_parts[0].end(), year);
         std::from_chars(date_parts[1].begin(), date_parts[1].end(), month);
         std::from_chars(date_parts[2].begin(), date_parts[2].end(), day);
-        auto &[summaries] = m_heatmap[repo_key][author][chrono::year_month_day{
-            chrono::year{year}, chrono::month{month}, chrono::day{day}
-        }];
+        auto &[summaries, tags] =
+                m_heatmap[repo_key][author][chrono::year_month_day{
+                    chrono::year{year}, chrono::month{month}, chrono::day{day}
+                }];
         summaries[m_current_repo].emplace_back(summary_view);
+        if (!tags_view.empty()) {
+            tags[m_current_repo].emplace_back(tags_view);
+        }
     }
 }
 
--- a/src/html.cpp	Sun Aug 10 11:59:03 2025 +0200
+++ b/src/html.cpp	Sun Aug 10 15:22:25 2025 +0200
@@ -86,20 +86,21 @@
         <style>
             table.heatmap {
                 table-layout: fixed;
-                border-collapse: collapse;
+                border-collapse: separate;
+                border-spacing: 4px;
                 font-family: sans-serif;
             }
 
             table.heatmap td, table.heatmap th {
                 text-align: center;
-                border: solid 1px lightgray;
-                height: 1.5em;
+                font-size: 0.75rem;
+                border: solid 2px transparent;
+                border-radius: 4px;
+                height: 1rem;
             }
 
             table.heatmap td {
-                border: solid 1px lightgray;
-                width: 1.5em;
-                height: 1.5em;
+                width: 1rem;
             }
 
             table.heatmap td:hover, table.heatmap td.popup-open {
@@ -134,6 +135,10 @@
                 background-color: #008000;
             }
 
+            table.heatmap td[data-tags]:not([data-tags=""]) {
+                border-color: gold;
+            }
+
             /* Popup styles */
             .commit-popup {
                 position: absolute;
@@ -156,6 +161,15 @@
                 padding-bottom: 5px;
             }
 
+            .commit-popup h4 {
+                margin-top: .5em;
+                margin-bottom: .5em;
+            }
+
+            .commit-popup h5 {
+                margin-top: 0;
+            }
+
             .commit-popup ul {
                 margin: 0;
                 padding-left: 20px;
@@ -191,6 +205,9 @@
                         let content = '<span class="close-btn">×</span>';
                         if (Array.isArray(summaries)) {
                             content += `<h3>${date}: ${summaries.length} commit${summaries.length !== 1 ? 's' : ''}</h3>`;
+                            if (this.dataset.tags) {
+                                content += `<h5>Tags: ${this.dataset.tags}</h5>`;
+                            }
                             content += '<ul>';
                             summaries.forEach(summary => {
                                 content += `<li>${summary}</li>`;
@@ -198,6 +215,7 @@
                             content += '</ul>';
                         } else {
                             const repos = Object.keys(summaries).sort();
+                            const tags = this.dataset.tags ? JSON.parse(this.dataset.tags) : {};
                             let total_commits = 0;
                             for (const repo of repos) total_commits += summaries[repo].length;
                             content += `<h3>${date}: ${total_commits} commit${total_commits !== 1 ? 's' : ''}</h3>`;
@@ -208,6 +226,9 @@
                                 } else {
                                     content += `<h4>${repo}</h4>`;
                                 }
+                                if (tags[repo]) {
+                                    content += `<h5>Tags: ${tags[repo]}</h5>`;
+                                }
                                 content += '<ul>';
                                 commits.forEach(commit => {
                                     content += `<li>${commit}</li>`;
@@ -372,7 +393,7 @@
         static_cast<unsigned>(ymd.month()),
         static_cast<unsigned>(ymd.day()));
 
-    // Build a JSON object of commit summaries
+    // Utility function to escape strings in JSON
     auto escape_json = [](std::string str) static {
         size_t pos = str.find('\"');
         if (pos == std::string::npos) return str;
@@ -383,6 +404,8 @@
         } while ((pos = escaped.find('\"', pos)) != std::string::npos);
         return escaped;
     };
+
+    // Build a JSON object of commit summaries
     auto add_summaries = [escape_json](std::string &json, const std::vector<std::string> &summaries) {
         // We have to iterate in reverse order to sort the summaries chronologically
         for (const auto &summary :  summaries | std::views::reverse) {
@@ -410,13 +433,47 @@
         summaries_json += '}';
     }
 
+    // Build a JSON object of tags
+    std::string tags_json;
+    if (hide_repo_names) {
+        for (const auto &tags_vector: commits.tags | std::views::values) {
+            for (const auto &tag: tags_vector) {
+                tags_json += escape_json(tag);
+                tags_json += ' ';
+            }
+        }
+        if (!tags_json.empty()) {
+            tags_json.pop_back();
+        }
+    } else {
+        tags_json += '{';
+        for (const auto &[repo, tags_vector] : commits.tags) {
+            tags_json += "\"" + escape_json(repo) + "\":\"";
+            for (const auto &tag: tags_vector) {
+                tags_json += escape_json(tag);
+                tags_json += ' ';
+            }
+            if (!tags_vector.empty()) {
+                tags_json.pop_back();
+            }
+            tags_json += "\",";
+        }
+        // note: in contrast to summaries, we want an empty string here when there's nothing to report
+        tags_json.pop_back();
+        if (!commits.tags.empty()) {
+            tags_json += '}';
+        }
+    }
+
+    // Output
     indent();
-    printf("<td class=\"%s\" title=\"%s: %u %s\" data-date=\"%s\" data-summaries=\"%s\"></td>\n",
+    printf("<td class=\"%s\" title=\"%s: %u %s\" data-date=\"%s\" data-summaries=\"%s\" data-tags=\"%s\"></td>\n",
         color_class,
         date_str,
         commits.count(),
         commits.count() == 1 ? "commit" : "commits",
         date_str,
-        encode(summaries_json).c_str()
+        encode(summaries_json).c_str(),
+        encode(tags_json).c_str()
     );
 }
--- a/src/main.cpp	Sun Aug 10 11:59:03 2025 +0200
+++ b/src/main.cpp	Sun Aug 10 15:22:25 2025 +0200
@@ -260,7 +260,7 @@
             proc.setbin(settings.hg);
             if (proc.exec_log({"log",
                 "--date", std::format("{0}-01-01 00:00:00 to {0}-12-31 23:59:59", report_year),
-                "--template", "{author}#{date|shortdate}#{desc|firstline}\\n"})) {
+                "--template", "{author}#{date|shortdate}#{tags}#{desc|firstline}\\n"})) {
                 fprintf(stderr, "Reading commit log for repo '%s' failed!\n", repo.path.c_str());
                 return EXIT_FAILURE;
             }
@@ -268,9 +268,11 @@
         } else {
             proc.setbin(settings.git);
             if (proc.exec_log({"log",
+                "--decorate=short",
+                "--decorate-refs=refs/tags/",
                 "--since", std::format("{0}-01-01 00:00:00", report_year),
                 "--until", std::format("{0}-12-31 23:59:59", report_year),
-                "--format=tformat:%an <%ae>#%cs#%s"})) {
+                "--format=tformat:%an <%ae>#%cs#%(decorate:prefix=,suffix=,tag=,separator= )#%s"})) {
                 fprintf(stderr, "Reading commit log for repo '%s' failed!\n", repo.path.c_str());
                 return EXIT_FAILURE;
             }

mercurial