From 5e5a092da111693ca08e701373b6f1f29f6bcb24 Mon Sep 17 00:00:00 2001 From: Quentin Rouland Date: Fri, 10 Jan 2025 13:48:38 +0100 Subject: [PATCH] Initial git clone feature --- CMakeLists.txt | 7 ++- plugins/CMakeLists.txt | 1 + plugins/Git/CMakeLists.txt | 37 ++++++++++++ plugins/Git/git.cpp | 18 ++++++ plugins/Git/git.h | 20 +++++++ plugins/Git/libgit.cpp | 51 ++++++++++++++++ plugins/Git/libgit.h | 32 ++++++++++ plugins/Git/plugin.cpp | 10 ++++ plugins/Git/plugin.h | 16 +++++ plugins/Git/qmldir | 2 + plugins/Pass/CMakeLists.txt | 5 +- plugins/Pass/git.cpp | 32 ---------- plugins/Pass/git.h | 27 --------- plugins/Pass/gpg.h | 3 +- plugins/Pass/pass.cpp | 14 +---- plugins/Pass/pass.h | 8 ++- po/utpass.qrouland.pot | 42 +++++++++++-- qml/components/FileDir.qml | 3 - qml/dialogs/ErrorDialog.qml | 6 +- qml/pages/PasswordList.qml | 2 +- qml/pages/settings/ImportGitClone.qml | 85 +++++++++++++++++++++++++++ qml/pages/settings/Settings.qml | 4 ++ 22 files changed, 334 insertions(+), 91 deletions(-) create mode 100644 plugins/Git/CMakeLists.txt create mode 100644 plugins/Git/git.cpp create mode 100644 plugins/Git/git.h create mode 100644 plugins/Git/libgit.cpp create mode 100644 plugins/Git/libgit.h create mode 100644 plugins/Git/plugin.cpp create mode 100644 plugins/Git/plugin.h create mode 100644 plugins/Git/qmldir delete mode 100644 plugins/Pass/git.cpp delete mode 100644 plugins/Pass/git.h create mode 100644 qml/pages/settings/ImportGitClone.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 24aabec..161ee7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,10 @@ cmake_minimum_required(VERSION 3.5.1) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release") +endif() + execute_process( COMMAND dpkg-architecture -qDEB_HOST_ARCH OUTPUT_VARIABLE CLICK_ARCH @@ -31,7 +35,8 @@ set(DATA_DIR /) set(DESKTOP_FILE_NAME ${PROJECT_NAME}.desktop) -add_executable(${PROJECT_NAME} main.cpp) +add_executable(${PROJECT_NAME} main.cpp + qml/pages/settings/ImportGitClone.qml) qt5_use_modules(${PROJECT_NAME} Gui Qml Quick) install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 6b05459..d10ae09 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -1,2 +1,3 @@ +add_subdirectory(Git) add_subdirectory(Pass) add_subdirectory(Utils) diff --git a/plugins/Git/CMakeLists.txt b/plugins/Git/CMakeLists.txt new file mode 100644 index 0000000..3001484 --- /dev/null +++ b/plugins/Git/CMakeLists.txt @@ -0,0 +1,37 @@ +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") +set(PLUGIN "Git") + +set( + SRC + plugin.cpp + libgit.cpp + git.cpp + +) + +set(CMAKE_AUTOMOC ON) + +execute_process( + COMMAND dpkg-architecture -qDEB_HOST_MULTIARCH + OUTPUT_VARIABLE ARCH_TRIPLET + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +if(ARCH_TRIPLET STREQUAL "") + set(ARCH_TRIPLET x86_64-linux-gnu) +endif() + +add_library(${PLUGIN} MODULE ${SRC}) +set_target_properties(${PLUGIN} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PLUGIN}) +qt5_use_modules(${PLUGIN} Qml Quick DBus) + + +add_library(libgit2 SHARED IMPORTED) +set_property(TARGET libgit2 PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libgit2.so") + +target_link_libraries(${PLUGIN} libgit2) + + +set(QT_IMPORTS_DIR "/lib/${ARCH_TRIPLET}") +install(TARGETS ${PLUGIN} DESTINATION ${QT_IMPORTS_DIR}/${PLUGIN}/) +install(FILES qmldir DESTINATION ${QT_IMPORTS_DIR}/${PLUGIN}/) diff --git a/plugins/Git/git.cpp b/plugins/Git/git.cpp new file mode 100644 index 0000000..66fdcc0 --- /dev/null +++ b/plugins/Git/git.cpp @@ -0,0 +1,18 @@ +#include +#include +#include + +#include "git.h" +#include "libgit.h" + + +Git::Git() +{} + +bool Git::clone(QString url, QString path) +{ + qInfo() << "Cloning " << url << "password_store to " << path; + QDir dir(path); + dir.removeRecursively(); // TODO see if we delete only after sucessfull clone / Will be change anyway when will add update etc.. + return LibGit::instance()->clone(url, path); +} diff --git a/plugins/Git/git.h b/plugins/Git/git.h new file mode 100644 index 0000000..1927a11 --- /dev/null +++ b/plugins/Git/git.h @@ -0,0 +1,20 @@ +#ifndef GIT_H +#define GIT_H + +#include +#include + + +class Git : public QObject +{ + Q_OBJECT + +public: + Git(); + ~Git() override = default; + + Q_INVOKABLE bool clone(QString url, QString path); + // Q_INVOKABLE bool update(QUrl url, QString path); +}; + +#endif diff --git a/plugins/Git/libgit.cpp b/plugins/Git/libgit.cpp new file mode 100644 index 0000000..5f873ad --- /dev/null +++ b/plugins/Git/libgit.cpp @@ -0,0 +1,51 @@ +#include +#include +extern "C" { +#include +} + +#include "libgit.h" + + + +LibGit::LibGit() +{ + git_libgit2_init(); +} + +LibGit::~LibGit() { + git_libgit2_shutdown(); +} + +int LibGit::credentials_cb(git_cred **out, const char *url, const char *username_from_url, + unsigned int allowed_types, void *payload) +{ + int error; + const char *user, *pass; + + /* + * Ask the user via the UI. On error, store the information and return GIT_EUSER which will be + * bubbled up to the code performing the fetch or push. Using GIT_EUSER allows the application + * to know it was an error from the application instead of libgit2. + */ + // if ((error = ask_user(&user, &pass, url, username_from_url, allowed_types)) < 0) { + // store_error(error); + // return GIT_EUSER; + // } + // user = "user"; + // pass = "pass"; + // return git_cred_userpass_plaintext_new(out, user, pass); + return GIT_EUSER; +} + +bool LibGit::clone(QString url, QString path) { + qDebug("yo"); + git_repository *repo = NULL; + git_clone_options opts = GIT_CLONE_OPTIONS_INIT; + opts.fetch_opts.callbacks.credentials = *credentials_cb; + int ret = git_clone(&repo, url.toLocal8Bit().data(), path.toLocal8Bit().data(), &opts); + if (repo) { + git_repository_free(repo); + } + return ret == 0; // TODO Clean error handling to return specifics errors for the ui +} diff --git a/plugins/Git/libgit.h b/plugins/Git/libgit.h new file mode 100644 index 0000000..08ee5d7 --- /dev/null +++ b/plugins/Git/libgit.h @@ -0,0 +1,32 @@ +#ifndef LIBGIT_H +#define LIBGIT_H + +#include +#include +extern "C" { +#include +} +#include + +class LibGit +{ +private: + LibGit(); + static int credentials_cb(git_cred **out, const char *url, const char *username_from_url, + unsigned int allowed_types, void *payload); + + +public: + ~LibGit(); + static std::shared_ptr instance() + { + static std::shared_ptr s{new LibGit}; + return s; + } + LibGit(LibGit const &) = delete; + void operator=(LibGit const &) = delete; + + bool clone(QString url, QString path); +}; + +#endif diff --git a/plugins/Git/plugin.cpp b/plugins/Git/plugin.cpp new file mode 100644 index 0000000..51166ec --- /dev/null +++ b/plugins/Git/plugin.cpp @@ -0,0 +1,10 @@ +#include + +#include "plugin.h" +#include "git.h" + +void GitPlugin::registerTypes(const char *uri) +{ + //@uri Git + qmlRegisterSingletonType(uri, 1, 0, "Git", [](QQmlEngine *, QJSEngine *) -> QObject * { return new Git; }); +} diff --git a/plugins/Git/plugin.h b/plugins/Git/plugin.h new file mode 100644 index 0000000..cce3028 --- /dev/null +++ b/plugins/Git/plugin.h @@ -0,0 +1,16 @@ +#ifndef GITPLUGIN_H +#define GITPLUGIN_H + +#include + +class GitPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID + "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; + +#endif diff --git a/plugins/Git/qmldir b/plugins/Git/qmldir new file mode 100644 index 0000000..5e2e41c --- /dev/null +++ b/plugins/Git/qmldir @@ -0,0 +1,2 @@ +module Git +plugin Git diff --git a/plugins/Pass/CMakeLists.txt b/plugins/Pass/CMakeLists.txt index 59703fa..a650a9d 100644 --- a/plugins/Pass/CMakeLists.txt +++ b/plugins/Pass/CMakeLists.txt @@ -4,7 +4,6 @@ set(PLUGIN "Pass") set( SRC plugin.cpp - git.cpp pass.cpp gpg.cpp passkeymodel.h @@ -43,10 +42,8 @@ set_property(TARGET libgpgmepp PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPL add_library(libqgpgme SHARED IMPORTED) set_property(TARGET libqgpgme PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libqgpgme.so") -add_library(libgit2 SHARED IMPORTED) -set_property(TARGET libgit2 PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libgit2.so") -target_link_libraries(${PLUGIN} gpgerror libassuan libgpgme libgpgmepp libqgpgme libgit2) +target_link_libraries(${PLUGIN} gpgerror libassuan libgpgme libgpgmepp libqgpgme) set(QT_IMPORTS_DIR "/lib/${ARCH_TRIPLET}") diff --git a/plugins/Pass/git.cpp b/plugins/Pass/git.cpp deleted file mode 100644 index a2a2cc5..0000000 --- a/plugins/Pass/git.cpp +++ /dev/null @@ -1,32 +0,0 @@ - - - -#include -#include -extern "C" { -#include -} - -#include "git.h" - - - -Git::Git() -{ - git_libgit2_init(); -} - -Git::~Git() { - git_libgit2_shutdown(); -} - -bool Git::clone(QString url, QString path) { - git_repository *repo = NULL; - git_clone_options opts = GIT_CLONE_OPTIONS_INIT; - - int ret = git_clone(&repo, url.toLocal8Bit().data(), path.toLocal8Bit().data(), &opts); - if (repo) { - git_repository_free(repo); - } - return ret == 0; // TODO better error handling to return specifics errors for the ui -} diff --git a/plugins/Pass/git.h b/plugins/Pass/git.h deleted file mode 100644 index f7f302f..0000000 --- a/plugins/Pass/git.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef GIT_H -#define GIT_H - -#include -#include -#include - -class Git -{ -private: - Git(); -public: - - ~Git(); - - static std::shared_ptr instance() - { - static std::shared_ptr s{new Git}; - return s; - } - Git(Git const &) = delete; - void operator=(Git const &) = delete; - - bool clone(QString url, QString path); -}; - -#endif diff --git a/plugins/Pass/gpg.h b/plugins/Pass/gpg.h index 2b81897..a307ed1 100644 --- a/plugins/Pass/gpg.h +++ b/plugins/Pass/gpg.h @@ -13,6 +13,7 @@ class Gpg { private: Gpg(); + QObject *m_window; QString findCommandPath(const QString &command); @@ -42,7 +43,7 @@ public: }; - QPair< Error, std::vector< Key > > getAllKeys(bool remote = false, bool include_sigs = {}, bool + QPair> getAllKeys(bool remote = false, bool include_sigs = {}, bool validate = false); QPair> getKeys( QString pattern_uid, bool remote = false, bool include_sigs = false, diff --git a/plugins/Pass/pass.cpp b/plugins/Pass/pass.cpp index df5bdfc..50e1d63 100644 --- a/plugins/Pass/pass.cpp +++ b/plugins/Pass/pass.cpp @@ -3,7 +3,6 @@ #include #include "pass.h" -#include "git.h" #include "gpg.h" #include "passkeymodel.h" @@ -22,8 +21,9 @@ void Pass::init(QObject *window) Gpg::instance()->setWindow(window); QDir dir(m_password_store); - if (!dir.exists()) + if (!dir.exists()) { dir.mkpath("."); + } qInfo() << "Password Store is :" << m_password_store; } @@ -62,13 +62,3 @@ QVariant Pass::gpgGetAllKeysModel() Gpg::instance()->getAllKeys().second)); } -QString Pass::getPasswordStore() -{ - return m_password_store; -} - -bool Pass::gitClone(QString url) -{ - qInfo() << "Cloning . password_store from " << url; - return Git::instance()->clone(url, m_password_store); -} diff --git a/plugins/Pass/pass.h b/plugins/Pass/pass.h index fdbc7ed..0598b90 100644 --- a/plugins/Pass/pass.h +++ b/plugins/Pass/pass.h @@ -9,6 +9,9 @@ class Pass : public QObject { Q_OBJECT + Q_PROPERTY(QString password_store READ password_store) + +private: QString m_password_store; signals: @@ -16,18 +19,17 @@ signals: void decryptCanceled(); void decryptFailed(); - public: Pass(); ~Pass() override = default; + QString password_store() const { return m_password_store; } + Q_INVOKABLE void init(QObject *window); - Q_INVOKABLE QString getPasswordStore(); Q_INVOKABLE void decrypt(QUrl url); Q_INVOKABLE bool gpgDeleteKeyId(QString id); Q_INVOKABLE bool gpgImportKeyFromFile(QUrl url); Q_INVOKABLE QVariant gpgGetAllKeysModel(); - Q_INVOKABLE bool gitClone(QString url); }; #endif diff --git a/po/utpass.qrouland.pot b/po/utpass.qrouland.pot index 5a4b82c..0209db2 100644 --- a/po/utpass.qrouland.pot +++ b/po/utpass.qrouland.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: utpass.qrouland\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-07 13:36+0000\n" +"POT-Creation-Date: 2025-01-10 13:46+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -37,8 +37,8 @@ msgstr "" msgid "Error !" msgstr "" -#: ../qml/dialogs/ErrorDialog.qml:15 ../qml/dialogs/SuccessDialog.qml:15 -msgid "OK" +#: ../qml/dialogs/ErrorDialog.qml:15 +msgid "Close" msgstr "" #: ../qml/dialogs/PassphraseDialog.qml:7 @@ -57,6 +57,10 @@ msgstr "" msgid "Success !" msgstr "" +#: ../qml/dialogs/SuccessDialog.qml:15 +msgid "OK" +msgstr "" + #: ../qml/pages/Info.qml:11 ../qml/pages/headers/MainHeader.qml:58 msgid "Info" msgstr "" @@ -103,6 +107,32 @@ msgstr "" msgid "Settings" msgstr "" +#: ../qml/pages/settings/ImportGitClone.qml:15 +msgid "Git Clone Import" +msgstr "" + +#: ../qml/pages/settings/ImportGitClone.qml:36 +msgid "Repo Url" +msgstr "" + +#: ../qml/pages/settings/ImportGitClone.qml:44 +msgid "Git repo url" +msgstr "" + +#: ../qml/pages/settings/ImportGitClone.qml:50 +msgid "Clone" +msgstr "" + +#: ../qml/pages/settings/ImportGitClone.qml:68 +msgid "" +"Importing a git repo will delete
any existing password store!" +"
Continue ?" +msgstr "" + +#: ../qml/pages/settings/ImportGitClone.qml:78 +msgid "An error occured during git clone !" +msgstr "" + #: ../qml/pages/settings/ImportKeyFile.qml:17 msgid "GPG Key Import" msgstr "" @@ -177,9 +207,13 @@ msgid "Password Store" msgstr "" #: ../qml/pages/settings/Settings.qml:47 +msgid "Import a Password Store using Git" +msgstr "" + +#: ../qml/pages/settings/Settings.qml:51 msgid "Import a Password Store Zip" msgstr "" -#: ../qml/pages/settings/Settings.qml:56 +#: ../qml/pages/settings/Settings.qml:60 msgid "Warning: importing delete any exiting Password Store" msgstr "" diff --git a/qml/components/FileDir.qml b/qml/components/FileDir.qml index b6c09f1..8633374 100644 --- a/qml/components/FileDir.qml +++ b/qml/components/FileDir.qml @@ -69,9 +69,6 @@ Component { id: passwordPageDecryptError ErrorDialog { textError: i18n.tr("Decryption failed !") - onDialogClosed: { - pageStack.pop() - } } } } diff --git a/qml/dialogs/ErrorDialog.qml b/qml/dialogs/ErrorDialog.qml index 960ac86..eabe3bb 100644 --- a/qml/dialogs/ErrorDialog.qml +++ b/qml/dialogs/ErrorDialog.qml @@ -3,7 +3,7 @@ import Lomiri.Components 1.3 import Lomiri.Components.Popups 1.3 Dialog { - id: dialogSuccess + id: dialogError property string textError @@ -12,11 +12,11 @@ Dialog { title: i18n.tr("Error !") text: textError Button { - text: i18n.tr("OK") + text: i18n.tr("Close") color: LomiriColors.red onClicked: function () { dialogClosed() - PopupUtils.close(dialogSuccess) + PopupUtils.close(dialogError) } } } diff --git a/qml/pages/PasswordList.qml b/qml/pages/PasswordList.qml index 5a7d4e8..83ef4bb 100644 --- a/qml/pages/PasswordList.qml +++ b/qml/pages/PasswordList.qml @@ -67,6 +67,6 @@ Page { } Component.onCompleted: { - passwordStorePath = "file:" + Pass.getPasswordStore() + passwordStorePath = "file:" + Pass.password_store } } diff --git a/qml/pages/settings/ImportGitClone.qml b/qml/pages/settings/ImportGitClone.qml new file mode 100644 index 0000000..b589675 --- /dev/null +++ b/qml/pages/settings/ImportGitClone.qml @@ -0,0 +1,85 @@ +import QtQuick 2.4 +import Lomiri.Components 1.3 +import Lomiri.Components.Popups 1.3 +import Git 1.0 +import Pass 1.0 +import "../headers" +import "../../components" +import "../../dialogs" + +Page { + id: importGitClonePage + + header: StackHeader { + id: importGitCloneHeader + title: i18n.tr('Git Clone Import') + } + + Flow { + anchors.top: importGitCloneHeader.bottom + anchors.right: parent.right + anchors.left: parent.left + anchors.leftMargin: units.gu(2) + anchors.rightMargin: units.gu(2) + spacing: units.gu(1) + + Rectangle { + width: parent.width + height: units.gu(1) + } + + Text { + id: repoUrlLabe + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + width: parent.width + text: i18n.tr('Repo Url') + } + + TextField { + id: textFieldInput + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + width: parent.width + placeholderText: i18n.tr('Git repo url') + } + + Button { + id: buttonAdd + width: parent.width + text: i18n.tr('Clone') + onClicked: { + var ret = Git.clone(textFieldInput.text, Pass.password_store) + if(ret) { + pageStack.pop() + } else { + PopupUtils.open(importGitCloneError, importGitClonePage) + } + + + } + } + } + + Component { + id: importGitCloneValidation + SimpleValidationDialog { + text: i18n.tr( + "Importing a git repo will delete
any existing password store!
Continue ?") + onCanceled: { + pageStack.pop() + } + } + } + + Component { + id: importGitCloneError + ErrorDialog { + textError: i18n.tr("An error occured during git clone !") + } + } + + Component.onCompleted: { + PopupUtils.open(importGitCloneValidation, importGitClonePage) + } +} diff --git a/qml/pages/settings/Settings.qml b/qml/pages/settings/Settings.qml index 06e2943..20f789c 100644 --- a/qml/pages/settings/Settings.qml +++ b/qml/pages/settings/Settings.qml @@ -42,6 +42,10 @@ Page { height: units.gu(4) text: i18n.tr('Password Store') } + PageStackLink { + page: Qt.resolvedUrl("ImportGitClone.qml") + text: i18n.tr('Import a Password Store using Git') + } PageStackLink { page: Qt.resolvedUrl("ImportZip.qml") text: i18n.tr('Import a Password Store Zip')