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

commit 242ae076ddb665b60394f51a158ebba169250c9e
parent 709e26144f2bd17fda81e65ef982857a18f5b681
author Dimitrije Dobrota < mail@dimitrijedobrota.com >
date Thu, 24 Apr 2025 09:02:09 +0200

Improve formatting

Diffstat:
M .clang-format | + -
M source/common.cpp | +++++++++++++++++++++++++++++++++ -----------------------
M source/common.hpp | ++++++++++++++++ ------------
M source/diff.cpp | +++++++++++++++++ ------------
M source/diff.hpp | ++++++++++++++ ------------
M source/file.cpp | ++ --
M source/html.cpp | ++++++++++++++++++++++++++++++++++++++++++++++++ ----------------------------------
M source/html.hpp | ++++++++ ------
M source/repository.cpp | +++++ ---
M source/repository.hpp | +++ --
M source/startgit-index.cpp | +++++++++++++++++++++++++++++++++++++++++++ ---------------------------------------
M source/startgit.cpp | +++++++++++++++++++++++++++++++++++++++++++++ -------------------------------------
M source/utils.cpp | +++++++++ ------

13 files changed, 521 insertions(+), 412 deletions(-)


diff --git a/ .clang-format b/ .clang-format

@@ -2,7 +2,7 @@

Language: Cpp
# BasedOnStyle: Chromium
AccessModifierOffset: -2
AlignAfterOpenBracket: Align
AlignAfterOpenBracket: BlockIndent
AlignConsecutiveMacros: false
AlignConsecutiveAssignments: false
AlignConsecutiveBitFields: false

diff --git a/ source/common.cpp b/ source/common.cpp

@@ -9,12 +9,14 @@

namespace startgit
{

void write_header(std::ostream& ost,
const std::string& title_txt,
const std::string& description,
const std::string& author,
const std::string& relpath,
bool has_feed)
void write_header(
std::ostream& ost,
const std::string& title_txt,
const std::string& description,
const std::string& author,
const std::string& relpath,
bool has_feed
)
{
using namespace hemplate::html; // NOLINT
using hemplate::html::div;

@@ -97,22 +99,28 @@ void write_header(std::ostream& ost,

});
}

void write_header(std::ostream& ost,
const repository& repo,
const branch& branch,
const std::string& description,
const std::string& relpath,
bool has_feed)
void write_header(
std::ostream& ost,
const repository& repo,
const branch& branch,
const std::string& description,
const std::string& relpath,
bool has_feed
)
{
write_header(ost,
std::format("{} ({}) - {}",
repo.get_name(),
branch.get_name(),
repo.get_description()),
description,
repo.get_owner(),
relpath,
has_feed);
write_header(
ost,
std::format(
"{} ({}) - {}",
repo.get_name(),
branch.get_name(),
repo.get_description()
),
description,
repo.get_owner(),
relpath,
has_feed
);
}

void write_footer(std::ostream& ost)

@@ -132,7 +140,8 @@ void write_footer(std::ostream& ost)

" arr[4] = value;"
" history.replaceState(history.state, '', arr.join('/'));"
" location.reload();"
"}");
"}"
);
ost << style(
" table { "
" margin-left: 0;"

@@ -149,7 +158,8 @@ void write_footer(std::ostream& ost)

" color: var(--theme_green);"
"} .del {"
" color: var(--theme_red);"
"}");
"}"
);
ost << body();
ost << html();
}

diff --git a/ source/common.hpp b/ source/common.hpp

@@ -9,19 +9,23 @@

namespace startgit
{

void write_header(std::ostream& ost,
const std::string& title_txt,
const std::string& description,
const std::string& author,
const std::string& relpath = "./",
bool has_feed = true);
void write_header(
std::ostream& ost,
const std::string& title_txt,
const std::string& description,
const std::string& author,
const std::string& relpath = "./",
bool has_feed = true
);

void write_header(std::ostream& ost,
const repository& repo,
const branch& branch,
const std::string& description,
const std::string& relpath = "./",
bool has_feed = true);
void write_header(
std::ostream& ost,
const repository& repo,
const branch& branch,
const std::string& description,
const std::string& relpath = "./",
bool has_feed = true
);

void write_footer(std::ostream& ost);

diff --git a/ source/diff.cpp b/ source/diff.cpp

@@ -13,8 +13,11 @@ diff::diff(const git2wrap::commit& cmmt)


git2wrap::diff_options opts;
git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION);

// NOLINTBEGIN hicpp-signed-bitwise
opts.flags = GIT_DIFF_DISABLE_PATHSPEC_MATCH | GIT_DIFF_IGNORE_SUBMODULES
| GIT_DIFF_INCLUDE_TYPECHANGE;
// NOLINTEND hicpp-signed-bitwise

m_diff = git2wrap::diff::tree_to_tree(ptree, cmmt.get_tree(), &opts);
m_stats = m_diff.get_stats();

@@ -27,7 +30,8 @@ const std::vector<delta>& diff::get_deltas() const

}

