Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add merge subcommand (fastforward)
  • Loading branch information
SandrineP committed Oct 28, 2025
commit 2ed957e8dee57c585863e1e7ca5b6c30077feeeb
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ set(GIT2CPP_SRC
${GIT2CPP_SOURCE_DIR}/subcommand/init_subcommand.hpp
${GIT2CPP_SOURCE_DIR}/subcommand/log_subcommand.cpp
${GIT2CPP_SOURCE_DIR}/subcommand/log_subcommand.hpp
${GIT2CPP_SOURCE_DIR}/subcommand/merge_subcommand.cpp
${GIT2CPP_SOURCE_DIR}/subcommand/merge_subcommand.hpp
${GIT2CPP_SOURCE_DIR}/subcommand/reset_subcommand.cpp
${GIT2CPP_SOURCE_DIR}/subcommand/reset_subcommand.hpp
${GIT2CPP_SOURCE_DIR}/subcommand/status_subcommand.cpp
Expand Down
3 changes: 3 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <CLI/CLI.hpp>
#include <cmath>
#include <git2.h> // For version number only
#include <iostream>

Expand All @@ -11,6 +12,7 @@
#include "subcommand/commit_subcommand.hpp"
#include "subcommand/init_subcommand.hpp"
#include "subcommand/log_subcommand.hpp"
#include "subcommand/merge_subcommand.hpp"
#include "subcommand/reset_subcommand.hpp"
#include "subcommand/status_subcommand.hpp"

Expand All @@ -35,6 +37,7 @@ int main(int argc, char** argv)
commit_subcommand commit(lg2_obj, app);
reset_subcommand reset(lg2_obj, app);
log_subcommand log(lg2_obj, app);
merge_subcommand merge(lg2_obj, app);

app.require_subcommand(/* min */ 0, /* max */ 1);

Expand Down
2 changes: 1 addition & 1 deletion src/subcommand/checkout_subcommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ void checkout_subcommand::run()
}
else
{
auto optional_commit = resolve_local_ref(repo, m_branch_name);
auto optional_commit = repo.resolve_local_ref(m_branch_name);
if (!optional_commit)
{
// TODO: handle remote refs
Expand Down
2 changes: 1 addition & 1 deletion src/subcommand/commit_subcommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ void commit_subcommand::run()
}
}

repo.create_commit(author_committer_signatures, m_commit_message);
repo.create_commit(author_committer_signatures, m_commit_message, std::nullopt);
}
182 changes: 182 additions & 0 deletions src/subcommand/merge_subcommand.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#include <cassert>
#include <git2/types.h>

#include "merge_subcommand.hpp"
#include "../wrapper/repository_wrapper.hpp"


merge_subcommand::merge_subcommand(const libgit2_object&, CLI::App& app)
{
auto *sub = app.add_subcommand("merge", "Join two or more development histories together");

sub->add_option("<branch>", m_branches_to_merge, "Branch(es) to merge");

sub->callback([this]() { this->run(); });
}

annotated_commit_list_wrapper resolve_heads(const repository_wrapper& repo, std::vector<std::string> m_branches_to_merge)
Comment thread
ianthomas23 marked this conversation as resolved.
Outdated
{
std::vector<annotated_commit_wrapper> commits_to_merge;
commits_to_merge.reserve(m_branches_to_merge.size());

for (const auto branch_name:m_branches_to_merge)
{
std::optional<annotated_commit_wrapper> commit = repo.resolve_local_ref(branch_name);
if (commit.has_value())
{
commits_to_merge.push_back(std::move(commit).value());
Comment thread
ianthomas23 marked this conversation as resolved.
}
}
return annotated_commit_list_wrapper(std::move(commits_to_merge));
}

void perform_fastforward(repository_wrapper& repo, const git_oid* target_oid, int is_unborn)
Comment thread
JohanMabille marked this conversation as resolved.
Outdated
{
const git_checkout_options ff_checkout_options = GIT_CHECKOUT_OPTIONS_INIT;

auto lambda_get_target_ref = [] (auto repo, auto is_unborn)
{
if (!is_unborn)
{
return repo->head();
}
else
{
return repo->find_reference("HEAD");
}
};
auto target_ref = lambda_get_target_ref(&repo, is_unborn);

auto target = repo.find_object(target_oid, GIT_OBJECT_COMMIT);
Comment thread
JohanMabille marked this conversation as resolved.
Outdated

repo.checkout_tree(target, &ff_checkout_options);

auto new_target_ref = target_ref.new_ref(target_oid);
Comment thread
ianthomas23 marked this conversation as resolved.
Outdated
}

