/* Copyright 2018 WobLight * * This program 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. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "addon.h" #include #include #include #include "control.h" #ifdef Q_OS_WIN32 #include #include #endif #include #include #include #include Addon::Addon(QString name, git_repository *repo, QObject *parent) : QObject(parent), m_name(name), m_repo(repo, &git_repository_free), m_progress(0), m_total(0), m_status(Status::Ready), m_gitStatus(GitStatusFlag::UpToDate), m_pool(new QThreadPool(this)) { connect(this, &Addon::remoteChanged, this, &Addon::fetchRemote); connect(this, &Addon::currentBranchChanged, this, &Addon::updateGitStatus); scanBranches(); } QString Addon::name() const { return m_name; } void Addon::setName(QString name) { if (m_name == name) return; m_name = name; emit nameChanged(m_name); } void Addon::update() { removeSubfolders(); delegate("Update", [this](){ git_reference *lbr = nullptr; git_reference *rbr = nullptr; git_reference *ubr = nullptr; int error = git_branch_lookup(&lbr, m_repo.get(), m_currentBranch.toLocal8Bit(), GIT_BRANCH_LOCAL); git_annotated_commit *ac; git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; opts.checkout_strategy = GIT_CHECKOUT_FORCE; if (error == GIT_ENOTFOUND) { git_branch_lookup(&lbr, m_repo.get(), m_currentBranch.toLocal8Bit(), GIT_BRANCH_REMOTE); git_annotated_commit_from_ref(&ac, m_repo.get(), lbr); } else { git_branch_upstream(&rbr, lbr); git_annotated_commit_from_ref(&ac, m_repo.get(), rbr); } if (!git_branch_is_head(lbr)) git_repository_set_head(m_repo.get(), git_reference_name(lbr)); git_reference_set_target(&ubr, lbr, git_annotated_commit_id(ac), nullptr); git_checkout_head(m_repo.get(), &opts); git_annotated_commit_free(ac); if (rbr) git_reference_free(rbr); git_reference_free(lbr); git_reference_free(ubr); }, [this](){updateGitStatus();}); unpackSubfolders(); } void Addon::scanBranches() { delegate("Branch Scan", [this](){ struct { QStringList rs; QStringList bs; QString cb; QString cr; } data; git_strarray remotes; int error = git_remote_list(&remotes, m_repo.get()); for (unsigned int i = 0; i < remotes.count; i++) data.rs << remotes.strings[i]; git_strarray_free(&remotes); git_branch_iterator *iter = nullptr; error = git_branch_iterator_new(&iter, m_repo.get(), GIT_BRANCH_ALL); git_reference *ref = nullptr; const char *bname = nullptr; git_branch_t btype; while (!(error = git_branch_next(&ref, &btype, iter))) { git_branch_name(&bname, ref); data.bs << bname; if (git_branch_is_head(ref)) { data.cb = bname; git_buf buf = GIT_BUF_INIT_CONST(0, nullptr); (git_reference_is_remote(ref) ? git_branch_remote_name : git_branch_upstream_remote )(&buf, m_repo.get(), git_reference_name(ref)); data.cr = buf.ptr; } git_reference_free(ref); } git_branch_iterator_free(iter); return data; },[this](auto data){ setRemote(data.cr); setCurrentBranch(data.cb); setRemotes(data.rs); setBranches(data.bs); scanSubfolders(); }); } void Addon::scanSubfolders() { delegate("Subfolders Scan", [this](){ QStringList sf; git_object *obj = nullptr; git_revparse_single(&obj, m_repo.get(), "HEAD^{tree}"); git_tree *tree = reinterpret_cast(obj); git_tree_walk(tree, GIT_TREEWALK_PRE, [](const char *root, const git_tree_entry *entry, void *payload) -> int{ QString path = root; if (path.isEmpty()) return 0; QStringList *sf = static_cast(payload); QString file = git_tree_entry_name(entry); if (file.toLower() == path.toLower().chopped(1) + ".toc") { *sf << path.chopped(1); return 1; } if (path.split("/").size() > 1) return 1; return 0; }, &sf); git_object_free(obj); return sf; },[this](auto subs){setSubfolders(subs);}); } void Addon::setBranches(QStringList branches) { if (m_branches == branches) return; m_branches = branches; emit branchesChanged(m_branches); } void Addon::setCurrentBranch(QString currentBranch) { if (m_currentBranch == currentBranch) return; m_currentBranch = currentBranch; emit currentBranchChanged(m_currentBranch); } void Addon::setRemote(QString remote) { if (m_remote == remote) return; m_remote = remote; emit remoteChanged(m_remote); } void Addon::setRemotes(QStringList remotes) { if (m_remotes == remotes) return; m_remotes = remotes; emit remotesChanged(m_remotes); } int fetch_progress_cb(const git_transfer_progress *stats, void *payload) { Addon *a = static_cast(payload); QMetaObject::invokeMethod(a,"setTotal",Q_ARG(int, stats->total_objects)); QMetaObject::invokeMethod(a,"setProgress",Q_ARG(int, stats->received_objects)); return 0; } void Addon::fetchRemote(QString remote) { delegate(remote + " Fetch", [this, remote](){ git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT; fetch_opts.callbacks.transfer_progress = &fetch_progress_cb; fetch_opts.callbacks.payload = this; git_remote *r; git_remote_lookup(&r, m_repo.get(), remote.toLocal8Bit()); int error = git_remote_fetch(r,nullptr,&fetch_opts,nullptr); if (error < 0) { const git_error *e = giterr_last(); qDebug() <klass, e->message); } git_remote_free(r); }, [this](){updateGitStatus();}); } void Addon::setProgress(int fetchProgress) { if (m_progress == fetchProgress) return; m_progress = fetchProgress; emit progressChanged(m_progress); } void Addon::setTotal(int fetchSize) { if (m_total == fetchSize) return; m_total = fetchSize; emit totalChanged(m_total); } void Addon::setStatus(Addon::Status status) { if (m_status == status) return; m_status = status; emit statusChanged(m_status); } void Addon::setGitStatus(Addon::GitStatus gitStatus) { if (m_gitStatus == gitStatus) return; m_gitStatus = gitStatus; emit gitStatusChanged(m_gitStatus); } void Addon::updateGitStatus() { delegate("Git Status Update", [this](){ git_reference *tr; git_branch_lookup(&tr, m_repo.get(), m_currentBranch.toLocal8Bit(), GIT_BRANCH_LOCAL); if (!tr) { git_branch_lookup(&tr, m_repo.get(), m_currentBranch.toLocal8Bit(), GIT_BRANCH_REMOTE); } else { git_reference *rbr; git_branch_upstream(&rbr, tr); git_reference_free(tr); tr = rbr; } if (!tr) return GitStatusFlag::UpToDate; git_reference *lr; git_repository_head(&lr, m_repo.get()); git_annotated_commit *uc; git_annotated_commit *lc; git_annotated_commit_from_ref(&uc, m_repo.get(), tr); git_annotated_commit_from_ref(&lc, m_repo.get(), lr); size_t ahead; size_t behind; git_graph_ahead_behind(&ahead, &behind, m_repo.get(), git_annotated_commit_id(lc), git_annotated_commit_id(uc)); if (!(ahead || behind)) return GitStatusFlag::UpToDate; else if (ahead && !behind) return GitStatusFlag::Ahead; else if (!ahead) return GitStatusFlag::Behind; else return GitStatusFlag::Diverged; git_reference_free(tr); git_reference_free(lr); git_annotated_commit_free(uc); git_annotated_commit_free(lc); /// TODO: check for local changes/merge /*if (git_repository_head_detached(m_repo.get())) { setGitStatus(GitStatus::Conflicting); return; } git_reference *tr; git_branch_lookup(&tr, m_repo.get(), m_currentBranch.toLocal8Bit(), GIT_BRANCH_LOCAL); if (!tr) { git_branch_lookup(&tr, m_repo.get(), m_currentBranch.toLocal8Bit(), GIT_BRANCH_REMOTE); } else { git_reference *rbr; git_branch_upstream(&rbr, tr); git_reference_free(tr); tr = rbr; } git_buf buf = GIT_BUF_INIT_CONST(0, nullptr); git_branch_remote_name(&buf, m_repo.get(), git_reference_name(tr)); if (buf.ptr != remote()) setRemote(buf.ptr); else { git_annotated_commit *ac; git_annotated_commit_from_ref(&ac, m_repo.get(), tr); git_merge_analysis_t an; git_merge_preference_t pref; git_merge_analysis(&an, &pref, m_repo.get(),(const git_annotated_commit **)&ac, 1); if (an & GIT_MERGE_ANALYSIS_UP_TO_DATE) { if (git_annotated_commit_id(*ac) != git_head) setGitStatus(GitStatus::Conflicting); else setGitStatus(GitStatus::UpToDate); } else if (an & GIT_MERGE_ANALYSIS_FASTFORWARD) setGitStatus(GitStatus::FastForwardable); else setGitStatus(GitStatus::Conflicting); git_annotated_commit_free(ac); } git_reference_free(tr);*/ }, [this](auto s){ setGitStatus(s); }); } void Addon::uninstall() { removeSubfolders(); closeRepo(); delegate("Removing addon folder", [this](){ removeFolder(Control::instance()->addonsPath() + "/" + m_name); }, [](){Control::instance()->scanForAddons();}); } void Addon::setSubfolders(QStringList subfolders) { if (m_subfolders == subfolders) return; m_subfolders = subfolders; emit subfoldersChanged(m_subfolders); } void walkFolders(QFileInfo &info, auto f){ if (info.isDir() && !info.isSymLink()) { foreach (QFileInfo sub, QDir(info.absoluteFilePath()).entryInfoList(QDir::AllEntries|QDir::NoDotAndDotDot|QDir::Hidden)) walkFolders(sub, f); } f(info); } void Addon::removeFolder(QString path, bool ask) { QFileInfo info(path); QStringList files; bool ok = ask; if (info.exists()) { walkFolders(info, [&files](QFileInfo info){ files << info.absoluteFilePath(); }); if (ask) { m_mutex.lock(); m_result = &ok; QMetaObject::invokeMethod(this, "setFilesToRemove", Q_ARG(QString, files.join('\n'))); m_wait.wait(&m_mutex); m_mutex.unlock(); } if (ok) { emit totalChanged(files.size()); for (int i = 0; i < files.size(); i++) { QFileInfo info(files[i]); if (!info.isSymLink() && info.isDir()) info.dir().rmdir(info.fileName()); else QFile(info.absoluteFilePath()).remove(); emit totalChanged(i); } } } } void Addon::removeSubfolders() { delegate("Removing Subfolders", [this](){ foreach (QString subf, m_subfolders) removeFolder(Control::instance()->addonsPath() + "/" + subf); }); } void Addon::unpackSubfolders(){ delegate("Unpacking Subfolders", [this](){ QStringList errors; QDir addonsDir(Control::instance()->addonsPath()); if (m_subfolders.contains(m_name, Qt::CaseInsensitive)) { QString oldName = m_name; setName(m_name + ".repo"); addonsDir.rename(oldName, m_name); openRepo(); } QDir::setCurrent(addonsDir.canonicalPath()); QDir dir(m_name); foreach (QString subfn, m_subfolders) { QFileInfo addonFolder(subfn); if (addonFolder.exists()) { QFileInfo dest(addonFolder.fileName() + ".bak"); int i = 0; while (dest.exists()) dest = QFileInfo(addonFolder.fileName() + ".bak." + QString::number(++i)); addonsDir.rename(addonFolder.fileName(), dest.fileName()); if (QFileInfo(subfn).exists()) { errors << QString("The folder \"%1\" already existed and couldn't be renamed. The addon subfolder has been skipped").arg(subfn, dest.exists()); continue; } errors << QString("The folder \"%1\" already existed and has been renamed to \"%2\"").arg(subfn, dest.fileName()); } #ifdef Q_OS_WIN32 QString link = subfn; link.replace("/","\\"); QString target = "./"+m_name+"/"+subfn; target.replace("/","\\"); wchar_t link_w[link.size() +1]; wchar_t target_w[target.size() +1]; int pos = link.toWCharArray(link_w); link_w[pos] = '\0'; pos = target.toWCharArray(target_w); target_w[pos] = '\0'; #else QFile::link("./"+m_name+"/"+subfn, subfn); #endif if (!QFileInfo(subfn).exists()) { dir.rename(subfn, "../"+subfn); } QFileInfo info(subfn); if (!info.exists()) { errors << "Failed to link or move \""+ info.absoluteFilePath() + "\" to \"" + addonFolder.absoluteFilePath()+"\""; } } return errors; }, [this](auto errors){ if (errors.size() > 0) { setStatus(Status::Error); //setStatusMessage(errors.join('\n')); } }); } void Addon::setFilesToRemove(QString filesToRemove) { if (m_filesToRemove == filesToRemove) return; m_filesToRemove = filesToRemove; emit filesToRemoveChanged(m_filesToRemove); } void Addon::confirmFileRemove(bool confirmed) { m_mutex.lock(); *static_cast(m_result) = confirmed; m_wait.wakeOne(); m_mutex.unlock(); } QStringList Addon::branches() const { return m_branches; } QString Addon::currentBranch() const { return m_currentBranch; } QString Addon::remote() const { return m_remote; } QStringList Addon::remotes() const { return m_remotes; } int Addon::progress() const { return m_progress; } int Addon::total() const { return m_total; } Addon::Status Addon::status() const { return m_status; } Addon::GitStatus Addon::gitStatus() const { return m_gitStatus; } QStringList Addon::subfolders() const { return m_subfolders; } QString Addon::filesToRemove() const { return m_filesToRemove; } void Addon::closeRepo() { m_repo.reset(); } void Addon::openRepo() { git_repository *repo = nullptr; git_repository_open(&repo, (Control::instance()->addonsPath() + "/" + m_name).toLocal8Bit()); m_repo.reset(repo); } void Addon::delegate(QString taskname, auto work, auto callback) { Q_ASSERT_X(QThread::currentThread() == thread(), "delegate", "Attempt to delegate from another thread."); if (status() != Status::Ready) { qInfo() << name() << "Enqueueing" << taskname; m_tasks.enqueue({taskname, [this, taskname, work, callback](){delegate(taskname, work,callback);}}); return; } qInfo() << name() << "Executing " << taskname; setProgress(0); setTotal(0); setStatus(Status::Busy); using ret_t = typename std::invoke_result::type; QFuture fut = QtConcurrent::run(m_pool, work); QFutureWatcher *fw = new QFutureWatcher(); connect(fw, &QFutureWatcher::finished, [this, callback, fw](){ setStatus(Status::Ready); TaskQueue old; m_tasks.swap(old); if constexpr (std::is_same::value) callback(); else callback(fw->result()); m_tasks.append(old); if (!m_tasks.isEmpty() && status() != Status::Busy) m_tasks.dequeue().second(); }); connect(fw, &QFutureWatcher::finished, fw, &QFutureWatcher::deleteLater); fw->setFuture(fut); } void Addon::delegate(QString taskname, auto work) { using ret_t = typename std::invoke_result::type; if constexpr (std::is_same::value) delegate(taskname, work, [](){}); else delegate(taskname, work, [](ret_t){}); }