m_diff.foreach(
file_cb, nullptr, hunk_cb, line_cb, const_cast<diff*>(this)); // NOLINT
file_cb, nullptr, hunk_cb, line_cb, const_cast<diff*>(this) // NOLINT
);

for (auto& delta : m_deltas) {
for (const auto& hunk : delta.get_hunks()) {

@@ -37,32 +41,33 @@ const std::vector<delta>& diff::get_deltas() const

}
}
}

return m_deltas;
}

int diff::file_cb(const git_diff_delta* delta,
float /* progress */,
void* payload)
int diff::file_cb(
const git_diff_delta* delta, float /* progress */, void* payload
)
{
diff& crnt = *reinterpret_cast<diff*>(payload); // NOLINT
crnt.m_deltas.emplace_back(delta);
return 0;
}

int diff::hunk_cb(const git_diff_delta* /* delta */,
const git_diff_hunk* hunk,
void* payload)
int diff::hunk_cb(
const git_diff_delta* /* delta */, const git_diff_hunk* hunk, void* payload
)
{
diff& crnt = *reinterpret_cast<diff*>(payload); // NOLINT
crnt.m_deltas.back().m_hunks.emplace_back(hunk);
return 0;
}

int diff::line_cb(const git_diff_delta* /* delta */,
const git_diff_hunk* /* hunk */,
const git_diff_line* line,
void* payload)
int diff::line_cb(
const git_diff_delta* /* delta */,
const git_diff_hunk* /* hunk */,
const git_diff_line* line,
void* payload
)
{
diff& crnt = *reinterpret_cast<diff*>(payload); // NOLINT
crnt.m_deltas.back().m_hunks.back().m_lines.emplace_back(line);

diff --git a/ source/diff.hpp b/ source/diff.hpp

@@ -73,18 +73,20 @@ public:

const std::vector<delta>& get_deltas() const;

private:
static int file_cb(const git_diff_delta* delta,
float progress,
void* payload);

static int hunk_cb(const git_diff_delta* delta,
const git_diff_hunk* hunk,
void* payload);

static int line_cb(const git_diff_delta* delta,
const git_diff_hunk* hunk,
const git_diff_line* line,
void* payload);
static int file_cb(
const git_diff_delta* delta, float progress, void* payload
);

static int hunk_cb(
const git_diff_delta* delta, const git_diff_hunk* hunk, void* payload
);

static int line_cb(
const git_diff_delta* delta,
const git_diff_hunk* hunk,
const git_diff_line* line,
void* payload
);

git2wrap::diff m_diff;
git2wrap::diff_stats m_stats;

diff --git a/ source/file.cpp b/ source/file.cpp

@@ -13,8 +13,8 @@ namespace startgit

file::file(const git2wrap::tree_entry& entry, std::filesystem::path path)
: m_filemode(filemode(entry.get_filemode()))
, m_path(std::move(path))
, m_blob(
git2wrap::repository(entry.get_owner()).blob_lookup(entry.get_id()))
, m_blob(git2wrap::repository(entry.get_owner()).blob_lookup(entry.get_id())
)
{
}

diff --git a/ source/html.cpp b/ source/html.cpp

@@ -53,8 +53,9 @@ public:

void render_open_ol_block(const MD_BLOCK_OL_DETAIL* det);
void render_open_li_block(const MD_BLOCK_LI_DETAIL* det);
void render_open_code_block(const MD_BLOCK_CODE_DETAIL* det);
void render_open_td_block(const MD_CHAR* cell_type,
const MD_BLOCK_TD_DETAIL* det);
void render_open_td_block(
const MD_CHAR* cell_type, const MD_BLOCK_TD_DETAIL* det
);
void render_open_a_span(const MD_SPAN_A_DETAIL* det);
void render_open_img_span(const MD_SPAN_IMG_DETAIL* det);
void render_close_img_span(const MD_SPAN_IMG_DETAIL* det);

@@ -175,9 +176,11 @@ std::string translate_url(const MD_CHAR* data, MD_SIZE size)

auto itr = startgit::args.special.find(url.substr(rslash + 1));
if (itr != startgit::args.special.end()) {
auto cpy = *itr;
url = std::format("{}/{}.html",
url.substr(0, rslash),
cpy.replace_extension().string());
url = std::format(
"{}/{}.html",
url.substr(0, rslash),
cpy.replace_extension().string()
);
} else {
const std::size_t slash = url.find('/', bpos + 1);
url.replace(slash, 1, "/file/");

@@ -206,9 +209,9 @@ void md_html::render_url_escaped(const MD_CHAR* data, MD_SIZE size)

MD_OFFSET beg = 0;
MD_OFFSET off = 0;

const auto url = translate_url(data, size);
size = static_cast<unsigned>(url.size());
data = url.data();
const auto urll = translate_url(data, size);
size = static_cast<unsigned>(urll.size());
data = urll.data();

while (true) {
while (off < size && !md_html::need_url_esc(data[off])) { // NOLINT

@@ -228,10 +231,12 @@ void md_html::render_url_escaped(const MD_CHAR* data, MD_SIZE size)

break;
default:
hex[0] = '%';
hex[1] = hex_chars[(static_cast<unsigned>(data[off]) >> 4) // NOLINT
& 0xf]; // NOLINT
hex[2] = hex_chars[(static_cast<unsigned>(data[off]) >> 0) // NOLINT
& 0xf]; // NOLINT
hex[1] = hex_chars // NOLINT
[(static_cast<unsigned>(data[off]) >> 4) // NOLINT
& 0xf]; // NOLINT
hex[2] = hex_chars // NOLINT
[(static_cast<unsigned>(data[off]) >> 0) // NOLINT
& 0xf]; // NOLINT
render_verbatim(hex.data(), 3);
break;
}

@@ -261,7 +266,8 @@ unsigned hex_val(char chr)

void md_html::render_utf8_codepoint(unsigned codepoint, append_fn fn_append)
{
static const MD_CHAR utf8_replacement_char[] = {
char(0xef), char(0xbf), char(0xbd)};
char(0xef), char(0xbf), char(0xbd)
};

unsigned char utf8[4];
size_t n;

@@ -287,10 +293,12 @@ void md_html::render_utf8_codepoint(unsigned codepoint, append_fn fn_append)

}

if (0 < codepoint && codepoint <= 0x10ffff) {
std::invoke(fn_append,
this,
reinterpret_cast<char*>(utf8),
static_cast<MD_SIZE>(n)); // NOLINT
std::invoke(
fn_append,
this,
reinterpret_cast<char*>(utf8),
static_cast<MD_SIZE>(n)
); // NOLINT
} else {
std::invoke(fn_append, this, utf8_replacement_char, 3);
}

@@ -299,9 +307,9 @@ void md_html::render_utf8_codepoint(unsigned codepoint, append_fn fn_append)


/* Translate entity to its UTF-8 equivalent, or output the verbatim one
* if such entity is unknown (or if the translation is disabled). */
void md_html::render_entity(const MD_CHAR* text,
MD_SIZE size,
append_fn fn_append)
void md_html::render_entity(
const MD_CHAR* text, MD_SIZE size, append_fn fn_append
)
{
/* We assume UTF-8 output is what is desired. */
if (size > 3 && text[1] == '#') { // NOLINT

@@ -366,7 +374,8 @@ void md_html::render_open_li_block(const MD_BLOCK_LI_DETAIL* det)

render_verbatim(
"<li class=\"task-list-item\">"
"<input type=\"checkbox\" "
"class=\"task-list-item-checkbox\" disabled");
"class=\"task-list-item-checkbox\" disabled"
);
if (det->task_mark == 'x' || det->task_mark == 'X') {
render_verbatim(" checked");
}

@@ -390,8 +399,9 @@ void md_html::render_open_code_block(const MD_BLOCK_CODE_DETAIL* det)

render_verbatim(">");
}

void md_html::render_open_td_block(const MD_CHAR* cell_type,
const MD_BLOCK_TD_DETAIL* det)
void md_html::render_open_td_block(
const MD_CHAR* cell_type, const MD_BLOCK_TD_DETAIL* det
)
{
render_verbatim("<");
render_verbatim(cell_type);

@@ -463,7 +473,8 @@ int enter_block_callback(MD_BLOCKTYPE type, void* detail, void* userdata)

"<h3>",
"<h4>",
"<h5>",
"<h6>"};
"<h6>"
};
auto* data = static_cast<class md_html*>(userdata);

switch (type) {

@@ -476,23 +487,25 @@ int enter_block_callback(MD_BLOCKTYPE type, void* detail, void* userdata)

data->render_verbatim("<ul>\n");
break;
case MD_BLOCK_OL:
data->render_open_ol_block(
static_cast<const MD_BLOCK_OL_DETAIL*>(detail));
data->render_open_ol_block(static_cast<const MD_BLOCK_OL_DETAIL*>(detail)
);
break;
case MD_BLOCK_LI:
data->render_open_li_block(
static_cast<const MD_BLOCK_LI_DETAIL*>(detail));
data->render_open_li_block(static_cast<const MD_BLOCK_LI_DETAIL*>(detail)
);
break;
case MD_BLOCK_HR:
data->render_verbatim("<hr>\n");
break;
case MD_BLOCK_H:
data->render_verbatim(
head[static_cast<MD_BLOCK_H_DETAIL*>(detail)->level - 1]); // NOLINT
head[static_cast<MD_BLOCK_H_DETAIL*>(detail)->level - 1] // NOLINT
);
break;
case MD_BLOCK_CODE:
data->render_open_code_block(
static_cast<const MD_BLOCK_CODE_DETAIL*>(detail));
static_cast<const MD_BLOCK_CODE_DETAIL*>(detail)
);
break;
case MD_BLOCK_HTML: /* noop */
break;