// static void create_merge_commit(repository_wrapper repo, index_wrapper index, std::vector<std::string> m_branches_to_merge,
// std::vector<annotated_commit_wrapper> commits_to_merge)
// {
// auto head_ref = repo.head();
// auto merge_ref = repo.find_reference_dwim(m_branches_to_merge.front());
// // if (ref)
// // {
// // auto merge_ref = std::move(ref).value();
// // }
// auto merge_commit = repo.resolve_local_ref(m_branches_to_merge.front()).value();

// size_t annotated_count = commits_to_merge.size();
// std::vector<commit_wrapper> parents_list;
// parents_list.reserve(annotated_count + 1);
// parents_list.push_back(std::move(head_ref.peel<commit_wrapper>()));
// for (size_t i=0; i<annotated_count; ++i)
// {
// parents_list.push_back(repo.find_commit(commits_to_merge[i].oid()));
// }
// auto parents = commit_list_wrapper(std::move(parents_list));

// auto author_committer_sign = signature_wrapper::get_default_signature_from_env(repo);
// std::string author_name;
// author_name = author_committer_sign.first.name();
// std::string author_email;
// author_email = author_committer_sign.first.email();
// auto author_committer_sign_now = signature_wrapper::signature_now(author_name, author_email, author_name, author_email);

// std::string msg_target = NULL;
// if (merge_ref)
// {
// msg_target = merge_ref->short_name();
// }
// else
// {
// msg_target = git_oid_tostr_s(&(merge_commit.oid()));
// }

// std::string msg;
// msg = "Merge ";
// if (merge_ref)
// {
// msg.append("branch ");
// }
// else
// {
// msg.append("commit ");
// }
// msg.append(msg_target);
// std::cout << msg << std::endl;

// repo.create_commit(author_committer_sign_now, msg, std::optional<commit_list_wrapper>(std::move(parents)));
// }

void merge_subcommand::run()
{
auto directory = get_current_git_path();
auto bare = false;
auto repo = repository_wrapper::open(directory);

auto state = repo.state();
if (state != GIT_REPOSITORY_STATE_NONE)
{
std::cout << "repository is in unexpected state " << state <<std::endl;
}

git_merge_analysis_t analysis;
git_merge_preference_t preference;
annotated_commit_list_wrapper commits_to_merge = resolve_heads(repo, m_branches_to_merge);
size_t num_commits_to_merge = commits_to_merge.size();
git_annotated_commit** c_commits_to_merge = commits_to_merge;
auto commits_to_merge_const = const_cast<const git_annotated_commit**>(c_commits_to_merge);

throw_if_error(git_merge_analysis(&analysis, &preference, repo, commits_to_merge_const, num_commits_to_merge));

if (analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE)
{
std::cout << "Already up-to-date" << std::endl;
}
else if (analysis & GIT_MERGE_ANALYSIS_UNBORN ||
(analysis & GIT_MERGE_ANALYSIS_FASTFORWARD &&
!(preference & GIT_MERGE_PREFERENCE_NO_FASTFORWARD)))
{
const git_oid* target_oid;
if (analysis & GIT_MERGE_ANALYSIS_UNBORN)
{
std::cout << "Unborn" << std::endl;
}
else
{
std::cout << "Fast-forward" << std::endl;
}
const annotated_commit_wrapper& commit = commits_to_merge.front();
target_oid = &commit.oid();
assert(num_commits_to_merge == 1);
Comment thread
ianthomas23 marked this conversation as resolved.
perform_fastforward(repo, target_oid, (analysis & GIT_MERGE_ANALYSIS_UNBORN));
}
else if (analysis & GIT_MERGE_ANALYSIS_NORMAL)
{
git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT;
git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;

merge_opts.flags = 0;
merge_opts.file_flags = GIT_MERGE_FILE_STYLE_DIFF3;

checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE|GIT_CHECKOUT_ALLOW_CONFLICTS;

if (preference & GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY)
{
std::cout << "Fast-forward is preferred, but only a merge is possible\n" << std::endl;
// how to break ?
}

// git_merge(repo,
// (const git_annotated_commit **)opts.annotated, opts.annotated_count,
// &merge_opts, &checkout_opts);
}

// if (git_index_has_conflicts(index)) {
// /* Handle conflicts */
// output_conflicts(index);
// } else if (!opts.no_commit) {
// create_merge_commit(repo, index, &opts);
// printf("Merge made\n");
// }
}
16 changes: 16 additions & 0 deletions src/subcommand/merge_subcommand.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

#include <CLI/CLI.hpp>

#include "../utils/common.hpp"

class merge_subcommand
{
public:

explicit merge_subcommand(const libgit2_object&, CLI::App& app);
void run();

private:
std::vector<std::string> m_branches_to_merge;
};
36 changes: 36 additions & 0 deletions src/wrapper/annotated_commit_wrapper.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "../wrapper/annotated_commit_wrapper.hpp"
#include <iostream>

