/* This file is part of FediBlock-backend. * Copyright © 2020 tastytea * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include "git.hpp" #include "config.hpp" #include "files.hpp" #include "fs-compat.hpp" #include "gitea.hpp" #include "json.hpp" #include #include // For compatibility with fmt 4. #include #include #include #include #include #include #include #include #if LIBGIT2_VER_MAJOR == 0 # if LIBGIT2_VER_MINOR < 28 # define git_error_last giterr_last # endif # if LIBGIT2_VER_MINOR < 99 # define git_credential_ssh_key_new git_cred_ssh_key_new # define git_push_options_init git_push_init_options # define git_fetch_options_init git_fetch_init_options # define git_checkout_options_init git_checkout_init_options # endif #endif namespace FediBlock::git { using fmt::format; using std::ofstream; using std::runtime_error; using std::string; using std::string_view; using std::to_string; using std::uint8_t; using std::this_thread::sleep_for; using namespace std::chrono_literals; git_repository *_repo{nullptr}; const string _clone_url{format("git@{:s}:{:s}/{:s}.git", config::forge_domain, config::forge_org, config::forge_repo_data)}; fs::path _repo_dir{}; void init(const bool cache) { if (cache) { _repo_dir = files::get_cachedir() / "repo"; } else { _repo_dir = files::get_tmpdir() / "repo"; } git_libgit2_init(); } void cleanup(const bool cache) { git_libgit2_shutdown(); if (cache) { files::remove_lockfile(); } else { files::remove_tmpdir(); } } void check(int error) { if (error != 0) { const git_error *e = git_error_last(); throw runtime_error{e->message}; } } int cred_acquire(git_cred **cred, const char * /*url*/, const char *username_from_url, unsigned int /*allowed_types*/, void * /*payload*/) { const auto datadir{files::get_datadir()}; return git_credential_ssh_key_new(cred, username_from_url, (datadir / "ssh_id.pub").c_str(), (datadir / "ssh_id").c_str(), nullptr); } void clone() { git_clone_options options = GIT_CLONE_OPTIONS_INIT; options.fetch_opts.callbacks.credentials = cred_acquire; check(git_clone(&_repo, _clone_url.data(), _repo_dir.c_str(), &options)); } void create_branch() { const string branch_name{get_branch_name()}; const string ref_name{"refs/heads/" + branch_name}; git_oid oid_parent; git_commit *commit{nullptr}; // Get SHA1 of HEAD. check(git_reference_name_to_id(&oid_parent, _repo, "HEAD")); // Translate SHA-1 to git_commit. check(git_commit_lookup(&commit, _repo, &oid_parent)); git_reference *branch{nullptr}; // Create new branch. check(git_branch_create(&branch, _repo, branch_name.c_str(), commit, 0)); check(git_repository_set_head(_repo, ref_name.c_str())); } void commit(const entry_type &entry) { // Write files. const string basename{_repo_dir / get_branch_name()}; ofstream file(basename + ".json"); if (!file.good()) { throw runtime_error{ format("Could not create file: {:s}.json", basename)}; } file << json::to_json(entry); file.close(); if (!entry.screenshot_filepaths.empty()) { uint8_t counter{0}; for (const auto &screenshot : entry.screenshot_filepaths) { ++counter; const string extension{fs::path(screenshot).extension()}; fs::copy(screenshot, basename + "-" += to_string(counter) += extension); } } // Add files. git_index *index{nullptr}; check(git_repository_index(&index, _repo)); check(git_index_add_all(index, nullptr, 0, nullptr, nullptr)); check(git_index_write(index)); // Create commit. git_signature *sig{nullptr}; git_oid oid_commit; git_oid oid_tree; git_oid oid_parent; git_tree *tree{nullptr}; git_object *parent{nullptr}; git_reference *ref{nullptr}; check(git_signature_now(&sig, "Web", "Don't @ me")); check(git_revparse_ext(&parent, &ref, _repo, "HEAD")); check(git_repository_index(&index, _repo)); check(git_index_write_tree(&oid_tree, index)); check(git_index_write(index)); check(git_tree_lookup(&tree, _repo, &oid_tree)); check(git_reference_name_to_id(&oid_parent, _repo, "HEAD")); // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) check(git_commit_create_v(&oid_commit, _repo, "HEAD", sig, sig, nullptr, ("New entry: " + entry.instance).c_str(), tree, parent != nullptr ? 1 : 0, parent)); git_index_free(index); git_signature_free(sig); git_tree_free(tree); git_object_free(parent); git_reference_free(ref); } void push() { git_push_options options; git_remote *remote{nullptr}; string refspec_str{("refs/heads/" + get_branch_name())}; char *refspec = &refspec_str[0]; const git_strarray refspecs = {&refspec, 1}; check(git_remote_lookup(&remote, _repo, "origin")); check(git_push_options_init(&options, GIT_PUSH_OPTIONS_VERSION)); options.callbacks.credentials = cred_acquire; check(git_remote_push(remote, &refspecs, &options)); git_remote_free(remote); } string get_branch_name() { const auto id{gitea::get_last_pr_number() + 1}; return "web-" + to_string(id); } void update_cached_repo(const uint8_t timeout) { bool lock_created{false}; for (uint8_t counter{0}; counter < timeout; ++counter) { if ((lock_created = files::create_lockfile())) { break; } sleep_for(1s); } if (!lock_created) { throw runtime_error{"Repository was locked for too long, giving up."}; } if (!fs::exists(_repo_dir / ".git")) { clone(); return; } git_remote *remote{nullptr}; check(git_repository_open(&_repo, _repo_dir.c_str())); check(git_remote_lookup(&remote, _repo, "origin")); git_fetch_options fetch_opts; check(git_fetch_options_init(&fetch_opts, GIT_FETCH_OPTIONS_VERSION)); fetch_opts.callbacks.credentials = cred_acquire; check(git_remote_fetch(remote, nullptr, &fetch_opts, nullptr)); // FIXME: HEAD is detached. git_checkout_options checkout_opts; check(git_checkout_options_init(&checkout_opts, GIT_CHECKOUT_OPTIONS_VERSION)); checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; check(git_repository_set_head(_repo, "refs/remotes/origin/main")); check(git_checkout_head(_repo, &checkout_opts)); git_remote_free(remote); } } // namespace FediBlock::git