src/main.cpp

changeset 81
1ff88eb9555c
parent 79
cd8ad39dda76
child 82
f3242673777c
--- a/src/main.cpp	Fri Feb 27 14:38:01 2026 +0100
+++ b/src/main.cpp	Thu Mar 12 12:00:50 2026 +0100
@@ -45,21 +45,23 @@
     fputs(
         "Usage: repoheat [OPTION]... [PATH]...\n\n"
         "Options:\n"
-        "   -a, --author <name>       Only report this author\n"
-        "                             (repeat option to report multiple authors)\n"
-        "   -A, --authormap <file>    Apply an author mapping file\n"
-        "       --csv                 Output the gathered data as CSV to stdout\n"
-        "       --csv-path <file>     Write CSV data to file instead of stdout\n"
-        "   -d, --depth <num>         The search depth (default: 1, max: 255)\n"
-        "   -f, --fragment [indent]   Output as fragment (HTML only)\n"
-        "   -h, --help                Print this help message\n"
-        "   -p, --pull                Try to pull the repositories\n"
-        "   -s, --separate            Output a separate heat map for each repository\n"
-        "       --styles-and-script   Output the default CSS and Javascript and quit\n"
-        "   -V, --version             Output the version of this program and exit\n"
-        "   -y, --year <year>         The year for which to create the heat map\n"
-        "       --hg <path>           Path to hg binary (default: /usr/bin/hg)\n"
-        "       --git <path>          Path to git binary (default: /usr/bin/git)\n\n"
+        "  -a, --author <name>          Only report this author\n"
+        "                               (repeat option to report multiple authors)\n"
+        "  -A, --authormap <file>       Apply an author mapping file\n"
+        "      --csv                    Output the gathered data as CSV to stdout\n"
+        "      --csv-output <file>      Write CSV data to file instead of stdout\n"
+        "  -d, --depth <num>            The search depth (default: 1, max: 255)\n"
+        "  -f, --fragment [indent]      Output as fragment (HTML only)\n"
+        "  -h, --help                   Print this help message\n"
+        "  -o, --output <file>          Write output to file instead of stdout\n"
+        "  -p, --pull                   Try to pull the repositories\n"
+        "  -s, --separate               Output a separate heat map for each repository\n"
+        "  -S, --separate-output <file> Write results for -s to file instead of stdout\n"
+        "      --styles-and-script      Output the default CSS and Javascript and quit\n"
+        "  -V, --version                Output the version of this program and exit\n"
+        "  -y, --year <year>            The year for which to create the heat map\n"
+        "      --hg <path>              Path to hg binary (default: /usr/bin/hg)\n"
+        "      --git <path>             Path to git binary (default: /usr/bin/git)\n\n"
         "Scans all specified paths recursively for Mercurial and Git repositories and\n"
         "creates a commit heat map for the specified \033[1myear\033[22m or the current year.\n"
         "By default, the recursion \033[1mdepth\033[22m is one, meaning that this tool assumes that\n"
@@ -88,16 +90,20 @@
         "Finally, this tool prints an HTML page to stdout. A separate heap map is\n"
         "generated for each author showing commits across all repositories, unless the\n"
         "\033[1m--separate\033[22m option is specified in which case each repository is displayed with\n"
-        "its own heat map. By using the \033[1m--fragment\033[22m option, the tool only outputs a\n"
-        "single HTML div container without any header or footer that can be embedded in\n"
-        "your custom web page. You can optionally specify an indentation for that\n"
-        "container (default is 0 and maximum is 12).\n"
+        "its own heat map.\n"
+        "If you use \033[1m--separate-output\033[22m you can specify a file instead, and both the\n"
+        "aggregated and separated heat maps are written. The aggregated output is still\n"
+        "written to stdout unless you specify a file with \033[1m--output\033[22m.\n"
+        "By using the \033[1m--fragment\033[22m option, the tool only outputs a single HTML div\n"
+        "container without any header or footer that can be embedded in your custom web\n"
+        "page. You can optionally specify an indentation for that container (default is\n"
+        "0 and maximum is 12).\n"
         "When you want to combine this with the default style and scripts, you can use\n"
         "the \033[1m--styles-and-script\033[22m option print the defaults to stdout and redirect them\n"
         "into a file when you are composing your custom HTML page.\n\n"
         "In case you want to work with the raw data, you can advise the tool to output\n"
         "the data in CSV format with the \033[1m--csv\033[22m option. The CSV data will be written to\n"
-        "stdout instead of the HTML unless you specify a different file with \033[1m--csv-path\033[22m.\n"
+        "stdout instead of the HTML. You can use \033[1m--csv-output\033[22m to specify a file, instead.\n"
         , stderr);
 }
 
