startgit

Static page generator for git repositories
git clone git://git.dimitrijedobrota.com/startgit.git
Log | Files | Refs | README | LICENSE | HACKING | CONTRIBUTING | CODE_OF_CONDUCT | BUILDING |

main.cpp (27448B)


1 #include <cmath> 2 #include <cstring> 3 #include <filesystem> 4 #include <format> 5 #include <fstream> 6 #include <iostream> 7 #include <string> 8 9 #include <git2wrap/error.hpp> 10 #include <git2wrap/libgit2.hpp> 11 #include <hemplate/classes.hpp> 12 #include <poafloc/poafloc.hpp> 13 14 #include "arguments.hpp" 15 #include "html.hpp" 16 #include "repository.hpp" 17 #include "utils.hpp" 18 19 void write_header(std::ostream& ost, 20 const std::string& title, 21 const std::string& description, 22 const std::string& author, 23 const std::string& relpath = "./", 24 bool has_feed = true) 25 { 26 using namespace hemplate; // NOLINT 27 28 ost << html::doctype(); 29 ost << html::html().set("lang", "en"); 30 ost << html::head(); 31 ost << html::title(title); 32 33 // Meta tags 34 ost << html::meta({{"charset", "UTF-8"}}); 35 ost << html::meta({{"name", "author"}, {"content", author}}); 36 ost << html::meta({{"name", "description"}, {"content", description}}); 37 38 ost << html::meta({{"content", "width=device-width, initial-scale=1"}, 39 {"name", "viewport"}}); 40 41 // Stylesheets 42 ost << html::link({{"rel", "stylesheet"}, {"type", "text/css"}}) 43 .set("href", startgit::args.resource_url + "/css/index.css"); 44 ost << html::link({{"rel", "stylesheet"}, {"type", "text/css"}}) 45 .set("href", startgit::args.resource_url + "/css/colors.css"); 46 47 if (has_feed) { 48 // Rss feed 49 ost << html::link({{"rel", "alternate"}, 50 {"type", "application/atom+xml"}, 51 {"title", "RSS feed"}, 52 {"href", relpath + "rss.xml"}}); 53 // Atom feed 54 ost << html::link({{"rel", "alternate"}, 55 {"type", "application/atom+xml"}, 56 {"title", "Atom feed"}, 57 {"href", relpath + "atom.xml"}}); 58 } 59 60 // Icons 61 ost << html::link({{"rel", "icon"}, {"type", "image/png"}}) 62 .set("sizes", "32x32") 63 .set("href", 64 startgit::args.resource_url + "/img/favicon-32x32.png"); 65 ost << html::link({{"rel", "icon"}, {"type", "image/png"}}) 66 .set("sizes", "16x16") 67 .set("href", 68 startgit::args.resource_url + "/img/favicon-16x16.png"); 69 ost << html::head(); 70 ost << html::body(); 71 ost << html::input() 72 .set("type", "checkbox") 73 .set("id", "theme_switch") 74 .set("class", "theme_switch"); 75 76 ost << html::div().set("id", "content"); 77 html::div().tgl_state(); 78 79 ost << html::main(); 80 ost << html::label(" ") 81 .set("for", "theme_switch") 82 .set("class", "switch_label"); 83 } 84 85 void write_header(std::ostream& ost, 86 const startgit::repository& repo, 87 const startgit::branch& branch, 88 const std::string& description, 89 const std::string& relpath = "./", 90 bool has_feed = true) 91 { 92 write_header(ost, 93 std::format("{} ({}) - {}", 94 repo.get_name(), 95 branch.get_name(), 96 repo.get_description()), 97 description, 98 repo.get_owner(), 99 relpath, 100 has_feed); 101 } 102 103 void write_title(std::ostream& ost, 104 const startgit::repository& repo, 105 const startgit::branch& branch, 106 const std::string& relpath = "./") 107 { 108 using namespace hemplate; // NOLINT 109 110 const auto dropdown = [&]() 111 { 112 auto span = html::span(); 113 span.add(html::label("Branch: ").set("for", "branch")); 114 span.add(html::select( 115 {{"id", "branch"}, {"onChange", "switchPage(this.value)"}})); 116 117 for (const auto& c_branch : repo.get_branches()) { 118 auto option = html::option(c_branch.get_name()); 119 option.set("value", c_branch.get_name()); 120 if (c_branch.get_name() == branch.get_name()) { 121 option.set("selected", "true"); 122 } 123 span.add(option); 124 } 125 126 span.add(html::select()); 127 return span; 128 }(); 129 130 ost << html::table(); 131 ost << html::tr().add(html::td() 132 .add(html::h1(repo.get_name())) 133 .add(html::span(repo.get_description()))); 134 ost << html::tr().add( 135 html::td() 136 .add(html::text("git clone ")) 137 .add(html::a(repo.get_url()).set("href", repo.get_url()))); 138 139 ost << html::tr() << html::td(); 140 ost << html::a("Log").set("href", relpath + "log.html"); 141 ost << html::text(" | ") 142 << html::a("Files").set("href", relpath + "files.html"); 143 ost << html::text(" | ") 144 << html::a("Refs").set("href", relpath + "refs.html"); 145 146 for (const auto& file : branch.get_special()) { 147 const auto filename = file.get_path().replace_extension("html").string(); 148 const auto name = file.get_path().replace_extension().string(); 149 ost << html::text(" | ") << html::a(name).set("href", relpath + filename); 150 } 151 152 ost << html::text(" | ") << dropdown; 153 ost << html::td() << html::tr(); 154 155 ost << html::table(); 156 ost << html::hr(); 157 } 158 159 void write_commit_table(std::ostream& ost, const startgit::branch& branch) 160 { 161 using namespace hemplate; // NOLINT 162 163 ost << html::table(); 164 ost << html::thead(); 165 ost << html::tr() 166 .add(html::td("Date")) 167 .add(html::td("Commit message")) 168 .add(html::td("Author")) 169 .add(html::td("Files")) 170 .add(html::td("+")) 171 .add(html::td("-")); 172 ost << html::thead(); 173 ost << html::tbody(); 174 175 for (const auto& commit : branch.get_commits()) { 176 const auto url = std::format("./commit/{}.html", commit.get_id()); 177 178 ost << html::tr() 179 .add(html::td(commit.get_time())) 180 .add(html::td().add( 181 html::a(commit.get_summary()).set("href", url))) 182 .add(html::td(commit.get_author_name())) 183 .add(html::td(commit.get_diff().get_files_changed())) 184 .add(html::td(commit.get_diff().get_insertions())) 185 .add(html::td(commit.get_diff().get_deletions())); 186 } 187 188 ost << html::tbody(); 189 ost << html::table(); 190 } 191 192 void write_repo_table_entry(std::ostream& ost, const startgit::repository& repo) 193 { 194 using namespace hemplate; // NOLINT 195 196 for (const auto& branch : repo.get_branches()) { 197 if (branch.get_name() != "master") { 198 continue; 199 } 200 201 const auto url = repo.get_name() + "/master/log.html"; 202 203 ost << html::tr() 204 .add(html::td().add(html::a(repo.get_name()).set("href", url))) 205 .add(html::td(repo.get_description())) 206 .add(html::td(repo.get_owner())) 207 .add(html::td(branch.get_commits()[0].get_time())); 208 return; 209 } 210 211 std::cerr << std::format("Warning: {} doesn't have master branch\n", 212 repo.get_path().string()); 213 } 214 215 void write_repo_table(std::ostream& ost, const std::stringstream& index) 216 { 217 using namespace hemplate; // NOLINT 218 219 ost << html::h1(startgit::args.title); 220 ost << html::p(startgit::args.description); 221 222 ost << html::table(); 223 ost << html::thead(); 224 ost << html::tr() 225 .add(html::td("Name")) 226 .add(html::td("Description")) 227 .add(html::td("Owner")) 228 .add(html::td("Last commit")); 229 ost << html::thead(); 230 ost << html::tbody(); 231 232 ost << index.str(); 233 234 ost << html::tbody(); 235 ost << html::table(); 236 } 237 238 void write_files_table(std::ostream& ost, const startgit::branch& branch) 239 { 240 using namespace hemplate; // NOLINT 241 242 ost << html::table(); 243 ost << html::thead(); 244 ost << html::tr() 245 .add(html::td("Mode")) 246 .add(html::td("Name")) 247 .add(html::td("Size")); 248 ost << html::thead(); 249 ost << html::tbody(); 250 251 for (const auto& file : branch.get_files()) { 252 const auto url = std::format("./file/{}.html", file.get_path().string()); 253 const auto size = file.is_binary() ? std::format("{}B", file.get_size()) 254 : std::format("{}L", file.get_lines()); 255 256 ost << html::tr() 257 .add(html::td(file.get_filemode())) 258 .add(html::td().add(html::a(file.get_path()).set("href", url))) 259 .add(html::td(size)); 260 } 261 262 ost << html::tbody(); 263 ost << html::table(); 264 } 265 266 void write_branch_table(std::ostream& ost, 267 const startgit::repository& repo, 268 const std::string& branch_name) 269 { 270 using namespace hemplate; // NOLINT 271 272 ost << html::h2("Branches"); 273 ost << html::table(); 274 ost << html::thead(); 275 ost << html::tr() 276 .add(html::td("&nbsp;")) 277 .add(html::td("Name")) 278 .add(html::td("Last commit date")) 279 .add(html::td("Author")); 280 ost << html::thead(); 281 ost << html::tbody(); 282 283 for (const auto& branch : repo.get_branches()) { 284 const auto& last = branch.get_last_commit(); 285 const auto url = branch.get_name() != branch_name 286 ? std::format("../{}/refs.html", branch.get_name()) 287 : ""; 288 289 ost << html::tr() 290 .add(html::td(branch.get_name() == branch_name ? "*" : "&nbsp;")) 291 .add(html::td().add(html::a(branch.get_name()).set("href", url))) 292 .add(html::td(last.get_time())) 293 .add(html::td(last.get_author_name())); 294 } 295 296 ost << html::tbody(); 297 ost << html::table(); 298 } 299 300 void write_tag_table(std::ostream& ost, const startgit::repository& repo) 301 { 302 using namespace hemplate; // NOLINT 303 304 ost << html::h2("Tags"); 305 ost << html::table(); 306 ost << html::thead(); 307 ost << html::tr() 308 .add(html::td("&nbsp;")) 309 .add(html::td("Name")) 310 .add(html::td("Last commit date")) 311 .add(html::td("Author")); 312 ost << html::thead(); 313 ost << html::tbody(); 314 315 for (const auto& tag : repo.get_tags()) { 316 ost << html::tr() 317 .add(html::td("&nbsp;")) 318 .add(html::td(tag.get_name())) 319 .add(html::td(tag.get_time())) 320 .add(html::td(tag.get_author())); 321 } 322 323 ost << html::tbody(); 324 ost << html::table(); 325 } 326 327 void write_file_changes(std::ostream& ost, const startgit::diff& diff) 328 { 329 using namespace hemplate; // NOLINT 330 331 ost << html::b("Diffstat:"); 332 ost << html::table() << html::tbody(); 333 334 for (const auto& delta : diff.get_deltas()) { 335 static const char* marker = " ADMRC T "; 336 337 const std::string link = std::format("#{}", delta->new_file.path); 338 339 uint32_t add = delta.get_adds(); 340 uint32_t del = delta.get_dels(); 341 const uint32_t changed = add + del; 342 const uint32_t total = 80; 343 if (changed > total) { 344 const double percent = 1.0 * total / changed; 345 346 if (add > 0) { 347 add = static_cast<uint32_t>(std::lround(percent * add) + 1); 348 } 349 350 if (del > 0) { 351 del = static_cast<uint32_t>(std::lround(percent * del) + 1); 352 } 353 } 354 355 ost << html::tr() 356 .add(html::td(std::string(1, marker[delta->status]))) // NOLINT 357 .add(html::td().add( 358 html::a(delta->new_file.path).set("href", link))) 359 .add(html::td("|")) 360 .add(html::td() 361 .add(html::span() 362 .add(html::text(std::string(add, '+'))) 363 .set("class", "add")) 364 .add(html::span() 365 .add(html::text(std::string(del, '-'))) 366 .set("class", "del"))); 367 } 368 369 ost << html::tbody() << html::table(); 370 ost << html::p( 371 std::format("{} files changed, {} insertions(+), {} deletions(-)", 372 diff.get_files_changed(), 373 diff.get_insertions(), 374 diff.get_deletions())); 375 } 376 377 void write_file_diffs(std::ostream& ost, const startgit::diff& diff) 378 { 379 using namespace hemplate; // NOLINT 380 381 for (const auto& delta : diff.get_deltas()) { 382 const auto new_link = std::format("../file/{}.html", delta->new_file.path); 383 const auto old_link = std::format("../file/{}.html", delta->old_file.path); 384 385 ost << html::h3().set("id", delta->new_file.path); 386 ost << "diff --git"; 387 ost << " a/" << html::a(delta->new_file.path).set("href", new_link); 388 ost << " b/" << html::a(delta->old_file.path).set("href", old_link); 389 ost << html::h3(); 390 391 for (const auto& hunk : delta.get_hunks()) { 392 const std::string header(hunk->header); // NOLINT 393 394 ost << html::h4(); 395 ost << std::format("@@ -{},{} +{},{} @@ ", 396 hunk->old_start, 397 hunk->old_lines, 398 hunk->new_start, 399 hunk->new_lines); 400 401 startgit::xmlencode(ost, header.substr(header.rfind('@') + 2)); 402 ost << html::h4(); 403 404 ost << html::span().set("style", "white-space: pre"); 405 for (const auto& line : hunk.get_lines()) { 406 auto div = html::div(); 407 if (line.is_add()) { 408 div.set("class", "add"); 409 } else if (line.is_del()) { 410 div.set("class", "del"); 411 } 412 413 ost << div; 414 startgit::xmlencode(ost, line.get_content()); 415 ost << div; 416 } 417 ost << html::span(); 418 } 419 } 420 } 421 422 void write_commit_diff(std::ostream& ost, const startgit::commit& commit) 423 { 424 using namespace hemplate; // NOLINT 425 426 ost << html::table() << html::tbody(); 427 428 const auto url = std::format("../commit/{}.html", commit.get_id()); 429 ost << html::tr() 430 .add(html::td().add(html::b("commit"))) 431 .add(html::td().add(html::a(commit.get_id()).set("href", url))); 432 433 if (commit.get_parentcount() > 0) { 434 const auto purl = std::format("../commit/{}.html", commit.get_parent_id()); 435 ost << html::tr() 436 .add(html::td().add(html::b("parent"))) 437 .add(html::td().add( 438 html::a(commit.get_parent_id()).set("href", purl))); 439 } 440 441 const auto mailto = std::string("mailto:") + commit.get_author_email(); 442 ost << html::tr(); 443 ost << html::td().add(html::b("author")); 444 ost << html::td() << commit.get_author_name() << " &lt;"; 445 ost << html::a(commit.get_author_email()).set("href", mailto); 446 ost << "&gt;" << html::td(); 447 ost << html::tr(); 448 449 ost << html::tr() 450 .add(html::td().add(html::b("date"))) 451 .add(html::td(commit.get_time_long())); 452 ost << html::tbody() << html::table(); 453 454 ost << html::br() << html::p().set("style", "white-space: pre;"); 455 startgit::xmlencode(ost, commit.get_message()); 456 ost << html::p(); 457 458 write_file_changes(ost, commit.get_diff()); 459 ost << html::hr(); 460 write_file_diffs(ost, commit.get_diff()); 461 } 462 463 void write_file_title(std::ostream& ost, const startgit::file& file) 464 { 465 using namespace hemplate; // NOLINT 466 467 ost << html::h3(std::format( 468 "{} ({}B)", file.get_path().filename().string(), file.get_size())); 469 ost << html::hr(); 470 } 471 472 void write_file_content(std::ostream& ost, const startgit::file& file) 473 { 474 using namespace hemplate; // NOLINT 475 476 if (file.is_binary()) { 477 ost << html::h4("Binary file"); 478 return; 479 } 480 481 const std::string str(file.get_content(), file.get_size()); 482 std::stringstream sstr(str); 483 484 std::string line; 485 486 ost << html::span().set("style", "white-space: pre;"); 487 for (int count = 1; std::getline(sstr, line, '\n'); count++) { 488 ost << std::format( 489 R"(<a id="{}" href="#{}">{:5}</a>)", count, count, count); 490 ost << " "; 491 startgit::xmlencode(ost, line); 492 ost << '\n'; 493 } 494 ost << html::span(); 495 } 496 497 void write_html(std::ostream& ost, const startgit::file& file) 498 { 499 static const auto process_output = 500 +[](const MD_CHAR* str, MD_SIZE size, void* data) 501 { 502 std::ofstream& ofs = *static_cast<std::ofstream*>(data); 503 ofs << std::string(str, size); 504 }; 505 506 startgit::md_html(file.get_content(), 507 static_cast<MD_SIZE>(file.get_size()), 508 process_output, 509 &ost, 510 MD_DIALECT_GITHUB, 511 0); 512 } 513 514 void write_footer(std::ostream& ost) 515 { 516 using namespace hemplate; // NOLINT 517 518 ost << html::main(); 519 520 html::div().tgl_state(); 521 ost << html::div(); 522 523 const auto jss = startgit::args.resource_url + "/scripts/main.js"; 524 ost << html::script(" ").set("src", jss); 525 ost << html::script( 526 "function switchPage(value) {" 527 " let arr = window.location.href.split('/');" 528 " arr[4] = value;" 529 " history.replaceState(history.state, '', arr.join('/'));" 530 " location.reload();" 531 "}"); 532 ost << html::style( 533 " table { " 534 " margin-left: 0;" 535 " background-color: inherit;" 536 " border: none" 537 "} select { " 538 " color: var(--theme_fg1);" 539 " background-color: inherit;" 540 " border: 1px solid var(--theme_bg4);" 541 "} select option {" 542 " color: var(--theme_fg2) !important;" 543 " background-color: var(--theme_bg3) !important;" 544 "} .add {" 545 " color: var(--theme_green);" 546 "} .del {" 547 " color: var(--theme_red);" 548 "}"); 549 ost << html::body(); 550 ost << html::html(); 551 } 552 553 void write_log(const std::filesystem::path& base, 554 const startgit::repository& repo, 555 const startgit::branch& branch) 556 { 557 std::ofstream ofs(base / "log.html"); 558 559 write_header(ofs, repo, branch, "Commit list"); 560 write_title(ofs, repo, branch); 561 write_commit_table(ofs, branch); 562 write_footer(ofs); 563 } 564 565 void write_file(const std::filesystem::path& base, 566 const startgit::repository& repo, 567 const startgit::branch& branch) 568 { 569 std::ofstream ofs(base / "files.html"); 570 571 write_header(ofs, repo, branch, "File list"); 572 write_title(ofs, repo, branch); 573 write_files_table(ofs, branch); 574 write_footer(ofs); 575 } 576 577 void write_refs(const std::filesystem::path& base, 578 const startgit::repository& repo, 579 const startgit::branch& branch) 580 { 581 std::ofstream ofs(base / "refs.html"); 582 583 write_header(ofs, repo, branch, "Refs list"); 584 write_title(ofs, repo, branch); 585 write_branch_table(ofs, repo, branch.get_name()); 586 write_tag_table(ofs, repo); 587 write_footer(ofs); 588 } 589 590 bool write_commits(const std::filesystem::path& base, 591 const startgit::repository& repo, 592 const startgit::branch& branch) 593 { 594 bool changed = false; 595 596 for (const auto& commit : branch.get_commits()) { 597 const std::string file = base / (commit.get_id() + ".html"); 598 if (!startgit::args.force && std::filesystem::exists(file)) { 599 break; 600 } 601 std::ofstream ofs(file); 602 603 write_header(ofs, repo, branch, commit.get_summary(), "../"); 604 write_title(ofs, repo, branch, "../"); 605 write_commit_diff(ofs, commit); 606 write_footer(ofs); 607 changed = true; 608 } 609 610 return changed; 611 } 612 613 void write_files(const std::filesystem::path& base, 614 const startgit::repository& repo, 615 const startgit::branch& branch) 616 { 617 for (const auto& file : branch.get_files()) { 618 const std::filesystem::path path = 619 base / (file.get_path().string() + ".html"); 620 std::filesystem::create_directories(path.parent_path()); 621 std::ofstream ofs(path); 622 623 std::string relpath = "../"; 624 for (const char chr : file.get_path().string()) { 625 if (chr == '/') { 626 relpath += "../"; 627 } 628 } 629 630 write_header(ofs, repo, branch, file.get_path(), relpath); 631 write_title(ofs, repo, branch, relpath); 632 write_file_title(ofs, file); 633 write_file_content(ofs, file); 634 write_footer(ofs); 635 } 636 } 637 638 void write_readme_licence(const std::filesystem::path& base, 639 const startgit::repository& repo, 640 const startgit::branch& branch) 641 { 642 for (const auto& file : branch.get_special()) { 643 std::ofstream ofs(base / file.get_path().replace_extension("html")); 644 write_header(ofs, repo, branch, file.get_path()); 645 write_title(ofs, repo, branch); 646 write_html(ofs, file); 647 write_footer(ofs); 648 } 649 } 650 651 void write_atom(std::ostream& ost, 652 const startgit::branch& branch, 653 const std::string& base_url) 654 { 655 using namespace hemplate; // NOLINT 656 657 ost << atom::feed(); 658 ost << atom::title(startgit::args.title); 659 ost << atom::subtitle(startgit::args.description); 660 661 ost << atom::id(base_url + '/'); 662 ost << atom::updated(atom::format_time_now()); 663 ost << atom::author().add(atom::name(startgit::args.author)); 664 ost << atom::link(" ", {{"rel", "self"}, {"href", base_url + "/atom.xml"}}); 665 ost << atom::link(" ", 666 {{"href", startgit::args.resource_url}, 667 {"rel", "alternate"}, 668 {"type", "text/html"}}); 669 670 for (const auto& commit : branch.get_commits()) { 671 const auto url = 672 std::format("{}/commit/{}.html", base_url, commit.get_id()); 673 674 ost << atom::entry() 675 .add(atom::id(url)) 676 .add(atom::updated(atom::format_time(commit.get_time_raw()))) 677 .add(atom::title(commit.get_summary())) 678 .add(atom::link(" ").set("href", url)) 679 .add(atom::author() 680 .add(atom::name(commit.get_author_name())) 681 .add(atom::email(commit.get_author_email()))) 682 .add(atom::content(commit.get_message())); 683 } 684 685 ost << atom::feed(); 686 } 687 688 void write_rss(std::ostream& ost, 689 const startgit::branch& branch, 690 const std::string& base_url) 691 { 692 using namespace hemplate; // NOLINT 693 694 ost << xml(); 695 ost << rss::rss(); 696 ost << rss::channel(); 697 698 ost << rss::title(startgit::args.title); 699 ost << rss::description(startgit::args.description); 700 ost << rss::link(base_url + '/'); 701 ost << rss::generator("startgit"); 702 ost << rss::language("en-us"); 703 ost << rss::atomLink().set("href", base_url + "/atom.xml"); 704 705 for (const auto& commit : branch.get_commits()) { 706 const auto url = 707 std::format("{}/commit/{}.html", base_url, commit.get_id()); 708 709 ost << rss::item() 710 .add(rss::title(commit.get_summary())) 711 .add(rss::link(url)) 712 .add(rss::guid(url)) 713 .add(rss::pubDate(rss::format_time(commit.get_time_raw()))) 714 .add(rss::author(std::format("{} ({})", 715 commit.get_author_email(), 716 commit.get_author_name()))); 717 } 718 719 ost << rss::channel(); 720 ost << rss::rss(); 721 } 722 723 int parse_opt(int key, const char* arg, poafloc::Parser* parser) 724 { 725 auto* l_args = static_cast<startgit::arguments_t*>(parser->input()); 726 switch (key) { 727 case 'o': 728 l_args->output_dir = arg; 729 break; 730 case 'b': 731 l_args->base_url = arg; 732 if (l_args->base_url.back() == '/') { 733 l_args->base_url.pop_back(); 734 } 735 break; 736 case 'r': 737 l_args->resource_url = arg; 738 if (l_args->resource_url.back() == '/') { 739 l_args->resource_url.pop_back(); 740 } 741 break; 742 case 'a': 743 l_args->author = arg; 744 break; 745 case 'd': 746 l_args->description = arg; 747 break; 748 case 'g': 749 l_args->github = arg; 750 break; 751 case 'f': 752 l_args->force = true; 753 break; 754 case 's': { 755 std::stringstream sstream(arg); 756 std::string crnt; 757 758 l_args->special.clear(); 759 while (std::getline(sstream, crnt, ',')) { 760 l_args->special.emplace(crnt); 761 } 762 763 break; 764 } 765 case poafloc::ARG: 766 try { 767 l_args->repos.emplace_back(std::filesystem::canonical(arg)); 768 } catch (const std::filesystem::filesystem_error& arr) { 769 std::cerr << std::format("Warning: {} doesn't exist\n", arg); 770 } 771 break; 772 default: 773 break; 774 } 775 return 0; 776 } 777 778 // NOLINTBEGIN 779 // clang-format off 780 static const poafloc::option_t options[] = { 781 {0, 0, 0, 0, "Output mode", 1}, 782 {"output", 'o', "DIR", 0, "Output directory"}, 783 {"force", 'f', 0, 0, "Force write even if file exists"}, 784 {"special", 's', "NAME", 0, "Comma separated files to be rendered to html"}, 785 {"github", 'g', "USERNAME", 0, "Github username for url translation"}, 786 {0, 0, 0, 0, "General information", 2}, 787 {"base", 'b', "URL", 0, "Absolute destination URL"}, 788 {"resource", 'r', "URL", 0, "URL that houses styles and scripts"}, 789 {"author", 'a', "NAME", 0, "Owner of the repository"}, 790 {"title", 't', "TITLE", 0, "Title for the index page"}, 791 {"description", 'd', "DESC", 0, "Description for the index page"}, 792 {0, 0, 0, 0, "Informational Options", -1}, 793 {0}, 794 }; 795 // clang-format on 796 797 static const poafloc::arg_t arg { 798 options, 799 parse_opt, 800 "repositories...", 801 "", 802 }; 803 // NOLINTEND 804 805 int main(int argc, char* argv[]) 806 { 807 if (poafloc::parse(&arg, argc, argv, 0, &startgit::args) != 0) { 808 std::cerr << "There was an error while parsing arguments"; 809 return 1; 810 } 811 812 try { 813 const git2wrap::libgit2 libgit; 814 std::stringstream index; 815 816 auto& output_dir = startgit::args.output_dir; 817 std::filesystem::create_directories(output_dir); 818 output_dir = std::filesystem::canonical(output_dir); 819 820 for (const auto& repo_path : startgit::args.repos) { 821 try { 822 const startgit::repository repo(repo_path); 823 const std::filesystem::path base = 824 startgit::args.output_dir / repo.get_name(); 825 std::filesystem::create_directory(base); 826 827 for (const auto& branch : repo.get_branches()) { 828 const std::filesystem::path base_branch = base / branch.get_name(); 829 std::filesystem::create_directory(base_branch); 830 831 const std::filesystem::path commit = base_branch / "commit"; 832 std::filesystem::create_directory(commit); 833 834 const bool changed = write_commits(commit, repo, branch); 835 if (!startgit::args.force && !changed) { 836 continue; 837 }; 838 839 write_log(base_branch, repo, branch); 840 write_file(base_branch, repo, branch); 841 write_refs(base_branch, repo, branch); 842 write_readme_licence(base_branch, repo, branch); 843 844 const std::filesystem::path file = base_branch / "file"; 845 std::filesystem::create_directory(file); 846 847 write_files(file, repo, branch); 848 849 const std::string relative = 850 std::filesystem::relative(base_branch, startgit::args.output_dir); 851 const auto absolute = "https://git.dimitrijedobrota.com/" + relative; 852 853 std::ofstream atom(base_branch / "atom.xml"); 854 write_atom(atom, branch, absolute); 855 856 std::ofstream rss(base_branch / "rss.xml"); 857 write_rss(rss, branch, absolute); 858 } 859 860 write_repo_table_entry(index, repo); 861 } catch (const git2wrap::error<git2wrap::error_code_t::ENOTFOUND>& err) { 862 std::cerr << std::format("Warning: {} is not a repository\n", 863 repo_path.string()); 864 } 865 } 866 867 std::ofstream ofs(startgit::args.output_dir / "index.html"); 868 write_header(ofs, 869 startgit::args.title, 870 startgit::args.description, 871 startgit::args.author, 872 "./", 873 /*has_feed=*/false); 874 write_repo_table(ofs, index); 875 write_footer(ofs); 876 } catch (const git2wrap::runtime_error& err) { 877 std::cerr << std::format("Error (git2wrap): {}\n", err.what()); 878 } catch (const std::runtime_error& err) { 879 std::cerr << std::format("Error: {}\n", err.what()); 880 } catch (...) { 881 std::cerr << std::format("Unknown error\n"); 882 } 883 884 return 0; 885 }