@@ -512,12 +525,14 @@ int enter_block_callback(MD_BLOCKTYPE type, void* detail, void* userdata)

data->render_verbatim("<tr>\n");
break;
case MD_BLOCK_TH:
data->render_open_td_block("th",
static_cast<MD_BLOCK_TD_DETAIL*>(detail));
data->render_open_td_block(
"th", static_cast<MD_BLOCK_TD_DETAIL*>(detail)
);
break;
case MD_BLOCK_TD:
data->render_open_td_block("td",
static_cast<MD_BLOCK_TD_DETAIL*>(detail));
data->render_open_td_block(
"td", static_cast<MD_BLOCK_TD_DETAIL*>(detail)
);
break;
}

@@ -532,7 +547,8 @@ int leave_block_callback(MD_BLOCKTYPE type, void* detail, void* userdata)

"</h3>\n",
"</h4>\n",
"</h5>\n",
"</h6>\n"};
"</h6>\n"
};
auto* data = static_cast<class md_html*>(userdata);

switch (type) {

@@ -554,7 +570,8 @@ int leave_block_callback(MD_BLOCKTYPE type, void* detail, void* userdata)

break;
case MD_BLOCK_H:
data->render_verbatim(
head[static_cast<MD_BLOCK_H_DETAIL*>(detail)->level - 1]); // NOLINT
head[static_cast<MD_BLOCK_H_DETAIL*>(detail)->level - 1] // NOLINT
);
break;
case MD_BLOCK_CODE:
data->render_verbatim("</code></pre>\n");