@@ -144,14 +150,21 @@
             }
         } else if (chk_arg(argv[i], "--csv", nullptr)) {
             settings.csv = true;
-        } else if (chk_arg(argv[i], "--csv-path", nullptr)) {
-            settings.csv = true;
+        } else if (chk_arg(argv[i], "--csv-output", nullptr)) {
             if (i + 1 < argc) {
+                settings.csv = true;
                 settings.csv_path.assign(argv[++i]);
             } else {
                 fputs("missing csv path\n", stderr);
                 return -1;
             }
+        } else if (chk_arg(argv[i], "-o", "--output")) {
+            if (i + 1 < argc) {
+                settings.out_path.assign(argv[++i]);
+            } else {
+                fputs("missing output path\n", stderr);
+                return -1;
+            }
         } else if (chk_arg(argv[i], "-p", "--pull")) {
             settings.update_repos = true;
         } else if (chk_arg(argv[i], "-f", "--fragment")) {
@@ -164,6 +177,14 @@
             }
         } else if (chk_arg(argv[i], "-s", "--separate")) {
             settings.separate = true;
+        } else if (chk_arg(argv[i], "-S", "--separate-output")) {
+            if (i + 1 < argc) {
+                settings.separate = true;
+                settings.separate_path.assign(argv[++i]);
+            } else {
+                fputs("missing separate output path\n", stderr);
+                return -1;
+            }
         } else if (chk_arg(argv[i], "-A", "--authormap")) {
             if (i + 1 < argc) {
                 if (settings.parse_authormap(argv[++i])) {
@@ -216,9 +237,9 @@
         const fm::settings &settings,
         year report_year, year_month_day report_begin, year_month_day report_end,
         const fm::heatmap &heatmap) {
-
+    const auto &heatmap_data = settings.separate ? heatmap.separated() : heatmap.aggregated();
     html::open(settings.fragment, settings.fragment_indent);
-    for (const auto &[repo, authors] : heatmap.data()) {
+    for (const auto &[repo, authors] : heatmap_data) {
         bool h1_rendered = false;
         for (const auto &[author, entries] : authors) {
             if (settings.exclude_author(author)) continue;
@@ -317,7 +338,7 @@
     };
 
     fprintf(out, "repository,author,date,hash,summary,is_tag\n");
-    for (const auto &authors: heatmap.data() | std::views::values) {
+    for (const auto &authors: heatmap.aggregated() | std::views::values) {
         for (const auto &[author, entries]: authors) {
             for (const auto &[date, commits]: entries) {
                 for (const auto &[repo, infos] : commits.summaries) {
@@ -362,6 +383,7 @@
 
     // check special options
     if (settings.styles_and_script) {
+        // note: no need to set output path - not supported for styles and scripts
         html::styles_and_script();
         return 0;
     }
@@ -425,7 +447,7 @@
     year_month_day report_end{report_year, December, 31d};
 
     // read the commit logs
-    fm::heatmap heatmap{settings.separate};
+    fm::heatmap heatmap;
     for (auto &&repo : repos.list()) {
         heatmap.set_repo(repo.name);
         proc.chdir(repo.path);
@@ -454,14 +476,33 @@
         }
     }
 
-    bool html = true;
-    if (settings.csv) {
+    const bool write_csv = settings.csv;
+    const bool write_separate = settings.separate;
+    const bool write_default = !settings.out_path.empty() || (
+        !(write_csv && settings.csv_path.empty())
+        && !(write_separate && settings.separate_path.empty())
+    );
+
+    if (write_csv) {
         if (generate_csv(settings, heatmap)) {
+            perror("Cannot open file for writing");
             return EXIT_FAILURE;
         }
-        html = !settings.csv_path.empty();
     }
-    if (html) {
+    if (write_separate) {
+        settings.separate = true;
+        if (html::set_output(settings.separate_path)) {
+            perror("Cannot open file for writing");
+            return EXIT_FAILURE;
+        }
+        generate_html(settings, report_year, report_begin, report_end, heatmap);
+    }
+    if (write_default) {
+        settings.separate = false;
+        if (html::set_output(settings.out_path)) {
+            perror("Cannot open file for writing");
+            return EXIT_FAILURE;
+        }
         generate_html(settings, report_year, report_begin, report_end, heatmap);
     }
 

mercurial