add popups with commit summaries - resolves #644 default tip

Sat, 28 Jun 2025 11:32:08 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 28 Jun 2025 11:32:08 +0200
changeset 54
586dcd606e47
parent 53
1c80ba4a0d62

add popups with commit summaries - resolves #644

src/Makefile file | annotate | diff | comparison | revisions
src/commit-data.h 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
--- a/src/Makefile	Fri Jun 20 17:15:18 2025 +0200
+++ b/src/Makefile	Sat Jun 28 11:32:08 2025 +0200
@@ -40,16 +40,16 @@
 
 FORCE:
 
-../build/heatmap.o: heatmap.cpp heatmap.h settings.h
+../build/heatmap.o: heatmap.cpp heatmap.h settings.h commit-data.h
 	@echo "Compiling $<"
 	$(CXX) -o $@ $(CXXFLAGS)  -c $<
 
-../build/html.o: html.cpp html.h
+../build/html.o: html.cpp html.h commit-data.h
 	@echo "Compiling $<"
 	$(CXX) -o $@ $(CXXFLAGS)  -c $<
 
 ../build/main.o: main.cpp settings.h repositories.h process.h heatmap.h \
- html.h
+ commit-data.h html.h
 	@echo "Compiling $<"
 	$(CXX) -o $@ $(CXXFLAGS)  -c $<
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commit-data.h	Sat Jun 28 11:32:08 2025 +0200
@@ -0,0 +1,35 @@
+/* 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 COMMIT_DATA_H
+#define COMMIT_DATA_H
+
+namespace fm {
+    struct commits final {
+        std::vector<std::string> summaries;
+        [[nodiscard]] unsigned count() const {return summaries.size();}
+    };
+}
+
+#endif //COMMIT_DATA_H
--- a/src/heatmap.cpp	Fri Jun 20 17:15:18 2025 +0200
+++ b/src/heatmap.cpp	Sat Jun 28 11:32:08 2025 +0200
@@ -48,9 +48,11 @@
         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);
-        m_heatmap[m_current_repo][author][chrono::year_month_day{
+        auto &[summaries] = m_heatmap[m_current_repo][author][chrono::year_month_day{
             chrono::year{year}, chrono::month{month}, chrono::day{day}
-        }]++;
+        }];
+        ++parts_iter;
+        summaries.emplace_back(std::string_view{*parts_iter});
     }
 }
 
@@ -62,7 +64,7 @@
     std::array<unsigned int, 12> result{};
     for (auto&& [ymd, commits] : m_heatmap.at(repo).at(author)) {
         if (ymd.year() != year) continue;
-        result[static_cast<unsigned int>(ymd.month())-1] += commits;
+        result[static_cast<unsigned int>(ymd.month())-1] += commits.count();
     }
     return result;
 }
--- a/src/heatmap.h	Fri Jun 20 17:15:18 2025 +0200
+++ b/src/heatmap.h	Sat Jun 28 11:32:08 2025 +0200
@@ -31,6 +31,7 @@
 #include <chrono>
 
 #include "settings.h"
+#include "commit-data.h"
 
 namespace fm {
 
@@ -42,7 +43,7 @@
             std::string, // author
             std::map<
                 std::chrono::year_month_day, // date
-                unsigned int // commits
+                commits
     >>> m_heatmap;
     std::string m_current_repo = "All Repositories";
 public:
--- a/src/html.cpp	Fri Jun 20 17:15:18 2025 +0200
+++ b/src/html.cpp	Sat Jun 28 11:32:08 2025 +0200
@@ -24,6 +24,7 @@
 
 #include "html.h"
 
+#include <ranges>
 #include <cstdio>
 #include <cassert>
 
@@ -36,7 +37,6 @@
     static const char *tabs = "                                                                ";
     static void indent(int change = 0) {
         indentation += change;
-        assert(indentation >= 0);
         assert(indentation <= max_indentation);
         fwrite(tabs, 4, indentation, stdout);
     }
@@ -61,6 +61,9 @@
                 case '>':
                     buffer.append("&gt;");
                     break;
+                case '#':
+                    buffer.append("&#35;");
+                    break;
                 default:
                     buffer.append(&pos, 1);
                     break;
@@ -79,22 +82,30 @@
         puts(R"(<!DOCTYPE html>
 <html>
     <head>
+        <meta charset="UTF-8">
         <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:hover, table.heatmap td.popup-open {
+                filter: hue-rotate(90deg);
+            }
+
             table.heatmap td.out-of-range {
                 background-color: gray;
             }
@@ -122,7 +133,105 @@
             table.heatmap td.commit-spam {
                 background-color: #008000;
             }
+
+            /* Popup styles */
+            .commit-popup {
+                position: absolute;
+                background-color: #fff;
+                border: 1px solid #ccc;
+                border-radius: 4px;
+                box-shadow: 0 2px 10px rgba(0,0,0,0.2);
+                padding: .2rem;
+                z-index: 1000;
+                width: 40ch;
+                font-family: sans-serif;
+                font-size: smaller;
+                display: none;
+            }
+
+            .commit-popup h3 {
+                margin-top: 0;
+                border-bottom: 1px solid #eee;
+                padding-bottom: 5px;
+            }
+
+            .commit-popup ul {
+                margin: 0;
+                padding-left: 20px;
+            }
+
+            .commit-popup li {
+                margin-bottom: 5px;
+            }
+
+            .commit-popup .close-btn {
+                position: absolute;
+                top: 5px;
+                right: 8px;
+                cursor: pointer;
+                font-weight: bold;
+            }
         </style>