@@ -630,7 +647,8 @@ int enter_span_callback(MD_SPANTYPE type, void* detail, void* userdata)

break;
case MD_SPAN_WIKILINK:
data->render_open_wikilink_span(
static_cast<MD_SPAN_WIKILINK_DETAIL*>(detail));
static_cast<MD_SPAN_WIKILINK_DETAIL*>(detail)
);
break;
}

@@ -683,10 +701,9 @@ int leave_span_callback(MD_SPANTYPE type, void* detail, void* userdata)

return 0;
}

int text_callback(MD_TEXTTYPE type,
const MD_CHAR* text,
MD_SIZE size,
void* userdata)
int text_callback(
MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size, void* userdata
)
{
auto* data = static_cast<class md_html*>(userdata);

@@ -695,8 +712,8 @@ int text_callback(MD_TEXTTYPE type,

data->render_utf8_codepoint(0x0000, &md_html::render_verbatim);
break;
case MD_TEXT_BR:
data->render_verbatim(
(data->image_nesting_level == 0 ? ("<br>\n") : " "));
data->render_verbatim((data->image_nesting_level == 0 ? ("<br>\n") : " ")
);
break;
case MD_TEXT_SOFTBR:
data->render_verbatim((data->image_nesting_level == 0 ? "\n" : " "));

@@ -718,24 +735,33 @@ int text_callback(MD_TEXTTYPE type,

namespace startgit
{

int md_html(const MD_CHAR* input,
MD_SIZE input_size,
void (*process_output)(const MD_CHAR*, MD_SIZE, void*),
void* userdata,
unsigned parser_flags,
unsigned renderer_flags)
int md_html(
const MD_CHAR* input,
MD_SIZE input_size,
void (*process_output)(const MD_CHAR*, MD_SIZE, void*),
void* userdata,
unsigned parser_flags,
unsigned renderer_flags
)
{
class md_html render = {process_output, userdata, renderer_flags, 0};

const MD_PARSER parser = {0,
parser_flags,
enter_block_callback,
leave_block_callback,
enter_span_callback,
leave_span_callback,
text_callback,
nullptr,
nullptr};
class md_html render = {
.process_output = process_output,
.userdata = userdata,
.flags = renderer_flags,
.image_nesting_level = 0
};

const MD_PARSER parser = {
0,
parser_flags,
enter_block_callback,
leave_block_callback,
enter_span_callback,
leave_span_callback,
text_callback,
nullptr,
nullptr
};

return md_parse(input, input_size, &parser, &render);
}

diff --git a/ source/html.hpp b/ source/html.hpp

@@ -5,11 +5,13 @@

namespace startgit
{

int md_html(const MD_CHAR* input,
MD_SIZE input_size,
void (*process_output)(const MD_CHAR*, MD_SIZE, void*),
void* userdata,
unsigned parser_flags,
unsigned renderer_flags);
int md_html(
const MD_CHAR* input,
MD_SIZE input_size,
void (*process_output)(const MD_CHAR*, MD_SIZE, void*),
void* userdata,
unsigned parser_flags,
unsigned renderer_flags
);

} // namespace startgit

diff --git a/ source/repository.cpp b/ source/repository.cpp

@@ -8,7 +8,8 @@ namespace startgit

repository::repository(const std::filesystem::path& path)
: m_path(path)
, m_repo(git2wrap::repository::open(
path.c_str(), GIT_REPOSITORY_OPEN_NO_SEARCH, nullptr))
path.c_str(), GIT_REPOSITORY_OPEN_NO_SEARCH, nullptr
))
, m_name(path.stem().string())
, m_url(read_file(path, "url"))
, m_owner(read_file(path, "owner"))

@@ -33,8 +34,9 @@ repository::repository(const std::filesystem::path& path)

m_repo.tag_foreach(callback, this);
}

std::string repository::read_file(const std::filesystem::path& base,
const char* file)
std::string repository::read_file(
const std::filesystem::path& base, const char* file
)
{
std::ifstream ifs(base / file);

diff --git a/ source/repository.hpp b/ source/repository.hpp

@@ -34,8 +34,9 @@ public:

const auto& get_tags() const { return m_tags; }

private:
static std::string read_file(const std::filesystem::path& base,
const char* file);
static std::string read_file(
const std::filesystem::path& base, const char* file
);

std::filesystem::path m_path;
git2wrap::repository m_repo;

diff --git a/ source/startgit-index.cpp b/ source/startgit-index.cpp

@@ -109,12 +109,14 @@ int main(int argc, char* argv[])

output_dir = std::filesystem::canonical(output_dir);

std::ofstream ofs(args.output_dir / "index.html");
write_header(ofs,
args.title,
args.description,
args.author,
"./",
/*has_feed=*/false);
write_header(
ofs,
args.title,
args.description,
args.author,
"./",
/*has_feed=*/false
);

ofs << h1(args.title);
ofs << p(args.description);

@@ -129,39 +131,41 @@ int main(int argc, char* argv[])

},
},
tbody {
transform(args.repos,
[](const auto& repo_path) -> element
{
using git2wrap::error_code_t::ENOTFOUND;
try {
const repository repo(repo_path);

for (const auto& branch : repo.get_branches()) {
if (branch.get_name() != "master") {
continue;
}

const auto url =
repo.get_name() + "/master/log.html";
return tr {
td {a {{{"href", url}}, repo.get_name()}},
td {repo.get_description()},
td {repo.get_owner()},
td {branch.get_commits()[0].get_time()},
};
}

std::cerr << std::format(
"Warning: {} doesn't have master branch\n",
repo.get_path().string());
} catch (const git2wrap::error<ENOTFOUND>& err) {
std::cerr << std::format(
"Warning: {} is not a repository\n",
repo_path.string());
}

return text();
}),
transform(
args.repos,
[](const auto& repo_path) -> element
{
using git2wrap::error_code_t::ENOTFOUND;
try {
const repository repo(repo_path);

for (const auto& branch : repo.get_branches()) {
if (branch.get_name() != "master") {
continue;
}

const auto url = repo.get_name() + "/master/log.html";
return tr {
td {a {{{"href", url}}, repo.get_name()}},
td {repo.get_description()},
td {repo.get_owner()},
td {branch.get_commits()[0].get_time()},
};
}

std::cerr << std::format(
"Warning: {} doesn't have master branch\n",
repo.get_path().string()
);
} catch (const git2wrap::error<ENOTFOUND>& err) {
std::cerr << std::format(
"Warning: {} is not a repository\n", repo_path.string()
);
}

return text();
}
),
},
};

