/* Copyright 2018-2019 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, QString path, 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)), m_path(path)
{
connect(this, &Addon::remoteChanged, this, &Addon::fetchRemote);
connect(this, &Addon::currentBranchChanged, this, &Addon::updateGitStatus);
scanBranches();
loadReadme();
}
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](){
auto lbr = branchRef(m_currentBranch.toLocal8Bit());
auto tr = remoteRefForBranch(m_currentBranch.toLocal8Bit());
AutoPtr ubr(&git_reference_free);
if (!git_branch_is_head(lbr))
check_git_return(git_repository_set_head(m_repo.get(), git_reference_name(lbr)));
check_git_return(git_reference_set_target(&ubr, lbr, git_reference_target(!tr?lbr:tr), nullptr));
git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
opts.checkout_strategy = GIT_CHECKOUT_FORCE;
check_git_return(git_checkout_head(m_repo.get(), &opts));
}, [this](){updateGitStatus();});
unpackSubfolders();
}
void Addon::scanBranches()
{
delegate("Branch Scan", [this](){
struct {
QStringList rs;
QStringList bs;
QString cb;
QString cr;
} data;
git_strarray remotes;
check_git_return(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);
AutoPtr iter(&git_branch_iterator_free);
check_git_return(git_branch_iterator_new(&iter, m_repo.get(), GIT_BRANCH_ALL));
AutoPtr ref(&git_reference_free);
const char *bname = nullptr;
git_branch_t btype;
while (!check_git_return(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;
}
ref.reset();
}
if (data.cb.isEmpty()) {
AutoPtr reflog(&git_reflog_free);
if (!git_reflog_read(&reflog, m_repo.get(), "HEAD")) {
for (size_t i = 0; i < git_reflog_entrycount(reflog); i++) {
auto entry = git_reflog_entry_byindex(reflog, i);
QRegularExpression rex("checkout: moving from \\S+ to (.*)");
auto match = rex.match(git_reflog_entry_message(entry));
if (match.hasMatch()) {
auto f = &git_branch_upstream_remote;
git_branch_lookup(&ref, m_repo.get(), match.captured(1).toLocal8Bit(), GIT_BRANCH_LOCAL);
if (!ref) {
f = &git_branch_remote_name;
git_branch_lookup(&ref, m_repo.get(), match.captured(1).toLocal8Bit(), GIT_BRANCH_REMOTE);
}
if (!ref) break;
git_buf buf = GIT_BUF_INIT_CONST(0, nullptr);
if (!f(&buf, m_repo.get(), git_reference_name(ref))) {
data.cr = buf.ptr;
data.cb = match.captured(1);
}
ref.reset();
break;
}
}
}
}
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;
AutoPtr obj(git_object_free);
git_revparse_single(&obj, m_repo.get(), "HEAD^{tree}");
git_tree *tree = reinterpret_cast(obj.pointer);
if (!tree)
return sf;
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;
path = path.chopped(1);
QStringList *sf = static_cast(payload);
QString file = git_tree_entry_name(entry);
if (!sf->contains(path) && !file.compare(path + ".toc", Qt::CaseInsensitive)) {
*sf << path;
return 1;
}
Control::removeTocSuffixes(file);
if (!sf->contains(path) && !file.compare(path + ".toc", Qt::CaseInsensitive)) {
*sf << path;
return 1;
}
if (path.split("/").size() > 1)
return 1;
return 0;
}, &sf);
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;
AutoPtr r(git_remote_free);
check_git_return(git_remote_lookup(&r, m_repo.get(), remote.toLocal8Bit()));
check_git_return(git_remote_fetch(r,nullptr,&fetch_opts,nullptr));
}, [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](){
auto tr = remoteRefForBranch(m_currentBranch);
if (!tr)
return GitStatusFlag::UpToDate;
AutoPtr lr(&git_reference_free);
check_git_return(git_repository_head(&lr, m_repo.get()));
AutoPtr uc(&git_annotated_commit_free);
AutoPtr lc(&git_annotated_commit_free);
check_git_return(git_annotated_commit_from_ref(&uc, m_repo.get(), tr));
check_git_return(git_annotated_commit_from_ref(&lc, m_repo.get(), lr));
if (!lc)
return GitStatusFlag::Error;
size_t ahead = 0;
size_t behind = 0;
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;
/// TODO: check for local changes/merge
/*if (git_repository_head_detached(m_repo.get())) {
setGitStatus(GitStatus::Conflicting);
return;
}
AutoPtr tr(&git_reference_free);
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 {
AutoPtr rbr(&git_reference_free);
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 {
AutoPtr ac(&git_annotated_commit_free);
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 AutoPtr *)&ac, 1)(&git_annotated_commit_free);
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(m_path + "/" + m_name);
}, [](){Control::instance()->scanForAddons();});
}
void Addon::setSubfolders(QStringList subfolders)
{
if (m_subfolders == subfolders)
return;
m_subfolders = subfolders;
emit subfoldersChanged(m_subfolders);
}
void walkFolders(const 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);
}
bool Addon::removeFolders(QStringList paths, bool ask) {
QStringList files;
bool ok = ask;
foreach (QString path, paths){
QFileInfo info(path);
if (info.exists()) {
walkFolders(info, [&files](QFileInfo info){
files << info.absoluteFilePath();
});
}
}
if (files.isEmpty()) return true;
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();
m_filesToRemove.clear();
}
if (ok) {
setTotal(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 f(info.absoluteFilePath());
f.setPermissions(f.permissions()|QFile::WriteOther);
f.remove();
}
setProgress(i);
}
return true;
}
return false;
}
bool Addon::removeFolder(QString path, bool ask) {
return removeFolders({path}, ask);
}
void Addon::removeSubfolders()
{
delegate("Removing Subfolders", [this](){
QStringList paths(m_subfolders);
std::transform(paths.begin(), paths.end(), paths.begin(), [this](auto p){return m_path + "/" + p;});
removeFolders(paths);
});
}
void Addon::unpackSubfolders(){
delegate("Unpacking Subfolders", [this](){
QStringList errors;
QDir addonsDir(m_path);
if (m_subfolders.contains(m_name, Qt::CaseInsensitive)) {
closeRepo();
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();
}
void Addon::reset()
{
removeSubfolders();
delegate("Reset", [this](){
AutoPtr ref(&git_reference_free);
git_repository_head(&ref, m_repo.get());
git_object *obj = nullptr;
git_object_lookup(&obj, m_repo.get(), git_reference_target(ref), GIT_OBJ_ANY);
git_reset(m_repo.get(), obj, GIT_RESET_HARD, nullptr);
});
unpackSubfolders();
}
void Addon::reclone()
{
delegate("Reclone", [this](){
git_strarray remotes;
AutoPtr rr(&remotes, git_strarray_free);
check_git_return(git_remote_list(rr, m_repo.get()));
QStringList urls;
for (size_t i = 0; i < remotes.count; i++) {
AutoPtr remote(&git_remote_free);
check_git_return(git_remote_lookup(&remote, m_repo.get(), remotes.strings[i]));
urls << git_remote_url(remote);
}
const char *remote = nullptr;
check_git_return(git_branch_name(&remote, remoteRefForBranch(m_currentBranch)));
QString oldRemote(remote);
closeRepo();
if (removeFolder(m_path + "/" + name() + "/.git")){
git_repository *repo = nullptr;
check_git_return(git_repository_init(&repo, (m_path + "/" + m_name).toLocal8Bit(), false));
m_repo.reset(repo);
for (size_t i = 0; i < remotes.count; i++) {
AutoPtr remote(&git_remote_free);
check_git_return(git_remote_create(&remote, m_repo.get(), remotes.strings[i], urls[i].toLocal8Bit()));
git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT;
fetch_opts.callbacks.transfer_progress = &fetch_progress_cb;
fetch_opts.callbacks.payload = this;
AutoPtr r(git_remote_free);
check_git_return(git_remote_lookup(&r, m_repo.get(), git_remote_name(remote)));
check_git_return(git_remote_fetch(r,nullptr,&fetch_opts,nullptr));
}
setCurrentBranch(oldRemote);
return true;
}
return false;
},[this](bool ok){
if (ok) {
update();
scanBranches();
}
}, true);
}
void Addon::loadReadme()
{
delegate("Readme Loading",[this](){
QFileInfo md(m_path + "/" + m_name + "/README.md");
if (!md.exists() && !md.isFile()) {
QDir repodir(m_path + "/" + m_name);
auto a = repodir.entryInfoList({"readme*"});
if (a.size() > 0)
md = a[0];
}
if (md.exists() && md.isFile()) {
QFile file(md.absoluteFilePath());
if (file.open(QFile::ReadOnly))
return QString(file.readAll());
}
return QString("");
}, [this](auto readme){
setReadme(readme);
});
}
void Addon::setReadme(QString readme)
{
if (m_readme == readme)
return;
m_readme = readme;
emit readmeChanged(m_readme);
}
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;
}
QString Addon::readme() const
{
return m_readme;
}
QString Addon::path() const
{
return m_path;
}
QUrl Addon::getUrl(QString name) const
{
git_remote *remote = nullptr;
if (name.isNull())
name = m_remote;
if (git_remote_lookup(&remote, m_repo.get(), name.toLocal8Bit()))
return QUrl();
auto url = QUrl(git_remote_url(remote));
git_remote_free(remote);
return url;
}
void Addon::closeRepo()
{
m_repo.reset();
}
void Addon::openRepo()
{
git_repository *repo = nullptr;
git_repository_open(&repo, (m_path + "/" + m_name).toLocal8Bit());
m_repo.reset(repo);
}
AutoPtr Addon::remoteRefForBranch(QString branch)
{
AutoPtr lbr(&git_reference_free);
AutoPtr tr(&git_reference_free);
int error = check_git_return(git_branch_lookup(&lbr, m_repo.get(), branch.toLocal8Bit(), GIT_BRANCH_LOCAL));
if (error == GIT_ENOTFOUND)
check_git_return(git_branch_lookup(&lbr, m_repo.get(), branch.toLocal8Bit(), GIT_BRANCH_REMOTE));
else
check_git_return(git_branch_upstream(&tr, lbr));
if (!tr)
return lbr;
else
return tr;
}
AutoPtr Addon::branchRef(QString name)
{
AutoPtr lbr(&git_reference_free);
int error = check_git_return(git_branch_lookup(&lbr, m_repo.get(), name.toLocal8Bit(), GIT_BRANCH_LOCAL));
if (error == GIT_ENOTFOUND)
check_git_return(git_branch_lookup(&lbr, m_repo.get(), name.toLocal8Bit(), GIT_BRANCH_REMOTE));
return lbr;
}
void Addon::delegate(QString taskname, auto work, auto callback, bool force)
{
Q_ASSERT_X(QThread::currentThread() == thread(), "delegate", "Attempt to delegate from another thread.");
if (m_lock && !force) {
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);
m_lock = true;
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, taskname](){
m_lock = false;
if (fw->isCanceled()) {
try {
fw->waitForFinished();
} catch (const GitException &error) {
qCritical() << name() << "encountered an error while executing" << taskname << ": " << error.errorString();
setStatus(Status::Error);
setGitStatus(GitStatusFlag::Error);
}
return;
}
TaskQueue old;
m_tasks.swap(old);
if constexpr (std::is_same::value)
callback();
else
callback(fw->result());
m_tasks.append(old);
if (m_tasks.empty())
setStatus(Status::Ready);
else if (!m_lock)
m_tasks.dequeue().second();
});
connect(fw, &QFutureWatcher::finished, fw, &QFutureWatcher::deleteLater);
fw->setFuture(fut);
}
void Addon::delegate(QString taskname, auto work, bool force)
{
using ret_t = typename std::invoke_result::type;
if constexpr (std::is_same::value)
delegate(taskname, work, [](){}, force);
else
delegate(taskname, work, [](ret_t){}, force);
}
void Addon::delegate(QString taskname, auto work)
{
delegate(taskname, work, false);
}
void Addon::delegate(QString taskname, auto work, auto callback)
{
delegate(taskname, work, callback, false);
}