annotated_commit_wrapper::annotated_commit_wrapper(git_annotated_commit* commit)
: base_type(commit)
Expand All @@ -21,3 +22,38 @@ std::string_view annotated_commit_wrapper::reference_name() const
const char* res = git_annotated_commit_ref(*this);
return res ? res : std::string_view{};
}

annotated_commit_list_wrapper::annotated_commit_list_wrapper(std::vector<annotated_commit_wrapper> annotated_commit_list)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have multiple whatever_list_wrapper classes there is probably a way we can move the generic vector functionality into a single templated class that we inherit from. But that can be future work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new list_wrapper is really nice, thanks!

: m_annotated_commit_list(std::move(annotated_commit_list))
{
p_resource = new git_annotated_commit*[m_annotated_commit_list.size()];
for (size_t i=0; i<m_annotated_commit_list.size(); ++i)
{
p_resource[i] = m_annotated_commit_list[i];
}
}

annotated_commit_list_wrapper::~annotated_commit_list_wrapper()
{
delete[] p_resource;
p_resource = nullptr;
}

size_t annotated_commit_list_wrapper::size() const
{
return m_annotated_commit_list.size();
}

annotated_commit_wrapper annotated_commit_list_wrapper::front()
{
return annotated_commit_wrapper(std::move(m_annotated_commit_list.front()));
}

void annotated_commit_list_wrapper::print() const
{
std::cout << "aclw - p_resource = " << p_resource <<std::endl;
for (size_t i=0; i<m_annotated_commit_list.size(); ++i)
{
std::cout << "aclw - p_resource[i] = :" << p_resource[i] << std::endl;
}
}
23 changes: 23 additions & 0 deletions src/wrapper/annotated_commit_wrapper.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <string_view>
#include <vector>

#include <git2.h>

Expand All @@ -27,3 +28,25 @@ class annotated_commit_wrapper : public wrapper_base<git_annotated_commit>
friend class repository_wrapper;
};

class annotated_commit_list_wrapper : public wrapper_base<git_annotated_commit*>
{
public:

using base_type = wrapper_base<git_annotated_commit*>;

explicit annotated_commit_list_wrapper(std::vector<annotated_commit_wrapper> annotated_commit_list);

~annotated_commit_list_wrapper();

annotated_commit_list_wrapper(annotated_commit_list_wrapper&&) noexcept = default;
annotated_commit_list_wrapper& operator=(annotated_commit_list_wrapper&&) noexcept = default;

size_t size() const;
annotated_commit_wrapper front();
void print() const;
Comment thread
ianthomas23 marked this conversation as resolved.
Outdated

private:

std::vector<annotated_commit_wrapper> m_annotated_commit_list;

};
20 changes: 20 additions & 0 deletions src/wrapper/commit_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,23 @@ std::string commit_wrapper::commit_oid_tostr() const
char buf[GIT_OID_SHA1_HEXSIZE + 1];
return git_oid_tostr(buf, sizeof(buf), &this->oid());
}

commit_list_wrapper::commit_list_wrapper(std::vector<commit_wrapper> commit_list)
Comment thread
ianthomas23 marked this conversation as resolved.
Outdated
{
git_commit** p_resource = new git_commit*[m_commit_list.size()];
for (size_t i=0; i<m_commit_list.size(); ++i)
{
p_resource[i] = m_commit_list[i];
}
}

commit_list_wrapper::~commit_list_wrapper()
{
delete[] p_resource;
p_resource = nullptr;
}

size_t commit_list_wrapper::size() const
{
return m_commit_list.size();
}
24 changes: 23 additions & 1 deletion src/wrapper/commit_wrapper.hpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#pragma once

#include <git2.h>
#include <vector>
#include <string>

#include "../wrapper/repository_wrapper.hpp"
#include "../wrapper/wrapper_base.hpp"

class commit_wrapper : public wrapper_base<git_commit>
Expand All @@ -28,3 +29,24 @@ class commit_wrapper : public wrapper_base<git_commit>
friend class repository_wrapper;
friend class reference_wrapper;
};

class commit_list_wrapper : public wrapper_base<git_commit*>
{
public:

using base_type = wrapper_base<git_commit*>;

explicit commit_list_wrapper(std::vector<commit_wrapper> commit_list);

~commit_list_wrapper();

commit_list_wrapper(commit_list_wrapper&&) noexcept = default;
commit_list_wrapper& operator=(commit_list_wrapper&&) noexcept = default;

size_t size() const;

private:

std::vector<commit_wrapper> m_commit_list;

};
Loading