diff --git a/ source/startgit.cpp b/ source/startgit.cpp

@@ -23,18 +23,21 @@ namespace

{

template<std::ranges::forward_range R>
void wtable(std::ostream& ost,
std::initializer_list<std::string_view> head,
const R& range,
based::Procedure<std::ranges::range_value_t<R>> auto proc)
void wtable(
std::ostream& ost,
std::initializer_list<std::string_view> head,
const R& range,
based::Procedure<std::ranges::range_value_t<R>> auto proc
)
{
using namespace hemplate::html; // NOLINT

ost << table {
thead {
tr {
transform(head,
[](const auto& elem) { return td {text {elem}}; }),
transform(
head, [](const auto& elem) { return td {text {elem}}; }
),
},
},
tbody {

@@ -48,10 +51,12 @@ void wtable(std::ostream& ost,

namespace startgit
{

void write_title(std::ostream& ost,
const repository& repo,
const branch& branch,
const std::string& relpath = "./")
void write_title(
std::ostream& ost,
const repository& repo,
const branch& branch,
const std::string& relpath = "./"
)
{
using namespace hemplate::html; // NOLINT

@@ -77,18 +82,20 @@ void write_title(std::ostream& ost,

a {{{"href", relpath + "refs.html"}}, "Refs"},
},
},
transform(branch.get_special(),
[&](const auto& file)
{
auto path = file.get_path();
const auto filename = path.replace_extension("html").string();
const auto name = path.replace_extension().string();

return transparent {
text {" | "},
a {{{"href", relpath + filename}}, name},
};
}),
transform(
branch.get_special(),
[&](const auto& file)
{
auto path = file.get_path();
const auto filename = path.replace_extension("html").string();
const auto name = path.replace_extension().string();

return transparent {
text {" | "},
a {{{"href", relpath + filename}}, name},
};
}
),
} << hr {};
}

@@ -96,73 +103,79 @@ void write_commit_table(std::ostream& ost, const branch& branch)

{
using namespace hemplate::html; // NOLINT

wtable(ost,
{"Date", "Commit message", "Author", "Files", "+", "-"},
branch.get_commits(),
[&](const auto& commit)
{
const auto idd = commit.get_id();
const auto url = std::format("./commit/{}.html", idd);

return tr {
td {commit.get_time()},
td {a {{{"href", url}}, commit.get_summary()}},
td {commit.get_author_name()},
td {commit.get_diff().get_files_changed()},
td {commit.get_diff().get_insertions()},
td {commit.get_diff().get_deletions()},
};
});
wtable(
ost,
{"Date", "Commit message", "Author", "Files", "+", "-"},
branch.get_commits(),
[&](const auto& commit)
{
const auto idd = commit.get_id();
const auto url = std::format("./commit/{}.html", idd);

return tr {
td {commit.get_time()},
td {a {{{"href", url}}, commit.get_summary()}},
td {commit.get_author_name()},
td {commit.get_diff().get_files_changed()},
td {commit.get_diff().get_insertions()},
td {commit.get_diff().get_deletions()},
};
}
);
}

void write_files_table(std::ostream& ost, const branch& branch)
{
using namespace hemplate::html; // NOLINT

wtable(ost,
{"Mode", "Name", "Size"},
branch.get_files(),
[&](const auto& file)
{
const auto path = file.get_path().string();
const auto url = std::format("./file/{}.html", path);
const auto size = file.is_binary()
? std::format("{}B", file.get_size())
: std::format("{}L", file.get_lines());

return tr {
td {file.get_filemode()},
td {a {{{"href", url}}, path}},
td {size},
};
});
wtable(
ost,
{"Mode", "Name", "Size"},
branch.get_files(),
[&](const auto& file)
{
const auto path = file.get_path().string();
const auto url = std::format("./file/{}.html", path);
const auto size = file.is_binary()
? std::format("{}B", file.get_size())
: std::format("{}L", file.get_lines());

return tr {
td {file.get_filemode()},
td {a {{{"href", url}}, path}},
td {size},
};
}
);
}

void write_branch_table(std::ostream& ost,
const repository& repo,
const std::string& branch_name)
void write_branch_table(
std::ostream& ost, const repository& repo, const std::string& branch_name
)
{
using namespace hemplate::html; // NOLINT

ost << h2("Branches");
wtable(ost,
{"&nbsp;", "Name", "Last commit date", "Author"},
repo.get_branches(),
[&](const auto& branch)
{
const auto& last = branch.get_last_commit();
const auto url = branch.get_name() != branch_name
? std::format("../{}/refs.html", branch.get_name())
: "";
const auto name = branch.get_name() == branch_name ? "*" : "&nbsp;";

return tr {
td {name},
td {a {{{"href", url}}, branch.get_name()}},
td {last.get_time()},
td {last.get_author_name()},
};
});
wtable(
ost,
{"&nbsp;", "Name", "Last commit date", "Author"},
repo.get_branches(),
[&](const auto& branch)
{
const auto& last = branch.get_last_commit();
const auto url = branch.get_name() != branch_name
? std::format("../{}/refs.html", branch.get_name())
: "";
const auto name = branch.get_name() == branch_name ? "*" : "&nbsp;";

return tr {
td {name},
td {a {{{"href", url}}, branch.get_name()}},
td {last.get_time()},
td {last.get_author_name()},
};
}
);
}

void write_tag_table(std::ostream& ost, const repository& repo)

@@ -170,18 +183,20 @@ void write_tag_table(std::ostream& ost, const repository& repo)

using namespace hemplate::html; // NOLINT

ost << h2("Tags");
wtable(ost,
{"&nbsp;", "Name", "Last commit date", "Author"},
repo.get_tags(),
[&](const auto& tag)
{
return tr {
td {"&nbsp;"},
td {tag.get_name()},
td {tag.get_time()},
td {tag.get_author()},
};
});
wtable(
ost,
{"&nbsp;", "Name", "Last commit date", "Author"},
repo.get_tags(),
[&](const auto& tag)
{
return tr {
td {"&nbsp;"},
td {tag.get_name()},
td {tag.get_time()},
td {tag.get_author()},
};
}
);
}

void write_file_changes(std::ostream& ost, const diff& diff)

@@ -189,47 +204,51 @@ void write_file_changes(std::ostream& ost, const diff& diff)

using namespace hemplate::html; // NOLINT

ost << b("Diffstat:");
wtable(ost,
{},
diff.get_deltas(),
[&](const auto& delta)
{
static const char* marker = " ADMRC T ";

const std::string link = std::format("#{}", delta->new_file.path);

uint32_t add = delta.get_adds();
uint32_t del = delta.get_dels();
const uint32_t changed = add + del;
const uint32_t total = 80;
if (changed > total) {
const double percent = 1.0 * total / changed;

if (add > 0) {
add = static_cast<uint32_t>(std::lround(percent * add) + 1);
}

if (del > 0) {
del = static_cast<uint32_t>(std::lround(percent * del) + 1);
}
}

return tr {
td {std::string(1, marker[delta->status])}, // NOLINT
td {a {{{"href", link}}, delta->new_file.path}},
td {"|"},
td {
span {{{"class", "add"}}, std::string(add, '+')},
span {{{"class", "del"}}, std::string(del, '-')},
},
};
});
wtable(
ost,
{},
diff.get_deltas(),
[&](const auto& delta)
{
static const char* marker = " ADMRC T ";

const std::string link = std::format("#{}", delta->new_file.path);

uint32_t add = delta.get_adds();
uint32_t del = delta.get_dels();
const uint32_t changed = add + del;
const uint32_t total = 80;
if (changed > total) {
const double percent = 1.0 * total / changed;

if (add > 0) {
add = static_cast<uint32_t>(std::lround(percent * add) + 1);
}

if (del > 0) {
del = static_cast<uint32_t>(std::lround(percent * del) + 1);
}
}

return tr {
td {std::string(1, marker[delta->status])}, // NOLINT
td {a {{{"href", link}}, delta->new_file.path}},
td {"|"},
td {
span {{{"class", "add"}}, std::string(add, '+')},
span {{{"class", "del"}}, std::string(del, '-')},
},
};
}
);

ost << p {
std::format("{} files changed, {} insertions(+), {} deletions(-)",
diff.get_files_changed(),
diff.get_insertions(),
diff.get_deletions()),
std::format(
"{} files changed, {} insertions(+), {} deletions(-)",
diff.get_files_changed(),
diff.get_insertions(),
diff.get_deletions()
),
};
}

@@ -262,46 +281,52 @@ void write_file_diffs(std::ostream& ost, const diff& diff)

return transparent {
h4 {
text {
std::format("@@ -{},{} +{},{} @@ ",
hunk->old_start,
hunk->old_lines,
hunk->new_start,
hunk->new_lines),
std::format(
"@@ -{},{} +{},{} @@ ",
hunk->old_start,
hunk->old_lines,
hunk->new_start,
hunk->new_lines
),
},
text {
xmlencode(header.substr(header.rfind('@') + 2)),
},
span {
{{"style", "white-space: pre"}},
transform(hunk.get_lines(),
[](const auto& line) -> element
{
using hemplate::html::div;

if (line.is_add()) {
return div {
{{"class", "add"}},
xmlencode(line.get_content()),
};
}

if (line.is_del()) {
return div {
{{"class", "del"}},
xmlencode(line.get_content()),
};
}

return text {
xmlencode(line.get_content()),
};
}),
transform(
hunk.get_lines(),
[](const auto& line) -> element
{
using hemplate::html::div;

if (line.is_add()) {
return div {
{{"class", "add"}},
xmlencode(line.get_content()),
};
}

if (line.is_del()) {
return div {
{{"class", "del"}},
xmlencode(line.get_content()),
};
}

return text {
xmlencode(line.get_content()),
};
}
),
},
},
};
}),
}
),
};
});
}
);
}

