# HG changeset patch # User Mike Becker # Date 1773313250 -3600 # Node ID 1ff88eb9555cd34c39c69902ee29a03e754e9e96 # Parent fa5f493adfb572f3c980a9cca6814a376cc72d1b add options to output aggregated and/or separated heatmaps to files in one go resolves #809 diff -r fa5f493adfb5 -r 1ff88eb9555c CHANGELOG --- a/CHANGELOG Fri Feb 27 14:38:01 2026 +0100 +++ b/CHANGELOG Thu Mar 12 12:00:50 2026 +0100 @@ -3,6 +3,7 @@ - Add --styles-and-script option to output the default CSS and Javascript - Add monthly summaries - Add reading commit hashes from the commit log +- Add options to output aggregated and/or separated heatmaps to files in one go - Add options to create a CSV export of the raw data Version 1.1.2 - 2025-12-15 diff -r fa5f493adfb5 -r 1ff88eb9555c src/heatmap.cpp --- a/src/heatmap.cpp Fri Feb 27 14:38:01 2026 +0100 +++ b/src/heatmap.cpp Thu Mar 12 12:00:50 2026 +0100 @@ -32,7 +32,6 @@ void fm::heatmap::add(const settings &settings, const std::string &log) { using std::string_view_literals::operator ""sv; - const std::string repo_key = m_separate ? m_current_repo : "All Repositories"; for (auto line: std::views::split(log, "\n"sv)) { if (line.empty()) continue; @@ -59,15 +58,20 @@ 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, 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(hash_view, summary_view); - // special case: if the (only) tag is "tip", we do not add it - if (!tags_view.empty() && tags_view != "tip") { - tags[m_current_repo].emplace_back(hash_view, tags_view); - } + const auto ymd = chrono::year_month_day{ + chrono::year{year}, chrono::month{month}, chrono::day{day} + }; + + auto add_info = [this, hash_view, summary_view, tags_view] (commits & commit_info) { + commit_info.summaries[m_current_repo].emplace_back(hash_view, summary_view); + // special case: if the (only) tag is "tip", we do not add it + if (!tags_view.empty() && tags_view != "tip") { + commit_info.tags[m_current_repo].emplace_back(hash_view, tags_view); + } + }; + + add_info(m_heatmap[m_current_repo][author][ymd]); + add_info(m_aggregated_heatmap[aggregated_repo_label][author][ymd]); } } @@ -77,7 +81,8 @@ chrono::year year ) const { std::array result{}; - for (auto &&[ymd, commits]: m_heatmap.at(repo).at(author)) { + for (const auto &heatmap_data = repo == aggregated_repo_label ? m_aggregated_heatmap : m_heatmap; + auto &&[ymd, commits]: heatmap_data.at(repo).at(author)) { if (ymd.year() != year) continue; commit_summary &cs = result[static_cast(ymd.month()) - 1]; for (auto &&[reponame, summaries]: commits.summaries) { diff -r fa5f493adfb5 -r 1ff88eb9555c src/heatmap.h --- a/src/heatmap.h Fri Feb 27 14:38:01 2026 +0100 +++ b/src/heatmap.h Thu Mar 12 12:00:50 2026 +0100 @@ -45,20 +45,31 @@ std::chrono::year_month_day, // date commits >>> m_heatmap; + std::map< + std::string, // dummy string - so that both maps have the same type + std::map< + std::string, // author + std::map< + std::chrono::year_month_day, // date + commits + >>> m_aggregated_heatmap; std::string m_current_repo; - bool m_separate; + + static constexpr auto aggregated_repo_label = "All Repositories"; public: - explicit heatmap(bool separate) noexcept : m_separate(separate) {} - void set_repo(const std::string& repo) { m_current_repo.assign(repo); } void add(const settings &settings, const std::string& log); - [[nodiscard]] const auto& data() const { + [[nodiscard]] const auto& separated() const { return m_heatmap; } + [[nodiscard]] const auto& aggregated() const { + return m_aggregated_heatmap; + } + [[nodiscard]] std::array commits_per_month( const std::string &repo, const std::string& author, diff -r fa5f493adfb5 -r 1ff88eb9555c src/html.cpp --- a/src/html.cpp Fri Feb 27 14:38:01 2026 +0100 +++ b/src/html.cpp Thu Mar 12 12:00:50 2026 +0100 @@ -27,18 +27,42 @@ #include #include #include +#include using namespace std::chrono; namespace html { static constexpr const char* weekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; + struct output_reference { + FILE *file; + explicit output_reference(FILE *f) : file{f} {} + output_reference(output_reference const &) = delete; + output_reference(output_reference && other) noexcept : file(other.file) { + other.file = nullptr; + } + output_reference &operator=(output_reference const &) = delete; + output_reference &operator=(output_reference && other) noexcept { + using std::swap; + swap(other.file, file); + return *this; + } + ~output_reference() { + if (file != nullptr && file != stdout) { + fclose(file); + file = nullptr; + } + } + }; + + static output_reference output{stdout}; + static unsigned indentation; static const char *tabs = " "; static void indent(int change = 0) { indentation += change; assert(indentation <= max_indentation); - fwrite(tabs, 4, indentation, stdout); + fwrite(tabs, 4, indentation, output.file); } static std::string encode(const std::string &data) { @@ -158,8 +182,21 @@ } } +int html::set_output(std::string const &path) { + if (path.empty()) { + output = output_reference{stdout}; + return 0; + } + FILE * f = fopen(path.c_str(), "w"); + if (f == nullptr) { + return -1; + } + output = output_reference{f}; + return 0; +} + void html::styles_and_script() { - puts(R"(