diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp index d299e7c9fb7dcacdc7399ed037fb7daa898b2785..089c15107e9149d600948e33543d2fe9bc8d466a 100644 --- a/src/jamidht/conversationrepository.cpp +++ b/src/jamidht/conversationrepository.cpp @@ -157,17 +157,17 @@ add_initial_files(GitRepository& repo, const std::shared_ptr<JamiAccount>& accou } // git add -A - git_index* index; + git_index* index_ptr = nullptr; git_strarray array = {0}; - if (git_repository_index(&index, repo.get()) < 0) { + if (git_repository_index(&index_ptr, repo.get()) < 0) { JAMI_ERR("Could not open repository index"); return false; } - git_index_add_all(index, &array, 0, nullptr, nullptr); - git_index_write(index); - git_index_free(index); + GitIndex index {index_ptr, git_index_free}; + git_index_add_all(index.get(), &array, 0, nullptr, nullptr); + git_index_write(index.get()); JAMI_INFO("Initial files added in %s", repoPath.c_str()); return true; @@ -187,41 +187,47 @@ initial_commit(GitRepository& repo, const std::shared_ptr<JamiAccount>& account) if (name.empty()) name = deviceId; - git_signature* sig; - git_index* index; + git_signature* sig_ptr = nullptr; + git_index* index_ptr = nullptr; git_oid tree_id, commit_id; - git_tree* tree; + git_tree* tree_ptr = nullptr; git_buf to_sign = {}; // Sign commit's buffer - if (git_signature_new(&sig, name.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) { + 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}; - if (git_repository_index(&index, repo.get()) < 0) { + if (git_repository_index(&index_ptr, repo.get()) < 0) { JAMI_ERR("Could not open repository index"); return {}; } + GitIndex index {index_ptr, git_index_free}; - if (git_index_write_tree(&tree_id, index) < 0) { + if (git_index_write_tree(&tree_id, index.get()) < 0) { JAMI_ERR("Unable to write initial tree from index"); return {}; } - git_index_free(index); - - if (git_tree_lookup(&tree, repo.get(), &tree_id) < 0) { + if (git_tree_lookup(&tree_ptr, repo.get(), &tree_id) < 0) { JAMI_ERR("Could not look up initial tree"); - git_tree_free(tree); return {}; } + GitTree tree = {tree_ptr, git_tree_free}; - if (git_commit_create_buffer( - &to_sign, repo.get(), sig, sig, nullptr, "Initial commit", tree, 0, nullptr) + if (git_commit_create_buffer(&to_sign, + repo.get(), + sig.get(), + sig.get(), + nullptr, + "Initial commit", + tree.get(), + 0, + nullptr) < 0) { JAMI_ERR("Could not create initial buffer"); - git_tree_free(tree); return {}; } @@ -237,22 +243,18 @@ initial_commit(GitRepository& repo, const std::shared_ptr<JamiAccount>& account) "signature") < 0) { JAMI_ERR("Could not sign initial commit"); - git_tree_free(tree); return {}; } // Move commit to master branch - git_commit* commit; + git_commit* commit = nullptr; if (git_commit_lookup(&commit, repo.get(), &commit_id) == 0) { - git_reference* ref; + git_reference* ref = nullptr; git_branch_create(&ref, repo.get(), "master", commit, true); - git_reference_free(ref); git_commit_free(commit); + git_reference_free(ref); } - git_tree_free(tree); - git_signature_free(sig); - auto commit_str = git_oid_tostr_s(&commit_id); if (commit_str) return commit_str; @@ -332,8 +334,8 @@ ConversationRepository::cloneConversation(const std::weak_ptr<JamiAccount>& acco JAMI_ERR("Error when retrieving remote conversation: %s", err->message); return nullptr; } - JAMI_INFO("New conversation cloned in %s", path.c_str()); git_repository_free(rep); + JAMI_INFO("New conversation cloned in %s", path.c_str()); return std::make_unique<ConversationRepository>(account, conversationId); } @@ -356,12 +358,11 @@ bool ConversationRepository::fetch(const std::string& remoteDeviceId) { // Fetch distant repository - git_remote* remote = nullptr; + git_remote* remote_ptr = nullptr; git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT; // Assert that repository exists std::string channelName = "git://" + remoteDeviceId + '/' + pimpl_->id_; - git_remote* remote_ptr = nullptr; auto res = git_remote_lookup(&remote_ptr, pimpl_->repository_.get(), remoteDeviceId.c_str()); if (res != 0) { if (res != GIT_ENOTFOUND) { @@ -378,14 +379,17 @@ ConversationRepository::fetch(const std::string& remoteDeviceId) return false; } } + GitRemote remote {remote_ptr, git_remote_free}; - if (git_remote_fetch(remote, nullptr, &fetch_opts, "fetch") < 0) { - JAMI_ERR("Could not fetch remote repository for conversation %s", pimpl_->id_.c_str()); - git_remote_free(remote); + if (git_remote_fetch(remote.get(), nullptr, &fetch_opts, "fetch") < 0) { + const git_error* err = giterr_last(); + if (err) + JAMI_ERR("Could not fetch remote repository for conversation %s: %s", + pimpl_->id_.c_str(), + err->message); return false; } - git_remote_free(remote); return true; } @@ -443,69 +447,62 @@ ConversationRepository::sendMessage(const std::string& msg) file.close(); // git add - git_index* index; - - if (git_repository_index(&index, pimpl_->repository_.get()) < 0) { + git_index* index_ptr = nullptr; + if (git_repository_index(&index_ptr, pimpl_->repository_.get()) < 0) { JAMI_ERR("Could not open repository index"); return {}; } + GitIndex index {index_ptr, git_index_free}; - git_index_add_bypath(index, devicePath.c_str()); - git_index_write(index); - git_index_free(index); + git_index_add_bypath(index.get(), devicePath.c_str()); + git_index_write(index.get()); } - git_signature* sig; + git_signature* sig_ptr = nullptr; // Sign commit's buffer - if (git_signature_new(&sig, name.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) { + 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, pimpl_->repository_.get(), "HEAD") < 0) { JAMI_ERR("Cannot get reference for HEAD"); - git_signature_free(sig); return {}; } - git_commit* head_commit; - if (git_commit_lookup(&head_commit, pimpl_->repository_.get(), &commit_id) < 0) { + git_commit* head_ptr = nullptr; + if (git_commit_lookup(&head_ptr, pimpl_->repository_.get(), &commit_id) < 0) { JAMI_ERR("Could not look up HEAD commit"); - git_signature_free(sig); return {}; } + GitCommit head_commit {head_ptr, git_commit_free}; - git_tree* tree; - if (git_commit_tree(&tree, head_commit) < 0) { + git_tree* tree_ptr = nullptr; + if (git_commit_tree(&tree_ptr, head_commit.get()) < 0) { JAMI_ERR("Could not look up initial tree"); - git_signature_free(sig); return {}; } + GitTree tree {tree_ptr, git_tree_free}; git_buf to_sign = {}; - const git_commit* head_ref[1] = {head_commit}; + const git_commit* head_ref[1] = {head_commit.get()}; if (git_commit_create_buffer(&to_sign, pimpl_->repository_.get(), - sig, - sig, + sig.get(), + sig.get(), nullptr, msg.c_str(), - tree, + tree.get(), 1, &head_ref[0]) < 0) { JAMI_ERR("Could not create commit buffer"); - git_commit_free(head_commit); - git_tree_free(tree); - git_signature_free(sig); return {}; } - git_tree_free(tree); - git_commit_free(head_commit); - // git commit -S auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size); auto signed_buf = account->identity().first->sign(to_sign_vec); @@ -520,11 +517,9 @@ ConversationRepository::sendMessage(const std::string& msg) return {}; } - git_signature_free(sig); - // Move commit to master branch - git_reference* ref; - if (git_reference_create(&ref, + git_reference* ref_ptr = nullptr; + if (git_reference_create(&ref_ptr, pimpl_->repository_.get(), "refs/heads/master", &commit_id, @@ -533,7 +528,7 @@ ConversationRepository::sendMessage(const std::string& msg) < 0) { JAMI_WARN("Could not move commit to master"); } - git_reference_free(ref); + git_reference_free(ref_ptr); auto commit_str = git_oid_tostr_s(&commit_id); if (commit_str) { @@ -542,4 +537,79 @@ ConversationRepository::sendMessage(const std::string& msg) return commit_str ? commit_str : ""; } -} // namespace jami \ No newline at end of file +std::vector<ConversationCommit> +ConversationRepository::log(const std::string& last, unsigned n) +{ + std::vector<ConversationCommit> commits {}; + + git_oid oid; + if (last.empty()) { + if (git_reference_name_to_id(&oid, pimpl_->repository_.get(), "HEAD") < 0) { + JAMI_ERR("Cannot get reference for HEAD"); + return commits; + } + } else { + if (git_oid_fromstr(&oid, last.c_str()) < 0) { + JAMI_ERR("Cannot get reference for commit %s", last.c_str()); + return commits; + } + } + + git_revwalk* walker_ptr = nullptr; + if (git_revwalk_new(&walker_ptr, pimpl_->repository_.get()) < 0 + || git_revwalk_push(walker_ptr, &oid) < 0) { + JAMI_ERR("Couldn't init revwalker for conversation %s", pimpl_->id_.c_str()); + return commits; + } + GitRevWalker walker {walker_ptr, git_revwalk_free}; + git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL); + + auto x = git_oid_tostr_s(&oid); + for (auto idx = 0; !git_revwalk_next(&oid, walker.get()); ++idx) { + if (n != 0 && idx == n) { + break; + } + git_commit* commit_ptr = nullptr; + std::string id = git_oid_tostr_s(&oid); + if (git_commit_lookup(&commit_ptr, pimpl_->repository_.get(), &oid) < 0) { + JAMI_WARN("Failed to look up commit %s", id.c_str()); + break; + } + GitCommit commit {commit_ptr, git_commit_free}; + + const git_signature* sig = git_commit_author(commit.get()); + GitAuthor author; + author.name = sig->name; + author.email = sig->email; + std::string parent {}; + const git_oid* pid = git_commit_parent_id(commit.get(), 0); + if (pid) { + parent = git_oid_tostr_s(pid); + } + + auto cc = commits.emplace(commits.end(), ConversationCommit {}); + cc->id = std::move(id); + cc->commit_msg = git_commit_message(commit.get()); + cc->author = std::move(author); + cc->parent = std::move(parent); + git_buf signature = {}, signed_data = {}; + if (git_commit_extract_signature(&signature, + &signed_data, + pimpl_->repository_.get(), + &oid, + "signature") + < 0) { + JAMI_WARN("Could not extract signature for commit %s", id.c_str()); + } else { + cc->signature = base64::decode( + std::string(signature.ptr, signature.ptr + signature.size)); + cc->signed_content = std::vector<uint8_t>(signed_data.ptr, + signed_data.ptr + signed_data.size); + } + cc->timestamp = git_commit_time(commit.get()); + } + + return commits; +} + +} // namespace jami diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h index e34c22f242fa443e2cd8f1daf4cbcb26fc95c880..c1bf213e6f3efd8e3d6c078fa5f92ccc7c6609f9 100644 --- a/src/jamidht/conversationrepository.h +++ b/src/jamidht/conversationrepository.h @@ -20,6 +20,7 @@ #include <git2.h> #include <memory> #include <string> +#include <vector> #include "def.h" @@ -46,6 +47,23 @@ namespace jami { class JamiAccount; class ChannelSocket; +struct GitAuthor +{ + std::string name {}; + std::string email {}; +}; + +struct ConversationCommit +{ + std::string id {}; + std::string parent {}; + GitAuthor author {}; + std::vector<uint8_t> signed_content {}; + std::vector<uint8_t> signature {}; + std::string commit_msg {}; + int64_t timestamp {0}; +}; + /** * This class gives access to the git repository that represents the conversation */ @@ -110,6 +128,14 @@ public: */ std::string sendMessage(const std::string& msg); + /** + * Get commits from [last-n, last] + * @param last last commit (default empty) + * @param n Max commits number to get (default: 0) + * @return a list of commits + */ + std::vector<ConversationCommit> log(const std::string& last = "", unsigned n = 0); + private: ConversationRepository() = delete; class Impl; diff --git a/src/jamidht/gitserver.cpp b/src/jamidht/gitserver.cpp index a68f219c798d456066d0f8866a0ae0c6b017d41c..10314f0778621e40bad175831f9feefb948e8db0 100644 --- a/src/jamidht/gitserver.cpp +++ b/src/jamidht/gitserver.cpp @@ -36,7 +36,8 @@ constexpr auto NAK_PKT = "0008NAK\n"sv; constexpr auto DONE_PKT = "0009done\n"sv; constexpr auto WANT_CMD = "want"sv; constexpr auto HAVE_CMD = "have"sv; -constexpr auto SERVER_CAPABILITIES = " HEAD\0side-band side-band-64k shallow no-progress include-tag"sv; +constexpr auto SERVER_CAPABILITIES + = " HEAD\0side-band side-band-64k shallow no-progress include-tag"sv; namespace jami { @@ -106,7 +107,7 @@ GitServer::Impl::parseOrder(const uint8_t* buf, std::size_t len) // The first four bytes define the length of the packet and 0000 is a FLUSH pkt unsigned int pkt_len; - std::from_chars(pkt.data(), pkt.data()+4, pkt_len, 16); + std::from_chars(pkt.data(), pkt.data() + 4, pkt_len, 16); if (pkt_len != pkt.size()) { // Store next packet part if (pkt_len == 0) { @@ -234,8 +235,7 @@ GitServer::Impl::sendReferenceCapabilities(bool sendVersion) // Send references std::stringstream capabilities; - capabilities << currentHead - << SERVER_CAPABILITIES; + capabilities << currentHead << SERVER_CAPABILITIES; std::string capStr = capabilities.str(); packet.str(""); @@ -401,7 +401,7 @@ GitServer::Impl::sendPackData() // cf https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166 // In 'side-band-64k' mode it will send up to 65519 data bytes plus 1 control code, for a // total of up to 65520 bytes in a pkt-line. - std::size_t pkt_size = std::min(static_cast<std::size_t>(65519), len - sent); + std::size_t pkt_size = std::min(static_cast<std::size_t>(65515), len - sent); std::stringstream toSend; toSend << std::setw(4) << std::setfill('0') << std::hex << ((pkt_size + 5) & 0x0FFFF); toSend << "\x1" << std::string_view(data.ptr + sent, pkt_size); diff --git a/test/unitTest/conversationRepository/conversationRepository.cpp b/test/unitTest/conversationRepository/conversationRepository.cpp index 898b4062d7f8644568f0aee6bd1a229ecb6fb918..e456714cf48daa2e06c2270497adfc07370f9dbb 100644 --- a/test/unitTest/conversationRepository/conversationRepository.cpp +++ b/test/unitTest/conversationRepository/conversationRepository.cpp @@ -59,12 +59,14 @@ private: void testCreateRepository(); void testCloneViaChannelSocket(); void testAddSomeMessages(); + void testLogMessages(); void testFetch(); CPPUNIT_TEST_SUITE(ConversationRepositoryTest); CPPUNIT_TEST(testCreateRepository); CPPUNIT_TEST(testCloneViaChannelSocket); CPPUNIT_TEST(testAddSomeMessages); + CPPUNIT_TEST(testLogMessages); CPPUNIT_TEST(testFetch); CPPUNIT_TEST_SUITE_END(); }; @@ -303,6 +305,14 @@ ConversationRepositoryTest::testCloneViaChannelSocket() std::istreambuf_iterator<char>()); CPPUNIT_ASSERT(deviceCrtStr == deviceCert); + + // Check cloned messages + auto messages = cloned->log(); + CPPUNIT_ASSERT(messages.size() == 1); + CPPUNIT_ASSERT(messages[0].id == repository->id()); + CPPUNIT_ASSERT( + aliceAccount->identity().second->getPublicKey().checkSignature(messages[0].signed_content, + messages[0].signature)); } void @@ -311,10 +321,59 @@ ConversationRepositoryTest::testAddSomeMessages() auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); auto repository = ConversationRepository::createConversation(aliceAccount->weak()); - repository->sendMessage("Commit 1"); - repository->sendMessage("Commit 2"); - repository->sendMessage("Commit 3"); - // TODO check commits => needs something to get messages + auto id1 = repository->sendMessage("Commit 1"); + auto id2 = repository->sendMessage("Commit 2"); + auto id3 = repository->sendMessage("Commit 3"); + + auto messages = repository->log(); + CPPUNIT_ASSERT(messages.size() == 4 /* 3 + initial */); + CPPUNIT_ASSERT(messages[0].id == id3); + CPPUNIT_ASSERT(messages[0].parent == id2); + CPPUNIT_ASSERT(messages[0].commit_msg == "Commit 3"); + CPPUNIT_ASSERT(messages[0].author.name == messages[3].author.name); + CPPUNIT_ASSERT(messages[0].author.email == messages[3].author.email); + CPPUNIT_ASSERT(messages[1].id == id2); + CPPUNIT_ASSERT(messages[1].parent == id1); + CPPUNIT_ASSERT(messages[1].commit_msg == "Commit 2"); + CPPUNIT_ASSERT(messages[1].author.name == messages[3].author.name); + CPPUNIT_ASSERT(messages[1].author.email == messages[3].author.email); + CPPUNIT_ASSERT(messages[2].id == id1); + CPPUNIT_ASSERT(messages[2].commit_msg == "Commit 1"); + CPPUNIT_ASSERT(messages[2].author.name == messages[3].author.name); + CPPUNIT_ASSERT(messages[2].author.email == messages[3].author.email); + CPPUNIT_ASSERT(messages[2].parent == repository->id()); + // Check sig + CPPUNIT_ASSERT( + aliceAccount->identity().second->getPublicKey().checkSignature(messages[0].signed_content, + messages[0].signature)); + CPPUNIT_ASSERT( + aliceAccount->identity().second->getPublicKey().checkSignature(messages[1].signed_content, + messages[1].signature)); + CPPUNIT_ASSERT( + aliceAccount->identity().second->getPublicKey().checkSignature(messages[2].signed_content, + messages[2].signature)); +} + +void +ConversationRepositoryTest::testLogMessages() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + 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 messages = repository->log(repository->id(), 1); + CPPUNIT_ASSERT(messages.size() == 1); + CPPUNIT_ASSERT(messages[0].id == repository->id()); + messages = repository->log(id2, 2); + CPPUNIT_ASSERT(messages.size() == 2); + CPPUNIT_ASSERT(messages[0].id == id2); + CPPUNIT_ASSERT(messages[1].id == id1); + messages = repository->log(repository->id(), 3); + CPPUNIT_ASSERT(messages.size() == 1); + CPPUNIT_ASSERT(messages[0].id == repository->id()); } void @@ -350,7 +409,7 @@ ConversationRepositoryTest::testFetch() }); aliceAccount->connectionManager().onChannelRequest( - [&](const DeviceId&, const std::string& name) { return true; }); + [&](const DeviceId&, const std::string&) { return true; }); bobAccount->connectionManager().onConnectionReady( [&](const DeviceId&, const std::string& name, std::shared_ptr<ChannelSocket> socket) { @@ -381,7 +440,7 @@ ConversationRepositoryTest::testFetch() std::thread sendT = std::thread([&]() { gs.run(); }); // Clone repository - repository->sendMessage("Commit 1"); + auto id1 = repository->sendMessage("Commit 1"); auto cloned = ConversationRepository::cloneConversation(bobAccount->weak(), aliceDeviceId, repository->id()); @@ -390,7 +449,7 @@ ConversationRepositoryTest::testFetch() bobAccount->removeGitSocket(aliceDeviceId, repository->id()); // Add some new messages to fetch - repository->sendMessage("Commit 2"); + auto id2 = repository->sendMessage("Commit 2"); auto id3 = repository->sendMessage("Commit 3"); // Open a new channel to simulate the fact that we are later @@ -418,7 +477,33 @@ ConversationRepositoryTest::testFetch() bobAccount->removeGitSocket(aliceDeviceId, repository->id()); sendT2.join(); - // TODO check commits => needs something to get messages + auto messages = cloned->log(id3); + CPPUNIT_ASSERT(messages.size() == 4 /* 3 + initial */); + CPPUNIT_ASSERT(messages[0].id == id3); + CPPUNIT_ASSERT(messages[0].parent == id2); + CPPUNIT_ASSERT(messages[0].commit_msg == "Commit 3"); + CPPUNIT_ASSERT(messages[0].author.name == messages[3].author.name); + CPPUNIT_ASSERT(messages[0].author.email == messages[3].author.email); + CPPUNIT_ASSERT(messages[1].id == id2); + CPPUNIT_ASSERT(messages[1].parent == id1); + CPPUNIT_ASSERT(messages[1].commit_msg == "Commit 2"); + CPPUNIT_ASSERT(messages[1].author.name == messages[3].author.name); + CPPUNIT_ASSERT(messages[1].author.email == messages[3].author.email); + CPPUNIT_ASSERT(messages[2].id == id1); + CPPUNIT_ASSERT(messages[2].commit_msg == "Commit 1"); + CPPUNIT_ASSERT(messages[2].author.name == messages[3].author.name); + CPPUNIT_ASSERT(messages[2].author.email == messages[3].author.email); + CPPUNIT_ASSERT(messages[2].parent == repository->id()); + // Check sig + CPPUNIT_ASSERT( + aliceAccount->identity().second->getPublicKey().checkSignature(messages[0].signed_content, + messages[0].signature)); + CPPUNIT_ASSERT( + aliceAccount->identity().second->getPublicKey().checkSignature(messages[1].signed_content, + messages[1].signature)); + CPPUNIT_ASSERT( + aliceAccount->identity().second->getPublicKey().checkSignature(messages[2].signed_content, + messages[2].signature)); } } // namespace test