From ec33ed77d5aeec87b1883af2c2895592177b170a Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Sun, 14 Jun 2015 00:23:57 +0100 Subject: [PATCH] Refactored git code. The masterlist-specific code in has been moved into masterlist.cpp, and the more general git-related code has been moved into a GitHelper class. Although it's a helper, there are a few loot::error exceptions thrown in its code, as it provides an error code that distinguishes git related errors from others, so is useful. There is still a lot of git code in masterlist.cpp. Not all of this will be moved, but there may be some bits that can be. --- CMakeLists.txt | 3 +- src/backend/git.cpp | 592 ----------------------------- src/backend/helpers/git_helper.cpp | 185 +++++++++ src/backend/helpers/git_helper.h | 76 ++++ src/backend/masterlist.cpp | 378 ++++++++++++++++++ 5 files changed, 641 insertions(+), 593 deletions(-) delete mode 100644 src/backend/git.cpp create mode 100644 src/backend/helpers/git_helper.cpp create mode 100644 src/backend/helpers/git_helper.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b498518..8d2ea8a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,11 +93,11 @@ set (LOOT_SRC "${CMAKE_SOURCE_DIR}/src/backend/metadata/conditional_metadata.c "${CMAKE_SOURCE_DIR}/src/backend/masterlist.cpp" "${CMAKE_SOURCE_DIR}/src/backend/plugin/plugin.cpp" "${CMAKE_SOURCE_DIR}/src/backend/plugin/plugin_loader.cpp" + "${CMAKE_SOURCE_DIR}/src/backend/helpers/git_helper.cpp" "${CMAKE_SOURCE_DIR}/src/backend/helpers/helpers.cpp" "${CMAKE_SOURCE_DIR}/src/backend/helpers/language.cpp" "${CMAKE_SOURCE_DIR}/src/backend/helpers/version.cpp" "${CMAKE_SOURCE_DIR}/src/backend/graph.cpp" - "${CMAKE_SOURCE_DIR}/src/backend/git.cpp" "${CMAKE_BINARY_DIR}/generated/globals.cpp") set (LOOT_HEADERS "${CMAKE_SOURCE_DIR}/src/backend/metadata/condition_grammar.h" @@ -117,6 +117,7 @@ set (LOOT_HEADERS "${CMAKE_SOURCE_DIR}/src/backend/metadata/condition_grammar.h" "${CMAKE_SOURCE_DIR}/src/backend/masterlist.h" "${CMAKE_SOURCE_DIR}/src/backend/plugin/plugin.h" "${CMAKE_SOURCE_DIR}/src/backend/plugin/plugin_loader.h" + "${CMAKE_SOURCE_DIR}/src/backend/helpers/git_helper.h" "${CMAKE_SOURCE_DIR}/src/backend/helpers/helpers.h" "${CMAKE_SOURCE_DIR}/src/backend/helpers/language.h" "${CMAKE_SOURCE_DIR}/src/backend/helpers/streams.h" diff --git a/src/backend/git.cpp b/src/backend/git.cpp deleted file mode 100644 index 5baa0294..00000000 --- a/src/backend/git.cpp +++ /dev/null @@ -1,592 +0,0 @@ -/* LOOT - - A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and - Fallout: New Vegas. - - Copyright (C) 2012-2015 WrinklyNinja - - This file is part of LOOT. - - LOOT is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public License - as published by the Free Software Foundation, either version 3 of - the License, or (at your option) any later version. - - LOOT 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with LOOT. If not, see - . - */ - -#include "error.h" -#include "helpers/streams.h" -#include "helpers/helpers.h" -#include "game/game.h" - -#include -#include -#include - -#include - -using namespace std; - -namespace fs = boost::filesystem; -namespace lc = boost::locale; - -namespace loot { - // Removes the read-only flag from some files in git repositories created by libgit2. - void FixRepoPermissions(const fs::path& path) { - BOOST_LOG_TRIVIAL(trace) << "Recursively setting write permission on directory: " << path; - for (fs::recursive_directory_iterator it(path); it != fs::recursive_directory_iterator(); ++it) { - BOOST_LOG_TRIVIAL(trace) << "Setting write permission for: " << it->path(); - fs::permissions(it->path(), fs::add_perms | fs::owner_write | fs::group_write | fs::others_write); - } - } - - struct git_handler { - public: - git_handler() : - repo(nullptr), - remote(nullptr), - cfg(nullptr), - obj(nullptr), - commit(nullptr), - ref(nullptr), - ref2(nullptr), - sig(nullptr), - blob(nullptr), - annotated_commit(nullptr), - tree(nullptr), - diff(nullptr), - buf({0}) { - // Init threading system and OpenSSL (for Linux builds). - git_libgit2_init(); - } - - ~git_handler() { - string path; - if (repo != nullptr) - path = git_repository_path(repo); - - free(); - - if (!path.empty()) { - try { - FixRepoPermissions(path); - } - catch (exception&) {} - } - - git_libgit2_shutdown(); - } - - void free() { - git_commit_free(commit); - git_object_free(obj); - git_config_free(cfg); - git_remote_free(remote); - git_repository_free(repo); - git_reference_free(ref); - git_reference_free(ref2); - git_signature_free(sig); - git_blob_free(blob); - git_annotated_commit_free(annotated_commit); - git_tree_free(tree); - git_diff_free(diff); - git_buf_free(&buf); - - commit = nullptr; - obj = nullptr; - cfg = nullptr; - remote = nullptr; - repo = nullptr; - ref = nullptr; - ref2 = nullptr; - sig = nullptr; - blob = nullptr; - annotated_commit = nullptr; - tree = nullptr; - diff = nullptr; - buf = {0}; - } - - void call(int error_code) { - if (!error_code) - return; - - const git_error * last_error = giterr_last(); - std::string error_message; - if (last_error == nullptr) - error_message = to_string(error_code) + "."; - else - error_message = to_string(error_code) + "; " + last_error->message; - giterr_clear(); - - if (ui_message.empty()) - ui_message = (boost::format(lc::translate("Git operation failed. Error: %1%")) % error_message).str(); - - BOOST_LOG_TRIVIAL(error) << "Git operation failed. Error: " << error_message; - throw loot::error(loot::error::git_error, ui_message); - } - - git_repository * repo; - git_remote * remote; - git_config * cfg; - git_object * obj; - git_commit * commit; - git_reference * ref; - git_reference * ref2; - git_signature * sig; - git_blob * blob; - git_annotated_commit * annotated_commit; - git_tree * tree; - git_diff * diff; - git_buf buf; - - std::string ui_message; - }; - - struct git_diff_payload { - public: - bool fileFound; - const char * fileToFind; - }; - - bool isRepository(const fs::path& path) { - return git_repository_open_ext(NULL, path.string().c_str(), GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) == 0; - } - - int diffFileCallback(const git_diff_delta *delta, float progress, void * payload) { - BOOST_LOG_TRIVIAL(trace) << "Checking diff for: " << delta->old_file.path; - git_diff_payload * gdp = (git_diff_payload*)payload; - if (strcmp(delta->old_file.path, gdp->fileToFind) == 0) { - BOOST_LOG_TRIVIAL(warning) << "Edited masterlist found."; - gdp->fileFound = true; - } - - return 0; - } - - bool IsMasterlistDifferent(git_handler& git, const std::string& filename) { - if (git.obj) { - BOOST_LOG_TRIVIAL(error) << "Object memory already allocated!"; - throw error(error::git_error, lc::translate("Object memory already allocated!")); - } - else if (git.tree) { - BOOST_LOG_TRIVIAL(error) << "Tree memory already allocated!"; - throw error(error::git_error, lc::translate("Tree memory already allocated!")); - } - else if (git.diff) { - BOOST_LOG_TRIVIAL(error) << "Diff memory already allocated!"; - throw error(error::git_error, lc::translate("Diff memory already allocated!")); - } - else if (!git.repo) { - BOOST_LOG_TRIVIAL(error) << "Repository handle not open!"; - throw error(error::git_error, lc::translate("Repository handle not open!")); - } - - // Perform a git diff, then iterate the deltas to see if one exists for the masterlist. - BOOST_LOG_TRIVIAL(trace) << "Getting the tree for the HEAD revision."; - git.call(git_revparse_single(&git.obj, git.repo, "HEAD^{tree}")); - git.call(git_tree_lookup(&git.tree, git.repo, git_object_id(git.obj))); - - BOOST_LOG_TRIVIAL(trace) << "Performing git diff."; - git.call(git_diff_tree_to_workdir_with_index(&git.diff, git.repo, git.tree, NULL)); - - BOOST_LOG_TRIVIAL(trace) << "Iterating over git diff deltas."; - git_diff_payload payload; - payload.fileFound = false; - payload.fileToFind = filename.c_str(); - git.call(git_diff_foreach(git.diff, &diffFileCallback, NULL, NULL, &payload)); - - // Clean up memory - git_object_free(git.obj); - git.obj = nullptr; - git_tree_free(git.tree); - git.tree = nullptr; - git_diff_free(git.diff); - git.diff = nullptr; - - return payload.fileFound; - } - - void Masterlist::GetGitInfo(const boost::filesystem::path& path, bool shortID) { - // Compare HEAD and working copy, and get revision info. - git_handler git; - git.ui_message = (boost::format(lc::translate("An error occurred while trying to read the local masterlist's version. If this error happens again, try deleting the \".git\" folder in %1%.")) % path.parent_path().string()).str(); - - if (!fs::exists(path)) { - BOOST_LOG_TRIVIAL(info) << "Unknown masterlist revision: No masterlist present."; - throw error(error::ok, lc::translate("N/A: No masterlist present")); - } - else if (!isRepository(path.parent_path())) { - BOOST_LOG_TRIVIAL(info) << "Unknown masterlist revision: Git repository missing."; - throw error(error::ok, lc::translate("Unknown: Git repository missing")); - } - - BOOST_LOG_TRIVIAL(debug) << "Existing repository found, attempting to open it."; - git.call(git_repository_open(&git.repo, path.parent_path().string().c_str())); - - //Need to get the HEAD object, because the individual file has a different SHA. - BOOST_LOG_TRIVIAL(info) << "Getting the Git object for the tree at HEAD."; - git.call(git_revparse_single(&git.obj, git.repo, "HEAD")); - - BOOST_LOG_TRIVIAL(trace) << "Generating hex string for Git object ID."; - if (shortID) { - git.call(git_object_short_id(&git.buf, git.obj)); - revision = git.buf.ptr; - } - else { - char c_rev[GIT_OID_HEXSZ + 1]; - revision = git_oid_tostr(c_rev, GIT_OID_HEXSZ + 1, git_object_id(git.obj)); - } - - BOOST_LOG_TRIVIAL(trace) << "Getting date for Git object."; - const git_oid * oid = git_object_id(git.obj); - git.call(git_commit_lookup(&git.commit, git.repo, oid)); - git_time_t time = git_commit_time(git.commit); - boost::locale::date_time dateTime(time); - stringstream out; - out << boost::locale::as::ftime("%Y-%m-%d") << dateTime; - date = out.str(); - - // Free object memory. - git_object_free(git.obj); - git.obj = nullptr; - - BOOST_LOG_TRIVIAL(trace) << "Diffing masterlist HEAD and working copy."; - if (IsMasterlistDifferent(git, path.filename().string())) { - revision += string(" ") + lc::translate("(edited)").str(); - date += string(" ") + lc::translate("(edited)").str(); - } - } - - bool Masterlist::Update(const Game& game) { - return Update(game.MasterlistPath(), game.RepoURL(), game.RepoBranch()); - } - - bool Masterlist::Update(const boost::filesystem::path& path, const std::string& repoURL, const std::string& repoBranch) { - git_handler git; - fs::path repo_path = path.parent_path(); - string filename = path.filename().string(); - - // First initialise some stuff that isn't specific to a repository. - BOOST_LOG_TRIVIAL(debug) << "Creating a reflog signature to use."; - // Create a reflog signature for any changes. - git.call(git_signature_new(&git.sig, "LOOT", "loot@placeholder.net", 0, 0)); - - BOOST_LOG_TRIVIAL(debug) << "Setting up checkout options."; - char * paths = new char[filename.length() + 1]; - strcpy(paths, filename.c_str()); - git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; - checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE | GIT_CHECKOUT_DONT_REMOVE_EXISTING; - checkout_opts.paths.strings = &paths; - checkout_opts.paths.count = 1; - - // Now try to access the repository if it exists, or clone one if it doesn't. - BOOST_LOG_TRIVIAL(trace) << "Attempting to open the Git repository at: " << repo_path; - if (!isRepository(repo_path)) { - git.ui_message = lc::translate("An error occurred while trying to clone the remote masterlist repository."); - // Clone the remote repository. - BOOST_LOG_TRIVIAL(info) << "Repository doesn't exist, cloning the remote repository."; - - fs::path temp_path = repo_path.string() + ".temp"; - if (!fs::is_empty(repo_path)) { - // Clear any read-only flags first. - FixRepoPermissions(repo_path); - // Now, libgit2 doesn't support cloning into non-empty folders. Rename the folder - - // temporarily, and move its contents back in afterwards, skipping any that then conflict. - BOOST_LOG_TRIVIAL(trace) << "Repo path not empty, renaming folder."; - // If the temp path already exists, it needs to be deleted. - if (fs::exists(temp_path)) { - FixRepoPermissions(temp_path); - fs::remove_all(temp_path); - } - // There's no point moving the .git folder, so delete that. - fs::remove_all(repo_path / ".git"); - // Now move to temp path. - fs::rename(repo_path, temp_path); - // Recreate the game folder so that we don't inadvertently cause any other errors (everything past LOOT init assumes it exists). - fs::create_directory(repo_path); - } - - //First set up clone options. - - git_clone_options clone_options = GIT_CLONE_OPTIONS_INIT; - clone_options.checkout_opts = checkout_opts; - clone_options.bare = 0; - clone_options.checkout_branch = repoBranch.c_str(); - clone_options.signature = git.sig; - - //Now perform the clone. - git.call(git_clone(&git.repo, repoURL.c_str(), repo_path.string().c_str(), &clone_options)); - - if (fs::exists(temp_path)) { - //Move contents back in. - BOOST_LOG_TRIVIAL(trace) << "Repo path wasn't empty, moving previous files back in."; - for (fs::directory_iterator it(temp_path); it != fs::directory_iterator(); ++it) { - if (!fs::exists(repo_path / it->path().filename())) { - //No conflict, OK to move back in. - fs::rename(it->path(), repo_path / it->path().filename()); - } - } - //Delete temporary folder. - FixRepoPermissions(temp_path); - fs::remove_all(temp_path); - } - } - else { - // Repository exists: check settings are correct, then pull updates. - git.ui_message = (boost::format(lc::translate("An error occurred while trying to access the local masterlist repository. If this error happens again, try deleting the \".git\" folder in %1%.")) % repo_path.string()).str(); - - // Open the repository. - BOOST_LOG_TRIVIAL(info) << "Existing repository found, attempting to open it."; - git.call(git_repository_open(&git.repo, repo_path.string().c_str())); - - // Check that the repository's remote settings match LOOT's. - BOOST_LOG_TRIVIAL(info) << "Checking to see if remote URL matches URL in settings."; - git.call(git_remote_lookup(&git.remote, git.repo, "origin")); - const char * url = git_remote_url(git.remote); - - BOOST_LOG_TRIVIAL(info) << "Remote URL given: " << repoURL; - BOOST_LOG_TRIVIAL(info) << "Remote URL in repository settings: " << url; - if (url != repoURL) { - BOOST_LOG_TRIVIAL(info) << "URLs do not match, setting repository URL to URL in settings."; - // The URLs don't match. Change the remote URL to match the one LOOT has. - git.call(git_remote_set_url(git.remote, repoURL.c_str())); - - // Now save change. - git.call(git_remote_save(git.remote)); - } - - // Now fetch updates from the remote. - BOOST_LOG_TRIVIAL(trace) << "Fetching updates from remote."; - git.ui_message = lc::translate("An error occurred while trying to update the masterlist. This could be due to a server-side error. Try again in a few minutes."); - - git.call(git_remote_fetch(git.remote, nullptr, git.sig, nullptr)); - - // Print some stats on what was fetched either during update or clone. - const git_transfer_progress * stats = git_remote_stats(git.remote); - BOOST_LOG_TRIVIAL(info) << "Received " << stats->indexed_objects << " of " << stats->total_objects << " objects in " << stats->received_bytes << " bytes."; - - // Check that a branch with the correct name exists. - git.ui_message = (boost::format(lc::translate("An error occurred while trying to access the local masterlist repository. If this error happens again, try deleting the \".git\" folder in %1%.")) % repo_path.string()).str(); - int ret = git_branch_lookup(&git.ref, git.repo, repoBranch.c_str(), GIT_BRANCH_LOCAL); - if (ret == GIT_ENOTFOUND) { - // Branch doesn't exist. Create a new branch using the remote branch's latest commit. - - BOOST_LOG_TRIVIAL(trace) << "Looking up commit referred to by the remote branch \"" << repoBranch << "\"."; - git.call(git_revparse_single(&git.obj, git.repo, (string("origin/") + repoBranch).c_str())); - const git_oid * commit_id = git_object_id(git.obj); - - BOOST_LOG_TRIVIAL(trace) << "Creating the new branch."; - // Create a branch. - git.call(git_commit_lookup(&git.commit, git.repo, commit_id)); - git.call(git_branch_create(&git.ref, git.repo, repoBranch.c_str(), git.commit, 0, git.sig, NULL)); - - // Set upstream. Don't really know if this is necessary or not. - git.call(git_branch_set_upstream(git.ref, (string("origin/") + repoBranch).c_str())); - - BOOST_LOG_TRIVIAL(trace) << "Setting the upstream for the new branch."; - // Free tree and commit pointers. Reference pointer is still used below. - git_object_free(git.obj); - git_commit_free(git.commit); - git.commit = nullptr; - git.obj = nullptr; - - BOOST_LOG_TRIVIAL(trace) << "Done creating the new branch."; - } - else if (ret != 0) - git.call(ret); // Handle other errors. - - // Check if HEAD points to the desired branch and set it to if not. - if (!git_branch_is_head(git.ref)) { - BOOST_LOG_TRIVIAL(trace) << "Setting HEAD to follow branch: " << repoBranch; - git.call(git_repository_set_head(git.repo, (string("refs/heads/") + repoBranch).c_str(), git.sig, (string("Updated HEAD to ") + repoBranch).c_str())); - } - - if (ret == 0) { - /* The branch did exist, and is now pointed at by HEAD. - Need to merge the remote branch into it. Just do a fast-forward merge because - that's all that should be necessary as the local repo shouldn't get changed by - the user. - */ - - BOOST_LOG_TRIVIAL(trace) << "Checking that local and remote branches can be merged by fast-forward."; - git_merge_analysis_t analysis; - git_merge_preference_t pref; - git.call(git_reference_lookup(&git.ref2, git.repo, (string("refs/remotes/origin/") + repoBranch).c_str())); - git.call(git_annotated_commit_from_ref(&git.annotated_commit, git.repo, git.ref2)); - git.call(git_merge_analysis(&analysis, &pref, git.repo, (const git_annotated_commit **)&git.annotated_commit, 1)); - - if ((analysis & GIT_MERGE_ANALYSIS_FASTFORWARD) != 0) { - BOOST_LOG_TRIVIAL(trace) << "Local branch can be fast-forwarded to remote branch."; - // The remote branch reference points to a particular commit. We just want to - // update the local branch reference to point to the same commit. - - // Get the commit object ID. - git.call(git_reference_peel(&git.obj, git.ref2, GIT_OBJ_COMMIT)); - const git_oid * commit_id = git_object_id(git.obj); - git_reference_free(git.ref2); - git.ref2 = nullptr; - git_object_free(git.obj); - git.obj = nullptr; - - // Set the reference target. - git.call(git_reference_set_target(&git.ref2, git.ref, commit_id, git.sig, "Setting branch reference.")); - - git_reference_free(git.ref2); - git.ref2 = nullptr; - } - else if ((analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) != 0) { - // No merge is required, but HEAD might be ahead of the remote branch. Check - // to see if that's the case, and move HEAD back to match the remote branch - // if so. - BOOST_LOG_TRIVIAL(trace) << "Local branch is up-to-date with remote branch."; - - BOOST_LOG_TRIVIAL(trace) << "Checking to see if local and remote branch heads are equal."; - // Get the local branch and remote branch head commit IDs. - - // Local branch. - git.call(git_reference_peel(&git.obj, git.ref, GIT_OBJ_COMMIT)); - const git_oid * local_commit_id = git_object_id(git.obj); - git_object_free(git.obj); - git.obj = nullptr; - - // Remote branch. - git.call(git_reference_peel(&git.obj, git.ref2, GIT_OBJ_COMMIT)); - const git_oid * remote_commit_id = git_object_id(git.obj); - git_reference_free(git.ref2); - git.ref2 = nullptr; - git_object_free(git.obj); - git.obj = nullptr; - - if (local_commit_id->id != remote_commit_id->id) { - BOOST_LOG_TRIVIAL(trace) << "Branch heads are not equal, updating local HEAD."; - // Commit IDs don't match, update HEAD, and continue with normal update procedure. - git.call(git_reference_set_target(&git.ref2, git.ref, remote_commit_id, git.sig, "Setting branch reference.")); - git_reference_free(git.ref2); - git.ref2 = nullptr; - } - else { - // HEAD matches the remote branch. If the masterlist in - // HEAD also matches the masterlist file, no further - // action needs to be taken. Otherwise, a checkout - // must be performed and the checked-out file parsed. - BOOST_LOG_TRIVIAL(trace) << "Branch heads are equal."; - - BOOST_LOG_TRIVIAL(trace) << "Diffing HEAD and filesystem masterlists."; - if (!IsMasterlistDifferent(git, filename)) { - return false; - } - } - } - else { - // The local repository can't be easily merged. It's best just to delete and re-clone it. - FixRepoPermissions(repo_path / ".git"); - git.free(); - fs::remove_all(repo_path / ".git"); - return this->Update(path, repoURL, repoBranch); - } - } - - // Free branch pointer. - git_reference_free(git.ref); - git.ref = nullptr; - - BOOST_LOG_TRIVIAL(trace) << "Performing a Git checkout of HEAD."; - git.call(git_checkout_head(git.repo, &checkout_opts)); - } - - // Now whether the repository was cloned or updated, the working directory contains - // the latest masterlist. Try parsing it: on failure, detach the HEAD back one commit - // and try again. - - bool parsingFailed = false; - std::string parsingError; - git.ui_message = (boost::format(lc::translate("An error occurred while trying to read information on the updated masterlist. If this error happens again, try deleting the \".git\" folder in %1%.")) % repo_path.string()).str(); - do { - // Get some descriptive info about what was checked out. - - BOOST_LOG_TRIVIAL(trace) << "Getting the Git object for HEAD."; - git.call(git_repository_head(&git.ref, git.repo)); - git.call(git_reference_peel(&git.obj, git.ref, GIT_OBJ_COMMIT)); - - BOOST_LOG_TRIVIAL(trace) << "Generating hex string for Git object ID."; - git.call(git_object_short_id(&git.buf, git.obj)); - revision = git.buf.ptr; - - BOOST_LOG_TRIVIAL(trace) << "Getting date for Git object."; - const git_oid * oid = git_object_id(git.obj); - git.call(git_commit_lookup(&git.commit, git.repo, oid)); - git_time_t time = git_commit_time(git.commit); - // Now convert into a nice text format. - boost::locale::date_time dateTime(time); - stringstream out; - out << boost::locale::as::ftime("%Y-%m-%d") << dateTime; - date = out.str(); - - BOOST_LOG_TRIVIAL(debug) << "Set HEAD to commit (date): " << revision << " (" << date << ")."; - BOOST_LOG_TRIVIAL(trace) << "Freeing pointers."; - git_reference_free(git.ref); - git_object_free(git.obj); - git_buf_free(&git.buf); - git_commit_free(git.commit); - git.ref = nullptr; - git.obj = nullptr; - git.commit = nullptr; - git.buf = {0}; - - //Now try parsing the masterlist. - BOOST_LOG_TRIVIAL(debug) << "Testing masterlist parsing."; - try { - this->MetadataList::Load(path); - - for (auto &plugin : plugins) { - plugin.ParseAllConditions(); - } - for (auto &plugin : regexPlugins) { - plugin.ParseAllConditions(); - } - for (auto &message : messages) { - message.ParseCondition(); - } - - parsingFailed = false; - } - catch (std::exception& e) { - parsingFailed = true; - - //Roll back one revision if there's an error. - BOOST_LOG_TRIVIAL(error) << "Masterlist parsing failed. Masterlist revision " + string(revision) + ": " + e.what(); - - // Get an object ID for 'HEAD~1'. - git.call(git_revparse_single(&git.obj, git.repo, "HEAD~1")); - const git_oid * oid = git_object_id(git.obj); - git_object_free(git.obj); - git.obj = nullptr; - - // Detach HEAD to HEAD~1. This will roll back HEAD by one commit each time it is called. - git.call(git_repository_set_head_detached(git.repo, oid, git.sig, "Updated HEAD to HEAD~1")); - - // Checkout the new HEAD. - BOOST_LOG_TRIVIAL(trace) << "Performing a Git checkout of HEAD."; - git.call(git_checkout_head(git.repo, &checkout_opts)); - - if (parsingError.empty()) - parsingError = boost::locale::translate("Masterlist revision").str() + " " + string(revision) + ": " + e.what() + ". " + boost::locale::translate("Rolled back to the previous revision.").str(); - } - } while (parsingFailed); - - if (!parsingError.empty()) - throw error(error::ok, parsingError); //Throw an OK because the process still completed in a successful state. - - return true; - } -} diff --git a/src/backend/helpers/git_helper.cpp b/src/backend/helpers/git_helper.cpp new file mode 100644 index 00000000..0f9eeb7f --- /dev/null +++ b/src/backend/helpers/git_helper.cpp @@ -0,0 +1,185 @@ +/* LOOT + + A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and + Fallout: New Vegas. + + Copyright (C) 2012-2015 WrinklyNinja + + This file is part of LOOT. + + LOOT is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + LOOT 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with LOOT. If not, see + . + */ + +#include "git_helper.h" +#include "../error.h" + +#include +#include +#include + +using namespace std; + +namespace fs = boost::filesystem; +namespace lc = boost::locale; + +namespace loot { + GitHelper::GitHelper() : + repo(nullptr), + remote(nullptr), + cfg(nullptr), + obj(nullptr), + commit(nullptr), + ref(nullptr), + ref2(nullptr), + sig(nullptr), + blob(nullptr), + annotated_commit(nullptr), + tree(nullptr), + diff(nullptr), + buf({0}) { + // Init threading system and OpenSSL (for Linux builds). + git_libgit2_init(); + } + + GitHelper::~GitHelper() { + string path; + if (repo != nullptr) + path = git_repository_path(repo); + + Free(); + + if (!path.empty()) { + try { + FixRepoPermissions(path); + } + catch (exception&) {} + } + + git_libgit2_shutdown(); + } + + void GitHelper::Call(int error_code) { + if (!error_code) + return; + + const git_error * last_error = giterr_last(); + std::string gitError; + if (last_error == nullptr) + gitError = to_string(error_code) + "."; + else + gitError = to_string(error_code) + "; " + last_error->message; + giterr_clear(); + + if (errorMessage.empty()) + errorMessage = (boost::format(lc::translate("Git operation failed. Error: %1%")) % gitError).str(); + + BOOST_LOG_TRIVIAL(error) << "Git operation failed. Error: " << gitError; + throw loot::error(loot::error::git_error, errorMessage); + } + + void GitHelper::SetErrorMessage(const std::string& message) { + errorMessage = message; + } + + void GitHelper::Free() { + git_commit_free(commit); + git_object_free(obj); + git_config_free(cfg); + git_remote_free(remote); + git_repository_free(repo); + git_reference_free(ref); + git_reference_free(ref2); + git_signature_free(sig); + git_blob_free(blob); + git_annotated_commit_free(annotated_commit); + git_tree_free(tree); + git_diff_free(diff); + git_buf_free(&buf); + + commit = nullptr; + obj = nullptr; + cfg = nullptr; + remote = nullptr; + repo = nullptr; + ref = nullptr; + ref2 = nullptr; + sig = nullptr; + blob = nullptr; + annotated_commit = nullptr; + tree = nullptr; + diff = nullptr; + buf = {0}; + } + + bool GitHelper::IsRepository(const boost::filesystem::path& path) { + return git_repository_open_ext(NULL, path.string().c_str(), GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) == 0; + } + + // Removes the read-only flag from some files in git repositories created by libgit2. + void GitHelper::FixRepoPermissions(const boost::filesystem::path& path) { + BOOST_LOG_TRIVIAL(trace) << "Recursively setting write permission on directory: " << path; + for (fs::recursive_directory_iterator it(path); it != fs::recursive_directory_iterator(); ++it) { + BOOST_LOG_TRIVIAL(trace) << "Setting write permission for: " << it->path(); + fs::permissions(it->path(), fs::add_perms | fs::owner_write | fs::group_write | fs::others_write); + } + } + + int GitHelper::diff_file_cb(const git_diff_delta *delta, float progress, void * payload) { + BOOST_LOG_TRIVIAL(trace) << "Checking diff for: " << delta->old_file.path; + git_diff_payload * gdp = (git_diff_payload*)payload; + if (strcmp(delta->old_file.path, gdp->fileToFind) == 0) { + BOOST_LOG_TRIVIAL(warning) << "Edited masterlist found."; + gdp->fileFound = true; + } + + return 0; + } + + bool IsFileDifferent(const boost::filesystem::path& repoRoot, const std::string& filename) { + GitHelper git; + + if (!git.IsRepository(repoRoot)) { + BOOST_LOG_TRIVIAL(info) << "Unknown masterlist revision: Git repository missing."; + throw error(error::ok, lc::translate("Unknown: Git repository missing")); + } + + BOOST_LOG_TRIVIAL(debug) << "Existing repository found, attempting to open it."; + git.Call(git_repository_open(&git.repo, repoRoot.string().c_str())); + + // Perform a git diff, then iterate the deltas to see if one exists for the masterlist. + BOOST_LOG_TRIVIAL(trace) << "Getting the tree for the HEAD revision."; + git.Call(git_revparse_single(&git.obj, git.repo, "HEAD^{tree}")); + git.Call(git_tree_lookup(&git.tree, git.repo, git_object_id(git.obj))); + + BOOST_LOG_TRIVIAL(trace) << "Performing git diff."; + git.Call(git_diff_tree_to_workdir_with_index(&git.diff, git.repo, git.tree, NULL)); + + BOOST_LOG_TRIVIAL(trace) << "Iterating over git diff deltas."; + GitHelper::git_diff_payload payload; + payload.fileFound = false; + payload.fileToFind = filename.c_str(); + git.Call(git_diff_foreach(git.diff, &git.diff_file_cb, NULL, NULL, &payload)); + + // Clean up memory + git_object_free(git.obj); + git.obj = nullptr; + git_tree_free(git.tree); + git.tree = nullptr; + git_diff_free(git.diff); + git.diff = nullptr; + + return payload.fileFound; + } +} diff --git a/src/backend/helpers/git_helper.h b/src/backend/helpers/git_helper.h new file mode 100644 index 00000000..798ae86d --- /dev/null +++ b/src/backend/helpers/git_helper.h @@ -0,0 +1,76 @@ +/* LOOT + + A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and + Fallout: New Vegas. + + Copyright (C) 2012-2015 WrinklyNinja + + This file is part of LOOT. + + LOOT is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + LOOT 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with LOOT. If not, see + . + */ + +#ifndef __LOOT_GIT_HELPER__ +#define __LOOT_GIT_HELPER__ + +#include + +#include + +#include + +namespace loot { + class GitHelper { + public: + GitHelper(); + ~GitHelper(); + + void Call(int error_code); + void SetErrorMessage(const std::string& message); + void Free(); + + static bool IsRepository(const boost::filesystem::path& path); + + // Removes the read-only flag from some files in git repositories + // created by libgit2. + static void FixRepoPermissions(const boost::filesystem::path& path); + + static int diff_file_cb(const git_diff_delta *delta, float progress, void * payload); + + git_repository * repo; + git_remote * remote; + git_config * cfg; + git_object * obj; + git_commit * commit; + git_reference * ref; + git_reference * ref2; + git_signature * sig; + git_blob * blob; + git_annotated_commit * annotated_commit; + git_tree * tree; + git_diff * diff; + git_buf buf; + + struct git_diff_payload { + bool fileFound; + const char * fileToFind; + }; + private: + std::string errorMessage; + }; + + bool IsFileDifferent(const boost::filesystem::path& repoRoot, const std::string& filename); +} +#endif diff --git a/src/backend/masterlist.cpp b/src/backend/masterlist.cpp index c6e72ba7..1e2ed132 100644 --- a/src/backend/masterlist.cpp +++ b/src/backend/masterlist.cpp @@ -23,9 +23,12 @@ */ #include "masterlist.h" +#include "helpers/git_helper.h" #include "game/game.h" #include "error.h" +#include + using namespace std; namespace fs = boost::filesystem; @@ -61,4 +64,379 @@ namespace loot { return date; } + + void Masterlist::GetGitInfo(const boost::filesystem::path& path, bool shortID) { + // Compare HEAD and working copy, and get revision info. + GitHelper git; + git.SetErrorMessage((boost::format(lc::translate("An error occurred while trying to read the local masterlist's version. If this error happens again, try deleting the \".git\" folder in %1%.")) % path.parent_path().string()).str()); + + if (!fs::exists(path)) { + BOOST_LOG_TRIVIAL(info) << "Unknown masterlist revision: No masterlist present."; + throw error(error::ok, lc::translate("N/A: No masterlist present")); + } + else if (!git.IsRepository(path.parent_path())) { + BOOST_LOG_TRIVIAL(info) << "Unknown masterlist revision: Git repository missing."; + throw error(error::ok, lc::translate("Unknown: Git repository missing")); + } + + BOOST_LOG_TRIVIAL(debug) << "Existing repository found, attempting to open it."; + git.Call(git_repository_open(&git.repo, path.parent_path().string().c_str())); + + //Need to get the HEAD object, because the individual file has a different SHA. + BOOST_LOG_TRIVIAL(info) << "Getting the Git object for the tree at HEAD."; + git.Call(git_revparse_single(&git.obj, git.repo, "HEAD")); + + BOOST_LOG_TRIVIAL(trace) << "Generating hex string for Git object ID."; + if (shortID) { + git.Call(git_object_short_id(&git.buf, git.obj)); + revision = git.buf.ptr; + } + else { + char c_rev[GIT_OID_HEXSZ + 1]; + revision = git_oid_tostr(c_rev, GIT_OID_HEXSZ + 1, git_object_id(git.obj)); + } + + BOOST_LOG_TRIVIAL(trace) << "Getting date for Git object."; + const git_oid * oid = git_object_id(git.obj); + git.Call(git_commit_lookup(&git.commit, git.repo, oid)); + git_time_t time = git_commit_time(git.commit); + boost::locale::date_time dateTime(time); + stringstream out; + out << boost::locale::as::ftime("%Y-%m-%d") << dateTime; + date = out.str(); + + // Free object memory. + git_object_free(git.obj); + git.obj = nullptr; + + BOOST_LOG_TRIVIAL(trace) << "Diffing masterlist HEAD and working copy."; + if (IsFileDifferent(path.parent_path(), path.filename().string())) { + revision += string(" ") + lc::translate("(edited)").str(); + date += string(" ") + lc::translate("(edited)").str(); + } + } + + bool Masterlist::Update(const Game& game) { + return Update(game.MasterlistPath(), game.RepoURL(), game.RepoBranch()); + } + + bool Masterlist::Update(const boost::filesystem::path& path, const std::string& repoURL, const std::string& repoBranch) { + GitHelper git; + fs::path repo_path = path.parent_path(); + string filename = path.filename().string(); + + // First initialise some stuff that isn't specific to a repository. + BOOST_LOG_TRIVIAL(debug) << "Creating a reflog signature to use."; + // Create a reflog signature for any changes. + git.Call(git_signature_new(&git.sig, "LOOT", "loot@placeholder.net", 0, 0)); + + BOOST_LOG_TRIVIAL(debug) << "Setting up checkout options."; + char * paths = new char[filename.length() + 1]; + strcpy(paths, filename.c_str()); + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE | GIT_CHECKOUT_DONT_REMOVE_EXISTING; + checkout_opts.paths.strings = &paths; + checkout_opts.paths.count = 1; + + // Now try to access the repository if it exists, or clone one if it doesn't. + BOOST_LOG_TRIVIAL(trace) << "Attempting to open the Git repository at: " << repo_path; + if (!git.IsRepository(repo_path)) { + git.SetErrorMessage(lc::translate("An error occurred while trying to clone the remote masterlist repository.")); + // Clone the remote repository. + BOOST_LOG_TRIVIAL(info) << "Repository doesn't exist, cloning the remote repository."; + + fs::path temp_path = repo_path.string() + ".temp"; + if (!fs::is_empty(repo_path)) { + // Clear any read-only flags first. + git.FixRepoPermissions(repo_path); + // Now, libgit2 doesn't support cloning into non-empty folders. Rename the folder + + // temporarily, and move its contents back in afterwards, skipping any that then conflict. + BOOST_LOG_TRIVIAL(trace) << "Repo path not empty, renaming folder."; + // If the temp path already exists, it needs to be deleted. + if (fs::exists(temp_path)) { + git.FixRepoPermissions(temp_path); + fs::remove_all(temp_path); + } + // There's no point moving the .git folder, so delete that. + fs::remove_all(repo_path / ".git"); + // Now move to temp path. + fs::rename(repo_path, temp_path); + // Recreate the game folder so that we don't inadvertently cause any other errors (everything past LOOT init assumes it exists). + fs::create_directory(repo_path); + } + + //First set up clone options. + + git_clone_options clone_options = GIT_CLONE_OPTIONS_INIT; + clone_options.checkout_opts = checkout_opts; + clone_options.bare = 0; + clone_options.checkout_branch = repoBranch.c_str(); + clone_options.signature = git.sig; + + //Now perform the clone. + git.Call(git_clone(&git.repo, repoURL.c_str(), repo_path.string().c_str(), &clone_options)); + + if (fs::exists(temp_path)) { + //Move contents back in. + BOOST_LOG_TRIVIAL(trace) << "Repo path wasn't empty, moving previous files back in."; + for (fs::directory_iterator it(temp_path); it != fs::directory_iterator(); ++it) { + if (!fs::exists(repo_path / it->path().filename())) { + //No conflict, OK to move back in. + fs::rename(it->path(), repo_path / it->path().filename()); + } + } + //Delete temporary folder. + git.FixRepoPermissions(temp_path); + fs::remove_all(temp_path); + } + } + else { + // Repository exists: check settings are correct, then pull updates. + git.SetErrorMessage((boost::format(lc::translate("An error occurred while trying to access the local masterlist repository. If this error happens again, try deleting the \".git\" folder in %1%.")) % repo_path.string()).str()); + + // Open the repository. + BOOST_LOG_TRIVIAL(info) << "Existing repository found, attempting to open it."; + git.Call(git_repository_open(&git.repo, repo_path.string().c_str())); + + // Check that the repository's remote settings match LOOT's. + BOOST_LOG_TRIVIAL(info) << "Checking to see if remote URL matches URL in settings."; + git.Call(git_remote_lookup(&git.remote, git.repo, "origin")); + const char * url = git_remote_url(git.remote); + + BOOST_LOG_TRIVIAL(info) << "Remote URL given: " << repoURL; + BOOST_LOG_TRIVIAL(info) << "Remote URL in repository settings: " << url; + if (url != repoURL) { + BOOST_LOG_TRIVIAL(info) << "URLs do not match, setting repository URL to URL in settings."; + // The URLs don't match. Change the remote URL to match the one LOOT has. + git.Call(git_remote_set_url(git.remote, repoURL.c_str())); + + // Now save change. + git.Call(git_remote_save(git.remote)); + } + + // Now fetch updates from the remote. + BOOST_LOG_TRIVIAL(trace) << "Fetching updates from remote."; + git.SetErrorMessage(lc::translate("An error occurred while trying to update the masterlist. This could be due to a server-side error. Try again in a few minutes.")); + + git.Call(git_remote_fetch(git.remote, nullptr, git.sig, nullptr)); + + // Print some stats on what was fetched either during update or clone. + const git_transfer_progress * stats = git_remote_stats(git.remote); + BOOST_LOG_TRIVIAL(info) << "Received " << stats->indexed_objects << " of " << stats->total_objects << " objects in " << stats->received_bytes << " bytes."; + + // Check that a branch with the correct name exists. + git.SetErrorMessage((boost::format(lc::translate("An error occurred while trying to access the local masterlist repository. If this error happens again, try deleting the \".git\" folder in %1%.")) % repo_path.string()).str()); + int ret = git_branch_lookup(&git.ref, git.repo, repoBranch.c_str(), GIT_BRANCH_LOCAL); + if (ret == GIT_ENOTFOUND) { + // Branch doesn't exist. Create a new branch using the remote branch's latest commit. + + BOOST_LOG_TRIVIAL(trace) << "Looking up commit referred to by the remote branch \"" << repoBranch << "\"."; + git.Call(git_revparse_single(&git.obj, git.repo, (string("origin/") + repoBranch).c_str())); + const git_oid * commit_id = git_object_id(git.obj); + + BOOST_LOG_TRIVIAL(trace) << "Creating the new branch."; + // Create a branch. + git.Call(git_commit_lookup(&git.commit, git.repo, commit_id)); + git.Call(git_branch_create(&git.ref, git.repo, repoBranch.c_str(), git.commit, 0, git.sig, NULL)); + + // Set upstream. Don't really know if this is necessary or not. + git.Call(git_branch_set_upstream(git.ref, (string("origin/") + repoBranch).c_str())); + + BOOST_LOG_TRIVIAL(trace) << "Setting the upstream for the new branch."; + // Free tree and commit pointers. Reference pointer is still used below. + git_object_free(git.obj); + git_commit_free(git.commit); + git.commit = nullptr; + git.obj = nullptr; + + BOOST_LOG_TRIVIAL(trace) << "Done creating the new branch."; + } + else if (ret != 0) + git.Call(ret); // Handle other errors. + + // Check if HEAD points to the desired branch and set it to if not. + if (!git_branch_is_head(git.ref)) { + BOOST_LOG_TRIVIAL(trace) << "Setting HEAD to follow branch: " << repoBranch; + git.Call(git_repository_set_head(git.repo, (string("refs/heads/") + repoBranch).c_str(), git.sig, (string("Updated HEAD to ") + repoBranch).c_str())); + } + + if (ret == 0) { + /* The branch did exist, and is now pointed at by HEAD. + Need to merge the remote branch into it. Just do a fast-forward merge because + that's all that should be necessary as the local repo shouldn't get changed by + the user. + */ + + BOOST_LOG_TRIVIAL(trace) << "Checking that local and remote branches can be merged by fast-forward."; + git_merge_analysis_t analysis; + git_merge_preference_t pref; + git.Call(git_reference_lookup(&git.ref2, git.repo, (string("refs/remotes/origin/") + repoBranch).c_str())); + git.Call(git_annotated_commit_from_ref(&git.annotated_commit, git.repo, git.ref2)); + git.Call(git_merge_analysis(&analysis, &pref, git.repo, (const git_annotated_commit **)&git.annotated_commit, 1)); + + if ((analysis & GIT_MERGE_ANALYSIS_FASTFORWARD) != 0) { + BOOST_LOG_TRIVIAL(trace) << "Local branch can be fast-forwarded to remote branch."; + // The remote branch reference points to a particular commit. We just want to + // update the local branch reference to point to the same commit. + + // Get the commit object ID. + git.Call(git_reference_peel(&git.obj, git.ref2, GIT_OBJ_COMMIT)); + const git_oid * commit_id = git_object_id(git.obj); + git_reference_free(git.ref2); + git.ref2 = nullptr; + git_object_free(git.obj); + git.obj = nullptr; + + // Set the reference target. + git.Call(git_reference_set_target(&git.ref2, git.ref, commit_id, git.sig, "Setting branch reference.")); + + git_reference_free(git.ref2); + git.ref2 = nullptr; + } + else if ((analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) != 0) { + // No merge is required, but HEAD might be ahead of the remote branch. Check + // to see if that's the case, and move HEAD back to match the remote branch + // if so. + BOOST_LOG_TRIVIAL(trace) << "Local branch is up-to-date with remote branch."; + + BOOST_LOG_TRIVIAL(trace) << "Checking to see if local and remote branch heads are equal."; + // Get the local branch and remote branch head commit IDs. + + // Local branch. + git.Call(git_reference_peel(&git.obj, git.ref, GIT_OBJ_COMMIT)); + const git_oid * local_commit_id = git_object_id(git.obj); + git_object_free(git.obj); + git.obj = nullptr; + + // Remote branch. + git.Call(git_reference_peel(&git.obj, git.ref2, GIT_OBJ_COMMIT)); + const git_oid * remote_commit_id = git_object_id(git.obj); + git_reference_free(git.ref2); + git.ref2 = nullptr; + git_object_free(git.obj); + git.obj = nullptr; + + if (local_commit_id->id != remote_commit_id->id) { + BOOST_LOG_TRIVIAL(trace) << "Branch heads are not equal, updating local HEAD."; + // Commit IDs don't match, update HEAD, and continue with normal update procedure. + git.Call(git_reference_set_target(&git.ref2, git.ref, remote_commit_id, git.sig, "Setting branch reference.")); + git_reference_free(git.ref2); + git.ref2 = nullptr; + } + else { + // HEAD matches the remote branch. If the masterlist in + // HEAD also matches the masterlist file, no further + // action needs to be taken. Otherwise, a checkout + // must be performed and the checked-out file parsed. + BOOST_LOG_TRIVIAL(trace) << "Branch heads are equal."; + + BOOST_LOG_TRIVIAL(trace) << "Diffing HEAD and filesystem masterlists."; + if (!IsFileDifferent(repo_path, filename)) { + return false; + } + } + } + else { + // The local repository can't be easily merged. It's best just to delete and re-clone it. + git.FixRepoPermissions(repo_path / ".git"); + git.Free(); + fs::remove_all(repo_path / ".git"); + return this->Update(path, repoURL, repoBranch); + } + } + + // Free branch pointer. + git_reference_free(git.ref); + git.ref = nullptr; + + BOOST_LOG_TRIVIAL(trace) << "Performing a Git checkout of HEAD."; + git.Call(git_checkout_head(git.repo, &checkout_opts)); + } + + // Now whether the repository was cloned or updated, the working directory contains + // the latest masterlist. Try parsing it: on failure, detach the HEAD back one commit + // and try again. + + bool parsingFailed = false; + std::string parsingError; + git.SetErrorMessage((boost::format(lc::translate("An error occurred while trying to read information on the updated masterlist. If this error happens again, try deleting the \".git\" folder in %1%.")) % repo_path.string()).str()); + do { + // Get some descriptive info about what was checked out. + + BOOST_LOG_TRIVIAL(trace) << "Getting the Git object for HEAD."; + git.Call(git_repository_head(&git.ref, git.repo)); + git.Call(git_reference_peel(&git.obj, git.ref, GIT_OBJ_COMMIT)); + + BOOST_LOG_TRIVIAL(trace) << "Generating hex string for Git object ID."; + git.Call(git_object_short_id(&git.buf, git.obj)); + revision = git.buf.ptr; + + BOOST_LOG_TRIVIAL(trace) << "Getting date for Git object."; + const git_oid * oid = git_object_id(git.obj); + git.Call(git_commit_lookup(&git.commit, git.repo, oid)); + git_time_t time = git_commit_time(git.commit); + // Now convert into a nice text format. + boost::locale::date_time dateTime(time); + stringstream out; + out << boost::locale::as::ftime("%Y-%m-%d") << dateTime; + date = out.str(); + + BOOST_LOG_TRIVIAL(debug) << "Set HEAD to commit (date): " << revision << " (" << date << ")."; + BOOST_LOG_TRIVIAL(trace) << "Freeing pointers."; + git_reference_free(git.ref); + git_object_free(git.obj); + git_buf_free(&git.buf); + git_commit_free(git.commit); + git.ref = nullptr; + git.obj = nullptr; + git.commit = nullptr; + git.buf = {0}; + + //Now try parsing the masterlist. + BOOST_LOG_TRIVIAL(debug) << "Testing masterlist parsing."; + try { + this->MetadataList::Load(path); + + for (auto &plugin : plugins) { + plugin.ParseAllConditions(); + } + for (auto &plugin : regexPlugins) { + plugin.ParseAllConditions(); + } + for (auto &message : messages) { + message.ParseCondition(); + } + + parsingFailed = false; + } + catch (std::exception& e) { + parsingFailed = true; + + //Roll back one revision if there's an error. + BOOST_LOG_TRIVIAL(error) << "Masterlist parsing failed. Masterlist revision " + string(revision) + ": " + e.what(); + + // Get an object ID for 'HEAD~1'. + git.Call(git_revparse_single(&git.obj, git.repo, "HEAD~1")); + const git_oid * oid = git_object_id(git.obj); + git_object_free(git.obj); + git.obj = nullptr; + + // Detach HEAD to HEAD~1. This will roll back HEAD by one commit each time it is called. + git.Call(git_repository_set_head_detached(git.repo, oid, git.sig, "Updated HEAD to HEAD~1")); + + // Checkout the new HEAD. + BOOST_LOG_TRIVIAL(trace) << "Performing a Git checkout of HEAD."; + git.Call(git_checkout_head(git.repo, &checkout_opts)); + + if (parsingError.empty()) + parsingError = boost::locale::translate("Masterlist revision").str() + " " + string(revision) + ": " + e.what() + ". " + boost::locale::translate("Rolled back to the previous revision.").str(); + } + } while (parsingFailed); + + if (!parsingError.empty()) + throw error(error::ok, parsingError); //Throw an OK because the process still completed in a successful state. + + return true; + } }