diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..87cf1fb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,39 @@ +name: Build Github++ + +on: + push: + branches: master + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: 6.7.0 + modules: + cache: true + + - uses: actions/checkout@v4 + + - name: Configure Github++ build + shell: pwsh + run: | + cmake --preset vs2022-windows ` + "-DCMAKE_PREFIX_PATH=${env:QT_ROOT_DIR}\msvc2019_64" ` + "-DCMAKE_INSTALL_PREFIX=install" + + - name: Build Github++ + run: cmake --build vsbuild --config RelWithDebInfo + + - name: Install Github++ + run: cmake --install vsbuild --config RelWithDebInfo + + - name: Upload Github++ artifact + uses: actions/upload-artifact@master + with: + name: githubpp + path: ./install diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 0000000..8e5defa --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Github++ + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/CMakeLists.txt b/CMakeLists.txt index 85731fe..e2c515f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,27 @@ cmake_minimum_required(VERSION 3.16) -if(DEFINED DEPENDENCIES_DIR) - include(${DEPENDENCIES_DIR}/modorganizer_super/cmake_common/mo2.cmake) -else() - include(${CMAKE_CURRENT_LIST_DIR}/../cmake_common/mo2.cmake) -endif() +include(CMakePackageConfigHelpers) project(githubpp) + add_subdirectory(src) + +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/mo2-githubpp-config.cmake" + INSTALL_DESTINATION "lib/cmake/mo2-githubpp" + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO +) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/mo2-githubpp-config-version.cmake" + VERSION "0.0.1" + COMPATIBILITY AnyNewerVersion + ARCH_INDEPENDENT +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/mo2-githubpp-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/mo2-githubpp-config-version.cmake + DESTINATION lib/cmake/mo2-githubpp +) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..fb2f5ab --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,36 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4" + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev"], + "name": "vs2022-windows", + "toolset": "v143" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/cmake/config.cmake.in b/cmake/config.cmake.in new file mode 100644 index 0000000..12e7d81 --- /dev/null +++ b/cmake/config.cmake.in @@ -0,0 +1,3 @@ +@PACKAGE_INIT@ + +include ( "${CMAKE_CURRENT_LIST_DIR}/mo2-githubpp-targets.cmake" ) diff --git a/src/github.h b/include/githubpp/github.h similarity index 59% rename from src/github.h rename to include/githubpp/github.h index b8d5ffb..0085fd5 100644 --- a/src/github.h +++ b/include/githubpp/github.h @@ -1,32 +1,28 @@ #pragma once -#include -#include +#include #include #include -#include -#include +#include +#include #include +#include #include class GitHubException : public std::exception { public: - GitHubException(const QJsonObject &errorObj) - : std::exception() + GitHubException(const QJsonObject& errorObj) : std::exception() { initMessage(errorObj); } virtual ~GitHubException() throw() override {} - virtual const char *what() const throw() - { - return m_Message.constData(); - } + virtual const char* what() const throw() { return m_Message.constData(); } private: - void initMessage(const QJsonObject &obj) + void initMessage(const QJsonObject& obj) { if (obj.contains("http_status")) { m_Message = QString("HTTP Status %1: %2") @@ -55,48 +51,51 @@ class GitHub : public QObject Q_OBJECT public: - enum class Method { GET, POST }; + enum class Method + { + GET, + POST + }; - struct Repository { - Repository(const QString &owner, const QString &project) - : owner(owner) - , project(project) - { - } + struct Repository + { + Repository(const QString& owner, const QString& project) + : owner(owner), project(project) + {} QString owner; QString project; }; public: - GitHub(const char *clientId = nullptr); + GitHub(const char* clientId = nullptr); ~GitHub(); - QJsonArray releases(const Repository &repo); - void releases(const Repository &repo, - const std::function &callback); + QJsonArray releases(const Repository& repo); + void releases(const Repository& repo, + const std::function& callback); private: - QJsonDocument request(Method method, const QString &path, - const QByteArray &data, bool relative); - void request(Method method, const QString &path, const QByteArray &data, - const std::function &callback, + QJsonDocument request(Method method, const QString& path, const QByteArray& data, + bool relative); + void request(Method method, const QString& path, const QByteArray& data, + const std::function& callback, bool relative); - QJsonDocument handleReply(QNetworkReply *reply); - QNetworkReply *genReply(Method method, const QString &path, - const QByteArray &data, bool relative); + QJsonDocument handleReply(QNetworkReply* reply); + QNetworkReply* genReply(Method method, const QString& path, const QByteArray& data, + bool relative); private: struct Request { Method method = Method::GET; QByteArray data; - std::function callback; - QTimer* timer = nullptr; + std::function callback; + QTimer* timer = nullptr; QNetworkReply* reply = nullptr; }; - QNetworkAccessManager *m_AccessManager; + QNetworkAccessManager* m_AccessManager; // remember the replies that are in flight and delete them in the destructor std::vector m_replies; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 243240c..4dcfa11 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,8 +1,51 @@ cmake_minimum_required(VERSION 3.16) +find_package(Qt6 CONFIG REQUIRED COMPONENTS Network) + add_library(githubpp STATIC) -mo2_configure_library(githubpp - WARNINGS OFF + +target_sources(githubpp + PRIVATE + github.cpp + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_LIST_DIR}/../include + FILES ${CMAKE_CURRENT_LIST_DIR}/../include/githubpp/github.h +) + +target_link_libraries(githubpp PRIVATE Qt::Core Qt::Network) +set_target_properties(githubpp + PROPERTIES AUTOMOC ON - PRIVATE_DEPENDS Qt::Core Qt::Network) -mo2_install_target(githubpp) + CXX_STANDARD 20 +) +target_include_directories(githubpp PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../include/githubpp) + +if (MSVC) + target_compile_options(githubpp + PRIVATE + "/MP" + "/Wall" + "/external:anglebrackets" + "/external:W0" + ) + target_link_options(githubpp + PRIVATE + $<$:/LTCG /INCREMENTAL:NO /OPT:REF /OPT:ICF> + ) + + set_target_properties(githubpp PROPERTIES + VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin + VS_DEBUGGER_COMMAND ${CMAKE_INSTALL_PREFIX}/bin/ModOrganizer.exe + VS_STARTUP_PROJECT githubpp) +endif() + +add_library(mo2::githubpp ALIAS githubpp) + +# installation +install(TARGETS githubpp EXPORT githubppTargets FILE_SET HEADERS) +install(EXPORT githubppTargets + FILE mo2-githubpp-targets.cmake + NAMESPACE mo2:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mo2-githubpp +) diff --git a/src/github.cpp b/src/github.cpp index 6ecfee4..1c91cfa 100644 --- a/src/github.cpp +++ b/src/github.cpp @@ -1,17 +1,16 @@ -#include +#include "github.h" #include +#include #include -#include "github.h" -#include #include +#include static const QString GITHUB_URL("https://api.github.com"); static const QString USER_AGENT("GitHubPP"); - -GitHub::GitHub(const char* clientId) - : m_AccessManager(new QNetworkAccessManager(this)) {} +GitHub::GitHub(const char* clientId) : m_AccessManager(new QNetworkAccessManager(this)) +{} GitHub::~GitHub() { @@ -23,43 +22,40 @@ GitHub::~GitHub() } } -QJsonArray GitHub::releases(const Repository &repo) +QJsonArray GitHub::releases(const Repository& repo) { - QJsonDocument result - = request(Method::GET, - QString("repos/%1/%2/releases").arg(repo.owner, repo.project), - QByteArray(), - true); + QJsonDocument result = request( + Method::GET, QString("repos/%1/%2/releases").arg(repo.owner, repo.project), + QByteArray(), true); return result.array(); } -void GitHub::releases(const Repository &repo, - const std::function &callback) +void GitHub::releases(const Repository& repo, + const std::function& callback) { - request(Method::GET, - QString("repos/%1/%2/releases").arg(repo.owner, repo.project), - QByteArray(), [callback](const QJsonDocument &result) { - callback(result.array()); - }, true); + request( + Method::GET, QString("repos/%1/%2/releases").arg(repo.owner, repo.project), + QByteArray(), + [callback](const QJsonDocument& result) { + callback(result.array()); + }, + true); } -QJsonDocument GitHub::handleReply(QNetworkReply *reply) +QJsonDocument GitHub::handleReply(QNetworkReply* reply) { - int statusCode - = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (statusCode != 200) { return QJsonDocument(QJsonObject( {{"http_status", statusCode}, {"redirection", - reply->attribute(QNetworkRequest::RedirectionTargetAttribute) - .toString()}, - {"reason", reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute) - .toString()}})); + reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString()}, + {"reason", + reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString()}})); } QByteArray data = reply->readAll(); - if (data.isNull() || data.isEmpty() - || (strcmp(data.constData(), "null") == 0)) { + if (data.isNull() || data.isEmpty() || (strcmp(data.constData(), "null") == 0)) { return QJsonDocument(); } @@ -67,15 +63,14 @@ QJsonDocument GitHub::handleReply(QNetworkReply *reply) QJsonDocument result = QJsonDocument::fromJson(data, &parseError); if (parseError.error != QJsonParseError::NoError) { - return QJsonDocument( - QJsonObject({{"parse_error", parseError.errorString()}})); + return QJsonDocument(QJsonObject({{"parse_error", parseError.errorString()}})); } return result; } -QNetworkReply *GitHub::genReply(Method method, const QString &path, - const QByteArray &data, bool relative) +QNetworkReply* GitHub::genReply(Method method, const QString& path, + const QByteArray& data, bool relative) { QNetworkRequest request(relative ? GITHUB_URL + "/" + path : path); @@ -83,21 +78,21 @@ QNetworkReply *GitHub::genReply(Method method, const QString &path, request.setRawHeader("Accept", "application/vnd.github.v3+json"); switch (method) { - case Method::GET: - return m_AccessManager->get(request); - case Method::POST: - return m_AccessManager->post(request, data); - default: - // this shouldn't be possible as all enum options are handled - throw std::runtime_error("invalid method"); + case Method::GET: + return m_AccessManager->get(request); + case Method::POST: + return m_AccessManager->post(request, data); + default: + // this shouldn't be possible as all enum options are handled + throw std::runtime_error("invalid method"); } } -QJsonDocument GitHub::request(Method method, const QString &path, - const QByteArray &data, bool relative) +QJsonDocument GitHub::request(Method method, const QString& path, + const QByteArray& data, bool relative) { QEventLoop wait; - QNetworkReply *reply = genReply(method, path, data, relative); + QNetworkReply* reply = genReply(method, path, data, relative); connect(reply, SIGNAL(finished), &wait, SLOT(quit())); wait.exec(); @@ -112,18 +107,18 @@ QJsonDocument GitHub::request(Method method, const QString &path, } } -void GitHub::request(Method method, const QString &path, const QByteArray &data, - const std::function &callback, +void GitHub::request(Method method, const QString& path, const QByteArray& data, + const std::function& callback, bool relative) { // make sure the timer is owned by this so it's deleted correctly and // doesn't fire after the GitHub object is destroyed; this happens when // restarting MO by switching instances, for example - QTimer *timer = new QTimer(this); + QTimer* timer = new QTimer(this); timer->setSingleShot(true); timer->setInterval(10000); - QNetworkReply *reply = genReply(method, path, data, relative); + QNetworkReply* reply = genReply(method, path, data, relative); // remember this reply so it can be deleted in the destructor if necessary m_replies.push_back(reply); @@ -131,15 +126,20 @@ void GitHub::request(Method method, const QString &path, const QByteArray &data, Request req = {method, data, callback, timer, reply}; // finished - connect(reply, &QNetworkReply::finished, [this, req]{ onFinished(req); }); + connect(reply, &QNetworkReply::finished, [this, req] { + onFinished(req); + }); // error - connect( - reply, qOverload(&QNetworkReply::errorOccurred), - [this, req](auto&& error){ onError(req, error); }); + connect(reply, qOverload(&QNetworkReply::errorOccurred), + [this, req](auto&& error) { + onError(req, error); + }); // timeout - connect(timer, &QTimer::timeout, [this, req]{ onTimeout(req); }); + connect(timer, &QTimer::timeout, [this, req] { + onTimeout(req); + }); timer->start(); } @@ -147,14 +147,13 @@ void GitHub::request(Method method, const QString &path, const QByteArray &data, void GitHub::onFinished(const Request& req) { QJsonDocument result = handleReply(req.reply); - QJsonObject object = result.object(); + QJsonObject object = result.object(); req.timer->stop(); if (object.value("http_status").toInt() == 301) { - request( - req.method, object.value("redirection").toString(), - req.data, req.callback, false); + request(req.method, object.value("redirection").toString(), req.data, req.callback, + false); } else { req.callback(result); } @@ -168,8 +167,8 @@ void GitHub::onError(const Request& req, QNetworkReply::NetworkError error) // already logs a message if (error != QNetworkReply::OperationCanceledError) { qCritical().noquote().nospace() - << "Github: request for " << req.reply->url().toString() << " failed, " - << req.reply->errorString() << " (" << error << ")"; + << "Github: request for " << req.reply->url().toString() << " failed, " + << req.reply->errorString() << " (" << error << ")"; } req.timer->stop(); @@ -186,7 +185,7 @@ void GitHub::onError(const Request& req, QNetworkReply::NetworkError error) void GitHub::onTimeout(const Request& req) { qCritical().noquote().nospace() - << "Github: request for " << req.reply->url().toString() << " timed out"; + << "Github: request for " << req.reply->url().toString() << " timed out"; // don't delete the reply, abort will fire the error() handler above req.reply->abort();