void write_commit_diff(std::ostream& ost, const commit& commit)

@@ -380,16 +405,20 @@ void write_file_content(std::ostream& ost, const file& file)

int count = 0;
ost << span {
{{"style", "white-space: pre;"}},
transform(lines,
[&](const auto& line)
{
return text {
std::format(R"(<a id="{0}" href="#{0}">{0:5}</a> {1}\n)",
count++,
xmlencode(line)),

};
}),
transform(
lines,
[&](const auto& line)
{
return text {
std::format(
R"(<a id="{0}" href="#{0}">{0:5}</a> {1}\n)",
count++,
xmlencode(line)
),

};
}
),
};
}

@@ -402,17 +431,21 @@ void write_html(std::ostream& ost, const file& file)

ofs << std::string(str, size);
};

md_html(file.get_content(),
static_cast<MD_SIZE>(file.get_size()),
process_output,
&ost,
MD_DIALECT_GITHUB,
0);
md_html(
file.get_content(),
static_cast<MD_SIZE>(file.get_size()),
process_output,
&ost,
MD_DIALECT_GITHUB,
0
);
}

void write_log(const std::filesystem::path& base,
const repository& repo,
const branch& branch)
void write_log(
const std::filesystem::path& base,
const repository& repo,
const branch& branch
)
{
std::ofstream ofs(base / "log.html");

@@ -422,9 +455,11 @@ void write_log(const std::filesystem::path& base,

write_footer(ofs);
}

void write_file(const std::filesystem::path& base,
const repository& repo,
const branch& branch)
void write_file(
const std::filesystem::path& base,
const repository& repo,
const branch& branch
)
{
std::ofstream ofs(base / "files.html");

@@ -434,9 +469,11 @@ void write_file(const std::filesystem::path& base,

write_footer(ofs);
}

void write_refs(const std::filesystem::path& base,
const repository& repo,
const branch& branch)
void write_refs(
const std::filesystem::path& base,
const repository& repo,
const branch& branch
)
{
std::ofstream ofs(base / "refs.html");

@@ -447,9 +484,11 @@ void write_refs(const std::filesystem::path& base,

write_footer(ofs);
}

bool write_commits(const std::filesystem::path& base,
const repository& repo,
const branch& branch)
bool write_commits(
const std::filesystem::path& base,
const repository& repo,
const branch& branch
)
{
bool changed = false;

@@ -470,9 +509,11 @@ bool write_commits(const std::filesystem::path& base,

return changed;
}

void write_files(const std::filesystem::path& base,
const repository& repo,
const branch& branch)
void write_files(
const std::filesystem::path& base,
const repository& repo,
const branch& branch
)
{
for (const auto& file : branch.get_files()) {
const std::filesystem::path path =

@@ -495,9 +536,11 @@ void write_files(const std::filesystem::path& base,

}
}

void write_readme_licence(const std::filesystem::path& base,
const repository& repo,
const branch& branch)
void write_readme_licence(
const std::filesystem::path& base,
const repository& repo,
const branch& branch
)
{
for (const auto& file : branch.get_special()) {
std::ofstream ofs(base / file.get_path().replace_extension("html"));

@@ -508,9 +551,9 @@ void write_readme_licence(const std::filesystem::path& base,

}
}

void write_atom(std::ostream& ost,
const branch& branch,
const std::string& base_url)
void write_atom(
std::ostream& ost, const branch& branch, const std::string& base_url
)
{
using namespace hemplate::atom; // NOLINT
using hemplate::atom::link;

@@ -534,30 +577,32 @@ void write_atom(std::ostream& ost,

author {name {args.author}},
link {self, " "},
link {alter, " "},
transform(branch.get_commits(),
[&](const auto& commit)
{
const auto url = std::format(
"{}/commit/{}.html", base_url, commit.get_id());

return entry {
id {url},
updated {format_time(commit.get_time_raw())},
title {commit.get_summary()},
link {{{"href", url}}, " "},
author {
name {commit.get_author_name()},
email {commit.get_author_email()},
},
content {commit.get_message()},
};
}),
transform(
branch.get_commits(),
[&](const auto& commit)
{
const auto url =
std::format("{}/commit/{}.html", base_url, commit.get_id());

return entry {
id {url},
updated {format_time(commit.get_time_raw())},
title {commit.get_summary()},
link {{{"href", url}}, " "},
author {
name {commit.get_author_name()},
email {commit.get_author_email()},
},
content {commit.get_message()},
};
}
),
};
}

void write_rss(std::ostream& ost,
const branch& branch,
const std::string& base_url)
void write_rss(
std::ostream& ost, const branch& branch, const std::string& base_url
)
{
using namespace hemplate::rss; // NOLINT
using hemplate::rss::link;

@@ -572,22 +617,26 @@ void write_rss(std::ostream& ost,

generator {"startgit"},
language {"en-us"},
atomLink {{{"href", base_url + "/atom.xml"}}},
transform(branch.get_commits(),
[&](const auto& commit)
{
const auto url = std::format(
"{}/commit/{}.html", base_url, commit.get_id());

return item {
title {commit.get_summary()},
link {url},
guid {url},
pubDate {format_time(commit.get_time_raw())},
author {std::format("{} ({})",
commit.get_author_email(),
commit.get_author_name())},
};
}),
transform(
branch.get_commits(),
[&](const auto& commit)
{
const auto url =
std::format("{}/commit/{}.html", base_url, commit.get_id());

return item {
title {commit.get_summary()},
link {url},
guid {url},
pubDate {format_time(commit.get_time_raw())},
author {std::format(
"{} ({})",
commit.get_author_email(),
commit.get_author_name()
)},
};
}
),
},
};
}

@@ -748,8 +797,9 @@ int main(int argc, char* argv[])

write_rss(rss, branch, absolute);
}
} catch (const git2wrap::error<git2wrap::error_code_t::ENOTFOUND>& err) {
std::cerr << std::format("Warning: {} is not a repository\n",
args.repos[0].string());
std::cerr << std::format(
"Warning: {} is not a repository\n", args.repos[0].string()
);
} catch (const git2wrap::runtime_error& err) {
std::cerr << std::format("Error (git2wrap): {}\n", err.what());
} catch (const std::runtime_error& err) {

diff --git a/ source/utils.cpp b/ source/utils.cpp

@@ -12,7 +12,8 @@ auto sec_since_epoch(std::int64_t sec)

{
return std::chrono::time_point_cast<std::chrono::seconds>(
std::chrono::system_clock::from_time_t(time_t {0})
+ std::chrono::seconds(sec));
+ std::chrono::seconds(sec)
);
}

std::string time_short(std::int64_t date)

@@ -22,11 +23,13 @@ std::string time_short(std::int64_t date)


std::string time_long(const git2wrap::time& time)
{
return std::format("{:%a, %e %b %Y %H:%M:%S} {}{:02}{:02}",
sec_since_epoch(time.time),
time.offset < 0 ? '-' : '+',
time.offset / 60, // NOLINT
time.offset % 60); // NOLINT
return std::format(
"{:%a, %e %b %Y %H:%M:%S} {}{:02}{:02}",
sec_since_epoch(time.time),
time.offset < 0 ? '-' : '+',
time.offset / 60, // NOLINT
time.offset % 60 // NOLINT
);
}
// NOLINTBEGIN
// clang-format off