--- a/src/html.cpp Sat Jun 28 11:32:08 2025 +0200 +++ b/src/html.cpp Tue Jul 15 19:14:29 2025 +0200 @@ -187,13 +187,33 @@ 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>'; + let content = '<span class="close-btn">×</span>'; + if (Array.isArray(summaries)) { + content += `<h3>${date}: ${summaries.length} commit${summaries.length !== 1 ? 's' : ''}</h3>`; + content += '<ul>'; + summaries.forEach(summary => { + content += `<li>${summary}</li>`; + }); + content += '</ul>'; + } else { + const repos = Object.keys(summaries).sort(); + 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>`; + for (const repo of repos) { + const commits = summaries[repo]; + if (repos.length > 1) { + content += `<h4>${repo} (${commits.length} commit${commits.length !== 1 ? 's' : ''})</h4>`; + } else { + content += `<h4>${repo}</h4>`; + } + content += '<ul>'; + commits.forEach(commit => { + content += `<li>${commit}</li>`; + }); + content += '</ul>'; + } + } popup.innerHTML = content; // Position popup near the cell @@ -326,7 +346,7 @@ puts("<td class=\"out-of-range\"></td>"); } -void html::cell(year_month_day ymd, const fm::commits &commits) { +void html::cell(year_month_day ymd, bool hide_repo_names, const fm::commits &commits) { const char *color_class; if (commits.count() == 0) { color_class = "zero-commits"; @@ -351,25 +371,41 @@ static_cast<unsigned>(ymd.month()), 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 + "\","; + // Build a JSON object of commit summaries + auto escape_json = [](std::string str) static { + size_t pos = str.find('\"'); + if (pos == std::string::npos) return str; + std::string escaped = std::move(str); + do { + escaped.replace(pos, 1, "\\\""); + pos += 2; + } while ((pos = escaped.find('\"', pos)) != std::string::npos); + return escaped; + }; + 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) { + json += "\"" + escape_json(summary) + "\","; } + json.pop_back(); + }; + std::string summaries_json; + if (hide_repo_names) { + summaries_json += '['; + for (const auto &summaries: commits.summaries | std::views::values) { + add_summaries(summaries_json, summaries); + } + summaries_json += ']'; + } else { + summaries_json += '{'; + for (const auto &[repo, summaries] : commits.summaries) { + summaries_json += "\"" + escape_json(repo) + "\":["; + add_summaries(summaries_json, summaries); + summaries_json += "],"; + } + summaries_json.pop_back(); + summaries_json += '}'; } - summaries_json.pop_back(); - summaries_json += "]"; indent(); printf("<td class=\"%s\" title=\"%s: %u %s\" data-date=\"%s\" data-summaries=\"%s\"></td>\n",