Sun, 10 Aug 2025 15:22:25 +0200
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; }