+        <script>
+            document.addEventListener('DOMContentLoaded', function() {
+                // Create popup element
+                const popup = document.createElement('div');
+                popup.className = 'commit-popup';
+                document.body.appendChild(popup);
+
+                // Add click event listeners to all commit cells
+                const cells = document.querySelectorAll('table.heatmap td:not(.out-of-range, .zero-commits)');
+                cells.forEach(cell => {
+                    cell.addEventListener('click', function(e) {
+                        const date = this.dataset.date;
+                        const summaries = JSON.parse(this.dataset.summaries);
+
+                        // Create popup content
+                        let content = `<span class="close-btn">×</span>
+                                      <h3>${date}: ${summaries.length} commit${summaries.length !== '1' ? 's' : ''}</h3>`;
+                        content += '<ul>';
+                        summaries.forEach(summary => {
+                            content += `<li>${summary}</li>`;
+                        });
+                        content += '</ul>';
+                        popup.innerHTML = content;
+
+                        // Position popup near the cell
+                        const rect = this.getBoundingClientRect();
+                        popup.style.left = rect.left + window.scrollX + 'px';
+                        popup.style.top = (rect.bottom + window.scrollY + 5) + 'px';
+                        popup.style.display = 'block';
+
+                        // Highlight the cell to which the popup belongs
+                        document.querySelectorAll('table.heatmap td.popup-open').forEach(old_cell => {
+                            old_cell.classList.toggle("popup-open");
+                        });
+                        cell.classList.toggle("popup-open");
+
+                        // Add close button handler
+                        const closeBtn = popup.querySelector('.close-btn');
+                        if (closeBtn) {
+                            closeBtn.addEventListener('click', function() {
+                                popup.style.display = 'none';
+                                cell.classList.toggle("popup-open");
+                            });
+                        }
+
+                        e.stopPropagation();
+                    });
+                });
+
+                // Close popup when clicking elsewhere
+                document.addEventListener('click', function(e) {
+                    if (!popup.contains(e.target)) {
+                        popup.style.display = 'none';
+                        document.querySelectorAll('table.heatmap td.popup-open').forEach(cell => {
+                            cell.classList.toggle("popup-open");
+                        });
+                    }
+                });
+            });
+        </script>
     </head>
     <body>
         <div class="heatmap-content">)");
@@ -217,28 +326,58 @@
     puts("<td class=\"out-of-range\"></td>");
 }
 
