| startgitStatic page generator for git repositories | 
| git clone git://git.dimitrijedobrota.com/startgit.git | 
| Log | Files | Refs | README | LICENSE | HACKING | CONTRIBUTING | CODE_OF_CONDUCT | BUILDING | 
startgit.cpp (20898B)
    0 #include <cmath>
              1 #include <filesystem>
              2 #include <format>
              3 #include <fstream>
              4 #include <iostream>
              5 #include <string>
          
              7 #include <git2wrap/error.hpp>
              8 #include <git2wrap/libgit2.hpp>
              9 #include <hemplate/atom.hpp>
             10 #include <hemplate/html.hpp>
             11 #include <hemplate/rss.hpp>
             12 #include <poafloc/error.hpp>
             13 #include <poafloc/poafloc.hpp>
          
             15 #include "arguments.hpp"
             16 #include "document.hpp"
             17 #include "html.hpp"
             18 #include "repository.hpp"
             19 #include "utils.hpp"
          
             21 using hemplate::element;
             22 namespace
             23 {
          
             25 template<std::ranges::forward_range R>
             26 element wtable(
             27     std::initializer_list<std::string_view> head_content,
             28     const R& range,
             29     based::Procedure<element, std::ranges::range_value_t<R>> auto proc
             30 )
             31 {
             32   using namespace hemplate::html;  // NOLINT
          
             34   return table {
             35       thead {
             36           tr {
             37               transform(
             38                   head_content,
             39                   [](const auto& elem)
             40                   {
             41                     return td {
             42                         elem,
             43                     };
             44                   }
             45               ),
             46           },
             47       },
             48       tbody {
             49           transform(range, proc),
             50       },
             51   };
             52 }
          
             54 }  // namespace
          
             56 namespace startgit
             57 {
          
             59 element page_title(
             60     const repository& repo,
             61     const branch& branch,
             62     const std::string& relpath = "./"
             63 )
             64 {
             65   using namespace hemplate::html;  // NOLINT
          
             67   return element {
             68       table {
             69           tr {
             70               td {
             71                   h1 {repo.get_name()},
             72                   span {repo.get_description()},
             73               },
             74           },
             75           tr {
             76               td {
             77                   "git clone ",
             78                   aHref {repo.get_url(), repo.get_url()},
             79               },
             80           },
             81           tr {
             82               td {
             83                   aHref {relpath + "log.html", "Log"},
             84                   " | ",
             85                   aHref {relpath + "files.html", "Files"},
             86                   " | ",
             87                   aHref {relpath + "refs.html", "Refs"},
             88                   transform(
             89                       branch.get_special(),
             90                       [&](const auto& file)
             91                       {
             92                         auto path = file.get_path();
             93                         const auto filename =
             94                             path.replace_extension("html").string();
             95                         const auto name = path.replace_extension().string();
          
             97                         return element {
             98                             " | ",
             99                             aHref {relpath + filename, name},
            100                         };
            101                       }
            102                   ),
            103               },
            104           },
            105       },
            106       hr {},
            107   };
            108 }
          
            110 element commit_table(const branch& branch)
            111 {
            112   using namespace hemplate::html;  // NOLINT
          
            114   return wtable(
            115       {"Date", "Commit message", "Author", "Files", "+", "-"},
            116       branch.get_commits(),
            117       [&](const auto& commit)
            118       {
            119         const auto idd = commit.get_id();
            120         const auto url = std::format("./commit/{}.html", idd);
          
            122         return tr {
            123             td {commit.get_time()},
            124             td {aHref {url, commit.get_summary()}},
            125             td {commit.get_author_name()},
            126             td {commit.get_diff().get_files_changed()},
            127             td {commit.get_diff().get_insertions()},
            128             td {commit.get_diff().get_deletions()},
            129         };
            130       }
            131   );
            132 }
          
            134 element files_table(const branch& branch)
            135 {
            136   using namespace hemplate::html;  // NOLINT
          
            138   return wtable(
            139       {"Mode", "Name", "Size"},
            140       branch.get_files(),
            141       [&](const auto& file)
            142       {
            143         const auto path = file.get_path().string();
            144         const auto url = std::format("./file/{}.html", path);
            145         const auto size = file.is_binary()
            146             ? std::format("{}B", file.get_size())
            147             : std::format("{}L", file.get_lines());
          
            149         return tr {
            150             td {file.get_filemode()},
            151             td {aHref {url, path}},
            152             td {size},
            153         };
            154       }
            155   );
            156 }
          
            158 element branch_table(const repository& repo, const std::string& branch_name)
            159 {
            160   using namespace hemplate::html;  // NOLINT
          
            162   return element {
            163       h2 {"Branches"},
            164       wtable(
            165           {" ", "Name", "Last commit date", "Author"},
            166           repo.get_branches(),
            167           [&](const auto& branch)
            168           {
            169             const auto& last = branch.get_last_commit();
            170             const auto url = branch.get_name() != branch_name
            171                 ? std::format("../{}/refs.html", branch.get_name())
            172                 : "";
            173             const auto name = branch.get_name() == branch_name ? "*" : " ";
          
            175             return tr {
            176                 td {name},
            177                 td {aHref {url, branch.get_name()}},
            178                 td {last.get_time()},
            179                 td {last.get_author_name()},
            180             };
            181           }
            182       ),
            183   };
            184 }
          
            186 element tag_table(const repository& repo)
            187 {
            188   using namespace hemplate::html;  // NOLINT
          
            190   return element {
            191       h2 {"Tags"},
            192       wtable(
            193           {" ", "Name", "Last commit date", "Author"},
            194           repo.get_tags(),
            195           [&](const auto& tag)
            196           {
            197             return tr {
            198                 td {" "},
            199                 td {tag.get_name()},
            200                 td {tag.get_time()},
            201                 td {tag.get_author()},
            202             };
            203           }
            204       ),
            205   };
            206 }
          
            208 element file_changes(const diff& diff)
            209 {
            210   using namespace hemplate::html;  // NOLINT
          
            212   return element {
            213       b {"Diffstat:"},
            214       wtable(
            215           {},
            216           diff.get_deltas(),
            217           [&](const auto& delta)
            218           {
            219             static const char* marker = " ADMRC  T  ";
          
            221             const std::string link = std::format("#{}", delta->new_file.path);
          
            223             uint32_t add = delta.get_adds();
            224             uint32_t del = delta.get_dels();
            225             const uint32_t changed = add + del;
            226             const uint32_t total = 80;
            227             if (changed > total) {
            228               const double percent = 1.0 * total / changed;
          
            230               if (add > 0) {
            231                 add = static_cast<uint32_t>(std::lround(percent * add) + 1);
            232               }
          
            234               if (del > 0) {
            235                 del = static_cast<uint32_t>(std::lround(percent * del) + 1);
            236               }
            237             }
          
            239             return tr {
            240                 td {std::string(1, marker[delta->status])},  // NOLINT
            241                 td {aHref {link, delta->new_file.path}},
            242                 td {"|"},
            243                 td {
            244                     span {{{"class", "add"}}, std::string(add, '+')},
            245                     span {{{"class", "del"}}, std::string(del, '-')},
            246                 },
            247             };
            248           }
            249       ),
          
            251       p {
            252           std::format(
            253               "{} files changed, {} insertions(+), {} deletions(-)",
            254               diff.get_files_changed(),
            255               diff.get_insertions(),
            256               diff.get_deletions()
            257           ),
            258       },
            259   };
            260 }
          
            262 element diff_hunk(const hunk& hunk)
            263 {
            264   using namespace hemplate::html;  // NOLINT
          
            266   const std::string header(hunk->header);  // NOLINT
            267   return element {
            268       h4 {
            269           std::format(
            270               "@@ -{},{} +{},{} @@ ",
            271               hunk->old_start,
            272               hunk->old_lines,
            273               hunk->new_start,
            274               hunk->new_lines
            275           ),
            276           xmlencode(header.substr(header.rfind('@') + 2)),
            277       },
            278       span {
            279           transform(
            280               hunk.get_lines(),
            281               [](const auto& line) -> element
            282               {
            283                 using hemplate::html::div;
          
            285                 if (line.is_add()) {
            286                   return div {
            287                       {{"class", "inline add"}},
            288                       xmlencode(line.get_content()),
            289                   };
            290                 }
          
            292                 if (line.is_del()) {
            293                   return div {
            294                       {{"class", "inline del"}},
            295                       xmlencode(line.get_content()),
            296                   };
            297                 }
          
            299                 return div {
            300                     {{"class", "inline"}},
            301                     xmlencode(line.get_content()),
            302                 };
            303               }
            304           ),
            305       },
            306   };
            307 }
          
            309 element file_diffs(const diff& diff)
            310 {
            311   using namespace hemplate::html;  // NOLINT
          
            313   return transform(
            314       diff.get_deltas(),
            315       [](const auto& delta)
            316       {
            317         const auto& new_file = delta->new_file.path;
            318         const auto& old_file = delta->new_file.path;
            319         const auto new_link = std::format("../file/{}.html", new_file);
            320         const auto old_link = std::format("../file/{}.html", old_file);
          
            322         return element {
            323             h3 {
            324                 {{"id", delta->new_file.path}},
            325                 "diff --git",
            326                 "a/",
            327                 aHref {new_link, new_file},
            328                 "b/",
            329                 aHref {old_link, old_file},
            330             },
            331             transform(delta.get_hunks(), diff_hunk),
            332         };
            333       }
            334   );
            335 }
          
            337 element commit_diff(const commit& commit)
            338 {
            339   using namespace hemplate::html;  // NOLINT
          
            341   const auto url = std::format("../commit/{}.html", commit.get_id());
            342   const auto mailto = std::string("mailto:") + commit.get_author_email();
          
            344   return element {
            345       table {
            346           tbody {
            347               tr {
            348                   td {b {"commit"}},
            349                   td {aHref {url, commit.get_id()}},
            350               },
            351               commit.get_parentcount() == 0 ? element {} : [&]() -> element
            352               {
            353                 const auto purl =
            354                     std::format("../commit/{}.html", commit.get_parent_id());
          
            356                 return tr {
            357                     td {b {"parent"}},
            358                     td {aHref {purl, commit.get_parent_id()}},
            359                 };
            360               }(),
            361               tr {
            362                   td {b {"author"}},
            363                   td {
            364                       commit.get_author_name(),
            365                       "<",
            366                       aHref {mailto, commit.get_author_email()},
            367                       ">",
            368                   },
            369               },
            370               tr {
            371                   td {b {"date"}},
            372                   td {commit.get_time_long()},
            373               },
            374           },
            375       },
            376       br {},
            377       p {
            378           {{"class", "inline"}},
            379           xmlencode(commit.get_message()),
            380       },
            381       file_changes(commit.get_diff()),
            382       hr {},
            383       file_diffs(commit.get_diff()),
            384   };
            385 }
          
            387 element write_file_title(const file& file)
            388 {
            389   using namespace hemplate::html;  // NOLINT
          
            391   const auto path = file.get_path().filename().string();
          
            393   return element {
            394       h3 {std::format("{} ({}B)", path, file.get_size())},
            395       hr {},
            396   };
            397 }
          
            399 element write_file_content(const file& file)
            400 {
            401   using namespace hemplate::html;  // NOLINT
          
            403   if (file.is_binary()) {
            404     return h4("Binary file");
            405   }
          
            407   const std::string str(file.get_content(), file.get_size());
            408   std::stringstream sstr(str);
          
            410   std::vector<std::string> lines;
            411   std::string tmp;
          
            413   while (std::getline(sstr, tmp, '\n')) {
            414     lines.emplace_back(std::move(tmp));
            415   }
          
            417   int count = 0;
            418   return span {
            419       transform(
            420           lines,
            421           [&](const auto& line)
            422           {
            423             return hemplate::html::div {
            424                 {{"class", "inline"}},
            425                 std::format(
            426                     R"(<a id="{0}" href="#{0}">{0:5}</a> {1})",
            427                     count++,
            428                     xmlencode(line)
            429                 )
            430             };
            431           }
            432       ),
            433   };
            434 }
          
            436 void write_log(
            437     const std::filesystem::path& base,
            438     const repository& repo,
            439     const branch& branch
            440 )
            441 {
            442   std::ofstream ofs(base / "log.html");
            443   document(repo, branch, "Commit list")
            444       .render(
            445           ofs,
            446           [&]()
            447           {
            448             return element {
            449                 page_title(repo, branch),
            450                 commit_table(branch),
            451             };
            452           }
            453       );
            454 }
          
            456 void write_file(
            457     const std::filesystem::path& base,
            458     const repository& repo,
            459     const branch& branch
            460 )
            461 {
            462   std::ofstream ofs(base / "files.html");
            463   document {repo, branch, "File list"}.render(
            464       ofs,
            465       [&]()
            466       {
            467         return element {
            468             page_title(repo, branch),
            469             files_table(branch),
            470         };
            471       }
            472   );
            473 }
          
            475 void write_refs(
            476     const std::filesystem::path& base,
            477     const repository& repo,
            478     const branch& branch
            479 )
            480 {
            481   std::ofstream ofs(base / "refs.html");
            482   document {repo, branch, "Refs list"}.render(
            483       ofs,
            484       [&]()
            485       {
            486         return element {
            487             page_title(repo, branch),
            488             branch_table(repo, branch.get_name()),
            489             tag_table(repo),
            490         };
            491       }
            492   );
            493 }
          
            495 bool write_commits(
            496     const std::filesystem::path& base,
            497     const repository& repo,
            498     const branch& branch
            499 )
            500 {
            501   bool changed = false;
          
            503   for (const auto& commit : branch.get_commits()) {
            504     const std::string file = base / (commit.get_id() + ".html");
            505     if (!args.force && std::filesystem::exists(file)) {
            506       break;
            507     }
          
            509     std::ofstream ofs(file);
            510     document {repo, branch, commit.get_summary(), "../"}.render(
            511         ofs,
            512         [&]()
            513         {
            514           return element {
            515               page_title(repo, branch, "../"),
            516               commit_diff(commit),
            517           };
            518         }
            519     );
            520     changed = true;
            521   }
          
            523   return changed;
            524 }
          
            526 void write_files(
            527     const std::filesystem::path& base,
            528     const repository& repo,
            529     const branch& branch
            530 )
            531 {
            532   for (const auto& file : branch.get_files()) {
            533     const std::filesystem::path path =
            534         base / (file.get_path().string() + ".html");
            535     std::filesystem::create_directories(path.parent_path());
            536     std::ofstream ofs(path);
          
            538     std::string relpath = "../";
            539     for (const char chr : file.get_path().string()) {
            540       if (chr == '/') {
            541         relpath += "../";
            542       }
            543     }
          
            545     document {repo, branch, file.get_path().string(), relpath}.render(
            546         ofs,
            547         [&]()
            548         {
            549           return element {
            550               page_title(repo, branch, relpath),
            551               write_file_title(file),
            552               write_file_content(file),
            553           };
            554         }
            555     );
            556   }
            557 }
          
            559 void write_readme_licence(
            560     const std::filesystem::path& base,
            561     const repository& repo,
            562     const branch& branch
            563 )
            564 {
            565   for (const auto& file : branch.get_special()) {
            566     std::ofstream ofs(base / file.get_path().replace_extension("html"));
            567     document {repo, branch, file.get_path().string()}.render(
            568         ofs,
            569         [&]()
            570         {
            571           std::string html;
          
            573           static const auto process_output =
            574               +[](const MD_CHAR* str, MD_SIZE size, void* data)
            575           {
            576             auto buffer = *static_cast<std::string*>(data);
            577             buffer += std::string(str, size);
            578           };
          
            580           md_html(
            581               file.get_content(),
            582               static_cast<MD_SIZE>(file.get_size()),
            583               process_output,
            584               &html,
            585               MD_DIALECT_GITHUB,
            586               0
            587           );
            588           return element {
            589               page_title(repo, branch),
            590               html,
            591           };
            592         }
            593     );
            594   }
            595 }
          
            597 void write_atom(
            598     std::ostream& ost, const branch& branch, const std::string& base_url
            599 )
            600 {
            601   using namespace hemplate::atom;  // NOLINT
            602   using hemplate::atom::link;
          
            604   ost << feed {
            605       title {args.title},
            606       subtitle {args.description},
            607       id {base_url + '/'},
            608       updated {format_time_now()},
            609       author {name {args.author}},
            610       linkSelf {base_url + "/atom.xml"},
            611       linkAlternate {args.resource_url},
            612       transform(
            613           branch.get_commits(),
            614           [&](const auto& commit)
            615           {
            616             const auto url =
            617                 std::format("{}/commit/{}.html", base_url, commit.get_id());
          
            619             return entry {
            620                 id {url},
            621                 updated {format_time(commit.get_time_raw())},
            622                 title {commit.get_summary()},
            623                 linkHref {url},
            624                 author {
            625                     name {commit.get_author_name()},
            626                     email {commit.get_author_email()},
            627                 },
            628                 content {commit.get_message()},
            629             };
            630           }
            631       ),
            632   };
            633 }
          
            635 void write_rss(
            636     std::ostream& ost, const branch& branch, const std::string& base_url
            637 )
            638 {
            639   using namespace hemplate::rss;  // NOLINT
            640   using hemplate::rss::link;
            641   using hemplate::rss::rss;
          
            643   ost << xml {};
            644   ost << rss {
            645       channel {
            646           title {args.title},
            647           description {args.description},
            648           link {base_url + '/'},
            649           generator {"startgit"},
            650           language {"en-us"},
            651           atomLink {base_url + "/atom.xml"},
            652           transform(
            653               branch.get_commits(),
            654               [&](const auto& commit)
            655               {
            656                 const auto url =
            657                     std::format("{}/commit/{}.html", base_url, commit.get_id());
          
            659                 return item {
            660                     title {commit.get_summary()},
            661                     link {url},
            662                     guid {url},
            663                     pubDate {format_time(commit.get_time_raw())},
            664                     author {std::format(
            665                         "{} ({})",
            666                         commit.get_author_email(),
            667                         commit.get_author_name()
            668                     )},
            669                 };
            670               }
            671           ),
            672       },
            673   };
            674 }
          
            676 }  // namespace startgit
          
            678 int main(int argc, const char* argv[])
            679 {
            680   using namespace startgit;  // NOLINT
            681   using namespace poafloc;  // NOLINT
          
            683   auto program = parser<arguments_t> {
            684       positional {
            685           argument {"repository", &arguments_t::set_repository},
            686       },
            687       group {
            688           "Output mode",
            689           direct {
            690               "o output",
            691               &arguments_t::output_dir,
            692               "DIR Output directory",
            693           },
            694           boolean {
            695               "f force",
            696               &arguments_t::force,
            697               "Force write even if file exists",
            698           },
            699           list {
            700               "s special",
            701               &arguments_t::add_special,
            702               "FILE Files to be rendered to html",
            703           },
            704           direct {
            705               "g github",
            706               &arguments_t::github,
            707               "USERNAME Github username for url translation",
            708           },
            709       },
            710       group {
            711           "General Information",
            712           direct {
            713               "b base",
            714               &arguments_t::set_base,
            715               "URL Absolute destination",
            716           },
            717           direct {
            718               "r resources",
            719               &arguments_t::set_resource,
            720               "URL Location of styles and scripts",
            721           },
            722           direct {
            723               "a author",
            724               &arguments_t::author,
            725               "NAME Owner of the repository",
            726           },
            727           direct {
            728               "t title",
            729               &arguments_t::title,
            730               "TITLE Title for the index page",
            731           },
            732           direct {
            733               "d description",
            734               &arguments_t::description,
            735               "DESC Description for the index page",
            736           },
            737       },
            738   };
          
            740   try {
            741     program(args, argc, argv);
            742     if (args.repos.empty()) {
            743       return -1;
            744     }
          
            746     const git2wrap::libgit2 libgit;
          
            748     auto& output_dir = args.output_dir;
            749     std::filesystem::create_directories(output_dir);
            750     output_dir = std::filesystem::canonical(output_dir);
          
            752     const repository repo(args.repos.front());
            753     const std::filesystem::path base = args.output_dir / repo.get_name();
            754     std::filesystem::create_directory(base);
          
            756     for (const auto& branch : repo.get_branches()) {
            757       const std::filesystem::path base_branch = base / branch.get_name();
            758       std::filesystem::create_directory(base_branch);
          
            760       const std::filesystem::path commit = base_branch / "commit";
            761       std::filesystem::create_directory(commit);
          
            763       // always update refs in case of a new branch or tag
            764       write_refs(base_branch, repo, branch);
          
            766       const bool changed = write_commits(commit, repo, branch);
            767       if (!args.force && !changed) {
            768         continue;
            769       };
          
            771       write_log(base_branch, repo, branch);
            772       write_file(base_branch, repo, branch);
            773       write_readme_licence(base_branch, repo, branch);
          
            775       const std::filesystem::path file = base_branch / "file";
            776       std::filesystem::create_directory(file);
          
            778       write_files(file, repo, branch);
          
            780       const std::string relative =
            781           std::filesystem::relative(base_branch, args.output_dir);
            782       const auto absolute = "https://git.dimitrijedobrota.com/" + relative;
          
            784       std::ofstream atom(base_branch / "atom.xml");
            785       write_atom(atom, branch, absolute);
          
            787       std::ofstream rss(base_branch / "rss.xml");
            788       write_rss(rss, branch, absolute);
            789     }
            790   } catch (const git2wrap::error<git2wrap::error_code_t::enotfound>& err) {
            791     std::cerr << std::format(
            792         "Warning: {} is not a repository\n", args.repos.front().string()
            793     );
            794   } catch (const poafloc::runtime_error& err) {
            795     std::cerr << std::format("Error (poafloc): {}\n", err.what());
            796     return 1;
            797   } catch (const git2wrap::runtime_error& err) {
            798     std::cerr << std::format("Error (git2wrap): {}\n", err.what());
            799   } catch (const std::runtime_error& err) {
            800     std::cerr << std::format("Error: {}\n", err.what());
            801   } catch (...) {
            802     std::cerr << std::format("Unknown error\n");
            803   }
          
            805   return 0;
            806 }