diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp index 8f6b94408..1ffb88708 100644 --- a/src/jamidht/conversationrepository.cpp +++ b/src/jamidht/conversationrepository.cpp @@ -22,12 +22,17 @@ #include "jamiaccount.h" #include "fileutils.h" #include "gittransport.h" +#include "string_utils.h" -#include using random_device = dht::crypto::random_device; #include #include +#include +#include + +using namespace std::string_view_literals; +constexpr auto DIFF_REGEX = " +\\| +[0-9]+.*"sv; namespace jami { @@ -57,6 +62,12 @@ public: bool mergeFastforward(const git_oid* target_oid, int is_unborn); bool createMergeCommit(git_index* index, const std::string& wanted_ref); + bool add(const std::string& path); + std::string commit(const std::string& msg); + + GitDiff diff(const std::string& idNew, const std::string& idOld) const; + std::string diffStats(const GitDiff& diff) const; + std::weak_ptr account_; const std::string id_; GitRepository repository_ {nullptr, git_repository_free}; @@ -76,8 +87,7 @@ create_empty_repository(const std::string& path) git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT; opts.flags |= GIT_REPOSITORY_INIT_MKPATH; opts.initial_head = "main"; - if (git_repository_init_ext(&repo, path.c_str(), &opts) - < 0) { + if (git_repository_init_ext(&repo, path.c_str(), &opts) < 0) { JAMI_ERR("Couldn't create a git repository in %s", path.c_str()); } return {std::move(repo), git_repository_free}; @@ -286,8 +296,7 @@ ConversationRepository::Impl::signature() JAMI_ERR("Unable to create a commit signature."); return {nullptr, git_signature_free}; } - GitSignature sig {sig_ptr, git_signature_free}; - return std::move(sig); + return {sig_ptr, git_signature_free}; } bool @@ -478,6 +487,203 @@ ConversationRepository::Impl::mergeFastforward(const git_oid* target_oid, int is return 0; } +bool +ConversationRepository::Impl::add(const std::string& path) +{ + if (!repository_) + return false; + git_index* index_ptr = nullptr; + if (git_repository_index(&index_ptr, repository_.get()) < 0) { + JAMI_ERR("Could not open repository index"); + return false; + } + GitIndex index {index_ptr, git_index_free}; + if (git_index_add_bypath(index.get(), path.c_str()) != 0) { + const git_error* err = giterr_last(); + if (err) + JAMI_ERR("Error when adding file: %s", err->message); + return false; + } + return git_index_write(index.get()) == 0; +} + +std::string +ConversationRepository::Impl::commit(const std::string& msg) +{ + if (!repository_) + return {}; + + auto account = account_.lock(); + if (!account) + return {}; + auto deviceId = std::string(account->currentDeviceId()); + auto name = account->getDisplayName(); + if (name.empty()) + name = deviceId; + + git_signature* sig_ptr = nullptr; + // Sign commit's buffer + if (git_signature_new(&sig_ptr, name.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) { + JAMI_ERR("Unable to create a commit signature."); + return {}; + } + GitSignature sig {sig_ptr, git_signature_free}; + + // Retrieve current HEAD + git_oid commit_id; + if (git_reference_name_to_id(&commit_id, repository_.get(), "HEAD") < 0) { + JAMI_ERR("Cannot get reference for HEAD"); + return {}; + } + + git_commit* head_ptr = nullptr; + if (git_commit_lookup(&head_ptr, repository_.get(), &commit_id) < 0) { + JAMI_ERR("Could not look up HEAD commit"); + return {}; + } + GitCommit head_commit {head_ptr, git_commit_free}; + + git_tree* tree_ptr = nullptr; + if (git_commit_tree(&tree_ptr, head_commit.get()) < 0) { + JAMI_ERR("Could not look up initial tree"); + return {}; + } + GitTree tree {tree_ptr, git_tree_free}; + + git_buf to_sign = {}; + const git_commit* head_ref[1] = {head_commit.get()}; + if (git_commit_create_buffer(&to_sign, + repository_.get(), + sig.get(), + sig.get(), + nullptr, + msg.c_str(), + tree.get(), + 1, + &head_ref[0]) + < 0) { + JAMI_ERR("Could not create commit buffer"); + return {}; + } + + // git commit -S + auto to_sign_vec = std::vector(to_sign.ptr, to_sign.ptr + to_sign.size); + auto signed_buf = account->identity().first->sign(to_sign_vec); + std::string signed_str = base64::encode(signed_buf); + if (git_commit_create_with_signature(&commit_id, + repository_.get(), + to_sign.ptr, + signed_str.c_str(), + "signature") + < 0) { + JAMI_ERR("Could not sign commit"); + return {}; + } + + // Move commit to main branch + git_reference* ref_ptr = nullptr; + if (git_reference_create(&ref_ptr, repository_.get(), "refs/heads/main", &commit_id, true, nullptr) + < 0) { + JAMI_WARN("Could not move commit to main"); + } + git_reference_free(ref_ptr); + + auto commit_str = git_oid_tostr_s(&commit_id); + if (commit_str) { + JAMI_INFO("New message added with id: %s", commit_str); + } + return commit_str ? commit_str : ""; +} + +GitDiff +ConversationRepository::Impl::diff(const std::string& idNew, const std::string& idOld) const +{ + if (!repository_) + return {nullptr, git_diff_free}; + + // Retrieve tree for commit new + git_oid oid; + git_commit* commitNew = nullptr; + if (idNew == "HEAD") { + JAMI_ERR("@@@ HEAD"); + if (git_reference_name_to_id(&oid, repository_.get(), "HEAD") < 0) { + JAMI_ERR("Cannot get reference for HEAD"); + return {nullptr, git_diff_free}; + } + + if (git_commit_lookup(&commitNew, repository_.get(), &oid) < 0) { + JAMI_ERR("Could not look up HEAD commit"); + return {nullptr, git_diff_free}; + } + } else { + if (git_oid_fromstr(&oid, idNew.c_str()) < 0 + || git_commit_lookup(&commitNew, repository_.get(), &oid) < 0) { + JAMI_WARN("Failed to look up commit %s", idNew.c_str()); + return {nullptr, git_diff_free}; + } + } + GitCommit new_commit = {commitNew, git_commit_free}; + + git_tree* tNew = nullptr; + if (git_commit_tree(&tNew, new_commit.get()) < 0) { + JAMI_ERR("Could not look up initial tree"); + return {nullptr, git_diff_free}; + } + GitTree treeNew = {tNew, git_tree_free}; + + git_diff* diff_ptr = nullptr; + if (idOld.empty()) { + if (git_diff_tree_to_tree(&diff_ptr, repository_.get(), nullptr, treeNew.get(), {}) < 0) { + JAMI_ERR("Could not get diff to empty repository"); + return {nullptr, git_diff_free}; + } + return {diff_ptr, git_diff_free}; + } + + // Retrieve tree for commit old + git_commit* commitOld = nullptr; + if (git_oid_fromstr(&oid, idOld.c_str()) < 0 + || git_commit_lookup(&commitOld, repository_.get(), &oid) < 0) { + JAMI_WARN("Failed to look up commit %s", idOld.c_str()); + return {nullptr, git_diff_free}; + } + GitCommit old_commit {commitOld, git_commit_free}; + + git_tree* tOld = nullptr; + if (git_commit_tree(&tOld, old_commit.get()) < 0) { + JAMI_ERR("Could not look up initial tree"); + return {nullptr, git_diff_free}; + } + GitTree treeOld = {tOld, git_tree_free}; + + // Calc diff + if (git_diff_tree_to_tree(&diff_ptr, repository_.get(), treeOld.get(), treeNew.get(), {}) < 0) { + JAMI_ERR("Could not get diff between %s and %s", idOld.c_str(), idNew.c_str()); + return {nullptr, git_diff_free}; + } + return {diff_ptr, git_diff_free}; +} + +std::string +ConversationRepository::Impl::diffStats(const GitDiff& diff) const +{ + git_diff_stats* stats_ptr = nullptr; + if (git_diff_get_stats(&stats_ptr, diff.get()) < 0) { + JAMI_ERR("Could not get diff stats"); + return {}; + } + GitDiffStats stats = {stats_ptr, git_diff_stats_free}; + + git_diff_stats_format_t format = GIT_DIFF_STATS_FULL; + git_buf statsBuf = {}; + if (git_diff_stats_to_buf(&statsBuf, stats.get(), format, 80) < 0) { + JAMI_ERR("Could not format diff stats"); + return {}; + } + + return std::string(statsBuf.ptr, statsBuf.ptr + statsBuf.size); +} + ////////////////////////////////// std::unique_ptr @@ -923,4 +1129,28 @@ ConversationRepository::merge(const std::string& merge_id) return result; } +std::string +ConversationRepository::diffStats(const std::string& newId, const std::string& oldId) const +{ + auto diff = pimpl_->diff(newId, oldId); + if (!diff) + return {}; + return pimpl_->diffStats(diff); +} + +std::vector +ConversationRepository::changedFiles(const std::string_view& diffStats) +{ + std::string line; + std::vector changedFiles; + for (auto line : split_string(diffStats, '\n')) { + std::regex re(" +\\| +[0-9]+.*"); + std::svmatch match; + if (!std::regex_search(line, match, re) && match.size() == 0) + continue; + changedFiles.emplace_back(std::regex_replace(std::string {line}, re, "").substr(1)); + } + return changedFiles; +} + } // namespace jami diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h index b2d85c918..afcd6a9b2 100644 --- a/src/jamidht/conversationrepository.h +++ b/src/jamidht/conversationrepository.h @@ -143,6 +143,22 @@ public: */ bool merge(const std::string& merge_id); + /** + * Get current diff stats between two commits + * @param oldId Old commit + * @param newId Recent commit (empty value will compare to the empty repository) + * @note "HEAD" is also accepted as parameter for newId + * @return diff stats + */ + std::string diffStats(const std::string& newId, const std::string& oldId = "") const; + + /** + * Get changed files from a git diff + * @param diffStats The stats to analyze + * @return get the changed files from a git diff + */ + static std::vector changedFiles(const std::string_view& diffStats); + private: ConversationRepository() = delete; class Impl; diff --git a/test/unitTest/conversationRepository/conversationRepository.cpp b/test/unitTest/conversationRepository/conversationRepository.cpp index 4a45b5d4f..4da580e76 100644 --- a/test/unitTest/conversationRepository/conversationRepository.cpp +++ b/test/unitTest/conversationRepository/conversationRepository.cpp @@ -47,6 +47,13 @@ namespace test { class ConversationRepositoryTest : public CppUnit::TestFixture { public: + ConversationRepositoryTest() + { + // Init daemon + DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG)); + if (not Manager::instance().initialized) + CPPUNIT_ASSERT(DRing::start("dring-sample.yml")); + } ~ConversationRepositoryTest() { DRing::fini(); } static std::string name() { return "ConversationRepository"; } void setUp(); @@ -63,6 +70,7 @@ private: void testFetch(); void testMerge(); void testFFMerge(); + void testDiff(); std::string addCommit(git_repository* repo, const std::shared_ptr account, @@ -80,6 +88,7 @@ private: CPPUNIT_TEST(testFetch); CPPUNIT_TEST(testMerge); CPPUNIT_TEST(testFFMerge); + CPPUNIT_TEST(testDiff); CPPUNIT_TEST_SUITE_END(); }; @@ -89,11 +98,6 @@ CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(ConversationRepositoryTest, void ConversationRepositoryTest::setUp() { - // Init daemon - DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG)); - if (not Manager::instance().initialized) - CPPUNIT_ASSERT(DRing::start("dring-sample.yml")); - std::map details = DRing::getAccountTemplate("RING"); details[ConfProperties::TYPE] = "RING"; details[ConfProperties::DISPLAYNAME] = "ALICE"; @@ -673,6 +677,27 @@ ConversationRepositoryTest::testFFMerge() CPPUNIT_ASSERT(repository->log().size() == 3 /* Initial, commit 1, 2 */); } +void +ConversationRepositoryTest::testDiff() +{ + auto aliceAccount = Manager::instance().getAccount(aliceId); + auto aliceDeviceId = aliceAccount->currentDeviceId(); + auto uri = aliceAccount->getUsername(); + auto repository = ConversationRepository::createConversation(aliceAccount->weak()); + + auto id1 = repository->sendMessage("Commit 1"); + auto id2 = repository->sendMessage("Commit 2"); + auto id3 = repository->sendMessage("Commit 3"); + + auto diff = repository->diffStats(id2, id1); + CPPUNIT_ASSERT(ConversationRepository::changedFiles(diff).empty()); + diff = repository->diffStats(id1); + auto changedFiles = ConversationRepository::changedFiles(diff); + CPPUNIT_ASSERT(!changedFiles.empty()); + CPPUNIT_ASSERT(changedFiles[0] == "admins/" + uri + ".crt"); + CPPUNIT_ASSERT(changedFiles[1] == "devices/" + aliceDeviceId + ".crt"); +} + } // namespace test } // namespace jami