-void html::cell(year_month_day ymd, unsigned commits) {
+void html::cell(year_month_day ymd, const fm::commits &commits) {
     const char *color_class;
-    if (commits == 0) {
+    if (commits.count() == 0) {
         color_class = "zero-commits";
-    } else if (commits == 1) {
+    } else if (commits.count() == 1) {
         color_class = "one-commit";
-    } else if (commits <= 5) {
+    } else if (commits.count() <= 5) {
         color_class = "up-to-5-commits";
-    } else if (commits <= 10) {
+    } else if (commits.count() <= 10) {
         color_class = "up-to-10-commits";
-    } else if (commits <= 20) {
+    } else if (commits.count() <= 20) {
         color_class = "up-to-20-commits";
     } else {
         color_class = "commit-spam";
     }
-    indent();
-    printf("<td class=\"%s\" title=\"%s, %d-%02u-%02u: %u %s\"></td>\n",
-        color_class,
+
+
+    // Format date for display
+    char date_str[32];
+    sprintf(date_str, "%s, %d-%02u-%02u",
         weekdays[weekday(ymd).iso_encoding() - 1],
         static_cast<int>(ymd.year()),
         static_cast<unsigned>(ymd.month()),
-        static_cast<unsigned>(ymd.day()),
-        commits,
-        commits == 1 ? "commit" : "commits");
+        static_cast<unsigned>(ymd.day()));
+
+    // Build a JSON array of commit summaries
+    // We have to iterate in reverse order to sort the summaries chronologically
+    std::string summaries_json = "[";
+    for (const auto &summary :  commits.summaries | std::views::reverse) {
+        // Escape quotes in JSON
+        size_t pos = summary.find('\"');
+        if (pos == std::string::npos) {
+            summaries_json += "\"" + summary + "\",";
+        } else {
+            std::string escaped = summary;
+            do {
+                escaped.replace(pos, 1, "\\\"");
+                pos += 2;
+            } while ((pos = escaped.find('\"', pos)) != std::string::npos);
+            summaries_json += "\"" + escaped + "\",";
+        }
+    }
+    summaries_json.pop_back();
+    summaries_json += "]";
+
+    indent();
+    printf("<td class=\"%s\" title=\"%s: %u %s\" data-date=\"%s\" data-summaries=\"%s\"></td>\n",
+        color_class,
+        date_str,
+        commits.count(),
+        commits.count() == 1 ? "commit" : "commits",
+        date_str,
+        encode(summaries_json).c_str()
+    );
 }
--- a/src/html.h	Fri Jun 20 17:15:18 2025 +0200
+++ b/src/html.h	Sat Jun 28 11:32:08 2025 +0200
@@ -29,6 +29,8 @@
 #include <chrono>
 #include <array>
 
+#include "commit-data.h"
+
 namespace html {
 
     static constexpr unsigned max_indentation = 16;
@@ -47,7 +49,7 @@
     void row_begin(unsigned int row);
     void row_end();
     void cell_out_of_range();
-    void cell(std::chrono::year_month_day ymd, unsigned commits);
+    void cell(std::chrono::year_month_day ymd, const fm::commits &commits = {});
 
 }
 
--- a/src/main.cpp	Fri Jun 20 17:15:18 2025 +0200
+++ b/src/main.cpp	Sat Jun 28 11:32:08 2025 +0200
@@ -262,7 +262,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}\n"})) {
+                "--template", "{author}#{date|shortdate}#{desc|firstline}\n"})) {
                 fprintf(stderr, "Reading commit log for repo '%s' failed!\n", repo.path.c_str());
                 return EXIT_FAILURE;
             }
@@ -272,7 +272,7 @@
             if (proc.exec_log({"log",
                 "--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"})) {
+                "--format=tformat:%an <%ae>#%cs#%s"})) {
                 fprintf(stderr, "Reading commit log for repo '%s' failed!\n", repo.path.c_str());
                 return EXIT_FAILURE;
             }
@@ -323,7 +323,7 @@
                     // 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);
+                        html::cell(day_to_check);
                     } else {
                         html::cell(day_to_check, find_result->second);
                     }

mercurial