swarm: add a method to get a range of commits

This patch introduces ConversationRepository::log(last, n) to get the commits
from [last-n, last], where last (if not specified) is current HEAD.

Change-Id: Ia6b8eb96b8a4a0de3614d4e422c1179f976d2ab0
This commit is contained in:
Sébastien Blin
2020-01-02 16:57:29 -05:00
parent ad5607380f
commit 99700f5524
4 changed files with 257 additions and 76 deletions

View File

@ -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
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

View File

@ -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;

View File

@ -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);

View File

@ -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